tool: unbounded arrays in the front end, the IR and the tool's wire and text (#531) - #548
Conversation
…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>
…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>
…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>
…#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>
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>
…531) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…531) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…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>
20d62c7 to
0114252
Compare
|
Rulings on the silences: (1) attributes right of the bar on a []T field qualify the ELEMENT exactly as they do on a [..N]T field, so []fixed(16,16) | min = A, max = B and []int128 | min, max are spelled that way; §11's refusal is of a COUNT bound on a list, and the page is restated to say so in the C++ codec PR. (2) A list renders kind=14 in the cook projection as the page says; a bounded array keeps rendering its element's kind; both stated. (3) []?T unreachable by grammar is stronger than refused and stands. The C++ codec (the TableList runtime, the five element classes, the extent and pack, the cook write, the JSON walk, the §8.1 descriptor migration with the map's emission, the builder's three, the nine-row corpus and goldens) is the next PR for this row, and the #366 cell moves with it. |
SPEC-TABLES.md §2.9 specifies unbounded arrays ahead of their implementation. This lands the front end, the IR and the tool's WIRE and TEXT halves, and it does not land a code generator: every target refuses a unit that declares a
[]Tby name, and §2.9's and USAGE's status lines now say exactly that. The C++ reference's codec is the next PR.What lands
The front end takes the spelling.
[]Tand[]*Treach the IR asArrayList. The element set is[..N]T's exactly, so[]uint128,[]bool,[]Grade,[]*Tand[]Tableare legal and[]bits(N),[]string(N),[]map[K]Vand[]*bytesare refused on the bounded array's own diagnostic.Every refusal §11 names. A
[]Tin atypebody; a[]Tor[]*Tas a union arm, with the table wrapper named;[..]Tand[0..]T, each naming[]Tas the fix;?[]T; a specified default; a qualification;[..N][]T,[N][]Tand[][]Tas arrays of arrays; a table holding a[]of itself by value; and the three claimed names<Table><Field>{Add,Each,Erase}.TableListjoins the unit-level runtime claim.The wire. A
[]Twrites the kind14body a[..N]Twrites — the same element kind, the same count, the same bytes — and an empty one elides under §3's by-value rule. A count above theint32storage cap is the refusalLoadBuilderanswers NULL for, and the slots are grown against the body's own length so a count no body can cover allocates nothing.clampedcannot fire on the count. The node walk reaches a list at its field's position, in index order, so a[]*Tdeclared before a pointer field numbers the shared node first.The text. The JSON array it already is, with every element the text carries read,
nullakind_mismatchat a value element and a pointer slot at a[]*T, and&nodesharing intact.The projections. The baseline renders
array=unboundedwith nobound=, which is what makes a bound added warn and a bound removed pass in silence (§18.2). The cook projection renderskind=14 array=unbounded elem=<the element's own storage size>beside the sixteen-byte slot'ssize=.Proof, end to end
§2.9's own example packs and unpacks through the tool,
&nodesharing and null slot included, and the same content under[..8]Tpacks to byte-identical bytes —list_migrates's claim, as a unit test.Gates and their negative controls
Every gate below was proved by breaking it and watching the named test go red.
[]Tand[..N]Tare one wire[]*Tgrouped after the pointer fieldsTestListWireIsTheBoundedArrayWire: 128 bytes against 165, and they differTestListRoundTrip: the round trip did not reproduce the wireTestListRoundTrip: two slots naming one node hold one node[]*Tbefore a pointerTestListWalkOrder: both records ride: -1 and 17TestListOfTablesReachesTheirEdges: a node named from two elements is one nodeTestListEmptyElides: an empty list rides NO bytes at all: 49TestListRoundTrip: report={… Clamped:3 …}TestListCountOverLength: a count the body cannot cover is framing damageTestListElementKindMismatch: an element kind that disagrees is one kind_mismatch and no damagearray=unboundedwith nobound=array=bounded bound=0TestUnboundedRendersWithNoBound,TestAddingABoundWarnsTestTheUnboundedArrayProjectsAsAnArrayWithNoBoundTestEveryTargetRefusesAList: --lang c emitted for a unit declaring an unbounded arrayrefuseListsdropped fromtarget_cpp.gocheckListSpellingreturns trueTestTableRefusals: eight subtests, expected a diagnostic containing …, got noneWhat is NOT here, and why
The corpus
tables/lists,make tables-list-measure-refusals, the clamp control at 100,000, and every negative control §2.9 names against a generated codec all need a backend that emits one. So doesTableRefuseReasonandLoadMeasure's reason out-parameter, which §6.5 states are owed as schema#523. None of it is stubbed here:refuseListsnames the state plainly and the status lines say the same.The zero-cost law, and why no benchmark rides here
The owner's 2026-09-05 law binds anything a PR adds that REPORTS — counters, reasons, descriptors — to zero measured cost on the read and write paths of both wires. This PR adds none of them. It adds compile-time refusals, IR facts and handling inside the tool's own engines; it adds no counter, no
TableRefuseReason, no descriptor column, and it changes no emitter. ThegeneratedCI gate byte-compares the checked-in generated tree and passes, which is what says no generated read or write path moved. The benchmark sitting is owed by the PR that lands the codec.The #366 matrix
No cell moves. #366's matrix is per LANGUAGE — fixed class, variable class, text form, reflection, block, cook, build version — and no language gained a feature here. The construct reaches that matrix with the C++ codec.
The lock
bench/LOCKcarries no prefixes as of #545, and nothing here would have matched one anyway: the construct lives entirely outsideinternal/codegen/c,internal/codegen/cppand the generated C and C++ trees. CI'scpp-lockjob passes.Silences and contradictions found on the page
| maxon a[]Tagainst[]fixed(I, F)and[]int128. §11 refuses| maxon an unbounded array and §2.9 says the element set is[..N]T's exactly, nothing held back. Butfixed(I, F)and signedint128require| min, max(SPEC.md §4.3), so under §11's literal reading[]fixed(16,16)is unspellable in both directions: bare it is refused for missing bounds, qualified it is refused for carrying them.[..8]fixed(16,16) | min = -1, max = 1stays legal. This PR implements §11 literally and refuses the whole qualification vocabulary exceptwasandjson, on the map's own precedent. A ruling is owed: either the two element types leave the set by name, or| min/| maxare admitted as the ELEMENT's range where they already are on[..N]Tand only a count bound is refused.kind=on the cook projection's array lines. §20's table says an unbounded array's field line carrieskind=14, as a map's does. Bounded and fixed arrays render the ELEMENT's kind there today (kind=13,kind=30), which §20.2's own grammar does not contradict. This PR follows the page and renderskind=14for a list, which leaves[]Placementatkind=14and[..8]Placementatkind=13on otherwise identical lines. Worth a ruling on whether the bounded rows should move too.[]?Thas no spelling. §2.9 and §11 refuse[]?Tas an array of?T. The grammar binds?before the bracket, so the only spelling reachable is?[]T, which is refused by name. There is nothing else to refuse.The by-value cycle
[]Selfneeds no new refusal. A list's storage is a reference, so its holder is not infinite — but §2.9 states it is refused as a by-value cycle, and the existing composition-cycle walk already refuses it naming the cycle. Nothing was added.Closes nothing; #531 stays open for the backend.