Skip to content

tables: text map values, string(N), wstring(N) and bytes(N), in the C++ reference (#619) - #626

Merged
rowan-claude merged 8 commits into
mainfrom
fix-619-text-map-value
Sep 7, 2026
Merged

tables: text map values, string(N), wstring(N) and bytes(N), in the C++ reference (#619)#626
rowan-claude merged 8 commits into
mainfrom
fix-619-text-map-value

Conversation

@rowan-claude

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

Copy link
Copy Markdown
Collaborator

map[K]string(N), map[K]wstring(N) and map[K]bytes(N) now emit C++ that
compiles, and the corpus, the goldens, the oracle and a control say so.

Closes #619.

What lands

The emitter. internal/codegen/cpptable/maps.go gains mapValueIsText,
and mapValueStorageType answers the ENTRY for those three kinds. The entry's
own storage was never wrong: §2.8 makes the entry a real table whose value is
an ordinary field, so it already emitted char value[N + 1] beside
int32_t value_length, the §7.2 row. What was missing was the HANDLE.
mapValueStorageType asked cppFieldType for one type spelling, and that
function answers types, not pairs, so it returned /* ? */ and the entry
surface, Insert, Find and IndexFind all spelled it.

TableEntryValue and TableEntryFound now answer <Entry> * and
const <Entry> *. Nothing about the entry's layout, its descriptor, its codec,
its cook or its text form moves: each of the three was always an ordinary field
of the entry, and only the accessor was unspellable.

The corpus. tables/maps/Text.schema, one table, three maps and a field
past them:

table Text
{
    names map[string(8)]string(16) // kind 12 under a string key
    wide  map[uint16]wstring(6) // kind 33, the value kind that is never a key
    blobs map[int32]bytes(10) // a byte buffer by value, under a signed key
    after int32
}

Both engines. test/tables/maps_main.cpp's test_text_values pins
testdata/wire/tables/map_text.bin, holds measure == save, loads the region
and re-saves it byte for byte, and reads the text form back to the same wire.
test/conformance/harness/maps_test.go gains {"map_text", "Text"}, so the
compiler's own engine reads the reference's bytes, writes them back byte for
byte, renders the text and reads that text to the same bytes. The two engines'
TEXTS are the same bytes too:

{
  "names": { "alpha": "first", "beta": "second" },
  "wide": { "5": "Aé中😀" },
  "blobs": { "-3": "3q2+7w==", "2": "AAGA/w==" },
  "after": 9
}

The wide value is five UTF-16 code units ending in a SURROGATE PAIR, so the
transcode runs both ways and a clamp that split the pair would show; the byte
values carry a zero byte and a high byte, which text cannot hold.

Red first

tables/maps/Text.schema landed on its own in 77403e2, before any emitter
change. Against main's emitter the generated header does not compile. clang at
the repo's own table flags, over a translation unit that includes TextTable.h
and nothing else, gives twenty-one errors (clang stops printing at twenty):

TextTable.h:4643:24: error: a type specifier is required for all declarations
 4643 | inline const /* ? */ * TableEntryFound( const TextNamesEntry * entry ) { return entry != NULL ? &entry->value : NULL; }
TextTable.h:4643:81: error: cannot initialize return object of type 'const int *' with an rvalue of type 'const char (*)[17]'
TextTable.h:4719:80: error: cannot initialize return object of type 'const int *' with an rvalue of type 'const char16_t (*)[7]'
TextTable.h:4797:81: error: cannot initialize return object of type 'const int *' with an rvalue of type 'const uint8_t (*)[10]'

The pointer types in those three messages are the whole diagnosis: the storage
was already char[17], char16_t[7] and uint8_t[10], and only the return
type was unspellable.

The control

tables-maps-value-reset-negative-control drops TableResetMapValue at its
call site in TableMapPlace, so a DUPLICATE key no longer resets the value
half §2.8 says is reset WHOLE. A TEXT value is where that bites hardest, which
is why the control arrives with Text: the storage is a PAIR, so the repeat
rides the first insert's bytes AND its length.

RED under the sabotage, three of the four in the new rows:

FAIL test/tables/maps_main.cpp:257: again->health = 100, want 0
FAIL test/tables/maps_main.cpp:1453: repeat != NULL && repeat->value_length == 0
FAIL test/tables/maps_main.cpp:1454: repeat != NULL && repeat->value[0] == 0
FAIL test/tables/maps_main.cpp:1468: landed != NULL && landed->value_length == 0
4 map check(s) failed
negative control: valuereset turns the MAP GATE red — 4 failures

GREEN without it:

maps: all checks passed (docs/SPEC-TABLES.md §2.8)

Stated plainly: that sabotage was reachable before this PR too, through the
Fleet row at line 257. What the new rows add is the PAIR half of the
evidence, the two members a text value's storage is.

The compile-time half of the fix has no runtime control, and cannot: a sabotage
of the storage line does not produce a binary to run. Its evidence is the
red-first quote above.

The goldens

NEW: testdata/golden/tables/maps/TextTable.{h,cpp} and
testdata/wire/tables/map_text.bin.

MOVED, and every one is the UNIT'S SHARED VOCABULARY (§3.3) growing because
mapdemo gained four tables, Text and the three entries its maps generate.
The vocabulary is emitted identically into every header of a unit, which is why
three unrelated headers move and why no per-type codec does:

before after
TableIds::kCapacity 44 50
kTableMessageEntriesHere 44 52
kTableNodeTableFieldSlot 33 40
kTableAnnounceBytes 521 615
BuildVersion 0xe763b68d79174b68 0x28abc7f5927a7539

plus every w.put( <slot>, kTableMessageRefBitsHere ) and node.type_slot,
because the vocabulary is sorted and new types shift the slots after them. The
two message wires follow from the same fact: map_conn IS the announcement,
521 to 615 bytes, and map_full_message stays 172 bytes and repacks, because a
body's type reference is an index into that vocabulary. Those hunks are the
whole diff in DepthTable.h, FleetTable.h and RowsTable.h.

AND SINCE THE MERGE with origin/main (#615, retain-unknown), one more row of the
same class: kTableRetainKnownIds, a unit-wide sorted list of known field ids,
44 to 50, growing by the six the Text unit adds. TextTable.h is regenerated
whole over that merge, because it was first generated before it.

tables/maps/tables.baseline is ADDITIVE, updated with its reason: the Text
line and the three anonymous entry lines §2.8 keys by the holder's and the
field's wire id. The value lines record kind=12 size=16, kind=33 size=6 and
kind=14 elem=6 size=10.

Page silences decided, for the owner rather than quietly

  1. What a text value's HANDLE is. §2.8 makes the entry a real table whose
    value is an ordinary field, and §7.2 gives that field a buffer beside an
    int32 length. Neither says what Find, Insert and Each hand back when
    the value's storage is TWO members. Decided: the ENTRY, which reaches both.
    The two alternatives were rejected on the page's own words. A pointer to the
    buffer alone loses the length an Insert exists to let a caller set. And
    wrapping the pair in an unnamed struct, the way §2.6 does for a union ARM,
    would move offsetof( Entry, value_length ), which the descriptor's
    count_offset spells (§8.1), and would contradict §2.8's "It has two
    fields". The cost of the decision, stated: the handle also reaches key, so
    a caller CAN write it and break the order. The emitted comment says not to.
    If the page would rather have a narrower handle, this is the line to rule on.

  2. A bytes(N) value's framing under a map key. §4.2's fuzz list names
    kind 33 under a map key and says nothing about a byte buffer there. Decided:
    the value takes the FIELD's framing, kind 14 over element kind 6, because
    §2.8's rule is that a value is a field of the entry and nothing in it carves
    an exception. The baseline records it that way.

  3. The text form of a text value under a map key. §16.2's map[K]V row
    says the value takes its own row; §2.8's worked example only ever shows a
    table value. Decided: the value's own §16.2 row verbatim, a string as
    itself, wstring(N) transcoded to UTF-8, bytes(N) base64 and padded. Both
    engines produce the same bytes, quoted above, so this is now pinned either
    way and worth a ruling if it should read otherwise.

Not a silence, checked and closed: a map value never carries a declared
default. The parser refuses a default on the map FIELD by name, and the grammar
has no place to spell one on the value, so the elision rule is exercised at the
empty end only.

Neighbours found and NOT fixed here

The same root cause reaches four more value kinds that #619 does not name. Each
was confirmed by generating and compiling, and each is left for its own issue
rather than widened into this one:

  • map[K][N]T, map[K][..N]T, map[K][E]T and map[K][]T:
    mapValueStorageType answers the ELEMENT type, so &entry->value is a
    pointer to an array and the header does not compile. Same class, different
    kinds.
  • map[K]*string and map[K]*bytes: mapValueIsPointer is true and
    TableEntryFound spells %sAt( entry->value ) from value.Type.Name, which
    a blob has none of, so the header carries At( entry->value ) and does not
    compile.

map[K]*wstring is refused by name at the front end and is not in that set.

Merged, not rebased

origin/main moved mid-branch when #615 landed. Integrated with
git merge origin/main, then make update-goldens over the merge. No golden
was hand-merged.

Test

make test whole, log kept. Five hard failures, all Error 127, all the
absent toolchains this box was briefed to have: dotnet (build-cs-cook,
build-conformance-cs), dart (build/conformance-dart), elixirc
(build/elixir-tables-ebin), and javac under a dist/ that does not exist
(build-conformance-java). Nothing else failed: every other FAILED line in
the log is a negative control going red on purpose. The C++, C, Go and Rust
legs built and ran. go test ./... is green across 30 packages.

🤖 Generated with Claude Code

rowan-claude and others added 7 commits September 6, 2026 19:34
`tables/maps/Text.schema` declares the three text value kinds §2.8 names as
legal map values: `map[string(8)]string(16)`, `map[uint16]wstring(6)` and
`map[int32]bytes(10)`, with an `int32` past them so a stop is visible.

Against the emitter as it stands the generated header does not compile.
`internal/codegen/cpptable/codecs.go`'s `cppFieldType` has no case for
TString, TWString or TBytes, so `mapValueStorageType` answers `/* ? */` and
the entry surface reads:

    inline const /* ? */ * TableEntryFound( const TextNamesEntry * entry )
    inline /* ? */ * TableEntryValue( TextNamesEntry * entry )

clang, at the repo's own table flags, over a translation unit that includes
`TextTable.h` and nothing else, gives thirteen errors:

    TextTable.h:4643:24: error: a type specifier is required for all declarations
    TextTable.h:4643:81: error: cannot initialize return object of type
        'const int *' with an rvalue of type 'const char (*)[17]'
    TextTable.h:4719:80: error: cannot initialize return object of type
        'const int *' with an rvalue of type 'const char16_t (*)[7]'
    TextTable.h:4797:81: error: cannot initialize return object of type
        'const int *' with an rvalue of type 'const uint8_t (*)[10]'

The entry's own storage is already right — `char value[16 + 1]` beside
`int32_t value_length` — so what is missing is the handle, not the layout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#619)

`string(N)`, `wstring(N)` and `bytes(N)` are legal map values (§2.8, "A VALUE
is anything a table field can hold"), and the generated entry already stored
each correctly: `char value[N + 1]` beside `int32_t value_length`, the same
storage row the field takes anywhere (§7.2). What was missing was the HANDLE.
`mapValueStorageType` asked `cppFieldType` for a single type spelling, got
`/* ? */` because that function answers types and not pairs, and the entry
surface, Insert, Find and IndexFind all spelled it.

A pair is two members, so no one member is the value. The handle is therefore
the ENTRY, which reaches both: `TableEntryValue` and `TableEntryFound` answer
`<Entry> *` and `const <Entry> *`, a caller fills `value` and `value_length`,
and `key` stays the map's, which owns the order it carries. Nothing about the
entry's layout, its descriptor, its codec or its text form moves: each of the
three is an ordinary field of the entry and always was.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`test_text_values` builds the three text values through the entry handle, out
of key order, pins `testdata/wire/tables/map_text.bin`, holds measure == save,
loads the region and re-saves it byte for byte, and reads the text form back
to the same wire. The wide value is five UTF-16 code units ending in a
SURROGATE PAIR, so the transcode runs both ways and a clamp that split the
pair would show; the byte values carry a zero byte and a high byte, which
text cannot hold.

`maps_test.go` gains `{"map_text", "Text"}`, so the compiler's own engine
reads the reference's bytes, writes them back byte for byte, renders the text
and reads that text to the same bytes. The two engines' texts are the same
bytes as well:

    {
      "names": { "alpha": "first", "beta": "second" },
      "wide": { "5": "Aé中😀" },
      "blobs": { "-3": "3q2+7w==", "2": "AAGA/w==" },
      "after": 9
    }

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ves (#619)

NEW, one unit's worth:
  testdata/golden/tables/maps/TextTable.h, TextTable.cpp
  testdata/wire/tables/map_text.bin (pinned in the previous commit)

MOVED, and every one of them is the UNIT'S SHARED VOCABULARY (§3.3) growing
because `mapdemo` gained four tables — `Text` and the three entries its maps
generate. The vocabulary is emitted identically into every header of a unit,
which is why three unrelated headers move and why no per-type codec does:

  kCapacity                  44 -> 50   the unit's distinct field ids
  kTableMessageEntriesHere   44 -> 52   the announced vocabulary's size
  kTableNodeTableFieldSlot   33 -> 40   the reserved node-table id's slot
  kTableAnnounceBytes       521 -> 615  the announcement, byte for byte
  BuildVersion   0xe763b68d79174b68 -> 0x28abc7f5927a7539
  every `w.put( <slot>, kTableMessageRefBitsHere )` and `node.type_slot`

The vocabulary is sorted, so new types shift the slots after them; the two
message wires follow from the same fact. `map_conn` IS the announcement, 521
-> 615 bytes. `map_full_message` stays 172 bytes and repacks, because a
body's type reference is an index into that vocabulary.

`tables/maps/tables.baseline` is ADDITIVE: the `Text` line and the three
anonymous entry lines §2.8 keys by the holder's and the field's wire id. The
value lines record what the page says a value line records — kind 12 size 16,
kind 33 size 6, and kind 14 elem 6 size 10 for the byte buffer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`tables-maps-value-reset-negative-control` drops `TableResetMapValue` at its
call site in `TableMapPlace`, so a DUPLICATE key no longer resets the value
half §2.8 says is reset WHOLE. A TEXT value is where that bites hardest,
which is why the control arrives with `Text`: the storage is a PAIR, so the
repeat rides the first insert's bytes AND its length.

`test_text_values` gains the duplicate row the control needs, and the gate
answers by name.

RED under the sabotage, three of the four in the new rows:

    FAIL test/tables/maps_main.cpp:257: again->health = 100, want 0
    FAIL test/tables/maps_main.cpp:1453: repeat != NULL && repeat->value_length == 0
    FAIL test/tables/maps_main.cpp:1454: repeat != NULL && repeat->value[0] == 0
    FAIL test/tables/maps_main.cpp:1468: landed != NULL && landed->value_length == 0
    4 map check(s) failed
    negative control: valuereset turns the MAP GATE red — 4 failures

GREEN without it:

    maps: all checks passed (docs/SPEC-TABLES.md §2.8)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…619)

`origin/main` moved under this branch: #615 landed retain-unknown in the C++
reference. `make update-goldens` over the merge, hand-merging nothing.

`TextTable.h` is rewritten because it was generated before the merge and now
carries #615's emitter whole. The other three maps headers move twenty lines
each, and they are the SAME CLASS as the vocabulary rows this branch already
moves: `kTableRetainKnownIds` is a unit-wide sorted list of known field ids,
44 -> 50, growing by the six the `Text` unit adds.

The value handle's three-way choice is a switch. golangci-lint's gocritic
reads the if-else chain as one, and it is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
)

The control drops the TableResetMapValue call at the placement site; the comment beside its rows said the reset moved to the key half, which no sabotage performs. The Makefile paragraph states the rule in the present tense.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@rowan-claude
rowan-claude merged commit bb5f3bd into main Sep 7, 2026
20 checks passed
@rowan-claude
rowan-claude deleted the fix-619-text-map-value branch September 7, 2026 00:50
rowan-claude added a commit that referenced this pull request Sep 7, 2026
)

The handle every map surface hands back now follows the STORAGE the page
gives a field of the value's kind, so the six kinds #628 names emit C++
that compiles (docs/SPEC-TABLES.md §2.8, §4.2).

The entry's own storage was right in all six: §2.8 makes the entry a real
table whose `value` is an ordinary field, so `int32_t value[3]`, the
element array beside `value_count`, `TableKeyed<int32_t, Slot>`,
`TableList<Item>` and the eight-byte `TableRef` were all already emitted.
What had no spelling was the handle, because `mapValueStorageType` asked
`cppFieldType` for one type name and that function answers a field's
ELEMENT type, not its array, its keyed slot, its list slot or its blob.

- `[N]T` is ONE member, so the handle is a pointer to the ARRAY and keeps
  the extent. A return type cannot spell `T (*)[N]` without wrapping the
  declarator around the function name, so the entry gains one alias,
  `<Entry>Value`, and the handle is `<Entry>Value *`.
- `[..N]T` is a PAIR, the elements beside `value_count`, so the handle is
  the ENTRY, which is #626's rule for a text value applied to the second
  storage shape that has two members. `mapValueIsText` becomes
  `mapValueIsPair` and covers both.
- `[E]T` is `TableKeyed<T, E>` and `[]T` is `TableList<T>`, one member
  each, so the handle is that member.
- `*string` and `*bytes` name a BLOB NODE (§2.5), which has no declared
  type name, so the const Find resolves through `TableBlobAt` and answers
  `const TableBlob *` where a `map[K]*T` answers `<T>At`'s pointer. The
  builder's handle stays the slot, which is what an Emplace fills.

RED FIRST, against this commit's parent. clang at the repo's own table
flags, over a translation unit that includes one generated header and
nothing else:

    CellsTable.h:6144: cannot initialize return object of type 'const int32_t *'
      with an rvalue of type 'const int32_t (*)[3]'
    RunsTable.h:6134: cannot initialize return object of type 'const Item *'
      with an rvalue of type 'const Item (*)[4]'
    SlotsTable.h:6204: cannot initialize return object of type 'const int32_t *'
      with an rvalue of type 'const TableKeyed<int32_t, Slot> *'
    SpansTable.h:6141: cannot initialize return object of type 'const Item *'
      with an rvalue of type 'const TableList<Item> *'
    DocsTable.h:6154: a type specifier is required for all declarations
      inline const  * TableEntryFound( const DocsPagesEntry * entry )
    ChunksTable.h:6145: use of undeclared identifier 'At'

BOTH ENGINES. `test/tables/maps_main.cpp` gains five rows, one per unit,
each pinning its wire, holding `measure == save`, loading the region,
re-saving it byte for byte, and reading the text form back to the same
wire. `test/conformance/harness/maps_test.go` gains the six names, so the
compiler's own engine reads the reference's bytes, writes them back byte
for byte, renders the text and reads that text to the same bytes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rowan-claude added a commit that referenced this pull request Sep 7, 2026
NOT THIS ISSUE'S CHANGE, and it is here because the merge carries it.

main is red at 19a2372 on its own CI run 34081461383, and it was green
at bb5f3bd. #625 moved `internal/codegen/cpptable/json.go` and re-pinned
the map goldens it knew about, DepthTable.cpp, FleetTable.cpp and
RowsTable.cpp, each by the same 91 lines. Text.schema and its golden
landed in #626, which merged after #625's branch was cut, so
maps/TextTable.cpp never got the re-pin and `make tables-block-zero-cost`
reports it moved.

The gap is exactly the 91 lines the three siblings took, the
TableJsonInterpretExact reader and the decimal band beside it, which
every table holding a map now emits. The file here is the generator's
own output, copied by the path `make update-goldens` copies it, with no
hand edit: copying the whole maps directory moves this one file and
nothing else, which is what says the other three were already current.

    block zero-cost gate: 105 Table sources byte-identical to their pins

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rowan-claude added a commit that referenced this pull request Sep 7, 2026
…ero-cost gate on main) (#664)

#625 moved the emitted JSON walker in every map-bearing unit and regenerated the goldens on its branch before #626's TextTable golden existed on it; the merge of main carried the golden in unchanged, so main's committed TextTable.cpp lags the compiler by the key-reader change and the block zero-cost gate refuses main. Regenerated with main's compiler; no header, wire pin or id moves.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
rowan-claude added a commit that referenced this pull request Sep 7, 2026
…ure (#605) (#657)

* red first: the enum-bound corpus, ten shapes and three controls (#605)

SPEC-TABLES.md §2.4 and §11 state the refusal on the bound's PROVENANCE:
a positional array whose bound folds from an enum is refused in a TABLE
BODY and in a UNION ARM, in every spelling the bound has. The checker
follows the SPELLING, so eight of the ten shapes compile clean today.

test/tables/enumbound holds one unit a shape. Against this commit:

    ArmConstCount.schema    COMPILES (exit 0)
    ArmConstMax.schema      COMPILES (exit 0)
    ArmCount.schema         COMPILES (exit 0)
    ArmFolded.schema        COMPILES (exit 0)
    ArmMax.schema           REFUSED, naming neither the arm nor the table
    BodyConstCount.schema   COMPILES (exit 0)
    BodyConstMax.schema     COMPILES (exit 0)
    BodyCount.schema        COMPILES (exit 0)
    BodyFolded.schema       COMPILES (exit 0)
    BodyMax.schema          REFUSED, naming neither the table
    ControlPacket.schema    COMPILES (exit 0)
    ControlPlain.schema     COMPILES (exit 0)
    ControlTypeHeld.schema  COMPILES (exit 0)

TestEnumBoundProvenanceCorpus reads the directory and holds each file to
its answer, so a shape is red the moment it compiles or the diagnostic
stops naming the field, the enum, the constant the field spells and
`[E]T` as the fix. All ten refused rows are red at this commit and the
three controls are green.

The controls hold the other edge: the packet wire, a bound that folds
from no enum, and the `type`-held case schema#606 rules on, which this
issue does not decide and this corpus does not move.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* check: the enum-bound refusal follows the bound's provenance (#605)

The rule docs/SPEC-TABLES.md §2.4 and §11 state reads the bound's
PROVENANCE and the checker read its SPELLING, so `[E.Count]T` and `[N]T`
under a `const N` that folds from either compiled in a table body and in
a union arm and carried the positional class §4.1 counts as closed.

`enumBoundProvenance` follows a bound to the enum it folds from, through
named constants and constant arithmetic at any depth (SPEC.md §4.2),
carrying its own visiting guard over the constant graph. It looks at an
ENUM alone: a `flags` bound is refused by its own rule, and a bound that
reaches neither enum nor flags is a plain positional array.

`checkPositionalEnumBoundInClosure` runs once the closure is known, which
is what lets a union arm's diagnostic name the table that reaches the
union, as #572's closure refusal names the edge that pulled a `type` in.
THE WALK IS THE FENCE: it starts at TABLE bodies and descends UNIONS
alone, so the `type` a table closure reaches is never visited and
schema#606 keeps its ruling, and a `type` no table reaches is not in the
closure at all, so the packet wire is untouched.

The resolve-time spelling check is gone; the bound still evaluates
there, because a bound that cannot be evaluated is a different
diagnostic and belongs at the field.

`exprSpelling` now renders constant arithmetic, so a bound like
`[Grade.Max + 1]` is quoted back as the source spells it instead of as
the placeholder `N`.

test/tables/V1.schema and V2.schema: the refusal caught two live fields.
`tally` in both generations and `ledger` in V1 were sized `[Grade.Max +
1]`, a positional array in a table body whose bound folds from an enum,
which is the shape the page says is refused and the fixture rested on
the gap. Both keep their extents, 3 in V1 and 4 in V2, spelled as the
plain constants TallySlots and LedgerSlots, so every generation the
evolution test exercises is unmoved: the growing bound, the clamped
count, and V1-positional against V2-keyed for `ledger`. `schema fmt`
re-canonicalized the attribute column in both files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* check: the enum-bound gate and its negative control (#605)

`check-enum-bound-negative-control` runs the compiler over
test/tables/enumbound and reads three answers back.

THE GATE: each of the ten refused shapes must be refused, and each
diagnostic must name the field or the arm, the enum, the constant where
the bound reaches the enum through one, and `[E]T` as the fix. The arm
rows must name the table that reaches the union. The THREE POSITIVE
CONTROLS must compile: the packet wire, whose `[E.Max]T` is a plain
array the connect gate covers; a bound that folds from no enum; and the
`type`-held case, which is schema#606's ruling and not this refusal's.

THE NEGATIVE CONTROL is the one §2.4 names: it removes the CONSTANT FOLD
from the bound check through `go build -overlay`, writing no tracked
file, and every row whose bound reaches its enum through a constant must
then compile clean. It is targeted rather than blanket, so the four
direct spellings must stay refused under the same sabotage. A control
that turned the whole rule off would go red for a reason that says
nothing about the fold.

Both halves are green:

    gate: ten shapes refused on the bound's provenance, three controls compile
    negative control: without the constant fold, six folded rows compile clean
    and the four direct spellings stay refused

Wired into `make test` beside check-zero-range-negative-control. It
compiles no C++ and needs no toolchain beyond Go.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: the enum-bound refusal's status lines, on the tree (#605)

SPEC-TABLES.md §2.4's CHECKER STATUS paragraph stated a split between
the rule and the checker, and the paragraph carried its own instruction
to be deleted by the PR that closes it. It now states one thing: the
refusal reads the bound's provenance and not its text, in a table body
and in a union arm, and it names what the diagnostic names. §11's
checker-status line says the same in its own register.

Both RULING STATUS lines for schema#606 stay exactly as they were: the
type-held case is the owner's and this refusal does not reach it.

VERSIONING.md: the #540 row leaves "Owed before 3.0.0", whose contract
is a claim the page makes with the repository not yet behind it. The
repository is behind it. The evolution table's "a keyed array made
positional" row named the table body alone, which #605 flagged; it now
names the union arm as well and states the provenance rule, which is the
whole of what the checker refuses there.

USAGE.md carried the same split as an italic caveat, and a caveat that
is no longer true is worse than one out of scope, so it is replaced by
what the diagnostic actually says. The #606 caveat beside it stays.

Present tense, no history, no em dashes added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* goldens: re-pin maps/TextTable.cpp, which main left behind (#625, #626)

NOT THIS ISSUE'S CHANGE, and it is here because the merge carries it.

main is red at 19a2372 on its own CI run 34081461383, and it was green
at bb5f3bd. #625 moved `internal/codegen/cpptable/json.go` and re-pinned
the map goldens it knew about, DepthTable.cpp, FleetTable.cpp and
RowsTable.cpp, each by the same 91 lines. Text.schema and its golden
landed in #626, which merged after #625's branch was cut, so
maps/TextTable.cpp never got the re-pin and `make tables-block-zero-cost`
reports it moved.

The gap is exactly the 91 lines the three siblings took, the
TableJsonInterpretExact reader and the decimal band beside it, which
every table holding a map now emits. The file here is the generator's
own output, copied by the path `make update-goldens` copies it, with no
hand edit: copying the whole maps directory moves this one file and
nothing else, which is what says the other three were already current.

    block zero-cost gate: 105 Table sources byte-identical to their pins

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* corpus: drop an em dash from the enum-bound README (#605)

House style takes none in anything added here, and the heading carried
one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
rowan-claude added a commit that referenced this pull request Sep 7, 2026
… the C++ reference (#628) (#662)

* tables: the map corpus for the six value kinds that do not compile (#628)

Six units under tables/maps, one per value kind §2.8 lists that the C++
reference cannot spell today, each a map and a field past it so a stop is
visible: Cells map[string(8)][3]int32, Runs map[uint16][..4]Item, Slots
map[int32][Slot]int32, Spans map[uint8][]Item, Docs map[string(8)]*string
and Chunks map[int32]*bytes.

RED FIRST. Against this commit's emitter each generated header is refused
alone, clang at the repo's own table flags over a translation unit that
includes it and nothing else:

    CellsTable.h:6138: cannot initialize return object of type 'const int32_t *'
      with an rvalue of type 'const int32_t (*)[3]'
    RunsTable.h:6128: cannot initialize return object of type 'const Item *'
      with an rvalue of type 'const Item (*)[4]'
    SlotsTable.h:6198: cannot initialize return object of type 'const int32_t *'
      with an rvalue of type 'const TableKeyed<int32_t, Slot> *'
    SpansTable.h:6135: cannot initialize return object of type 'const Item *'
      with an rvalue of type 'const TableList<Item> *'
    DocsTable.h:6148: a type specifier is required for all declarations
      inline const  * TableEntryFound( const DocsPagesEntry * entry )
    ChunksTable.h:6139: use of undeclared identifier 'At'

The entry's own storage is right in every one of the six: §2.8 makes the
entry a real table whose `value` is an ordinary field, so the array, the
count companion, the keyed slot, the list slot and the buffer reference are
all already emitted. What has no spelling is the HANDLE.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* tables: the tool's numbering descends a map's entries (#628)

`internal/tablewire`'s `visitEdges` had no case for a MAP field, so the
node-table numbering never reached the pointer slots inside an entry. The
corpus hid it: `Fleet.by_id`'s two keys name the node `Fleet.flagship`
names, so every node reachable through a map was already numbered through
a pointer field beside it.

`Docs.pages` is `map[string(8)]*string` and `Chunks.blobs` is
`map[int32]*bytes`, and each is the first slot to name a node NOTHING
ELSE names. Against this commit's parent the engine read the reference's
bytes clean and then re-encoded 64 bytes where the reference wrote 123,
dropping both blob records and both pointer fields:

    map_docs: the engine re-encoded 64 bytes, the reference wrote 123
    map_chunks: the engine re-encoded 61 bytes, the reference wrote 107

The fix is the page's own sentence: a map is a by-value edge of the one
declaration-order walk, reached at its field's position, its entries
visited in ascending key order, each entry descended before the next
(docs/SPEC-TABLES.md §2.8, §3.1). `MapEntryOrder` is that order, lifted
out of `encodeMap` so the numbering and the writer take it from one
place and a wire cannot disagree with its own node table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* tables: map values of array, enum-extent, unbounded and blob kinds (#628)

The handle every map surface hands back now follows the STORAGE the page
gives a field of the value's kind, so the six kinds #628 names emit C++
that compiles (docs/SPEC-TABLES.md §2.8, §4.2).

The entry's own storage was right in all six: §2.8 makes the entry a real
table whose `value` is an ordinary field, so `int32_t value[3]`, the
element array beside `value_count`, `TableKeyed<int32_t, Slot>`,
`TableList<Item>` and the eight-byte `TableRef` were all already emitted.
What had no spelling was the handle, because `mapValueStorageType` asked
`cppFieldType` for one type name and that function answers a field's
ELEMENT type, not its array, its keyed slot, its list slot or its blob.

- `[N]T` is ONE member, so the handle is a pointer to the ARRAY and keeps
  the extent. A return type cannot spell `T (*)[N]` without wrapping the
  declarator around the function name, so the entry gains one alias,
  `<Entry>Value`, and the handle is `<Entry>Value *`.
- `[..N]T` is a PAIR, the elements beside `value_count`, so the handle is
  the ENTRY, which is #626's rule for a text value applied to the second
  storage shape that has two members. `mapValueIsText` becomes
  `mapValueIsPair` and covers both.
- `[E]T` is `TableKeyed<T, E>` and `[]T` is `TableList<T>`, one member
  each, so the handle is that member.
- `*string` and `*bytes` name a BLOB NODE (§2.5), which has no declared
  type name, so the const Find resolves through `TableBlobAt` and answers
  `const TableBlob *` where a `map[K]*T` answers `<T>At`'s pointer. The
  builder's handle stays the slot, which is what an Emplace fills.

RED FIRST, against this commit's parent. clang at the repo's own table
flags, over a translation unit that includes one generated header and
nothing else:

    CellsTable.h:6144: cannot initialize return object of type 'const int32_t *'
      with an rvalue of type 'const int32_t (*)[3]'
    RunsTable.h:6134: cannot initialize return object of type 'const Item *'
      with an rvalue of type 'const Item (*)[4]'
    SlotsTable.h:6204: cannot initialize return object of type 'const int32_t *'
      with an rvalue of type 'const TableKeyed<int32_t, Slot> *'
    SpansTable.h:6141: cannot initialize return object of type 'const Item *'
      with an rvalue of type 'const TableList<Item> *'
    DocsTable.h:6154: a type specifier is required for all declarations
      inline const  * TableEntryFound( const DocsPagesEntry * entry )
    ChunksTable.h:6145: use of undeclared identifier 'At'

BOTH ENGINES. `test/tables/maps_main.cpp` gains five rows, one per unit,
each pinning its wire, holding `measure == save`, loading the region,
re-saving it byte for byte, and reading the text form back to the same
wire. `test/conformance/harness/maps_test.go` gains the six names, so the
compiler's own engine reads the reference's bytes, writes them back byte
for byte, renders the text and reads that text to the same bytes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* tables: a negative control for the entry the numbering descends (#628)

`tables-maps-entry-node-negative-control` short-circuits the per-entry
`Number` call in the map's by-value edge, so a node named ONLY by a map
entry takes no index. The `Docs` and `Chunks` rows are what meet it:
their `*string` and `*bytes` values are the only slots in this corpus
that name a node nothing else names, and `Fleet.by_id` cannot meet it
because `Fleet.flagship` names the same node.

RED under the sabotage, all six failures in the new rows, and the answer
is the -1 §7.6 gives an unreached pointer rather than a wrong wire:

    FAIL table wire golden map_docs: -1 bytes written, 123 pinned
    FAIL test/tables/maps_main.cpp:1873: need > 0
    FAIL test/tables/maps_main.cpp:1874: need = -1, past the 268435456 byte measure ceiling
    FAIL table wire golden map_chunks: -1 bytes written, 107 pinned
    FAIL test/tables/maps_main.cpp:1959: chunk_need > 0
    FAIL test/tables/maps_main.cpp:1960: chunk_need = -1, past the 268435456 byte measure ceiling

Its sabotage carries an unbalanced parenthesis a `$(call)` argument
cannot, so the recipe is spelled out as the keylength one is.

Two rows moved so the instrument survives the sabotages beside it. The
`Chunks` half left the tail of `test_blob_values` for its own function,
because a sabotage that refuses one unit's save must leave the other unit
reporting. And the `Spans` element reads sit under their count, because a
list whose elements were never placed has a NULL element pointer: under
the `depth` sabotage that read faulted, and a fault takes the buffered
output of every row with it, which is what "the gate went red, but not on
a CHECK" was saying.

The controls now red on the new rows by name, and each is the sabotage
that owns that kind's runtime behaviour:

- `valuereset` 4 to 10 failures, one per value kind's duplicate row: the
  fixed array reset whole, the counted array's elements AND its count,
  every keyed slot, the list slot back to empty, the blob slot to null.
- `depth` names the `Spans` rows: an entry's list elements are a term of
  the map's extent at the entry's depth, so a measure summed at one depth
  only leaves them unplaced.
- The compile-time half of this PR has NO runtime control by
  construction: a sabotage of the storage line produces no binary to run,
  and its evidence is the red-first quote in 4a2d885.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: the storage and the handle per value kind under a map key (#628)

§2.8 gains two paragraphs. THE VALUE'S STORAGE IS THE ROW ITS KIND TAKES
AT A FIELD, kind by kind, because the entry is a real table and `value` is
an ordinary field of it. THE HANDLE FOLLOWS THE STORAGE: a pointer to the
`value` member where the storage is one member, the ENTRY where it is two,
the ARRAY rather than its first element for a `[N]T`, and for a pointer
value the SLOT on the builder against the RESOLVED node on the const
`Find`.

§4.2 gains one: a map's value is a FIELD POSITION, so the array element
kind, the kind 17 node index and the kind 12 and kind 33 payloads the
mutator list already names all land inside an entry without a strategy of
their own, and what stays the map's is the KEY.

§15 gains a named follow-on. `?T` and `?[N]T` store the value beside a
`bool` presence companion, which is two members, so §2.8's rule answers
the entry; the C++ reference answers the value member, so a
`map[K]?T`'s presence cannot be set and the value is elided on every wire.
`?[..N]T` is the exception by accident, its count companion already making
it a pair. Found by probing every value kind §2.8 lists; left for its own
issue rather than widened into this one.

The one value kind refused BY NAME gains a diagnostics case:
`map[uint32]*wstring`. The entry is where the refusal has to reach a map's
value, because the value is a field of a table nobody wrote. Every other
kind §2.8 lists compiles as the ordinary field it is, each confirmed by
generating a unit and compiling the header alone: a scalar, an enum, a
`flags` mask, a declared `type`, `?T`, `?[N]T`, `?[..N]T` and a union.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* tables: regenerate the six map units' walkers over main's key reader (#628)

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

---------

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.

A text map value does not compile: map[K]string(N) emits const /* ? */ * in the C++ reference

1 participant