Skip to content

Externalize size cache to remove __buffa_cached_size struct field#22

Draft
iainmcgin wants to merge 2 commits intomainfrom
size-cache-external
Draft

Externalize size cache to remove __buffa_cached_size struct field#22
iainmcgin wants to merge 2 commits intomainfrom
size-cache-external

Conversation

@iainmcgin
Copy link
Copy Markdown
Collaborator

Closes #14.

Problem

Generated structs carried a hidden __buffa_cached_size: CachedSize (AtomicU32) field. This made exhaustive destructuring impossible — let Msg { a, b } = m; forced a .. that then absorbed any new proto fields too, defeating the use case of having the compiler flag every destructure site when the schema grows.

Approach

Replace the in-struct cache with an external SizeCache (a Vec<u32> + cursor) threaded through the trait methods:

  • compute_size(&self, cache: &mut SizeCache) reserves slots in pre-order (reserve before recursing into a length-delimited nested message, set after)
  • write_to(&self, cache: &mut SizeCache, buf) consumes in the same pre-order via cache.next_size()
  • Groups thread the cache to children without reserving (no length prefix)

Both passes are generated from the same field list, both take &self, both use identical presence guards — traversal order is structurally guaranteed. The provided encode* methods construct and discard a fresh cache internally; a new encoded_len() provided method covers the "just want the size" case.

Side fix: map_write_to_stmt previously recomputed v.compute_size() inside the entry_size formula during write — idempotent with the old in-struct cache, but would corrupt a push-based one. Now reads the value size from the cache once. Minor win: map message values are walked twice instead of three times.

Generated structs now contain only proto fields plus __buffa_unknown_fields. No interior mutability remaining — Sync comes for free instead of via AtomicU32-with-Relaxed.

Verification

  • task lint clean
  • task test — 221 pass, 0 fail
  • task conformance — all 6 suites PASSED, 0 unexpected failures
  • Source diff net: −235 lines (excluding generated/tests)

Generated structs previously carried a hidden __buffa_cached_size: CachedSize
(AtomicU32) field that compute_size wrote and write_to read. This made
exhaustive destructuring impossible — let Msg { a, b } = m; forced a ..
that then absorbed any new proto fields too, defeating the use case of
having the compiler flag every destructure site when the schema grows.

Replace the in-struct cache with an external SizeCache (Vec<u32> + cursor)
threaded through the trait methods. compute_size reserves slots in pre-order
(reserve before recursing into a length-delimited nested message, set after),
write_to consumes in the same pre-order. Both passes are generated from the
same field list, both take &self, both use identical presence guards — the
traversal order is structurally guaranteed to match. Groups thread the cache
without reserving (no length prefix).

The provided encode* methods construct and discard a fresh cache internally,
so the common case is unchanged at call sites. A new encoded_len() provided
method replaces direct compute_size() calls for users who just want the size.

Side fix: map_write_to_stmt previously recomputed v.compute_size() inside
the entry_size formula during write — idempotent with the old in-struct
cache, but would corrupt a push-based one. Now reads the value size from
the cache once and derives entry_size from it. Also a minor win: map message
values are no longer walked three times (compute, entry_size-recompute,
write) but twice.

Generated structs now contain only proto fields plus __buffa_unknown_fields.
No interior mutability remaining — Sync comes for free instead of via
AtomicU32-with-Relaxed contortion.

Closes #14.
…essage

Review follow-up:

- map_compute_size_stmt uses .values() when key size is constant, while
  map_write_to_stmt always uses for (k, v). The cache-slot order between
  these must match. For HashMap (both std and hashbrown) it does — both
  walk the table in slot order — but this is not obvious from a cold read.
  Added a comment at the optimization site.

- next_size() previously panicked with a generic index-out-of-bounds
  message. Now reports cursor position vs slot count, with #[track_caller]
  so the panic location points at the caller. Helps manual Message
  implementers diagnose traversal-order mismatches.

- Dropped model: opus from the rust-code-reviewer agent definition.
  That model was emitting text-based pseudo-tool-call syntax the harness
  does not parse (tool_uses: 0 on every run), so all findings were
  confabulated from nonexistent file content. Inherits the main loop
  model now.
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.

__buffa_cached_size makes destructuring harder

1 participant