Skip to content

spec: unbounded []T and []*T in the variable class (#523) - #531

Merged
gafferongames merged 1 commit into
mainfrom
spec-unbounded-arrays
Sep 4, 2026
Merged

gafferongames merged 1 commit into
mainfrom
spec-unbounded-arrays

Conversation

@gafferongames

@gafferongames gafferongames commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Ruling 3 on #523: unbounded []T and []*T in the variable class, yes. The map's storage, walks and measure without the key and the sort, wire-neutral under the existing kind and 64-bit lengths.

Specification only, no code. New §2.9, plus the clauses the construct needs in §2, §2.2, §2.6, §2.8, §3, §4, §4.1, §6.5, §6.6, §7.2, §7.4, §8.1, §11, §15, §16.2, §18.1, §18.2, §20.1 and §20.2, and one clause in SPEC.md §1.

Updated for the cold read of #531: three owner rulings and thirteen named changes, all made.

The construct

  • []T and []*T, legal in a table body only. A type body refuses one by name, which keeps SPEC.md's "no unbounded collections" true of the type wire and refuses one on a packet. A UNION ARM refuses one too (ruling below).
  • Storage is the map's slot exactly: an int64 self-relative TableRef and an int32 count, sixteen bytes, with the elements laid by value in the holder's node extent at alignof( T ), zero slack. Lists and maps are one population in that extent, placed pre-order in the declaration order of the fields that hold them.
  • The wire is the existing kind 14 at the element's own element kind with the live count, elided at zero. []T and [..N]T are the same bytes, and a bound is added or removed without touching a stored file.
  • LoadMeasure's term is N × sizeof( T ) rounded up to alignof( T ), at every depth, reached by the same header walk a map's N needs. The block form is none, by absence.

The three owner rulings from the cold read

  1. Erase exists, by the map's own mechanism. An element is erased by its pointer — dead bit in the segment's slot, live count decremented, the four walks skip it — exactly as a map entry is, with the storage reclaimed at reset and never reused mid-build. The key is what differs: a map erases by the key, a list by the element's own address, which is the one thing the builder already promises never moves. Why the builder carries it: the save-edit cycle (LoadBuilder, edit, Save) is the tool's path, and a game's inventory removes items in slot order, which a map[K]T keyed by an id cannot keep. Indices are not stable across an erase and the pointer is, which is why the pointer is the handle.
    The retention paragraph now states §6.6's hazard honestly: on the region path a list's count and elements are ordinary writable memory like any array's, §6.6's rule applies unchanged, and the builder is irrelevant to retention because retention is a region round trip.
  2. LoadMeasure's -1 carries a reason, from the same enum ruling 8 gives Open and BlockOpen (§7, §19.2), with the same spellings in every target. Stated once in §6.5 as a table, on §7's own rule of one value per clause: unknown_form, count_over_length, count_over_extent_cap, blob_over_size_cap, data_cycle (the authoring side's). §2.9 cites it rather than restating it. A refusal still moves no counter.
  3. The index accessor is bounds-checked in every build, on §2.4's rule and for §2.4's reason: size() came from a file, NDEBUG does not remove the compare, and there is no undefined-behavior path in any configuration. C++ asserts then aborts, C# throws, Rust and Go panic.
  4. A map is not a union arm either, refused by name in §2.6 and §11 on the same ground []T is: both put their elements in the holder's node extent through a placement walk over what the record reaches by value, and an arm is reached by value only when its tag says so, so an arm's array would make the extent's contents and every offset after it depend on a discriminant — a layout no cook can be byte-stable under and no cook-check clause can bound. §2.6's admission of a map at an arm predates this PR; it is corrected here because §2.9's refusal made it load-bearing. No corpus schema declares one, so the refusal breaks nothing, and §20.2's arm grammar already excluded array=map — the prose now says why.

The thirteen named changes

  1. Both overflow outcomes, per path, as a two-row table: into a region, LoadMeasure answers -1 with its reason, no Load, no report; into a builder, a count the body cannot cover is §4's malformed with the covered prefix kept, and a count above the int32 cap is a NULL from LoadBuilder with the partial builder discarded and the report as it stood. "Never disagree about a wire" is scoped: the two paths agree on every wire either of them decodes, and where they differ is at a refusal, which is not a decode. The five LoadMeasure controls moved off the report surface.
  2. The [][]T fix is a TABLE wrapper, spelled — SPEC.md's "wrap the inner array in a type" does not reach here, because a type body refuses a list:
    table Row   { items []Sample }
    table Sheet { rows  []Row }
    
  3. The int32 cap control is a named unit test, make tables-list-measure-refusals, with synthetic counts: no golden can carry two gigabytes, and a refusal produces no counters for a report row to pin. It covers the cap, the count over L, both at depth, and a clean wire beside them.
  4. The baseline rule replaces the arm-disjointness analogy: bound= appearing or vanishing on an array= move is the capacity fact, judged as a shrink (warn) or a growth (silence); elem=, type=, enum=, union= and kind= keep judging exactly as before. It still bumps no rendering version.
  5. unbounded added to §20.2's array= grammar, with the prose and a row in §20.1's fact table. It carries no bound=, for the map's reason.
  6. §8.1 now carries the rule for a map's and a list's descriptor — neither had one — as one shape for both: kind/element kind, element_size as the pitch, counted with count_offset, table naming the element's descriptor, and array_bound = 0 as the one tell that the field's offset names an int64 reference rather than the first element. Zero is free because every other shape's bound is at least one, so it costs no new column in nine ports.
    Backend status is stated on the page, because the reference does not carry it yet. The C++ map descriptor emitted today leaves kind at 0 and is_array false and describes the map through the entry's own TableTypeInfo beside three map-specific function columns (map_count, map_at, map_insert). It moves to the columns above when the list lands, and they land together for one reason: a second out-of-line shape would otherwise need a second set of function columns, and three per construct is how a descriptor becomes a per-construct API instead of a vocabulary. The resolver the text walk genuinely cannot spell for itself stays; what changes is that the SHAPE is read from the array columns like every other array's rather than inferred from a non-NULL entry.
  7. []T as a union arm: refused by name in §2.6 and §11. An arm's storage is overlaid and a list's elements live in the holder's node extent for every list the record reaches by value, so an arm's array would make the extent depend on the tag. An arm holding a list is a table arm's job.
  8. The clamp control's count is pinned above 2^16 (100,000 elements), so no bound a control author happens to pick can clamp it and let the row pass.
  9. list_migrates golden added: one content, two declarations of the holder ([..N]T and []T), one pinned wire both write byte for byte and both read into equal values, report silent both directions. list_erased added beside it for the dead-element control.
  10. SPEC.md §1's non-goal scoped to the type wire, naming the table wire's map and []T and why neither is reachable from a packet.
  11. Cites fixed: §4.2 for the truncated bound, §4.3 for arrays of arrays; ?[..N]T (a presence bit on the array, landed) distinguished from []?T (an element's presence bit, a follow-on); and the additions count corrected against the bullets that follow it.
  12. string(N) and bytes(N) dropped from the element-set sentence, which is now the rule rather than a list: whatever [..N]T admits, []T admits, and whatever it refuses, []T refuses on the bounded array's own diagnostic.
  13. []*T with a null slot named as what serves the []?T case today.

The decisions the page made beyond the ruling

The element set is [..N]T's and not the map's value list, so every element refusal is one an array already carries; no entry type is generated, which is where four of the map's six framing bytes an entry go; clamped cannot fire on the count; what bounds a load is the arena, the caller's allocation under LoadMeasure, and the int32 count slot; the construct claims three names against its field (Add, Each, Erase) where a map claims eight; §4.1's silent class stays at four; retain-unknown addresses an element by its ordinal (§6.6); and position here is not a vocabulary, so §2.4's [E.Max]T refusal is not reopened.

Ports

Backend status is the map's: the C++ reference and the tool carry it, every other backend refuses a unit that declares one by name, and the ports are a named follow-on (§15). What a port needs is smaller than what a map needed, by exactly the key.

Merge check

Rebased onto main at 8fd34f1 (after #507, #527, #529, #530). git merge-tree --write-tree reports zero conflicts against main and against #532's head. #530's §3.3, its reserved-id rule and its §5/§11 edits touch nothing this PR touches: an unbounded array spends no reserved id and adds no framing, so the message form carries it exactly as the file form does. The §11 and §6.6 edits were re-checked in place after the rebase.

Two edits were deliberately relocated to keep them off lines #532 touches: the §11 runtime-name claim and the §16.2 text row.

One landing-order note: §6.5's refusal-reason table shares the enum ruling 8 gives Open and BlockOpen, which #532 defines as TableOpenReason. This page refers to it by description and by section rather than by type name, so nothing here claims that name or conflicts with it. Whichever of the two merges second adds the back-reference; a build with one has the other.

🤖 Generated with Claude Code

@gafferongames

Copy link
Copy Markdown
Contributor Author

Rulings from the cold read, under the owner's word of 2026-09-04:

  1. Erase exists, by the map's own mechanism: an element erased by its pointer is marked dead with a live count and the four walks skip it. The save-edit cycle is the tool's path, and an inventory removes items in slot order, which a map by id cannot keep.
  2. LoadMeasure's -1 carries a reason from the enum family Open and BlockOpen received (unknown form, count over L, count over the int32 cap, a cycle), the same names in every target.
  3. The index accessor's bounds check stands in every build, as the keyed accessor's does: one compare per index over data from a file, no undefined-behavior path in release.
  4. A []T is not a union arm, refused by name.

The named page changes from the read land on this branch before it leaves draft.

rowan-claude pushed a commit that referenced this pull request Sep 4, 2026
Ruling 3 on schema#523: the map's storage, walks and measure without the
key and the sort, wire-neutral under the existing array kind and the
wire's 64-bit counts. Plus the three owner rulings and the thirteen named
changes from the cold read of #531.

New §2.9. The declaration is `[]T` and `[]*T`, legal in a table body only,
refused in a `type` body and therefore on the packet wire, and refused as
a union arm. Storage is the map's slot exactly: an `int64` self-relative
reference and an `int32` count, sixteen bytes, with the elements laid by
value in the holder's node extent, lists and maps ordered as one
population pre-order. The wire is kind `14` at the element's own kind with
the live count, so `[]T` and `[..N]T` are the same bytes and a bound is
added or removed without touching a stored file. The text form is a JSON
array with no bound to clamp against.

The owner rulings:

- ERASE EXISTS, by the map's own mechanism, addressed by the element's
  POINTER: dead bit, live count, the four walks skip it. The save-edit
  cycle is the tool's path and a game's inventory removes in slot order,
  which a map by id cannot keep. Retention then restates §6.6's hazard
  honestly: a region's count and elements are ordinary writable memory,
  the hazard applies unchanged, and the builder is not on that path.
- `LoadMeasure`'s `-1` CARRIES A REASON, from the enum ruling 8 gives
  `Open` and `BlockOpen`, one value per clause on §7's own rule, stated
  once in §6.5: `unknown_form`, `count_over_length`,
  `count_over_extent_cap`, `blob_over_size_cap`, `data_cycle`.
- The index accessor is BOUNDS-CHECKED IN EVERY BUILD, on §2.4's rule:
  the extent came from a file, so there is no undefined-behavior path in
  release.

The named changes: the two overflow outcomes stated per path, region
against builder, with the agreement claim scoped to what either path
decodes; the `[][]T` fix spelled as a TABLE wrapper, since a `type` body
refuses a list; the four `LoadMeasure` refusals moved to a named unit
test with synthetic counts, because a refusal produces no counters and no
golden can carry two gigabytes; the baseline rule stated as the capacity
fact `bound=` carries when it appears or vanishes, with `elem=`, `type=`
and `kind=` still judging; `array=unbounded` added to §20.2's grammar and
§20.1's fact table; the map's descriptor columns documented in §8.1 for
the first time, with `array_bound = 0` as the tell that a field's offset
names a reference rather than the first element, and the list taking them
unchanged; the clamp control pinned above 2^16; `list_migrates` and
`list_erased` added to the corpus; one clause in SPEC.md §1 scoping "no
unbounded collections" to the type wire; the §4.2 and §4.3 cites fixed,
`?[..N]T` distinguished from `[]?T` with `[]*T` and a null slot named as
what serves; `string(N)` and `bytes(N)` dropped from the element-set
sentence, which is now the rule that `[]T` admits exactly what `[..N]T`
admits; and the additions count corrected against the bullets that follow
it.

Specification only, no code. Ports are a named follow-on (§15).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ruling 3 on schema#523: the map's storage, walks and measure without the
key and the sort, wire-neutral under the existing array kind and the
wire's 64-bit counts. Plus the four owner rulings and the thirteen named
changes from the cold read of #531.

New §2.9. The declaration is `[]T` and `[]*T`, legal in a table body only,
refused in a `type` body and therefore on the packet wire, and refused as
a union arm. Storage is the map's slot exactly: an `int64` self-relative
reference and an `int32` count, sixteen bytes, with the elements laid by
value in the holder's node extent, lists and maps ordered as one
population pre-order. The wire is kind `14` at the element's own kind with
the live count, so `[]T` and `[..N]T` are the same bytes and a bound is
added or removed without touching a stored file. The text form is a JSON
array with no bound to clamp against.

The owner rulings:

- ERASE EXISTS, by the map's own mechanism, addressed by the element's
  POINTER: dead bit, live count, the four walks skip it. The save-edit
  cycle is the tool's path and a game's inventory removes in slot order,
  which a map by id cannot keep. Retention then restates §6.6's hazard
  honestly: a region's count and elements are ordinary writable memory,
  the hazard applies unchanged, and the builder is not on that path.
- `LoadMeasure`'s `-1` CARRIES A REASON, from the enum ruling 8 gives
  `Open` and `BlockOpen`, one value per clause on §7's own rule, stated
  once in §6.5: `unknown_form`, `count_over_length`,
  `count_over_extent_cap`, `blob_over_size_cap`, `data_cycle`. The name
  is #532's to define, so this page cites it by description and section.
- The index accessor is BOUNDS-CHECKED IN EVERY BUILD, on §2.4's rule:
  the extent came from a file, so there is no undefined-behavior path in
  release.
- A MAP IS NOT A UNION ARM EITHER, refused by name in §2.6 and §11 on the
  same ground `[]T` is: both put their elements in the holder's node
  extent through a placement walk over what the record reaches BY VALUE,
  and an arm is reached by value only when its tag says so, so an arm's
  array would make the extent depend on a discriminant. §2.6's admission
  of a map at an arm predates this PR and is corrected here, because
  §2.9's refusal made it load-bearing. No corpus schema declares one.

The named changes: the two overflow outcomes stated per path, region
against builder, with the agreement claim scoped to what either path
decodes; the `[][]T` fix spelled as a TABLE wrapper, since a `type` body
refuses a list; the four `LoadMeasure` refusals moved to a named unit
test with synthetic counts, because a refusal produces no counters and no
golden can carry two gigabytes; the baseline rule stated as the capacity
fact `bound=` carries when it appears or vanishes, with `elem=`, `type=`
and `kind=` still judging; `array=unbounded` added to §20.2's grammar and
§20.1's fact table, and the arm line's own token set widened to name what
it refuses; the map's descriptor columns documented in §8.1 for the first
time, with `array_bound = 0` as the tell that a field's offset names a
reference rather than the first element, and the list taking them
unchanged; the clamp control pinned above 2^16; `list_migrates` and
`list_erased` added to the corpus; one clause in SPEC.md §1 scoping "no
unbounded collections" to the type wire; the §4.2 and §4.3 cites fixed,
`?[..N]T` distinguished from `[]?T` with `[]*T` and a null slot named as
what serves; `string(N)` and `bytes(N)` dropped from the element-set
sentence, which is now the rule that `[]T` admits exactly what `[..N]T`
admits; and the additions count corrected against the bullets that follow
it.

Specification only, no code. Ports are a named follow-on (§15).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gafferongames
gafferongames marked this pull request as ready for review September 4, 2026 15:56
@gafferongames
gafferongames merged commit dd39038 into main Sep 4, 2026
20 checks passed
@gafferongames
gafferongames deleted the spec-unbounded-arrays branch September 4, 2026 15:59
rowan-claude pushed a commit that referenced this pull request Sep 4, 2026
rowan-claude pushed a commit that referenced this pull request Sep 4, 2026
gafferongames added a commit that referenced this pull request Sep 4, 2026
…omments (#523) (#532)

* spec: the wstring table kind, widening, float16, Open reasons, text comments (#523)

Six rulings from #523, specification only.

1. wstring on the table wire: kind 33, `*wstring` under the reserved blob
   id fnv1a64("wstring"), the cooked storage row, the text row, the
   LoadMeasure term, the map-key refusal by name, and the #510 closure
   refusal deleted.
2. Widening on read: an integer kind into a wider one of the same
   signedness, and f32 into f64, decodes exactly under a new `widened`
   counter. Every other pair stays kind_mismatch. The silent class does
   not move.
3. Flags storage stays uint64 in every target, with the reason.
4. float16 declined, kind 34 reserved by name.
5. Open and BlockOpen fill a TableOpenReason beside the null.
6. The text form accepts // and /* */ comments on read and never writes
   them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* spec: keep the wstring refusal clause off the byte-buffers bullet's opening lines

* spec: ill-formed text is framing-class damage on both text kinds, and the cold read's named changes (#523)

* spec: reconcile with the message form (#530), and fix its form-byte fuzzer pin

* spec: keep the three-spelling list edits clear of the unbounded-array lines (#531)

* spec: the wide-text writer rows are storage built in code, not loaded

* spec: name TableOpenReason from the measure's reason table, and widen its stated scope (#531)

* spec: the refusal enum is TableRefuseReason, since LoadMeasure's -1 carries it too (#531)

* spec: the arm companion list carries wstring, three reserved-id refusals, one header on the refusal table

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames added a commit that referenced this pull request Sep 4, 2026
…sals (#531)

The `[]T` and `[]*T` spellings reach the IR as ArrayList, the near-miss
spellings `[..]T` and `[0..]T` are refused by name with `[]T` as the fix, and
arrays of arrays are refused where a second bracket stands. The construct's
own refusals land beside the element's: a `type` body, a union arm, `?[]T`, a
specified default, a qualification, and the three claimed names
`<Table><Field>{Add,Each,Erase}`. `TableList` joins the unit-level runtime
claim. Every port refuses a unit that declares one, naming the fields and cpp
as the carrier, and the tool's cook surfaces refuse one on the map's terms.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames added a commit that referenced this pull request Sep 4, 2026
…sals (#531)

The `[]T` and `[]*T` spellings reach the IR as ArrayList, the near-miss
spellings `[..]T` and `[0..]T` are refused by name with `[]T` as the fix, and
arrays of arrays are refused where a second bracket stands. The construct's
own refusals land beside the element's: a `type` body, a union arm, `?[]T`, a
specified default, a qualification, and the three claimed names
`<Table><Field>{Add,Each,Erase}`. `TableList` joins the unit-level runtime
claim. Every port refuses a unit that declares one, naming the fields and cpp
as the carrier, and the tool's cook surfaces refuse one on the map's terms.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames added a commit that referenced this pull request Sep 4, 2026
…ions (#531)

The wire engine writes and reads a `[]T` as the kind 14 body a `[..N]T`
writes: the same element kind, the same count, the same elision of an empty
one. A count above the int32 storage cap is the refusal LoadBuilder answers
NULL for, and the slots are grown against the body's own length so a count no
body can cover allocates nothing. `clamped` cannot fire on the count, because
there is no bound to clamp against.

The text form is the JSON array it already was, with every element the text
carries read. The node walk reaches a list at its field's position, in index
order, so a `[]*T` declared before a pointer field numbers the shared node
first. The baseline renders `array=unbounded` with no `bound=`, which is what
makes a bound added warn and a bound removed pass, and the cook projection
renders `kind=14 array=unbounded elem=` the element's own storage size beside
the sixteen-byte slot's `size=`.

Proved end to end through `pack` and `unpack` over §2.9's own example, and the
same content under `[..8]T` packs to byte-identical bytes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames added a commit that referenced this pull request Sep 4, 2026
…531)

Every claim §2.9 makes about the halves this branch carries is a test with a
negative control behind it: the checker's refusals, the wire's index order and
its shared nodes, the empty list's elision, the element-kind rule, a count the
body cannot cover, the walk order at a `[]*T` declared before a pointer, the
baseline's `array=unbounded` with no `bound=` in both directions, and the cook
projection's sixteen-byte slot.

No code generator carries the construct, so every target refuses a unit that
declares one by name and the tool's cook surfaces refuse on the map's terms.
§2.9's and USAGE's status lines say what is now true: the front end takes the
spelling and holds every refusal, `pack` and `unpack` read and write one, and
the C++ reference lands the codec next.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames added a commit that referenced this pull request Sep 4, 2026
…#531)

The gate the struct-array walk needed: a `[]Row` whose element holds a `*Leaf`
reaches that node from inside each element, in index order, so two elements
naming one node hold one node and a null slot stays null. The control that
drops the list from the struct-array case goes red on the sharing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames added a commit that referenced this pull request Sep 4, 2026
RefuseTableLists, UnitHasList and ListFieldsOf had no caller: the port refusal
lives in compiler/tableslists.go beside the map's, and the two gates the
codegen would use land with the codegen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames added a commit that referenced this pull request Sep 4, 2026
…531)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames added a commit that referenced this pull request Sep 4, 2026
…531)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames added a commit that referenced this pull request Sep 4, 2026
…arrier (#531)

golangci-lint named registerListCarrier unused, which it is: no code
generator carries the construct, so the registry the map's file keeps lands
here with the first carrier and the refusal names the target directly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames added a commit that referenced this pull request Sep 4, 2026
…sals (#531)

The `[]T` and `[]*T` spellings reach the IR as ArrayList, the near-miss
spellings `[..]T` and `[0..]T` are refused by name with `[]T` as the fix, and
arrays of arrays are refused where a second bracket stands. The construct's
own refusals land beside the element's: a `type` body, a union arm, `?[]T`, a
specified default, a qualification, and the three claimed names
`<Table><Field>{Add,Each,Erase}`. `TableList` joins the unit-level runtime
claim. Every port refuses a unit that declares one, naming the fields and cpp
as the carrier, and the tool's cook surfaces refuse one on the map's terms.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames added a commit that referenced this pull request Sep 4, 2026
…ions (#531)

The wire engine writes and reads a `[]T` as the kind 14 body a `[..N]T`
writes: the same element kind, the same count, the same elision of an empty
one. A count above the int32 storage cap is the refusal LoadBuilder answers
NULL for, and the slots are grown against the body's own length so a count no
body can cover allocates nothing. `clamped` cannot fire on the count, because
there is no bound to clamp against.

The text form is the JSON array it already was, with every element the text
carries read. The node walk reaches a list at its field's position, in index
order, so a `[]*T` declared before a pointer field numbers the shared node
first. The baseline renders `array=unbounded` with no `bound=`, which is what
makes a bound added warn and a bound removed pass, and the cook projection
renders `kind=14 array=unbounded elem=` the element's own storage size beside
the sixteen-byte slot's `size=`.

Proved end to end through `pack` and `unpack` over §2.9's own example, and the
same content under `[..8]T` packs to byte-identical bytes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames added a commit that referenced this pull request Sep 4, 2026
…531)

Every claim §2.9 makes about the halves this branch carries is a test with a
negative control behind it: the checker's refusals, the wire's index order and
its shared nodes, the empty list's elision, the element-kind rule, a count the
body cannot cover, the walk order at a `[]*T` declared before a pointer, the
baseline's `array=unbounded` with no `bound=` in both directions, and the cook
projection's sixteen-byte slot.

No code generator carries the construct, so every target refuses a unit that
declares one by name and the tool's cook surfaces refuse on the map's terms.
§2.9's and USAGE's status lines say what is now true: the front end takes the
spelling and holds every refusal, `pack` and `unpack` read and write one, and
the C++ reference lands the codec next.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames added a commit that referenced this pull request Sep 4, 2026
…#531)

The gate the struct-array walk needed: a `[]Row` whose element holds a `*Leaf`
reaches that node from inside each element, in index order, so two elements
naming one node hold one node and a null slot stays null. The control that
drops the list from the struct-array case goes red on the sharing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames added a commit that referenced this pull request Sep 4, 2026
RefuseTableLists, UnitHasList and ListFieldsOf had no caller: the port refusal
lives in compiler/tableslists.go beside the map's, and the two gates the
codegen would use land with the codegen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames added a commit that referenced this pull request Sep 4, 2026
…531)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames added a commit that referenced this pull request Sep 4, 2026
…531)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames added a commit that referenced this pull request Sep 4, 2026
…arrier (#531)

golangci-lint named registerListCarrier unused, which it is: no code
generator carries the construct, so the registry the map's file keeps lands
here with the first carrier and the refusal names the target directly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames added a commit that referenced this pull request Sep 4, 2026
…nd text (#531) (#548)

* wip: unbounded arrays, the front end and the ir (uncommitted at the stall)

* tool: the unbounded-array front end, the IR facts and the ported refusals (#531)

The `[]T` and `[]*T` spellings reach the IR as ArrayList, the near-miss
spellings `[..]T` and `[0..]T` are refused by name with `[]T` as the fix, and
arrays of arrays are refused where a second bracket stands. The construct's
own refusals land beside the element's: a `type` body, a union arm, `?[]T`, a
specified default, a qualification, and the three claimed names
`<Table><Field>{Add,Each,Erase}`. `TableList` joins the unit-level runtime
claim. Every port refuses a unit that declares one, naming the fields and cpp
as the carrier, and the tool's cook surfaces refuse one on the map's terms.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* tool: the unbounded array on the wire, in the text and in the projections (#531)

The wire engine writes and reads a `[]T` as the kind 14 body a `[..N]T`
writes: the same element kind, the same count, the same elision of an empty
one. A count above the int32 storage cap is the refusal LoadBuilder answers
NULL for, and the slots are grown against the body's own length so a count no
body can cover allocates nothing. `clamped` cannot fire on the count, because
there is no bound to clamp against.

The text form is the JSON array it already was, with every element the text
carries read. The node walk reaches a list at its field's position, in index
order, so a `[]*T` declared before a pointer field numbers the shared node
first. The baseline renders `array=unbounded` with no `bound=`, which is what
makes a bound added warn and a bound removed pass, and the cook projection
renders `kind=14 array=unbounded elem=` the element's own storage size beside
the sixteen-byte slot's `size=`.

Proved end to end through `pack` and `unpack` over §2.9's own example, and the
same content under `[..8]T` packs to byte-identical bytes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* tool: the unbounded array's gates, and the backend status it leaves (#531)

Every claim §2.9 makes about the halves this branch carries is a test with a
negative control behind it: the checker's refusals, the wire's index order and
its shared nodes, the empty list's elision, the element-kind rule, a count the
body cannot cover, the walk order at a `[]*T` declared before a pointer, the
baseline's `array=unbounded` with no `bound=` in both directions, and the cook
projection's sixteen-byte slot.

No code generator carries the construct, so every target refuses a unit that
declares one by name and the tool's cook surfaces refuse on the map's terms.
§2.9's and USAGE's status lines say what is now true: the front end takes the
spelling and holds every refusal, `pack` and `unpack` read and write one, and
the C++ reference lands the codec next.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* tool: a list of tables is descended for the edges inside its elements (#531)

The gate the struct-array walk needed: a `[]Row` whose element holds a `*Leaf`
reaches that node from inside each element, in index order, so two elements
naming one node hold one node and a null slot stays null. The control that
drops the list from the struct-array case goes red on the sharing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* tool: the unused half of the wip IR file, removed (#531)

RefuseTableLists, UnitHasList and ListFieldsOf had no caller: the port refusal
lives in compiler/tableslists.go beside the map's, and the two gates the
codegen would use land with the codegen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: the comparison table says which half of the construct is live (#531)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test: the list text case ranges over int, as the modernize pin wants (#531)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* tool: the list refusal carries no carrier registry until there is a carrier (#531)

golangci-lint named registerListCarrier unused, which it is: no code
generator carries the construct, so the registry the map's file keeps lands
here with the first carrier and the refusal names the target directly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
rowan-claude pushed a commit that referenced this pull request Sep 5, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames added a commit that referenced this pull request Sep 5, 2026
* tables: the C++ codec for unbounded arrays, work in progress (#531)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* tables: cook-check refuses a map slot by name where its scan meets one

The unit-level map refusal at cook-check refused every cook from a unit
that declares a map anywhere, which is the whole tables/lists corpus. The
scan now refuses the slot it cannot bound, naming the field, the reference
that reads it and schema#380 as the PR that lands the clause, and a cook of
a map-free root in the same unit checks as any other does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* tables: the list controls run as one target, and the element-kind control's script survives make

A blank line inside the aggregate target's continuation ended its
dependency list, and the element-kind control's sed script carried commas
and an unbalanced parenthesis that make's call split and cut.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* tables: pin the header goldens of the lists corpus

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* tables: re-pin the C++ table goldens under the list adapters and the §8.1 columns

Every list-free unit gains the JSON walk's three list adapter stubs, as it
carries the map's, and the map units take the descriptor columns §8.1 now
names, the TableRefuseReason enum and the extent carve.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* tables: the list test's loop ranges over the count

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* tables: the cold read's five changes on the list codec

The update-goldens recipe's for-loop is whole again: the blank line
after its first continuation is gone, and GNU Make 3.81 runs it through.

The preorder negative control sabotages both writers of a list whose
element holds a map, the pack's extent walk and the cook's extent writer,
and runs schema cook-check on the cook the sabotaged gate wrote. Both of
the instruments the page names go red: the pinned list_of_maps_cook byte
compare, and the containment clause, the array leaves the node.

The page states the cook-check stopgap under 7.4 item 4: the tool refuses
a map slot by name where its scan meets one until schema#380 lands the
clause, and the #380 entry in 15 names that clause as the piece owed.

The map's wire extent tests the int32 cap before the body's L, so a map
count above the cap answers count_over_extent_cap exactly as a list's
does, one rule for both constructs. The measure-refusals battery builds a
synthetic Army wire with the map's count as its knob and asserts both
reasons, with a clean map-holding wire beside them. The eight Table.h
goldens the emission moves are re-pinned.

The em dash in TestToolRefusesMapsByName's comment is a period.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant