Skip to main content

zerodds_lang_rules/emit/
elixir.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The Elixir backend of the shared codec walk.
4//!
5//! DDS-XTypes 1.3 §7.4 from the clause, the same as `runtime/c`,
6//! `runtime/python` and the rest do, and none of them calls another.
7//! Where two columns disagree on an octet, one has misread the
8//! specification — which is the only reason to have a tenth.
9//!
10//! # The decision this column turns on
11//!
12//! **Elixir has no mutation, and the walk assumes one.** The shared walk
13//! rebinds locals inside loop bodies and `if` branches — `w` after every
14//! write, `v` after every member — and expects the binding to survive
15//! the block, which in Elixir it does not. This backend answers by
16//! building the statement layer of `zerodds_lang_cst::elixir`, where
17//! every binding is *named on the statement*, and
18//! `zerodds_lang_render::elixir::codec_function` threads the changed
19//! bindings out of each block as an accumulator tuple. The division of
20//! labour is deliberate: this file decides *which* statement a walk
21//! operation is, the renderer decides how a block hands its rebindings
22//! out, and neither answers the other's question.
23//!
24//! Concretely, every runtime operation is classified once, in
25//! `writer_op` and `reader_op`:
26//!
27//! - a **pure** operation (`reader_position`, `bitfield`, the trims)
28//!   stays an expression: `ZeroddsWire.Reader.position(r)`;
29//! - a **state-changing** operation with no result rebinds its
30//!   receiver: `w = ZeroddsWire.Writer.write_u32(w, x)`;
31//! - one with a result binds both: `{zd_rv, r} =
32//!   ZeroddsWire.Reader.read_u32(r)` — and the value the walk wanted is
33//!   `zd_rv`, hoisted ahead of the expression it sat in.
34//!
35//! A state-changing call that survives into a position this file cannot
36//! hoist from — an `until` condition, which is re-evaluated per pass —
37//! is a panic naming the operation, never a call quietly evaluated
38//! once: the three conditions the walks build are pure, and a fourth
39//! that is not must fail the generator rather than ship a loop that
40//! stops early.
41//!
42//! # Two smaller decisions
43//!
44//! **An integer is unbounded, so a width is never a cast.** The same
45//! ground the Python column stands on: [`CodecTarget::cast`] is the
46//! identity on every integer, the runtime range-checks at the octet,
47//! and the width lives in the codec's *name* through `type_tag`.
48//!
49//! **A `map` entry is a two-element list, both directions.** The
50//! materialiser's `@type` writes `list({K, V})`, but a tuple cannot be
51//! written into half at a time, which is exactly what the shared read
52//! walk does — `entry[0]`, then `entry[1]`. The probe's own filler
53//! builds `[k, v]` lists and its dumper reads them, so the codec never
54//! meets a tuple; `ZeroddsWire.entry_half/2` accepts both so a
55//! hand-built sample that used tuples is read rather than crashed on.
56
57use super::{CodecTarget, Constant, Handle, Handoff, Routine, Scalar};
58use crate::adapter::elixir::type_module_path;
59use crate::encode::EncodeError;
60use zerodds_ast::{Ast, CharWidth, MemberNode, TypeKind, TypeNode};
61use zerodds_ast_core::Id;
62use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
63use zerodds_lang_cst::elixir::{self as ex, atom_name};
64
65/// The Elixir backend.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct Elixir;
68
69/// How deep a chain of typedefs and collections is followed.
70const MAX_TYPE_DEPTH: usize = 64;
71
72/// The type name [`CodecTarget::named`] is handed for C's signed
73/// `char`; see the Python backend, which this mirrors.
74const SIGNED_CHAR: &str = "signed char";
75
76/// What [`CodecTarget::map_entry_name`] answers with — a marker the
77/// seams that receive it match on, because a `map` entry has no Elixir
78/// module and both need to know the thing they are making room for is a
79/// two-slot list.
80const MAP_ENTRY: &str = "_zd_map_entry";
81
82/// The marker segment a writer operation carries until a statement seam
83/// resolves it. Never rendered: everything that can hold an expression
84/// goes through [`lower`] first, and `lower` panics on a marker it
85/// cannot place.
86const W_MARK: &str = "ZdW";
87/// The reader counterpart of [`W_MARK`].
88const R_MARK: &str = "ZdR";
89
90thread_local! {
91    /// The number the next temporary of the routine being built takes.
92    /// See [`Elixir::fresh`] and [`Elixir::reset_temporaries`].
93    static NEXT_TEMPORARY: core::cell::Cell<u64> = const { core::cell::Cell::new(0) };
94}
95
96/// What a writer operation does to the writer.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98enum OpShape {
99    /// No state change: an ordinary expression.
100    Pure,
101    /// Rebinds the receiver, answers nothing.
102    Mutates,
103    /// Rebinds the receiver *and* answers a value.
104    MutatesWithValue,
105}
106
107/// The shape of one writer operation.
108fn writer_op(method: &str) -> OpShape {
109    match method {
110        "bitfield" => OpShape::Pure,
111        "write_dheader"
112        | "write_emheader"
113        | "write_pid"
114        | "write_pid_extended"
115        | "begin_packed_element" => OpShape::MutatesWithValue,
116        _ => OpShape::Mutates,
117    }
118}
119
120/// The shape of one reader operation.
121fn reader_op(method: &str) -> OpShape {
122    match method {
123        "reader_status" | "reader_position" | "reader_remaining" | "reader_save" | "trim_utf8"
124        | "trim_utf16" => OpShape::Pure,
125        "reader_refuse"
126        | "reader_restore"
127        | "reader_skip_to"
128        | "finish_pid_read"
129        | "reader_finish_packed_element" => OpShape::Mutates,
130        // Every `read_*`, and `reader_clear_discard`.
131        _ => OpShape::MutatesWithValue,
132    }
133}
134
135impl Elixir {
136    /// The runtime module a marked call resolves to.
137    fn runtime_path(marker: &str, method: &str) -> Path {
138        let module = if marker == W_MARK { "Writer" } else { "Reader" };
139        Path::new(&["ZeroddsWire", module, method])
140    }
141
142    /// The receiver a marked call threads.
143    fn receiver(marker: &str) -> &'static str {
144        if marker == W_MARK { "w" } else { "r" }
145    }
146
147    /// A temporary's name, unique within the routine being built.
148    ///
149    /// Unique rather than rebound: a temporary reused across scopes is
150    /// a name bound before an `if` and rebound inside it, which the
151    /// renderer's threading would then carry out of the block — and the
152    /// carried binding, never read again, is a warning, and warnings
153    /// are failures in this column's compile gates.
154    ///
155    /// The counter is thread-local and [`Self::routine`] returns it to
156    /// zero as it closes each routine, so the same input produces the
157    /// same text: a routine is one Elixir function and one scope, which
158    /// is as far as a temporary's name has to reach, and a counter that
159    /// ran on past the end of a routine would number the next one
160    /// differently depending on what else this process had generated
161    /// first — two types in one run, or two tests on one thread.
162    fn fresh(prefix: &str) -> Ident {
163        let number = NEXT_TEMPORARY.with(|next| {
164            let number = next.get();
165            next.set(number.wrapping_add(1));
166            number
167        });
168        Ident::new(&format!("{prefix}{number}"))
169    }
170
171    /// The temporary counter back to zero, at the end of a routine.
172    fn reset_temporaries() {
173        NEXT_TEMPORARY.with(|next| next.set(0));
174    }
175
176    /// Hoist every state-changing runtime call out of `expr`.
177    ///
178    /// Answers the statements that have to run first and the residual
179    /// expression, which is pure.
180    fn lower(expr: ex::Expr) -> (Vec<ex::Statement>, ex::Expr) {
181        let mut statements = Vec::new();
182        let mut temporaries = 0usize;
183        let value = Self::lower_into(expr, &mut statements, &mut temporaries);
184        (statements, value)
185    }
186
187    fn lower_into(
188        expr: ex::Expr,
189        statements: &mut Vec<ex::Statement>,
190        temporaries: &mut usize,
191    ) -> ex::Expr {
192        match expr {
193            ex::Expr::Call { func, args } => {
194                let marker = func
195                    .segments()
196                    .first()
197                    .map(|segment| segment.as_str().to_string());
198                let args: Vec<ex::Expr> = args
199                    .into_iter()
200                    .map(|argument| Self::lower_into(argument, statements, temporaries))
201                    .collect();
202                let Some(marker) = marker.filter(|m| m == W_MARK || m == R_MARK) else {
203                    return ex::Expr::Call { func, args };
204                };
205                let method = func
206                    .segments()
207                    .get(1)
208                    .map(Ident::as_str)
209                    .unwrap_or_default()
210                    .to_string();
211                let shape = if marker == W_MARK {
212                    writer_op(&method)
213                } else {
214                    reader_op(&method)
215                };
216                let receiver = Self::receiver(&marker);
217                let mut full_args = vec![ex::Expr::Var(Ident::new(receiver))];
218                full_args.extend(args);
219                let call = ex::Expr::Call {
220                    func: Self::runtime_path(&marker, &method),
221                    args: full_args,
222                };
223                match shape {
224                    OpShape::Pure => call,
225                    // A state change with no value in expression
226                    // position is a walk shape this backend has no
227                    // lowering for. The two statement seams
228                    // ([`Elixir::perform`], [`Elixir::drop_value`])
229                    // intercept it first, so reaching this is a walk
230                    // that changed underneath the backend — and the
231                    // no-panic contract turns the refusal into a call
232                    // to a name no module defines: `elixirc` stops on
233                    // it and names the operation, which is louder than
234                    // a call quietly evaluated for a value it does not
235                    // have.
236                    OpShape::Mutates => ex::Expr::Call {
237                        func: Path::single(&format!("zd_unlowered_{method}")),
238                        args: Vec::new(),
239                    },
240                    OpShape::MutatesWithValue => {
241                        *temporaries += 1;
242                        let name = Self::fresh("zd_rv");
243                        statements.push(ex::Statement::BindTuple {
244                            names: vec![name.clone(), Ident::new(receiver)],
245                            value: call,
246                        });
247                        ex::Expr::Var(name)
248                    }
249                }
250            }
251            ex::Expr::Binary { op, lhs, rhs } => {
252                let lhs = Self::lower_into(*lhs, statements, temporaries);
253                let rhs = Self::lower_into(*rhs, statements, temporaries);
254                ex::Expr::Binary {
255                    op,
256                    lhs: Box::new(lhs),
257                    rhs: Box::new(rhs),
258                }
259            }
260            ex::Expr::Unary { op, value } => ex::Expr::Unary {
261                op,
262                value: Box::new(Self::lower_into(*value, statements, temporaries)),
263            },
264            ex::Expr::Paren(value) => {
265                ex::Expr::Paren(Box::new(Self::lower_into(*value, statements, temporaries)))
266            }
267            ex::Expr::Field { value, name } => ex::Expr::Field {
268                value: Box::new(Self::lower_into(*value, statements, temporaries)),
269                name,
270            },
271            ex::Expr::Index { value, index } => ex::Expr::Index {
272                value: Box::new(Self::lower_into(*value, statements, temporaries)),
273                index: Box::new(Self::lower_into(*index, statements, temporaries)),
274            },
275            ex::Expr::IfElse {
276                condition,
277                then,
278                otherwise,
279            } => ex::Expr::IfElse {
280                condition: Box::new(Self::lower_into(*condition, statements, temporaries)),
281                then: Box::new(Self::lower_into(*then, statements, temporaries)),
282                otherwise: Box::new(Self::lower_into(*otherwise, statements, temporaries)),
283            },
284            other @ (ex::Expr::Literal(_)
285            | ex::Expr::Atom(_)
286            | ex::Expr::Var(_)
287            | ex::Expr::Attr(_)
288            | ex::Expr::Module(_)
289            | ex::Expr::List(_)
290            | ex::Expr::Tuple(_)
291            | ex::Expr::Keyword(_)
292            | ex::Expr::StructLiteral { .. }) => other,
293        }
294    }
295
296    /// Where a value lands: the base variable and the access path from
297    /// it. `None` for an expression the walk never assigns into.
298    fn place_of(place: &ex::Expr) -> Option<(Ident, Vec<ex::Access>)> {
299        match place {
300            ex::Expr::Var(name) => Some((name.clone(), Vec::new())),
301            ex::Expr::Field { value, name } => {
302                let (base, mut path) = Self::place_of(value)?;
303                path.push(ex::Access::Key(name.clone()));
304                Some((base, path))
305            }
306            ex::Expr::Index { value, index } => {
307                let (base, mut path) = Self::place_of(value)?;
308                path.push(ex::Access::At((**index).clone()));
309                Some((base, path))
310            }
311            _ => None,
312        }
313    }
314
315    /// The statement a place this backend cannot decompose becomes: a
316    /// call to a name no module defines, so `elixirc` stops on the
317    /// generated file and the value is never quietly dropped.
318    fn refused_place(place: ex::Expr, value: ex::Expr) -> ex::Statement {
319        ex::Statement::Bind {
320            name: Ident::new("_"),
321            value: ex::Expr::Call {
322                func: Path::single("zd_refused_not_a_place"),
323                args: vec![place, value],
324            },
325        }
326    }
327
328    /// `place = value`, as the statements that get it there.
329    fn assign(place: ex::Expr, value: ex::Expr) -> Vec<ex::Statement> {
330        let (mut statements, value) = Self::lower(value);
331        let Some((base, path)) = Self::place_of(&place) else {
332            statements.push(Self::refused_place(place, value));
333            return statements;
334        };
335        if path.is_empty() {
336            statements.push(ex::Statement::Bind { name: base, value });
337        } else {
338            statements.push(ex::Statement::Update { base, path, value });
339        }
340        statements
341    }
342
343    /// One statement out of several, flattened later by the renderer.
344    fn seq(mut statements: Vec<ex::Statement>) -> ex::Statement {
345        if statements.len() == 1 {
346            return statements.remove(0);
347        }
348        ex::Statement::Seq(statements)
349    }
350
351    /// Whether a call is to a generated *encoder*, which threads the
352    /// writer: `w = zd_enc_T(w, value)`.
353    fn is_encoder_call(func: &Path) -> bool {
354        func.segments().len() == 1
355            && func.segments().first().is_some_and(|name| {
356                let name = name.as_str();
357                name.starts_with("zd_enc_")
358                    || name.starts_with("zd_kenc_")
359                    || name.starts_with("zd_keyholder_")
360            })
361    }
362}
363
364impl CodecTarget for Elixir {
365    type Expr = ex::Expr;
366    type Stmt = ex::Statement;
367    type Block = Vec<ex::Statement>;
368    type Type = ex::Type;
369    type Decl = ex::CodecFunction;
370
371    // -- types --------------------------------------------------------
372
373    /// One `integer()` for every integer width, because Elixir has one.
374    ///
375    /// A `char` and a `wchar` are marked with their own tag: both are a
376    /// one-character binary at run time, and [`Self::cast`] needs to
377    /// tell a character place from a string one to rewrite a union
378    /// label into the right literal.
379    fn scalar(kind: Scalar) -> Self::Type {
380        match kind {
381            Scalar::Bool => ex::Type::Basic(ex::Basic::Boolean),
382            Scalar::Char => ex::Type::Atom("char".to_string()),
383            Scalar::Float | Scalar::Double => ex::Type::Basic(ex::Basic::Float),
384            Scalar::LongDouble => ex::Type::Atom("float128".to_string()),
385            Scalar::Int8
386            | Scalar::UInt8
387            | Scalar::Int16
388            | Scalar::UInt16
389            | Scalar::Int32
390            | Scalar::UInt32
391            | Scalar::Int64
392            | Scalar::UInt64
393            | Scalar::Index
394            | Scalar::Size => ex::Type::Basic(ex::Basic::Integer),
395        }
396    }
397
398    fn named(name: &str) -> Self::Type {
399        ex::Type::Atom(name.to_string())
400    }
401
402    fn nothing() -> Self::Type {
403        ex::Type::Basic(ex::Basic::Nil)
404    }
405
406    /// The runtime's handles are maps and need no declared type; the
407    /// tag exists so a declaration without an initialiser can be told
408    /// apart in a debugger of the generator, not of the generated code.
409    fn handle(kind: Handle) -> Self::Type {
410        ex::Type::Atom(
411            match kind {
412                Handle::DHeader => "dheader",
413                Handle::EmHeader => "emheader",
414                Handle::Pid => "pid",
415                Handle::TryConstruct => "try_construct",
416                Handle::ReaderMark => "reader_mark",
417                Handle::PidRead => "pid_read",
418                Handle::EmHeaderRead => "emheader_read",
419            }
420            .to_string(),
421        )
422    }
423
424    /// A one-character binary is what a `char` is here; the runtime
425    /// takes it whole and refuses what the type cannot hold.
426    fn code_point(value: Self::Expr, _narrow: bool) -> Self::Expr {
427        value
428    }
429
430    /// The runtime's policy values, as the atoms
431    /// `runtime/elixir` matches on.
432    ///
433    /// Atoms rather than strings because that is what an Elixir function
434    /// head matches, and the quoted forms — `:"character-count"` — keep
435    /// the exact spellings of `crates/ast-layout/src/key.rs`, so the
436    /// measurement's names survive into the generated file. One arm per
437    /// constant and no catch-all, for the reason every backend states.
438    fn constant(which: Constant) -> Self::Expr {
439        let atom = |name: &str| ex::Expr::Atom(name.to_string());
440        match which {
441            Constant::WcharWidth2 => ex::Expr::int(2),
442            Constant::WcharWidth4 => ex::Expr::int(4),
443            Constant::WcharFixed => atom("fixed"),
444            Constant::WcharGiopLengthPrefixed => atom("giop-length-prefixed"),
445            Constant::WstringUnitsStream => atom("stream"),
446            Constant::WstringUnitsBigEndianAlways => atom("big-endian-always"),
447            Constant::WstringUnitsByteOrderMark => atom("byte-order-mark"),
448            Constant::WstringUnitsHostUnswapped => atom("host-unswapped"),
449            Constant::WstringSpecification => atom("specification"),
450            Constant::WstringCharacterCount => atom("character-count"),
451            Constant::WstringCharacterCountTerminated => atom("character-count-terminated"),
452            Constant::WstringSpecificationWideUnits => atom("specification-wide-units"),
453            Constant::WstringRosTypeSupport => atom("ros-type-support"),
454            Constant::WstringOctetsTerminated2 => atom("octets/terminated/2"),
455            Constant::WstringOctetsTerminated4 => atom("octets/terminated/4"),
456            Constant::WstringCharactersTerminated2 => atom("characters/terminated/2"),
457            Constant::FixedDeclared => atom("declared"),
458            Constant::FixedValuePrecision => atom("value-precision"),
459            Constant::FixedValueStringDigits => atom("value-string-digits"),
460            Constant::ParameterMember => atom("member"),
461            Constant::ParameterOptional => atom("optional"),
462            Constant::TryConstructDiscard => atom("discard"),
463            Constant::TryConstructTrim => atom("trim"),
464            Constant::TryConstructUseDefault => atom("use_default"),
465            Constant::StatusOk => atom("ok"),
466            Constant::StatusDiscarded => atom("discarded"),
467            Constant::StatusImpossibleLength => atom("impossible_length"),
468            Constant::StatusOutOfRange => atom("out_of_range"),
469            Constant::StatusTruncated => atom("truncated"),
470        }
471    }
472
473    /// `integer()`, at every bit bound.
474    fn unsigned_holder(_bits: u16) -> Self::Type {
475        ex::Type::Basic(ex::Basic::Integer)
476    }
477
478    fn value_type(ast: &Ast, id: Id<TypeNode>) -> Self::Type {
479        value_type_of(ast, id, 0)
480    }
481
482    fn type_name(ast: &Ast, id: Id<TypeNode>) -> String {
483        type_tag(ast, id, 0)
484    }
485
486    /// A `map`'s entry is a two-element list and has no module; see the
487    /// module documentation.
488    fn map_entry_name(_ast: &Ast, _id: Id<TypeNode>) -> String {
489        MAP_ENTRY.to_string()
490    }
491
492    /// Nothing: an Elixir module name carries its whole scope and every
493    /// reference is written from the root.
494    fn module_prefix(_ast: &Ast, _id: Id<TypeNode>) -> String {
495        String::new()
496    }
497
498    fn escape(name: &str) -> String {
499        atom_name(name)
500    }
501
502    // -- expressions ---------------------------------------------------
503
504    fn name(text: &str) -> Self::Expr {
505        ex::Expr::Var(Ident::new(text))
506    }
507
508    fn int(value: i128) -> Self::Expr {
509        ex::Expr::int(value)
510    }
511
512    /// The number: an Elixir integer is unbounded and needs no suffix.
513    fn wide_int(value: i128) -> Self::Expr {
514        ex::Expr::int(value)
515    }
516
517    /// As [`Self::wide_int`].
518    fn wide_unsigned(value: u64) -> Self::Expr {
519        ex::Expr::int(i128::from(value))
520    }
521
522    /// As [`Self::wide_unsigned`].
523    fn wide_signed(value: i128) -> Self::Expr {
524        ex::Expr::int(value)
525    }
526
527    /// The number, with no suffix: `2u` is not an Elixir token.
528    fn unsigned_int(value: i128) -> Self::Expr {
529        ex::Expr::int(value)
530    }
531
532    /// A literal, with the two raw tokens the walk escapes through
533    /// [`Literal::Int`] rewritten — `true` and `false` are the same
534    /// spelling in Elixir, but the tree's boolean is what says a
535    /// boolean was meant.
536    fn literal(value: Literal) -> Self::Expr {
537        let Literal::Int(text) = &value else {
538            return ex::Expr::Literal(value);
539        };
540        match text.as_str() {
541            "true" => ex::Expr::Literal(Literal::Bool(true)),
542            "false" => ex::Expr::Literal(Literal::Bool(false)),
543            _ => ex::Expr::Literal(value),
544        }
545    }
546
547    /// A call to a generated function — or, for `strcmp`, the sign
548    /// expression Elixir's own binary comparison gives.
549    ///
550    /// Erlang compares binaries bytewise, which is `strcmp`'s
551    /// `unsigned char` order (C11 §7.24.4), so the sign is two
552    /// comparisons and a subtraction.
553    fn call(callee: &str, args: Vec<Self::Expr>) -> Self::Expr {
554        if callee == "strcmp" {
555            return strcmp_sign(args);
556        }
557        ex::Expr::Call {
558            func: Path::single(callee),
559            args,
560        }
561    }
562
563    /// An operation on the runtime, carried as a marked call until a
564    /// statement seam places it. See the module documentation.
565    fn runtime(receiver: &str, method: &str, args: Vec<Self::Expr>) -> Self::Expr {
566        let marker = if receiver == "w" { W_MARK } else { R_MARK };
567        ex::Expr::Call {
568            func: Path::new(&[marker, method]),
569            args,
570        }
571    }
572
573    /// A free function of the `ZeroddsWire` module; every one the walks
574    /// ask for is pure.
575    fn runtime_static(method: &str, args: Vec<Self::Expr>) -> Self::Expr {
576        ex::Expr::Call {
577            func: Path::new(&["ZeroddsWire", method]),
578            args,
579        }
580    }
581
582    /// `runtime/elixir` carries the three HDDS axes:
583    /// `begin_packed_element` / `finish_packed_element` on the writer and
584    /// `reader_begin_packed_element` / `reader_finish_packed_element` on the
585    /// reader, the DHEADER pair for a sequence element's own length, and
586    /// `write_wstring_utf8_octets` / `read_wstring_utf8_octets`.
587    fn carries_hdds_axes() -> bool {
588        true
589    }
590
591    /// A field name with dots is a path through the members that hold a base
592    /// (`parent.x`, XTypes 1.3 §7.5.1.4.1.3): one field step per segment, which is what
593    /// the place of an update decomposes into.
594    fn member(base: &str, field: &str) -> Self::Expr {
595        let mut place = Self::name(base);
596        for segment in field.split('.') {
597            place = ex::Expr::Field {
598                value: Box::new(place),
599                name: Ident::new(segment),
600            };
601        }
602        place
603    }
604
605    /// The base is the first member `parent` (XTypes 1.3 §7.5.1.4.1.3).
606    fn inherited_path(depth: usize) -> Option<String> {
607        Some(format!("{}.", atom_name("parent")).repeat(depth))
608    }
609
610    fn whole(base: &str) -> Self::Expr {
611        Self::name(base)
612    }
613
614    fn field_of(value: Self::Expr, field: &str) -> Self::Expr {
615        ex::Expr::Field {
616            value: Box::new(value),
617            name: Ident::new(field),
618        }
619    }
620
621    /// The runtime's headers are plain maps, but two of their field
622    /// names are not Elixir's to take: `end` is a reserved word and
623    /// does not parse after a dot, so the reader publishes the
624    /// sentinel flag as `list_end`.
625    fn header_field(value: Self::Expr, field: &str) -> Self::Expr {
626        let named = if field == "end" { "list_end" } else { field };
627        Self::field_of(value, named)
628    }
629
630    fn index(value: Self::Expr, at: Self::Expr) -> Self::Expr {
631        ex::Expr::Index {
632            value: Box::new(value),
633            index: Box::new(at),
634        }
635    }
636
637    /// `length(v)`, or the bound where the materialiser fixed one.
638    fn count_of(base: &str, fixed: Option<u64>) -> Self::Expr {
639        match fixed {
640            Some(bound) => ex::Expr::int(i128::from(bound)),
641            None => ex::Expr::Call {
642                func: Path::single("length"),
643                args: vec![Self::name(base)],
644            },
645        }
646    }
647
648    fn elements(base: &str) -> Self::Expr {
649        Self::name(base)
650    }
651
652    /// `v.field` — a union's case member is a plain struct field, the
653    /// materialiser's one-struct union shape.
654    fn case_member(base: &str, field: &str) -> Self::Expr {
655        Self::member(base, field)
656    }
657
658    /// `ZeroddsWire.entry_half(entry, 0 | 1)` on the way *out* would be
659    /// unassignable, so both halves are the index form and the write
660    /// side rebuilds through `Access.at`. See the module documentation.
661    fn entry_half(entry: Self::Expr, key: bool) -> Self::Expr {
662        ex::Expr::Index {
663            value: Box::new(entry),
664            index: Box::new(ex::Expr::int(i128::from(!key))),
665        }
666    }
667
668    fn absent() -> Self::Expr {
669        ex::Expr::nil()
670    }
671
672    /// Nothing: an Elixir binary owns its storage and a read either
673    /// produced one or raised.
674    fn materialised(_value: Self::Expr) -> Option<Self::Expr> {
675        None
676    }
677
678    /// The member itself: absence is `nil` and the value needs no
679    /// unwrapping.
680    fn optional_value(_ast: &Ast, _id: Id<TypeNode>, base: &str, field: &str) -> Self::Expr {
681        Self::member(base, field)
682    }
683
684    /// `v.field != nil`.
685    ///
686    /// `!=` and not `!==`: the compared value is `nil` and nothing else
687    /// compares equal to it, so the strict form would claim a
688    /// distinction that does not exist here.
689    fn present(base: &str, field: &str) -> Self::Expr {
690        ex::Expr::Binary {
691            op: "!=",
692            lhs: Box::new(Self::member(base, field)),
693            rhs: Box::new(Self::absent()),
694        }
695    }
696
697    /// `v = ZeroddsWire.array_to_lists(v, depth)`: the tuple
698    /// `default_elixir.rs` answers for an array, as the nested lists
699    /// `put_in(v, [Access.at(..)], ..)` writes into — `Access.at/1`
700    /// refuses a tuple.
701    fn open_array(base: &str, lengths: &[i128]) -> Vec<Self::Stmt> {
702        vec![ex::Statement::Bind {
703            name: Ident::new(base),
704            value: Self::runtime_static(
705                "array_to_lists",
706                vec![Self::whole(base), ex::Expr::int(lengths.len() as i128)],
707            ),
708        }]
709    }
710
711    /// `v = ZeroddsWire.array_to_tuples(v, depth)`: the rows back as
712    /// the `{T, …}` the materialiser declares the member as. A list in
713    /// its place is not `==` to the tuple a caller built (measured:
714    /// `%A{w: [1, 2, 3]} == %A{w: {1, 2, 3}}` was `false` for a decoded
715    /// `long w[3]`).
716    fn close_array(base: &str, lengths: &[i128]) -> Vec<Self::Stmt> {
717        vec![ex::Statement::Bind {
718            name: Ident::new(base),
719            value: Self::runtime_static(
720                "array_to_tuples",
721                vec![Self::whole(base), ex::Expr::int(lengths.len() as i128)],
722            ),
723        }]
724    }
725
726    /// Nothing on the way in — writing the member is what makes it
727    /// present — and `v = %{v | field: nil}` on the way out.
728    fn set_present(base: &str, field: &str, present: bool) -> Vec<Self::Stmt> {
729        if present {
730            return Vec::new();
731        }
732        vec![ex::Statement::Update {
733            base: Ident::new(base),
734            path: vec![ex::Access::Key(Ident::new(field))],
735            value: Self::absent(),
736        }]
737    }
738
739    /// `v = %{v | field: nil}` for every case: a union is a struct with
740    /// one nullable field per branch, and only the selected one holds a
741    /// value. The same statement as an absent
742    /// `@optional` member, [`Self::set_present`] with `false`.
743    fn clear_union_cases(base: &str, fields: &[String]) -> Vec<Self::Stmt> {
744        fields
745            .iter()
746            .flat_map(|field| Self::set_present(base, field, false))
747            .collect()
748    }
749
750    /// The member, with nothing to do first: a struct field holding
751    /// `nil` is already somewhere to write.
752    fn optional_destination(base: &str, field: &str) -> (Vec<Self::Stmt>, Self::Expr) {
753        (Vec::new(), Self::member(base, field))
754    }
755
756    /// The operators, in Elixir's spelling.
757    ///
758    /// `and`/`or` and not `&&`/`||`: the strict forms take booleans
759    /// only, and everything the walk tests is one — a truthiness
760    /// operator would quietly accept a `nil` where a boolean belongs.
761    /// The bitwise four are `Bitwise`'s operator forms, which the
762    /// generated module imports.
763    fn binary(op: &'static str, lhs: Self::Expr, rhs: Self::Expr) -> Self::Expr {
764        ex::Expr::Binary {
765            op: match op {
766                "&&" => "and",
767                "||" => "or",
768                "&" => "&&&",
769                "|" => "|||",
770                ">>" => ">>>",
771                "<<" => "<<<",
772                other => other,
773            },
774            lhs: Box::new(lhs),
775            rhs: Box::new(rhs),
776        }
777    }
778
779    /// `not a`; bitwise complement is `Bitwise.bnot(a)` because `~~~`
780    /// is deprecated in the Elixir this column measures against.
781    fn unary(op: &'static str, value: Self::Expr) -> Self::Expr {
782        if op == "~" {
783            return ex::Expr::Call {
784                func: Path::new(&["Bitwise", "bnot"]),
785                args: vec![value],
786            };
787        }
788        ex::Expr::Unary {
789            op: match op {
790                "!" => "not ",
791                other => other,
792            },
793            value: Box::new(value),
794        }
795    }
796
797    fn paren(value: Self::Expr) -> Self::Expr {
798        ex::Expr::Paren(Box::new(value))
799    }
800
801    /// The identity on every integer, and a rewrite on three things
802    /// that are not integers — the same three as the Python column, at
803    /// a union's discriminator.
804    fn cast(type_: Self::Type, value: Self::Expr) -> Self::Expr {
805        if type_ == Self::named(SIGNED_CHAR) {
806            return signed_char(value);
807        }
808        let ex::Expr::Literal(Literal::Int(text)) = &value else {
809            return value;
810        };
811        let digits = text.strip_prefix('-').unwrap_or(text);
812        if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
813            return value;
814        }
815        if type_ == Self::scalar(Scalar::Char) {
816            // The one-character binary the discriminator holds. A lone
817            // surrogate has no UTF-8 spelling and no Elixir binary can
818            // hold one, and the runtime refuses reading one — so a
819            // surrogate label becomes an atom no binary ever equals,
820            // which is "selects no branch" said in a comparison.
821            return text
822                .parse::<u32>()
823                .ok()
824                .filter(|code| *code <= u32::from(u16::MAX))
825                .map_or_else(
826                    || value.clone(),
827                    |code| match char::from_u32(code) {
828                        Some(point) => ex::Expr::Literal(Literal::Str(point.to_string())),
829                        None => ex::Expr::Atom(format!("zd_surrogate_{code:x}")),
830                    },
831                );
832        }
833        if type_ == Self::scalar(Scalar::Bool) {
834            return ex::Expr::Literal(Literal::Bool(text != "0"));
835        }
836        value
837    }
838
839    /// The value, unchanged: everything is handed by value and rebuilt
840    /// by the caller.
841    fn handoff(value: Self::Expr, _form: Handoff) -> Self::Expr {
842        value
843    }
844
845    // -- statements ----------------------------------------------------
846
847    /// `name = value`, or `name = nil` where the walk declared without
848    /// a value — the binding the renderer's threading needs to exist
849    /// before the block that rebinds it.
850    fn declare(_type: Self::Type, name: &str, value: Option<Self::Expr>) -> Self::Stmt {
851        match value {
852            Some(value) => Self::seq(Self::assign(Self::name(name), value)),
853            None => ex::Statement::Bind {
854                name: Ident::new(name),
855                value: Self::absent(),
856            },
857        }
858    }
859
860    /// An expression evaluated for its effect — which in this column is
861    /// always a rebinding: an assignment the walk built, a runtime call
862    /// that threads its receiver, or a generated encoder call that
863    /// threads the writer.
864    fn perform(value: Self::Expr) -> Self::Stmt {
865        if let ex::Expr::Binary { op: "=", lhs, rhs } = value {
866            return Self::seq(Self::assign(*lhs, *rhs));
867        }
868        if let ex::Expr::Call { func, args } = &value {
869            let head = func.segments().first().map(Ident::as_str);
870            if head == Some(W_MARK) || head == Some(R_MARK) {
871                let marker = head.unwrap_or_default().to_string();
872                let method = func
873                    .segments()
874                    .get(1)
875                    .map(Ident::as_str)
876                    .unwrap_or_default()
877                    .to_string();
878                let shape = if marker == W_MARK {
879                    writer_op(&method)
880                } else {
881                    reader_op(&method)
882                };
883                let receiver = Self::receiver(&marker);
884                let mut statements = Vec::new();
885                let mut temporaries = 0usize;
886                let args: Vec<ex::Expr> = args
887                    .iter()
888                    .cloned()
889                    .map(|argument| Self::lower_into(argument, &mut statements, &mut temporaries))
890                    .collect();
891                let mut full_args = vec![ex::Expr::Var(Ident::new(receiver))];
892                full_args.extend(args);
893                let call = ex::Expr::Call {
894                    func: Self::runtime_path(&marker, &method),
895                    args: full_args,
896                };
897                statements.push(match shape {
898                    OpShape::Mutates => ex::Statement::Bind {
899                        name: Ident::new(receiver),
900                        value: call,
901                    },
902                    // A value-answering call performed for its effect
903                    // still moves the state; the value is discarded by
904                    // name.
905                    OpShape::MutatesWithValue => ex::Statement::BindTuple {
906                        names: vec![Ident::new("_"), Ident::new(receiver)],
907                        value: call,
908                    },
909                    OpShape::Pure => ex::Statement::Bind {
910                        name: Ident::new("_"),
911                        value: call,
912                    },
913                });
914                return Self::seq(statements);
915            }
916            if Self::is_encoder_call(func) {
917                // `w = zd_enc_T(w, value)`: a generated encoder answers
918                // the writer it advanced.
919                let (mut statements, lowered) = Self::lower(value.clone());
920                statements.push(ex::Statement::Bind {
921                    name: Ident::new("w"),
922                    value: lowered,
923                });
924                return Self::seq(statements);
925            }
926        }
927        let (mut statements, lowered) = Self::lower(value);
928        statements.push(ex::Statement::Bind {
929            name: Ident::new("_"),
930            value: lowered,
931        });
932        Self::seq(statements)
933    }
934
935    /// `throw({:zd_return, {v, r}})` — every `give_back` the decode
936    /// walk builds is the bare form, and a decoder here answers the
937    /// sample *and* the reader it consumed. A `give_back` that ever
938    /// arrives with a value answers that value in the sample's place,
939    /// which is what the C shape means by it.
940    fn give_back(value: Option<Self::Expr>) -> Self::Stmt {
941        ex::Statement::Return(ex::Expr::Tuple(vec![
942            value.unwrap_or_else(|| Self::name("v")),
943            Self::name("r"),
944        ]))
945    }
946
947    fn when(
948        condition: Self::Expr,
949        then: Self::Block,
950        otherwise: Option<Self::Block>,
951    ) -> Self::Stmt {
952        // `reader_clear_discard` arrives here as a condition and moves
953        // the reader; an `if` evaluates its condition once, so hoisting
954        // it out in front is the same program.
955        let (mut statements, condition) = Self::lower(condition);
956        statements.push(ex::Statement::If {
957            condition,
958            then,
959            otherwise: otherwise.unwrap_or_default(),
960        });
961        Self::seq(statements)
962    }
963
964    fn count_up(counter: &str, bound: Self::Expr, body: Self::Block) -> Self::Stmt {
965        let (mut statements, bound) = Self::lower(bound);
966        statements.push(ex::Statement::For {
967            counter: Ident::new(counter),
968            bound,
969            body,
970        });
971        Self::seq(statements)
972    }
973
974    /// A loop whose condition is re-evaluated per pass, so a
975    /// state-changing call inside it has no lowering at all — the three
976    /// conditions the walks build are pure, and a fourth that is not
977    /// must stop at the generated file rather than run once: the hoist
978    /// is kept *and* a call to a name no module defines goes in front,
979    /// so `elixirc` refuses the file and names the fault, and the loop
980    /// can never run with a condition evaluated at the wrong time.
981    fn until(condition: Self::Expr, body: Self::Block) -> Self::Stmt {
982        let (mut statements, condition) = Self::lower(condition);
983        if !statements.is_empty() {
984            statements.push(ex::Statement::Bind {
985                name: Ident::new("_"),
986                value: ex::Expr::Call {
987                    func: Path::single("zd_refused_stateful_loop_condition"),
988                    args: Vec::new(),
989                },
990            });
991        }
992        statements.push(ex::Statement::While { condition, body });
993        Self::seq(statements)
994    }
995
996    fn leave() -> Self::Stmt {
997        ex::Statement::Break
998    }
999
1000    fn note(doc: Doc) -> Self::Stmt {
1001        ex::Statement::Comment(doc)
1002    }
1003
1004    /// A value read for its effect: the reader still moves, the value
1005    /// is discarded by name. Exactly [`Self::perform`]'s job — a
1006    /// runtime call performed for its effect binds `{_, receiver}`,
1007    /// anything else binds `_` — and one implementation rather than a
1008    /// second copy that could drift into leaving a temporary unread.
1009    fn drop_value(value: Self::Expr) -> Self::Stmt {
1010        Self::perform(value)
1011    }
1012
1013    /// `_ = zd_tc` — the one spelling Elixir gives "this parameter is
1014    /// deliberately not read", and warnings are failures in this
1015    /// column's compile gates.
1016    fn mark_used(name: &str) -> Vec<Self::Stmt> {
1017        vec![ex::Statement::Bind {
1018            name: Ident::new("_"),
1019            value: Self::name(name),
1020        }]
1021    }
1022
1023    /// `{<place>, r} = zd_dec_T(r, behaviour)` — a generated decoder
1024    /// answers the value and the reader, and the destination is the
1025    /// bind's left-hand side.
1026    fn read_call(
1027        callee: &str,
1028        place: Self::Expr,
1029        _form: Handoff,
1030        behaviour: Self::Expr,
1031    ) -> Self::Stmt {
1032        let call = ex::Expr::Call {
1033            func: Path::single(callee),
1034            args: vec![Self::name("r"), behaviour],
1035        };
1036        let Some((base, path)) = Self::place_of(&place) else {
1037            return Self::refused_place(place, call);
1038        };
1039        if path.is_empty() {
1040            return ex::Statement::BindTuple {
1041                names: vec![base, Ident::new("r")],
1042                value: call,
1043            };
1044        }
1045        let temporary = Self::fresh("zd_rd");
1046        Self::seq(vec![
1047            ex::Statement::BindTuple {
1048                names: vec![temporary.clone(), Ident::new("r")],
1049                value: call,
1050            },
1051            ex::Statement::Update {
1052                base,
1053                path,
1054                value: ex::Expr::Var(temporary),
1055            },
1056        ])
1057    }
1058
1059    /// `{v, r} = zd_dec_T(r, zd_tc)` — a `typedef`, handed straight on.
1060    fn read_forward(callee: &str) -> Self::Stmt {
1061        ex::Statement::BindTuple {
1062            names: vec![Ident::new("v"), Ident::new("r")],
1063            value: ex::Expr::Call {
1064                func: Path::single(callee),
1065                args: vec![Self::name("r"), Self::name("zd_tc")],
1066            },
1067        }
1068    }
1069
1070    fn block(statements: Vec<Self::Stmt>) -> Self::Block {
1071        statements
1072    }
1073
1074    fn into_statements(block: Self::Block) -> Vec<Self::Stmt> {
1075        block
1076    }
1077
1078    /// Room for `count` values, and the fill inside it.
1079    ///
1080    /// A sequence's slots are assigned outright, so `List.duplicate`
1081    /// over `nil` is enough; a `map`'s are written half at a time, so
1082    /// each slot is a two-element list — and unlike the Python column
1083    /// there is no aliasing to avoid, because an Elixir list is
1084    /// immutable and `put_in` rebuilds the spine. The entries *stay*
1085    /// lists; the filler builds lists and the dumper reads them.
1086    fn make_room(
1087        base: &str,
1088        _element: Self::Type,
1089        count: Self::Expr,
1090        assigned_whole: bool,
1091        fill: Vec<Self::Stmt>,
1092    ) -> Vec<Self::Stmt> {
1093        let slot = if assigned_whole {
1094            Self::absent()
1095        } else {
1096            ex::Expr::List(vec![Self::absent(), Self::absent()])
1097        };
1098        let mut out = vec![ex::Statement::Bind {
1099            name: Ident::new(base),
1100            value: ex::Expr::Call {
1101                func: Path::new(&["List", "duplicate"]),
1102                args: vec![slot, count],
1103            },
1104        }];
1105        out.extend(fill);
1106        out
1107    }
1108
1109    /// `v = Enum.take(v, count)` — §7.3.1.2.1.12.3's TRIM.
1110    fn keep_first(base: &str, count: Self::Expr) -> Vec<Self::Stmt> {
1111        vec![ex::Statement::Bind {
1112            name: Ident::new(base),
1113            value: ex::Expr::Call {
1114                func: Path::new(&["Enum", "take"]),
1115                args: vec![Self::name(base), count],
1116            },
1117        }]
1118    }
1119
1120    /// The subscript, with nothing before or after it: a list element
1121    /// is an ordinary value at every element type.
1122    fn sequence_element(
1123        base: &str,
1124        index: Self::Expr,
1125        _element: Self::Type,
1126    ) -> (Vec<Self::Stmt>, Self::Expr, Vec<Self::Stmt>) {
1127        (
1128            Vec::new(),
1129            Self::index(Self::elements(base), index),
1130            Vec::new(),
1131        )
1132    }
1133
1134    /// A local the body writes *into*: the `map` scratch entry is a
1135    /// two-slot list, everything else is an ordinary declaration.
1136    fn storage(type_: Self::Type, name: &str) -> Self::Stmt {
1137        if type_ == ex::Type::Atom(MAP_ENTRY.to_string()) {
1138            return ex::Statement::Bind {
1139                name: Ident::new(name),
1140                value: ex::Expr::List(vec![Self::absent(), Self::absent()]),
1141            };
1142        }
1143        Self::declare(type_, name, None)
1144    }
1145
1146    // -- assembly ------------------------------------------------------
1147
1148    /// `def zd_enc_T(w, v) do … w end` and
1149    /// `def zd_dec_T(r, zd_tc) do v = zd_def_T() … {v, r} end`.
1150    ///
1151    /// A forward declaration is answered with nothing: an Elixir module
1152    /// resolves a local call at run time and a recursive type needs no
1153    /// declaration ahead of its body.
1154    fn routine(spec: Routine<Self>) -> Option<Self::Decl> {
1155        // The routine's body is complete by the time it arrives here, so
1156        // this is where its temporaries stop being reachable and the
1157        // counter goes back to zero. Before the early return: a
1158        // forward declaration carries no body but the walk may well have
1159        // numbered temporaries deciding that.
1160        Self::reset_temporaries();
1161        let body = spec.body?;
1162        let mut params = Vec::new();
1163        if spec.writing {
1164            params.push(Ident::new("w"));
1165            params.push(Ident::new("v"));
1166        } else {
1167            params.push(Ident::new("r"));
1168        }
1169        params.extend(spec.extra.iter().map(|(_, name)| Ident::new(name)));
1170        let (body, result) = if spec.writing {
1171            // `_ = v` first: an aggregate with no members writes its
1172            // (absent) delimiter and nothing else, and a parameter the
1173            // body never reads is a warning — and warnings are failures
1174            // in this column's compile gates.
1175            let mut statements = vec![ex::Statement::Bind {
1176                name: Ident::new("_"),
1177                value: Self::name("v"),
1178            }];
1179            statements.extend(body);
1180            (statements, Self::name("w"))
1181        } else {
1182            let defaulter = crate::default_elixir::defaulter_from_decoder(&spec.name);
1183            let mut statements = vec![
1184                ex::Statement::Bind {
1185                    name: Ident::new("v"),
1186                    value: ex::Expr::Call {
1187                        func: Path::single(&defaulter),
1188                        args: Vec::new(),
1189                    },
1190                },
1191                // `_ = v` reads the Table 9 binding: a scalar's decoder
1192                // rebinds `v` outright and would otherwise leave the
1193                // defaulter's binding unread, which is a warning — and
1194                // warnings are failures in this column's compile gates.
1195                ex::Statement::Bind {
1196                    name: Ident::new("_"),
1197                    value: Self::name("v"),
1198                },
1199            ];
1200            statements.extend(body);
1201            (
1202                statements,
1203                ex::Expr::Tuple(vec![Self::name("v"), Self::name("r")]),
1204            )
1205        };
1206        Some(ex::CodecFunction {
1207            doc: spec.doc,
1208            name: Ident::new(&spec.name),
1209            params,
1210            body,
1211            result,
1212        })
1213    }
1214
1215    /// `place = zd_def_T()` — Table 9, through the generated defaulter.
1216    ///
1217    /// # Errors
1218    /// Never; the signature matches the emitters around it.
1219    fn default_into(
1220        ast: &Ast,
1221        id: Id<TypeNode>,
1222        place: Self::Expr,
1223    ) -> Result<Self::Stmt, EncodeError> {
1224        Ok(crate::default_elixir::default_into(ast, id, place))
1225    }
1226
1227    /// # Errors
1228    /// As [`crate::default_elixir::member_default`]: a `@default` this
1229    /// column cannot spell is refused rather than replaced by Table 9's
1230    /// zero.
1231    fn member_default(
1232        ast: &Ast,
1233        node: &MemberNode,
1234        type_: Id<TypeNode>,
1235        place: Self::Expr,
1236    ) -> Result<Vec<Self::Stmt>, EncodeError> {
1237        crate::default_elixir::member_default(ast, node, type_, place)
1238    }
1239
1240    /// `Common.Mode.active()` — the enumeration module's constant
1241    /// function, which answers the declared integer. The materialiser's
1242    /// own module path, so the two cannot part.
1243    fn enumerator_constant(ast: &Ast, id: Id<TypeNode>, enumerator: &str) -> Self::Expr {
1244        let module = type_module_path(ast, id);
1245        let mut segments: Vec<String> = module
1246            .segments()
1247            .iter()
1248            .map(|segment| segment.as_str().to_string())
1249            .collect();
1250        segments.push(atom_name(enumerator));
1251        ex::Expr::Call {
1252            func: Path::new(&segments.iter().map(String::as_str).collect::<Vec<&str>>()),
1253            args: Vec::new(),
1254        }
1255    }
1256}
1257
1258/// A character's code point read as C's signed `char`:
1259/// `rem(hd(String.to_charlist(k)) + 128, 256) - 128`.
1260///
1261/// `rem` and not `Integer.mod`: the operand is a code point plus 128 and
1262/// never negative, so the two agree, and `rem` needs no alias. Inline
1263/// arithmetic and not a runtime helper, for the reason the Python column
1264/// records: how wide and how signed C spells a character is a language
1265/// mapping question and does not belong in a runtime whose subject is
1266/// §7.4.
1267fn signed_char(value: ex::Expr) -> ex::Expr {
1268    let point = ex::Expr::Call {
1269        func: Path::single("hd"),
1270        args: vec![ex::Expr::Call {
1271            func: Path::new(&["String", "to_charlist"]),
1272            args: vec![value],
1273        }],
1274    };
1275    let wrapped = ex::Expr::Call {
1276        func: Path::single("rem"),
1277        args: vec![
1278            ex::Expr::Binary {
1279                op: "+",
1280                lhs: Box::new(point),
1281                rhs: Box::new(ex::Expr::int(128)),
1282            },
1283            ex::Expr::int(256),
1284        ],
1285    };
1286    ex::Expr::Binary {
1287        op: "-",
1288        lhs: Box::new(ex::Expr::Paren(Box::new(wrapped))),
1289        rhs: Box::new(ex::Expr::int(128)),
1290    }
1291}
1292
1293/// `strcmp(a, b)` as its sign, computed with Elixir's own binary
1294/// comparison — bytewise, which is `strcmp`'s `unsigned char` order.
1295fn strcmp_sign(args: Vec<ex::Expr>) -> ex::Expr {
1296    let [left, right] = <[ex::Expr; 2]>::try_from(args).unwrap_or_else(|args| {
1297        // Unreachable for the one call site, which passes exactly two;
1298        // a different arity is a walk that changed under this file, and
1299        // the surviving shape is a call to a name no module defines.
1300        [
1301            ex::Expr::Call {
1302                func: Path::single("strcmp"),
1303                args,
1304            },
1305            ex::Expr::int(0),
1306        ]
1307    });
1308    let side = |op: &'static str| {
1309        ex::Expr::Paren(Box::new(ex::Expr::IfElse {
1310            condition: Box::new(ex::Expr::Binary {
1311                op,
1312                lhs: Box::new(left.clone()),
1313                rhs: Box::new(right.clone()),
1314            }),
1315            then: Box::new(ex::Expr::int(1)),
1316            otherwise: Box::new(ex::Expr::int(0)),
1317        }))
1318    };
1319    ex::Expr::Paren(Box::new(ex::Expr::Binary {
1320        op: "-",
1321        lhs: Box::new(side(">")),
1322        rhs: Box::new(side("<")),
1323    }))
1324}
1325
1326/// The bound a `string`, a `sequence` or a `map` carries, as the suffix
1327/// that keeps two of them apart.
1328///
1329/// The tag names the routine a hoisted collection's codec is emitted
1330/// under, and one body is emitted per name. With the bound left out,
1331/// `sequence<long>` and `sequence<long, 4>` are one name and whichever
1332/// was declared first decides: measured on
1333/// `@final struct S { sequence<long> b; sequence<long, 4> a; };`, the
1334/// generated Elixir held no `zd_over` check at all, so `a` accepted a
1335/// stream of any length; with the members the other way round both got
1336/// the bounded body, so `b` refused — and truncated — a length the IDL
1337/// puts no limit on. `string<4>` beside `string` and `wstring<4>`
1338/// beside `wstring` collapse the same way, onto the trimming call.
1339/// Either way the reader answered with a value the IDL does not
1340/// describe and said nothing, which is why the bound is part of the
1341/// name. The same suffix the Zig column carries.
1342///
1343/// An `array` needs nothing here: its `dims` are already in the tag,
1344/// under the `Array` arm below.
1345fn bound_tag(ast: &Ast, bound: &zerodds_ast::Bound) -> String {
1346    match bound.expr() {
1347        None => String::new(),
1348        // `_b` and not a bare `_n`: `seq_long_4` is what the `Array`
1349        // arm answers for an array of four `long`, and the two are
1350        // different wire forms.
1351        Some(expr) => match ast.eval_integral(expr) {
1352            Ok(limit) => format!("_b{limit}"),
1353            // As the array's `_x`: a bound that does not evaluate is
1354            // still not the unbounded form, and the materialiser
1355            // reports the expression that would not evaluate.
1356            Err(_) => "_bx".to_string(),
1357        },
1358    }
1359}
1360
1361/// The tag a codec function is named after.
1362///
1363/// The Elixir counterpart of `adapter::python::type_tag`: built from
1364/// the IDL kind for a primitive — `double` and `long double` share
1365/// nothing here, but the naming convention is one across columns — and
1366/// from the module path for a declared type, its segments joined by
1367/// `_` because a function name cannot carry a dot.
1368///
1369/// zerodds-lint: recursion-depth 64 (collection nesting; bounded by
1370/// `MAX_TYPE_DEPTH`)
1371pub(crate) fn type_tag(ast: &Ast, id: Id<TypeNode>, depth: usize) -> String {
1372    if depth > MAX_TYPE_DEPTH {
1373        return "unknown".to_string();
1374    }
1375    let Ok(node) = ast.type_(id) else {
1376        return "unknown".to_string();
1377    };
1378    match &node.kind {
1379        TypeKind::Primitive(kind) => kind.as_str().replace(' ', "_"),
1380        TypeKind::String {
1381            width: CharWidth::Char,
1382            bound,
1383        } => format!("string{}", bound_tag(ast, bound)),
1384        TypeKind::String {
1385            width: CharWidth::WChar,
1386            bound,
1387        } => format!("wstring{}", bound_tag(ast, bound)),
1388        TypeKind::Fixed { digits, scale } => format!("fixed_{digits}_{scale}"),
1389        TypeKind::Sequence { element, bound } => match element.resolved() {
1390            Some(element) => format!(
1391                "seq{}_{}",
1392                bound_tag(ast, bound),
1393                type_tag(ast, element, depth + 1)
1394            ),
1395            None => format!("seq_unknown{}", bound_tag(ast, bound)),
1396        },
1397        TypeKind::Array { element, dims } => {
1398            let mut name = match element.resolved() {
1399                Some(element) => format!("arr_{}", type_tag(ast, element, depth + 1)),
1400                None => "arr_unknown".to_string(),
1401            };
1402            for dim in dims {
1403                match ast.eval_integral(*dim) {
1404                    Ok(length) => name.push_str(&format!("_{length}")),
1405                    Err(_) => name.push_str("_x"),
1406                }
1407            }
1408            name
1409        }
1410        TypeKind::Map { key, value, bound } => {
1411            let half = |half: Option<Id<TypeNode>>| match half {
1412                Some(target) => type_tag(ast, target, depth + 1),
1413                None => "unknown".to_string(),
1414            };
1415            format!(
1416                "map{}_{}_{}",
1417                bound_tag(ast, bound),
1418                half(key.resolved()),
1419                half(value.resolved())
1420            )
1421        }
1422        TypeKind::Alias { aliased } => match aliased.resolved() {
1423            Some(target) => type_tag(ast, target, depth + 1),
1424            None => "unknown".to_string(),
1425        },
1426        TypeKind::ValueBox { boxed } => match boxed.resolved() {
1427            Some(target) => format!("box_{}", type_tag(ast, target, depth + 1)),
1428            None => "box_unknown".to_string(),
1429        },
1430        TypeKind::Structure { .. }
1431        | TypeKind::Union { .. }
1432        | TypeKind::Enumeration { .. }
1433        | TypeKind::Bitmask { .. }
1434        | TypeKind::Bitset { .. }
1435        | TypeKind::Valuetype { .. }
1436        | TypeKind::Native => type_module_path(ast, id)
1437            .segments()
1438            .iter()
1439            .map(|segment| segment.as_str())
1440            .collect::<Vec<&str>>()
1441            .join("_"),
1442        TypeKind::Any => "any".to_string(),
1443        TypeKind::ValueBase => "valuebase".to_string(),
1444        TypeKind::ObjectRef { .. } => "objectref".to_string(),
1445        TypeKind::AnnotationType { .. } => "annotation".to_string(),
1446    }
1447}
1448
1449/// The Elixir shape of a value, structurally.
1450///
1451/// Barely rendered — a codec body carries no annotations — but the
1452/// comparisons in [`Elixir::cast`] read it, so a `char` place is marked
1453/// apart from a `string` one although both hold a binary at run time.
1454///
1455/// zerodds-lint: recursion-depth 64 (collection nesting; bounded by
1456/// `MAX_TYPE_DEPTH`)
1457fn value_type_of(ast: &Ast, id: Id<TypeNode>, depth: usize) -> ex::Type {
1458    use zerodds_ast::PrimitiveKind;
1459    let unknown = || ex::Type::Basic(ex::Basic::Any);
1460    if depth > MAX_TYPE_DEPTH {
1461        return unknown();
1462    }
1463    let Ok(node) = ast.type_(id) else {
1464        return unknown();
1465    };
1466    let inner = |half: Option<Id<TypeNode>>| {
1467        half.map_or_else(unknown, |target| value_type_of(ast, target, depth + 1))
1468    };
1469    match &node.kind {
1470        TypeKind::Primitive(kind) => match kind {
1471            PrimitiveKind::Boolean => Elixir::scalar(Scalar::Bool),
1472            PrimitiveKind::Char | PrimitiveKind::WChar => Elixir::scalar(Scalar::Char),
1473            PrimitiveKind::Float32 | PrimitiveKind::Float64 => Elixir::scalar(Scalar::Double),
1474            PrimitiveKind::Float128 => Elixir::scalar(Scalar::LongDouble),
1475            _ => Elixir::scalar(Scalar::Int32),
1476        },
1477        TypeKind::String { .. } | TypeKind::Fixed { .. } => ex::Type::Basic(ex::Basic::Binary),
1478        // An enumeration member is a plain integer in this binding —
1479        // the materialiser says why — and a bitmask and a bitset's
1480        // fields likewise.
1481        TypeKind::Enumeration { .. } | TypeKind::Bitmask { .. } => {
1482            ex::Type::Basic(ex::Basic::Integer)
1483        }
1484        TypeKind::Bitset { .. }
1485        | TypeKind::Structure { .. }
1486        | TypeKind::Union { .. }
1487        | TypeKind::Valuetype { .. }
1488        | TypeKind::Native => ex::Type::Atom(type_tag(ast, id, depth)),
1489        TypeKind::Sequence { element, .. } => ex::Type::list(inner(element.resolved())),
1490        TypeKind::Array { element, dims } => {
1491            let mut out = inner(element.resolved());
1492            for _ in dims {
1493                out = ex::Type::list(out);
1494            }
1495            out
1496        }
1497        TypeKind::Map { key, value, .. } => ex::Type::list(ex::Type::Tuple(vec![
1498            inner(key.resolved()),
1499            inner(value.resolved()),
1500        ])),
1501        TypeKind::Alias { aliased } => inner(aliased.resolved()),
1502        TypeKind::ValueBox { boxed } => inner(boxed.resolved()).or_nil(),
1503        TypeKind::Any
1504        | TypeKind::ValueBase
1505        | TypeKind::ObjectRef { .. }
1506        | TypeKind::AnnotationType { .. } => unknown(),
1507    }
1508}