tables: the C++ codec for unbounded arrays (#531) - #562
Merged
Merged
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
…trol'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>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…§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>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
gafferongames
added a commit
that referenced
this pull request
Sep 5, 2026
…cap check gets its control (#564) Main dc6ce20 is red at tables-maps-fit-negative-control on every make test leg. PR #562 rewrote TableMapWireExtent: the L bound line now assigns the reason count_over_length, and an int32 cap test sits before it. The fit control's sed pattern still named the old line, so the sabotage patched nothing. Repointing it was not enough: the map gate had no row asserting count_over_length by name, and the hostile count the reader row used sits past INT32_MAX, so the cap test refused it before the L check was reached and the gate stayed green under the sabotage. The map gate gains test_measure_refusals in the list gate's shape: a Fleet wire whose map count is 100000 against a short body answers -1 with count_over_length, a count of 0x80000000 answers count_over_extent_cap, and a clean wire measures and loads. It prints one summary line under `schema_test_maps measure-refusals`, wired into make test as tables-maps-measure-refusals beside the list one. The fit control targets the new line, keeping its shape, and tables-maps-cap-negative-control sabotages the INT32_MAX line beside it. Two more map controls had patterns #562 moved out from under them: textorder named a map_at call the JSON walk no longer makes, and unreached named TableMapUnreachedEmpty, which extent.go's TableExtentUnreachedEmpty replaced. Both point at the emitted lines as they stand, and every one of the eleven map controls turns the gate red on a CHECK. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
rowan-claude
pushed a commit
that referenced
this pull request
Sep 5, 2026
…e batch, and schema#571 (#530) Merges origin/main (#562, #564, #573) and lands the page's second round (#575) on both engines: a ranged base encoded by its kind's signedness, the quantized f32 row as min, max and res in float32 with the step count and the width derived by SPEC.md §4.3's rule, quantization in float32 with two roundings on each side, a refused first announcement terminal, and the Envelope over a union of the three messages as the batch vector (244 bytes, the singles at 52, 148 and 43, the announcement at 361). Unbounded arrays ride the C++ message wire with the count the data decides and the elements carved from the node's extent. schema#571's six findings are fixed with a vector and a control each: surplus elements decode into scratch (M1), a ranged 128-bit value is one arithmetic for measure, write and read (M2), a width above the kind's domain is refused in both shape parsers and the bit reader (M3), the Go engine writes the count as its offset from the minimum (M4), one quantization rule (M5), and the C++ reader clamps while wide and narrows after (M6). The bases unit (test/tables/Bases.schema) holds the vectors in both engines. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
rowan-claude
added a commit
that referenced
this pull request
Sep 7, 2026
Twice on 2026-09-05 main's certification went red on a merge whose pull-request run was 20 of 20 green, and one of the two was a stale sabotage pattern: #562 rewrote the line the maps fit control aimed at, the sabotage patched nothing, and the control refused where only certification could see it. The controls were doing their job; nobody was reading them until after the merge. The leg is two jobs. negative-controls-matrix runs the enumeration gate and prints the plan as a matrix. negative-controls fans out over it, builds bin/schema and the generated tree once, and runs its group's controls in one `make -k` invocation so a control that refuses does not hide the ones behind it. Red comes from two places: make's exit status, and a scan of the log for the controls' own refusal lines, which puts the sentence a reader needs into the run's annotations instead of leaving it a thousand compiler lines deep. The scan was held against the 159 real control logs and matches no passing one. The per-port groups install their SDK exactly as the conformance jobs above do, keyed on the row's field rather than on a language name, so a group is a row in make/negative-controls.json and no edit here. Also a prerequisite this leg found by running a control on its own for the first time: conformance-negative-control-absent's second half runs the harness over a substituted registry naming the Go driver, which execs build/conformance-go, and the target did not name it. Inside `make test` the Go leg is already built, which is why the omission stayed invisible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rowan-claude
added a commit
that referenced
this pull request
Sep 7, 2026
… Makefile (#563) (#659) * ci: enumerate every negative control from the Makefile (#563) A negative control proves a gate is watching by breaking what the gate watches and requiring the gate to go red. Each one in this tree already refuses when its sabotage patches nothing, so a control whose pattern has drifted off the line it aims at says so. Until now the only reader of that refusal was `make test`, which runs after a merge. This is the half that makes "every control" mean what it says. tools/negativecontrols reads the Makefile and every file the Makefile includes, collects each explicit target whose name carries `negative-control`, and holds that set against make/negative-controls.json, the plan the pull-request leg's matrix comes from. The test fails on a difference in either direction: a control the makefiles define and the plan does not carry, and a control the plan names and no makefile defines. An exclusion needs a non-empty reason, so nothing leaves the leg silently. The reader follows the tree's own include lines rather than a glob typed here, and it reads the rule heads a makefile actually writes: several targets on one head, a head continued over a backslash, `.PHONY` lines, and `define` blocks and `:=` assignments that only look like rules. Both readings of this tree agree at 159 controls. The plan groups those 159 by what a runner has to install. Eight groups are toolchain families; two more, wire-fuzz and message-form, are base-toolchain families split out on measured cost. Closes #563 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci: every negative control runs on pull requests (#563) Twice on 2026-09-05 main's certification went red on a merge whose pull-request run was 20 of 20 green, and one of the two was a stale sabotage pattern: #562 rewrote the line the maps fit control aimed at, the sabotage patched nothing, and the control refused where only certification could see it. The controls were doing their job; nobody was reading them until after the merge. The leg is two jobs. negative-controls-matrix runs the enumeration gate and prints the plan as a matrix. negative-controls fans out over it, builds bin/schema and the generated tree once, and runs its group's controls in one `make -k` invocation so a control that refuses does not hide the ones behind it. Red comes from two places: make's exit status, and a scan of the log for the controls' own refusal lines, which puts the sentence a reader needs into the run's annotations instead of leaving it a thousand compiler lines deep. The scan was held against the 159 real control logs and matches no passing one. The per-port groups install their SDK exactly as the conformance jobs above do, keyed on the row's field rather than on a language name, so a group is a row in make/negative-controls.json and no edit here. Also a prerequisite this leg found by running a control on its own for the first time: conformance-negative-control-absent's second half runs the harness over a substituted registry naming the Go driver, which execs build/conformance-go, and the target did not name it. Inside `make test` the Go leg is already built, which is why the omission stayed invisible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci: the first stale control the leg found, and lint on the reader (#563) tables-block-layout-model-negative-control refused on the leg's first run with "NEGATIVE CONTROL FAILED: C# went red, but not on the layout check", and it was right: the C# build had failed on `CS0246: The type or namespace name 'Blockhome' could not be found`, not on the layout check the control exists to watch. The C# half overrides BlockGeneratedDir alone, so the project's BlockHomeGeneratedDir keeps its default and the blockhome sources have to be on disk, and the target named only bin/schema. The padding and pitch controls beside it already name build/tables-generated-cs/.stamp; this one now does too. Inside `make test` an earlier leg had generated those sources already, which is why nothing said so until every control ran on its own. The reader's four golangci-lint findings, all mechanical: SplitSeq and FieldsSeq for the range loops, CutSuffix for the line continuation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci: every negative-control job fits the owner's two-minute rule (#563) The rule (Glenn, 2026-09-07): "if we have any CI that runs per-commit it needs to finish 1 minute, 2 minutes max. Anything longer than that needs to be nightly or manually triggered as needed." It is a rule about a JOB, and a matrix row is a job, so every row of this leg is cut to fit it rather than the leg as a whole being cut to fit it. MEASURED on this branch's own pull-request run (34102397965), before this change: base 8:57, wire-fuzz 7:13, cs 2:46, message-form 2:36, conformance 2:31, block-fuzz 1:59. Five of the eleven rows were over the rule and a sixth was one second under it. WHAT MOVED. The plan now carries 34 pull-request groups instead of 11, cut where the measurement cuts them and not where the family names do: - base splits ten ways. The map gate is 17 controls at 4 s each on the author's machine, so it runs as base-maps and base-maps-keys; the conformance driver's two sabotage rebuilds are 45 s and 47 s of runner time each, so they are base-conformance and base-conformance-absence; the rest are the packet families, the list gate, the arm gate, the cook, the keyed controls and what is left. - wire-fuzz splits eleven ways, one control per job for ten of them. Each rebuilds the compiler under a source overlay and then fuzzes the sabotaged wire, 55 s to 75 s on the runner, so no two of them fit together; the four pinned-vector oracles are cheap and share the eleventh. - cs splits four ways. The C# layout check is the instrument for three block controls at 39 s, 31 s and 29 s. - block-fuzz splits in two, at 48 s and 33 s over two SDK installs. - conformance is gone as a group. Its one control had the bare `conformance` target as its prerequisite, which builds and runs all nine legs and needed all seven SDKs on one runner. The prerequisite is now `conformance-rust`, the rust leg and the derived manifest the control actually reads, and the control rides one SDK in rust-alloc. The soak and the audit keep `conformance`, deliberately: a NUMBER measured over a corpus whose matrix is red is a number about a defect, and this control is not a number. ONE CONTROL DOES NOT FIT AT ANY GROUP SIZE, so it does not run on the pull request at all: tables-message-form-negative-control drives 49 sabotage rows one submake each and takes 124 s where the rule is 120. It is the `nightly` group, and certify.yml runs that tier on the schedule it already carries. Its four blades stay on the pull request in `message-form`. So a group now names its tier. `when` is `pull-request` or `nightly`, `matrix` renders one tier, and the enumerator REFUSES a group that names neither: a control cannot leave the pull request without landing on the nightly, and the package's test reads both workflows to prove each runs the plan rather than a typed list. The 35 controls main gained since this branch last merged it are in the plan too, toolchain-negative-control from #660 among them, and the three family umbrellas that would re-run a split family's other half join the exclusion list with the reason. 194 controls, all of them in a group or in an explained exclusion. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ci: the two controls main gained while this branch was measuring (#563) tables-block-const-negative-control and tables-json-list-walk-negative-controls landed on main mid-run, and the enumerator did what it is for: the leg went red naming both rather than running 194 of 196. Both are cheap, 1.6 s and 3.7 s on the author's machine, and both belong beside the block and JSON refusers already in `base`, which measures 24 s as a group with them in it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ci: cut again where the first cut was inside the rule by luck (#563) MEASURED on run 34115675206, the first run of the split leg: 52 of the 54 jobs were inside the owner's two-minute rule, and the two that were not are tables-wire-fuzz-retain-negative-control at 140 s of control time and `big-endian`, which is not this leg's job and is named in this file's header. The retain control is over the rule ON ITS OWN, so grouping cannot save it: it joins tables-message-form-negative-control in the `nightly` group, which certify.yml runs. Its two siblings, the retain-class control and the retain oracle, are inside the rule and stay on the pull request, so what moves is that one pass. Eight groups came in between 100 s and 111 s, which is a rule met by luck rather than by design: `big-endian` was 1:49 when this file's header measured it and is 4:19 today, on a job nobody changed. So base-maps, base-maps-keys, base-arms, base-keyed and the oracle half of wire-fuzz are each two groups now, cut where the measurement puts the weight rather than where the family name does. 38 pull-request groups, 196 controls. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ci: three controls the naming convention hid, and an umbrella off the nightly (#563) The marker `tools/negativecontrols` reads is `negative-control`, and three controls the Makefile introduces as "Its NEGATIVE CONTROL" spell themselves `-negative`: tables-big-endian-negative, tables-hostile-negative and tables-pack-negative. The enumerator cannot see a name it does not match, so "every negative control runs on every pull request" passed over three of them, and two ran only inside `make test`. They are renamed into the convention and placed. tables-hostile-negative-control and tables-pack-negative-control go in a new base-pack group: one pack corpus, one JSON engine and a `go build -overlay` each, 19 s over the two in one invocation on a cold Go build cache with bin/schema and the generated tree already built. tables-big-endian-negative-control goes in the exclusion list. It is the body of ci.yml's big-endian job, which runs it on every pull request already, and issue #684 owns moving that job; a second s390x cross-compile on this leg proves nothing the first does not. tables-wire-fuzz-retain-negative-control leaves the nightly group for the same list. Makefile:4428 gives it no recipe: it is an umbrella over the retain oracle and retain class leaves, which wire-fuzz-message-oracle and wire-fuzz-retain-class already run. The nightly tier is one control now, and stays a group, because it is where the next control that outgrows the rule lands. Also: the negative-control leg's timeout drops from 25 minutes to 5, which is the rule enforcing itself on a leg whose rows are cut to 120 s; ci.yml no longer says certify.yml runs on every push to main (#669); and the past-tense narration on this branch's own lines is present tense. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ci: the negative-control tests hold three joins a substring scan cannot (#563) The leg's target list is only the plan while the leg EXPANDS the plan. TestTheLegRunsTheManifestAndNotATypedList greps three substrings, and a leg that typed its own include list and left the old expression in a comment satisfies every one of them. So the workflows are parsed. tools/negativecontrols carries a reader for the block-YAML subset these files are written in, and the new test asserts what each leg actually expands: `strategy.matrix` IS `${{ fromJSON(needs.<matrix-job>.outputs.matrix) }}`, the leg `needs:` that job, and that job's matrix output comes from the step that runs this tool. Both tiers, ci.yml and certify.yml. Proven red first. With ci.yml's matrix expression replaced by a hand-typed three-row include list, TestTheLegRunsTheManifestAndNotATypedList still passes, and the new test says: .github/workflows/ci.yml: the negative-controls job expands map[string]interface {}{"include":[]interface {}{...}} as its matrix, want "${{ fromJSON(needs.negative-controls-matrix.outputs.matrix) }}": a matrix written any other way is a target list this package did not enumerate A marked rule head spelled through a variable or as a pattern rule is refused by name rather than dropped. A dropped head is a control in no group, in no exclusion and in no job, with every test in the package green, which is the one outcome the package exists to prevent. And the plan's toolchain versions are held against test/conformance/*/ci.json. node 20, dart 3.13.2, java 21, otp 29.0.5 and elixir 1.20.4 are written in both files and nothing joined them; a bump in one alone runs a negative control against a runtime its own conformance leg no longer uses. dotnet is exempt by construction: the SDK version lives in .github/dotnet-version and the test refuses a group that writes one here. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ci: the four controls main gained while this branch was reading (#563) The enumeration gate on the merge ref named all four rather than running 199 of 203, which is the gate doing its job. Measured off one warm build on the author's machine and placed in the group whose corpus each already reads: tables-float-nan-negative-control 1.6 s base tables-maps-cook-check-negative-control 1.6 s base-maps tables-lists-tool-cook-negative-control 0.3 s base-lists tables-message-form-retain-negative-control 2.4 s message-form No group changes tier and none goes near the rule: base 28 s over 24, base-maps 20 s over 5, base-lists 35 s over 14, message-form 16 s over 5. base-pack's own line is restated in the same convention the rest of the file uses, off one warm build rather than off a cold Go build cache: 20 s over the two, most of it the sanitized build of the hostile-value driver. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ci: cut base-pack, the tightest job the last run measured (#563) On run 34120798793 the base-pack row was 81 s of control time in a 113 s job, which is the band this branch already cut once: a rule met by luck rather than by design. The two controls share no compile that matters. tables-pack's floor is a `go build -overlay` and a byte comparison; tables-hostile's floor is the SANITIZED build of the hostile-value driver, which nothing else in the leg builds and which is most of the 81 s. So they run as two rows, base-pack at 7 s and base-hostile at 14 s off one warm build. The map gate's count in docs/CONTRIBUTING.md is four groups, not two, which it has been since the second cut. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ci: the message form has five blades on the leg, not four (#563) tables-message-form-retain-negative-control joined the message-form group when main landed it, so the two places that count the blades say "other blades" rather than a number that moves whenever the family grows. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The second half of #531, after #548 landed the front end, the IR, the tool's wire and text halves and every backend's refusal by name. The C++ reference now carries the codec, held by the
tables/listscorpus, and the page's §2.9 status line is gone.What lands
TableListruntime: the sixteen-byte slot (a self-relative reference and anint32count), segments carved from the builder's arena in bulk, the dead bit and the live count,Addanswering NULL at the arena or at theint32cap,Eraseby the element's own pointer,Eachin index order over live elements, and a const form whosesize(),operator[]and iteration are members of the storage type.operator[]is bounds-checked in every build,NDEBUGincluded, on §2.4's rule.*T. The four writing walks run in index order and skip dead elements.measure == saveholds on every instance.Cookand byLock.schema cook-check's element-array clause (§7.4): containment, alignment, fit and no overlap against the holder's own extent, then the elements' own slots, companions and tags. The gate writes cooks from the runtime, checks them with the tool, and refuses a forgery whose list slot points past its holder.kindis the element kind,is_arrayset,array_bound = 0the tell for an out-of-line array,countedwithcount_offsetnaming the count,element_sizethe pitch, and one function column,place, serving both constructs.LoadMeasure's-1with a reason:TableRefuseReasonas a native C++ enum carried as a trailing out-parameter (TableRefuseReason * reason_out = NULL), never an exception, with the two values this PR needs,count_over_lengthandcount_over_extent_cap, emitted only by a unit that declares a map or a list. The other three values are owed as schema#523 and the page says so.tables/lists:list_empty,list_scalars,list_tables,list_mixed,list_shared,list_before_pointer,list_erased,list_of_maps,list_nestedand the migration goldenlist_migrates(one content, two declarations, one pinned wire, both read into equal values), each crossing the wire, the text and the cook intest/tables/lists_main.cpp, wire goldens pinned undertestdata/wire/tables/, header goldens undertestdata/golden/tables/lists/, the two cooks pinned beside the wires.make tables-list-measure-refusals: four synthetic wires (a count above theint32cap, a count the field'sLcannot carry, both at depth inside an element's own list) each answering-1with its own reason, a clean wire beside them that measures, and no report counter moved.2^16, decoded count checked.[]Tqualify the element as on a[..N]T(§2.9 and §11 restated), a list renderskind=14in the cook projection where a bounded array renders its element's kind (§20.2),[]?Tunreachable stands.What this PR changed on the way
schema cook-checkrefuses a MAP SLOT by name where its scan meets one, instead of refusing every cook from a unit that declares a map anywhere. The unit-level refusal made the wholetables/listscorpus uncheckable because one holder's element carries a map. The scan's refusal namesSquad.roster, the reference that reads it, and schema#380 as the PR that lands the map's clause (§7.4 item 4), which stays owed. A cook of a map-free root in the same unit checks as any other does.internal/tablecookholds the test, and the gate asserts the map-holding cook is refused on exactly that clause.$(call)split and cut. Both fixed, the aggregate target now runs all twelve.Gates
Every negative control the page names, red on a CHECK:
The checker's refusals (
type-body,[]Self, the claimed names) and the build-version rule are held incompilerandinternal/checktests.go test ./...green. The committedgenerated/tree regenerated as CI does and current: the one moved file isgenerated/bench/tables/cpp/BenchTableTable.cpp, the list-free bench unit gaining the JSON walk's list adapter stubs (TableJsonIsListanswering false) exactly as it carries the map's.The zero-cost law (#546)
The table read and write benchmarks over the existing corpus (
bench/corpus/BenchTable.schema,corpus_id b51387f36d9b59c4, 2147 wire bytes per record), before atd46eea1e(origin/main) and after at this branch, the samebench/tables/cpp/table_main.cppagainst each tree'sgenerated/bench/tables/cpp/BenchTableTable.h. The two leg binaries are byte-identical (cmpclean, 106408 bytes each): the one moved file in the bench unit isBenchTableTable.cpp's JSON walk, which the leg does not compile, and the header is unchanged. The sitting is therefore a noise reading of one machine code, and it reads as one.MacBook Air M2, 2026-09-05 08:31, five interleaved rounds (§2.4),
--round Keach, one-minute load 1.95 when the sitting started and 2.04 to 2.09 with the bench itself running (macOS, no core pinning, Spotlight had just finished indexing a worktree).Unchanged within noise: write 0.01% apart, round_trip 1.6% apart with each side's own spread wider than the gap. No cost measured, none landed.
Owed, named
[]T, beside the map's (§15).schema cook-check's map-slot clause, §7.4 item 4, schema#380.TableRefuseReasonvalues, §7's check order and §19.2's block clauses, schema#523.#366
The C++ cell for unbounded arrays moves to ✅. Every other column stays ❌, refusing by name.
🤖 Generated with Claude Code