zerodds_lang_rules/emit/nim.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The Nim 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 another.
9//!
10//! # The five decisions this column turns on
11//!
12//! **A member holds its wire width, under Nim's fixed-width names.** An
13//! IDL `long` is a Nim `int32` and a `char` a `uint8`, so
14//! [`CodecTarget::code_point`] is the identity and a union's
15//! discriminator compares as a number. Nim's own `int` appears only as
16//! a codec local — a stream position, which is what `len` answers in —
17//! and never as a member.
18//!
19//! **`@optional` is `Option[T]`, `@external` is `ref T`, and the union
20//! marks its selected branch with an `Option` of its own.** The
21//! materialiser (`adapter/nim.rs`) declares all three, and this backend
22//! carries a marked-expression form per shape so the read direction can
23//! route a decoded `T` through `some(..)`, `zdwire.zdRef(..)`, or both
24//! — Nim has no address-of on a call's result any more than Go does,
25//! and an `Option`'s payload is not a place.
26//!
27//! **A `long double` and a `fixed` are decimal text.** Nim has
28//! `float32`, `float64` and no binary128; `runtime/nim` parses the
29//! member's decimal correctly rounded, once, and prints the exact
30//! expansion.
31//!
32//! **A conversion is not a cast.** Nim's `T(v)` range-checks at run
33//! time, so the two places the walk needs the *bits* — a map key's
34//! signed reading of a `uint8` and the union overlay's 64-bit pattern —
35//! go through `cast[int8]` and `zdwire.zdI64`, and everything else
36//! takes the checked conversion.
37//!
38//! **Forward declarations are real here.** Nim resolves a module-scope
39//! name only after its declaration, so [`CodecTarget::routine`] answers
40//! the bodiless form where D and Go answer nothing — the same shape the
41//! C column needs, for the same reason.
42
43use super::{CodecTarget, Constant, Handle, Handoff, Routine, Scalar};
44use crate::adapter::nim::{
45 field_name, module_prefix, nim_bare_name, nim_enumerator_constant, nim_map_entry_name,
46 nim_value_type,
47};
48use crate::encode::EncodeError;
49use zerodds_ast::{Ast, CharWidth, MemberNode, TypeKind, TypeNode};
50use zerodds_ast_core::Id;
51use zerodds_lang_cst::common::{Doc, Ident, Literal};
52use zerodds_lang_cst::nim;
53
54/// The Nim backend.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct Nim;
57
58/// How deep a chain of typedefs and collections is followed.
59const MAX_TYPE_DEPTH: usize = 64;
60
61/// The module the generated file imports the runtime under.
62const RUNTIME: &str = "zdwire";
63
64/// The type name [`CodecTarget::named`] is handed for C's signed
65/// `char`; the Nim member is a `uint8` and the signed reading is a
66/// `cast[int8]` — the conversion `int8(..)` would range-check the very
67/// values the two's-complement reading exists for.
68const SIGNED_CHAR: &str = "signed char";
69
70/// The name a type with no Nim form falls back to.
71///
72/// Deliberately a name no scope declares: the materialiser refused the
73/// type before a codec walk ran, and this makes the impossible fail in
74/// the Nim compiler, loudly, rather than compile into a plausible
75/// member.
76const NO_FORM: &str = "ZdNoNimForm";
77
78/// An expression, with the one property the walk's seams cannot carry:
79/// which wrapper the place it names lives behind.
80///
81/// The `*Slot` forms are destinations: only the destination seams
82/// produce them and only [`Nim::read_call`], [`Nim::store`] and
83/// [`Nim::default_into`] act on them. [`NimExpr::CaseSlot`] is also a
84/// value — the write direction reaches a union branch through the same
85/// accessor — and reads as `.get` there.
86#[derive(Debug, Clone)]
87pub enum NimExpr {
88 /// An ordinary expression.
89 Plain(nim::Expr),
90 /// The `Option[T]` field of an `@optional` member.
91 OptionalSlot(nim::Expr),
92 /// The `ref T` field of an `@external` member.
93 ExternalSlot(nim::Expr),
94 /// The `Option[ref T]` field of an `@optional @external` member.
95 OptionalExternalSlot(nim::Expr),
96 /// The `Option[T]` field of a union branch.
97 CaseSlot(nim::Expr),
98 /// The `Option[ref T]` field of an `@external` union branch.
99 CaseExternalSlot(nim::Expr),
100}
101
102/// The expression as a *value*: the marked forms unwrap.
103fn value_of(expr: NimExpr) -> nim::Expr {
104 match expr {
105 NimExpr::Plain(e) => e,
106 // Only ever reached inside a branch the presence test guarded.
107 NimExpr::OptionalSlot(e) | NimExpr::CaseSlot(e) => nim::Expr::dot(e, "get"),
108 NimExpr::ExternalSlot(e) => nim::Expr::Deref(Box::new(e)),
109 NimExpr::OptionalExternalSlot(e) | NimExpr::CaseExternalSlot(e) => {
110 nim::Expr::Deref(Box::new(nim::Expr::dot(e, "get")))
111 }
112 }
113}
114
115fn plain(e: nim::Expr) -> NimExpr {
116 NimExpr::Plain(e)
117}
118
119impl Nim {
120 /// A runtime name, qualified: `zdwire.x`.
121 fn runtime_name(name: &str) -> nim::Expr {
122 nim::Expr::dot(nim::Expr::name(RUNTIME), name)
123 }
124
125 /// A runtime operation's Nim spelling: `write_u32` is `writeU32` —
126 /// the first segment as it stands, every later one with its first
127 /// letter raised. Nim compares the rest of an identifier without
128 /// case anyway; the camel form is the one `runtime/nim` publishes.
129 fn camel(method: &str) -> String {
130 let mut out = String::new();
131 for (index, segment) in method.split('_').enumerate() {
132 if index == 0 {
133 out.push_str(segment);
134 continue;
135 }
136 let mut chars = segment.chars();
137 if let Some(first) = chars.next() {
138 out.extend(first.to_uppercase());
139 out.push_str(chars.as_str());
140 }
141 }
142 out
143 }
144
145 fn text(value: &str) -> NimExpr {
146 plain(nim::Expr::Literal(Literal::Str(value.to_string())))
147 }
148
149 fn runtime_constant(name: &str) -> NimExpr {
150 plain(Self::runtime_name(name))
151 }
152
153 /// `some(value)`, `zdwire.zdRef(value)`, or the two nested — how a
154 /// decoded or defaulted value enters the field a marked place
155 /// names.
156 fn into_slot(place: NimExpr, value: nim::Expr) -> nim::Stmt {
157 let wrap_some = |inner: nim::Expr| nim::Expr::call(nim::Expr::name("some"), vec![inner]);
158 let wrap_ref = |inner: nim::Expr| nim::Expr::call(Self::runtime_name("zdRef"), vec![inner]);
159 match place {
160 NimExpr::Plain(field) => nim::Stmt::Assign {
161 place: field,
162 value,
163 },
164 NimExpr::OptionalSlot(field) | NimExpr::CaseSlot(field) => nim::Stmt::Assign {
165 place: field,
166 value: wrap_some(value),
167 },
168 NimExpr::ExternalSlot(field) => nim::Stmt::Assign {
169 place: field,
170 value: wrap_ref(value),
171 },
172 NimExpr::OptionalExternalSlot(field) | NimExpr::CaseExternalSlot(field) => {
173 nim::Stmt::Assign {
174 place: field,
175 value: wrap_some(wrap_ref(value)),
176 }
177 }
178 }
179 }
180
181 /// The Nim spelling of an operator the walk hands over in C's.
182 ///
183 /// One table, so the logical and the bitwise forms cannot drift:
184 /// Nim spells both `and`/`or`, overloaded by type.
185 fn op(op: &'static str) -> &'static str {
186 match op {
187 "&&" | "&" => "and",
188 "||" | "|" => "or",
189 "^" => "xor",
190 "<<" => "shl",
191 ">>" => "shr",
192 _ => op,
193 }
194 }
195}
196
197impl CodecTarget for Nim {
198 type Expr = NimExpr;
199 type Stmt = nim::Stmt;
200 type Block = Vec<nim::Stmt>;
201 type Type = nim::Type;
202 type Decl = nim::ProcDecl;
203
204 // -- types --------------------------------------------------------
205
206 /// The fixed-width Nim type each scalar local takes. `Index` is
207 /// `uint32` — a sequence length is a `uint32` on the wire — and
208 /// `Size` is Nim's `int`, which is what `len` and the reader's
209 /// positions answer in. `int` is not in the CST's own `Primitive`
210 /// set — a declared *member* must not change width between targets
211 /// — and a codec local is not a member.
212 fn scalar(kind: Scalar) -> Self::Type {
213 match kind {
214 Scalar::Bool => nim::Type::Primitive(nim::Primitive::Bool),
215 Scalar::Char | Scalar::UInt8 => nim::Type::Primitive(nim::Primitive::UInt8),
216 Scalar::Int8 => nim::Type::Primitive(nim::Primitive::Int8),
217 Scalar::Int16 => nim::Type::Primitive(nim::Primitive::Int16),
218 Scalar::UInt16 => nim::Type::Primitive(nim::Primitive::UInt16),
219 Scalar::Int32 => nim::Type::Primitive(nim::Primitive::Int32),
220 Scalar::UInt32 | Scalar::Index => nim::Type::Primitive(nim::Primitive::UInt32),
221 Scalar::Int64 => nim::Type::Primitive(nim::Primitive::Int64),
222 Scalar::UInt64 => nim::Type::Primitive(nim::Primitive::UInt64),
223 Scalar::Float => nim::Type::Primitive(nim::Primitive::Float32),
224 Scalar::Double => nim::Type::Primitive(nim::Primitive::Float64),
225 // The decimal text; the module documentation says why.
226 Scalar::LongDouble => nim::Type::Primitive(nim::Primitive::String),
227 Scalar::Size => nim::Type::named("int"),
228 }
229 }
230
231 fn named(name: &str) -> Self::Type {
232 nim::Type::named(name)
233 }
234
235 /// A name no scope declares: never rendered — [`Nim::routine`]
236 /// writes no result for a writer, and the two defaults that cast to
237 /// it are overridden — so reaching it is a walk that changed under
238 /// this file, and the Nim compiler names it.
239 fn nothing() -> Self::Type {
240 nim::Type::named("void")
241 }
242
243 /// The runtime's handles, qualified.
244 fn handle(kind: Handle) -> Self::Type {
245 nim::Type::named(match kind {
246 Handle::DHeader => "zdwire.DHeader",
247 Handle::EmHeader => "zdwire.EmHeader",
248 Handle::Pid => "zdwire.Pid",
249 Handle::TryConstruct => "zdwire.TryConstruct",
250 Handle::ReaderMark => "zdwire.ReaderMark",
251 Handle::PidRead => "zdwire.PidRead",
252 Handle::EmHeaderRead => "zdwire.EmHeaderRead",
253 })
254 }
255
256 /// The member already is the code unit — `uint8` for a `char`,
257 /// `uint16` for a `wchar` — so there is nothing to convert.
258 fn code_point(value: Self::Expr, _narrow: bool) -> Self::Expr {
259 value
260 }
261
262 /// The runtime's policy values: strings and integers where
263 /// `runtime/nim` takes them so, and qualified constants for the
264 /// enumerated kinds. One arm per constant and no catch-all.
265 fn constant(which: Constant) -> Self::Expr {
266 match which {
267 Constant::WcharWidth2 => plain(nim::Expr::int(2)),
268 Constant::WcharWidth4 => plain(nim::Expr::int(4)),
269 Constant::WcharFixed => Self::text("fixed"),
270 Constant::WcharGiopLengthPrefixed => Self::text("giop-length-prefixed"),
271 Constant::WstringUnitsStream => Self::text("stream"),
272 Constant::WstringUnitsBigEndianAlways => Self::text("big-endian-always"),
273 Constant::WstringUnitsByteOrderMark => Self::text("byte-order-mark"),
274 Constant::WstringUnitsHostUnswapped => Self::text("host-unswapped"),
275 Constant::WstringSpecification => Self::text("specification"),
276 Constant::WstringCharacterCount => Self::text("character-count"),
277 Constant::WstringCharacterCountTerminated => Self::text("character-count-terminated"),
278 Constant::WstringSpecificationWideUnits => Self::text("specification-wide-units"),
279 Constant::WstringRosTypeSupport => Self::text("ros-type-support"),
280 Constant::WstringOctetsTerminated2 => Self::text("octets/terminated/2"),
281 Constant::WstringOctetsTerminated4 => Self::text("octets/terminated/4"),
282 Constant::WstringCharactersTerminated2 => Self::text("characters/terminated/2"),
283 Constant::FixedDeclared => Self::text("declared"),
284 Constant::FixedValuePrecision => Self::text("value-precision"),
285 Constant::FixedValueStringDigits => Self::text("value-string-digits"),
286 Constant::ParameterMember => Self::runtime_constant("ParameterMember"),
287 Constant::ParameterOptional => Self::runtime_constant("ParameterOptional"),
288 Constant::TryConstructDiscard => Self::runtime_constant("TryConstructDiscard"),
289 Constant::TryConstructTrim => Self::runtime_constant("TryConstructTrim"),
290 Constant::TryConstructUseDefault => Self::runtime_constant("TryConstructUseDefault"),
291 Constant::StatusOk => Self::runtime_constant("StatusOk"),
292 Constant::StatusDiscarded => Self::runtime_constant("StatusDiscarded"),
293 Constant::StatusImpossibleLength => Self::runtime_constant("StatusImpossibleLength"),
294 Constant::StatusOutOfRange => Self::runtime_constant("StatusOutOfRange"),
295 Constant::StatusTruncated => Self::runtime_constant("StatusTruncated"),
296 }
297 }
298
299 /// The unsigned type wide enough for `bits` of a bit set — the same
300 /// answer the materialiser's `nim_holder` gives its fields.
301 fn unsigned_holder(bits: u16) -> Self::Type {
302 nim::Type::Primitive(crate::adapter::nim::nim_holder(bits))
303 }
304
305 fn value_type(ast: &Ast, id: Id<TypeNode>) -> Self::Type {
306 // The fallback carries its reason where the silent-drop guard
307 // reads it; see [`NO_FORM`].
308 #[allow(clippy::manual_unwrap_or_default, clippy::manual_unwrap_or)]
309 match nim_value_type(ast, id) {
310 Ok(type_) => type_,
311 Err(_) => nim::Type::named(NO_FORM),
312 }
313 }
314
315 fn type_name(ast: &Ast, id: Id<TypeNode>) -> String {
316 type_tag(ast, id, 0)
317 }
318
319 /// The entry name the materialiser declared, and a distinct one
320 /// per type when its tag walk refuses: the slot keeps two refused
321 /// entries from collapsing onto one declaration, which would merge
322 /// two map types under one name.
323 fn map_entry_name(ast: &Ast, id: Id<TypeNode>) -> String {
324 nim_map_entry_name(ast, id).unwrap_or_else(|_| format!("ZdUnresolvedEntry{}", id.slot()))
325 }
326
327 /// The flattened module path with its trailing separator, taken
328 /// from the same function the materialiser puts on an enumerator's
329 /// field — the one consumer of this in the walk is the enumerator
330 /// default this backend overrides anyway.
331 fn module_prefix(ast: &Ast, id: Id<TypeNode>) -> String {
332 module_prefix(ast, id)
333 }
334
335 /// The repair and the strop, exactly as the materialiser spells a
336 /// field.
337 fn escape(name: &str) -> String {
338 field_name(name)
339 }
340
341 /// The base is the first member `parent` (XTypes 1.3 §7.5.1.4.1.3).
342 fn inherited_path(depth: usize) -> Option<String> {
343 Some(format!("{}.", field_name("parent")).repeat(depth))
344 }
345
346 // -- expressions ---------------------------------------------------
347
348 fn name(text: &str) -> Self::Expr {
349 plain(nim::Expr::name(text))
350 }
351
352 fn int(value: i128) -> Self::Expr {
353 plain(nim::Expr::int(value))
354 }
355
356 /// The raw tokens the walk escapes through [`Literal::Int`],
357 /// rewritten into the tree's own boolean where they are one.
358 fn literal(value: Literal) -> Self::Expr {
359 let Literal::Int(text) = &value else {
360 return plain(nim::Expr::Literal(value));
361 };
362 match text.as_str() {
363 "true" => plain(nim::Expr::Literal(Literal::Bool(true))),
364 "false" => plain(nim::Expr::Literal(Literal::Bool(false))),
365 _ => plain(nim::Expr::Literal(value)),
366 }
367 }
368
369 /// A literal wide enough to survive its own width: Nim refuses a
370 /// bare decimal past `int64`, so a union label above it carries
371 /// `'u64` and one past `int32` carries `'i64` — inside `int32` a
372 /// bare literal converts against whatever type it meets.
373 fn wide_int(value: i128) -> Self::Expr {
374 if value > i128::from(i64::MAX) {
375 return plain(nim::Expr::Literal(Literal::Int(format!("{value}'u64"))));
376 }
377 if value > i128::from(i32::MAX) || value < i128::from(i32::MIN) {
378 return plain(nim::Expr::Literal(Literal::Int(format!("{value}'i64"))));
379 }
380 plain(nim::Expr::int(value))
381 }
382
383 /// The number, unsuffixed: `5u` is not a Nim token, and a bare
384 /// literal converts against a `uint32` counter or an `int`
385 /// position. A bound past `int32` gets its width from
386 /// [`CodecTarget::wide_int`].
387 fn unsigned_int(value: i128) -> Self::Expr {
388 Self::wide_int(value)
389 }
390
391 /// `..'u64`, typed: a mask the walk complements has to already be a
392 /// `uint64` — `not` on a bare literal would be an `int`'s
393 /// complement, a different pattern.
394 fn wide_unsigned(value: u64) -> Self::Expr {
395 plain(nim::Expr::Literal(Literal::Int(format!("{value}'u64"))))
396 }
397
398 /// `..'i64`, for the enumerator patterns `zd_raw` is compared
399 /// against.
400 fn wide_signed(value: i128) -> Self::Expr {
401 plain(nim::Expr::Literal(Literal::Int(format!("{value}'i64"))))
402 }
403
404 /// `v.x` — and `v.d` for the discriminator: `_d` is not a Nim
405 /// identifier (the production starts at a letter), so the
406 /// materialiser declares `d` and this backend maps the walk's
407 /// spelling onto it.
408 fn member(base: &str, field: &str) -> Self::Expr {
409 let spelled = if field == "_d" {
410 crate::adapter::nim::DISCRIMINATOR
411 } else {
412 field
413 };
414 plain(nim::Expr::dot(nim::Expr::name(base), spelled))
415 }
416
417 fn whole(base: &str) -> Self::Expr {
418 plain(nim::Expr::name(base))
419 }
420
421 fn field_of(value: Self::Expr, field: &str) -> Self::Expr {
422 plain(nim::Expr::dot(value_of(value), field))
423 }
424
425 /// A header's field, in the spelling `runtime/nim` publishes:
426 /// `member_id` is `memberId` — and `end`, which is a Nim keyword,
427 /// is `endOfList`.
428 fn header_field(value: Self::Expr, field: &str) -> Self::Expr {
429 let spelled = if field == "end" {
430 "endOfList".to_string()
431 } else {
432 Self::camel(field)
433 };
434 plain(nim::Expr::dot(value_of(value), &spelled))
435 }
436
437 /// `a[int(i)]`: every index the walk hands over is a `uint32`
438 /// counter, and Nim's `seq` and `array` subscripts take an `int` —
439 /// the conversion is the crossing written out.
440 fn index(value: Self::Expr, at: Self::Expr) -> Self::Expr {
441 plain(nim::Expr::Index {
442 value: Box::new(value_of(value)),
443 index: Box::new(nim::Expr::Convert {
444 type_: Box::new(nim::Type::named("int")),
445 value: Box::new(value_of(at)),
446 }),
447 })
448 }
449
450 /// `uint32(v.len)`, or the bound where the materialiser fixed one:
451 /// `len` answers an `int` and a sequence's count is a `uint32` on
452 /// the wire.
453 fn count_of(base: &str, fixed: Option<u64>) -> Self::Expr {
454 match fixed {
455 Some(bound) => plain(nim::Expr::int(i128::from(bound))),
456 None => plain(nim::Expr::Convert {
457 type_: Box::new(nim::Type::Primitive(nim::Primitive::UInt32)),
458 value: Box::new(nim::Expr::dot(nim::Expr::name(base), "len")),
459 }),
460 }
461 }
462
463 /// The `seq` itself: a Nim `seq` is the elements.
464 fn elements(base: &str) -> Self::Expr {
465 plain(nim::Expr::name(base))
466 }
467
468 /// `v.x`, marked: a union branch is an `Option[T]` — the write
469 /// direction reads it as `.get` and the read direction assigns
470 /// through `some(..)`, and the marker is what lets one accessor
471 /// serve both.
472 fn case_member(base: &str, field: &str) -> Self::Expr {
473 NimExpr::CaseSlot(value_of(Self::member(base, field)))
474 }
475
476 /// `entry.key` and `entry.value`, the two fields the materialiser
477 /// gives a map's entry object.
478 fn entry_half(entry: Self::Expr, key: bool) -> Self::Expr {
479 plain(nim::Expr::dot(
480 value_of(entry),
481 if key { "key" } else { "value" },
482 ))
483 }
484
485 fn absent() -> Self::Expr {
486 plain(nim::Expr::Literal(Literal::Null))
487 }
488
489 /// Nothing: a Nim `string` and a Nim `seq` own their storage, and
490 /// there is no allocation failure for the guard to test.
491 fn materialised(_value: Self::Expr) -> Option<Self::Expr> {
492 None
493 }
494
495 /// `v.x.get` — the value an `Option` holds, given that it holds
496 /// one. Only ever reached inside a branch the presence test
497 /// guarded, where `get`'s own defect cannot fire.
498 fn optional_value(_ast: &Ast, _id: Id<TypeNode>, base: &str, field: &str) -> Self::Expr {
499 plain(nim::Expr::dot(value_of(Self::member(base, field)), "get"))
500 }
501
502 /// `v.x.isSome`.
503 fn present(base: &str, field: &str) -> Self::Expr {
504 plain(nim::Expr::dot(
505 value_of(Self::member(base, field)),
506 "isSome",
507 ))
508 }
509
510 /// Nothing on the way in — the assignment through `some(..)` is
511 /// what makes the member present — and `reset(v.x)` on the way
512 /// out: `reset` puts a location back to its default, which for an
513 /// `Option` is the absence, and unlike `none(T)` it needs no type
514 /// argument this seam does not carry.
515 fn set_present(base: &str, field: &str, present: bool) -> Vec<Self::Stmt> {
516 if present {
517 return Vec::new();
518 }
519 vec![nim::Stmt::Expr(nim::Expr::call(
520 nim::Expr::name("reset"),
521 vec![value_of(Self::member(base, field))],
522 ))]
523 }
524
525 /// `reset(v.x)` for every case: a branch is an `Option[T]`, and only
526 /// the selected one is `some`. The same statement as an absent
527 /// `@optional` member, [`Self::set_present`] with `false`.
528 fn clear_union_cases(base: &str, fields: &[String]) -> Vec<Self::Stmt> {
529 fields
530 .iter()
531 .flat_map(|field| Self::set_present(base, field, false))
532 .collect()
533 }
534
535 /// The marked place; the room is made by [`Nim::read_call`], which
536 /// routes the value through `some(..)`.
537 fn optional_destination(base: &str, field: &str) -> (Vec<Self::Stmt>, Self::Expr) {
538 (
539 Vec::new(),
540 NimExpr::OptionalSlot(value_of(Self::member(base, field))),
541 )
542 }
543
544 /// `true`: an `@external` member is a `ref`, and a member on a cycle needs
545 /// the same: `Option[T]` stores its `T` inline.
546 /// [`crate::adapter::held_externally`] is the rule the adapter and
547 /// the defaulter ask as well.
548 fn holds_a_cycle_externally() -> bool {
549 true
550 }
551
552 /// `v.x[]` — the value behind the `ref`, or `v.x.get[]` where the
553 /// member is also `@optional` and the `Option` wraps the
554 /// reference: the member's codec takes the value.
555 fn external_value(
556 _ast: &Ast,
557 _id: Id<TypeNode>,
558 base: &str,
559 field: &str,
560 optional: bool,
561 ) -> Self::Expr {
562 let member = value_of(Self::member(base, field));
563 if optional {
564 return plain(nim::Expr::Deref(Box::new(nim::Expr::dot(member, "get"))));
565 }
566 plain(nim::Expr::Deref(Box::new(member)))
567 }
568
569 /// The marked place; [`Nim::read_call`] routes the decoded value
570 /// through `zdwire.zdRef(..)`, inside `some(..)` where the member
571 /// is also `@optional`.
572 fn external_destination(
573 base: &str,
574 field: &str,
575 optional: bool,
576 ) -> (Vec<Self::Stmt>, Self::Expr) {
577 let member = value_of(Self::member(base, field));
578 let slot = if optional {
579 NimExpr::OptionalExternalSlot(member)
580 } else {
581 NimExpr::ExternalSlot(member)
582 };
583 (Vec::new(), slot)
584 }
585
586 /// `v.x.isSome` where the pair is `@optional @external` —
587 /// `Option[ref T]`, so the `Option` answers — and the seam is only
588 /// ever emitted for that pair: a bare `@external` member is always
589 /// present and no guard reads it.
590 fn external_present(base: &str, field: &str) -> Self::Expr {
591 Self::present(base, field)
592 }
593
594 /// As [`Nim::set_present`], for the same reason: the `Option` is
595 /// the outer form of `@optional @external` too.
596 fn external_set_present(base: &str, field: &str, present: bool) -> Vec<Self::Stmt> {
597 Self::set_present(base, field, present)
598 }
599
600 /// `v.x.get[]` for a branch: the union arm's field is
601 /// `Option[ref T]`, and the branch's codec takes the value.
602 fn external_case_value(_ast: &Ast, _id: Id<TypeNode>, base: &str, field: &str) -> Self::Expr {
603 plain(nim::Expr::Deref(Box::new(nim::Expr::dot(
604 value_of(Self::member(base, field)),
605 "get",
606 ))))
607 }
608
609 /// The marked place; [`Nim::read_call`] routes the decoded value
610 /// through `some(zdwire.zdRef(..))`.
611 fn external_case_destination(base: &str, field: &str) -> (Vec<Self::Stmt>, Self::Expr) {
612 (
613 Vec::new(),
614 NimExpr::CaseExternalSlot(value_of(Self::member(base, field))),
615 )
616 }
617
618 /// The operators, respelled: Nim writes `and`, `or`, `xor`, `shl`
619 /// and `shr` where the walk hands C's forms, and the logical and
620 /// bitwise pairs share one keyword, overloaded by type. `=` never
621 /// reaches a rendered expression — [`Nim::perform`] turns it into
622 /// an assignment statement.
623 fn binary(op: &'static str, lhs: Self::Expr, rhs: Self::Expr) -> Self::Expr {
624 plain(nim::Expr::Binary {
625 op: Self::op(op),
626 lhs: Box::new(value_of(lhs)),
627 rhs: Box::new(value_of(rhs)),
628 })
629 }
630
631 /// `not a`, for both of C's spellings — Nim's `not` is the logical
632 /// and the bitwise complement, overloaded by type.
633 fn unary(op: &'static str, value: Self::Expr) -> Self::Expr {
634 if op == "*" {
635 return plain(nim::Expr::Deref(Box::new(value_of(value))));
636 }
637 plain(nim::Expr::Unary {
638 op: match op {
639 "!" | "~" => "not",
640 other => other,
641 },
642 value: Box::new(value_of(value)),
643 })
644 }
645
646 fn paren(value: Self::Expr) -> Self::Expr {
647 plain(nim::Expr::Paren(Box::new(value_of(value))))
648 }
649
650 /// `T(v)` — Nim's checked conversion — with three rewrites.
651 ///
652 /// A `bool` discriminator's integer label becomes the boolean
653 /// literal, because `bool(1)` is not the literal the comparison
654 /// wants written. The walk's `signed char` — the C spelling of a
655 /// map key's signed order — is `cast[int8]`, which reads a `uint8`
656 /// key as the two's complement the corpus was recorded under where
657 /// `int8(200'u8)` would raise. And the walk's `Int64` widening —
658 /// an enumeration's holder, the union overlay's pattern — goes
659 /// through `zdwire.zdI64`, which widens an enum or a signed value
660 /// by conversion and a `uint64` by `cast`, because `int64(x)` on a
661 /// `uint64` above `int64.high` raises where the overlay needs the
662 /// bits.
663 fn cast(type_: Self::Type, value: Self::Expr) -> Self::Expr {
664 if type_ == nim::Type::named(SIGNED_CHAR) {
665 return plain(nim::Expr::Cast {
666 type_: Box::new(nim::Type::Primitive(nim::Primitive::Int8)),
667 value: Box::new(value_of(value)),
668 });
669 }
670 if type_ == nim::Type::Primitive(nim::Primitive::Bool) {
671 if let NimExpr::Plain(nim::Expr::Literal(Literal::Int(text))) = &value {
672 return plain(nim::Expr::Literal(Literal::Bool(text != "0")));
673 }
674 }
675 if type_ == nim::Type::Primitive(nim::Primitive::Int64) {
676 return plain(nim::Expr::call(
677 Self::runtime_name("zdI64"),
678 vec![value_of(value)],
679 ));
680 }
681 plain(nim::Expr::Convert {
682 type_: Box::new(type_),
683 value: Box::new(value_of(value)),
684 })
685 }
686
687 /// The value, unchanged: Nim hands a `seq` by its handle, an
688 /// `array` by value and an object by value.
689 fn handoff(value: Self::Expr, _form: Handoff) -> Self::Expr {
690 value
691 }
692
693 /// A call to a module-level routine — a generated codec or
694 /// defaulter. `strcmp` is the runtime's, which spells C11 §7.24.4's
695 /// unsigned-octet order in a language whose own string order over
696 /// UTF-8 is exactly that.
697 fn call(callee: &str, args: Vec<Self::Expr>) -> Self::Expr {
698 let arguments: Vec<nim::Expr> = args.into_iter().map(value_of).collect();
699 if callee == "strcmp" {
700 return plain(nim::Expr::call(Self::runtime_name("strcmp"), arguments));
701 }
702 plain(nim::Expr::call(nim::Expr::name(callee), arguments))
703 }
704
705 /// `w.writeU32(x)` — a method on the writer or the reader. One
706 /// operation is renamed rather than converted: the walk's
707 /// `read_pid()` opens a value block §7.4.1.2 counts alignment
708 /// from, which is `readPidAs` in this runtime; the Go and D
709 /// columns do the same.
710 fn runtime(receiver: &str, method: &str, args: Vec<Self::Expr>) -> Self::Expr {
711 if method == "read_pid" && args.is_empty() {
712 return Self::runtime(
713 receiver,
714 "read_pid_as",
715 vec![Self::constant(Constant::ParameterMember)],
716 );
717 }
718 plain(nim::Expr::call(
719 nim::Expr::dot(nim::Expr::name(receiver), &Self::camel(method)),
720 args.into_iter().map(value_of).collect(),
721 ))
722 }
723
724 /// A free routine of the runtime, qualified — except
725 /// `pid_is_user_member`, which is a method on the header the
726 /// reader handed back.
727 fn runtime_static(method: &str, args: Vec<Self::Expr>) -> Self::Expr {
728 if method == "pid_is_user_member" {
729 let mut arguments = args.into_iter().map(value_of);
730 if let Some(header) = arguments.next() {
731 return plain(nim::Expr::call(
732 nim::Expr::dot(header, "isUserMember"),
733 Vec::new(),
734 ));
735 }
736 // The walk always hands the header; a call without one is a
737 // walk that changed under this file, and a routine on a name
738 // no scope declares is the loudest thing Nim can say here.
739 return plain(nim::Expr::call(
740 Self::runtime_name("isUserMember"),
741 Vec::new(),
742 ));
743 }
744 plain(nim::Expr::call(
745 Self::runtime_name(&Self::camel(method)),
746 args.into_iter().map(value_of).collect(),
747 ))
748 }
749
750 /// `runtime/nim` carries the three HDDS axes: `beginPackedElement` /
751 /// `finishPackedElement` on the writer and `readerBeginPackedElement`
752 /// / `readerFinishPackedElement` on the reader, the DHEADER pair for
753 /// a sequence element's own length, and `writeWstringUtf8Octets` /
754 /// `writeWstringUtf8OctetsBounded` / `readWstringUtf8Octets`.
755 fn carries_hdds_axes() -> bool {
756 true
757 }
758
759 // -- statements ----------------------------------------------------
760
761 /// `var name: T = value` — the type written, so a reader of the
762 /// generated file sees the width, and definite whether or not the
763 /// value is there.
764 fn declare(type_: Self::Type, name: &str, value: Option<Self::Expr>) -> Self::Stmt {
765 nim::Stmt::Var {
766 name: Ident::new(name),
767 type_: Some(type_),
768 value: value.map(value_of),
769 }
770 }
771
772 /// An expression evaluated for its effect — or, where the walk
773 /// built an assignment, the assignment as its own statement. Every
774 /// call the walk drops here answers nothing, which is what lets
775 /// the statement stand without a `discard`.
776 fn perform(value: Self::Expr) -> Self::Stmt {
777 if let NimExpr::Plain(nim::Expr::Binary { op: "=", lhs, rhs }) = value {
778 return nim::Stmt::Assign {
779 place: *lhs,
780 value: *rhs,
781 };
782 }
783 nim::Stmt::Expr(value_of(value))
784 }
785
786 /// `return v`: a Nim decoder answers with the sample it built.
787 fn give_back(value: Option<Self::Expr>) -> Self::Stmt {
788 nim::Stmt::Return(Some(match value {
789 Some(value) => value_of(value),
790 None => nim::Expr::name("v"),
791 }))
792 }
793
794 fn when(
795 condition: Self::Expr,
796 then: Self::Block,
797 otherwise: Option<Self::Block>,
798 ) -> Self::Stmt {
799 nim::Stmt::If {
800 condition: value_of(condition),
801 then,
802 otherwise: otherwise.filter(|body| !body.is_empty()),
803 }
804 }
805
806 /// `for i in 0'u32 ..< bound`: every bound the walk hands over is
807 /// a `uint32` count off the wire or an integer literal Nim
808 /// converts against the range.
809 fn count_up(counter: &str, bound: Self::Expr, body: Self::Block) -> Self::Stmt {
810 nim::Stmt::For {
811 counter: Ident::new(counter),
812 bound: Box::new(value_of(bound)),
813 body,
814 }
815 }
816
817 fn until(condition: Self::Expr, body: Self::Block) -> Self::Stmt {
818 nim::Stmt::While {
819 condition: value_of(condition),
820 body,
821 }
822 }
823
824 fn leave() -> Self::Stmt {
825 nim::Stmt::Break
826 }
827
828 fn note(doc: Doc) -> Self::Stmt {
829 nim::Stmt::Comment(doc)
830 }
831
832 /// `discard expr` — Nim's own statement for a value read for its
833 /// effect; an expression statement with a value does not compile.
834 fn drop_value(value: Self::Expr) -> Self::Stmt {
835 nim::Stmt::Discard(value_of(value))
836 }
837
838 /// `discard name` — a parameter deliberately not read, valid
839 /// whether or not the body goes on to use it.
840 fn mark_used(name: &str) -> Vec<Self::Stmt> {
841 vec![nim::Stmt::Discard(nim::Expr::name(name))]
842 }
843
844 /// `place = zd_dec_T(r, behaviour)` — and through `some(..)`,
845 /// `zdwire.zdRef(..)` or both where the place is a marked slot:
846 /// the decoder answers a `T` and the field holds a wrapper, a
847 /// reference, or a wrapper over one.
848 fn read_call(
849 callee: &str,
850 place: Self::Expr,
851 _form: Handoff,
852 behaviour: Self::Expr,
853 ) -> Self::Stmt {
854 let decoded = nim::Expr::call(
855 nim::Expr::name(callee),
856 vec![nim::Expr::name("r"), value_of(behaviour)],
857 );
858 Self::into_slot(place, decoded)
859 }
860
861 /// `v = zd_dec_T(r, zd_tc)` — a `typedef`, handed straight on.
862 fn read_forward(callee: &str) -> Self::Stmt {
863 Self::read_call(
864 callee,
865 Self::whole("v"),
866 Handoff::Value,
867 Self::name("zd_tc"),
868 )
869 }
870
871 /// One scalar into a place, through the slot markers exactly as
872 /// [`Nim::read_call`] routes them — the wchar element form is the
873 /// one scalar read that reaches a place directly.
874 fn store(place: Self::Expr, _type_: Self::Type, value: Self::Expr) -> Self::Stmt {
875 Self::into_slot(place, value_of(value))
876 }
877
878 fn block(statements: Vec<Self::Stmt>) -> Self::Block {
879 statements
880 }
881
882 fn into_statements(block: Self::Block) -> Vec<Self::Stmt> {
883 block
884 }
885
886 /// `v = newSeq[T](int(n))`, and the fill after it: a Nim `seq`'s
887 /// slots are storage the moment `newSeq` returns, for a sequence's
888 /// elements and a map entry's halves alike, so `assigned_whole`
889 /// makes no difference here.
890 fn make_room(
891 base: &str,
892 element: Self::Type,
893 count: Self::Expr,
894 _assigned_whole: bool,
895 fill: Vec<Self::Stmt>,
896 ) -> Vec<Self::Stmt> {
897 let mut out = vec![nim::Stmt::Assign {
898 place: nim::Expr::name(base),
899 value: nim::Expr::NewSeq {
900 element: Box::new(element),
901 length: Box::new(nim::Expr::Convert {
902 type_: Box::new(nim::Type::named("int")),
903 value: Box::new(value_of(count)),
904 }),
905 },
906 }];
907 out.extend(fill);
908 out
909 }
910
911 /// `v.setLen(int(count))` — §7.3.1.2.1.12.3's TRIM, applied after
912 /// every element has been read.
913 fn keep_first(base: &str, count: Self::Expr) -> Vec<Self::Stmt> {
914 vec![nim::Stmt::Expr(nim::Expr::call(
915 nim::Expr::dot(nim::Expr::name(base), "setLen"),
916 vec![nim::Expr::Convert {
917 type_: Box::new(nim::Type::named("int")),
918 value: Box::new(value_of(count)),
919 }],
920 ))]
921 }
922
923 /// The subscript, with nothing around it: a Nim `seq` element is a
924 /// real place at every element type.
925 fn sequence_element(
926 base: &str,
927 index: Self::Expr,
928 _element: Self::Type,
929 ) -> (Vec<Self::Stmt>, Self::Expr, Vec<Self::Stmt>) {
930 (
931 Vec::new(),
932 Self::index(Self::elements(base), index),
933 Vec::new(),
934 )
935 }
936
937 // -- assembly ------------------------------------------------------
938
939 /// `proc zd_enc_T(w: zdwire.Writer, v: T)` and
940 /// `proc zd_dec_T(r: zdwire.Reader, zd_tc: zdwire.TryConstruct): T`.
941 ///
942 /// A forward declaration is answered with the bodiless form: Nim
943 /// resolves a module-scope name only after its declaration, so a
944 /// recursive or mutually recursive pair needs the signatures ahead
945 /// of the bodies — the same shape the C column emits, and unlike D
946 /// and Go, where any order compiles.
947 fn routine(spec: Routine<Self>) -> Option<Self::Decl> {
948 let mut params = vec![nim::Param {
949 name: Ident::new(if spec.writing { "w" } else { "r" }),
950 type_: nim::Type::named(if spec.writing {
951 "zdwire.Writer"
952 } else {
953 "zdwire.Reader"
954 }),
955 }];
956 if spec.writing {
957 params.push(nim::Param {
958 name: Ident::new("v"),
959 type_: spec.sample.clone(),
960 });
961 }
962 for (type_, name) in spec.extra {
963 params.push(nim::Param {
964 name: Ident::new(&name),
965 type_,
966 });
967 }
968 let (returns, body) = if spec.writing {
969 (None, spec.body)
970 } else {
971 let body = spec.body.map(|body| {
972 // The sample starts as Table 9's value, through the
973 // generated defaulter, so every position the stream
974 // does not carry is already what the clause says it
975 // is.
976 let defaulter = crate::default_nim::defaulter_from_decoder(&spec.name);
977 let mut statements = vec![nim::Stmt::Var {
978 name: Ident::new("v"),
979 type_: None,
980 value: Some(nim::Expr::call(nim::Expr::name(&defaulter), Vec::new())),
981 }];
982 statements.extend(body);
983 statements.push(nim::Stmt::Return(Some(nim::Expr::name("v"))));
984 statements
985 });
986 (Some(spec.sample), body)
987 };
988 Some(nim::ProcDecl {
989 doc: spec.doc,
990 name: Ident::new(&spec.name),
991 // Nim's export marker. Without it the codec is visible only
992 // inside the generated file — `import gen` would not bring
993 // `zd_enc_Max` with it — and the probe harness never saw it
994 // because it writes its `main` into that same file.
995 exported: true,
996 params,
997 returns,
998 body,
999 })
1000 }
1001
1002 /// A call to the generated defaulter, which is Table 9 once per
1003 /// type rather than once per position — routed through the slot
1004 /// markers exactly as [`Nim::read_call`]'s value is.
1005 ///
1006 /// # Errors
1007 /// Never; the signature matches the emitters around it.
1008 fn default_into(
1009 ast: &Ast,
1010 id: Id<TypeNode>,
1011 place: Self::Expr,
1012 ) -> Result<Self::Stmt, EncodeError> {
1013 let defaulted = nim::Expr::call(
1014 nim::Expr::name(&crate::default_nim::defaulter_name(ast, id)),
1015 Vec::new(),
1016 );
1017 Ok(Self::into_slot(place, defaulted))
1018 }
1019
1020 /// # Errors
1021 /// As [`crate::default_nim::member_default`]: a `@default` this
1022 /// column cannot spell is refused rather than silently replaced by
1023 /// Table 9's zero.
1024 fn member_default(
1025 ast: &Ast,
1026 node: &MemberNode,
1027 type_: Id<TypeNode>,
1028 place: Self::Expr,
1029 ) -> Result<Vec<Self::Stmt>, EncodeError> {
1030 crate::default_nim::member_default(ast, node, type_, value_of(place))
1031 }
1032
1033 /// `common_Colour_RED` — the module-scope constant the
1034 /// materialiser declared the enumerator's field under. The trait's
1035 /// default concatenates the prefix and the escape, which would
1036 /// escape the segment rather than the joined name.
1037 fn enumerator_constant(ast: &Ast, id: Id<TypeNode>, enumerator: &str) -> Self::Expr {
1038 plain(nim::Expr::name(&nim_enumerator_constant(
1039 ast, id, enumerator,
1040 )))
1041 }
1042}
1043
1044/// The bound a `string`, a `sequence` or a `map` carries, as the suffix
1045/// that keeps two of them apart.
1046///
1047/// The tag names the routine a hoisted collection's codec is emitted
1048/// under, and one body is emitted per name. With the bound left out,
1049/// `sequence<long>` and `sequence<long, 4>` are one name and whichever
1050/// was declared first decides: measured on
1051/// `@final struct S { sequence<long> b; sequence<long, 4> a; };`, the
1052/// generated Nim held no `zd_over` check at all, so `a` accepted a
1053/// stream of any length; with the members the other way round both got
1054/// the bounded body, so `b` refused — and truncated — a length the IDL
1055/// puts no limit on. `string<4>` beside `string` and `wstring<4>`
1056/// beside `wstring` collapse the same way, onto the trimming call.
1057/// Either way the reader answered with a value the IDL does not
1058/// describe and said nothing, which is why the bound is part of the
1059/// name. The same suffix the Zig column carries.
1060///
1061/// An `array` needs nothing here: its `dims` are already in the tag,
1062/// under the `Array` arm below.
1063fn bound_tag(ast: &Ast, bound: &zerodds_ast::Bound) -> String {
1064 match bound.expr() {
1065 None => String::new(),
1066 // `_b` and not a bare `_n`: `seq_long_4` is what the `Array`
1067 // arm answers for an array of four `long`, and the two are
1068 // different wire forms.
1069 Some(expr) => match ast.eval_integral(expr) {
1070 Ok(limit) => format!("_b{limit}"),
1071 // As the array's `_x`: a bound that does not evaluate is
1072 // still not the unbounded form, and the materialiser
1073 // reports the expression that would not evaluate.
1074 Err(_) => "_bx".to_string(),
1075 },
1076 }
1077}
1078
1079/// The name a codec function takes for the type it codes: the IDL kind
1080/// and not the Nim spelling, structurally for a collection and the
1081/// flattened bare name for a declaration — the same scheme the D and Go
1082/// columns carry, for the reasons their `type_tag`s state. The bare
1083/// name and not [`crate::adapter::nim::nim_type_name`]: a stropped
1084/// segment inside `zd_enc_..` would not be an identifier, and the
1085/// `zd_enc_` prefix already keeps the name clear of every keyword.
1086///
1087/// zerodds-lint: recursion-depth 64 (collection nesting; bounded by `MAX_TYPE_DEPTH`)
1088#[must_use]
1089pub fn type_tag(ast: &Ast, id: Id<TypeNode>, depth: usize) -> String {
1090 if depth > MAX_TYPE_DEPTH {
1091 return "unresolved".to_string();
1092 }
1093 let node = match ast.type_(id) {
1094 Ok(node) => node,
1095 Err(_) => return "unresolved".to_string(),
1096 };
1097 match &node.kind {
1098 TypeKind::Primitive(kind) => kind.as_str().replace(' ', "_"),
1099 TypeKind::String {
1100 width: CharWidth::Char,
1101 bound,
1102 } => format!("string{}", bound_tag(ast, bound)),
1103 TypeKind::String {
1104 width: CharWidth::WChar,
1105 bound,
1106 } => format!("wstring{}", bound_tag(ast, bound)),
1107 TypeKind::Fixed { digits, scale } => format!("fixed_{digits}_{scale}"),
1108 TypeKind::Sequence { element, bound } => match element.resolved() {
1109 Some(element) => format!(
1110 "seq{}_{}",
1111 bound_tag(ast, bound),
1112 type_tag(ast, element, depth + 1)
1113 ),
1114 None => format!("seq_unresolved{}", bound_tag(ast, bound)),
1115 },
1116 TypeKind::Array { element, dims } => {
1117 let mut name = match element.resolved() {
1118 Some(element) => format!("arr_{}", type_tag(ast, element, depth + 1)),
1119 None => "arr_unresolved".to_string(),
1120 };
1121 for dim in dims {
1122 match ast.eval_integral(*dim) {
1123 Ok(length) => name.push_str(&format!("_{length}")),
1124 // No number rather than a zero: the name has to
1125 // differ from a `[0]` array's, and the materialiser
1126 // reports the dimension that would not evaluate.
1127 Err(_) => name.push_str("_x"),
1128 }
1129 }
1130 name
1131 }
1132 TypeKind::Map { key, value, bound } => {
1133 let half = |half: Option<Id<TypeNode>>| match half {
1134 Some(target) => type_tag(ast, target, depth + 1),
1135 None => "unresolved".to_string(),
1136 };
1137 format!(
1138 "map{}_{}_{}",
1139 bound_tag(ast, bound),
1140 half(key.resolved()),
1141 half(value.resolved())
1142 )
1143 }
1144 // An alias is followed through: two IDL names for one shape are
1145 // one wire form, and two codecs for it would be two chances to
1146 // differ.
1147 TypeKind::Alias { aliased } => match aliased.resolved() {
1148 Some(target) => type_tag(ast, target, depth + 1),
1149 None => "unresolved".to_string(),
1150 },
1151 TypeKind::ValueBox { boxed } => match boxed.resolved() {
1152 Some(target) => format!("box_{}", type_tag(ast, target, depth + 1)),
1153 None => "box_unresolved".to_string(),
1154 },
1155 TypeKind::Structure { .. }
1156 | TypeKind::Union { .. }
1157 | TypeKind::Enumeration { .. }
1158 | TypeKind::Bitmask { .. }
1159 | TypeKind::Bitset { .. }
1160 | TypeKind::Valuetype { .. }
1161 | TypeKind::Native => nim_bare_name(ast, id),
1162 TypeKind::Any => "any".to_string(),
1163 TypeKind::ValueBase => "valuebase".to_string(),
1164 TypeKind::ObjectRef { .. } => "objectref".to_string(),
1165 TypeKind::AnnotationType { .. } => "annotation".to_string(),
1166 }
1167}