Skip to content

tables: the list-walk gate derives the list-free set from the schemas (red on main since #662) - #674

Merged
rowan-claude merged 1 commit into
mainfrom
fix-list-walk-gate
Sep 7, 2026
Merged

tables: the list-walk gate derives the list-free set from the schemas (red on main since #662)#674
rowan-claude merged 1 commit into
mainfrom
fix-list-walk-gate

Conversation

@rowan-claude

Copy link
Copy Markdown
Collaborator

make test has been red at tables-json-list-walk on main since #662 merged
(e123f1b). This is the instrument, not the emitter.

Red first, on main's tree

Clean clone of origin/main (dcdd5cd), make bin/schema,
make build/tables-generated/.stamp, then:

$ make tables-json-list-walk
LIST-WALK GATE FAILED: the list half reached the list-free unit build/tables-generated/maps/CellsTable.cpp
make: *** [tables-json-list-walk] Error 1

The judgment

The gate's PREMISE is right. §13.5 put the JSON walk in <Base>Table.cpp
because it is non-template, non-constant code every consumer would otherwise
re-parse, and the list half of it rides only where a list rides. A list-free
unit pays nothing for the text form's list surface. That is §2.2's zero-cost
property holding for the text form, and it did not move.

The gate's INSTRUMENT was wrong. It named the list-free set by directory:

for f in build/tables-generated/examples/*Table.cpp \
         build/tables-generated/pointers/*Table.cpp \
         build/tables-generated/maps/*Table.cpp; do ...

That is a COPY of a fact the schemas own. #662 added
Spans { tracks map[uint8][]Item } to tables/maps, which makes the whole
unit list-bearing; the emitter emitted the half into all ten of its .cpp
files, correctly, and the copy standing in the recipe reported correct emission
as a leak. #662's own body records the movement ("the four <Base>Table.cpp
files gained the JSON LIST WALK") and did not carry it into the recipe. A
copied fact rots on the day the thing it copies changes.

The instrument's new shape

internal/listwalk asks the compiler's own IR.

  • The fact. A unit is LIST-FREE when no table in its closure carries an
    unbounded array. ir.ListFields walks ir.TableClosure and names every
    []T, and a map's generated entry is a real table of that closure (§2.8),
    so map[K][]T reaches the answer with no clause of its own. No directory
    name appears anywhere in the gate.
  • The corpus. Read out of the Makefile's tables_generate define, which
    is the list that GENERATES the tree the gate scans. One list, read by both
    halves, on internal/viewlisting's rule: a unit added to the build is under
    the gate the same day, list-bearing or list-free.
  • The assertion, per unit. A list-bearing unit's every generated .cpp
    carries the half; a list-free unit's carries none of it; the half is ONE
    half, the same bytes wherever it rides.
  • What it does NOT ask. The C++ emitter. internal/codegen/cpptable has
    its own unitHasList, and a gate that shared it would move with every
    sabotage of it and stay green. Two of the three controls below patch exactly
    that gating.

Driven from the Makefile the way tables-view drives internal/viewlisting:
SCHEMA_LIST_WALK_DIR points at the generated tree, SCHEMA_LIST_WALK_UNITS
narrows a control's run, and without either the test still derives the answer
for every unit of the corpus, so go test ./... keeps saying something about
the fact the gate is built on.

Green, and wider

$ make tables-json-list-walk
tables list-walk gate: one list half, byte-identical in 21 .cpp files across 5
list-bearing units, and in none of the 42 .cpp files of the 32 list-free ones,
the set derived from the schemas

The scan widened. It held five .cpp files in one directory and scanned three
directories for the absence; it now holds all 63 generated .cpp files of the
corpus. arms, rt1 and rt2 carry the list half and were held by neither
clause.

The controls

make tables-json-list-walk-negative-controls, new, and in make test beside
the gate.

PLANTED puts the half into a list-free unit's emitted .cpp in a throwaway
copy. Nothing is generated and no emitter is patched, so it holds the SCAN
alone:

LIST-WALK GATE FAILED: the list half reached the list-free unit
  build/list-walk-planted/examples/TablesTable.cpp.
  No table in tables/examples's closure carries an unbounded array

UNGATED removes the emitter's own gating through a Go overlay
(listAdapters := tableJsonNoListAdapters becomes the real half), so every
unit's .cpp carries it. This is the control the gate exists for: it is what
§13.5's ruling costs a list-free consumer the day the gating goes.

LIST-WALK GATE FAILED: the list half reached the list-free unit
  build/list-walk-ungated-tree/examples/GuardedTable.cpp.
  No table in tables/examples's closure carries an unbounded array
  ... 7 named files

DROPPED is the other half of that switch: the list half is never emitted,
so a list-bearing unit is left with the stub. A gate that only refused leaks
would stay green here.

LIST-WALK GATE FAILED: no list half in
  build/list-walk-dropped-tree/lists/HoldersTable.cpp.
  tables/lists declares Album.photos, Army.squads, Bytes.data, Floats.values,
  Ints.values, Mixed.bounds, Mixed.grades, Mixed.hits, Mixed.perms, Row.items,
  Save.log, Save.placements, Save.scores, Sheet.rows, Unbounded.items
  ... 5 named files

Each control narrows with SCHEMA_LIST_WALK_UNITS, so its red is its own and
not thirty-five absent trees. A sabotage that patches nothing is itself a
failure, and a red that is not on the named clause is a failure too.

Named follow-on, not folded in

The map-walk gate keeps the same hand-kept shape. It is narrow rather than
red: its byte compare covers the ten .cpp files of maps/ while seventeen
carry a map half (lists/'s five, rt1, rt2), and its absence scan names
examples/ and pointers/ only. Nothing is wrong with what it asserts today;
it is one corpus change away from the failure this PR repairs, and
internal/listwalk generalizes to it in a predicate. Left for its own issue
rather than widened into a red-fixing PR.

Test

make tables-json-list-walk and make tables-json-map-walk green.
make tables-json-list-walk-negative-controls red on all three controls, by
name. go test ./... green across 31 packages, including
internal/ci's TestEveryPackageTheBuildRunsIsCommitted, which is what makes
the new package's tracking a gate rather than a habit. make test whole to the
absent-toolchain stop: it reaches build-conformance-cs and stops on
dotnet: command not found (Error 127), with the list-walk gate and its three
controls green above it and no other failure in the log.

🤖 Generated with Claude Code

The gate asserts §13.5's zero-cost promise for the text form: the JSON
walker's list half is a body of non-template code, so it rides only in a
unit that declares an unbounded array and a list-free unit pays nothing
for it. The premise is right. The instrument named the list-free set by
directory, which is a copy of a fact the schemas own, and #662 changed
that fact: `Spans tracks map[uint8][]Item` makes the maps unit
list-bearing, the emitter emits the half into it correctly, and the gate
reports correct emission as a leak.

    LIST-WALK GATE FAILED: the list half reached the list-free unit
    build/tables-generated/maps/CellsTable.cpp

internal/listwalk asks the compiler's own IR instead. A unit is list-free
when no table in its closure carries an unbounded array, and a map's
generated entry is a table of that closure (§2.8), so `map[K][]T` reaches
the answer with no clause of its own. The corpus is read out of the
Makefile's `tables_generate` define, which is the list that generates the
tree the gate scans, so a unit added to the build is under the gate the
same day. The derivation never asks the C++ emitter, whose own
`unitHasList` the controls sabotage.

The scan widened with the fix: 21 list-bearing .cpp files across five
units where it held five in one directory, and none of the half in the
42 .cpp files of the 32 list-free units where it scanned three
directories. arms, rt1 and rt2 were list-bearing and unheld.

Three negative controls, each red by name. PLANTED puts the half into a
list-free unit's emitted .cpp and holds the scan alone. UNGATED removes
the emitter's `if anyList` so every unit carries the real half, which is
what §13.5's ruling costs a list-free consumer the day the gating goes.
DROPPED is the other half of that switch: a list-bearing unit left with
the stub, which a gate that only refused leaks would pass.

The map-walk gate keeps the same hand-kept shape and is narrow rather
than red: its byte compare covers ten .cpp files of the seventeen that
carry a map half, and its absence scan covers two directories. Owed
separately.

Co-Authored-By: Claude Opus 5 <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