Skip to content

codegen: namespace ancillary types under buffa_:: sentinel + per-package stitcher#62

Merged
iainmcgin merged 6 commits intomainfrom
prototype/sentinel-per-package
Apr 23, 2026
Merged

codegen: namespace ancillary types under buffa_:: sentinel + per-package stitcher#62
iainmcgin merged 6 commits intomainfrom
prototype/sentinel-per-package

Conversation

@rpb-ant
Copy link
Copy Markdown
Contributor

@rpb-ant rpb-ant commented Apr 23, 2026

Summary

Ancillary types emitted by buffa-codegen — view structs, oneof enums, oneof view enums, extension consts, and register_types — currently share the package-level Rust namespace with user proto types. Suffix-based disambiguation (FooView, KindOneof) handles most cases but still rejects pathological inputs (message FooView next to message Foo; nested type literally named {X}Oneof next to oneof {x}) with a codegen error.

This PR moves all ancillary kinds under a single reserved sentinel module __buffa:: per package, making collisions structurally impossible without name mangling, and has codegen emit a per-package <pkg>.mod.rs stitcher so consumers integrate via one buffa::include_proto!("pkg") call instead of hand-authoring the module wrapper.

Supersedes #54 and #37. Closes #32.

Layout

Item Before After
Owned message pkg::Foo pkg::Foo (unchanged)
View struct pkg::FooView pkg::__buffa::view::FooView
Nested view pkg::foo::BarView pkg::__buffa::view::foo::BarView
Oneof enum pkg::foo::KindOneof pkg::__buffa::oneof::foo::Kind
View-of-oneof pkg::foo::KindOneofView pkg::__buffa::view::oneof::foo::Kind
Extension const pkg::FOO pkg::__buffa::ext::FOO
Registration fn pkg::register_types (per file) pkg::__buffa::register_types (per package)

The sentinel __buffa is the only name codegen reserves in user namespace. It aligns with the existing __buffa_ reserved-field-name prefix (__buffa_cached_size, __buffa_unknown_fields), so the rule is uniformly "anything starting __buffa is reserved." validate_file errors with CodeGenError::ReservedModuleName if any proto package segment, message, or file-level enum would produce a __buffa-named module/type. Adding future ancillary kinds nests them under the existing sentinel; no new reserved words.

No convenience re-exports. Short-path aliases (pub use __buffa::view::*) are intentionally not emitted — they would work in the common case but silently change meaning when a user proto shadows them. The canonical __buffa:: path is unconditional.

Per-package .mod.rs stitcher

Codegen now emits, in addition to the five per-proto content files (<stem>.rs, <stem>.__view.rs, <stem>.__oneof.rs, <stem>.__view_oneof.rs, <stem>.__ext.rs), one <dotted.pkg>.mod.rs per package containing the pub mod __buffa { … } wrapper and a single merged register_types body. Consumers reference only the stitcher:

pub mod my_pkg {
    buffa::include_proto!("my.pkg");  // → include!(OUT_DIR/my.pkg.mod.rs)
}

Per-proto content files remain 1:1 with input protos (split-invocation safe for build systems that compile package subsets independently). The stitcher is the only per-package artifact and has the same caveat as any generated include file.

This collapses the wrapper-authoring previously duplicated across buffa-build, buffa-types, buffa-test, conformance, examples, benchmarks, gen_lib_rs.py, and protoc-gen-buffa-packaging to one place in codegen (the canonical ALLOW_LINTS list lives there too). buffa-build::generate_include_file now delegates to buffa_codegen::generate_module_tree.

register_types per-package

Because the stitcher is per-package, register_types is naturally one fn per package. The emit_register_fn=false workaround in gen_wkt_types.rs (seven WKT files × one fn each colliding in google.protobuf) is no longer needed.

Deleted

  • CodeGenError::OneofNameConflict and ::ViewNameConflict — collisions are structurally impossible.
  • proto_path_to_rust_module is #[deprecated] (consumers should use the stitcher path); follow-up will remove it.

Test plan

  • task lint — fmt + clippy clean (verified against rustc 1.95)
  • task lint-md — markdownlint clean
  • task test — 1462 tests pass
  • task verify — 6× CONFORMANCE SUITE PASSED, baseline numbers exact
  • task stress-googleapis — 47 protos compile clean
  • examples (addressbook/envelope/logging), benchmarks — compile clean
  • regression tests for message View { message Inner {} } (codegen unit + prelude_shadow.proto end-to-end) and package foo.view;
  • reservation tests for package foo.__buffa;, message __Buffa {}, and enum __buffa {}

Breaking changes

API-breaking for downstream consumers of generated code (import paths for views, oneofs, extensions, register_types). Migration is mechanical per the layout table above. Pre-1.0.

rpb-ant added 5 commits April 23, 2026 19:33
…age .mod.rs stitcher

Views, oneof enums, file-level extensions, and register_types() now live
under a single reserved `buffa_::` module per package instead of being
interleaved with owned types. Oneof enums drop the Oneof/View suffix
(tree position disambiguates); ViewNameConflict and OneofNameConflict
are gone — collisions with proto names are structurally impossible. The
sentinel is the only name reserved in user namespace and is checked.

Each .proto emits five content files; each package emits one
`<pkg>.mod.rs` stitcher that authors `pub mod buffa_ { ... }` and
include!s the content files. Consumers wire up only the stitcher via
`buffa::include_proto!("pkg")`. register_types is now naturally one fn
per package, fixing the multi-file-same-package collision (e.g. seven
WKT files in google.protobuf).

Regenerated WKT/bootstrap/logging-example output; migrated all
in-repo consumers (buffa-test, conformance, examples, benchmarks,
packaging plugin, googleapis stress).
- generate_package_mod: rebuild via quote! + prettyplease (was string
  concat); reuse format_tokens
- @generated header: protoc-gen-buffa → buffa-codegen (runs under
  buffa-build too)
- ALLOW_LINTS: single pub const at crate root; consumed by
  generate_module_tree, the .mod.rs stitcher, and buffa-build
- generate_module_tree: take IncludeMode {Relative, OutDir};
  buffa-build's generate_include_file now delegates to it instead of
  duplicating tree-building
- validate_file: merge the three reserved-name/module-conflict checks
  into one walk
- view.rs owned-path lookup: ? with CodeGenError::Other instead of a
  silent fallback
- rust_type_relative_split extern branch: debug_assert on the
  segment-count invariant + two unit tests
- ancillary_prefix: build super:: chain via quote! loop, not
  syn::parse_str
- empty-package stitcher filename: use the reserved sentinel
  (buffa_.mod.rs) so it provably can't collide with a real package

Regenerated WKT/bootstrap/logging .mod.rs files (prettyplease formatting
change only).
The buffa_:: canonical path is intentionally the only path. Convenience
re-exports would make short paths work in the common case but silently
change meaning when a user proto shadows them — predictability over
brevity.
- generate_module_tree: drop .unwrap() on infallible String fmt::Write;
  accept &[(impl AsRef<str>, impl AsRef<str>)] so callers pass owned
  vecs directly (drops the re-borrow shim in buffa-build and packaging)
- GeneratedFile / GeneratedFileKind / include_proto! docs: clarify that
  consumers wire up only PackageMod entries and that $pkg is the dotted
  proto package literal (empty package → "buffa_")
- emit_register_fn doc: reflect per-package stitcher emission (the old
  multi-file collision rationale is obsolete)
- ancillary_prefix: debug_assert dotless-FQN convention + doc note
- RegistryPaths::is_empty: one-line doc
validate_file checked package segments and message names against the
reserved sentinel but not file-level enum names. Enum type names are
emitted verbatim at package root, so `enum buffa_ { ... }` would land
beside `pub mod buffa_` (E0428). Nested enums live inside their owner's
module and cannot collide, so only file-level is checked. Adds two
naming.rs tests (rejected file-level / allowed nested).

Also: protoc-gen-buffa-packaging module doc still described per-file
output and referenced the removed proto_path_to_rust_module; rewrite
for the per-package .mod.rs stitcher model.
@iainmcgin
Copy link
Copy Markdown
Collaborator

[claude code] Renamed the sentinel from buffa_ to __buffa per DM agreement — aligns with the existing __buffa_ reserved-field-prefix convention so the rule stays "anything starting __buffa is ours." The rename was a one-line constant change (SENTINEL_MOD in context.rs); the rest is consumer paths and regen.

Also added two regression tests for the collision cases this PR fixes: generation.rs::test_child_package_named_view_no_collision (package foo + package foo.view) and message View { message Inner } in prelude_shadow.proto for end-to-end compilation. The four sentinel-reservation tests in naming.rs were already here — adapted in place.

I'll close #54 once this lands.

Aligns the sentinel with the existing reserved field-name prefix
(__buffa_cached_size, __buffa_unknown_fields) so the rule is uniformly
"anything starting __buffa is reserved by codegen". The trailing-
underscore form was a distinct second pattern.

Mechanical rename via SENTINEL_MOD constant; consumer paths and doc
strings updated. Validator inputs adjusted (`__Buffa` snake_cases to
`__buffa`; `Buffa_` no longer does). Adds two regression tests:
- generation.rs: `package foo` + `package foo.view` produces two
  stitchers with the kind tree under __buffa, not as a top-level
  `pub mod view` sibling.
- prelude_shadow.proto: `message View { Inner }` compiles end-to-end.

Regenerated WKT, bootstrap descriptor, and logging-example checked-in
code.
@iainmcgin iainmcgin force-pushed the prototype/sentinel-per-package branch from 893f7bd to 4bd7a6a Compare April 23, 2026 22:29
@iainmcgin iainmcgin marked this pull request as ready for review April 23, 2026 22:39
Copy link
Copy Markdown
Collaborator

@iainmcgin iainmcgin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[claude code] Reviewed: sentinel rename clean (single SENTINEL_MOD constant), validator covers package segments / messages / file-level enums with the right snake-case vs verbatim split, regression tests cover both collision repros end-to-end. Per-package .mod.rs stitcher + include_proto! macro + canonical ALLOW_LINTS eliminates the wrapper duplication. CI green at 4bd7a6a.

@iainmcgin iainmcgin merged commit 23a5a16 into main Apr 23, 2026
7 checks passed
@iainmcgin iainmcgin deleted the prototype/sentinel-per-package branch April 23, 2026 22:39
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 23, 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.

ViewNameConflict: codegen fails when message FooView exists alongside message Foo

2 participants