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, "e! {});
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;