engine: move protobuf code from prost to buffa - #1068
Conversation
project_io.proto now generates with buffa-build and links the buffa runtime. Nothing else in the workspace depended on prost, so prost and prost-derive leave the dependency graph entirely. libsimlin and simlin-cli reach the runtime through the engine's `pub use buffa` instead of declaring their own version, and the generator pins buffa-build to the version Cargo.lock resolves: generated code calls runtime internals that semver does not cover. The codegen options keep the generated types close to prost's so the conversions in serde.rs stay mechanical: one file, no zero-copy views, unknown fields dropped (no hidden bookkeeping field, so struct literals still work), idiomatic field names (`view_box`), and inline oneof variants. The remaining type differences are open enums as `EnumValue<E>` instead of raw i32 and singular messages as `MessageField<T>` instead of `Option<T>`; unknown enum values still map to the default. Decoding keeps buffa's default limits, including the 32 MiB element-memory budget: C-LEARN, the largest corpus model, needs 1.5 MiB of it, and the budget bounds what a crafted payload can make a decode allocate, which prost never bounded. simlin_project_serialize_protobuf uses the fallible encode, since buffa's panicking one would abort the process on a project past the 2 GiB limit.
open_binary read its input with read_until(0), which stops at the first zero byte. Every real project's encoding contains zero bytes (a double such as `dt = 1.0` is six of them), so reading a .pb file failed with a decode underflow, on main as well: the path had no test. It now reads the whole input and reports the I/O error it hit rather than the string "1".
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1068 +/- ##
==========================================
- Coverage 93.29% 92.05% -1.25%
==========================================
Files 282 282
Lines 165608 171663 +6055
==========================================
+ Hits 154509 158025 +3516
- Misses 11099 13638 +2539 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Code ReviewReviewed the non-generated changes: the generator config in No blocking findings. Behavior notes worth flagging (already documented in the PR description, not defects):
Overall correctness verdict: correctThe wire compatibility across 488 corpus models (byte-identical on 485; the 3 differing encodings are attributable to nondeterminism in MDL imports, issue 859), the older 🤖 Generated with Claude Code |
Why
The Rust side of
project_io.protonow uses buffa instead of prost. Nothing else in the workspace depended on prost, so this removes prost and prost-derive (and theiritertools 0.14) fromCargo.lockentirely. It addsbuffa,smoothutf8+simdutf8(buffa's UTF-8 validation), and second versions offoldhash(0.1) andhashbrown(0.15). The TypeScript side (google-protobuf,src/server/schemas) is untouched, and the proto schema changes only in a comment.What changed
Generator.
scripts/gen-rust-protobufs.shdrivesbuffa-build(still viaprotoc), pinned to exactly thebuffaversionCargo.lockresolves. Generated code calls runtime internals that semver does not cover, so generator and runtime must move together. The options keep the generated types close to prost's:project_io.gen.rskeeps its hash header and freshness test.with_*setters.viewBox->view_box.Types. Open enum fields are
EnumValue<E>(werei32), and singular message fields areMessageField<T>(wereOption<T>). Oneofs, strings, repeated fields and proto3optionalscalars are unchanged.serde.rsconverts withas_known().unwrap_or_default(), which keeps the old fallback of unknown enum values to the default variant. Enum variants keep their prost idiomatic spellings (SimMethod::Euler) through buffa's const aliases, so the conversion matches still read the same.Downstream crates. libsimlin and simlin-cli use the engine's
pub use buffare-export instead of declaring their own protobuf dependency, so the engine alone owns the runtime version.Encode.
simlin_project_serialize_protobufusestry_encode_to_vec: buffa's panicking encode would abort the process (panic = abort) on a project past the 2 GiB limit rather than return an error.Decode limits. Decoding keeps buffa's defaults, including a 32 MiB budget on the memory repeated fields may materialize. prost had no such bound. Measured with a throwaway harness that binary-searched the smallest budget each model decodes under:
C-LEARN, the largest model in the corpus, uses under a twentieth of the budget, and the budget bounds what a crafted payload can make a decode allocate. A project roughly 20x C-LEARN's size would be refused with a decode error, which prost did not do.
CLI fix (separate commit).
cli: read protobuf input to the end, found while building the wire check below.open_binaryread withread_until(0), which stops at the first zero byte, so the CLI could not read any real.pbfile, onmainas well. It had no test; it now has two.Evidence
simlinatmain(prost) and on this branch (buffa), both with theopen_binaryfix applied, and ran every.xmile/.stmx/.itmx/.mdl/.protobinundertest/throughconvert --to protobuf. That is 511 files; the 23 the prost CLI cannot convert at all are excluded, leaving 488.test/fishbanks.protobinandtest/logistic-growth.protobindecode through the LTM tests and the json_proptest protobin round trip.serde.rsunit tests, including the open-enum fallback rows and the legacy-field rows.json_proptest/json_sdai_proptest, which assert stable bytes after the first round trip.assert_eq!on every corpus model.open_binary_tests.wasm-release,--no-default-features, thenbuild.sh'swasm-opt -O3flags): 9,987,598 -> 10,017,186 bytes (+0.30%). Gzip -9: 3,181,543 -> 3,193,811 bytes (+12 KB).What this does not establish
.protobinfiles, and both implementations being conformant to the same wire format.🤖 Generated with Claude Code
https://claude.ai/code/session_01RqCF6kitvGj4xBVs5wZYw5