zerodds_lang_rules/emit/zig.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The Zig backend of the shared codec walk.
4//!
5//! DDS-XTypes 1.3 §7.4 from the clause, the same as the columns before
6//! it, and none of them calls another. Where two columns disagree on an
7//! octet, one has misread the specification — which is the only reason
8//! to have a further one.
9//!
10//! # The five decisions this column turns on
11//!
12//! **Errors travel as error unions, and the seam writes the `try`.**
13//! Zig has no exceptions: a runtime operation that can refuse answers
14//! `zdwire.WireError!T`, every generated routine answers
15//! `zdwire.WireError!void` or `!T`, and this backend prepends `try` at
16//! each fallible call. Which operations are fallible is a contract
17//! between this file and `runtime/zig/zdwire.zig` — the two tables
18//! `FALLIBLE_EXEMPT_METHODS` and `FALLIBLE_STATICS` are that
19//! contract's edge, and a name moved on one side without the other is a
20//! Zig compile error (`expected error union type`), never a silent
21//! drop.
22//!
23//! **A member holds its wire width, under Zig's own names.** An IDL
24//! `long` is `i32`, a `char` is `u8`, a `wchar` `u16`, so
25//! [`CodecTarget::code_point`] is the identity. A `long double` is a
26//! real `f128` — the one column whose built-in set reaches it; the
27//! adapter's table carries the argument.
28//!
29//! **There is no one cast operator, so [`CodecTarget::cast`] chooses.**
30//! `@as(T, @intCast(v))` between integer widths, `@enumFromInt` into an
31//! enumeration (the walk casts a union label into its discriminator's
32//! type), `@bitCast` for the walk's `signed char` reading of a map key,
33//! and `zdwire.asI64`/`asU64` where the walk widens a value it cannot
34//! see the type of — an enumeration sample and a union overlay arm are
35//! the two, and a comptime `switch` on `@typeInfo` answers for both.
36//!
37//! **`@optional` is `?T` and `@external` is `*T`, and the read
38//! direction allocates.** A decoded `T` coerces into a `?T` on its own;
39//! a `*T` needs storage, which `zdwire.zdPtr(r.heap, …)` takes from the
40//! reader's allocator — the same arena everything else the reader
41//! materialises comes from.
42//!
43//! **Zig's unused-and-shadowing rules are settled in [`Zig::routine`],
44//! once.** An unused parameter and a pointless discard are both compile
45//! errors, and which one a body earns depends on the whole body; the
46//! routine assembler scans it and keeps exactly the discards that are
47//! needed. A counted loop renders inside its own block, so two
48//! sequential loops can share a counter name without Zig's shadowing
49//! error — that one lives in the renderer.
50
51use super::{CodecTarget, Constant, Handle, Handoff, Routine, Scalar};
52use crate::adapter::zig::{field_name, zig_map_entry_name, zig_type_name, zig_value_type};
53use crate::encode::EncodeError;
54use zerodds_ast::{Ast, CharWidth, MemberNode, TypeKind, TypeNode};
55use zerodds_ast_core::Id;
56use zerodds_lang_cst::common::{Doc, Ident, Literal};
57use zerodds_lang_cst::zig;
58
59/// The Zig backend.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct Zig;
62
63/// How deep a chain of typedefs and collections is followed.
64const MAX_TYPE_DEPTH: usize = 64;
65
66/// The name the generated file imports the runtime under.
67const RUNTIME: &str = "zdwire";
68
69/// The type name [`CodecTarget::named`] is handed for C's signed
70/// `char`; the Zig member is a `u8` and the signed reading is
71/// `@as(i8, @bitCast(..))`.
72const SIGNED_CHAR: &str = "signed char";
73
74/// The reader and writer operations that answer plainly, everything
75/// else on a receiver being `WireError!T`.
76///
77/// One half of the fallibility contract with `runtime/zig/zdwire.zig`;
78/// the module documentation says why a mismatch is loud.
79const FALLIBLE_EXEMPT_METHODS: &[&str] = &[
80 "reader_status",
81 "reader_position",
82 "reader_remaining",
83 "reader_save",
84 "reader_restore",
85 "reader_refuse",
86 "reader_clear_discard",
87 "reader_begin_packed_element",
88 "reader_finish_packed_element",
89 "begin_packed_element",
90 "finish_packed_element",
91 "finish_pid_read",
92 "position",
93 "trim_utf8",
94 "trim_utf16",
95];
96
97/// The free functions of the runtime that answer `WireError!T`;
98/// everything else [`CodecTarget::runtime_static`] names answers
99/// plainly. The other half of the contract.
100const FALLIBLE_STATICS: &[&str] = &["utf16_units"];
101
102/// An expression, with the two properties the walk's seams cannot
103/// carry: whether it needs a `try`, and which indirection the place it
104/// names lives behind.
105#[derive(Debug, Clone)]
106pub enum ZigExpr {
107 /// An ordinary expression.
108 Plain(zig::Expr),
109 /// A call that answers `WireError!T`: `try` in a statement or
110 /// initialiser position, `(try …)` inside a larger expression.
111 Fallible(zig::Expr),
112 /// The `?T` field of an `@optional` member, as a place a decoded
113 /// `T` is about to be written into — the coercion is Zig's own.
114 OptionalSlot(zig::Expr),
115 /// The `*T` field of an `@external` member, likewise; the decoded
116 /// value goes through `zdwire.zdPtr` to get storage.
117 ExternalSlot(zig::Expr),
118}
119
120/// A Zig type, with the two properties [`CodecTarget::cast`] needs and
121/// the tree cannot carry: whether the type is an enumeration once
122/// typedefs are seen through, which decides `@enumFromInt` against
123/// `@intCast`, and whether a named type is an integer alias, which
124/// decides `@intCast` against a bare `@as`.
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct ZigType {
127 /// The type as written.
128 pub tree: zig::Type,
129 /// Whether a cast into it converts from an integer with
130 /// `@enumFromInt`.
131 pub enum_like: bool,
132 /// Whether the type names an integer under its own name — a
133 /// bitmask, whose Zig form is `pub const Flags = u8;`. The tree
134 /// says `Named("Flags")` and `@as(Flags, zd_bits)` on the walk's
135 /// sixty-four-bit holder is `expected type 'u8', found 'u64'`;
136 /// the narrowing has to be stated.
137 pub integer_alias: bool,
138}
139
140impl ZigType {
141 fn plain(tree: zig::Type) -> Self {
142 Self {
143 tree,
144 enum_like: false,
145 integer_alias: false,
146 }
147 }
148}
149
150/// The expression inside, whichever form it took; a fallible one is
151/// bracketed so `try` binds to the call and not to whatever expression
152/// it ends up inside.
153fn inner(expr: ZigExpr) -> zig::Expr {
154 match expr {
155 ZigExpr::Plain(e) | ZigExpr::OptionalSlot(e) | ZigExpr::ExternalSlot(e) => e,
156 ZigExpr::Fallible(e) => zig::Expr::Paren(Box::new(zig::Expr::try_(e))),
157 }
158}
159
160/// The expression with a bare `try` where one is due — an initialiser
161/// and an assignment right-hand side take it without the bracket.
162fn inner_bare(expr: ZigExpr) -> zig::Expr {
163 match expr {
164 ZigExpr::Plain(e) | ZigExpr::OptionalSlot(e) | ZigExpr::ExternalSlot(e) => e,
165 ZigExpr::Fallible(e) => zig::Expr::try_(e),
166 }
167}
168
169fn plain(e: zig::Expr) -> ZigExpr {
170 ZigExpr::Plain(e)
171}
172
173/// A local's name, moved off Zig's primitive namespace: the encode walk
174/// counts an array's dimensions with `i0`, `i1`, …, which Zig parses as
175/// arbitrary-width integer types and refuses as locals. The prefix and
176/// not `escape_decl`'s trailing underscore, because the lexer reads
177/// `i0_` as a malformed width too ("primitive integer type 'i0_' has
178/// leading zero", Zig 0.14.1). Applied at every place a local is
179/// written or read, so the two spellings cannot part.
180fn local_name(text: &str) -> String {
181 if zig::is_primitive_name(text) {
182 return format!("zd_{text}");
183 }
184 text.to_string()
185}
186
187impl Zig {
188 /// A runtime name, qualified: `zdwire.X`.
189 fn runtime_name(name: &str) -> zig::Expr {
190 zig::Expr::dot(zig::Expr::Name(Ident::new(RUNTIME)), name)
191 }
192
193 /// A runtime operation's Zig spelling: `write_u32` is `writeU32`,
194 /// each `_`-separated segment after the first with its first letter
195 /// raised — the names `runtime/zig` publishes, because that runtime
196 /// was written against this conversion.
197 fn camel(method: &str) -> String {
198 let mut out = String::new();
199 for (index, segment) in method.split('_').enumerate() {
200 if index == 0 {
201 out.push_str(segment);
202 continue;
203 }
204 let mut chars = segment.chars();
205 if let Some(first) = chars.next() {
206 out.extend(first.to_uppercase());
207 out.push_str(chars.as_str());
208 }
209 }
210 out
211 }
212
213 fn text(value: &str) -> ZigExpr {
214 plain(zig::Expr::Literal(Literal::Str(value.to_string())))
215 }
216
217 /// One value of a runtime enumeration: `zdwire.Status.ok`.
218 fn runtime_enum(type_: &str, value: &str) -> ZigExpr {
219 plain(zig::Expr::dot(Self::runtime_name(type_), value))
220 }
221
222 /// `r.heap` — the reader's arena, which is where everything the
223 /// read direction materialises comes from.
224 fn reader_heap() -> zig::Expr {
225 zig::Expr::dot(zig::Expr::Name(Ident::new("r")), "heap")
226 }
227
228 /// Whether the resolved type behind `id` is an enumeration.
229 fn is_enumeration(ast: &Ast, id: Id<TypeNode>) -> bool {
230 super::resolves_to_enumeration(ast, id)
231 }
232
233 /// Whether the resolved type behind `id` is a bit mask, whose Zig
234 /// form is a `pub const` alias of an unsigned integer rather than a
235 /// type of its own.
236 fn is_bitmask(ast: &Ast, id: Id<TypeNode>) -> bool {
237 let mut current = id;
238 for _ in 0..MAX_TYPE_DEPTH {
239 match ast.type_(current).map(|node| node.kind.clone()) {
240 Ok(TypeKind::Bitmask { .. }) => return true,
241 Ok(TypeKind::Alias { aliased }) => match aliased.resolved() {
242 Some(target) => current = target,
243 None => return false,
244 },
245 _ => return false,
246 }
247 }
248 false
249 }
250
251 /// Whether `tree` is a fixed-width integer or `usize` — the types a
252 /// cast reaches with `@intCast`.
253 fn is_integer_type(tree: &zig::Type) -> bool {
254 match tree {
255 zig::Type::Primitive(primitive) => matches!(
256 primitive,
257 zig::Primitive::I8
258 | zig::Primitive::I16
259 | zig::Primitive::I32
260 | zig::Primitive::I64
261 | zig::Primitive::U8
262 | zig::Primitive::U16
263 | zig::Primitive::U32
264 | zig::Primitive::U64
265 ),
266 zig::Type::Named(name) => name.as_str() == "usize",
267 _ => false,
268 }
269 }
270}
271
272impl CodecTarget for Zig {
273 type Expr = ZigExpr;
274 type Stmt = zig::Stmt;
275 type Block = Vec<zig::Stmt>;
276 type Type = ZigType;
277 type Decl = zig::Decl;
278
279 // -- types --------------------------------------------------------
280
281 /// The fixed-width Zig type each scalar local takes. `Index` is
282 /// `u32` — a sequence length is a `uint32` on the wire, and a `u32`
283 /// coerces into a slice index — and `Size` is `usize`, which is
284 /// what the reader's positions answer in.
285 fn scalar(kind: Scalar) -> Self::Type {
286 ZigType::plain(match kind {
287 Scalar::Bool => zig::Type::Primitive(zig::Primitive::Bool),
288 Scalar::Char | Scalar::UInt8 => zig::Type::Primitive(zig::Primitive::U8),
289 Scalar::Int8 => zig::Type::Primitive(zig::Primitive::I8),
290 Scalar::Int16 => zig::Type::Primitive(zig::Primitive::I16),
291 Scalar::UInt16 => zig::Type::Primitive(zig::Primitive::U16),
292 Scalar::Int32 => zig::Type::Primitive(zig::Primitive::I32),
293 Scalar::UInt32 | Scalar::Index => zig::Type::Primitive(zig::Primitive::U32),
294 Scalar::Int64 => zig::Type::Primitive(zig::Primitive::I64),
295 Scalar::UInt64 => zig::Type::Primitive(zig::Primitive::U64),
296 Scalar::Float => zig::Type::Primitive(zig::Primitive::F32),
297 Scalar::Double => zig::Type::Primitive(zig::Primitive::F64),
298 // The real binary128; the adapter's table says why this is
299 // the one column that has it.
300 Scalar::LongDouble => zig::Type::Primitive(zig::Primitive::F128),
301 Scalar::Size => zig::Type::named("usize"),
302 })
303 }
304
305 fn named(name: &str) -> Self::Type {
306 ZigType::plain(zig::Type::named(name))
307 }
308
309 fn nothing() -> Self::Type {
310 ZigType::plain(zig::Type::Primitive(zig::Primitive::Void))
311 }
312
313 /// The runtime's handles, qualified.
314 fn handle(kind: Handle) -> Self::Type {
315 ZigType::plain(zig::Type::named(match kind {
316 Handle::DHeader => "zdwire.DHeader",
317 Handle::EmHeader => "zdwire.EmHeader",
318 Handle::Pid => "zdwire.Pid",
319 Handle::TryConstruct => "zdwire.TryConstruct",
320 Handle::ReaderMark => "zdwire.ReaderMark",
321 Handle::PidRead => "zdwire.PidRead",
322 Handle::EmHeaderRead => "zdwire.EmHeaderRead",
323 }))
324 }
325
326 /// The member already is the code unit — `u8` for a `char`, `u16`
327 /// for a `wchar` — so there is nothing to convert.
328 fn code_point(value: Self::Expr, _narrow: bool) -> Self::Expr {
329 value
330 }
331
332 /// `@intFromEnum(v)` for an enumeration, the value itself for
333 /// anything else. `adapter/zig.rs` declares an enumeration as
334 /// `enum(i32)` with every enumerator's declared value written out,
335 /// and Zig's `<` refuses a pair of them ("operator < not allowed
336 /// for type 'main.Colour'", measured in the matrix refresh of
337 /// 2026-10-02 on `map-key-enum` under the Fast DDS and OpenDDS
338 /// dialects); the tag is
339 /// [`zerodds_ast_layout::MapKeyOrdering::EnumeratorValue`]'s order.
340 /// The union overlay asks the same seam for every integer arm, so a
341 /// type that is not an enumeration passes through.
342 fn enumerator_ordinal(ast: &Ast, id: Id<TypeNode>, value: Self::Expr) -> Self::Expr {
343 if !Self::is_enumeration(ast, id) {
344 return value;
345 }
346 plain(zig::Expr::call("@intFromEnum", vec![inner(value)]))
347 }
348
349 /// `zdwire.boolLess(a, b)` — `false` before `true`. An override
350 /// because Zig's own `<` refuses a pair of booleans outright
351 /// ("operator < not allowed for type 'bool'"), which is exactly the
352 /// class of target the trait documents this seam for.
353 fn bool_less(left: Self::Expr, right: Self::Expr) -> Self::Expr {
354 plain(zig::Expr::Call {
355 func: Box::new(Self::runtime_name("boolLess")),
356 args: vec![inner(left), inner(right)],
357 })
358 }
359
360 /// The runtime's policy values: integers and strings where
361 /// `runtime/zig` takes them so, and the runtime's own enumerations
362 /// for the enumerated kinds. One arm per constant and no catch-all.
363 fn constant(which: Constant) -> Self::Expr {
364 match which {
365 Constant::WcharWidth2 => plain(zig::Expr::int(2)),
366 Constant::WcharWidth4 => plain(zig::Expr::int(4)),
367 Constant::WcharFixed => Self::text("fixed"),
368 Constant::WcharGiopLengthPrefixed => Self::text("giop-length-prefixed"),
369 Constant::WstringUnitsStream => Self::text("stream"),
370 Constant::WstringUnitsBigEndianAlways => Self::text("big-endian-always"),
371 Constant::WstringUnitsByteOrderMark => Self::text("byte-order-mark"),
372 Constant::WstringUnitsHostUnswapped => Self::text("host-unswapped"),
373 Constant::WstringSpecification => Self::text("specification"),
374 Constant::WstringCharacterCount => Self::text("character-count"),
375 Constant::WstringCharacterCountTerminated => Self::text("character-count-terminated"),
376 Constant::WstringSpecificationWideUnits => Self::text("specification-wide-units"),
377 Constant::WstringRosTypeSupport => Self::text("ros-type-support"),
378 Constant::WstringOctetsTerminated2 => Self::text("octets/terminated/2"),
379 Constant::WstringOctetsTerminated4 => Self::text("octets/terminated/4"),
380 Constant::WstringCharactersTerminated2 => Self::text("characters/terminated/2"),
381 Constant::FixedDeclared => Self::text("declared"),
382 Constant::FixedValuePrecision => Self::text("value-precision"),
383 Constant::FixedValueStringDigits => Self::text("value-string-digits"),
384 Constant::ParameterMember => Self::runtime_enum("ParameterRole", "member"),
385 Constant::ParameterOptional => Self::runtime_enum("ParameterRole", "optional"),
386 Constant::TryConstructDiscard => Self::runtime_enum("TryConstruct", "discard"),
387 Constant::TryConstructTrim => Self::runtime_enum("TryConstruct", "trim"),
388 Constant::TryConstructUseDefault => Self::runtime_enum("TryConstruct", "use_default"),
389 Constant::StatusOk => Self::runtime_enum("Status", "ok"),
390 Constant::StatusDiscarded => Self::runtime_enum("Status", "discarded"),
391 Constant::StatusImpossibleLength => Self::runtime_enum("Status", "impossible_length"),
392 Constant::StatusOutOfRange => Self::runtime_enum("Status", "out_of_range"),
393 Constant::StatusTruncated => Self::runtime_enum("Status", "truncated"),
394 }
395 }
396
397 /// The unsigned type wide enough for `bits` of a bit set — the same
398 /// answer the materialiser's `zig_holder` gives its fields.
399 fn unsigned_holder(bits: u16) -> Self::Type {
400 ZigType::plain(zig::Type::Primitive(crate::adapter::zig::zig_holder(bits)))
401 }
402
403 fn value_type(ast: &Ast, id: Id<TypeNode>) -> Self::Type {
404 // A type with no Zig form was refused by the materialiser
405 // before a codec walk ran, and `void` makes the impossible fail
406 // in the Zig compiler, loudly, rather than compile into a
407 // plausible member.
408 let tree = match zig_value_type(ast, id, "codec") {
409 Ok(tree) => tree,
410 Err(_) => zig::Type::Primitive(zig::Primitive::Void),
411 };
412 ZigType {
413 tree,
414 enum_like: Self::is_enumeration(ast, id),
415 integer_alias: Self::is_bitmask(ast, id),
416 }
417 }
418
419 fn type_name(ast: &Ast, id: Id<TypeNode>) -> String {
420 type_tag(ast, id, 0)
421 }
422
423 /// The entry name the materialiser declared, and a distinct one
424 /// per type when its tag walk refuses: the slot keeps two refused
425 /// entries from collapsing onto one declaration, which would merge
426 /// two map types under one name.
427 fn map_entry_name(ast: &Ast, id: Id<TypeNode>) -> String {
428 zig_map_entry_name(ast, id).unwrap_or_else(|_| format!("ZdUnresolvedEntry{}", id.slot()))
429 }
430
431 /// Nothing: a declared type's Zig name is already flat — the
432 /// adapter folds the modules in — and an enumerator lives in its
433 /// enum's own scope, which [`CodecTarget::enumerator_constant`]'s
434 /// override reaches.
435 fn module_prefix(_ast: &Ast, _id: Id<TypeNode>) -> String {
436 String::new()
437 }
438
439 /// The escape and nothing else, exactly as the materialiser spells
440 /// a field.
441 fn escape(name: &str) -> String {
442 field_name(name)
443 }
444
445 /// The base is the first member `parent` (XTypes 1.3 §7.5.1.4.1.3).
446 fn inherited_path(depth: usize) -> Option<String> {
447 Some(format!("{}.", field_name("parent")).repeat(depth))
448 }
449
450 // -- expressions ---------------------------------------------------
451
452 fn name(text: &str) -> Self::Expr {
453 plain(zig::Expr::Name(Ident::new(&local_name(text))))
454 }
455
456 fn int(value: i128) -> Self::Expr {
457 plain(zig::Expr::int(value))
458 }
459
460 /// The raw tokens the walk escapes through [`Literal::Int`],
461 /// rewritten into the tree's own boolean where they are one.
462 fn literal(value: Literal) -> Self::Expr {
463 let Literal::Int(text) = &value else {
464 return plain(zig::Expr::Literal(value));
465 };
466 match text.as_str() {
467 "true" => plain(zig::Expr::Literal(Literal::Bool(true))),
468 "false" => plain(zig::Expr::Literal(Literal::Bool(false))),
469 _ => plain(zig::Expr::Literal(value)),
470 }
471 }
472
473 /// The number, unsuffixed: a Zig integer literal is a
474 /// `comptime_int` of arbitrary precision and coerces to whatever
475 /// the position wants, so no width suffix exists or is needed.
476 fn wide_int(value: i128) -> Self::Expr {
477 plain(zig::Expr::int(value))
478 }
479
480 /// As [`CodecTarget::wide_int`]: a bare `comptime_int`.
481 fn unsigned_int(value: i128) -> Self::Expr {
482 plain(zig::Expr::int(value))
483 }
484
485 /// The bare number here too; the one operation a `comptime_int`
486 /// refuses — the `~` a bitmask's declared-bit check complements —
487 /// is given its width at the complement, in [`Zig::unary`].
488 fn wide_unsigned(value: u64) -> Self::Expr {
489 plain(zig::Expr::int(i128::from(value)))
490 }
491
492 fn wide_signed(value: i128) -> Self::Expr {
493 plain(zig::Expr::int(value))
494 }
495
496 /// `v.x` — and `v._d` as it stands: Zig keeps the IDL-facing
497 /// spelling, underscore and all.
498 fn member(base: &str, field: &str) -> Self::Expr {
499 plain(zig::Expr::dot(zig::Expr::Name(Ident::new(base)), field))
500 }
501
502 fn whole(base: &str) -> Self::Expr {
503 plain(zig::Expr::Name(Ident::new(&local_name(base))))
504 }
505
506 fn field_of(value: Self::Expr, field: &str) -> Self::Expr {
507 plain(zig::Expr::dot(inner(value), field))
508 }
509
510 fn index(value: Self::Expr, at: Self::Expr) -> Self::Expr {
511 plain(zig::Expr::Index {
512 value: Box::new(inner(value)),
513 index: Box::new(inner(at)),
514 })
515 }
516
517 /// `@as(u32, @intCast(v.len))`, or the bound where the materialiser
518 /// fixed one: `.len` is a `usize` and a sequence's count is a
519 /// `uint32` on the wire.
520 fn count_of(base: &str, fixed: Option<u64>) -> Self::Expr {
521 match fixed {
522 Some(bound) => plain(zig::Expr::int(i128::from(bound))),
523 None => plain(zig::Expr::as_(
524 zig::Type::Primitive(zig::Primitive::U32),
525 zig::Expr::call(
526 "@intCast",
527 vec![zig::Expr::dot(zig::Expr::Name(Ident::new(base)), "len")],
528 ),
529 )),
530 }
531 }
532
533 /// The slice itself: a Zig slice is the elements.
534 fn elements(base: &str) -> Self::Expr {
535 plain(zig::Expr::Name(Ident::new(base)))
536 }
537
538 /// `v.x` — a union is a struct carrying its branches directly.
539 fn case_member(base: &str, field: &str) -> Self::Expr {
540 Self::member(base, field)
541 }
542
543 /// `entry.key` and `entry.value`, the two fields the materialiser
544 /// gives a map's entry struct.
545 fn entry_half(entry: Self::Expr, key: bool) -> Self::Expr {
546 plain(zig::Expr::dot(
547 inner(entry),
548 if key { "key" } else { "value" },
549 ))
550 }
551
552 fn absent() -> Self::Expr {
553 plain(zig::Expr::Literal(Literal::Null))
554 }
555
556 /// Nothing: a Zig slice and string always exist once the runtime
557 /// answered — an allocation failure is an error the `try` already
558 /// propagated — so there is no absent state for the guard to test.
559 fn materialised(_value: Self::Expr) -> Option<Self::Expr> {
560 None
561 }
562
563 /// `v.x.?` — the value a `?T` holds, given that it holds one. Only
564 /// ever reached inside a branch the presence test guarded, where
565 /// the unwrap cannot trip.
566 fn optional_value(_ast: &Ast, _id: Id<TypeNode>, base: &str, field: &str) -> Self::Expr {
567 plain(zig::Expr::dot(inner(Self::member(base, field)), "?"))
568 }
569
570 /// `v.x != null`.
571 fn present(base: &str, field: &str) -> Self::Expr {
572 plain(zig::Expr::Binary {
573 op: "!=",
574 lhs: Box::new(inner(Self::member(base, field))),
575 rhs: Box::new(zig::Expr::Literal(Literal::Null)),
576 })
577 }
578
579 /// Nothing on the way in — assigning the value is what makes the
580 /// member present, the `T` to `?T` coercion being Zig's own — and
581 /// `v.x = null;` on the way out.
582 fn set_present(base: &str, field: &str, present: bool) -> Vec<Self::Stmt> {
583 if present {
584 return Vec::new();
585 }
586 vec![zig::Stmt::Assign {
587 place: inner(Self::member(base, field)),
588 value: zig::Expr::Literal(Literal::Null),
589 }]
590 }
591
592 /// The marked place; the decoded `T` coerces into the `?T` field on
593 /// assignment, so no room has to be made first.
594 fn optional_destination(base: &str, field: &str) -> (Vec<Self::Stmt>, Self::Expr) {
595 (
596 Vec::new(),
597 ZigExpr::OptionalSlot(inner(Self::member(base, field))),
598 )
599 }
600
601 /// `true`: an `@external` member is a pointer, and a member on a cycle needs
602 /// the same: `?T` stores its `T` inline.
603 /// [`crate::adapter::held_externally`] is the rule the adapter and
604 /// the defaulter ask as well.
605 fn holds_a_cycle_externally() -> bool {
606 true
607 }
608
609 /// `v.x.*` — the pointer dereferenced — and `v.x.?.*` where the
610 /// member is also `@optional`: the optional carries the absence and
611 /// the pointer the indirection, in that order.
612 fn external_value(
613 _ast: &Ast,
614 _id: Id<TypeNode>,
615 base: &str,
616 field: &str,
617 optional: bool,
618 ) -> Self::Expr {
619 let mut place = inner(Self::member(base, field));
620 if optional {
621 place = zig::Expr::dot(place, "?");
622 }
623 plain(zig::Expr::dot(place, "*"))
624 }
625
626 /// The marked place; [`Zig::read_call`] routes the decoded value
627 /// through `zdwire.zdPtr`, and the `*T` it answers coerces into a
628 /// `?*T` where the member is also `@optional`.
629 fn external_destination(
630 base: &str,
631 field: &str,
632 _optional: bool,
633 ) -> (Vec<Self::Stmt>, Self::Expr) {
634 (
635 Vec::new(),
636 ZigExpr::ExternalSlot(inner(Self::member(base, field))),
637 )
638 }
639
640 /// `v.x.*` for a branch too: the union arm's field is the same `*T`
641 /// a structure's `@external` member is. No union member is
642 /// `@optional` (DDS-XTypes 1.3 §7.2.2.4.4.4.7), so no `.?` appears.
643 fn external_case_value(_ast: &Ast, _id: Id<TypeNode>, base: &str, field: &str) -> Self::Expr {
644 plain(zig::Expr::dot(inner(Self::case_member(base, field)), "*"))
645 }
646
647 /// The marked place, exactly as [`Zig::external_destination`]
648 /// answers for a structure member.
649 fn external_case_destination(base: &str, field: &str) -> (Vec<Self::Stmt>, Self::Expr) {
650 (
651 Vec::new(),
652 ZigExpr::ExternalSlot(inner(Self::case_member(base, field))),
653 )
654 }
655
656 /// `v.x != null`: the member is `?*T` wherever this is asked, and
657 /// the optional is what carries the absence.
658 fn external_present(base: &str, field: &str) -> Self::Expr {
659 Self::present(base, field)
660 }
661
662 /// Nothing on the way in, `v.x = null;` on the way out — the same
663 /// answer the plain optional gives, because the absence lives in
664 /// the same `?`.
665 fn external_set_present(base: &str, field: &str, present: bool) -> Vec<Self::Stmt> {
666 Self::set_present(base, field, present)
667 }
668
669 /// The operators, with the two boolean connectives translated:
670 /// the walk spells `&&` and `||` and Zig spells `and` and `or`.
671 /// `=` never reaches a rendered expression — [`Zig::perform`] turns
672 /// it into an assignment statement.
673 fn binary(op: &'static str, lhs: Self::Expr, rhs: Self::Expr) -> Self::Expr {
674 let spelled = match op {
675 "&&" => "and",
676 "||" => "or",
677 other => other,
678 };
679 plain(zig::Expr::Binary {
680 op: spelled,
681 lhs: Box::new(inner(lhs)),
682 rhs: Box::new(inner(rhs)),
683 })
684 }
685
686 /// The operator as given, with one rewrite: `~` on a bare integer
687 /// literal becomes `~@as(u64, ..)`, because a `comptime_int` has no
688 /// width to complement — the one place the walk builds that is the
689 /// bitmask reader's declared-bit check, whose other operand is the
690 /// sixty-four-bit holder.
691 fn unary(op: &'static str, value: Self::Expr) -> Self::Expr {
692 let operand = inner(value);
693 if op == "~" {
694 if let zig::Expr::Literal(Literal::Int(_)) = &operand {
695 return plain(zig::Expr::Unary {
696 op: "~",
697 value: Box::new(zig::Expr::as_(
698 zig::Type::Primitive(zig::Primitive::U64),
699 operand,
700 )),
701 });
702 }
703 }
704 plain(zig::Expr::Unary {
705 op,
706 value: Box::new(operand),
707 })
708 }
709
710 fn paren(value: Self::Expr) -> Self::Expr {
711 plain(zig::Expr::Paren(Box::new(inner(value))))
712 }
713
714 /// The conversion, spelled by what it converts into — the module
715 /// documentation carries the table. A `bool` target with an integer
716 /// literal becomes the boolean literal, as in the sibling columns.
717 fn cast(type_: Self::Type, value: Self::Expr) -> Self::Expr {
718 let operand = inner(value);
719 if type_.tree == zig::Type::named(SIGNED_CHAR) {
720 return plain(zig::Expr::as_(
721 zig::Type::Primitive(zig::Primitive::I8),
722 zig::Expr::call("@bitCast", vec![operand]),
723 ));
724 }
725 if type_.tree == zig::Type::Primitive(zig::Primitive::Bool) {
726 if let zig::Expr::Literal(Literal::Int(text)) = &operand {
727 return plain(zig::Expr::Literal(Literal::Bool(text != "0")));
728 }
729 }
730 if type_.enum_like {
731 return plain(zig::Expr::as_(
732 type_.tree,
733 zig::Expr::call("@enumFromInt", vec![operand]),
734 ));
735 }
736 if type_.tree == zig::Type::Primitive(zig::Primitive::I64) {
737 return plain(zig::Expr::Call {
738 func: Box::new(Self::runtime_name("asI64")),
739 args: vec![operand],
740 });
741 }
742 if type_.tree == zig::Type::Primitive(zig::Primitive::U64) {
743 return plain(zig::Expr::Call {
744 func: Box::new(Self::runtime_name("asU64")),
745 args: vec![operand],
746 });
747 }
748 if Self::is_integer_type(&type_.tree) || type_.integer_alias {
749 // A literal coerces on its own; a runtime value states the
750 // narrowing out loud.
751 if let zig::Expr::Literal(Literal::Int(_)) = &operand {
752 return plain(zig::Expr::as_(type_.tree, operand));
753 }
754 return plain(zig::Expr::as_(
755 type_.tree,
756 zig::Expr::call("@intCast", vec![operand]),
757 ));
758 }
759 plain(zig::Expr::as_(type_.tree, operand))
760 }
761
762 /// The value, unchanged: Zig hands a slice by header, an array and
763 /// a struct by value.
764 fn handoff(value: Self::Expr, _form: Handoff) -> Self::Expr {
765 value
766 }
767
768 /// A call to a generated codec — fallible, so the seam owns the
769 /// `try`. `strcmp` is the runtime's, which spells C11 §7.24.4's
770 /// unsigned-octet order over two UTF-8 slices, and answers plainly.
771 fn call(callee: &str, args: Vec<Self::Expr>) -> Self::Expr {
772 let arguments: Vec<zig::Expr> = args.into_iter().map(inner).collect();
773 if callee == "strcmp" {
774 return plain(zig::Expr::Call {
775 func: Box::new(Self::runtime_name("strcmp")),
776 args: arguments,
777 });
778 }
779 ZigExpr::Fallible(zig::Expr::call(callee, arguments))
780 }
781
782 /// `w.writeU32(x)` — a method on the writer or the reader, marked
783 /// fallible unless the contract's table exempts it.
784 fn runtime(receiver: &str, method: &str, args: Vec<Self::Expr>) -> Self::Expr {
785 let call = zig::Expr::Call {
786 func: Box::new(zig::Expr::dot(
787 zig::Expr::Name(Ident::new(receiver)),
788 &Self::camel(method),
789 )),
790 args: args.into_iter().map(inner).collect(),
791 };
792 if FALLIBLE_EXEMPT_METHODS.contains(&method) {
793 return plain(call);
794 }
795 ZigExpr::Fallible(call)
796 }
797
798 /// A free function of the runtime, qualified — except
799 /// `pid_is_user_member`, which is a method on the header the reader
800 /// handed back.
801 fn runtime_static(method: &str, args: Vec<Self::Expr>) -> Self::Expr {
802 if method == "pid_is_user_member" {
803 let mut arguments = args.into_iter().map(inner);
804 if let Some(header) = arguments.next() {
805 return plain(zig::Expr::Call {
806 func: Box::new(zig::Expr::dot(header, "isUserMember")),
807 args: Vec::new(),
808 });
809 }
810 // The walk always hands the header; a call without one is a
811 // walk that changed under this file, and a function no
812 // scope declares is the loudest thing Zig can say here.
813 return plain(zig::Expr::Call {
814 func: Box::new(Self::runtime_name("isUserMember")),
815 args: Vec::new(),
816 });
817 }
818 let call = zig::Expr::Call {
819 func: Box::new(Self::runtime_name(&Self::camel(method))),
820 args: args.into_iter().map(inner).collect(),
821 };
822 if FALLIBLE_STATICS.contains(&method) {
823 return ZigExpr::Fallible(call);
824 }
825 plain(call)
826 }
827
828 /// `runtime/zig` carries the three HDDS axes: `beginPackedElement` /
829 /// `finishPackedElement` on the writer and `readerBeginPackedElement`
830 /// / `readerFinishPackedElement` on the reader, the DHEADER pair for
831 /// a sequence element's own length, and `writeWstringUtf8Octets` /
832 /// `writeWstringUtf8OctetsBounded` / `readWstringUtf8Octets`.
833 fn carries_hdds_axes() -> bool {
834 true
835 }
836
837 // -- statements ----------------------------------------------------
838
839 /// `var name: T = value;` — the type written, so a reader of the
840 /// generated file sees the width. Everything starts as `var`;
841 /// [`Zig::routine`]'s assembly pass turns what the body never
842 /// assigns into `const`, because Zig refuses each where the other
843 /// is meant.
844 fn declare(type_: Self::Type, name: &str, value: Option<Self::Expr>) -> Self::Stmt {
845 zig::Stmt::Var {
846 name: Ident::new(&local_name(name)),
847 type_: Some(type_.tree),
848 value: value.map(inner_bare),
849 mutable: true,
850 }
851 }
852
853 /// An expression evaluated for its effect — with the `try` a
854 /// fallible one carries, and an assignment the walk built as its
855 /// own statement.
856 fn perform(value: Self::Expr) -> Self::Stmt {
857 match value {
858 ZigExpr::Plain(zig::Expr::Binary { op: "=", lhs, rhs }) => zig::Stmt::Assign {
859 place: *lhs,
860 value: *rhs,
861 },
862 ZigExpr::Fallible(e) => zig::Stmt::Expr(zig::Expr::try_(e)),
863 other => zig::Stmt::Expr(inner(other)),
864 }
865 }
866
867 /// `return v;`: a Zig decoder answers with the sample it built, and
868 /// the error union wraps it on its own.
869 fn give_back(value: Option<Self::Expr>) -> Self::Stmt {
870 zig::Stmt::Return(Some(match value {
871 Some(value) => inner(value),
872 None => zig::Expr::Name(Ident::new("v")),
873 }))
874 }
875
876 fn when(
877 condition: Self::Expr,
878 then: Self::Block,
879 otherwise: Option<Self::Block>,
880 ) -> Self::Stmt {
881 zig::Stmt::If {
882 condition: inner(condition),
883 then,
884 otherwise: otherwise.filter(|body| !body.is_empty()),
885 }
886 }
887
888 /// The counter block the renderer scopes: every bound the walk
889 /// hands over is a `u32` count off the wire or a literal.
890 fn count_up(counter: &str, bound: Self::Expr, body: Self::Block) -> Self::Stmt {
891 zig::Stmt::CountUp {
892 counter: Ident::new(&local_name(counter)),
893 type_: Box::new(zig::Type::Primitive(zig::Primitive::U32)),
894 bound: Box::new(inner(bound)),
895 body,
896 }
897 }
898
899 fn until(condition: Self::Expr, body: Self::Block) -> Self::Stmt {
900 zig::Stmt::While {
901 condition: inner(condition),
902 body,
903 }
904 }
905
906 fn leave() -> Self::Stmt {
907 zig::Stmt::Break
908 }
909
910 fn note(doc: Doc) -> Self::Stmt {
911 zig::Stmt::Comment(doc)
912 }
913
914 /// `_ = try …;` — Zig's spelling of a value read for its effect.
915 fn drop_value(value: Self::Expr) -> Self::Stmt {
916 zig::Stmt::DiscardExpr(inner_bare(value))
917 }
918
919 /// `_ = name;` — kept only where [`Zig::routine`]'s assembly pass
920 /// finds no other mention of the name: Zig refuses both an unused
921 /// parameter and a pointless discard, and which of the two a body
922 /// earns is the whole body's question.
923 fn mark_used(name: &str) -> Vec<Self::Stmt> {
924 vec![zig::Stmt::Discard(Ident::new(name))]
925 }
926
927 /// `place = try zd_dec_T(r, behaviour);` — and through
928 /// `zdwire.zdPtr` where the place is an `@external` member's field:
929 /// the decoder answers a `T` and the field holds a pointer. An
930 /// `@optional` member's `?T` takes the bare `T` by coercion.
931 fn read_call(
932 callee: &str,
933 place: Self::Expr,
934 _form: Handoff,
935 behaviour: Self::Expr,
936 ) -> Self::Stmt {
937 let decoded = zig::Expr::call(
938 callee,
939 vec![zig::Expr::Name(Ident::new("r")), inner(behaviour)],
940 );
941 match place {
942 ZigExpr::ExternalSlot(field) => zig::Stmt::Assign {
943 place: field,
944 value: zig::Expr::try_(zig::Expr::Call {
945 func: Box::new(Self::runtime_name("zdPtr")),
946 args: vec![Self::reader_heap(), zig::Expr::try_(decoded)],
947 }),
948 },
949 ZigExpr::OptionalSlot(field) | ZigExpr::Plain(field) | ZigExpr::Fallible(field) => {
950 zig::Stmt::Assign {
951 place: field,
952 value: zig::Expr::try_(decoded),
953 }
954 }
955 }
956 }
957
958 /// `v = try zd_dec_T(r, zd_tc);` — a `typedef`, handed straight on.
959 ///
960 /// The trait's default is the C shape, `zd_dec_T(r, v, zd_tc)` with
961 /// the value as an out-parameter, and a Zig decoder takes two
962 /// arguments and answers the value: `error: expected 2 argument(s),
963 /// found 3` on `resolve-typedef-chain-three` and
964 /// `sequence-of-typedef`. Every column whose decoders answer a value
965 /// rather than fill an out-parameter — C#, D, Go, Java, Python,
966 /// TypeScript — overrides it the same way; C and C++ keep the
967 /// default.
968 fn read_forward(callee: &str) -> Self::Stmt {
969 Self::read_call(
970 callee,
971 Self::whole("v"),
972 Handoff::Value,
973 Self::name("zd_tc"),
974 )
975 }
976
977 fn block(statements: Vec<Self::Stmt>) -> Self::Block {
978 statements
979 }
980
981 fn into_statements(block: Self::Block) -> Vec<Self::Stmt> {
982 block
983 }
984
985 /// `v = try r.heap.alloc(T, n);`, and the fill after it: the slots
986 /// are storage the moment the allocator answers, and the fill
987 /// assigns every one of them before anything reads it.
988 fn make_room(
989 base: &str,
990 element: Self::Type,
991 count: Self::Expr,
992 _assigned_whole: bool,
993 fill: Vec<Self::Stmt>,
994 ) -> Vec<Self::Stmt> {
995 let mut out = vec![zig::Stmt::Assign {
996 place: zig::Expr::Name(Ident::new(base)),
997 value: zig::Expr::try_(zig::Expr::Call {
998 func: Box::new(zig::Expr::dot(Self::reader_heap(), "alloc")),
999 args: vec![zig::Expr::type_(element.tree), inner(count)],
1000 }),
1001 }];
1002 out.extend(fill);
1003 out
1004 }
1005
1006 /// `v = v[0..count];` — §7.3.1.2.1.12.3's TRIM, applied after every
1007 /// element has been read: a Zig slice shortens by re-slicing.
1008 fn keep_first(base: &str, count: Self::Expr) -> Vec<Self::Stmt> {
1009 vec![zig::Stmt::Assign {
1010 place: zig::Expr::Name(Ident::new(base)),
1011 value: zig::Expr::Index {
1012 value: Box::new(zig::Expr::Name(Ident::new(base))),
1013 index: Box::new(zig::Expr::Binary {
1014 op: "..",
1015 lhs: Box::new(zig::Expr::int(0)),
1016 rhs: Box::new(inner(count)),
1017 }),
1018 },
1019 }]
1020 }
1021
1022 /// The subscript, with nothing around it: a Zig slice element is a
1023 /// real place at every element type.
1024 fn sequence_element(
1025 base: &str,
1026 index: Self::Expr,
1027 _element: Self::Type,
1028 ) -> (Vec<Self::Stmt>, Self::Expr, Vec<Self::Stmt>) {
1029 (
1030 Vec::new(),
1031 Self::index(Self::elements(base), index),
1032 Vec::new(),
1033 )
1034 }
1035
1036 // -- assembly ------------------------------------------------------
1037
1038 /// `fn zd_enc_T(w: *zdwire.Writer, v: T) zdwire.WireError!void` and
1039 /// `fn zd_dec_T(r: *zdwire.Reader, zd_tc: zdwire.TryConstruct)
1040 /// zdwire.WireError!T`.
1041 ///
1042 /// A forward declaration is answered with nothing: a Zig
1043 /// container-level name is in scope for the whole file however far
1044 /// down it is written, so a recursive type needs no declaration
1045 /// ahead of its body.
1046 ///
1047 /// This is also where Zig's unused-and-mutability rules are
1048 /// settled, because both are properties of the whole body: every
1049 /// local the body never assigns becomes `const`, a parameter the
1050 /// body never mentions gets a `_ = name;`, and a discard the walk
1051 /// asked for is kept only when the name has no other mention —
1052 /// Zig refuses a pointless discard as loudly as an unused
1053 /// parameter.
1054 fn routine(spec: Routine<Self>) -> Option<Self::Decl> {
1055 let body = spec.body?;
1056 let mut params = vec![zig::FnParam {
1057 name: Ident::new(if spec.writing { "w" } else { "r" }),
1058 type_: zig::Type::pointer(zig::Type::named(if spec.writing {
1059 "zdwire.Writer"
1060 } else {
1061 "zdwire.Reader"
1062 })),
1063 }];
1064 if spec.writing {
1065 params.push(zig::FnParam {
1066 name: Ident::new("v"),
1067 type_: spec.sample.tree.clone(),
1068 });
1069 }
1070 for (type_, name) in spec.extra {
1071 params.push(zig::FnParam {
1072 name: Ident::new(&name),
1073 type_: type_.tree,
1074 });
1075 }
1076 let error = zig::Type::named("zdwire.WireError");
1077 let (returns, mut body) = if spec.writing {
1078 (
1079 zig::Type::error_union(error, zig::Type::Primitive(zig::Primitive::Void)),
1080 body,
1081 )
1082 } else {
1083 // The sample starts as Table 9's value, through the
1084 // generated defaulter, so every position the stream does
1085 // not carry is already what the clause says it is.
1086 let defaulter = crate::default_zig::defaulter_from_decoder(&spec.name);
1087 let mut statements = vec![zig::Stmt::Var {
1088 name: Ident::new("v"),
1089 type_: Some(spec.sample.tree.clone()),
1090 value: Some(zig::Expr::try_(zig::Expr::call(
1091 &defaulter,
1092 vec![Self::reader_heap()],
1093 ))),
1094 mutable: true,
1095 }];
1096 statements.extend(body);
1097 statements.push(zig::Stmt::Return(Some(zig::Expr::Name(Ident::new("v")))));
1098 (zig::Type::error_union(error, spec.sample.tree), statements)
1099 };
1100 let assigned = assigned_roots(&body);
1101 fix_mutability(&mut body, &assigned);
1102 settle_discards(&mut body, ¶ms);
1103 Some(zig::Decl::Func(zig::FuncDecl {
1104 doc: spec.doc,
1105 // `pub`, because the emission is a module and a codec that
1106 // cannot be called from the file that imports it is not one:
1107 // a Zig declaration without it is private to its file (Zig
1108 // language reference, "Keyword pub"), so
1109 // `@import("gen.zig").zd_enc_Max` was an error. Measured:
1110 // `zig_endpoint_sync`/`_async` in `crates/endpoint-e2e`
1111 // build the generated module beside an app that calls
1112 // `gen.zd_enc_Ping`, and Zig answers `'zd_enc_Ping' is not
1113 // marked 'pub'`. Uniform over every routine rather than only
1114 // the top-level ones: `pub` at container level says nothing
1115 // about the file itself, and a helper the walk hoisted is
1116 // reachable from the same import.
1117 is_pub: true,
1118 name: Ident::new(&spec.name),
1119 params,
1120 returns,
1121 body,
1122 }))
1123 }
1124
1125 /// A call to the generated defaulter, which is Table 9 once per
1126 /// type rather than once per position. Reached only inside a
1127 /// decoder, where `r.heap` is the arena a defaulted `@external`
1128 /// pointer allocates from.
1129 ///
1130 /// # Errors
1131 /// Never; the signature matches the emitters around it.
1132 fn default_into(
1133 ast: &Ast,
1134 id: Id<TypeNode>,
1135 place: Self::Expr,
1136 ) -> Result<Self::Stmt, EncodeError> {
1137 let defaulted = zig::Expr::call(
1138 &crate::default_zig::defaulter_name(ast, id),
1139 vec![Self::reader_heap()],
1140 );
1141 // The slot markers route exactly as [`Zig::read_call`]'s do.
1142 Ok(match place {
1143 ZigExpr::ExternalSlot(field) => zig::Stmt::Assign {
1144 place: field,
1145 value: zig::Expr::try_(zig::Expr::Call {
1146 func: Box::new(Self::runtime_name("zdPtr")),
1147 args: vec![Self::reader_heap(), zig::Expr::try_(defaulted)],
1148 }),
1149 },
1150 ZigExpr::OptionalSlot(field) | ZigExpr::Plain(field) | ZigExpr::Fallible(field) => {
1151 zig::Stmt::Assign {
1152 place: field,
1153 value: zig::Expr::try_(defaulted),
1154 }
1155 }
1156 })
1157 }
1158
1159 /// # Errors
1160 /// As [`crate::default_zig::member_default`]: a `@default` this
1161 /// column cannot spell is refused rather than silently replaced by
1162 /// Table 9's zero.
1163 fn member_default(
1164 ast: &Ast,
1165 node: &MemberNode,
1166 type_: Id<TypeNode>,
1167 place: Self::Expr,
1168 ) -> Result<Vec<Self::Stmt>, EncodeError> {
1169 crate::default_zig::member_default(ast, node, type_, inner(place), Self::reader_heap())
1170 }
1171
1172 /// `Mode.IDLE` — the enum's own scope, which is where the
1173 /// materialiser declared the variant. The trait's default answers a
1174 /// bare prefixed name, which in Zig names nothing.
1175 fn enumerator_constant(ast: &Ast, id: Id<TypeNode>, enumerator: &str) -> Self::Expr {
1176 plain(zig::Expr::dot(
1177 zig::Expr::Name(Ident::new(&zig_type_name(ast, id))),
1178 &field_name(enumerator),
1179 ))
1180 }
1181}
1182
1183// -- the assembly pass ---------------------------------------------------
1184
1185/// Every base name the body assigns through — `v` for `v.x = …` and
1186/// `v[i] = …` alike — which is what decides `var` against `const`.
1187fn assigned_roots(body: &[zig::Stmt]) -> Vec<String> {
1188 let mut out = Vec::new();
1189 collect_assigned(body, &mut out);
1190 out
1191}
1192
1193/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
1194/// The tree walked here is the one this generator emitted and owns, so
1195/// it holds no cycle; the depth is that body's height, and a defaulter
1196/// body is a handful of statements deep.
1197fn collect_assigned(body: &[zig::Stmt], out: &mut Vec<String>) {
1198 for statement in body {
1199 match statement {
1200 zig::Stmt::Assign { place, .. } => {
1201 if let Some(root) = root_name(place) {
1202 if !out.contains(&root) {
1203 out.push(root);
1204 }
1205 }
1206 }
1207 zig::Stmt::If {
1208 then, otherwise, ..
1209 } => {
1210 collect_assigned(then, out);
1211 if let Some(otherwise) = otherwise {
1212 collect_assigned(otherwise, out);
1213 }
1214 }
1215 zig::Stmt::CountUp { body, .. } | zig::Stmt::While { body, .. } => {
1216 collect_assigned(body, out);
1217 }
1218 _ => {}
1219 }
1220 }
1221}
1222
1223/// The base a place is reached from: `v` for `v.x[i].*`.
1224///
1225/// zerodds-lint: recursion-depth 64 (place-expression nesting the walk
1226/// builds). The tree walked here is the one this generator emitted and
1227/// owns, so it holds no cycle; the depth is that expression's height.
1228fn root_name(place: &zig::Expr) -> Option<String> {
1229 match place {
1230 zig::Expr::Name(name) => Some(name.as_str().to_string()),
1231 zig::Expr::Dot { value, .. }
1232 | zig::Expr::Index { value, .. }
1233 | zig::Expr::Paren(value)
1234 | zig::Expr::Try(value) => root_name(value),
1235 _ => None,
1236 }
1237}
1238
1239/// Turn every `var` the body never assigns into a `const`: Zig refuses
1240/// a `var` that is never mutated by name.
1241///
1242/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
1243/// The tree walked here is the one this generator emitted and owns, so
1244/// it holds no cycle; the depth is that body's height, and a defaulter
1245/// body is a handful of statements deep.
1246fn fix_mutability(body: &mut [zig::Stmt], assigned: &[String]) {
1247 for statement in body {
1248 match statement {
1249 zig::Stmt::Var {
1250 name,
1251 value,
1252 mutable,
1253 ..
1254 } => {
1255 // A local declared `undefined` is assigned later by
1256 // definition; the scan sees those assignments too, but
1257 // keeping the rule explicit costs nothing.
1258 let is_assigned = assigned.iter().any(|root| root == name.as_str());
1259 *mutable = is_assigned || value.is_none();
1260 }
1261 zig::Stmt::If {
1262 then, otherwise, ..
1263 } => {
1264 fix_mutability(then, assigned);
1265 if let Some(otherwise) = otherwise {
1266 fix_mutability(otherwise, assigned);
1267 }
1268 }
1269 zig::Stmt::CountUp { body, .. } | zig::Stmt::While { body, .. } => {
1270 fix_mutability(body, assigned);
1271 }
1272 _ => {}
1273 }
1274 }
1275}
1276
1277/// Keep exactly the discards a body needs: drop `_ = x;` where `x` has
1278/// any other mention — Zig calls that a pointless discard — and add one
1279/// for every parameter the body never mentions, which Zig refuses as an
1280/// unused parameter.
1281fn settle_discards(body: &mut Vec<zig::Stmt>, params: &[zig::FnParam]) {
1282 let mut mentioned: Vec<String> = Vec::new();
1283 collect_mentions(body, &mut mentioned);
1284 prune_discards(body, &mentioned);
1285 let mut fronts = Vec::new();
1286 for param in params {
1287 let name = param.name.as_str();
1288 if !mentioned.iter().any(|m| m == name) {
1289 fronts.push(zig::Stmt::Discard(Ident::new(name)));
1290 }
1291 }
1292 if !fronts.is_empty() {
1293 fronts.append(body);
1294 *body = fronts;
1295 }
1296}
1297
1298/// Every name the body mentions outside a discard — in an expression,
1299/// as an assignment target, as a loop bound.
1300///
1301/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
1302/// The tree walked here is the one this generator emitted and owns, so
1303/// it holds no cycle; the depth is that body's height, and a defaulter
1304/// body is a handful of statements deep.
1305fn collect_mentions(body: &[zig::Stmt], out: &mut Vec<String>) {
1306 for statement in body {
1307 match statement {
1308 zig::Stmt::Var { value, .. } => {
1309 if let Some(value) = value {
1310 mentions_in_expr(value, out);
1311 }
1312 }
1313 zig::Stmt::Assign { place, value } => {
1314 mentions_in_expr(place, out);
1315 mentions_in_expr(value, out);
1316 }
1317 zig::Stmt::Expr(value)
1318 | zig::Stmt::DiscardExpr(value)
1319 | zig::Stmt::Return(Some(value)) => {
1320 mentions_in_expr(value, out);
1321 }
1322 zig::Stmt::If {
1323 condition,
1324 then,
1325 otherwise,
1326 } => {
1327 mentions_in_expr(condition, out);
1328 collect_mentions(then, out);
1329 if let Some(otherwise) = otherwise {
1330 collect_mentions(otherwise, out);
1331 }
1332 }
1333 zig::Stmt::CountUp { bound, body, .. } => {
1334 mentions_in_expr(bound, out);
1335 collect_mentions(body, out);
1336 }
1337 zig::Stmt::While { condition, body } => {
1338 mentions_in_expr(condition, out);
1339 collect_mentions(body, out);
1340 }
1341 zig::Stmt::Discard(_)
1342 | zig::Stmt::Return(None)
1343 | zig::Stmt::Break
1344 | zig::Stmt::Comment(_) => {}
1345 }
1346 }
1347}
1348
1349/// zerodds-lint: recursion-depth 64 (expression nesting the walk builds).
1350/// The tree walked here is the one this generator emitted and owns, so
1351/// it holds no cycle; the depth is that expression's height, which
1352/// follows the member nesting the type walk already bounded.
1353fn mentions_in_expr(expr: &zig::Expr, out: &mut Vec<String>) {
1354 match expr {
1355 zig::Expr::Name(name) => {
1356 let text = name.as_str().to_string();
1357 if !out.contains(&text) {
1358 out.push(text);
1359 }
1360 }
1361 zig::Expr::Binary { lhs, rhs, .. } => {
1362 mentions_in_expr(lhs, out);
1363 mentions_in_expr(rhs, out);
1364 }
1365 zig::Expr::Paren(value)
1366 | zig::Expr::Unary { value, .. }
1367 | zig::Expr::Try(value)
1368 | zig::Expr::Dot { value, .. } => mentions_in_expr(value, out),
1369 zig::Expr::Index { value, index } => {
1370 mentions_in_expr(value, out);
1371 mentions_in_expr(index, out);
1372 }
1373 zig::Expr::Call { func, args } => {
1374 mentions_in_expr(func, out);
1375 for arg in args {
1376 mentions_in_expr(arg, out);
1377 }
1378 }
1379 zig::Expr::StructLit(fields) => {
1380 for (_, value) in fields {
1381 mentions_in_expr(value, out);
1382 }
1383 }
1384 zig::Expr::ListLit(elements) => {
1385 for element in elements {
1386 mentions_in_expr(element, out);
1387 }
1388 }
1389 zig::Expr::Literal(_) | zig::Expr::Type(_) => {}
1390 }
1391}
1392
1393/// Drop every `_ = x;` whose `x` the body mentions anywhere else.
1394///
1395/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
1396/// The tree walked here is the one this generator emitted and owns, so
1397/// it holds no cycle; the depth is that body's height, and a defaulter
1398/// body is a handful of statements deep.
1399fn prune_discards(body: &mut Vec<zig::Stmt>, mentioned: &[String]) {
1400 body.retain(|statement| match statement {
1401 zig::Stmt::Discard(name) => !mentioned.iter().any(|m| m == name.as_str()),
1402 _ => true,
1403 });
1404 for statement in body {
1405 match statement {
1406 zig::Stmt::If {
1407 then, otherwise, ..
1408 } => {
1409 prune_discards(then, mentioned);
1410 if let Some(otherwise) = otherwise {
1411 prune_discards(otherwise, mentioned);
1412 }
1413 }
1414 zig::Stmt::CountUp { body, .. } | zig::Stmt::While { body, .. } => {
1415 prune_discards(body, mentioned);
1416 }
1417 _ => {}
1418 }
1419 }
1420}
1421
1422/// The bound a collection carries, as the suffix that keeps two of them
1423/// apart.
1424///
1425/// The tag names the routine a hoisted collection's codec is emitted
1426/// under, and `decoder_over` emits one body per name. Without the
1427/// bound, `sequence<long>` and `sequence<long, 4>` are one name and the
1428/// first body wins: measured on
1429/// `@final struct S { sequence<long> b; sequence<long, 4> a; };`, the
1430/// generated Zig held no `zd_over` check at all, so `a` accepted a
1431/// stream of any length; with the members the other way round both
1432/// members got the bounded body, so `b` refused — and truncated with
1433/// `v = v[0 .. 4]` — a length the IDL puts no limit on. `string` beside
1434/// `string<4>` behaved the same way through `trimUtf8`. Either way the
1435/// reader answered with a value the IDL does not describe and said
1436/// nothing, which is why the bound is part of the name.
1437fn bound_tag(ast: &Ast, bound: &zerodds_ast::Bound) -> String {
1438 match bound.expr() {
1439 None => String::new(),
1440 // `_b` and not a bare `_n`: `seq_long_4` would be an array of
1441 // four `long` under the arm above, and the two are different
1442 // wire forms.
1443 Some(expr) => match ast.eval_integral(expr) {
1444 Ok(limit) => format!("_b{limit}"),
1445 // As the array's `_x`: a bound that does not evaluate is
1446 // still not the unbounded form, and the materialiser
1447 // reports the expression that would not evaluate.
1448 Err(_) => "_bx".to_string(),
1449 },
1450 }
1451}
1452
1453/// The name a codec function takes for the type it codes: the IDL kind
1454/// and not the Zig spelling, structurally for a collection and the
1455/// flattened Zig name for a declaration — the same scheme the D and Go
1456/// columns carry, for the reasons their `type_tag`s state.
1457///
1458/// zerodds-lint: recursion-depth 64 (collection nesting; bounded by `MAX_TYPE_DEPTH`)
1459#[must_use]
1460pub fn type_tag(ast: &Ast, id: Id<TypeNode>, depth: usize) -> String {
1461 if depth > MAX_TYPE_DEPTH {
1462 return "unresolved".to_string();
1463 }
1464 let node = match ast.type_(id) {
1465 Ok(node) => node,
1466 Err(_) => return "unresolved".to_string(),
1467 };
1468 match &node.kind {
1469 TypeKind::Primitive(kind) => kind.as_str().replace(' ', "_"),
1470 TypeKind::String {
1471 width: CharWidth::Char,
1472 bound,
1473 } => format!("string{}", bound_tag(ast, bound)),
1474 TypeKind::String {
1475 width: CharWidth::WChar,
1476 bound,
1477 } => format!("wstring{}", bound_tag(ast, bound)),
1478 TypeKind::Fixed { digits, scale } => format!("fixed_{digits}_{scale}"),
1479 TypeKind::Sequence { element, bound } => match element.resolved() {
1480 Some(element) => format!(
1481 "seq{}_{}",
1482 bound_tag(ast, bound),
1483 type_tag(ast, element, depth + 1)
1484 ),
1485 None => format!("seq_unresolved{}", bound_tag(ast, bound)),
1486 },
1487 TypeKind::Array { element, dims } => {
1488 let mut name = match element.resolved() {
1489 Some(element) => format!("arr_{}", type_tag(ast, element, depth + 1)),
1490 None => "arr_unresolved".to_string(),
1491 };
1492 for dim in dims {
1493 match ast.eval_integral(*dim) {
1494 Ok(length) => name.push_str(&format!("_{length}")),
1495 // No number rather than a zero: the name has to
1496 // differ from a `[0]` array's, and the materialiser
1497 // reports the dimension that would not evaluate.
1498 Err(_) => name.push_str("_x"),
1499 }
1500 }
1501 name
1502 }
1503 TypeKind::Map { key, value, bound } => {
1504 let half = |half: Option<Id<TypeNode>>| match half {
1505 Some(target) => type_tag(ast, target, depth + 1),
1506 None => "unresolved".to_string(),
1507 };
1508 format!(
1509 "map{}_{}_{}",
1510 bound_tag(ast, bound),
1511 half(key.resolved()),
1512 half(value.resolved())
1513 )
1514 }
1515 // An alias is followed through: two IDL names for one shape are
1516 // one wire form, and two codecs for it would be two chances to
1517 // differ.
1518 TypeKind::Alias { aliased } => match aliased.resolved() {
1519 Some(target) => type_tag(ast, target, depth + 1),
1520 None => "unresolved".to_string(),
1521 },
1522 TypeKind::ValueBox { boxed } => match boxed.resolved() {
1523 Some(target) => format!("box_{}", type_tag(ast, target, depth + 1)),
1524 None => "box_unresolved".to_string(),
1525 },
1526 TypeKind::Structure { .. }
1527 | TypeKind::Union { .. }
1528 | TypeKind::Enumeration { .. }
1529 | TypeKind::Bitmask { .. }
1530 | TypeKind::Bitset { .. }
1531 | TypeKind::Valuetype { .. }
1532 | TypeKind::Native => zig_type_name(ast, id),
1533 TypeKind::Any => "any".to_string(),
1534 TypeKind::ValueBase => "valuebase".to_string(),
1535 TypeKind::ObjectRef { .. } => "objectref".to_string(),
1536 TypeKind::AnnotationType { .. } => "annotation".to_string(),
1537 }
1538}