Skip to content

docs/SPEC-TABLES.md §2.8: maps, the specification - #479

Merged
gafferongames merged 4 commits into
mainfrom
maps-spec
Sep 4, 2026
Merged

docs/SPEC-TABLES.md §2.8: maps, the specification#479
gafferongames merged 4 commits into
mainfrom
maps-spec

Conversation

@gafferongames

@gafferongames gafferongames commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

§2.8 is the maps design, 654 lines, and this PR is the whole of it in prose: no code, no goldens. It states the construct as present fact so an implementation can be built from it and held to it.

What the page states

A map is a lookup the runtime provides over entries the wire carries. On the wire, in a region and in a cook it is an array of one generated { key, value } table held in ascending key order, riding on kind 14 over kind 13. The wire spends no kind. The section states the spelling and the two spellings it refuses, the key set (string(N) and the integer kinds) with every other key refused by name and its reason, the total order, the unbounded count, the value set, and the map as a by-value edge of §3.1's one declaration-order walk.

The entry. Its name and its casing (<Table><Field>Entry, by_id on Fleet giving FleetByIdEntry), the nested form (<OuterEntry>ValueEntry), the file that declares it and where in that file, its two constant field ids at both wire widths, its wire shape, and the fact that it is the one exception to "a root is any table". It claims §11's suffix set as every closure member does.

The reader, rule by rule. The key is read before the slot is chosen, by a scan of the entry's field headers, so field order inside a body stays non-contractual. Ascending, equal and descending each say what happens to the slot, the counters and the parent. Keys never clamp: an entry whose key does not fit is dropped and counted clamped, one per entry, and the order check compares the wire keys of the entries that land. The key kind is the reader's declaration, and at the first entry that disagrees the map reads empty, one kind_mismatch is counted for the map, and the rest of the map is skipped by its L. One report per wire, so the region load and LoadBuilder never disagree.

The rest. The text form and its integer-key rule, the cook and Find, the absent block form, the memory layout down to the sixteen-byte slot and the pre-order placement of entry arrays inside the node's extent, LoadMeasure's term and its refusal, the optional runtime index and why its hash is not a cross-port contract, the builder's segments, growth and deletes, the cost model, the negative controls, the C++ surface, why the kind stays unspent and what that costs, and the row-by-row comparison with Protocol Buffers and FlatBuffers.

What this PR changes

It applies the rulings on a cold read of the previous revision. The read found four rules a port could not derive, two sentences that contradicted the rest of the standard, and a control list that named no corpus row.

The rules the page did not carry:

  • The key before the slot. The reader scans the entry's field headers for the key's id and reads the key, and the scan decides the slot. Where the body repeats the key's id the scan reads the occurrence §3 keeps. An entry with no key field has the key's default.
  • Keys never clamp. A tightened key bound is lossy by whole entries and never malformed. A widened one is lossless. A key a string value would refuse as malformed makes the map malformed.
  • The key kind is the reader's declaration, never the first entry's. One kind_mismatch for the map, the rest skipped by the map's L, and events counted inside earlier entries stand. A value field of the wrong kind stays the entry's ordinary per-field event.
  • One report per wire. The builder forgiveness is gone, and a hand-made body is held to the sort like any other.
  • LoadMeasure. The term is N x sizeof( Entry ) rounded up to alignof( Entry ), at every depth. A wire it refuses answers the -1 every measure's refusal answers (§7.6). §4.2's EXACT claim is scoped to a read that reports nothing, and its bound is restated from an entry rather than a record, at six wire bytes.
  • The build version and the baseline. The entry is anonymous in both, keyed by the holder's wire id and the map field's wire id, so a was rename moves no line. §20.1's closed table gains the map rows, §20.2's grammar gains array=map and the entry's record line, and §18.1's shape list gains the map with the key's kind and capacity.
  • The claimed names. §11 states the eight a map takes against its field (Entry, Insert, Find, Erase, Each, IndexMeasure, Index, IndexFind) and the two runtime types (TableMap, TableMapIndex).
  • The count is int32 like every companion, and §2.2's extent cap is the cap, at both wire widths.
  • An integer key's text is §16.2's integer rule and nothing else, so -0 is zero, and a malformed key stops the read where §16.1 stops it.
  • The surface. The const Find on map[K]*T answers what a pointer field's accessor answers. Insert answers NULL for a key that does not fit and for an arena that cannot grow alike, and NULL means not inserted.
  • An unreached non-empty map slot is refused by Cook and Lock, the refusal §7.6 gives a pointer there.

The contradictions:

  • A map of the holder's own type is refused as a by-value cycle (§2), not for infinite size.
  • cook-check reads containment, alignment, fit and no overlap, not the offset the layout rule computes, so §7's independence sentence stands unchanged.
  • An empty map is elided by §3's by-value elision rule, not §2.3's.
  • The [..N]Pair edit is not silent and safe. The bytes are identical, the read gains the order check, and a wire whose pairs were not ascending reads short, so the baseline warns.
  • A map's entry is the one exception to "a root is any table", and §7 now says so where that rule is stated.

The #435 seam. The wire widths are marked as the wire as it stands, with schema#435's beside them: ids become references, L and N become canonical LEB128, and the terminator becomes the zero reference. The cost table's wire rows say the same.

The tests. Every negative control names the corpus row or instance that meets it and the one instrument that goes red. Five are added: the walk-order pin, the parent-continues half of the descending rule, one kind_mismatch when the disagreeing entry is a later one, LoadMeasure over a map of maps, and cook-check's entry-array check. The one that was data rather than a control is gone, as is the one that stood on the deleted tool path.

The prose. The status paragraph, THE SEQUENCING, the owner quotations, the history and the plans are gone. Every twice-stated fact is stated once and cross-referenced. No gate name the page does not define, and no "reds" as a verb.

VERSIONING.md rides this PR for the two facts it owes: the map row in the three-frames table, and the read report paragraph naming the wire event that raises duplicate.

What the implementation PRs owe

The page is now ahead of the tree in four places, each deliberate and each a line #487 or the codec PR closes:

  1. The map field's build-version line. schema build-version --facts emits kind=0 with no tokens for a map field today. The page states kind=14, elem=13 and array=map.
  2. The entry's anonymity. Both projections emit record FleetShipsEntry and table FleetShipsEntry today. The page states the holder-id plus field-id key, in both.
  3. The baseline rendering version. The page prints schema-tables-baseline 5; internal/baseline's constant is 4 on main, and the map lines are what moves it.
  4. The claimed names and the entry's surface. The checker claims <Table><Field>Entry and nothing else, and with the C++ refusal lifted the generator gives the entry the full table surface. The page claims the eight and the two runtime types, and states that an entry gets no Open, Cook, Save or Load.

The cook projection's own form version is not bumped here, because §20.2's worked example is pinned against the compiler by compiler/tables_test.go. The PR that emits the map tokens bumps it and re-pins that number in the same change.

Held by

go test ./... and make check are green. The page is prose, so what holds every sentence is the implementation PR built from it.

🤖 Generated with Claude Code

gafferongames and others added 2 commits September 4, 2026 12:32
…#380)

The owner reversed the after-3.0.0 sequencing (#380, 2026-09-04): maps land in
the C++ reference and the tool inside spec finalization, before the port
sweep. The page said otherwise in two places and was stale in four more.

- The BACKEND STATUS paragraph and THE SEQUENCING paragraph move from "no
  backend, and no declaration reaches it — after 3.0.0" to the register every
  other landed construct uses: the C++ reference and the tool carry it, every
  other backend refuses a unit that declares one by name, the ports are a
  named follow-on. The closing confirmation keeps its content and loses its
  dependence on the freeze: the construct spends nothing the release freezes.
- The VALUE list gains `*bytes` and `*string` (#428) and `?[N]T` / `?[..N]T`
  (#437) — every spelling a table field can hold today.
- The ENTRY's two constant field ids are stated as constants of the RULE
  rather than beside it: today's fold (`key` 0xA079, `value` 0x9194) and, under
  the uniform 64-bit wire (#435), `fnv1a64` of the same two names
  (0x3DC94A19365B10EC, 0x7CE4FD9430E80CEA), carried in that wire's id table in
  first-use order like every other id. §5's constants paragraph says the same.
- The POINTER WALK over a map is stated (#438): a map is a by-value edge of
  the ONE declaration-order walk, reached where it is declared, its entries
  visited in ascending key order — the order the wire carries — with each
  entry's value descended before the next entry. §3.1's by-value edge list
  gains the entry.

Two reconciliations, named because they change an argument rather than a
status line:

- The SHOULD answer's decisive reason was "a dedicated map kind after 3.0.0
  would make every 3.0.0 reader meet framing damage". Landing before the
  freeze retires that reason — a kind spent now is inside the closed set. The
  kind still stays unspent, on the rule that actually governs it: a kind is
  spent to close a SILENT edit (§3.1, §3.2), and a map and an array of pairs
  are the same data read correctly either way. The migration argument stands.
- §15's follow-on row said the construct is designed and unbuilt, and deferred
  the optional runtime index behind a bench number. The row becomes what is
  actually outstanding — the eight ports — and the index ships with the
  construct in the reference: it is never stored and no golden names it, so
  what the bench defers is the SIZE AT WHICH A CALLER REACHES FOR IT, not the
  surface.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…y construct (#380)

The sequencing ruling stands — maps land now, before 3.0.0 and before the port
sweep. What the status paragraph must not do is claim a codec before it exists:
the language half lands first (the spelling, the key rules, the generated entry
and the name it claims), the C++ reference and the tool follow, and until a
backend's codec lands it refuses a map-bearing unit by name (§11) rather than
emitting a half-codec. §2.8's status paragraph, THE SEQUENCING and §15's row
all say that.

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

Copy link
Copy Markdown
Contributor Author

Rulings on the #479 cold read (maps spec, docs/SPEC-TABLES.md §2.8)

Numbers follow the reader's report (sections 1 to 5). Line numbers there are at 6cd17b9 on maps-spec. Every ruling below is a rule the page states as present fact, in the page's voice, with no history, no plan, no owner quotation.

1. Underdetermined

1.1 The key before the slot. Before an entry's body decodes, the reader scans the entry's field headers for the key's id and reads the key (a bounded string or an integer, always a primitive). The scan decides the slot: ascending lands the body in the next slot; equal resets the previous slot to defaults and decodes the body into it (last wins whole); descending stops the map as malformed, the parent continues. An entry whose body carries no key field has the key's default. Where the body repeats the key's id, the scan reads the occurrence §3's repeated-field rule keeps. The writer emits the key first in every entry, so on any wire this schema wrote the scan ends at the first header; the rule is the scan, not the position.

1.2 Keys never clamp. An entry whose key does not fit the reader's bound (longer than N, or one a string value would clamp) is skipped by its L and counted clamped, one per entry. The order check compares the wire keys of the entries that land, so a skip cannot reorder or collide. A tightened key bound is therefore lossy by whole entries and never malformed; a widened one is lossless. A key a string value would refuse as malformed makes the map malformed. Rewrite L1538 to L1541 to this.

1.3 Key kind. The reader's declaration is the reference, never the first entry. At the first entry whose key kind disagrees with it, the map resets to empty, ONE kind_mismatch is counted for the map, and the map's remaining bytes are skipped by the map's L: nothing after that entry is decoded or counted. Events counted inside earlier entries of that map stand. A value field of the wrong kind is §4's ordinary per-field event inside the entry: the value reads its default, the entry stays. A map header whose element kind is not 13 is the ordinary array kind mismatch.

1.4 One report per wire. Delete the builder forgiveness. LoadBuilder applies the same rules as Load and produces the same five counters and flag, so §4.2's oracle and §17.5's differential hold. A hand-made body must be sorted like any other; there is no tool path.

1.5 LoadMeasure. A map's term is N × sizeof(Entry) padded to alignof(Entry), at every depth. On refusal LoadMeasure answers with the spelling it already uses for a wire it refuses (the worker finds it on the page and cites it). §4.2's EXACT claim is scoped: exact for a wire that reads with an empty report; a read that reports uses at most the measure (an empty map on kind mismatch and a skipped entry both leave slots unused). Restate §4.2's bound from the smallest wire footprint that yields one storage unit, which is now an entry, not a record.

1.6 Build version and baseline. The entry record is anonymous in both. It is identified by the holder's wire id plus the field's wire id, never by its generated name. §20.1's closed table gains the map field line (an array line, element kind 13, bound N, key kind, key bound) and the entry's record line keyed as above; §20.2's grammar gains the tokens; the worker mirrors the existing token forms exactly. §18.1's shape gains a map value carrying key kind and key bound. The verdict for [..N]Pair to map[K]V is WARN, with the reason: the bytes are identical when Pair's fields are exactly key and value, and the read gains the order check, so a wire whose pairs were not ascending reads short. Strike "silent and safe". §4.1's table gains §2.8's four evolution events.

1.7 Entry name.

Entry with the field's name in PascalCase by §11's rule, cited. A map value that is itself a map names its entry ValueEntry. The entry is declared in the holder's file, immediately before the holder.

1.8 Claimed names. §11 claims the map surface:

followed by Entry, Insert, Find, Erase, Each, IndexMeasure, Index and IndexFind, and the runtime names TableMap and TableMapIndex. The entry is a table of the closure for walk, layout and cook, and it claims §11's suffixes like every table; it is not a root and has no Open, Cook, Save or Load of its own. The worker reads what #487's generator emits for a map field and makes the page's claim match that list exactly, naming internal helpers under whatever internal convention §11 already has.

1.9 An unreached non-empty map slot is refused by Cook and Lock, the same refusal §7.6 gives a pointer there, cited.

1.10 The count is int32 like every companion; §2.2's extent cap is the cap; a wire N above it is refused as any array's is, at both widths.

1.11 Integer key text is parsed by §16.2's integer rule and nothing else, so -0 is zero and anything §16.2 rejects is malformed. A malformed key stops the read by §16.1's rule; say so in §2.8 rather than leaving it to the cross-reference.

1.12 The const Find on map[K]*T returns what a pointer field's accessor returns (cite the convention). Iteration yields the stored key in the form a string field's accessor yields. Insert returns NULL for a key that does not fit and for a full map alike; NULL means not inserted, and a caller who needs the reason checks the key's length first.

2. Contradictions

2.1 A map of the holder's own type is refused as a by-value cycle (§2's rule), not for infinite size. Fix the reason.
2.2 Settled by 1.6: a was rename moves nothing because the entry has no name in the digest or the baseline.
2.3 Settled by 1.2.
2.4 Settled by 1.4.
2.5 Settled by 1.8.
2.6 cook-check verifies the entry array's containment, alignment, fit and no overlap, not the offset the layout rule computes. §7's independence sentence stands unchanged.
2.7 VERSIONING.md changes in this PR: the map row in the three-frames table and the duplicate sentence corrected to name the wire event.
2.8 Delete the status paragraph. The page states the contract; what parses on which branch belongs in the PR.
2.9 Cite §3's by-value elision rule for the empty map.

3. The #435 seam

One sentence at the head of the wire bullets: these widths are §3's today; under schema#435 ids are references, L and N are canonical LEB128, and the terminator is the zero reference. Mark the cost table as today's wire. Apply 1.10 at L1606.

4. The tests

Rewrite the control list so every control names the corpus row or instance that makes it red: #1 an instance built out of key order; #2 the shuffled report row; #3 a duplicate whose repeat elides a field the first occurrence set; #7 the report row under a changed key kind; #9 leaves the control list, it is data; #11 an erasing instance; #13 is deleted with the tool path; #15 the value half, not the counter; #17 the key bound as the isolating half. Add the missing controls: the walk-order pin (a map with *T values declared before a pointer field that reaches a shared node first), the parent-continues half of the descending rule, ONE kind_mismatch when the disagreeing entry is a later one, LoadMeasure exactness over a map of maps, and cook-check's entry-array check. Drop "reds" as a verb and every gate name the page does not define.

5. Prose

Remove the owner quotations, the history, the plans and THE SEQUENCING. State each twice-stated fact once. Split L1463 to L1468. Fix the log formula to floor(log2 n) + 1. Fix the broken bold span. No em dashes in rewritten prose. British spellings outside §2.8 are #402's and stay.

gafferongames and others added 2 commits September 4, 2026 13:30
A cold read of the maps design finds four rules underdetermined, two
sentences that contradict the rest of the page, and a control list that
names no corpus row. §2.8 now states each of them as present fact, and
the sections it touches say the same thing.

The rules the page did not carry:

- the KEY is read before the slot is chosen, by a scan of the entry's
  field headers, so field order inside a body stays non-contractual
- keys never clamp. An entry whose key does not fit the reader's bound
  is dropped and counted clamped, one per entry, and the order check
  compares the wire keys of the entries that land
- the key kind is the READER's declaration. At the first entry that
  disagrees the map reads empty, one kind_mismatch is counted for the
  map, and the rest of the map is skipped by its L
- one report per wire. The region load and LoadBuilder apply the same
  rules, so §4.2's oracle and §17.5's differential hold over a map
- LoadMeasure's term is N x sizeof(Entry) rounded up to alignof(Entry)
  at every depth, and a wire it refuses answers the -1 every measure's
  refusal answers. §4.2's EXACT claim is scoped and its bound restated
  from an entry rather than a record
- the entry is ANONYMOUS in the build version and in the baseline, keyed
  by the holder's wire id and the map field's wire id, so a was rename
  moves no line. §20.1, §20.2 and §18.1 carry the tokens
- the entry's name, its PascalCase, the nested map's form and the file
  that declares it
- §11 claims the eight names a map takes against its field, and the two
  runtime types beside them
- the slot's count is int32 like every companion, and §2.2's extent cap
  is the cap
- an integer key's text is §16.2's integer rule and nothing else, and a
  malformed key stops the read where §16.1 stops it
- the const Find on map[K]*T answers what a pointer field's accessor
  answers, and Insert answers NULL for not inserted

The contradictions:

- a map of the holder's own type is refused as a by-value cycle, not for
  infinite size
- cook-check reads containment, alignment, fit and no overlap, not the
  offset the layout rule computes, so §7's independence sentence stands
- an empty map is elided by §3's rule, not §2.3's
- the [..N]Pair edit is not silent and safe. The read gains the order
  check, so the baseline warns
- a map's entry is the one exception to "a root is any table", and §7
  says so where the rule is stated

The wire widths are marked as the wire as it stands, with schema#435's
named beside them. Every negative control names the corpus row or
instance that meets it and the one instrument that goes red, and the
missing controls are added: the walk-order pin, the parent-continues
half of the descending rule, one kind_mismatch from a later entry,
LoadMeasure over a map of maps, and cook-check's entry-array check.

The status paragraph, the sequencing, the owner quotations and the plans
are gone. The page states the contract.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ent (#380)

The three-frames table is the single statement of what an edit does, and
it had no map row. It gains one: a map's key kind changed or its key
bound tightened, across the read report, the baseline and the build
version.

The read report paragraph called duplicate the text form's counter. It
has one source on each side, and the wire's is a map's repeated key
(SPEC-TABLES.md §2.8, §4).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gafferongames gafferongames changed the title tables: maps land now — §2.8 reconciled to the tree, before the sweep (#380) docs/SPEC-TABLES.md §2.8: maps, the specification Sep 4, 2026
@gafferongames
gafferongames merged commit 9482a65 into main Sep 4, 2026
20 checks passed
@gafferongames
gafferongames deleted the maps-spec branch September 4, 2026 03:36
gafferongames added a commit that referenced this pull request Sep 4, 2026
The maps specification (#479) landed first, so the baseline rendering
version on the page is main's constant plus two, and the four overlaps
resolve to both sides: the pointer walk lists a map entry beside the set
arm, §18.2 warns on a tightened range whether it is a field's or an arm's,
and §20.2's field grammar keeps `array=map` while dropping the `stride=`
the emitter puts on `slot` lines only. `elem=` is stated for an array,
which is what an `arm` line carries.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames added a commit that referenced this pull request Sep 4, 2026
…nd SPEC.md carries the grammar (#380)

The FRONT END's half of §2.8 and §11, and the SPEC.md lines the construct was
missing.

- A QUALIFICATION OR A DEFAULT ON THE KEY is a refusal with the page's own
  sentence, not a parse error about a bracket. The grammar takes
  `map[uint32 | max = 4]int32` and `map[uint32 = 5]int32` on the terms it
  already takes `map[?uint32]`, so the diagnostic names the real problem: a key
  is an identity, and clamping an identity merges two entries.
- THE ENUM-KEY SUGGESTION never names a spelling the next compile refuses, and
  it keeps the value's array bound. `map[E][..4]int32` suggested `[E]int32`, a
  different declaration; `map[E]*V` suggested `[E]*V`, which the checker
  refuses as a named follow-on. The replacement clause now asks whether `[E]T`
  takes the value at all, and says what to do instead when it does not.
- THE MAP CLAIMS EIGHT NAMES against its field, on the rule §11 already states
  for the block form's row accessors: `<Table>` plus the field's PascalCase,
  then Entry, Insert, Find, Erase, Each, IndexMeasure, Index and IndexFind.
  The runtime names TableMap and TableMapIndex join internal/tablenames on
  TableKeyed's terms, claimed with the construct rather than with the codec,
  because a name freed now is a collision the day the codec lands.
- THE ENUM-KEY TEST fixture is renamed. It was an enum literally named `E`
  against a diagnostic that prints `[E]T`, so it could not tell a substitution
  from a hardcoded string. It is `ShipType` now.
- ir.MapEntryStructs, ir.MapKeyIsString and ir.HasMap go: scaffolding with no
  caller outside the tests. ir.MapFields lists the fields an AUTHOR WROTE, so
  a target's refusal no longer names `FleetLoadoutsEntry.value`, a generated
  name in no source file.

SPEC.md gains `map` in the reserved words and a `Map` production in the `Type`
grammar, with the same TABLE BODIES ONLY note the `?` and `*` productions
carry. #479 is SPEC-TABLES.md only, so nothing else covered it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames added a commit that referenced this pull request Sep 4, 2026
…nerated entry (#380) (#487)

* tables: the map front end — the `map[K]V` spelling, its key rules and the generated entry (#380)

The scanner takes `map` as a keyword; the AST carries the value as a whole
FIELD spelling, so a map of arrays, of optionals and of maps are one
production; the parser reads `map[K]V` wherever a field type stands.

The IR gains ir.TMap and Field.MapEntry — the generated `{ key, value }`
table, a real table of the closure with its own record, Reset and descriptor.
ir/tablemap.go carries the derived facts: the entry's name, its two constant
ids as ordinary hashes of two ordinary names, the census a refusal names.
VariableTables gains §2.8's clause (a map is a variable edge whatever its key
and value are) and TableClosure reaches through the entry.

The checker resolves the value through the ordinary field path and holds every
rule §2.8 states, each refused by name with the page's own reason: a map in a
`type` body, an enum key (naming [E]T), bool, float, flags, bits(N),
bytes(N), fixed, a type, a table, a pointer, an optional and a union keys, a
bound on the map, ?map, a default, an attribute, the by-value cycle through
the entry, and a declared table under the claimed <Table><Field>Entry name.

Every other backend refuses a map-bearing unit by name, through
compiler/tablesmaps.go per the registry split.

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

* tables: the map's wire kinds, record piece and every backend's refusal (#380)

The IR facts a map's codec will read, and the gate that keeps a half-codec
from being emitted before one exists.

- ir/tablekind.go: a map rides as kind 14 over element kind 13 — an array of
  the generated entry, no new kind and no new framing (§2.8, §3) — and
  TableTypeSpelling renders `map[K]V` so a diagnostic and a descriptor name
  the declaration the author wrote.
- ir/blocklayout.go: a map field is ONE sixteen-byte piece at eight (§2.8,
  §7.2) — an int64 self-relative reference and a uint32 count, then padding —
  one piece and not two for the reason the block form's out-of-line triple is
  one: both backends spell it as one member of a TableMap type, and a port
  that walked two would account for twelve bytes where sixteen are written.
- Every backend refuses a map-bearing unit by name, the C++ reference
  included, until its codec lands (compiler/tablesmaps.go). The refusal names
  the fields and schema#380, and target_cpp drops its call and registers
  through registerMapCarrier when its half lands.
- The zero-cost gate's header scan gains the map symbols (§2.2, §2.8): every
  unit it scans is map-free by construction, and the symbols say so
  mechanically rather than by inspection.
- The parser takes `?` on a key so the checker's OPTIONAL-key diagnostic lands
  where the spelling is, rather than a parse error naming a bracket.

Held by: compiler/tablesmaps_test.go (the derived facts at three depths, the
two constant ids, every target's refusal, and a map-free unit untouched) and
24 new rows in internal/check's refusal suite.

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

* tables: the carrier registry's second refusal shape, held by a test (#380)

refuseMaps has two shapes — no carrier yet, and a carrier named — and only the
first had a call site. The test registers cpp through registerMapCarrier and
holds the second, so the day target_cpp.go registers for real the message it
produces is already pinned. It also keeps the registry function from being
dead code between the two PRs.

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

* tables: §2.8's map example joins the doc-examples gate (#380, #460)

#482 excluded the block because maps did not parse. They do now, so the block
is anchored like every other declaration example and the header comment says
what the gate actually is: the PARSER's, so a block enters the moment its
construct has a grammar, whether or not a backend carries the codec.

Negative control: respelling the example to the REJECTED map<K, V> form turns
the gate red, naming the page, the anchor and the line.

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

* tables: a map's spelling names the value's star (#380)

ir.TableTypeSpelling rendered map[uint32]*ShipConfig as map[uint32]ShipConfig,
so a diagnostic and a descriptor would have named a declaration nobody wrote —
and map[K]T against map[K]*T is exactly the edit §3.1 spends a kind to keep
from being silent. Pinned by TestMapTypeSpelling over the three shapes.

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

* tables: schemafmt keeps the map spelling, and the pointer star binds to its type (#380)

Every command formats a source in place before it processes it, so a spelling
the formatter does not know is a spelling it silently rewrites. `needSpace` had
no case for the `map` keyword and none for the `]` that closes a map key, so
§2.8's own example came back as `map [string(32)]ShipConfig`,
`map [uint32] * ShipConfig` and `map [uint32] ?[..4]int32`: idempotent, and
away from the page for good.

The `] * ` half is not new. `[..2]*Chunk` in the stream corpus has been stored
as `[..2] * Chunk` since pointers landed, because type position was recognized
only at index 1. It is the same defect one line away, so it is fixed here.
Type position is index 1, the `]` that closes an array bound or a map key, and
the optional `?`. No expression in this grammar ends in either, so the rule
takes no case away from multiplication.

The formatter's SAFETY CHECK was blind to the construct too. Format re-parses
its own output and compares the two ASTs structurally, and the fingerprint
rendered no map at all, so two different maps and a map against a bare field
all fingerprinted alike. It renders the whole field type now: the key, the
bound, the star and a nested map included.

The gate reads §2.8's example OFF THE PAGE and compares the formatted bytes,
with the mangled spellings as its controls. Each is an input schemafmt itself
used to produce, and each must come back as the page states it.

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

* tables: the key takes no qualification, the map claims eight names, and SPEC.md carries the grammar (#380)

The FRONT END's half of §2.8 and §11, and the SPEC.md lines the construct was
missing.

- A QUALIFICATION OR A DEFAULT ON THE KEY is a refusal with the page's own
  sentence, not a parse error about a bracket. The grammar takes
  `map[uint32 | max = 4]int32` and `map[uint32 = 5]int32` on the terms it
  already takes `map[?uint32]`, so the diagnostic names the real problem: a key
  is an identity, and clamping an identity merges two entries.
- THE ENUM-KEY SUGGESTION never names a spelling the next compile refuses, and
  it keeps the value's array bound. `map[E][..4]int32` suggested `[E]int32`, a
  different declaration; `map[E]*V` suggested `[E]*V`, which the checker
  refuses as a named follow-on. The replacement clause now asks whether `[E]T`
  takes the value at all, and says what to do instead when it does not.
- THE MAP CLAIMS EIGHT NAMES against its field, on the rule §11 already states
  for the block form's row accessors: `<Table>` plus the field's PascalCase,
  then Entry, Insert, Find, Erase, Each, IndexMeasure, Index and IndexFind.
  The runtime names TableMap and TableMapIndex join internal/tablenames on
  TableKeyed's terms, claimed with the construct rather than with the codec,
  because a name freed now is a collision the day the codec lands.
- THE ENUM-KEY TEST fixture is renamed. It was an enum literally named `E`
  against a diagnostic that prints `[E]T`, so it could not tell a substitution
  from a hardcoded string. It is `ShipType` now.
- ir.MapEntryStructs, ir.MapKeyIsString and ir.HasMap go: scaffolding with no
  caller outside the tests. ir.MapFields lists the fields an AUTHOR WROTE, so
  a target's refusal no longer names `FleetLoadoutsEntry.value`, a generated
  name in no source file.

SPEC.md gains `map` in the reserved words and a `Map` production in the `Type`
grammar, with the same TABLE BODIES ONLY note the `?` and `*` productions
carry. #479 is SPEC-TABLES.md only, so nothing else covered it.

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

* tables: a map's entries are never rows, and neither projection sees the entry's name (#380)

The DERIVED FACTS half, and the cook projection's form version with it.

THE BLOCK FORM. §2.8 states that a map's entries are never rows, and it was
not so. `blockFormable` asked only whether a table is variable-length, and
`FleetShipsEntry`, a `string(32)` key and a by-value `ShipConfig`, is fixed,
so it took a block form, its counts, its storage and its projection asserts.
The rule is categorical now, because the entry can look perfectly fixed. And
the reason a table has no block form NAMES THE EDGE THE WALK FOUND, a pointer,
a map, or the member that reaches one, instead of the hardcoded sentence about
a pointer, which was emitted verbatim over a table whose only variable edge was
a map.

THE TWO PROJECTIONS. A generated entry is ANONYMOUS in both (§18.1, §20.2):
its record line carries the holder's wire id and the map field's wire id
joined by a dot, in place of a name, and both projections SORT over that text.
The entry's generated name is derived from the field's source spelling, so a
`was` rename moved a line, moved the record order and invalidated every cooked
file, under an edit that moves no byte. A test pins that the rename now moves
neither the projection nor the build version.

The map field's line is an array line: `kind=14`, `array=map`, `elem=` the
generated entry's own storage size, which is the pitch its entries lie at and
what `elem=` means on every other array line this projection writes, and no
`bound=`, because a map declares no extent. The KEY's kind and capacity ride on
the entry's own `key` line, which is where a key edit moves the id.

THE BASELINE gains the map shape token and the key's two facts beside it, and
the two policy rows that judge them. A key kind is fixed, because a key
arriving under a kind the reader does not declare resets the whole map to
empty; a key bound is an extent, because an entry whose key does not fit is
skipped whole and counted. The `[..N]Pair` to `map[K]V` edit is one WARN with
the page's reason, and it is one finding rather than four: the tokens the
construct owns describe the same edit, so judging them beside the shape would
say it three times and say it harsher. Two new judged tokens bump the baseline
rendering version to 5, and the two committed baselines are regenerated with
their history intact.

THE COOK PROJECTION'S FORM VERSION moves to 2, because this rendering emits
tokens the last one did not. Every unit's build version moves with it, map-free
ones included, which is what a form version is for. §20.2's worked number,
compiler's pin, docs/USAGE.md's printed number, the build-version goldens, the
generated headers' BuildVersion constants and every cooked conformance
artifact are re-pinned in this commit.

THE ENTRY IS NOT A ROOT. §2.8 makes it the one exception to §7's "a root is
any table": it is reached only through the map that generates it, so the C++
reference emits it no Open, no Cook, no Save and no Load, and its walk, its
layout and its cook body are the whole of what it carries.

§11's Maps list gains the two keys the checker already refuses and the page
did not name: a 128-bit integer and a fixed-point key. The map slot's count is
an int32 like every companion, and the IR says so.

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.

1 participant