Skip to content

tables: a float rides as its bit pattern, pinned; the tool's cook-check carries the map slot and the list halves (#480, #380) - #678

Merged
rowan-claude merged 6 commits into
mainfrom
contract-nan-cookcheck
Sep 7, 2026
Merged

tables: a float rides as its bit pattern, pinned; the tool's cook-check carries the map slot and the list halves (#480, #380)#678
rowan-claude merged 6 commits into
mainfrom
contract-nan-cookcheck

Conversation

@rowan-claude

@rowan-claude rowan-claude commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Two gaps in the C++ column's contraction pass, red first, in one PR.

Closes #480
Part of #380 (the tool's cook and uncook halves for a map stay owed there, and refuseToolMaps points at it)

1. A float rides as its bit pattern (#480)

The gap. A float32 NaN payload rides as its 32 bits in every port, and the page never said so. grep -n NaN docs/SPEC-TABLES.md found the widened rung's aside about payloads being exactly representable and nothing that made the bits a RULE; docs/PORTING.md had no row; no corpus instance carried a signalling NaN. A backend holding a float32 in a wider cell quiets one through the hardware conversion, and the byte identity nine languages hold over one corpus fails on exactly those values.

The red. The tool's own cook is one such backend. Against the new pin, before the fix:

the signalling slot holds 0x7FC00001 and the pattern is 0x7F800001: the cook converted it
the negative slot holds 0xFFE5A5A5 and the pattern is 0xFFA5A5A5: the cook converted it
the wire did not survive the cook: 127 bytes back, 127 pinned

internal/tablecook/region.go wrote math.Float32bits( float32( cell.F ) ) and uncook.go read float64( math.Float32frombits( ... ) ) — a float64 cell with a conversion at each end.

The fix.

  • The sentence. docs/SPEC.md §4.3 gains a wire-model bullet and its float32/float64 rows say it; docs/SPEC-TABLES.md §3 gains the paragraph beside the kind-10/11 payload row.
  • The row. docs/PORTING.md M21, with its nine cells: cpp and the tool carried, the eight ports ❌ #366.
  • The pin. test/tables/F1.schema and F2.schema (the same root one generation on, every float32 respelled float64 — §4's float rung). testdata/wire/tables/floats_nan.bin carries a signalling NaN 0x7F800001, a NaN with a mantissa past the quiet bit 0x7FC0DEAD, a negative signalling one 0xFFA5A5A5, two signalling array elements, and two double patterns. test/tables/main.cpp writes it and reloads it with every other golden, so the big-endian leg covers it.
  • The claim and its control. test/tables/floatnan_main.cpp asserts the writer's bytes, the identity read and the widened read. make tables-float-nan-negative-control puts the hardware conversion back in TableWidenF32 through an overlay, regenerates only F1/F2 from the sabotaged emitter, and the pin goes red on the payload: 0x7ff0000020000000 read back as 0x7ff8000020000000, the quiet bit the conversion set.
  • Both engines pass the pin. The Go oracle (TestOracleCarriesTheFloatBitPattern, TestOracleWidensTheFloatBitPattern) and the tool's cook (TestACookCarriesAFloatBitPattern). WidenF32/NarrowF32 are exported from internal/tablewire so one pair serves both.

The text form is unreachable by design. #480 named wire, text and cook rows; the text writer refuses every non-finite float and the reader counts a NaN token as a kind mismatch (§16.2), so there is no text row to carry, and M21 says so.

The eight ports are not this PR's. Their legs are a row on #366, and M21's cells say so.

2. #380's residue: the tool's cook-check and the list halves

The red, quoted from the tree.

internal/tablecook/check.go:212, on the cook the C++ reference wrote for Army:

node 1 (Army at offset 0): Army.squads: Squad.roster: a map slot, and `cook-check`
carries no map-slot clause yet (docs/SPEC-TABLES.md §7.4): the C++ reference reads
the cook (--lang cpp), and the tool's clause is schema#380's next PR

compiler/tableslists.go:58, through schema uncook over the same file:

unit declares an unbounded array in a table closure (...): the tool's WIRE and TEXT
halves carry the construct and `cook-check` reads one, and its COOK half does not,
so this command would lay out a region short of the element arrays rather than
refusing. The C++ reference carries the cook (--lang cpp)

The map-slot clause. The four clauses a list's slot already took are one helper now, placedArray, because the two slots are one shape and lists and maps are one population in a node's extent. On top of it the map takes the fifth: the keys read ascending with no repeat, at the key's own signedness for an integer and bytewise against the used length for a bounded string.

The list's cook and uncook halves. internal/tablecook/extent.go is the node extent in the tool: one pre-order carve walk with its writing half switched off for the measure, so the two cannot drift. Layout sizes an extent-bearing node from the instance it walked, Write fills each slot's sixteen bytes and holds the extent written to the extent measured, Uncook reads them back.

The two writers meet. make tables-lists-tool-cook has the generated SaveCook land on the tool's own bytes exactly, 352 bytes, in both byte orders, over test/tables/L1.schema — a list-bearing unit with no map in it. The CLI round trip runs beside it.

Controls. tables-maps-cook-check-negative-control (the shared containment sabotage must turn the map's test red beside the list's, and the keys' ascending clause gets its own, because a shared control would leave the fifth clause untested) and tables-lists-tool-cook-negative-control (the carve lays an array unaligned).

The page lines retired. §7.4 clause 4 loses "Until schema#380 lands this clause in the tool"; §21's maps bullet loses "which is the one piece still owed"; §21's unbounded-arrays bullet loses "the tool's COOK and UNCOOK halves are owed beside the map's".

Still owed, and unchanged: the tool's COOK and UNCOOK halves for a MAP. A map adds the sort, the entry array's key order and the two reader events to the extent the list's halves already carry. refuseToolMaps still names a map-bearing unit at those two surfaces, and the page now says that is what remains.

Goldens

make update-goldens adds exactly one file: testdata/wire/tables/floats_nan.bin. No conformance json, report or cook-write fixture moved.

The six maps headers make update-goldens once wanted to move, and the two gates (tables-json-list-walk, tables-block-zero-cost) that were red on this branch's base, were main's: #668 re-pinned the headers and #674 derives the list-walk set from the IR. Merged with main both gates exit 0; on the bare branch head they still exit 2, which is the base, not this diff.

Everything this PR does own was run green locally: go test ./..., make check, make fmt, make tables-float-nan, make tables-float-nan-negative-control, make tables-lists, make tables-lists-tool-cook, make tables-lists-negative-controls (twelve controls, each red on its own sabotage), make tables-maps, make tables-maps-cook-check-negative-control, make tables-cook-cli, make tables-cook-write, make tables-cook-open, and the wire fuzzer over the new units (0 divergences across 143,309 mutants and 44 roots).

🤖 Generated with Claude Code

rowan-claude and others added 6 commits September 7, 2026 06:34
The page said a float32's payload is 32 raw IEEE-754 bits and nowhere said
those bits ride UNCHANGED. `grep -n NaN docs/SPEC-TABLES.md` found the widened
rung's aside and nothing else; PORTING.md had no row; no corpus instance
carried a signalling NaN. A backend holding a float32 in a wider cell quiets
one through the hardware conversion, and the byte identity nine languages hold
over one corpus fails on exactly those values.

The RED, before the fix: the tool's own cook is one such backend.
`TestACookCarriesAFloatBitPattern` over the new pin reported

    the signalling slot holds 0x7FC00001 and the pattern is 0x7F800001
    the negative slot holds 0xFFE5A5A5 and the pattern is 0xFFA5A5A5
    the wire did not survive the cook: 127 bytes back, 127 pinned

because internal/tablecook/region.go wrote `math.Float32bits( float32( cell.F ) )`
and uncook.go read `float64( math.Float32frombits( ... ) )` — a float64 cell
with a conversion at each end.

The sentence: SPEC.md §4.3 gains the wire-model bullet and the float32/float64
rows say it; SPEC-TABLES.md §3 gains the paragraph beside the kind-10/11
payload row. PORTING.md gains M21, the technique with its nine cells — cpp and
the tool carried, the eight ports a row on #366.

The pin: test/tables/F1.schema and F2.schema, the second the same root with
every float32 respelled float64 (§4's float rung). test/tables/main.cpp writes
testdata/wire/tables/floats_nan.bin and reloads it with every other golden, so
the big-endian leg covers it too. The patterns are a signalling NaN
(0x7F800001), a NaN with a mantissa past the quiet bit (0x7FC0DEAD), a negative
signalling one (0xFFA5A5A5), two array elements, and both double widths.

The claim and its control: test/tables/floatnan_main.cpp asserts the writer's
bytes, the identity read and the widened read; `make
tables-float-nan-negative-control` puts the hardware conversion back in
TableWidenF32 through an overlay, regenerates only F1/F2 from it, and the pin
goes red on the payload — 0x7ff0000020000000 read back as 0x7ff8000020000000,
the quiet bit the conversion set.

The Go oracle passes the pin too (internal/tablewire/floatnan_test.go), and the
tool's cook now carries it: WidenF32 and NarrowF32 are exported from
internal/tablewire so one pair serves the wire engine and the cook engine.

THE EIGHT PORTS ARE NOT THIS PR'S. Their legs are a row on #366, and PORTING.md
M21's cells say so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…uncook carry the list (#380)

Two clauses SPEC-TABLES.md listed as owed, and the tool refused both by name.

The RED, quoted from the tree before this commit.

internal/tablecook/check.go:212, on the cook the C++ reference wrote for
`Army` (make tables-lists):

    node 1 (Army at offset 0): Army.squads: Squad.roster: a map slot, and
    `cook-check` carries no map-slot clause yet (docs/SPEC-TABLES.md §7.4):
    the C++ reference reads the cook (--lang cpp), and the tool's clause is
    schema#380's next PR

compiler/tableslists.go:58, through `schema uncook` over the same file:

    unit declares an unbounded array in a table closure (...): the tool's WIRE
    and TEXT halves carry the construct and `cook-check` reads one, and its
    COOK half does not, so this command would lay out a region short of the
    element arrays rather than refusing. The C++ reference carries the cook

THE MAP-SLOT CLAUSE (§7.4, check.go). The four clauses a list's slot already
took — containment, alignment, fit and no overlap — are now ONE helper,
`placedArray`, because the two slots are one shape and lists and maps are one
population in a node's extent. On top of it the map takes the fifth a list has
no analogue for: the keys read ASCENDING WITH NO REPEAT, at the key's own
signedness for an integer and bytewise against the used length for a bounded
string, because a cook a `Find` cannot search is a forgery.
`./bin/schema cook-check --root Army build/lists-cooks/army.cook tables/lists`
now reports ok, and `make tables-lists` asserts that instead of the refusal.

THE LIST'S COOK AND UNCOOK HALVES (§2.9, §7.6). internal/tablecook/extent.go
is the node extent in the tool: `recordHasExtent`, the record's storage
rounded to the region's alignment floor, and ONE pre-order carve walk with its
writing half switched off for the measure, so the two cannot drift. Layout
sizes an extent-bearing node from the instance it walked; Write fills each
slot's sixteen bytes with the self-relative delta and the count and holds the
extent written to the extent measured; Uncook reads them back.

THE TWO WRITERS MEET, which is the evidence that matters: `make
tables-lists-tool-cook` has the generated `SaveCook` land on the tool's own
bytes EXACTLY, in both byte orders, over test/tables/L1.schema — a
list-bearing unit with no map in it, because the tool's cook still refuses a
map-bearing one. 352 bytes, little and big. The CLI round trip runs beside it.

CONTROLS. tables-maps-cook-check-negative-control: the SHARED containment
sabotage must turn the map's test red beside the list's, and the keys'
ascending test gets its own sabotage, because a shared control would leave the
fifth clause untested. tables-lists-tool-cook-negative-control: the carve lays
an array unaligned and the extent's shape goes red.

THE PAGE. SPEC-TABLES.md §7.4 clause 4 loses "Until schema#380 lands this
clause in the tool"; §21's maps bullet loses "which is the one piece still
owed" and now names the tool's COOK and UNCOOK halves as what remains for a
MAP; §21's unbounded-arrays bullet loses "the tool's COOK and UNCOOK halves are
owed beside the map's". PORTING.md M19 carries the cook-check clause and its
two controls.

WHAT IS STILL OWED, unchanged: the tool's COOK and UNCOOK halves for a MAP.
The map adds the sort, the entry array's key order and the two reader events
to the extent the list's halves already carry, and refuseToolMaps still names
a map-bearing unit at those two surfaces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
golangci-lint v2.12.2 on the PR, both in internal/tablewire/floatnan_test.go:
godot on a declaration comment that did not end in a period, and unconvert on
a `uint32(...)` around a value already `uint32`. No behaviour moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`go run .../modernize@v0.23.0 ./...`, which the lint job runs after
golangci-lint:

    internal/tablecook/check.go:379:5: if statement can be modernized using max
    internal/tablecook/check.go:390:5: if statement can be modernized using min

Both are clamps in the key comparison, now min/max. Nothing moved: the local
run of the same analyzer is silent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…haring the helper

The rule's third place was 'a text cell'; the text writer refuses every
non-finite float and the reader counts a NaN token as a kind mismatch, so
no NaN payload rides through text. The reference sentence named
decode.go's kind-10 arm as a reader of the exported pair; it tests the
predicate inline and calls the shared helper, and the page now says so.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@rowan-claude
rowan-claude merged commit 6713e11 into main Sep 7, 2026
20 checks passed
@rowan-claude
rowan-claude deleted the contract-nan-cookcheck branch September 7, 2026 11:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant