|
|
@@ -0,0 +1,411 @@
|
|
|
+# Design — multi-dimensional open arrays
|
|
|
+
|
|
|
+**Status: design only (2026-10-23).** Nothing in this document is
|
|
|
+implemented yet. Baseline: suite **283/283**, fixpoint OK **4,184,457
|
|
|
+bytes**, corpus **512/612 compile-OK**, 0 crashes.
|
|
|
+
|
|
|
+Interactive decisions already taken:
|
|
|
+
|
|
|
+1. **Keep the per-level descriptor model** for array types and indexing.
|
|
|
+2. **Payload semantics** for `SIZE`: an array's payload bytes, not the
|
|
|
+ descriptor header.
|
|
|
+3. **Full corpus scope**: fixed/open 2-D and 3-D arrays, `VAR` formals,
|
|
|
+ subarray views, row → 1-D open array, 2-D → `ARRAY OF BYTE`
|
|
|
+ flattening, `SIZE`/`HIGH`/`ADR`, records and constructors.
|
|
|
+4. Reconcile (1) with the corpus by using **per-level view descriptors
|
|
|
+ over one contiguous backing block**. The existing inline
|
|
|
+ per-row storage is replaced for multi-dimensional arrays only;
|
|
|
+ 1-D arrays/strings keep their current descriptor.
|
|
|
+
|
|
|
+This document records the proposed ABI and implementation plan before
|
|
|
+any code is changed.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. Why the current representation cannot satisfy the corpus
|
|
|
+
|
|
|
+V3 currently represents every array value as a descriptor address:
|
|
|
+
|
|
|
+- 1-D array: `{ count, data... }` (`data` starts at offset 8).
|
|
|
+- N-D fixed array: an outer descriptor whose elements are **pointers to
|
|
|
+ per-row descriptors**; each row descriptor repeats the
|
|
|
+ `{ count, data... }` layout.
|
|
|
+- `SIZE(array)` uses `ObjectSize`, which includes the descriptor header.
|
|
|
+- `ADR(fixed array)` returns the descriptor address, not the first
|
|
|
+ element.
|
|
|
+- `ADR(open array)` currently exposes the descriptor/slot address.
|
|
|
+
|
|
|
+That layout fails the corpus requirements:
|
|
|
+
|
|
|
+| file | requirement |
|
|
|
+| --- | --- |
|
|
|
+| `unbounded` | `VAR a: ARRAY OF ARRAY OF CHAR`; `HIGH(a)`, `HIGH(a[0])`; `a[1,2]`; fixed 2-D actual |
|
|
|
+| `unbounded2` | same, plus `ARRAY BOOLEAN OF ARRAY BOOLEAN OF CHAR` |
|
|
|
+| `unbounded3` | `SIZE(a)=60`, `SIZE(a[0])=12`, `SIZE(a[0][0])=2`, `SIZE(a[0,0])=2` |
|
|
|
+| `unbounded4` | `VAR`/value `ARRAY OF ARRAY OF REAL`; `p[i][j] := q[i,j]` |
|
|
|
+| `unbounded6` | 2-D `ARRAY OF CARDINAL` passed to `ARRAY OF BYTE`; `SIZE` agrees |
|
|
|
+| `unbounded8` | row `a[0]` passed to `ARRAY OF BYTE`; `SIZE(a[0])` is row bytes |
|
|
|
+| `array9` | compile an empty 2-D-open procedure |
|
|
|
+| `adrunbounded3` | `ADR(m)=ADR(m[0])=ADR(m[0][0])` for row/element 0; row passed to 1-D `ARRAY OF REAL` |
|
|
|
+
|
|
|
+`adrunbounded3` and `unbounded3` require a **flat contiguous view** and
|
|
|
+payload `SIZE`. Per-row inline descriptors and descriptor-address
|
|
|
+`ADR` cannot provide that.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 2. Chosen representation
|
|
|
+
|
|
|
+### 2.1 Type model is unchanged
|
|
|
+
|
|
|
+`SymTab` keeps the existing array chain:
|
|
|
+
|
|
|
+```text
|
|
|
+FArray / FOpenArr -> element type -> element type -> ... -> scalar
|
|
|
+```
|
|
|
+
|
|
|
+`ArrayElem`, `ArrayLo`, `ArrayHi`, `ArrayDepth`, `IsOpenArray` and the
|
|
|
+grammar's nested array construction remain. What changes is the
|
|
|
+**runtime representation of multi-dimensional arrays**.
|
|
|
+
|
|
|
+### 2.2 Two storage families
|
|
|
+
|
|
|
+Keep the current layout for one-dimensional arrays:
|
|
|
+
|
|
|
+```text
|
|
|
+1-D fixed/open descriptor: { count, data... }
|
|
|
+```
|
|
|
+
|
|
|
+This preserves strings, `ARRAY OF CHAR`, `FileIO`, `printf`, pointer to
|
|
|
+array (`ADR(a)` for a fixed 1-D array), and all existing one-dimensional
|
|
|
+tests.
|
|
|
+
|
|
|
+For arrays of rank ≥ 2, use one contiguous **backing block** plus a
|
|
|
+chain of per-level **view descriptors**:
|
|
|
+
|
|
|
+```text
|
|
|
+ArrayView = record
|
|
|
+ count : LONGCARD; (* elements at this level *)
|
|
|
+ elemBytes: LONGCARD; (* bytes in one element at this level *)
|
|
|
+ stride : LONGCARD; (* bytes between adjacent indices *)
|
|
|
+ base : ADDRESS; (* address of the first element at this level *)
|
|
|
+ next : ADDRESS; (* next-level view template, or 0 at the leaf *)
|
|
|
+end
|
|
|
+```
|
|
|
+
|
|
|
+For a contiguous array `stride = elemBytes`. `elemBytes` at an outer
|
|
|
+level is the size of one lower-dimensional slice; at the final level it
|
|
|
+is the scalar element size.
|
|
|
+
|
|
|
+Example `ARRAY [0..4], [0..5], [0..1] OF CHAR`:
|
|
|
+
|
|
|
+```text
|
|
|
+outer view: count=5, elemBytes=12, stride=12, base=payload,
|
|
|
+ next -> row template
|
|
|
+row template: count=6, elemBytes=2, stride=2, base=0,
|
|
|
+ next -> cell template
|
|
|
+cell template: count=2, elemBytes=1, stride=1, base=0, next=0
|
|
|
+
|
|
|
+payload: 5 * 6 * 2 contiguous bytes
|
|
|
+```
|
|
|
+
|
|
|
+`SIZE` is `count * elemBytes` at each level:
|
|
|
+
|
|
|
+- outer: `5 * 12 = 60`
|
|
|
+- row: `6 * 2 = 12`
|
|
|
+- cell (1-D row): `2 * 1 = 2`
|
|
|
+
|
|
|
+### 2.3 Subarray views
|
|
|
+
|
|
|
+`m[i]` on a multi-dimensional view:
|
|
|
+
|
|
|
+1. bounds-check `i` against `count`;
|
|
|
+2. if `next = 0`, return `base + i * stride` (scalar address);
|
|
|
+3. otherwise copy the `next` template into a fresh stack `ArrayView`,
|
|
|
+ set its `base` to `base + i * stride`, and return the view pointer.
|
|
|
+
|
|
|
+The copied view is an expression temporary. It is valid for the
|
|
|
+duration of the enclosing expression/call. Open-array variables cannot
|
|
|
+escape a procedure, so this is sufficient for the supported language.
|
|
|
+
|
|
|
+This gives:
|
|
|
+
|
|
|
+- `ADR(m[i])` = the copied view's `base` (first element of row `i`);
|
|
|
+- `ADR(m[i][j])` = element address;
|
|
|
+- `HIGH(m[i])` = `next.count - 1`;
|
|
|
+- passing `m[i]` to `ARRAY OF REAL` = build a 1-D open descriptor from
|
|
|
+ the copied view.
|
|
|
+
|
|
|
+### 2.4 Fixed multi-dimensional arrays
|
|
|
+
|
|
|
+A fixed multi-dimensional array has:
|
|
|
+
|
|
|
+- a flat payload block, and
|
|
|
+- an outer view plus its static per-level templates.
|
|
|
+
|
|
|
+The view can be stored in static data:
|
|
|
+
|
|
|
+```qbe
|
|
|
+data $a_view = { l 5, l 12, l 12, l $a_data, l $a_row }
|
|
|
+data $a_row = { l 6, l 2, l 2, l 0, l $a_cell }
|
|
|
+data $a_cell = { l 2, l 1, l 1, l 0, l 0 }
|
|
|
+data $a_data = { ... flat payload ... }
|
|
|
+```
|
|
|
+
|
|
|
+For a local fixed array, the view/templates live in the local frame and
|
|
|
+the payload is a local allocation. Existing one-dimensional arrays keep
|
|
|
+their current inline descriptor allocation.
|
|
|
+
|
|
|
+A fixed multi-dimensional array field in a record uses inline flat
|
|
|
+payload storage; a view over that payload is synthesized when the field
|
|
|
+is indexed/passed. This avoids a heap allocation per record element.
|
|
|
+One-dimensional array fields stay as they are today.
|
|
|
+
|
|
|
+### 2.5 Open multi-dimensional formals
|
|
|
+
|
|
|
+An open multi-dimensional formal is passed as a pointer to the outer
|
|
|
+`ArrayView` (`l`), just as 1-D open arrays are passed as an `l`
|
|
|
+descriptor pointer.
|
|
|
+
|
|
|
+At a call:
|
|
|
+
|
|
|
+- fixed multi actual: materialize the outer view over the fixed payload;
|
|
|
+ its templates are static;
|
|
|
+- open multi actual to matching-rank formal: pass the existing view;
|
|
|
+- row subview actual: pass the synthesized row view;
|
|
|
+- 1-D formal: build the existing `{count, data}` descriptor;
|
|
|
+- `ARRAY OF BYTE`/`SBYTE` formal: build a 1-D byte descriptor with
|
|
|
+ `count = payload bytes` and `data = view.base`.
|
|
|
+
|
|
|
+`VAR` and value open parameters use the same view pointer. Element
|
|
|
+writes go through `base`; no payload copy is made. Whole-array copy
|
|
|
+uses the payload counts and a byte/structured copy.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 3. Semantics
|
|
|
+
|
|
|
+### 3.1 `HIGH`
|
|
|
+
|
|
|
+- 1-D array: existing `count - 1`.
|
|
|
+- multi view: `count - 1`.
|
|
|
+- `HIGH(m[0])`: the copied row view's `count - 1`.
|
|
|
+
|
|
|
+### 3.2 `SIZE`
|
|
|
+
|
|
|
+For an array value, `SIZE` returns **payload bytes**:
|
|
|
+
|
|
|
+- 1-D fixed/open: `count * elemBytes`, excluding the descriptor header;
|
|
|
+- multi view: `count * elemBytes` at the current level;
|
|
|
+- scalar: existing type size.
|
|
|
+
|
|
|
+`ObjectSize` remains the internal allocation footprint (`8 + payload`
|
|
|
+for 1-D inline arrays, plus view/templates for multi). `SIZE` no longer
|
|
|
+uses `ObjectSize` for arrays.
|
|
|
+
|
|
|
+For compatibility, `TSIZE` should follow the same array payload rule in
|
|
|
+the first implementation; the internal allocator continues to use
|
|
|
+`ObjectSize`. This is called out in the open questions.
|
|
|
+
|
|
|
+### 3.3 `ADR`
|
|
|
+
|
|
|
+- multi-dimensional array/view: first payload element (`view.base`);
|
|
|
+- open 1-D array: first payload element (`descriptor + 8`);
|
|
|
+- fixed 1-D array: **existing descriptor address**, keeping
|
|
|
+ `POINTER TO ARRAY` (`t_addrptr`) working;
|
|
|
+- scalar: its address.
|
|
|
+
|
|
|
+The fixed-1-D exception is deliberate. It is documented as a V3
|
|
|
+pointer-to-array compatibility rule.
|
|
|
+
|
|
|
+### 3.4 Indexing
|
|
|
+
|
|
|
+- 1-D: existing `ElemAddr` (`base + 8 + (i-lo)*elemSize`).
|
|
|
+- multi view: `base + (i-lo)*stride`, then either scalar address or a
|
|
|
+ copied next-level view.
|
|
|
+- Multi-index syntax `m[i,j]` is lowered as the existing chained
|
|
|
+ `m[i][j]`; no separate layout.
|
|
|
+- Bounds checks use the type's lower bound and the runtime view `count`.
|
|
|
+
|
|
|
+### 3.5 Assignment and whole-array copy
|
|
|
+
|
|
|
+- Element assignment uses scalar address.
|
|
|
+- Row assignment uses the row view payload.
|
|
|
+- Whole-array assignment checks counts and copies payload bytes (or
|
|
|
+ element-wise for nested records/sets); descriptor/templates are not
|
|
|
+ copied from the source view.
|
|
|
+- `ARRAY OF BYTE` flattening is a view, not a copy, so `VAR` writes are
|
|
|
+ visible to the original array.
|
|
|
+
|
|
|
+### 3.6 Character arrays
|
|
|
+
|
|
|
+Existing 1-D `ARRAY OF CHAR` keeps its NUL-terminator slot and
|
|
|
+`HIGH = count-1`/`OpenHiChar` behavior. Multi-dimensional inner CHAR
|
|
|
+arrays use the declared element counts with no hidden per-row
|
|
|
+terminator, matching `unbounded3` (`SIZE(a[0][0])=2` for two CHARs).
|
|
|
+When a row is adapted to a 1-D `ARRAY OF CHAR` formal, the adapter uses
|
|
|
+the row's payload count; the terminator slot is not added implicitly.
|
|
|
+This is an open question to confirm against ISO.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 4. Type system changes
|
|
|
+
|
|
|
+Add queries in `SymTab`:
|
|
|
+
|
|
|
+```text
|
|
|
+ArrayRank (t) : CARDINAL
|
|
|
+IsMultiArray (t) : BOOLEAN
|
|
|
+ArrayStride (t, level) : CARDINAL
|
|
|
+ArrayPayloadSize (t) : CARDINAL
|
|
|
+ArrayViewType (t) : TypeIndex / representation marker
|
|
|
+```
|
|
|
+
|
|
|
+Compatibility rules:
|
|
|
+
|
|
|
+- same-rank fixed/open arrays remain compatible by element type and
|
|
|
+ bounds (existing `Assignable`/`VarParamOk` rules, extended to rank > 1);
|
|
|
+- a fixed/open multi view can be passed to a same-rank open formal;
|
|
|
+- a rank-1 open formal of `BYTE`/`SBYTE` accepts any array/view and uses
|
|
|
+ its total payload byte count (`unbounded6`/`unbounded8`);
|
|
|
+- a rank-1 open formal of another element type accepts a rank-1 row
|
|
|
+ view of that element type;
|
|
|
+- a multi-dimensional actual cannot silently become a 1-D formal of a
|
|
|
+ non-byte element type.
|
|
|
+
|
|
|
+`ObjectSize`, `FieldSize`, `ElemBytes`, `TypeSizeD` and `ArrObjSize` need
|
|
|
+multi-array cases. For a multi array variable/field, the inline slot is
|
|
|
+the view pointer; the payload is separate/static/local. For 1-D arrays,
|
|
|
+nothing changes.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. Lower / QbeGen changes
|
|
|
+
|
|
|
+### 5.1 New helpers
|
|
|
+
|
|
|
+```text
|
|
|
+MakeArrayView(actualType, actualAddr, formalType, VAR q)
|
|
|
+ViewBase(view)
|
|
|
+ViewCount(view)
|
|
|
+ViewSize(view)
|
|
|
+ViewHigh(view)
|
|
|
+ViewIndex(view, i, lo, lowerType, VAR q)
|
|
|
+ViewSub(view, i, VAR q)
|
|
|
+FlattenToByteDescriptor(view, VAR q)
|
|
|
+```
|
|
|
+
|
|
|
+The view descriptor is 5 words (40 bytes) per level.
|
|
|
+
|
|
|
+### 5.2 Call sites
|
|
|
+
|
|
|
+`LowerCall` needs a small conversion layer before `CallArg`:
|
|
|
+
|
|
|
+- actual rank = formal rank: build/pass view;
|
|
|
+- actual rank > formal rank 1 and formal is `BYTE`/`SBYTE`: flatten;
|
|
|
+- actual rank = 1 / formal rank = 1: existing descriptor path;
|
|
|
+- actual row/view to rank-1 open: build 1-D descriptor.
|
|
|
+
|
|
|
+The conversion must not copy `VAR` payloads. Temporary views may be
|
|
|
+stack `alloc8` objects.
|
|
|
+
|
|
|
+### 5.3 Builtins
|
|
|
+
|
|
|
+- `SIZE`/`TSIZE` array arguments dispatch to view size or 1-D count*elem.
|
|
|
+- `HIGH`/`LEN` dispatch to view count.
|
|
|
+- `ADR` dispatch as in §3.3.
|
|
|
+- `CopyArray` handles flat multi payloads and subviews.
|
|
|
+- `NEW`/`DISPOSE` and `ALLOCATE` for multi arrays use `ObjectSize` /
|
|
|
+ payload allocation, not the view size.
|
|
|
+
|
|
|
+### 5.4 Static data and locals
|
|
|
+
|
|
|
+- `ArrBodyItems`/`DeclArr`: multi-dimensional static arrays emit view
|
|
|
+ data, templates and flat payload; arrays of records with multi fields
|
|
|
+ recurse per element.
|
|
|
+- `AllocLocal`/`InitStack`: allocate view/templates and payload for local
|
|
|
+ multi arrays.
|
|
|
+- `DeclVar`/`DeclConst`: multi array constructors emit flat payload and
|
|
|
+ views.
|
|
|
+- Existing one-dimensional array emission remains unchanged.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 6. Corpus test mapping
|
|
|
+
|
|
|
+| file | expected result after implementation |
|
|
|
+| --- | --- |
|
|
|
+| `unbounded` | `HIGH(a)=4`, `HIGH(a[0])=5`; writes survive |
|
|
|
+| `unbounded2` | same for fixed 2-D `CHAR` and Boolean-index arrays |
|
|
|
+| `unbounded3` | `SIZE` = 60, 12, 2, 2 |
|
|
|
+| `unbounded4` | `VAR`/value 2-D REAL assignment works |
|
|
|
+| `unbounded6` | `SIZE(2-D CARDINAL)` equals `SIZE(ARRAY OF BYTE)` (600 bytes) |
|
|
|
+| `unbounded8` | row `a[0]` flattening gives `SIZE=120` |
|
|
|
+| `array9` | compiles and runs |
|
|
|
+| `adrunbounded3` | `ADR` base/row/element, row to 1-D open, `VAR` writes |
|
|
|
+
|
|
|
+Unit regressions:
|
|
|
+
|
|
|
+- `t_multidim` fixed 2-D layout/indexing;
|
|
|
+- `t_addrptr` fixed 1-D `ADR` compatibility;
|
|
|
+- `t_arrrecfld` 1-D array fields unchanged;
|
|
|
+- new `t_open2d_size`, `t_open2d_var`, `t_open2d_high`, `t_open2d_adr`,
|
|
|
+ `t_open_flatten_byte`, `t_open_row_to_1d`.
|
|
|
+
|
|
|
+Gates after each step: suite, fixpoint, corpus.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 7. Milestones
|
|
|
+
|
|
|
+1. **M1 — type/ABI helpers** (design + `ArrayRank`/payload/stride
|
|
|
+ queries; no codegen change).
|
|
|
+2. **M2 — fixed multi storage** (flat payload + static/local views;
|
|
|
+ keep 1-D unchanged).
|
|
|
+3. **M3 — open view descriptors** (stack views, calls, row subviews,
|
|
|
+ flattening).
|
|
|
+4. **M4 — builtins/indexing/assignment** (`SIZE`, `HIGH`, `ADR`,
|
|
|
+ `ElemAddr`, `CopyArray`).
|
|
|
+5. **M5 — records/constructors/corpus**
|
|
|
+ (multi array fields, records arrays, constructors; run the 8 corpus
|
|
|
+ cases).
|
|
|
+6. **M6 — docs/gates/tag `v3-open-arrays`.**
|
|
|
+
|
|
|
+`unbounded5` remains a declaration-order case, not this feature.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 8. Risks and open questions
|
|
|
+
|
|
|
+- **View lifetime:** subarray views are stack temporaries. Open array
|
|
|
+ parameters cannot escape a procedure, so this is sound for the
|
|
|
+ supported language; storing a view pointer in a global would be
|
|
|
+ invalid and must be rejected or documented.
|
|
|
+- **`VAR` aliasing:** view-based writes must not copy payloads. This
|
|
|
+ needs dedicated tests for nested `VAR` parameters.
|
|
|
+- **`ADR` compatibility:** fixed 1-D arrays keep descriptor `ADR`;
|
|
|
+ multi/open arrays use payload base. This asymmetry must be documented.
|
|
|
+- **`SIZE`/`TSIZE`:** payload vs allocation footprint needs a final
|
|
|
+ decision; the proposed rule is payload for both builtins, `ObjectSize`
|
|
|
+ for internal allocation.
|
|
|
+- **CHAR terminators:** 1-D strings keep a terminator; multi rows use
|
|
|
+ exact counts. Confirm against ISO and `unbounded3`.
|
|
|
+- **Records/constructors:** multi-dimensional array fields and
|
|
|
+ constructors are the highest-risk part; they are not corpus-driven but
|
|
|
+ are in the chosen full scope.
|
|
|
+- **Descriptor chain copies:** each `m[i]` may allocate a 40-byte stack
|
|
|
+ view; nested loops could allocate many views. The allocator is stack
|
|
|
+ bump allocation; no heap lifetime issue.
|
|
|
+- **Multi-index parser:** `m[i,j]` must keep expanding to chained
|
|
|
+ subviews.
|
|
|
+- **Bounds:** lower bounds come from the type; views store counts only.
|
|
|
+- **1-D compatibility:** all existing string/FileIO/`ARRAY OF CHAR`
|
|
|
+ behavior must remain byte-identical.
|
|
|
+
|
|
|
+## 9. Explicit non-goals for the first implementation
|
|
|
+
|
|
|
+- ISO generic array descriptors beyond rank 3 (the corpus needs 3).
|
|
|
+- Open arrays as function results or stored open-array variables.
|
|
|
+- Dynamic rank.
|
|
|
+- Changing one-dimensional array/string layout.
|
|
|
+- Separate compilation of array descriptors (future work).
|