Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions docs/SPEC-TABLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1699,16 +1699,34 @@ about a value's storage is the map's:
field has (§2.1, §2.5), one member, and the node it names takes its index
where the map is reached.

**AN ARRAY OF POINTERS IS ITS ARRAY FORM'S ROW OVER A REFERENCE ELEMENT**
(§2.1, §4.2). The element is a pointer, so the ELEMENT is the eight-byte
reference and the ARRAY FORM decides the rest, exactly as the same three
spellings decide it at any other field:

- `[N]*T` stores `N` references, one member;
- `[..N]*T` stores `N` references beside its `int32` used count, two members;
- `[]*T` stores the sixteen-byte list slot over reference elements, one member.

Each slot names a node of the walk below, so two slots may name one node and a
slot may name none, and nothing about that is the map's either. `[E]*T`,
`[N]*string` and `[]*bytes` are refused by name (§2.4, §15), and the refusal
reaches a map's value at the entry, because the value is a field of a table
nobody wrote.

**THE HANDLE FOLLOWS THE STORAGE.** `Insert`, `Find` and `Each` hand back a
pointer to the `value` member where the storage is ONE member, and the ENTRY
where it is two, because two members are not one addressable slot and a caller
that cannot set the length or the count cannot fill the value. A `[N]T` value's
handle points at the ARRAY rather than at its first element, so the extent
survives the handoff. A `*T`, `*string` or `*bytes` value's BUILDER handle is
the SLOT, which is what an `Emplace` fills, and the const `Find` answers the
RESOLVED node, one add on the self-relative delta. Where the handle is the
entry, the caller fills `value` and its companion and leaves `key` to the map,
which owns the order the key carries.
RESOLVED node, one add on the self-relative delta. THE ARRAY FORM DECIDES AN
ARRAY OF POINTERS' HANDLE, NEVER THE ELEMENT: a `[N]*T` hands back the array of
references, a `[..N]*T` the entry, a `[]*T` the list slot, and the caller
resolves each slot as it resolves any pointer. Where the handle is the entry,
the caller fills `value` and its companion and leaves `key` to the map, which
owns the order the key carries.

**And a map is a BY-VALUE EDGE of the ONE declaration-order walk** (§3.1,
schema#438). The numbering, the pack measure and the pack are one walk over
Expand Down Expand Up @@ -6158,7 +6176,9 @@ without a rule of its own.** The value is an ordinary field of the generated
entry and its storage is that field's own row (§2.8), so an entry's body
carries a field header, a length and a payload of the value's own kind: an
array's `N` and ELEMENT KIND where the value is `[N]T`, `[..N]T`, `[E]T` or an
unbounded `[]T`; a kind `17` NODE INDEX where it is `*T`, `*string` or
unbounded `[]T`, and that element kind is `17` where the element is a pointer,
so a `[N]*T`, a `[..N]*T` and a `[]*T` are the array strategies over node
indices; a kind `17` NODE INDEX where the value is `*T`, `*string` or
`*bytes`, and the blob record it names is a record of the node table like any
other; and the kind `12` and kind `33` payloads the text strategies above name.
The strategies are enumerated over field positions, so each lands inside an
Expand Down
69 changes: 48 additions & 21 deletions internal/codegen/cpptable/maps.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,45 +82,70 @@ func mapKeyOrderType(f *ir.Field) string {
return "uint64_t"
}

// mapValueIsPointer reports a `map[K]*T` — the value is a pointer SLOT, so the
// const form's Find answers the resolved `const T *` (docs/SPEC-TABLES.md §2.8).
func mapValueIsPointer(f *ir.Field) bool { return ir.MapValueField(f).Type.Pointer }
// mapValueIsPointer reports a `map[K]*T`, whose value is ONE pointer SLOT, so
// the const form's Find answers the resolved `const T *` (docs/SPEC-TABLES.md
// §2.8). AN ARRAY OF POINTERS IS NOT THIS CASE. `[N]*T`, `[..N]*T` and `[]*T`
// store a `TableRef` PER ELEMENT (§2.1, §4.2), so each takes the arm its array
// form takes and carries `TableRef` as the element; a predicate that read
// `Type.Pointer` alone sent all three here and spelled `<T>At` on an array.
// `[E]*T` is refused by name (§2.4, §15) and reaches no arm at all.
func mapValueIsPointer(f *ir.Field) bool {
value := ir.MapValueField(f)
return value.Type.Pointer && value.Array == ir.ArrayNone && value.KeyEnum == ""
}

// mapValueIsPair reports a value whose storage is TWO MEMBERS: the buffer or
// the element array, and the `int32` length or count beside it that says how
// much of it is used (docs/SPEC-TABLES.md §2.8, §4.2, §7.2). That is
// `string(N)`, `wstring(N)`, `bytes(N)` and `[..N]T`. Each is an ordinary
// field of the generated entry, and two members are not one addressable slot,
// so the handle every map surface hands back is the ENTRY, which reaches both.
// `string(N)`, `wstring(N)`, `bytes(N)`, `[..N]T` and `[..N]*T`. Each is an
// ordinary field of the generated entry, and two members are not one
// addressable slot, so the handle every map surface hands back is the ENTRY,
// which reaches both. A COUNTED ARRAY IS A PAIR WHATEVER ITS ELEMENT IS: a
// pointer element changes the element's storage to a `TableRef` and changes
// nothing about the count beside it.
func mapValueIsPair(f *ir.Field) bool {
value := ir.MapValueField(f)
if value.Type.Pointer {
return false
}
if value.Array == ir.ArrayCounted {
return true
}
if value.Type.Pointer {
return false
}
switch value.Type.Kind {
case ir.TString, ir.TWString, ir.TBytes:
return true
}
return false
}

// mapValueIsFixedArray reports a `[N]T` value. Its storage is ONE member, an
// array of a fixed extent (docs/SPEC-TABLES.md §4.2), and a pointer to that
// member keeps the extent. `[E]T` is not this case: it stores a
// mapValueIsFixedArray reports a `[N]T` or `[N]*T` value. Its storage is ONE
// member, an array of a fixed extent (docs/SPEC-TABLES.md §4.2), and a pointer
// to that member keeps the extent. `[E]T` is not this case: it stores a
// `TableKeyed<T, E>`, which is an ordinary one-member type spelling.
func mapValueIsFixedArray(f *ir.Field) bool {
value := ir.MapValueField(f)
return !value.Type.Pointer && value.KeyEnum == "" && value.Array == ir.ArrayFixed
return value.KeyEnum == "" && value.Array == ir.ArrayFixed
}

// mapValueArrayAlias names the array type a `[N]T` value's handle points at.
// A return type cannot spell `T (*)[N]` inline without wrapping the declarator
// around the function name, so the alias is what makes the handle readable.
func mapValueArrayAlias(f *ir.Field) string { return mapEntryOf(f).Name + "Value" }

// mapValueElementType is the C++ type ONE ELEMENT of an array value is stored
// as: a `TableRef` where the element is a pointer, which is the row §4.2 gives
// an array of pointers at a field, and the element's own type otherwise
// (docs/SPEC-TABLES.md §2.1, §4.2).
func (g *tableGen) mapValueElementType(f *ir.Field) string {
value := ir.MapValueField(f)
if value.Type.Pointer {
g.noteRef(value.Type.Name)
return "TableRef"
}
typ, _ := g.cppFieldType(value.Type)
return typ
}

// ---- the runtime (docs/SPEC-TABLES.md §2.8) ----

// tableMapRuntime is the map half of the variable-length runtime: the storage
Expand Down Expand Up @@ -886,10 +911,10 @@ func (g *tableGen) emitMapEntrySurface(owner *ir.Struct, f *ir.Field) {
g.pf("inline const %s * TableEntryFound( const %s * entry ) { return entry != NULL ? %sAt( entry->value ) : NULL; }\n", t, n, t)
g.pf("inline TableRef * TableEntryValue( %s * entry ) { return &entry->value; } // the builder hands back the SLOT\n", n)
case mapValueIsPair(f):
// a map[K]string(N), map[K]wstring(N), map[K]bytes(N) or map[K][..N]T:
// the value's storage is the buffer or the element array and the int32
// used length or count beside it (§2.8, §4.2, §7.2), two members, so
// the ENTRY is the handle that reaches both. A caller fills
// a map[K]string(N), map[K]wstring(N), map[K]bytes(N), map[K][..N]T or
// map[K][..N]*T: the value's storage is the buffer or the element array
// and the int32 used length or count beside it (§2.8, §4.2, §7.2), two
// members, so the ENTRY is the handle that reaches both. A caller fills
// `entry->value` and its companion, and leaves `entry->key` to the
// map, which owns the sort the key carries.
g.pf("// THIS VALUE'S STORAGE IS A PAIR (§2.8, §4.2, §7.2): `value` beside\n")
Expand All @@ -898,10 +923,12 @@ func (g *tableGen) emitMapEntrySurface(owner *ir.Struct, f *ir.Field) {
g.pf("inline const %s * TableEntryFound( const %s * entry ) { return entry; }\n", n, n)
g.pf("inline %s * TableEntryValue( %s * entry ) { return entry; }\n", n, n)
case mapValueIsFixedArray(f):
// a map[K][N]T: the value's storage is ONE member, an array of a fixed
// extent (§4.2), so the handle is a pointer to the ARRAY and keeps
// that extent. The alias is what lets a return type spell it.
elem, _ := g.cppFieldType(value.Type)
// a map[K][N]T or map[K][N]*T: the value's storage is ONE member, an
// array of a fixed extent (§4.2), so the handle is a pointer to the
// ARRAY and keeps that extent. A pointer element makes that element a
// `TableRef` (§2.1) and changes nothing else. The alias is what lets a
// return type spell it.
elem := g.mapValueElementType(f)
alias := mapValueArrayAlias(f)
g.pf("// A FIXED ARRAY VALUE IS ONE MEMBER (§2.8, §4.2): the handle points at the\n")
g.pf("// ARRAY and not at its first element, so the extent survives the handoff.\n")
Expand Down
14 changes: 14 additions & 0 deletions tables/maps/Crews.schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package mapdemo

// A map whose VALUE is a COUNTED ARRAY OF POINTERS (docs/SPEC-TABLES.md §2.1,
// §2.8): "A VALUE is anything a table field can hold ... `[..N]T`", and the
// element is a pointer, so the value is an ordinary field of the generated
// entry and its storage is that field's own row, `TableRef value[N]` beside
// its `int32` used count (§4.2). Two members are not one addressable slot, so
// the handle every map surface hands back is the ENTRY, which reaches both.

table Crews
{
members map[uint32][..2]*Item // [..N]*T: pointer slots and the used count beside them
after int32
}
15 changes: 15 additions & 0 deletions tables/maps/Pairs.schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package mapdemo

// A map whose VALUE is a FIXED ARRAY OF POINTERS (docs/SPEC-TABLES.md §2.1,
// §2.8): "A VALUE is anything a table field can hold ... `[N]T`", and the
// element is a pointer, so the value is an ordinary field of the generated
// entry and its storage is that field's own row, `TableRef value[N]` (§4.2).
// That is ONE member, so the handle points at the ARRAY and keeps the extent.
// Every slot names a node, and two slots may name one node exactly as two
// pointer fields may.

table Pairs
{
slots map[uint32][2]*Item // [N]*T: a fixed extent of pointer slots
after int32
}
15 changes: 15 additions & 0 deletions tables/maps/Trails.schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package mapdemo

// A map whose VALUE is an UNBOUNDED ARRAY OF POINTERS (docs/SPEC-TABLES.md
// §2.1, §2.8, §2.9): "A VALUE is anything a table field can hold ... and an
// unbounded `[]T` (§2.9)", and the element is a pointer, so the value is an
// ordinary field of the generated entry and its storage is that field's own
// row, the sixteen-byte list slot over `TableRef` elements (§4.2). That is one
// member, so the handle is the slot itself and the list's own surface reaches
// the pointer slots, which ride in the HOLDER'S node extent as any list's do.

table Trails
{
steps map[uint32][]*Item // []*T: an unbounded extent of pointer slots
after int32
}
27 changes: 27 additions & 0 deletions tables/maps/tables.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ table 0a53e00afba279af.7b024c46e98d3404
field key id=0x3dc94a19365b10ec kind=8
field value id=0x7ce4fd9430e80cea kind=17 type=ShipConfig

table 1404200dab337086.e68c2e6bb1ee5646
field key id=0x3dc94a19365b10ec kind=8
field value id=0x7ce4fd9430e80cea kind=14 elem=17 type=Item array=fixed bound=2

table 1f781dc01a2b5152.a3a7061ff10a8138
field key id=0x3dc94a19365b10ec kind=12 size=8
field value id=0x7ce4fd9430e80cea kind=14 elem=4 array=fixed bound=3
Expand Down Expand Up @@ -57,6 +61,10 @@ table Chunks
field blobs id=0x14e2eaab9cde925b kind=14 elem=13 array=map keykind=4
field after id=0xbf82010f6f71eae9 kind=4

table Crews
field members id=0x79d594675e391090 kind=14 elem=13 array=map keykind=8
field after id=0xbf82010f6f71eae9 kind=4

table Depth
field one id=0x1a08aa1921ca5caf kind=13 type=Squad
field many id=0x1f6459a2cea1fc02 kind=14 elem=13 type=Squad array=bounded bound=3
Expand All @@ -83,6 +91,10 @@ table Fleet
table Item
field count id=0xb1e5e28e4479a274 kind=4

table Pairs
field slots id=0xe68c2e6bb1ee5646 kind=14 elem=13 array=map keykind=8
field after id=0xbf82010f6f71eae9 kind=4

table Row
field entries id=0xc5b2a72c0845a253 kind=14 elem=13 array=map keykind=12 keybound=8
field after id=0xbf82010f6f71eae9 kind=4
Expand Down Expand Up @@ -112,6 +124,10 @@ table Text
field blobs id=0x14e2eaab9cde925b kind=14 elem=13 array=map keykind=4
field after id=0xbf82010f6f71eae9 kind=4

table Trails
field steps id=0x124250ad5a5b6d14 kind=14 elem=13 array=map keykind=8
field after id=0xbf82010f6f71eae9 kind=4

table WideRow
field entries id=0xc5b2a72c0845a253 kind=14 elem=13 array=map keykind=8
field after id=0xbf82010f6f71eae9 kind=4
Expand All @@ -124,6 +140,14 @@ table b413964e3571a316.c5b2a72c0845a253
field key id=0x3dc94a19365b10ec kind=8
field value id=0x7ce4fd9430e80cea kind=13 type=Item

table b4578774a78fb150.124250ad5a5b6d14
field key id=0x3dc94a19365b10ec kind=8
field value id=0x7ce4fd9430e80cea kind=14 elem=17 type=Item array=unbounded

table c85b940060088651.79d594675e391090
field key id=0x3dc94a19365b10ec kind=8
field value id=0x7ce4fd9430e80cea kind=14 elem=17 type=Item array=bounded bound=2

table e8130af045a036f8.2b7dea192bb7be29
field key id=0x3dc94a19365b10ec kind=9
field value id=0x7ce4fd9430e80cea kind=13 type=Item
Expand Down Expand Up @@ -170,3 +194,6 @@ union Force

### 2026-09-07 (UTC) — Cells, Runs, Slots, Spans, Docs and Chunks: one map per value kind that carries an extent or names a buffer node (#628)
- no compatibility-affecting edits; the wire absorbs the rest

### 2026-09-07 (UTC) — Pairs, Crews and Trails: one map per value kind whose element is a pointer (#666)
- no compatibility-affecting edits; the wire absorbs the rest
4 changes: 4 additions & 0 deletions test/conformance/harness/maps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ func TestTheToolWritesTheReferencesMapBytes(t *testing.T) {
// unbounded array, a text buffer and a byte buffer, each under a map key
{"map_cells", "Cells"}, {"map_runs", "Runs"}, {"map_slots", "Slots"},
{"map_spans", "Spans"}, {"map_docs", "Docs"}, {"map_chunks", "Chunks"},
// one row per value kind whose ELEMENT is a pointer (#666): a fixed
// array, a counted array and an unbounded array of node references,
// each carrying a shared node and a slot that names none
{"map_pairs", "Pairs"}, {"map_crews", "Crews"}, {"map_trails", "Trails"},
} {
name, rootName := tc.name, tc.root
data, err := os.ReadFile(root + "testdata/wire/tables/" + name + ".bin")
Expand Down
Loading
Loading