Skip to main content

ridl_backend_rust/
lib.rs

1//! IR v2 package to Rust source plus an extern-C header (ADR-0004 section 7,
2//! ADR-0007 decision 13).
3//!
4//! The full E1.12 backend over the typl surface. Each declaration in a
5//! [`v2::Package`] is realized twice: once as idiomatic Rust (the language
6//! layer of typl reference Appendix D — every integer is `i64`, every float is
7//! `f64`) and once, where the C ABI admits it, as an entry in a companion C
8//! header. Named scalar types become `#[repr(transparent)]` newtypes so unit
9//! safety survives into generated code (typl reference §5.7); composites map to
10//! structs, enums, and Rust `enum` unions.
11//!
12//! Rust source is built as a [`proc_macro2::TokenStream`] with `quote` and
13//! formatted with `prettyplease`, never by shelling out to `rustfmt`.
14//!
15//! Default derivation follows the leaf-recursion rule: an `impl Default` is
16//! emitted for a type only when every field it transitively contains is
17//! derivable. The IR `InitValue.derivable` flag on a composite-typed field is a
18//! one-level flag, so same-package composite references are re-checked by
19//! recursion rather than trusted (see the `defaults` module).
20//!
21//! Derive eligibility uses the same recursion over the transitive closure, in
22//! the `derives` module: `Debug`, `Clone` and `PartialEq` on every generated
23//! type, `Copy`, `Eq`, `Hash` and the ordering pair where the closure permits,
24//! and `Default` never, because it comes from the typl init value instead.
25
26use proc_macro2::{Ident, Span, TokenStream};
27use quote::{format_ident, quote};
28use ridl_ir::name::{camel_case, snake_case};
29use ridl_ir::projection::flatbuffers as fb_projection;
30use ridl_ir::v2;
31use std::cell::RefCell;
32use std::collections::{HashMap, HashSet};
33
34mod clauses;
35mod codec;
36mod defaults;
37mod derives;
38mod descriptors;
39mod face;
40
41/// The generated artifact for one package: Rust source.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct Generated {
44    pub rust_source: String,
45}
46
47/// A failure to generate code from a package.
48///
49/// Carried as a value so codegen stays total: no stage in the pipeline panics
50/// (ADR-0004 section 5). The `compile` driver folds `message` into its
51/// diagnostic list.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct GenerateError {
54    pub message: String,
55}
56
57/// Generates the Rust source for `package`: the domain types only. The only
58/// runtime paths in its output are the `::ridl_rt::payload::Violation` and
59/// `::ridl_rt::payload::Rule` that a named scalar's constructor (typl value
60/// objects, Task 3) and an enum's or enum set's `TryFrom<i64>` (Task 5) name;
61/// it emits no interaction face.
62///
63/// The compiler corpus runs this. The pipeline ran it too until E11.14, which
64/// gave the pipeline [`generate_pipeline`] — `ridl build --emit rust` calls
65/// that, and this output is a subset of it. The face is emitted by
66/// [`generate_face`] and [`generate_pipeline`], not from here, for the reason the Lane M plan records ("Where the face is
67/// emitted from"): the corpus interfaces carry contract clauses the M3 clause
68/// translator must refuse. The plan's second reason, that the corpus proofs
69/// passed no `--extern ridl_rt`, no longer holds: every compile proof links
70/// the runtime, because a generated named scalar, enum and enum set all name
71/// it. A package of pure `enum` and `enumset` declarations, with no named
72/// scalar at all, still depends on `ridl-rt`.
73///
74/// The call is total: it returns [`GenerateError`] rather than panicking. Every
75/// emitted identifier is produced through `ident`, which escapes Rust
76/// keywords as raw identifiers, so a typl name that happens to be a Rust
77/// keyword (for example a field named `override`) is emitted as `r#override`
78/// rather than panicking `format_ident!`. As a final guard the assembled token
79/// stream is parsed with `syn::parse2`; a parse failure (a codegen bug) surfaces
80/// as a `GenerateError` instead of unformatted output.
81pub fn generate(package: &v2::Package) -> Result<Generated, GenerateError> {
82    let ctx = Ctx::new(package);
83    render(package_items(&ctx, package)?)
84}
85
86/// Generates the Rust source for `package`: the domain types [`generate`]
87/// emits, plus the interaction-face descriptors over the `ridl-rt` runtime
88/// crate (Lane M stage M3).
89///
90/// This is the companion entry point of the M3 design's "companion entry
91/// point, not the pipeline" decision. The descriptors it appends name
92/// `::ridl_rt` and carry the translated `require`/`ensure` clause bodies, so it
93/// is the only caller of the clause translator, and the only entry point whose
94/// output names the runtime outside the domain types' own constructors and
95/// conversions. The domain types come from the same call because the
96/// checked-in fixture is brought in
97/// with a single `include!`: the face names those types, and the orphan rule
98/// needs them local to the test crate. The codec comes from the same call for
99/// the same reason: the face names `Payload<Wire>` for every payload type, and
100/// the implementations that satisfy it are the ones [`generate`] emits.
101///
102/// Total for the same reason [`generate`] is: it returns [`GenerateError`]
103/// rather than panicking, and additionally refuses a contract clause outside
104/// the accepted form and a call the M3 restriction cannot represent.
105pub fn generate_face(package: &v2::Package) -> Result<Generated, GenerateError> {
106    generate_face_with(package, WireEncoding::default())
107}
108
109/// [`generate_face`] with the package's wire encoding stated rather than
110/// defaulted (design note D-11 of
111/// `docs/archive/2026-09-20-flatbuffers-codec-design.md`).
112///
113/// `generate_face(package)` is
114/// `generate_face_with(package, WireEncoding::default())`, which is the
115/// relation `ridl-backend-proto` and `ridl-backend-flatbuffers` already give
116/// their own `generate_with`. The encoding reaches the output as one alias,
117/// `pub type Wire`, emitted once per package and named by every buffer the
118/// face sizes and every `Ref` it builds.
119///
120/// The alias is emitted here and not by [`generate`]: it exists so that the
121/// face names one thing rather than repeating an encoding at each of its
122/// sites, and a package generated with no face names it nowhere. What
123/// [`generate`] emits is unchanged by this entry point's existence.
124pub fn generate_face_with(
125    package: &v2::Package,
126    wire: WireEncoding,
127) -> Result<Generated, GenerateError> {
128    refuse_wire_collision(package)?;
129    let ctx = Ctx::new(package);
130    let mut items = vec![wire_alias(wire)];
131    items.extend(package_items(&ctx, package)?);
132    items.extend(descriptors::interface_items(&ctx, package)?);
133    items.extend(face::interface_items(package)?);
134    render(items)
135}
136
137/// [`generate`] with the other packages of the same build.
138///
139/// The relation is the one `ridl-backend-proto` and `ridl-backend-flatbuffers`
140/// already give their own `generate_with` (ADR-0017 decision 1):
141/// `generate(package)` is `generate_with(package, &[])`.
142///
143/// `others` is what lets the codec size and encode a cross-package reference.
144/// Without it a type reaching another package carries no
145/// `Payload<FlatBuffers>` implementation and a note saying so
146/// (driftsys/ridl#467); with it, such a type is emitted like any other.
147pub fn generate_with(
148    package: &v2::Package,
149    others: &[&v2::Package],
150) -> Result<Generated, GenerateError> {
151    let ctx = Ctx::with_others(package, others);
152    render(package_items(&ctx, package)?)
153}
154
155/// The pipeline's entry point: everything [`generate_face_with`] emits, over
156/// the whole build, with an interface the face cannot carry skipped rather
157/// than refused (E11.14 decisions 1, 2 and 4).
158///
159/// **Why the pipeline calls this and not [`generate`]** (decision 1): a
160/// consumer of `ridl build --emit rust` needs the face as much as the domain
161/// types, and the face compiles over the codec in the same unit, so the
162/// emitted unit is the superset rather than two calls.
163/// [`generate`] stays the entry point for every other caller, and it still
164/// emits no face and no descriptors. Its output is not byte-for-byte what it
165/// was — this story widened the visibility of `check` and the `__ridl_fb_*`
166/// functions, which moved every corpus snapshot — but what it emits is
167/// unchanged in kind. ADR-0023 decision 2 carries a dated consequence note
168/// (2026-09-21) saying the CLI calls this entry point rather than
169/// [`generate`]; the decision itself is not amended.
170///
171/// **Why an interface is skipped and not refused** (decision 2): the clause
172/// translator accepts one narrow form, and a multi-parameter call and a stream
173/// have no face at all. Refusing would make `--emit rust` reject legal ridl
174/// over a gap two named stories own — E5.1 replaces the translator, and the
175/// multi-parameter argument struct is lane M's parked follow-up — and would
176/// put a codegen error where a source diagnostic belongs. So the package keeps
177/// its domain types and its codec, the interface loses its `Client`,
178/// `Publisher`, `Provider` and `dispatch`, and a note at that site names the
179/// interface, the reason and the story that removes it.
180/// [`generate_face_with`] itself still refuses, for a direct caller.
181///
182/// **What a skipped interface also loses.** Its descriptors. Every cause
183/// decision 2 names is detected in the descriptor emitter, not the face
184/// emitter — `single_param_type`, `query_reply_type` and the clause translator
185/// all live there and serve both — so an interface whose face cannot be built
186/// cannot have its descriptors built either. The rest of the package's
187/// descriptors are unaffected; only the skipped interface's are absent, and
188/// the note says so.
189pub fn generate_pipeline(
190    package: &v2::Package,
191    wire: WireEncoding,
192    others: &[&v2::Package],
193) -> Result<Generated, GenerateError> {
194    refuse_wire_collision(package)?;
195    let ctx = Ctx::with_others(package, others);
196    let mut items = vec![wire_alias(wire)];
197    items.extend(package_items(&ctx, package)?);
198    for shape in package.shapes() {
199        if shape.service.is_some() {
200            continue;
201        }
202        match faced_interface(&ctx, package, shape.name, shape.interface) {
203            Ok(produced) => items.extend(produced),
204            Err(err) => items.push(skipped_interface_note(shape.name, shape.interface, &err)),
205        }
206    }
207    render(items)
208}
209
210/// The descriptors and the face of one interface, which stand or fall
211/// together for the reason [`generate_pipeline`] records.
212fn faced_interface(
213    ctx: &Ctx,
214    package: &v2::Package,
215    iface_name: &str,
216    interface: &v2::Interface,
217) -> Result<Vec<TokenStream>, GenerateError> {
218    let mut items = descriptors::one_interface_items(ctx, &package.name, iface_name, interface)?;
219    if let Some(module) = face::one_interface(iface_name, interface)? {
220        items.push(module);
221    }
222    Ok(items)
223}
224
225/// The note left where an interface's face was skipped (decision 2).
226///
227/// It is a `const` carrying doc attributes rather than a bare comment, for the
228/// reason the codec's withheld note gives: `quote!` emits tokens, and a doc
229/// attribute is the only comment that survives `prettyplease`. The name cannot
230/// collide with a typl constant — typl §15.1 gives one a SCREAMING_SNAKE name,
231/// and no typl name begins with an underscore.
232fn skipped_interface_note(
233    iface_name: &str,
234    interface: &v2::Interface,
235    err: &GenerateError,
236) -> TokenStream {
237    let name = format_ident!("__RIDL_NO_FACE_{}", snake_case(iface_name).to_uppercase());
238    let headline = format!(" Interface `{iface_name}` carries no generated interaction face.");
239    let reason = format!(" The emitter refused it: {}", err.message);
240    let owner = match face_gap(interface, err) {
241        FaceGap::CallShape => {
242            " A call the face cannot carry — an interaction that does not declare \
243             exactly one named parameter, or a query whose reply is not a named \
244             type. The induced argument struct that removes the first is lane M's \
245             parked multi-parameter follow-up."
246        }
247        FaceGap::Clause => {
248            " A contract clause outside the form the narrow translator accepts — \
249             `<subject> <comparison> <numeric literal>`, conjoined with `&&`. \
250             Story E5.1 replaces the translator and removes this."
251        }
252        FaceGap::Other => " No story below owns this one: the reason above is the whole of it.",
253    };
254    quote! {
255        #[doc = #headline]
256        ///
257        #[doc = #reason]
258        ///
259        #[doc = #owner]
260        ///
261        /// Its descriptors are absent for the same reason: the refusal is
262        /// raised by the descriptor emitter, which the face is built on. The
263        /// rest of this package — its domain types, its codec, and every
264        /// other interface — is unaffected, which is why the build succeeded
265        /// (E11.14 decision 2).
266        #[allow(dead_code)]
267        const #name: () = ();
268    }
269}
270
271/// Which of decision 2's two owners a skipped interface belongs to.
272///
273/// Decided by the refusal that was actually raised, and only then by reading
274/// the interface. Reading the interface alone reports the wrong owner
275/// whenever an interface has both gaps: it returns `CallShape` for the first
276/// badly shaped call it finds, even when what stopped the build was a clause
277/// on another interaction. The corpus's own `veh.cluster.VehicleStatus` is
278/// that shape, and its note used to name a reason from the clause translator
279/// under an owner line about multi-parameter calls.
280///
281/// The refusal is matched on [`clauses::CLAUSE_REFUSAL`] rather than on a
282/// literal here, so a rewording of the message changes this match with it
283/// instead of silently reclassifying.
284enum FaceGap {
285    /// An interaction the face has no shape for at all.
286    CallShape,
287    /// What was refused is a clause.
288    Clause,
289    /// Neither: the refusal is one no owner below claims, so the note names
290    /// the reason and no story. A `fixed` whose payload is not a named type
291    /// is the case this exists for — naming either owner there would blame a
292    /// story that does not remove it.
293    Other,
294}
295
296fn face_gap(interface: &v2::Interface, err: &GenerateError) -> FaceGap {
297    if err.message.starts_with(clauses::CLAUSE_REFUSAL) {
298        return FaceGap::Clause;
299    }
300    for decl in &interface.interactions {
301        let params = match decl.kind.as_ref() {
302            Some(v2::decl::Kind::CommandDef(command)) => &command.params,
303            Some(v2::decl::Kind::QueryDef(query)) => {
304                if descriptors::query_reply_type(query, &decl.name).is_err() {
305                    return FaceGap::CallShape;
306                }
307                &query.params
308            }
309            _ => continue,
310        };
311        if descriptors::single_param_type(params, &decl.name).is_err() {
312            return FaceGap::CallShape;
313        }
314    }
315    // Not a clause by the message, and every call has a face shape: the
316    // refusal came from somewhere neither owner claims.
317    FaceGap::Other
318}
319
320/// The name [`wire_alias`] emits at package scope.
321const WIRE_ALIAS: &str = "Wire";
322
323/// Refuses a package that declares an item whose emitted name is the encoding
324/// alias's (driftsys/ridl#476).
325///
326/// `generate_face_with` emits `pub type Wire` at package scope, and a typl
327/// declaration named `Wire` emits `pub struct Wire` at the same scope; rustc
328/// reports E0428 on the pair, in the consumer's build rather than here. This
329/// refuses it where the cause is, naming the declaration.
330///
331/// **Refusing rather than renaming** is E11.14 decision 5. The alias name is
332/// fixed by design note D-11 of the FlatBuffers codec and is named by every
333/// record and every consumer that follows it, so renaming it — or escaping the
334/// declaration — would move a name many documents state, to spare one package
335/// a name it is free to change. The rejected alternative is exactly that
336/// rename.
337///
338/// It is a **build error, not decision 2's per-interface skip**: the collision
339/// is a property of the package, not of one interface, so there is no interface
340/// to omit that would leave the rest of the package usable.
341///
342/// [`generate`] is unaffected — it emits no alias, so `Wire` is an ordinary
343/// declaration there, and a package built without a face keeps compiling.
344fn refuse_wire_collision(package: &v2::Package) -> Result<(), GenerateError> {
345    // Both namespaces, because both land at package scope: a declaration is
346    // emitted as its own item, and an interface is emitted as
347    // `pub struct <Interface>;` by the descriptor emitter. Scanning only the
348    // declarations let `interface Wire` through to a rustc E0428 in the
349    // emitted source, which is the failure this refusal exists to replace.
350    //
351    // The interface half walks `shapes()` rather than `interfaces`, which is
352    // the complete set of interface bodies (`xtask`'s `shape_walk` guard
353    // holds every reader to it), and then skips a service's inline shape for
354    // the same reason `descriptors::interface_items` does: no identity struct
355    // is emitted for one, so it collides with nothing.
356    //
357    // No case reaches that skip today — an inline shape's name is the
358    // service's own, which rsdl requires to be dotted and lowercase, so it
359    // can never be `Wire`. It is here so this walk and the emitter's stay the
360    // same shape, not because it changes an outcome.
361    let declared = package
362        .decls
363        .iter()
364        .map(|decl| (decl.name.as_str(), "declaration"));
365    let shapes = package
366        .shapes()
367        .filter(|shape| shape.service.is_none())
368        .map(|shape| (shape.name, "interface"));
369    for (name, kind) in declared.chain(shapes) {
370        if name == WIRE_ALIAS {
371            return Err(GenerateError {
372                message: format!(
373                    "`{}.{}` collides with the `{}` encoding alias the interaction \
374                     face emits at package scope; rename the {kind}",
375                    package.name, name, WIRE_ALIAS
376                ),
377            });
378        }
379    }
380    Ok(())
381}
382
383/// The domain types and the codec over them — what [`generate`] emits, and
384/// what a face is appended to.
385///
386/// There is one codec emitter and one call to it, which is why the face
387/// compiles over the codec `generate` emits rather than over one written for
388/// it (design note D-11, stage K9b).
389fn package_items(ctx: &Ctx, package: &v2::Package) -> Result<Vec<TokenStream>, GenerateError> {
390    let (mut items, tuples) = domain_items(ctx, package)?;
391    items.extend(codec::package_items(ctx, package, &tuples)?);
392    Ok(items)
393}
394
395/// The payload encoding a generated package's face encodes and verifies over
396/// (design note D-11; ADR-0020 decision 1 names the three encodings).
397///
398/// It is a per-package build-time choice rather than a type parameter on the
399/// face: the alternative would put an `E: Encoding` on every descriptor, every
400/// provider trait and every caller, and a `where` bound per payload type on
401/// every impl, for a choice made once per generated package.
402///
403/// One variant today. `ridl-backend-rust` emits a `Payload` implementation for
404/// one encoding — the FlatBuffers codec of story E11.7 — so naming another
405/// here would emit a face over implementations that do not exist. `repr(C)`
406/// and proto3 join when E11.12 and E11.8 emit their codecs, which is what
407/// `#[non_exhaustive]` says to a caller that matches on this.
408#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
409#[non_exhaustive]
410pub enum WireEncoding {
411    /// The FlatBuffers codec of story E11.7, which [`generate`] emits into
412    /// every package's own output.
413    #[default]
414    FlatBuffers,
415}
416
417/// The one `pub type Wire` alias a generated package carries.
418fn wire_alias(wire: WireEncoding) -> TokenStream {
419    let (path, doc) = match wire {
420        WireEncoding::FlatBuffers => (
421            quote! { ::ridl_rt::encoding::FlatBuffers },
422            "The payload encoding this package's generated interaction face \
423             encodes and verifies over, and the one the `Payload` \
424             implementations below implement: FlatBuffers (ADR-0019, ADR-0020 \
425             decision 2). It is named once here rather than repeated at every \
426             buffer and every `Ref` the face builds, so the package's \
427             encoding is one line to read and one line to change.",
428        ),
429    };
430    quote! {
431        #[doc = #doc]
432        pub type Wire = #path;
433    }
434}
435
436/// The domain-type items of `package` — the shared work of both entry points
437/// — and the induced tuple structs the walk discovered.
438///
439/// The tuples travel back out because the FlatBuffers codec needs them:
440/// a tuple's generated struct is a FlatBuffers table like any other
441/// (ADR-0019 decision 3), and only this walk knows which tuples exist and
442/// what each one is named.
443///
444/// This does not emit the codec. [`package_items`] appends it, for both entry
445/// points: the codec is `generate`'s output (design note D-1 as amended), and
446/// the face compiles over that same output (D-11, stage K9b).
447#[allow(clippy::type_complexity)]
448fn domain_items(
449    ctx: &Ctx,
450    package: &v2::Package,
451) -> Result<(Vec<TokenStream>, Vec<InducedTuple>), GenerateError> {
452    let mut items: Vec<TokenStream> = Vec::new();
453    let mut tuples: Vec<InducedTuple> = Vec::new();
454    let mut discovered: Vec<InducedTuple> = Vec::new();
455
456    for decl in &package.decls {
457        items.push(emit_decl(ctx, decl, &mut tuples));
458    }
459
460    // Tuple types generate a named nested struct each (typl §11). Process the
461    // worklist: emitting a tuple struct's fields can discover further nested
462    // tuples, which are appended and drained here.
463    //
464    // A name is emitted once. The same tuple genuinely arrives twice — the
465    // interaction module pre-discovers nested tuples to learn their names, and
466    // emitting the outer tuple's fields finds them again — so a repeat of an
467    // *identical* discovery is expected and skipped. A repeat under one name
468    // with a different shape is a collision, and it is refused rather than
469    // deduplicated; see [`tuple_collision`].
470    let mut seen: HashMap<String, InducedTuple> = HashMap::new();
471    let mut index = 0;
472    while index < tuples.len() {
473        let induced = tuples[index].clone();
474        index += 1;
475        if let Some(previous) = seen.get(&induced.name) {
476            if previous.tuple != induced.tuple || previous.visibility != induced.visibility {
477                return Err(tuple_collision(previous, &induced));
478            }
479            continue;
480        }
481        seen.insert(induced.name.clone(), induced.clone());
482        items.push(emit_tuple_struct(ctx, &induced, &mut tuples));
483        discovered.push(induced);
484    }
485
486    Ok((items, discovered))
487}
488
489// ---------------------------------------------------------------------------
490// The FlatBuffers size bound's refusal (design note D-7, stages K4 and K5).
491// ---------------------------------------------------------------------------
492
493/// Refuses one declaration with no finite FlatBuffers bound (design note
494/// D-7 of `docs/archive/2026-09-20-flatbuffers-codec-design.md`; §4a of that note
495/// records what stage K4 built and what stage K5 closed).
496///
497/// **Called per type, by the codec emitter**, at the point where it is about
498/// to emit that type's `Payload<FlatBuffers>` implementation — which is what
499/// D-7's own wording asks for: the emitter writes no implementation *for that
500/// type*. It is never a package-wide gate; a package-wide refusal would
501/// withhold every type's domain code over one type's unbounded codec, which
502/// D-7 does not authorise.
503///
504/// `Ok(())` therefore means one of two different things, and the caller
505/// already knows which: the type has a bound and its codec is emitted, or the
506/// cause of its missing bound is one this backend cannot judge — a
507/// cross-package reference it does not resolve, or a same-package cycle — and
508/// that one type simply carries no codec.
509///
510/// The refusal names the member wherever [`unbounded_member`] can name one,
511/// and says which of the other three causes it found otherwise.
512pub(crate) fn check_flatbuffers_bound(
513    ctx: &Ctx,
514    package: &v2::Package,
515    decl: &v2::Decl,
516) -> Result<(), GenerateError> {
517    if fb_projection::root_table(decl).is_none() {
518        return Ok(());
519    }
520    let packages = fb_projection::Packages {
521        package,
522        others: &[],
523    };
524    if fb_projection::max_size(packages, decl).is_some() {
525        return Ok(());
526    }
527    let pkg = &package.name;
528    let name = &decl.name;
529    match unbounded_member(ctx, package, decl) {
530        Attribution::Member(member) => Err(GenerateError {
531            message: format!("`{pkg}.{name}.{member}` has no finite FlatBuffers bound"),
532        }),
533        Attribution::Untyped(member) => Err(GenerateError {
534            message: format!(
535                "`{pkg}.{name}.{member}` carries no type, so `{pkg}.{name}` has no FlatBuffers \
536                 bound"
537            ),
538        }),
539        Attribution::Layout(message) => Err(GenerateError {
540            message: format!("`{pkg}.{name}` has no FlatBuffers table layout: {message}"),
541        }),
542        Attribution::Aggregate => Err(GenerateError {
543            message: format!(
544                "`{pkg}.{name}` has no finite FlatBuffers bound: every member is bounded on its \
545                 own and the total is not"
546            ),
547        }),
548        Attribution::Exempt => Ok(()),
549    }
550}
551
552/// What [`check_flatbuffers_bound`] found when `decl`'s own
553/// [`fb_projection::max_size`] answered `None`.
554///
555/// Stage K5 split what stage K4 called `Declaration` into three, closing the
556/// second gap §4a of the design note carried forward: the three causes that
557/// one variant covered are now told apart, and each writes its own message.
558enum Attribution {
559    /// One member — a struct field's name, a union arm's name, or the
560    /// `value` field of a box root (ADR-0019 decision 8) — is individually
561    /// unbounded.
562    Member(String),
563    /// One member carries no type at all — a struct field with no type, or
564    /// the `value` of a box root whose declaration names no backing — which
565    /// is malformed IR rather than an unbounded shape, and which no probe can
566    /// charge.
567    Untyped(String),
568    /// `fb_projection::struct_table` refused the declaration's layout — two
569    /// members sharing one ordinal, or an ordinal of 0. It is a property of
570    /// the whole declaration and no single-member probe reproduces it, so the
571    /// projection's own message is carried through.
572    Layout(String),
573    /// Every member is bounded on its own and the total is not: the summed
574    /// size overflows `u64`, or it exceeds
575    /// [`fb_projection::MAX_ENCODABLE`]. A member this backend cannot judge
576    /// does not shield the sum: it is charged as a `boolean` and the total
577    /// is over the ceiling even so.
578    Aggregate,
579    /// Every member that could be judged is individually bounded, at least
580    /// one could not be judged — a cross-package reference this backend does
581    /// not resolve, or a same-package cycle — and the struct as a whole still
582    /// fits with each unjudged leaf charged as a `boolean`. `decl`'s own
583    /// `None` is explained by that member, not by an unbounded shape.
584    Exempt,
585}
586
587/// What one type position contributes to the attribution.
588#[derive(Clone, Copy, PartialEq, Eq)]
589enum Verdict {
590    /// It has a finite bound of its own.
591    Bounded,
592    /// It has none, and this backend can say so.
593    Unbounded,
594    /// This backend cannot say: the position reaches a cross-package
595    /// reference or a cycle.
596    Unjudgeable,
597}
598
599/// Attributes `decl`'s unbounded [`fb_projection::max_size`] — see
600/// [`Attribution`].
601///
602/// Each member is judged on its own by [`judge`], never by trusting one
603/// member's unboundedness to explain another's. A struct with both a bare
604/// unbounded `string` map key and an unrelated cross-package field is refused
605/// over the first and exempted from nothing on account of the second.
606fn unbounded_member(ctx: &Ctx, package: &v2::Package, decl: &v2::Decl) -> Attribution {
607    let mut any_exempt = false;
608    match &decl.kind {
609        Some(v2::decl::Kind::StructDef(def)) => {
610            // The layout is refused for the declaration as a whole, and it is
611            // checked first: with two members on one ordinal, every member
612            // probes as bounded and only the aggregate answers `None`, which
613            // is exactly the confusion §4a asked K5 to end.
614            if let Err(err) = fb_projection::struct_table(&decl.name, def) {
615                return Attribution::Layout(err.message);
616            }
617            for member in &def.members {
618                let Some(v2::struct_member::Member::Field(field)) = &member.member else {
619                    // A reserved tombstone emits no field and charges one
620                    // slack byte only (typl §7.4); it is never the cause.
621                    continue;
622                };
623                let Some(ty) = field.r#type.as_ref() else {
624                    return Attribution::Untyped(field.name.clone());
625                };
626                match judge(ctx, package, ty) {
627                    Verdict::Unbounded => return Attribution::Member(field.name.clone()),
628                    Verdict::Unjudgeable => any_exempt = true,
629                    Verdict::Bounded => {}
630                }
631            }
632        }
633        Some(v2::decl::Kind::UnionDef(def)) => {
634            for arm in &def.arms {
635                let ty = v2::FieldType {
636                    optional: false,
637                    kind: Some(v2::field_type::Kind::Named(arm.type_ref.clone())),
638                };
639                match judge(ctx, package, &ty) {
640                    Verdict::Unbounded => return Attribution::Member(arm.name.clone()),
641                    Verdict::Unjudgeable => any_exempt = true,
642                    Verdict::Bounded => {}
643                }
644            }
645        }
646        // A declaration rooted in a box (ADR-0019 decision 8) has one member:
647        // the box's `value` field, holding the declaration itself. It is
648        // resolved in the package that declares it, so it is never
649        // unjudgeable — only bounded, or unbounded because the IR carries no
650        // width or no length bound for it.
651        Some(
652            v2::decl::Kind::TypeDef(_) | v2::decl::Kind::EnumDef(_) | v2::decl::Kind::EnumSetDef(_),
653        ) => {
654            // A named scalar with no backing at all is malformed IR rather
655            // than an unbounded shape, which is what `Untyped` is for. The
656            // front end leaves one behind after a parse error such as
657            // `type X:`, so this is the shape such a declaration reaches the
658            // backend in — not one a length or a width would fix.
659            if let Some(v2::decl::Kind::TypeDef(td)) = &decl.kind
660                && td.backing.is_none()
661            {
662                return Attribution::Untyped("value".to_string());
663            }
664            let ty = v2::FieldType {
665                optional: false,
666                kind: Some(v2::field_type::Kind::Named(decl.name.clone())),
667            };
668            match judge(ctx, package, &ty) {
669                Verdict::Unbounded => return Attribution::Member("value".to_string()),
670                Verdict::Unjudgeable => any_exempt = true,
671                Verdict::Bounded => {}
672            }
673        }
674        _ => {}
675    }
676    if !any_exempt {
677        return Attribution::Aggregate;
678    }
679    // Every judged member is bounded and at least one member could not be
680    // judged, so the aggregate is still open: two members each under the
681    // ceiling can sum over it, and a member this backend cannot judge must
682    // not shield that sum. The whole struct is charged once more over
683    // `lower_bound_stand_in`'s copy of each member, which charges the
684    // unjudged leaves no more than the real ones would, so `None` here is
685    // an aggregate cause whatever those leaves turn out to be. A union is
686    // its largest arm rather than a sum, so it has no aggregate to charge.
687    if let Some(v2::decl::Kind::StructDef(def)) = &decl.kind {
688        let stand_in = v2::Decl {
689            kind: Some(v2::decl::Kind::StructDef(v2::StructDef {
690                members: def
691                    .members
692                    .iter()
693                    .map(|member| match &member.member {
694                        Some(v2::struct_member::Member::Field(field)) => v2::StructMember {
695                            member: Some(v2::struct_member::Member::Field(v2::Field {
696                                r#type: field
697                                    .r#type
698                                    .as_ref()
699                                    .map(|ty| lower_bound_stand_in(ctx, ty)),
700                                ..field.clone()
701                            })),
702                        },
703                        _ => member.clone(),
704                    })
705                    .collect(),
706                fixed_layout: def.fixed_layout,
707            })),
708            ..decl.clone()
709        };
710        let packages = fb_projection::Packages {
711            package,
712            others: &[],
713        };
714        if fb_projection::max_size(packages, &stand_in).is_none() {
715            return Attribution::Aggregate;
716        }
717    }
718    Attribution::Exempt
719}
720
721/// One type position's verdict.
722///
723/// A position this backend can resolve in full is probed as a whole, which is
724/// the cheapest and the most faithful answer: [`probe_field_type`] charges it
725/// exactly what [`fb_projection::max_size`] charges it as one member.
726///
727/// A position it cannot resolve in full is probed as a whole too, over
728/// [`lower_bound_stand_in`]'s copy of it: every leaf this backend cannot
729/// judge is replaced by a `boolean`, the smallest thing the projection
730/// charges anything for, and what is left is charged once by the same
731/// `max_size` the real position would be charged by. That charges every
732/// count, every product of nested counts, and every locally known leaf in
733/// the position together, which neither a leaf-by-leaf descent nor a
734/// level-by-level probe of each count does — `[[veh.other.Speed; 0..2^20];
735/// 0..2^20]` is unbounded by the product of its two counts and by nothing
736/// else, and `[(veh.other.Speed, [boolean; 0..2^31]); 0..4]` by an outer
737/// count over a local inner one. Because each stand-in is a lower bound on
738/// the leaf it replaces, a stand-in that is unbounded proves the real
739/// position is, and a stand-in that fits says nothing about the real leaves,
740/// which is what `Unjudgeable` means.
741fn judge(ctx: &Ctx, package: &v2::Package, ty: &v2::FieldType) -> Verdict {
742    if member_resolves_locally(ctx, ty) {
743        return if probe_field_type(package, ty).is_some() {
744            Verdict::Bounded
745        } else {
746            Verdict::Unbounded
747        };
748    }
749    if probe_field_type(package, &lower_bound_stand_in(ctx, ty)).is_some() {
750        Verdict::Unjudgeable
751    } else {
752        Verdict::Unbounded
753    }
754}
755
756/// `ty` with every leaf this backend cannot judge replaced by a `boolean`,
757/// so that [`fb_projection::max_size`] can charge the rest.
758///
759/// A `boolean` is one inline byte and nothing out of line, and the
760/// projection charges every other leaf at least that: a named scalar its
761/// declared width, an enum eight bytes, a struct, a union or a string an
762/// offset plus its own table or body. A vector's charge and a table's bound
763/// are both monotone in the charge of what they hold, so the stand-in is
764/// charged no more than the real position, and `None` over the stand-in is
765/// `None` over the real position whatever the unjudged leaves turn out to be.
766/// The replaced leaves are the ones [`member_resolves_locally`] answers
767/// `false` for: a named reference that does not resolve in this package or
768/// reaches a cycle, a stream, and an unspecified primitive. A `FieldType` with
769/// no `kind` is kept, so it still probes to `Unbounded` (design note §4b).
770fn lower_bound_stand_in(ctx: &Ctx, ty: &v2::FieldType) -> v2::FieldType {
771    fn boolean(optional: bool) -> v2::FieldType {
772        v2::FieldType {
773            optional,
774            kind: Some(v2::field_type::Kind::Primitive(
775                v2::PrimitiveType::Boolean as i32,
776            )),
777        }
778    }
779    let stand_in = |leaf: &v2::FieldType| Box::new(lower_bound_stand_in(ctx, leaf));
780    let kind = match ty.kind.as_ref() {
781        Some(v2::field_type::Kind::Array(array)) => {
782            v2::field_type::Kind::Array(Box::new(v2::ArrayType {
783                element: array.element.as_deref().map(stand_in),
784                min: array.min,
785                max: array.max,
786            }))
787        }
788        Some(v2::field_type::Kind::Map(map)) => v2::field_type::Kind::Map(Box::new(v2::MapType {
789            key: map.key.as_deref().map(stand_in),
790            value: map.value.as_deref().map(stand_in),
791            min: map.min,
792            max: map.max,
793        })),
794        Some(v2::field_type::Kind::Tuple(tuple)) => v2::field_type::Kind::Tuple(v2::TupleType {
795            fields: tuple
796                .fields
797                .iter()
798                .map(|field| v2::TupleField {
799                    r#type: field.r#type.as_ref().map(|leaf| *stand_in(leaf)),
800                    ..field.clone()
801                })
802                .collect(),
803        }),
804        _ if member_resolves_locally(ctx, ty) => return ty.clone(),
805        // A named reference this backend does not resolve, a cycle, a
806        // stream, or an unspecified primitive: the leaf itself is what
807        // cannot be judged, and it is replaced.
808        _ => return boolean(ty.optional),
809    };
810    v2::FieldType {
811        optional: ty.optional,
812        kind: Some(kind),
813    }
814}
815
816/// The members of `decl` this backend cannot judge — a cross-package
817/// reference it does not resolve, a same-package cycle, a stream, or an
818/// unspecified primitive.
819///
820/// [`check_flatbuffers_bound`] answers `Ok(())` for a declaration whose only
821/// obstacle is one of these, and the codec emitter then withholds that type's
822/// implementation. This is what lets the emitted source say which member is
823/// the reason, rather than leaving a consumer with a missing trait
824/// implementation and nothing to read.
825pub(crate) fn unjudgeable_members(ctx: &Ctx, decl: &v2::Decl) -> Vec<String> {
826    let mut names = Vec::new();
827    match &decl.kind {
828        Some(v2::decl::Kind::StructDef(def)) => {
829            for member in &def.members {
830                if let Some(v2::struct_member::Member::Field(field)) = &member.member
831                    && let Some(ty) = field.r#type.as_ref()
832                    && !member_resolves_locally(ctx, ty)
833                {
834                    names.push(field.name.clone());
835                }
836            }
837        }
838        Some(v2::decl::Kind::UnionDef(def)) => {
839            for arm in &def.arms {
840                if !decl_resolves_locally(ctx, &arm.type_ref, &mut HashSet::new()) {
841                    names.push(arm.name.clone());
842                }
843            }
844        }
845        _ => {}
846    }
847    names
848}
849
850/// The bound of one type position alone, charged the way
851/// [`fb_projection::max_size`] charges it as one member of a struct:
852/// `struct_table_bound` sums each member's own `field_charge` independently
853/// and propagates the first `None`, so a struct of exactly this one field
854/// reproduces the charge the position contributes, with nothing else able to
855/// answer `None` in its place.
856fn probe_field_type(package: &v2::Package, ty: &v2::FieldType) -> Option<u64> {
857    let probe = v2::Decl {
858        kind: Some(v2::decl::Kind::StructDef(v2::StructDef {
859            members: vec![v2::StructMember {
860                member: Some(v2::struct_member::Member::Field(v2::Field {
861                    // Ordinal 1 is the first FlatBuffers id. It is written
862                    // here rather than copied from the real member so that a
863                    // declaration whose ordinals are themselves malformed is
864                    // attributed by `Attribution::Layout` and not mistaken
865                    // for an unbounded member.
866                    ordinal: 1,
867                    r#type: Some(ty.clone()),
868                    ..Default::default()
869                })),
870            }],
871            fixed_layout: false,
872        })),
873        ..Default::default()
874    };
875    let packages = fb_projection::Packages {
876        package,
877        others: &[],
878    };
879    fb_projection::max_size(packages, &probe)
880}
881
882/// Whether every named type reachable from `ty` resolves inside the package
883/// `ctx` indexes — the same shape [`fb_projection::max_size`] would have to
884/// resolve to size it.
885///
886/// Two things answer `false`, "this backend cannot judge this member,
887/// exempt it", rather than letting a probe answer for them — both scoped to
888/// the one member being checked, never to the whole declaration, so a
889/// sibling member with a genuine bound problem is still caught:
890///
891/// - **a cross-package (dotted) or unknown reference.** `ridl-backend-rust`
892///   generates one package at a time and resolves no cross-package reference
893///   itself — [`Ctx::lookup`] answers `None` for one by design, the same as
894///   every other same-package-only pass in this backend
895///   (`derives::type_ref_eligibility`, `defaults`). Handed `others: &[]`,
896///   `fb_projection::max_size` cannot tell "this reference does not resolve
897///   here" from "this member has no finite bound" — both answer `None` (its
898///   own doc, "a reference that does not resolve in `packages`"). This
899///   backend already generates a struct across such a reference — the
900///   corpus's `ClimateReport`
901///   (`crates/ridlc/tests/corpus/veh-cluster/cluster/services.ridl`,
902///   `cabin`/`setpoint` typed by the imported `veh.common.Temperature`) is
903///   one.
904/// - **a same-package composite that reaches itself.** typl rejects one
905///   (TYPL-206), so it is IR handed in directly the same as the case above.
906///   This backend already has a considered answer for a cyclic struct that
907///   is not refusal: `recursive_struct_default_terminates` and
908///   `a_cyclic_struct_takes_no_conditional_derives` both pin that a cyclic
909///   struct's *domain type* still generates, because Default derivation and
910///   the conditional-derive walk both guard the same cycle and degrade to
911///   the conservative answer rather than erroring.
912///
913/// A third case is exempted the same way: **a `Stream` field position.**
914/// ridl §12.3 keeps a stream at an interaction position; it never reaches a
915/// struct or tuple field in checked IR, `fb_projection::max_size` charges
916/// nothing for it, and `flatbuffers_bound_leaves_a_stream_field_alone` pins
917/// that this function exempts a member typed by one rather than treating the
918/// `None` `field_charge` gives it as a genuine bound failure.
919fn member_resolves_locally(ctx: &Ctx, ty: &v2::FieldType) -> bool {
920    field_type_resolves_locally(ctx, ty, &mut HashSet::new())
921}
922
923/// Whether `reference` and everything its own shape reaches resolves inside
924/// the package `ctx` indexes. See [`member_resolves_locally`] for the two
925/// cases this answers `false` for, and why each is scoped to one member.
926fn decl_resolves_locally(ctx: &Ctx, reference: &str, visiting: &mut HashSet<String>) -> bool {
927    let Some(decl) = ctx.lookup(reference) else {
928        return false;
929    };
930    if !visiting.insert(reference.to_string()) {
931        // On the path already being walked: a cycle. Left for
932        // `unbounded_member` to exempt this one member over (see the doc
933        // above).
934        return false;
935    }
936    let resolves = match &decl.kind {
937        Some(v2::decl::Kind::StructDef(def)) => {
938            def.members.iter().all(|member| match &member.member {
939                Some(v2::struct_member::Member::Field(field)) => field
940                    .r#type
941                    .as_ref()
942                    .is_some_and(|ty| field_type_resolves_locally(ctx, ty, visiting)),
943                _ => true,
944            })
945        }
946        Some(v2::decl::Kind::UnionDef(def)) => def
947            .arms
948            .iter()
949            .all(|arm| decl_resolves_locally(ctx, &arm.type_ref, visiting)),
950        _ => true,
951    };
952    visiting.remove(reference);
953    resolves
954}
955
956/// One type position's contribution to [`decl_resolves_locally`], over the
957/// same [`v2::FieldType`] shape `fb_projection::max_size` walks to size it.
958/// A `Stream` is exempted explicitly — see [`member_resolves_locally`]'s
959/// third bullet — rather than falling through to the `None` arm, which is
960/// reserved for a `FieldType` this walk genuinely does not recognize.
961fn field_type_resolves_locally(
962    ctx: &Ctx,
963    ty: &v2::FieldType,
964    visiting: &mut HashSet<String>,
965) -> bool {
966    match &ty.kind {
967        Some(v2::field_type::Kind::Named(reference)) => {
968            decl_resolves_locally(ctx, reference, visiting)
969        }
970        // An `Unspecified` field primitive emits `()` and is charged
971        // nothing, exactly as a `Stream` is, and `derives` lists the two side
972        // by side among its refusing positions. It is malformed IR rather
973        // than an unbounded shape, so it is exempted rather than refused.
974        Some(v2::field_type::Kind::Primitive(primitive)) => v2::PrimitiveType::try_from(*primitive)
975            .is_ok_and(|primitive| primitive != v2::PrimitiveType::Unspecified),
976        Some(v2::field_type::Kind::InlineScalar(_)) => true,
977        Some(v2::field_type::Kind::Tuple(tuple)) => tuple.fields.iter().all(|field| {
978            field
979                .r#type
980                .as_ref()
981                .is_some_and(|ty| field_type_resolves_locally(ctx, ty, visiting))
982        }),
983        Some(v2::field_type::Kind::Array(array)) => array
984            .element
985            .as_deref()
986            .is_some_and(|element| field_type_resolves_locally(ctx, element, visiting)),
987        Some(v2::field_type::Kind::Map(map)) => {
988            map.key
989                .as_deref()
990                .is_some_and(|key| field_type_resolves_locally(ctx, key, visiting))
991                && map
992                    .value
993                    .as_deref()
994                    .is_some_and(|value| field_type_resolves_locally(ctx, value, visiting))
995        }
996        Some(v2::field_type::Kind::Stream(_)) => false,
997        None => true,
998    }
999}
1000
1001/// Parses the assembled items as a bare `syn::File` (no inner attribute, so an
1002/// `include!` of the output stays legal) and formats them with prettyplease.
1003fn render(items: Vec<TokenStream>) -> Result<Generated, GenerateError> {
1004    let tokens = quote! { #(#items)* };
1005    let file: syn::File = syn::parse2(tokens).map_err(|err| GenerateError {
1006        message: format!("generated Rust does not parse: {err}"),
1007    })?;
1008
1009    Ok(Generated {
1010        rust_source: prettyplease::unparse(&file),
1011    })
1012}
1013
1014/// One tuple type reached from a declaration, with the visibility that
1015/// declaration was declared at (typl §11).
1016///
1017/// A tuple has no name in source; the struct it generates is named after the
1018/// path that reached it and is emitted at module scope beside the declaration
1019/// that induced it. The visibility travels with the discovery for the same
1020/// reason [`v2::InterfaceShape::visibility`] carries a service's: the value is
1021/// authoritative at the point of discovery and nowhere else. By the time
1022/// [`emit_tuple_struct`] runs, the tuple is one entry in a flat worklist and
1023/// the declaration it came from is out of reach — which is exactly how the
1024/// struct came to be emitted `pub` over an `internal` declaration's payload
1025/// (issue #167).
1026///
1027/// One name is one struct. The same discovery repeated — the interaction
1028/// module pre-discovers a nested tuple's name and the drain finds it again —
1029/// is skipped; a repeat under one name with a different shape or visibility is
1030/// a collision and is refused ([`tuple_collision`]), because carrying a
1031/// visibility onto a name two declarations share has no sound answer. See
1032/// [`generate`].
1033#[derive(Debug, Clone)]
1034pub(crate) struct InducedTuple {
1035    /// The generated struct name — the CamelCase of the path that reached the
1036    /// tuple.
1037    pub(crate) name: String,
1038    pub(crate) tuple: v2::TupleType,
1039    /// The visibility of the declaration this tuple was reached from: an
1040    /// `internal struct`'s field, or an `internal interface`'s query return.
1041    pub(crate) visibility: i32,
1042}
1043
1044/// Refuses a package in which two different tuples generate one struct name.
1045///
1046/// The name is the CamelCase of the path that reaches the tuple, and nothing
1047/// upstream keeps two paths from mangling to one string: `struct AB { c : … }`
1048/// and `struct A { bC : … }` both reach `ABC`, and neither draws a ridl
1049/// diagnostic. There is no sound way to pick between them, which is why this is
1050/// a refusal rather than a rule:
1051///
1052/// - **Keeping the first** — what the worklist did before — gives the second
1053///   declaration the *first one's shape*. `ridlc check` exits 0, the module
1054///   compiles, and the contract is silently wrong. It is also how carrying an
1055///   inducing declaration's visibility (issue #167) could narrow a struct a
1056///   public declaration uses, turning a silent wrong shape into a
1057///   `private_interfaces` build failure.
1058/// - **Keeping the widest visibility** would publish a package-private type's
1059///   shape to escape that build failure, which is the defect #167 fixed.
1060///
1061/// So neither dedup rule is sound and only rejection is. This is the same
1062/// answer `interact::check_name_collisions` gives every other generated-name
1063/// clash: codegen names the failure itself rather than handing rustc a module
1064/// whose meaning it cannot state.
1065///
1066/// The two shapes are named because the mangled name cannot distinguish them —
1067/// that is the whole defect — and the field lists are what a reader greps for.
1068fn tuple_collision(previous: &InducedTuple, current: &InducedTuple) -> GenerateError {
1069    fn shape(induced: &InducedTuple) -> String {
1070        let fields: Vec<String> = induced
1071            .tuple
1072            .fields
1073            .iter()
1074            .map(|field| field.name.clone())
1075            .collect();
1076        format!("({})", fields.join(", "))
1077    }
1078    GenerateError {
1079        message: format!(
1080            "the generated name {name} is claimed by two different tuple types, {a} and {b}; \
1081             a tuple generates a struct named for the path that reaches it, and these two paths \
1082             spell one name — rename a field or a declaration so they differ",
1083            name = current.name,
1084            a = shape(previous),
1085            b = shape(current),
1086        ),
1087    }
1088}
1089
1090// ---------------------------------------------------------------------------
1091// Package context — same-package name lookups for the leaf-recursion rules.
1092// ---------------------------------------------------------------------------
1093
1094/// Read-only view of a package indexed by declaration name, so the emitter and
1095/// the default-derivation pass can resolve a same-package reference to its
1096/// declaration (cross-package references stay unresolved by design — this
1097/// backend generates one package at a time).
1098pub(crate) struct Ctx<'a> {
1099    decls: HashMap<&'a str, &'a v2::Decl>,
1100    /// The other packages of the same build, in the shape
1101    /// `ridl-backend-flatbuffers::generate_with` already gives them
1102    /// (ADR-0017 decision 1). Empty for the single-package entry points.
1103    ///
1104    /// The codec reads them through the projection's `Packages` so a
1105    /// cross-package reference can be sized and encoded rather than withheld
1106    /// (driftsys/ridl#467).
1107    pub(crate) others: &'a [&'a v2::Package],
1108    /// The set of declaration names currently being expanded by the
1109    /// Default-derivation recursion. It guards against a cyclic IR: a
1110    /// same-package composite that reaches itself would otherwise recurse
1111    /// forever (C1b). The recursion inserts a name on entry and removes it on
1112    /// exit, so between top-level declarations the set is empty.
1113    visiting: RefCell<HashSet<String>>,
1114}
1115
1116impl<'a> Ctx<'a> {
1117    pub(crate) fn new(package: &'a v2::Package) -> Self {
1118        Ctx::with_others(package, &[])
1119    }
1120
1121    /// [`Ctx::new`] with the other packages of the same build, which the
1122    /// codec resolves a cross-package reference through (driftsys/ridl#467).
1123    pub(crate) fn with_others(package: &'a v2::Package, others: &'a [&'a v2::Package]) -> Self {
1124        let decls = package
1125            .decls
1126            .iter()
1127            .map(|decl| (decl.name.as_str(), decl))
1128            .collect();
1129        Ctx {
1130            decls,
1131            others,
1132            visiting: RefCell::new(HashSet::new()),
1133        }
1134    }
1135
1136    /// The declaration named `name` in this package, or `None` for a
1137    /// cross-package (dotted) or unknown reference.
1138    pub(crate) fn lookup(&self, name: &str) -> Option<&'a v2::Decl> {
1139        self.decls.get(name).copied()
1140    }
1141
1142    /// Marks `name` as being expanded by the Default recursion. Returns `true`
1143    /// when it was newly inserted, `false` when it is already on the expansion
1144    /// stack — a reference cycle that the caller must not recurse into (C1b).
1145    pub(crate) fn enter_default(&self, name: &str) -> bool {
1146        self.visiting.borrow_mut().insert(name.to_string())
1147    }
1148
1149    /// Removes `name` from the Default-recursion expansion stack, balancing a
1150    /// prior [`enter_default`](Ctx::enter_default) that returned `true`.
1151    pub(crate) fn leave_default(&self, name: &str) {
1152        self.visiting.borrow_mut().remove(name);
1153    }
1154}
1155
1156// ---------------------------------------------------------------------------
1157// Declaration emission.
1158// ---------------------------------------------------------------------------
1159
1160fn emit_decl(ctx: &Ctx, decl: &v2::Decl, tuples: &mut Vec<InducedTuple>) -> TokenStream {
1161    // The derive attribute is computed once and handed to the emitter, which
1162    // places it under the declaration's doc comment rather than above it.
1163    // Prepending it to the finished item would render it above the doc, which
1164    // is backwards from how Rust is written everywhere else — and `Default` is
1165    // never among the traits (`derives`, design decision 8).
1166    let derived = derives::derive_attr(ctx, decl);
1167    let item = match &decl.kind {
1168        Some(v2::decl::Kind::TypeDef(td)) => emit_type_def(decl, td, &derived),
1169        Some(v2::decl::Kind::ConstDef(cd)) => return emit_const(ctx, decl, cd),
1170        Some(v2::decl::Kind::StructDef(sd)) => emit_struct(decl, sd, &derived, tuples),
1171        Some(v2::decl::Kind::EnumDef(ed)) => emit_enum(decl, ed, &derived),
1172        Some(v2::decl::Kind::EnumSetDef(esd)) => emit_enum_set(decl, esd, &derived),
1173        Some(v2::decl::Kind::UnionDef(ud)) => emit_union(decl, ud, &derived),
1174        // Interaction kinds ride `Interface.interactions`, never a package
1175        // decl, so none of them reaches this match; nothing emits them today.
1176        Some(_) | None => return quote! {},
1177    };
1178
1179    let default_impl = defaults::decl_default_expr(ctx, decl)
1180        .map(|expr| {
1181            let name = ident(&decl.name);
1182            quote! { impl Default for #name { fn default() -> Self { #expr } } }
1183        })
1184        .unwrap_or_default();
1185
1186    quote! { #item #default_impl }
1187}
1188
1189/// A named scalar becomes a `#[repr(transparent)]` newtype with a private
1190/// inner value (typl §5.7). Construction goes through `new`, which enforces
1191/// the typl constraints, or `new_unchecked`, which does not.
1192///
1193/// `Violation` and `Rule` are named by absolute path and nothing is imported:
1194/// a typl package may declare a type named `Violation` or `Rule`, and a `use`
1195/// of either would collide with that declaration. The leading `::` covers a
1196/// package that declares a type named `ridl_rt`. The prelude names the
1197/// constructors use — `Result`, `Ok`, `Err`, `TryFrom`, `From` — are absolute
1198/// for the same reason: a type name is CamelCase (typl §15.1) and `ridl-sem`
1199/// reserves no identifier, so a package may declare `type Result`, and that
1200/// struct would shadow the prelude's in the module the constructors share
1201/// with it.
1202///
1203/// A deprecated declaration's impl blocks carry `#[allow(deprecated)]`, with
1204/// one exception: the `Default` impl `defaults::decl_default_expr` emits
1205/// carries no allow, because `emit_decl` attaches it outside this function.
1206/// Each covered impl block uses the deprecated type, and without the allow
1207/// the consumer's build draws the `deprecated` lint on code the consumer did
1208/// not write.
1209fn emit_type_def(decl: &v2::Decl, td: &v2::TypeDef, derived: &TokenStream) -> TokenStream {
1210    let name = ident(&decl.name);
1211    let inner = newtype_inner(td);
1212    let doc = doc_attrs(&decl.doc);
1213    let unchecked = unchecked_doc(td);
1214    // A blank doc line keeps the unchecked note out of the declaration's own
1215    // doc paragraph.
1216    let separator = if decl.doc.is_empty() || unchecked.is_empty() {
1217        quote! {}
1218    } else {
1219        quote! { #[doc = ""] }
1220    };
1221    let deprecated = deprecated_attr(decl.deprecated.as_deref());
1222    let allow_deprecated = if decl.deprecated.is_some() {
1223        quote! { #[allow(deprecated)] }
1224    } else {
1225        quote! {}
1226    };
1227    let vis = vis_tokens(decl.visibility);
1228    let type_name = decl.name.as_str();
1229
1230    if v2::constraint_is_vacuous(td.constraint.as_ref()) {
1231        return emit_vacuous_type_def(decl, td, derived);
1232    }
1233
1234    let check_param_ty = check_param_type(td);
1235    let check_shadow = check_deref_shadow(td);
1236    let check_body = constraint_checks(td, type_name, quote! { value });
1237    let getter = scalar_getter(td, vis.clone(), inner.clone());
1238
1239    quote! {
1240        #doc
1241        #separator
1242        #unchecked
1243        #derived
1244        #deprecated
1245        #[repr(transparent)]
1246        #vis struct #name(#inner);
1247
1248        #allow_deprecated
1249        impl #name {
1250            /// Constructs the value, enforcing its typl constraints.
1251            #vis fn new(
1252                value: #inner,
1253            ) -> ::core::result::Result<Self, ::ridl_rt::payload::Violation> {
1254                Self::check(&value)?;
1255                ::core::result::Result::Ok(Self::new_unchecked(value))
1256            }
1257
1258            /// Checks `value` against this type's typl constraints, without
1259            /// constructing it. `pub(crate)` rather than `pub`: a caller
1260            /// outside `new` is a function generated into this crate — since
1261            /// driftsys/ridl#467 that includes the codec of *another* package
1262            /// of the same build, which reaches this type through the module
1263            /// tree and so cannot see a private item here. The emitted crate
1264            /// is one crate per build, so `pub(crate)` reaches every such
1265            /// caller while adding nothing to the crate's public surface.
1266            /// Whether this becomes `pub` is Epic 10's call, still open.
1267            /// `new` is the composition of this and `new_unchecked`.
1268            pub(crate) fn check(
1269                value: #check_param_ty,
1270            ) -> ::core::result::Result<(), ::ridl_rt::payload::Violation> {
1271                #check_shadow
1272                #check_body
1273                ::core::result::Result::Ok(())
1274            }
1275
1276            /// Constructs the value without checking its constraints.
1277            ///
1278            /// Safe: nothing here relies on the invariant for memory
1279            /// soundness. Use it only for a value already known to satisfy
1280            /// the contract.
1281            #vis const fn new_unchecked(value: #inner) -> Self {
1282                Self(value)
1283            }
1284
1285            #getter
1286        }
1287
1288        #allow_deprecated
1289        impl ::core::convert::TryFrom<#inner> for #name {
1290            type Error = ::ridl_rt::payload::Violation;
1291            fn try_from(value: #inner) -> ::core::result::Result<Self, Self::Error> {
1292                Self::new(value)
1293            }
1294        }
1295
1296        #allow_deprecated
1297        impl ::core::convert::From<#name> for #inner {
1298            fn from(value: #name) -> Self {
1299                value.0
1300            }
1301        }
1302    }
1303}
1304
1305/// A named scalar whose constraint checks nothing: `boolean`, and `integer`
1306/// or `float` with no declared range. A `String` or `Vec<u8>` backing reaches
1307/// this only from hand-built IR: the checker always materializes the typl §4.4
1308/// default `[0..256]` length bound, so both are constrained on the source
1309/// route.
1310///
1311/// Construction is infallible, so `From<Inner>` is correct here. That is not
1312/// because the type carries no invariant at all — a `step`-only constraint
1313/// reaches this function too (`constraint_is_vacuous` excludes `step`), and
1314/// its quantization is a real invariant, which `unchecked_doc` names on the
1315/// type a few lines below. It is because `new` checks nothing `From` would
1316/// then bypass: `constraint_checks` emits a range branch only for a min or a
1317/// max, a length branch only for `len_min`/`len_max`, and a pattern branch
1318/// only for `pattern`/`pattern_const` — none of which a vacuous constraint
1319/// carries — and `step` is never checked by `new` on any type, constrained or
1320/// not. `From<Inner>` therefore introduces no failure the checked path would
1321/// have caught. Core's blanket `impl<T, U: Into<T>> TryFrom<U> for T` then
1322/// supplies `TryFrom<Inner>` with `Error = Infallible`, so generic consumer
1323/// code calling `try_from` compiles against both kinds of scalar. A manual
1324/// `TryFrom` would collide with that blanket impl (`rustc` reports `E0119`),
1325/// which is the second reason it is absent.
1326///
1327/// `new_unchecked` is deliberately absent: `new` already is the unchecked
1328/// path, and on this type it is `const`, so [`scalar_ctor`] routes a constant
1329/// and a derived default through `new` instead.
1330///
1331/// The prelude names are absolute for the reason [`emit_type_def`] records: a
1332/// typl package may declare `type From`, and that declaration shadows the
1333/// prelude in the module the generated impl shares with it.
1334fn emit_vacuous_type_def(decl: &v2::Decl, td: &v2::TypeDef, derived: &TokenStream) -> TokenStream {
1335    let name = ident(&decl.name);
1336    let inner = newtype_inner(td);
1337    let doc = doc_attrs(&decl.doc);
1338    // A `step`-only constraint is vacuous (`constraint_is_vacuous` excludes
1339    // `step`), and that is exactly the case `unchecked_doc` still speaks for,
1340    // so the note and its separator are computed here too.
1341    let unchecked = unchecked_doc(td);
1342    let separator = if decl.doc.is_empty() || unchecked.is_empty() {
1343        quote! {}
1344    } else {
1345        quote! { #[doc = ""] }
1346    };
1347    let deprecated = deprecated_attr(decl.deprecated.as_deref());
1348    let allow_deprecated = if decl.deprecated.is_some() {
1349        quote! { #[allow(deprecated)] }
1350    } else {
1351        quote! {}
1352    };
1353    let vis = vis_tokens(decl.visibility);
1354    let getter = scalar_getter(td, vis.clone(), inner.clone());
1355
1356    quote! {
1357        #doc
1358        #separator
1359        #unchecked
1360        #derived
1361        #deprecated
1362        #[repr(transparent)]
1363        #vis struct #name(#inner);
1364
1365        #allow_deprecated
1366        impl #name {
1367            /// Constructs the value. This type declares no constraint, so
1368            /// construction cannot fail.
1369            #vis const fn new(value: #inner) -> Self {
1370                Self(value)
1371            }
1372
1373            #getter
1374        }
1375
1376        #allow_deprecated
1377        impl ::core::convert::From<#inner> for #name {
1378            fn from(value: #inner) -> Self {
1379                Self(value)
1380            }
1381        }
1382
1383        #allow_deprecated
1384        impl ::core::convert::From<#name> for #inner {
1385            fn from(value: #name) -> Self {
1386                value.0
1387            }
1388        }
1389    }
1390}
1391
1392/// The associated function a constant or a derived default constructs a named
1393/// scalar through. A constrained type keeps `new_unchecked`, whose value is
1394/// checked by `ridlc` rather than at run time; a vacuous type has no
1395/// `new_unchecked` ([`emit_vacuous_type_def`]) and its `new` is `const`, so
1396/// both positions — a `const` item and the body of `fn default()` — accept it.
1397pub(crate) fn scalar_ctor(td: &v2::TypeDef) -> TokenStream {
1398    if v2::constraint_is_vacuous(td.constraint.as_ref()) {
1399        quote! { new }
1400    } else {
1401        quote! { new_unchecked }
1402    }
1403}
1404
1405/// The range, length and pattern checks for one constraint, as statements
1406/// that return early with a `Violation`. Only the branches the constraint
1407/// carries are emitted, so a string with a length bound and no range gets
1408/// only the length check. The pattern check is emitted last and is the only
1409/// one behind a feature gate.
1410///
1411/// A `min` or `max` is a numeric bound (typl §5.5), so a range check is
1412/// emitted only for a float or integer backing; on any other backing the two
1413/// are ignored rather than rendered as a literal of the wrong type.
1414fn constraint_checks(td: &v2::TypeDef, type_name: &str, value: TokenStream) -> TokenStream {
1415    let Some(c) = td.constraint.as_ref() else {
1416        return quote! {};
1417    };
1418    let mut checks = Vec::new();
1419
1420    let is_float = match backing_scalar(td) {
1421        ScalarBacking::Float => Some(true),
1422        ScalarBacking::Integer => Some(false),
1423        ScalarBacking::Boolean | ScalarBacking::String | ScalarBacking::Bytes => None,
1424    };
1425    if let Some(is_float) = is_float {
1426        if let Some(min) = c.min.as_deref() {
1427            let lit = numeric_tokens(min, is_float);
1428            checks.push(quote! {
1429                if #value < #lit {
1430                    return ::core::result::Result::Err(::ridl_rt::payload::Violation {
1431                        type_name: #type_name,
1432                        rule: ::ridl_rt::payload::Rule::Range,
1433                    });
1434                }
1435            });
1436        }
1437        // The newtype backing an integer is always `i64` (`newtype_inner`), so
1438        // a declared maximum at `i64::MAX` (9223372036854775807) makes
1439        // `value > 9223372036854775807` never true: rustc draws its
1440        // `unused_comparisons` warning on it in the consumer's build. The
1441        // branch is emitted only when the maximum is below the inner type's
1442        // maximum.
1443        let checked_max = c
1444            .max
1445            .as_deref()
1446            .filter(|max| is_float || max.parse::<i64>() != Ok(i64::MAX));
1447        if let Some(max) = checked_max {
1448            let lit = numeric_tokens(max, is_float);
1449            checks.push(quote! {
1450                if #value > #lit {
1451                    return ::core::result::Result::Err(::ridl_rt::payload::Violation {
1452                        type_name: #type_name,
1453                        rule: ::ridl_rt::payload::Rule::Range,
1454                    });
1455                }
1456            });
1457        }
1458    }
1459    // Length is in characters for string (typl §5.3) and bytes for bytes
1460    // (§5.4), which is why the two use different expressions. The cast is
1461    // parenthesized because `as u64 < 8` does not parse: after a cast type,
1462    // `<` opens a generic-argument list.
1463    if c.len_min.is_some() || c.len_max.is_some() {
1464        let len = match backing_scalar(td) {
1465            ScalarBacking::String => quote! { (#value.chars().count() as u64) },
1466            _ => quote! { (#value.len() as u64) },
1467        };
1468        // A minimum of 0 is the default length bound of string and bytes
1469        // (typl §4.4, §4.5), and `(… as u64) < 0` is never true: rustc draws
1470        // its `unused_comparisons` warning on it in the consumer's build. The
1471        // branch is emitted only for a positive minimum.
1472        if let Some(min) = c.len_min.filter(|min| *min > 0) {
1473            let lit = proc_macro2::Literal::u64_unsuffixed(min);
1474            checks.push(quote! {
1475                if #len < #lit {
1476                    return ::core::result::Result::Err(::ridl_rt::payload::Violation {
1477                        type_name: #type_name,
1478                        rule: ::ridl_rt::payload::Rule::Length,
1479                    });
1480                }
1481            });
1482        }
1483        if let Some(max) = c.len_max {
1484            let lit = proc_macro2::Literal::u64_unsuffixed(max);
1485            checks.push(quote! {
1486                if #len > #lit {
1487                    return ::core::result::Result::Err(::ridl_rt::payload::Violation {
1488                        type_name: #type_name,
1489                        rule: ::ridl_rt::payload::Rule::Length,
1490                    });
1491                }
1492            });
1493        }
1494    }
1495    if backing_scalar(td) == ScalarBacking::String
1496        && let Some(pattern) = c.pattern.as_deref()
1497    {
1498        // A `match` pattern is checked against text, and `regex::Regex`
1499        // matches `&str`. Only a `String` backing has a value that coerces
1500        // to `&str` (`newtype_inner`); a bytes backing carries `Vec<u8>`,
1501        // against which `Regex::is_match` does not type-check.
1502        //
1503        // No typl source reaches this: the reference gives bytes no `match`
1504        // (§4.5, §5.4) and `lower_scalar` passes `allow_pattern: false` for
1505        // that backing, so the pattern never enters the IR. The guard is
1506        // totality over the IR rather than over the surface, on the same
1507        // footing as the `is_float` guard above — `lower_len_scalar` always
1508        // leaves `min` and `max` absent, so that one is unreachable from a
1509        // typl source too, and is pinned by its own test. A backend reads
1510        // the IR, which need not have come from this checker.
1511        //
1512        // The pattern needs a regex engine, which `core` has none of. The
1513        // range and length checks above are not gated; only this one is, so
1514        // a `--no-default-features` build still validates the bounds it
1515        // emits.
1516        //
1517        // `::std` and `::regex` are absolute for the reason the prelude
1518        // names are: the face module of an interface named `Std` or `Regex`
1519        // is a module of that name in this same module, and it would shadow
1520        // the extern crate.
1521        let source = strip_regex_delimiters(pattern);
1522        checks.push(quote! {
1523            #[cfg(feature = "validate-pattern")]
1524            {
1525                static PATTERN: ::std::sync::LazyLock<::regex::Regex> =
1526                    ::std::sync::LazyLock::new(|| {
1527                        ::regex::Regex::new(#source).expect("ridlc emitted an invalid pattern")
1528                    });
1529                if !PATTERN.is_match(&#value) {
1530                    return ::core::result::Result::Err(::ridl_rt::payload::Violation {
1531                        type_name: #type_name,
1532                        rule: ::ridl_rt::payload::Rule::Pattern,
1533                    });
1534                }
1535            }
1536        });
1537    }
1538    quote! { #(#checks)* }
1539}
1540
1541/// The parameter type `check` (`emit_type_def`) borrows its value as. A
1542/// `String`/`Vec<u8>` backing borrows the slice form directly — `&str` and
1543/// `&[u8]` — which is what a zero-copy caller (the FlatBuffers codec's
1544/// `verify`) already holds and needs no allocation to produce; a `Copy`
1545/// backing borrows the newtype's own inner type, which [`check_deref_shadow`]
1546/// then reads back to a plain value so [`constraint_checks`]'s emitted
1547/// expressions need no change between `new`'s former inline form and `check`.
1548///
1549/// This matches on `backing_scalar` the same way [`newtype_inner`] does, by
1550/// the same backing's borrowed form rather than its owned one; the two
1551/// matches must stay in lockstep for every backing this function names, and
1552/// each names the other for that reason.
1553fn check_param_type(td: &v2::TypeDef) -> TokenStream {
1554    match backing_scalar(td) {
1555        ScalarBacking::Float => quote! { &f64 },
1556        ScalarBacking::Integer => quote! { &i64 },
1557        ScalarBacking::Boolean => quote! { &bool },
1558        ScalarBacking::String => quote! { &str },
1559        ScalarBacking::Bytes => quote! { &[u8] },
1560    }
1561}
1562
1563/// A `Copy` backing's `check` parameter is a reference (`check_param_type`),
1564/// while [`constraint_checks`]'s emitted comparisons are written against a
1565/// plain value (`value < 0.0`, not `*value < 0.0`). This reborrows the
1566/// parameter into a local of the same name and the owned type, so those
1567/// expressions type-check unchanged. `String` and `&[u8]` need no shadow:
1568/// their methods (`.chars()`, `.len()`) and the `&value` the pattern check
1569/// takes both work directly on the borrowed slice form.
1570fn check_deref_shadow(td: &v2::TypeDef) -> TokenStream {
1571    match backing_scalar(td) {
1572        ScalarBacking::Float | ScalarBacking::Integer | ScalarBacking::Boolean => {
1573            quote! { let value = *value; }
1574        }
1575        ScalarBacking::String | ScalarBacking::Bytes => quote! {},
1576    }
1577}
1578
1579/// The accessor. A `Copy` backing returns by value from a `const fn`; `String`
1580/// and `Vec<u8>` borrow, and gain `into_inner` for the owned form.
1581///
1582/// `backing_scalar` is total: it maps a unit backing and an absent backing to
1583/// `Float`, so every named scalar gets exactly one of the three forms.
1584fn scalar_getter(td: &v2::TypeDef, vis: TokenStream, inner: TokenStream) -> TokenStream {
1585    match backing_scalar(td) {
1586        ScalarBacking::String => quote! {
1587            #vis fn get(&self) -> &str { &self.0 }
1588            #vis fn into_inner(self) -> String { self.0 }
1589        },
1590        ScalarBacking::Bytes => quote! {
1591            #vis fn get(&self) -> &[u8] { &self.0 }
1592            #vis fn into_inner(self) -> Vec<u8> { self.0 }
1593        },
1594        _ => quote! {
1595            #vis const fn get(self) -> #inner { self.0 }
1596        },
1597    }
1598}
1599
1600/// The gaps a generated constructor does not close, named on the type itself
1601/// rather than left silent: a `step` is not checked by `new`. A literal
1602/// `match` pattern on a `String` backing is checked by `new`, but only under
1603/// the `validate-pattern` feature, so the type names that condition rather
1604/// than leaving the guarantee silently variable. On any other backing
1605/// `constraint_checks` emits no pattern branch at all (a `regex::Regex`
1606/// matches `&str`, and only a `String` backing's value coerces to one), so
1607/// the plain "not checked" line applies there instead. `pattern_const` is
1608/// read as well as `pattern`, because a pattern constant that did not resolve
1609/// leaves `pattern` absent while the type still carries a match constraint,
1610/// and no check is emitted for that case either, so it keeps the plain "not
1611/// checked" line.
1612fn unchecked_doc(td: &v2::TypeDef) -> TokenStream {
1613    let Some(c) = td.constraint.as_ref() else {
1614        return quote! {};
1615    };
1616    let mut lines = Vec::new();
1617    if c.step.is_some() {
1618        lines.push(" Quantization (`step`) is not checked by `new`.".to_string());
1619    }
1620    if c.pattern.is_some() && backing_scalar(td) == ScalarBacking::String {
1621        lines.push(
1622            " The `match` pattern is checked by `new` only when the crate is built with \
1623              the `validate-pattern` feature."
1624                .to_string(),
1625        );
1626    } else if c.pattern.is_some() || c.pattern_const.is_some() {
1627        lines.push(" The `match` pattern is not checked by `new`.".to_string());
1628    }
1629    quote! { #(#[doc = #lines])* }
1630}
1631
1632/// A constant becomes a `pub const`. A constant of a `String`-backed named type
1633/// (or of the `string` primitive, or a regex constant) is realized as a
1634/// `&'static str` rather than a value of the newtype: `String` cannot be
1635/// constructed in a `const` context. This asymmetry is documented in the C
1636/// header and here.
1637fn emit_const(ctx: &Ctx, decl: &v2::Decl, cd: &v2::ConstDef) -> TokenStream {
1638    // A constant is a value, not a type: there is nothing to derive on it.
1639    let attrs = decl_attrs(decl, &quote! {});
1640    let vis = vis_tokens(decl.visibility);
1641    let name = ident(&decl.name);
1642
1643    // A regex constant declares no type; it holds the pattern source text. The
1644    // IR stores that text with its typl `/…/` delimiters, which are syntax, not
1645    // pattern content, so they are stripped before the `&str` value is emitted:
1646    // the const holds the pattern a consumer can feed to a regex engine (M1).
1647    if let Some(regex) = &cd.regex {
1648        let pattern = strip_regex_delimiters(regex);
1649        return quote! { #attrs #vis const #name: &str = #pattern; };
1650    }
1651
1652    let Some(type_ref) = cd.type_ref.as_deref() else {
1653        return quote! {};
1654    };
1655
1656    // A named-type constant resolves through the type's backing; a
1657    // primitive-keyword constant reads the keyword directly.
1658    if let Some(backing) = same_package_scalar_backing(ctx, type_ref) {
1659        // `same_package_scalar_ctor` resolves the same declaration through
1660        // the same lookup as the `backing` above, so a `Some` here is
1661        // guaranteed once `backing` is: there is no reachable case with a
1662        // backing and no ctor. A fallback here would be dead code, and a
1663        // wrong one besides — `new` is fallible on a constrained type and
1664        // does not type-check in this `const` position (`scalar_ctor` names
1665        // `new_unchecked` for exactly that type).
1666        let ctor = same_package_scalar_ctor(ctx, type_ref)
1667            .expect("a same-package scalar backing implies a same-package scalar ctor");
1668        match backing {
1669            ScalarBacking::Float => {
1670                let value = numeric_tokens(&cd.value, true);
1671                let type_name = type_path(type_ref);
1672                quote! { #attrs #vis const #name: #type_name = #type_name::#ctor(#value); }
1673            }
1674            ScalarBacking::Integer => {
1675                let value = numeric_tokens(&cd.value, false);
1676                let type_name = type_path(type_ref);
1677                quote! { #attrs #vis const #name: #type_name = #type_name::#ctor(#value); }
1678            }
1679            ScalarBacking::Boolean => {
1680                let value = bool_tokens(&cd.value);
1681                let type_name = type_path(type_ref);
1682                quote! { #attrs #vis const #name: #type_name = #type_name::#ctor(#value); }
1683            }
1684            ScalarBacking::String => {
1685                let value = cd.value.as_str();
1686                quote! { #attrs #vis const #name: &str = #value; }
1687            }
1688            ScalarBacking::Bytes => quote! {},
1689        }
1690    } else if let Some(prim) = primitive_keyword(type_ref) {
1691        match prim {
1692            v2::PrimitiveType::Integer => {
1693                let value = numeric_tokens(&cd.value, false);
1694                quote! { #attrs #vis const #name: i64 = #value; }
1695            }
1696            v2::PrimitiveType::Float => {
1697                let value = numeric_tokens(&cd.value, true);
1698                quote! { #attrs #vis const #name: f64 = #value; }
1699            }
1700            v2::PrimitiveType::Boolean => {
1701                let value = bool_tokens(&cd.value);
1702                quote! { #attrs #vis const #name: bool = #value; }
1703            }
1704            v2::PrimitiveType::String => {
1705                let value = cd.value.as_str();
1706                quote! { #attrs #vis const #name: &str = #value; }
1707            }
1708            v2::PrimitiveType::Bytes | v2::PrimitiveType::Unspecified => quote! {},
1709        }
1710    } else {
1711        // A cross-package or unresolved constant type: the backing is unknown
1712        // here, so the constant is skipped rather than mis-typed.
1713        quote! {}
1714    }
1715}
1716
1717fn emit_struct(
1718    decl: &v2::Decl,
1719    sd: &v2::StructDef,
1720    derived: &TokenStream,
1721    tuples: &mut Vec<InducedTuple>,
1722) -> TokenStream {
1723    let name = ident(&decl.name);
1724    let attrs = decl_attrs(decl, derived);
1725    let vis = vis_tokens(decl.visibility);
1726    let repr = if sd.fixed_layout {
1727        quote! { #[repr(C)] }
1728    } else {
1729        quote! {}
1730    };
1731
1732    let fields = sd.members.iter().filter_map(|member| match &member.member {
1733        Some(v2::struct_member::Member::Field(field)) => {
1734            Some(emit_field(&decl.name, decl.visibility, field, tuples))
1735        }
1736        // A reserved tombstone occupies an ordinal but emits no field
1737        // (typl §7.4).
1738        Some(v2::struct_member::Member::Reserved(_)) | None => None,
1739    });
1740
1741    quote! {
1742        #attrs
1743        #repr
1744        #vis struct #name {
1745            #(#fields),*
1746        }
1747    }
1748}
1749
1750/// One struct field. The name is projected through the pinned transform
1751/// (ADR-0016 decisions 1 and 2): a typl field name is camelCase (typl §15.1)
1752/// and reaching generated Rust verbatim draws `non_snake_case` at every
1753/// consumer. The `hint` below keeps `camel_case`, because it builds the type
1754/// name of an induced tuple struct rather than a field name. That second
1755/// projection reaches a namespace RIDL-149 does not check — two field names
1756/// distinct under `snake_case` can induce one tuple type name — which is
1757/// driftsys/ridl#453, recorded in ADR-0016's consequences.
1758fn emit_field(
1759    parent: &str,
1760    visibility: i32,
1761    field: &v2::Field,
1762    tuples: &mut Vec<InducedTuple>,
1763) -> TokenStream {
1764    let field_name = ident(&snake_case(&field.name));
1765    let attrs = field_attrs(field);
1766    let hint = format!("{}{}", camel_case(parent), camel_case(&field.name));
1767    let ty = field
1768        .r#type
1769        .as_ref()
1770        .map(|ft| field_type_tokens(ft, &hint, visibility, tuples))
1771        .unwrap_or_else(|| quote! { () });
1772    quote! { #attrs pub #field_name: #ty }
1773}
1774
1775/// An enum becomes `#[repr(i64)]` with the declared discriminants (typl §8).
1776/// Variant names keep their typl `SCREAMING_SNAKE` spelling.
1777fn emit_enum(decl: &v2::Decl, ed: &v2::EnumDef, derived: &TokenStream) -> TokenStream {
1778    let name = ident(&decl.name);
1779    let attrs = decl_attrs(decl, derived);
1780    let vis = vis_tokens(decl.visibility);
1781
1782    let variants = ed.values.iter().map(|value| {
1783        let vname = ident(&value.name);
1784        let disc = int_tokens(value.value);
1785        let vdoc = doc_attrs(&value.doc);
1786        quote! { #vdoc #vname = #disc }
1787    });
1788
1789    // A raw discriminant off the wire is where an out-of-contract value
1790    // actually enters a program: a wire backend emits no constructor
1791    // (ADR-0013 decision 2), so this is the validating seam.
1792    let arms = ed.values.iter().map(|value| {
1793        let vname = ident(&value.name);
1794        let disc = int_tokens(value.value);
1795        quote! { #disc => ::core::result::Result::Ok(Self::#vname) }
1796    });
1797    let type_name = decl.name.as_str();
1798    let allow_deprecated = if decl.deprecated.is_some() {
1799        quote! { #[allow(deprecated)] }
1800    } else {
1801        quote! {}
1802    };
1803
1804    quote! {
1805        #attrs
1806        #[repr(i64)]
1807        #vis enum #name {
1808            #(#variants),*
1809        }
1810
1811        #allow_deprecated
1812        impl ::core::convert::TryFrom<i64> for #name {
1813            type Error = ::ridl_rt::payload::Violation;
1814            fn try_from(value: i64) -> ::core::result::Result<Self, Self::Error> {
1815                match value {
1816                    #(#arms,)*
1817                    _ => ::core::result::Result::Err(::ridl_rt::payload::Violation {
1818                        type_name: #type_name,
1819                        rule: ::ridl_rt::payload::Rule::Variant,
1820                    }),
1821                }
1822            }
1823        }
1824
1825        #allow_deprecated
1826        impl ::core::convert::From<#name> for i64 {
1827            fn from(value: #name) -> Self { value as i64 }
1828        }
1829    }
1830}
1831
1832/// An enum set becomes a `#[repr(transparent)]` newtype over `i64` (the
1833/// language layer width, Appendix D) with one associated bit constant per bit
1834/// position (typl §9).
1835///
1836/// The inner value is private, as a named scalar's is and for the same reason
1837/// ([`emit_type_def`]): a raw bit pattern enters through `TryFrom<i64>`, which
1838/// refuses a value carrying an undeclared bit. `get` reads it back.
1839///
1840/// The bit constants, `DECLARED_MASK` and `get` share one inherent impl block
1841/// so that a deprecated declaration carries `#[allow(deprecated)]` over all
1842/// three. The `impl` header itself names the deprecated type, as do the bit
1843/// constants and `get`; `DECLARED_MASK` names only `i64`, and is covered
1844/// because it shares the block. Without the allow the consumer's build draws
1845/// the `deprecated` lint on code the consumer did not write — which is what
1846/// driftsys/ridl#420 settled for a named scalar's impl blocks.
1847fn emit_enum_set(decl: &v2::Decl, esd: &v2::EnumSetDef, derived: &TokenStream) -> TokenStream {
1848    let name = ident(&decl.name);
1849    let attrs = decl_attrs(decl, derived);
1850    let vis = vis_tokens(decl.visibility);
1851
1852    let bits = esd.bits.iter().map(|bit| {
1853        let bname = ident(&bit.name);
1854        let shift = int_tokens(bit.value);
1855        quote! { #vis const #bname: #name = #name(1 << #shift); }
1856    });
1857
1858    // Refusing a value that carries an undeclared bit is this backend's
1859    // reading, not a rule the reference states. typl §9 fixes a bit's
1860    // identity as its declared position and infers the width from the highest
1861    // one; it says nothing about what an undeclared bit means. The reading is
1862    // in tension with `ridl-diff`, whose `EnumSetDef` arm classifies an
1863    // appended bit as compatible outright — it calls `appended_slot` with an
1864    // empty retired set, so an enum set has no retired half the way an enum's
1865    // values do (`crates/ridl-diff/src/classify.rs`). A producer that appends
1866    // a bit on that advice sends a value an older consumer's `TryFrom` then
1867    // refuses whole, rather than ignoring the bit it does not know. Whether
1868    // an enum set is closed or open on the wire is recorded as an open
1869    // question rather than settled here, because settling it changes that arm
1870    // of `ridl-diff` as well as this backend.
1871    //
1872    // A bit outside the int64 domain contributes nothing to the mask rather
1873    // than shifting by it. `ridl-sem` reports TYPL-111 for a position outside
1874    // 0..=63 and still carries the bit into the IR — its range guard covers
1875    // the width it derives, not the value it stores — so this fold can be
1876    // handed one. `1i64 << 64` panics in a debug build, and codegen is total:
1877    // every failure is a `GenerateError` value, never a panic.
1878    //
1879    // The filter covers the fold alone, and that is all it is for. The bit
1880    // constants still emit `#name(1 << 64)` as source text, which rustc
1881    // rejects under its deny-by-default `arithmetic_overflow`, and the mask
1882    // then omits a bit the type publishes as a constant. Both are reachable
1883    // only on a package the checker has already failed with TYPL-111, so no
1884    // build that produces usable output reaches either. The filter keeps the
1885    // compiler from panicking; it does not make such a package emit sound
1886    // code.
1887    let mask = esd
1888        .bits
1889        .iter()
1890        .filter(|bit| (0..=63).contains(&bit.value))
1891        .fold(0i64, |acc, bit| acc | (1i64 << bit.value));
1892    let mask_lit = int_tokens(mask);
1893    let type_name = decl.name.as_str();
1894    let allow_deprecated = if decl.deprecated.is_some() {
1895        quote! { #[allow(deprecated)] }
1896    } else {
1897        quote! {}
1898    };
1899
1900    quote! {
1901        #attrs
1902        #[repr(transparent)]
1903        #vis struct #name(i64);
1904        #allow_deprecated
1905        impl #name {
1906            #(#bits)*
1907
1908            /// The union of every declared bit. `TryFrom` refuses a value
1909            /// that carries any other bit.
1910            #vis const DECLARED_MASK: i64 = #mask_lit;
1911
1912            #vis const fn get(self) -> i64 { self.0 }
1913        }
1914
1915        #allow_deprecated
1916        impl ::core::convert::TryFrom<i64> for #name {
1917            type Error = ::ridl_rt::payload::Violation;
1918            fn try_from(value: i64) -> ::core::result::Result<Self, Self::Error> {
1919                if value & !Self::DECLARED_MASK != 0 {
1920                    return ::core::result::Result::Err(::ridl_rt::payload::Violation {
1921                        type_name: #type_name,
1922                        rule: ::ridl_rt::payload::Rule::Variant,
1923                    });
1924                }
1925                ::core::result::Result::Ok(Self(value))
1926            }
1927        }
1928
1929        #allow_deprecated
1930        impl ::core::convert::From<#name> for i64 {
1931            fn from(value: #name) -> Self { value.0 }
1932        }
1933    }
1934}
1935
1936/// A union becomes a `pub enum` with one variant per arm; arm names are
1937/// CamelCased (typl §10). Reserved arms are skipped.
1938fn emit_union(decl: &v2::Decl, ud: &v2::UnionDef, derived: &TokenStream) -> TokenStream {
1939    let name = ident(&decl.name);
1940    let attrs = decl_attrs(decl, derived);
1941    let vis = vis_tokens(decl.visibility);
1942
1943    let variants = ud.arms.iter().map(|arm| {
1944        let vname = ident(&camel_case(&arm.name));
1945        let ty = type_path(&arm.type_ref);
1946        let vdoc = doc_attrs(&arm.doc);
1947        quote! { #vdoc #vname(#ty) }
1948    });
1949
1950    quote! {
1951        #attrs
1952        #vis enum #name {
1953            #(#variants),*
1954        }
1955    }
1956}
1957
1958/// Emits the generated struct for one tuple type (typl §11), plus its `Default`
1959/// impl when every tuple field is derivable.
1960///
1961/// The struct carries the visibility of the declaration that induced it, and a
1962/// tuple nested inside it inherits the same one — a tuple has no visibility of
1963/// its own to declare, so the only visibility it can have is the one it was
1964/// reached at (see [`InducedTuple`] and [`vis_tokens`]). The fields stay `pub`,
1965/// as they are on a declared `struct`: a field's effective visibility is capped
1966/// by the item's, so `pub(crate) struct T { pub f: Private }` exposes nothing.
1967///
1968/// A tuple field name is projected through the pinned transform, the same one
1969/// [`emit_field`] applies to a declared struct's field (ADR-0016 decisions 1
1970/// and 2). The `hint` below keeps `camel_case`, because it builds a nested
1971/// tuple's type name rather than a field name. Neither namespace is checked:
1972/// two tuple field names distinct in typl can spell one Rust field name, which
1973/// rustc then rejects with E0124 — driftsys/ridl#449.
1974fn emit_tuple_struct(
1975    ctx: &Ctx,
1976    induced: &InducedTuple,
1977    tuples: &mut Vec<InducedTuple>,
1978) -> TokenStream {
1979    let InducedTuple {
1980        name,
1981        tuple,
1982        visibility,
1983    } = induced;
1984    let name_id = ident(name);
1985    let vis = vis_tokens(*visibility);
1986    let derived = derives::tuple_derive_attr(ctx, tuple);
1987    let fields = tuple.fields.iter().map(|field| {
1988        let fname = ident(&snake_case(&field.name));
1989        let hint = format!("{}{}", name, camel_case(&field.name));
1990        let ty = field
1991            .r#type
1992            .as_ref()
1993            .map(|ft| field_type_tokens(ft, &hint, *visibility, tuples))
1994            .unwrap_or_else(|| quote! { () });
1995        quote! { pub #fname: #ty }
1996    });
1997
1998    let struct_item = quote! {
1999        #derived
2000        #vis struct #name_id {
2001            #(#fields),*
2002        }
2003    };
2004
2005    let default_impl = defaults::tuple_default_expr(ctx, name, tuple)
2006        .map(|expr| quote! { impl Default for #name_id { fn default() -> Self { #expr } } })
2007        .unwrap_or_default();
2008
2009    quote! { #struct_item #default_impl }
2010}
2011
2012// ---------------------------------------------------------------------------
2013// Type mapping.
2014// ---------------------------------------------------------------------------
2015
2016/// The Rust type of a field. Tuple field types generate a named nested struct
2017/// (recorded in `tuples`); the struct name is `hint` (CamelCase of the path).
2018///
2019/// `visibility` is the visibility of the declaration this position belongs to.
2020/// It is carried rather than derived because a tuple is anonymous in source and
2021/// declares none of its own, and because it reaches [`emit_tuple_struct`]
2022/// through a flat worklist that has forgotten where it came from
2023/// ([`InducedTuple`]).
2024pub(crate) fn field_type_tokens(
2025    ft: &v2::FieldType,
2026    hint: &str,
2027    visibility: i32,
2028    tuples: &mut Vec<InducedTuple>,
2029) -> TokenStream {
2030    let inner = match &ft.kind {
2031        Some(v2::field_type::Kind::Named(name)) => type_path(name),
2032        Some(v2::field_type::Kind::Primitive(prim)) => primitive_tokens(*prim),
2033        Some(v2::field_type::Kind::InlineScalar(td)) => inline_scalar_tokens(td),
2034        Some(v2::field_type::Kind::Tuple(tuple)) => {
2035            let tuple_name = hint.to_string();
2036            tuples.push(InducedTuple {
2037                name: tuple_name.clone(),
2038                tuple: tuple.clone(),
2039                visibility,
2040            });
2041            let id = ident(&tuple_name);
2042            quote! { #id }
2043        }
2044        Some(v2::field_type::Kind::Array(array)) => {
2045            let element = array
2046                .element
2047                .as_ref()
2048                .map(|el| field_type_tokens(el, &format!("{hint}Element"), visibility, tuples))
2049                .unwrap_or_else(|| quote! { () });
2050            if array.min == array.max {
2051                let len = usize_tokens(array.min);
2052                quote! { [#element; #len] }
2053            } else {
2054                quote! { Vec<#element> }
2055            }
2056        }
2057        Some(v2::field_type::Kind::Map(map)) => {
2058            let key = map
2059                .key
2060                .as_ref()
2061                .map(|k| field_type_tokens(k, &format!("{hint}Key"), visibility, tuples))
2062                .unwrap_or_else(|| quote! { () });
2063            let value = map
2064                .value
2065                .as_ref()
2066                .map(|v| field_type_tokens(v, &format!("{hint}Value"), visibility, tuples))
2067                .unwrap_or_else(|| quote! { () });
2068            quote! { Vec<(#key, #value)> }
2069        }
2070        // A stream is an interaction-position type (ridl §12.3); it never
2071        // reaches a struct or tuple field in checked IR. Kept total.
2072        Some(v2::field_type::Kind::Stream(_)) | None => quote! { () },
2073    };
2074
2075    if ft.optional {
2076        quote! { Option<#inner> }
2077    } else {
2078        inner
2079    }
2080}
2081
2082/// The Rust newtype inner type for a named scalar backing (Appendix D language
2083/// layer): unit and float back to `f64`, integer to `i64`.
2084///
2085/// [`check_param_type`] matches on `backing_scalar` the same way, one entry
2086/// per backing this function names, in its borrowed form; the two matches
2087/// must stay in lockstep, and each names the other for that reason.
2088fn newtype_inner(td: &v2::TypeDef) -> TokenStream {
2089    match backing_scalar(td) {
2090        ScalarBacking::Float => quote! { f64 },
2091        ScalarBacking::Integer => quote! { i64 },
2092        ScalarBacking::Boolean => quote! { bool },
2093        ScalarBacking::String => quote! { String },
2094        ScalarBacking::Bytes => quote! { Vec<u8> },
2095    }
2096}
2097
2098fn inline_scalar_tokens(td: &v2::TypeDef) -> TokenStream {
2099    match backing_scalar(td) {
2100        ScalarBacking::Float => quote! { f64 },
2101        ScalarBacking::Integer => quote! { i64 },
2102        ScalarBacking::Boolean => quote! { bool },
2103        ScalarBacking::String => quote! { String },
2104        ScalarBacking::Bytes => quote! { Vec<u8> },
2105    }
2106}
2107
2108pub(crate) fn primitive_tokens(prim: i32) -> TokenStream {
2109    match v2::PrimitiveType::try_from(prim).unwrap_or(v2::PrimitiveType::Unspecified) {
2110        v2::PrimitiveType::Boolean => quote! { bool },
2111        v2::PrimitiveType::Integer => quote! { i64 },
2112        v2::PrimitiveType::Float => quote! { f64 },
2113        v2::PrimitiveType::String => quote! { String },
2114        v2::PrimitiveType::Bytes => quote! { Vec<u8> },
2115        v2::PrimitiveType::Unspecified => quote! { () },
2116    }
2117}
2118
2119/// A resolved type reference: a bare `Ident` for a same-package name, a
2120/// `crate::`-anchored path for a cross-package `pkg.Name` reference (typl §3.2).
2121/// The dotted package path maps directly to Rust module path segments. The
2122/// `crate::` anchor lets a consumer compose several generated packages as
2123/// sibling modules rooted at the crate — `crate::veh::common::Speed` resolves
2124/// from any module, whereas a bare `veh::common::Speed` only resolves from the
2125/// crate root (I4).
2126pub(crate) fn type_path(reference: &str) -> TokenStream {
2127    if reference.contains('.') {
2128        let segments = reference.split('.').map(ident);
2129        quote! { crate #(:: #segments)* }
2130    } else {
2131        let id = ident(reference);
2132        quote! { #id }
2133    }
2134}
2135
2136/// The Rust module-segment spelling of one typl package name segment: `mod`
2137/// becomes `r#mod`, `crate` becomes `crate_`, and an ordinary segment is
2138/// returned unchanged.
2139///
2140/// This exists so that the module tree `ridlc` writes for `--emit rust` and
2141/// the paths [`type_path`] emits cannot drift apart. Both spell a package
2142/// segment through [`ident`], which is the only definition of that spelling.
2143/// A tree that writes a segment raw emits `pub mod mod;`, which does not
2144/// parse, and a tree that escapes a keyword differently from the reference
2145/// emits a module the reference cannot name.
2146pub fn module_segment(segment: &str) -> String {
2147    ident(segment).to_string()
2148}
2149
2150/// Strips a regex literal's surrounding `/…/` delimiters, leaving the pattern
2151/// body. A value without both delimiters is returned unchanged.
2152fn strip_regex_delimiters(regex: &str) -> &str {
2153    regex
2154        .strip_prefix('/')
2155        .and_then(|rest| rest.strip_suffix('/'))
2156        .unwrap_or(regex)
2157}
2158
2159// ---------------------------------------------------------------------------
2160// Scalar backing classification (shared by emission and default derivation).
2161// ---------------------------------------------------------------------------
2162
2163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2164pub(crate) enum ScalarBacking {
2165    Float,
2166    Integer,
2167    Boolean,
2168    String,
2169    Bytes,
2170}
2171
2172/// The Rust-layer scalar class of a type definition's backing. A unit backing
2173/// implies float (typl §5.1).
2174pub(crate) fn backing_scalar(td: &v2::TypeDef) -> ScalarBacking {
2175    match td.backing.as_ref().and_then(|b| b.kind.as_ref()) {
2176        Some(v2::backing::Kind::Unit(_)) => ScalarBacking::Float,
2177        Some(v2::backing::Kind::Primitive(prim)) => {
2178            match v2::PrimitiveType::try_from(*prim).unwrap_or(v2::PrimitiveType::Unspecified) {
2179                v2::PrimitiveType::Boolean => ScalarBacking::Boolean,
2180                v2::PrimitiveType::Integer => ScalarBacking::Integer,
2181                v2::PrimitiveType::Float => ScalarBacking::Float,
2182                v2::PrimitiveType::String => ScalarBacking::String,
2183                v2::PrimitiveType::Bytes | v2::PrimitiveType::Unspecified => ScalarBacking::Bytes,
2184            }
2185        }
2186        None => ScalarBacking::Float,
2187    }
2188}
2189
2190/// The backing class of a same-package named scalar type, or `None` when the
2191/// reference does not name a scalar `TypeDef` in this package.
2192pub(crate) fn same_package_scalar_backing(ctx: &Ctx, reference: &str) -> Option<ScalarBacking> {
2193    match &ctx.lookup(reference)?.kind {
2194        Some(v2::decl::Kind::TypeDef(td)) => Some(backing_scalar(td)),
2195        _ => None,
2196    }
2197}
2198
2199/// [`scalar_ctor`] for a same-package named scalar, read through the same
2200/// lookup as [`same_package_scalar_backing`]. A reference that resolves to
2201/// anything but a named scalar has no constructor to name.
2202fn same_package_scalar_ctor(ctx: &Ctx, reference: &str) -> Option<TokenStream> {
2203    match &ctx.lookup(reference)?.kind {
2204        Some(v2::decl::Kind::TypeDef(td)) => Some(scalar_ctor(td)),
2205        _ => None,
2206    }
2207}
2208
2209/// Maps a typl primitive keyword written as a type reference to its primitive.
2210fn primitive_keyword(reference: &str) -> Option<v2::PrimitiveType> {
2211    match reference {
2212        "boolean" => Some(v2::PrimitiveType::Boolean),
2213        "integer" => Some(v2::PrimitiveType::Integer),
2214        "float" => Some(v2::PrimitiveType::Float),
2215        "string" => Some(v2::PrimitiveType::String),
2216        "bytes" => Some(v2::PrimitiveType::Bytes),
2217        _ => None,
2218    }
2219}
2220
2221// ---------------------------------------------------------------------------
2222// Attributes: docs, deprecation, visibility.
2223// ---------------------------------------------------------------------------
2224
2225/// The attributes that precede a generated item: its doc comment first, then
2226/// its `#[derive(...)]`, then `#[deprecated]`. The derive sits under the doc
2227/// comment because that is where Rust is conventionally written; it sits above
2228/// `#[deprecated]` and the `#[repr(...)]` each emitter adds because a reader
2229/// looks for the trait list first.
2230fn decl_attrs(decl: &v2::Decl, derived: &TokenStream) -> TokenStream {
2231    let doc = doc_attrs(&decl.doc);
2232    let deprecated = deprecated_attr(decl.deprecated.as_deref());
2233    quote! { #doc #derived #deprecated }
2234}
2235
2236fn field_attrs(field: &v2::Field) -> TokenStream {
2237    let doc = doc_attrs(&field.doc);
2238    let deprecated = deprecated_attr(field.deprecated.as_deref());
2239    quote! { #doc #deprecated }
2240}
2241
2242/// One `#[doc]` attribute per line; prettyplease renders these as `///`
2243/// comments. A leading space makes the rendered comment read `/// text`.
2244pub(crate) fn doc_attrs(doc: &str) -> TokenStream {
2245    if doc.is_empty() {
2246        return quote! {};
2247    }
2248    let lines = doc.split('\n').map(|line| {
2249        let text = format!(" {line}");
2250        quote! { #[doc = #text] }
2251    });
2252    quote! { #(#lines)* }
2253}
2254
2255/// `@deprecated` maps to `#[deprecated]`; a present-but-empty reason (the IR's
2256/// `Some("")`) still emits the bare attribute (typl §14.2).
2257pub(crate) fn deprecated_attr(reason: Option<&str>) -> TokenStream {
2258    match reason {
2259        Some("") => quote! { #[deprecated] },
2260        Some(reason) => quote! { #[deprecated(note = #reason)] },
2261        None => quote! {},
2262    }
2263}
2264
2265/// `internal` maps to `pub(crate)` — Rust's package-private mechanism
2266/// (ADR-0002 §8, ADR-0008 decision 7, typl §3.3). The rule is per declaration,
2267/// not per module: a package holding one `internal` and one public declaration
2268/// generates one `pub(crate)` item and one `pub` item.
2269///
2270/// It governs the item a declaration is realized as **and the auxiliary types
2271/// that item's shape induces**. A tuple in a field or an interaction position
2272/// generates a named struct of its own ([`emit_tuple_struct`]), and that struct
2273/// carries the visibility of the declaration that induced it, the way #160
2274/// derived one visibility per interface and applied it to all four of that
2275/// interface's names.
2276///
2277/// Until issue #167 the induced struct was fixed at `pub`, which is wrong in
2278/// both directions. It publishes the shape of a declaration the keyword hides —
2279/// the argument #160 made for the interface's own four names applies unchanged
2280/// to a fifth name the same declaration generates. And it does not compile: an
2281/// `internal` declaration may name `internal` declarations freely (typl §3.3),
2282/// so `internal struct Holder { t : (a : Hidden) }` puts a `pub(crate)` type in
2283/// a `pub` struct's field and rustc reports `private_interfaces`. The corpus
2284/// denies that lint by name, and `ridlc check` accepts the source, so the two
2285/// halves disagreed until the visibility was carried.
2286///
2287/// The reverse direction is closed by TYPL-005 on the **source** route: a
2288/// public declaration naming an `internal` one is rejected, so a `pub` induced
2289/// struct never holds a `pub(crate)` type. That is not the same as an invariant,
2290/// and the difference is load-bearing. Two declarations whose paths mangle to
2291/// one struct name reach the same state by a route TYPL-005 cannot see — one
2292/// declaration `internal`, the other public, no `internal` payload type
2293/// anywhere — and carrying a visibility onto a name two declarations share
2294/// would make a program that compiled today fail `private_interfaces`. That is
2295/// why [`tuple_collision`] refuses the collision instead: the invariant holds
2296/// because the state that breaks it is not generated, not because it cannot be
2297/// described.
2298pub(crate) fn vis_tokens(visibility: i32) -> TokenStream {
2299    match v2::Visibility::try_from(visibility).unwrap_or(v2::Visibility::Unspecified) {
2300        v2::Visibility::Internal => quote! { pub(crate) },
2301        _ => quote! { pub },
2302    }
2303}
2304
2305// ---------------------------------------------------------------------------
2306// Literals and identifiers.
2307// ---------------------------------------------------------------------------
2308
2309/// A Rust identifier for a typl name. typl names are always character-valid
2310/// identifiers (typl §2.3); the only conflict is a name that is a Rust keyword,
2311/// escaped here as a raw identifier (`r#override`). The four keywords that
2312/// cannot be raw identifiers (`crate`, `self`, `Self`, `super`) and the bare
2313/// underscore are mangled with a trailing underscore.
2314///
2315/// The call is total, per the codegen contract (ADR-0004 §5, and the
2316/// never-panics guarantee `ridlc::compile` documents). A valid typl name is
2317/// never empty, so an empty `name` only arrives from malformed IR — but the
2318/// backend is also reachable from the language server over half-written
2319/// source, so it must not panic. An empty name lowers to Rust's wildcard `_`.
2320/// It cannot collide with a real name: a typl name of `_` is mangled to `__`
2321/// on the branch above.
2322///
2323/// How far `_` is caught depends on the position, and the split is not
2324/// uniform:
2325///
2326/// - **Declaration-name positions** — a struct, enum, trait or type-alias
2327///   name, an enum variant, a `fn`, `static` or `mod` name, a trait or impl
2328///   method. `_` is rejected by the `syn::parse2` gate in [`generate`], which
2329///   returns a [`GenerateError`], so the malformed name is reported rather
2330///   than emitted.
2331/// - **Field and binding positions** — a struct field, a tuple-struct field, a
2332///   `fn` parameter, a `const` name. syn *accepts* `_` here
2333///   (`syn::Field::parse_named` calls `Ident::parse_any` once it peeks
2334///   `Token![_]`), so [`emit_field`] would emit `pub _: T` and the gate would
2335///   not catch it. `rustc` still rejects the field, so the output is never
2336///   silently valid, but no [`GenerateError`] is raised. A derived `Default`
2337///   usually catches it anyway, because the struct *expression* it builds has
2338///   no valid `Member` — but `defaults::struct_default` returns `None` for a
2339///   non-constructible field (a cross-package reference carrying a declared
2340///   init, for one), and then nothing is left to catch it.
2341///
2342/// A field-position empty name is unreachable today, and for a structural
2343/// reason rather than a lucky one: `ridl_syntax`'s `Parser::block_body` and
2344/// `Parser::param_list` announce a member only on `SyntaxKind::Ident`, so
2345/// `field_def`, `param`, `enum_value` and `union_arm` are never entered
2346/// without a name. `Parser::interface_body` is the exception — it announces
2347/// members by the *interaction keyword*, so the name can be missing — which is
2348/// precisely and only why interactions were the vulnerable site.
2349/// `generate_emits_an_empty_field_name_without_a_derivable_default` pins that
2350/// gap, so a regression that makes a nameless field reachable is visible
2351/// rather than silent.
2352pub(crate) fn ident(name: &str) -> Ident {
2353    if let Ok(parsed) = syn::parse_str::<Ident>(name) {
2354        return parsed;
2355    }
2356    if matches!(name, "crate" | "self" | "Self" | "super" | "_") {
2357        return Ident::new(&format!("{name}_"), Span::call_site());
2358    }
2359    if name.is_empty() {
2360        // `Ident::new_raw("")` and `Ident::new("")` both panic; `Ident::new`
2361        // accepts `_` (`Ident::new_raw` does not — `r#_` is not a raw
2362        // identifier).
2363        return Ident::new("_", Span::call_site());
2364    }
2365    Ident::new_raw(name, Span::call_site())
2366}
2367
2368/// Numeric literal tokens from a canonical decimal string. The int/float kind
2369/// comes from the caller (derived from the backing width), never from the
2370/// string form: the IR drops the float form, so a float value can read `"0"`.
2371/// A float literal is given a decimal point so it stays a float in Rust.
2372pub(crate) fn numeric_tokens(value: &str, is_float: bool) -> TokenStream {
2373    let text = if is_float && !value.contains('.') && !value.contains('e') && !value.contains('E') {
2374        format!("{value}.0")
2375    } else {
2376        value.to_string()
2377    };
2378    text.parse().unwrap_or_else(|_| quote! { 0 })
2379}
2380
2381fn int_tokens(value: i64) -> TokenStream {
2382    value.to_string().parse().unwrap_or_else(|_| quote! { 0 })
2383}
2384
2385fn usize_tokens(value: u64) -> TokenStream {
2386    value.to_string().parse().unwrap_or_else(|_| quote! { 0 })
2387}
2388
2389pub(crate) fn bool_tokens(value: &str) -> TokenStream {
2390    if value == "true" {
2391        quote! { true }
2392    } else {
2393        quote! { false }
2394    }
2395}
2396
2397#[cfg(test)]
2398mod tests;