Status: design + M1 helpers + M2 fixed storage + M3 open views + M4
builtins (2026-10-23). Rank-2/3 fixed and open arrays use the view ABI;
array SIZE/TSIZE and view ADR are implemented. The 1-D open
descriptor still uses its legacy {count,data} layout at ABI boundaries.
Current gates: suite 295/295, fixpoint OK 3,480,366 bytes, corpus
518/612 compile-OK, 0 crashes.
Interactive decisions already taken:
SIZE: an array's payload bytes, not the
descriptor header.VAR formals,
subarray views, row → 1-D open array, 2-D → ARRAY OF BYTE
flattening, SIZE/HIGH/ADR, records and constructors.This document records the proposed ABI and implementation plan before any code is changed.
V3 currently represents every array value as a descriptor address:
{ count, data... } (data starts at offset 8).{ 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.
SymTab keeps the existing array chain:
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.
Keep the current layout for one-dimensional arrays:
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:
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:
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:
5 * 12 = 606 * 2 = 122 * 1 = 2m[i] on a multi-dimensional view:
i against count;next = 0, return base + i * stride (scalar address);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;m[i] to ARRAY OF REAL = build a 1-D open descriptor from
the copied view.A fixed multi-dimensional array has:
The view can be stored in static data:
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.
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:
{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.
HIGHcount - 1.count - 1.HIGH(m[0]): the copied row view's count - 1.SIZEFor an array value, SIZE returns payload bytes:
count * elemBytes, excluding the descriptor header;count * elemBytes at the current level;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.
ADRview.base);descriptor + 8);POINTER TO ARRAY (t_addrptr) working;The fixed-1-D exception is deliberate. It is documented as a V3 pointer-to-array compatibility rule.
ElemAddr (base + 8 + (i-lo)*elemSize).base + (i-lo)*stride, then either scalar address or a
copied next-level view.m[i,j] is lowered as the existing chained
m[i][j]; no separate layout.count.ARRAY OF BYTE flattening is a view, not a copy, so VAR writes are
visible to the original array.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.
Add queries in SymTab (implemented in M1):
ArrayRank (t) : CARDINAL (* ArrayDepth under an ABI name *)
IsMultiArray (t) : BOOLEAN
ArrayLeafBytes (t) : CARDINAL (* scalar/record leaf bytes *)
ArrayStride (t, level) : CARDINAL
ArrayPayloadSize (t) : CARDINAL (* only fully fixed arrays *)
ArrayStride/ArrayPayloadSize return 0 for open/unknown counts; M3
will use the runtime view metadata for those cases.
Compatibility rules:
Assignable/VarParamOk rules, extended to rank > 1);BYTE/SBYTE accepts any array/view and uses
its total payload byte count (unbounded6/unbounded8);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.
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.
LowerCall needs a small conversion layer before CallArg:
BYTE/SBYTE: flatten;The conversion must not copy VAR payloads. Temporary views may be
stack alloc8 objects.
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.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.| 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;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.
ArrayRank, IsMultiArray, ArrayLeafBytes, ArrayStride,
ArrayPayloadSize in SymTab.MaxDefer raised from 256 to 512 (the compiler now declares 257
procedures) and the session SSA buffer raised from 4 to 8 MiB.{count, elemBytes, stride, base, next} views.ViewIndex produces subviews for array selectors and element
addresses for the final scalar selector.NEW/local initialization work.CopyOldNestedToView.{count, data} descriptor for fixed/open formals;
this is intentionally temporary until M3.t_nestarr remains the regression.t_multidim, t_multidim_local, t_array, t_ctor3,
t_nestidx, t_nestarr, t_ptrnest, showcase6; suite 284/284,
fixpoint OK, corpus 512/612 with 0 crashes.ARRAY OF ARRAY OF ...) parse correctly.ArrayView pointers; VAR writes go straight to base.count, stride,
base, next), and HIGH/LEN read the view count. A fixed
array, an open formal, a row subview and a forwarded subview all
work without rebuilding the payload.ARRAY OF BYTE/SBYTE and row → 1-D conversions remain adapted to
the legacy 1-D {count, data} descriptor. The temporary adapter
allocates a descriptor and, for VAR, copies the payload back;
this preserves write-through while the compiler still has the old
1-D ABI and C shims. It is freed after the call. Migrating that
legacy 1-D path to a pointer-based view remains a follow-up.t_open2d, t_open2d_value, t_open2d_high,
t_open2d_nested, t_open3d, t_open3d_row,
t_open_row_to_1d, t_open_flatten_byte. Corpus gains
unbounded, unbounded2, unbounded3, unbounded4 and
array9 (compile gate).SIZE/TSIZE now use payload bytes for arrays: fixed 1-D
count*leaf, open 1-D count*element, and runtime
count*elemBytes for every view level. The old descriptor header
and 1-D CHAR terminator are excluded.ADR returns the first payload byte for multi views, row subviews
and 1-D open descriptors; fixed 1-D arrays retain their descriptor
address for POINTER TO ARRAY compatibility.ARRAY OF BYTE compatibility adapter now appends the
legacy CHAR/UCHAR terminator when materialising an external 1-D
descriptor, and is freed after VAR copy-back.t_open2d_size, t_open2d_adr, t_open1d_size_adr,
plus the M3 open-array tests. unbounded3 runs successfully with
SIZE 60/12/2/2.ADR identity through a 1-D internal call).
The current adapter preserves writes but is copy-based. This does
not block the corpus cases that reach the compiler today.v3-open-arrays.unbounded5 remains a declaration-order case, not this feature.
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.unbounded3.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.m[i,j] must keep expanding to chained
subviews.ARRAY OF CHAR
behavior must remain byte-identical.