Skip to content

runtime: add map_codec, replace inline map-entry wire loops in owned codegen#194

Merged
iainmcgin merged 4 commits into
mainfrom
runtime/map-codec
Jun 18, 2026
Merged

runtime: add map_codec, replace inline map-entry wire loops in owned codegen#194
iainmcgin merged 4 commits into
mainfrom
runtime/map-codec

Conversation

@iainmcgin

Copy link
Copy Markdown
Collaborator

What this does

Adds a buffa::map_codec module and replaces the ~40–50-line inline entry size/write/merge expansion that generated code emitted per owned map field. The per-proto-type variation (int32/sint32/sfixed32 all map to i32 with different encodings) is captured as zero-sized codec types; generated call sites name codecs by turbofish and let the map's own types drive inference, so codegen needs no type-path resolution.

  • field_len / write_field — scalar-valued maps; the fixed-width fast path (len() * const entry) is preserved via MapCodec::FIXED_LEN, which folds at compile time.
  • message_field_len / write_message_field — message-valued maps, preserving the two-pass SizeCache slot discipline (both passes iterate the same map, so slot order matches by construction).
  • merge_entry — all maps: entry-limit arithmetic, defaults, unknown-field skip, position correction, identical to the previous inline expansion.

Everything monomorphizes to the previous code; map wire output is unchanged. Closed-enum map values keep the existing drop-unknown behaviour (documented known gap). View encode paths keep their duck-typed inline emission (view maps store borrowed element types); borrow-generic view codecs are a possible follow-up.

One adaptation relative to the original branch: MapValueDecode::merge and merge_entry take the DecodeContext introduced by #184 (recursion + unknown-field budgets) instead of a bare depth, matching the inline expansion they replace on current main.

Checked-in generated trees regenerated.

How we know it works

  • map_codec unit tests: round-trips for every codec family, defaults, unknown-entry-field skip, wire-type mismatch, truncation, two-pass message-map size/write agreement.
  • Full workspace suite green (2,089 tests), task lint clean.
  • Conformance (14/14 suites, Docker-less scripts from tooling: run the conformance suite without Docker #192) run at the head of this PR stack; the CI conformance job validates this PR independently.

Stacked on #193.

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown

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

The Docker path needs the private GHCR tools image (or a local image
build) plus a Docker daemon — unavailable in some dev environments.
conformance_test_runner drives the testee over stdin/stdout pipes, so
nothing actually requires a container:

- scripts/build-conformance-tools.sh (task conformance-tools-local):
  native equivalent of Dockerfile.tools — pre-installs jsoncpp, builds
  conformance_test_runner from the pinned protobuf tag into .local/bin/
  (host-linked; the image's -static is only needed for distribution),
  and populates conformance/protos/ with the same proto set.
- scripts/run-conformance-local.sh (task conformance-local): native
  equivalent of run-conformance.sh + the image build — cargo-builds the
  std/no_std conformance binaries and executes the same seven runs
  (including via-lazy) with the same failure lists and CONFORMANCE_OUT
  logging.
Every generated decode arm (merge_field, view decode, lazy record arms,
map-entry loops) emitted a 7-line inline wire-type comparison +
WireTypeMismatch construction — ~1,100 copies across the generated
corpus, the single largest block of generated boilerplate (~8%).

Add ::buffa::encoding::check_wire_type(tag, expected) with a #[cold]
out-of-line wire_type_mismatch constructor, and rewrite the codegen
wire_type_check helper to emit the one-line call. Repeated-field arms
that accept two wire types (packed + unpacked) call the constructor
directly, reporting the packed form as expected — same error payload as
before. Moving error construction out of the per-field decode loop is
icache-friendly; the hot path keeps a single inline comparison.

The lazy-view record arms (added since this change was first written)
go through the same helper, so the fold covers the lazy family too.

Checked-in generated trees regenerated (buffa-types, buffa-descriptor);
error values are byte-identical so wire behaviour is unchanged.
@iainmcgin iainmcgin force-pushed the codegen/check-wire-type branch from bc3f644 to 0962e45 Compare June 12, 2026 21:30
…codegen

Every owned map field expanded ~40-50 lines of entry size/write/merge
code that varied only by the key/value proto types. Capture the
variation as zero-sized per-proto-type codecs (proto types, not Rust
types — int32/sint32/sfixed32 share i32 with different encodings) in a
new buffa::map_codec module, plus generic field-level helpers:

  field_len / write_field          scalar-valued maps
  message_field_len /
    write_message_field            message-valued maps (preserves the
                                   two-pass SizeCache slot discipline:
                                   both passes iterate the same map, so
                                   slot order matches by construction)
  merge_entry                      all maps (entry-limit arithmetic,
                                   defaults, unknown-field skip,
                                   position correction — identical to
                                   the previous inline expansion)

Generated call sites name codecs by turbofish and let the map's own
types drive inference (Open<_>/Closed<_>/Msg<_>), so codegen needs no
type-path resolution. Everything monomorphizes to the previous code;
the fixed-width fast path (len() * const entry) is kept via
MapCodec::FIXED_LEN, which folds at compile time. Closed-enum map
values keep the existing drop-unknown behaviour (documented known gap).

View encode methods keep the previous duck-typed inline emission
(view maps store borrowed element types and view message values are
not buffa::Message), retained as map_view_* emitters; borrow-generic
codecs for views are a possible follow-up.

Checked-in generated trees regenerated; map wire output is unchanged.

Adapted to the DecodeContext threading introduced by the unknown-field
limit work (#184): MapValueDecode::merge and merge_entry take
DecodeContext instead of a bare depth, message values forward it to
merge_length_delimited, and unknown entry fields skip via ctx.depth() —
matching the inline expansion this replaces on current main.
@iainmcgin iainmcgin force-pushed the runtime/map-codec branch from 0f31feb to 2b7e734 Compare June 12, 2026 21:30
Base automatically changed from codegen/check-wire-type to main June 17, 2026 01:38
Resolve conflicts from #192 and #193 being squash-merged onto main while
their pre-squash commits remained on this branch:

- impl_message.rs: keep this branch's map_codec rewrite, which already
  carries the check_wire_type refactor; main's only change here was that
  same refactor.
- CHANGELOG.md: keep both the check_wire_type and map_codec entries under
  Unreleased.
- Regenerate checked-in WKT and bootstrap descriptor types; only the
  descriptor view changes, picking up the proto2 required-field presence
  work from main.
@iainmcgin iainmcgin marked this pull request as ready for review June 18, 2026 19:09
@iainmcgin iainmcgin requested a review from rpb-ant June 18, 2026 19:09
@iainmcgin iainmcgin enabled auto-merge June 18, 2026 19:09
@iainmcgin iainmcgin added this pull request to the merge queue Jun 18, 2026
iainmcgin added a commit that referenced this pull request Jun 18, 2026
Propagate #194's merge of main (which carries #192, #193, #199, #200) up
the stack. Clean merge: the put_*_field writer changes don't overlap the
map_codec/main changes, and the merge base already contains the #192/#193
commits, so no squash-merge-artifact conflict this time. Generated trees
unchanged.
Merged via the queue into main with commit 71965c2 Jun 18, 2026
7 checks passed
@iainmcgin iainmcgin deleted the runtime/map-codec branch June 18, 2026 19:14
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 18, 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