Skip to content

table: use the table for messages with map fields - #477

Merged
iainmcgin merged 5 commits into
table-oneoffrom
table-map
Oct 10, 2026
Merged

iainmcgin merged 5 commits into
table-oneoffrom
table-map

Conversation

@iainmcgin

Copy link
Copy Markdown
Collaborator

Messages with map fields use the table under CodecStrategy::Table. A map field is one table entry of kind Map. Its MapVt holds the key and value kinds and two functions instantiated for the collection type (iterate, insert a decoded entry); keys and values go through the existing interpreters. A map value whose message has no table is reached through its Message impl, as in #475. Stacked on #476.

On whatsapp.proto (size text bytes, fat LTO, panic=abort, owned types only), all 752 messages use the table (749 on #476; the three left had maps). Moving them adds 6,824 bytes at z, the map interpreters. Per map field, on a synthetic schema of 600 maps over 96 key/value type pairs, table against unrolled, in text bytes: 152 vs 284 at z, 425 vs 434 at s, 536 vs 465 at 3.

Time, bare metal (c7i.metal-24xl), opt-level=3, table over unrolled at 8, 64 and 512 entries: decode 1.51x, 1.30x, 1.34x; encode 2.22x, 2.00x, 2.03x; compute_size 1.48x, 1.49x, 1.45x. One run, ±5%.

Decoding mirrors unrolled code: element-memory and recursion limits, unknown closed-enum entries kept as unknown fields, and a failed merge leaves the same state. One difference: clear() on a table message releases map capacity, where unrolled code keeps it.

The loops test for a map before dispatching. Naming the map interpreters in the dispatch arms makes opt-level=3 builds 2.6% larger on a schema without maps and 4% larger on whatsapp.proto.

buffa::table gains public MapVt, DirectMsgVt and KindMarker (and KindSlot now requires KindMarker), documented as unstable support code. A custom collection, string or bytes type in a map still falls back, with the reason "has a field with a custom string, bytes or collection type".

@github-actions

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@iainmcgin
iainmcgin added this pull request to stack #497 October 1, 2026 18:26
@iainmcgin
iainmcgin marked this pull request as ready for review October 1, 2026 18:47
zayedu pushed a commit to zayedu/buffa that referenced this pull request Oct 2, 2026
Adds `CodecStrategy::Table`, the generator for the table codec proposed
in anthropics#463. `Unrolled` stays the default. Stacked on anthropics#468 (the
`buffa::table` runtime), which is stacked on anthropics#467.

```rust
buffa_build::Config::new()
    .codec_strategy(CodecStrategy::Table)
    .codec_strategy_in(CodecStrategy::Unrolled, &[".wa.Message"])
```

The plugin takes `codec_strategy=table` and repeatable
`codec_strategy_in=<path>=<strategy>`. Rules match like
`preserve_unknown_fields_in`: prefix, last match wins, rules over the
global setting. A message with a `oneof`, `map` or group field, a custom
string, bytes or collection type, `MessageSet`, or extension ranges with
JSON stays unrolled, and so does every message that holds one.
`table_plan.rs` computes that closure and reports it in one
`TableCodecFallbackSummary` warning, silent when the user's own
`Unrolled` rule is the cause. A rule that names such a message by its
exact path is an error. `compile()` errors on rustc older than 1.77, and
the MSRV job now also tests the table code on 1.77, so its timeout goes
from 10 to 20 minutes.

The 1,644 KB to 817 KB figure in anthropics#463 was measured with oneofs and maps
flattened, so a real schema saves less until the follow-up that lets
table messages hold unrolled, extern and well-known-type children.

The conformance suite does not run under `Table`: `TestAllTypesProto3`
has oneofs and maps, so its messages fall back. Parity rests on
`buffa-test`, which compiles each schema twice under renamed packages
and compares bytes, sizes, decoded values and errors, including on
truncated, bit-flipped and noise input. Under an experiment that set all
66 `buffa-test` protos to `Table`, 602 tests pass with 119 table
messages.

A table message differs from an unrolled one in three ways, documented
in the guide: a length past the end of its enclosing message fails at
once with `UnexpectedEof`; `merge_field` cannot gather a non-contiguous
buffer, so a type that another crate or run uses as a group or
`DELIMITED` field must stay `Unrolled`; and `clear()` releases capacity.
The generated code is tied to `buffa::table`, so regenerate it whenever
`buffa` updates.

About 1,300 lines are outside test files, well over the 250-line
guideline.


Followed by anthropics#475 (message fields that hold messages without a table),
anthropics#476 (`oneof`) and anthropics#477 (`map`), stacked in that order.
azdagron
azdagron previously approved these changes Oct 7, 2026
@iainmcgin
iainmcgin added this pull request to the merge queue Oct 7, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue because a pull request earlier in the stack was removed Oct 7, 2026
A map field is one table entry of kind Map whose descriptor holds the
entry's key and value kinds and two functions instantiated for the
collection type: one to iterate it, one to insert a decoded entry. The
key and value are read and written through the interpreters that
already exist, so a map adds one interpreter arm per pass and no code
per key or value type.

Decoding mirrors the unrolled codec: each entry counts against the
element-memory limit, a message value counts against the recursion limit,
an entry with an unknown closed-enum number is kept whole as an unknown
field, and a repeated key or value in one entry keeps the last.
The planner accepts a map with the default collection and default
string and bytes types, and follows the value of a map of messages as
a child. A custom collection or element type still falls back.
Every key type, every value type, entries of unusual shape, closed and
open enum values, the element-memory, unknown-field and recursion limits,
and HashMap and BTreeMap collections.
The guide, DESIGN.md, the CodecStrategy documentation and the table module
documentation listed maps among the fields that keep a message unrolled.
@iainmcgin
iainmcgin added this pull request to the merge queue Oct 10, 2026
Merged via the queue into main with commit 770160a Oct 10, 2026
11 checks passed
@iainmcgin
iainmcgin deleted the table-map branch October 10, 2026 18:41
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 10, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants