Skip to main content

zerodds_lang_rules/emit/
julia.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The Julia 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`, `runtime/typescript` and the rest do, and none of
7//! them calls another. Where two columns disagree on an octet, one has
8//! misread the specification — which is the only reason to have another.
9//!
10//! # The three decisions this column turns on
11//!
12//! **The declared types are immutable and the decoder still assigns
13//! fields.** `adapter/julia.rs` declares every aggregate as an immutable
14//! `struct` — a decoded sample is a value, not a handle — and the shared
15//! walk writes `v.field = …` at every member. The two meet in the
16//! runtime's `ZdScratch`: a decoder's `v` starts as the Table 9 scratch
17//! its defaulter answers (`crate::default_julia`), every store lands in
18//! the scratch's dictionary, and each `return` goes through
19//! `zd_freeze(zd_rt, v)`, which builds the immutable value by the
20//! declared field order. `zd_rt` is a local the reading body binds to
21//! the sample's own type, because [`CodecTarget::give_back`] has no type
22//! parameter to name it with.
23//!
24//! **A collection under construction is a `Vector{Any}`.** An IDL array
25//! is an `NTuple` — the one shape Julia's own type system fixes the
26//! length of — and an `NTuple` cannot be written into element by
27//! element. The reader therefore fills `Any`-vectors and `zd_freeze`
28//! converts, dimension by dimension. A `map`'s entry under construction
29//! is a `ZdScratch` with `key` and `value` slots for the same reason,
30//! so [`CodecTarget::entry_half`] is one spelling — `.key` / `.value` —
31//! in both directions: a field access reaches a real `ZdMapEntry_…` on
32//! the way out and the scratch's dictionary on the way in.
33//!
34//! **A subscript adds one.** Julia counts from one and the walk counts
35//! from zero; the counter stays the walk's and the `+ 1` is written at
36//! the subscript, in [`CodecTarget::index`], so an index that is also
37//! arithmetic — a rank, a position — keeps the walk's meaning.
38//!
39//! # Two width traps the spellings guard against
40//!
41//! - `for i in 0:(n - 1)` with an unsigned `n` of zero wraps to
42//!   `0:0xffffffff` and runs four billion iterations, so every counted
43//!   loop bound goes through `Int64(…)` first.
44//! - a bare `1 << 40` is a platform `Int`, and a small hex literal is a
45//!   small unsigned type whose `~` loses the high bits, so every 64-bit
46//!   pattern is written `UInt64(…)`.
47
48use super::{CodecTarget, Constant, Handle, Handoff, Routine, Scalar};
49use crate::adapter::julia::{
50    julia_map_entry_name, julia_module_prefix, julia_type_name, julia_value_type,
51};
52use crate::encode::EncodeError;
53use zerodds_ast::{Ast, CharWidth, MemberNode, TypeKind, TypeNode};
54use zerodds_ast_core::Id;
55use zerodds_lang_cst::common::{Doc, Ident, Literal};
56use zerodds_lang_cst::julia::{self as jl, escape_field, escape_name};
57
58/// The Julia backend.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub struct Julia;
61
62/// How deep a chain of typedefs and collections is followed.
63const MAX_TYPE_DEPTH: usize = 64;
64
65/// The type name [`CodecTarget::named`] is handed for C's signed `char`.
66///
67/// `emit/encode.rs`'s `map_key_less` writes `T::named("signed char")`
68/// where a `map`'s key is a `char` and the entry order is
69/// `MapKeyOrdering::SignedInteger`. That is a C spelling in the walk and
70/// not a type Julia has; this column recognises the string and answers
71/// with the two's-complement arithmetic. See [`signed_char`].
72const SIGNED_CHAR: &str = "signed char";
73
74/// What [`CodecTarget::map_entry_name`] answers with.
75///
76/// Not a type this column declares. It is a marker the one seam that
77/// receives it — [`Julia::storage`], through the default
78/// `map_entry_type` — matches on, because a `map` entry under
79/// construction is a runtime `ZdScratch` and not the materialiser's
80/// `ZdMapEntry_…` struct, which is immutable and cannot take one half
81/// at a time.
82const MAP_ENTRY: &str = "_ZdMapEntryScratch";
83
84impl Julia {
85    /// A body's statements, unchanged: Julia accepts an empty block.
86    fn suite(body: Vec<jl::Statement>) -> Vec<jl::Statement> {
87        body
88    }
89
90    /// A reader's body, with the scratch it fills and the freeze that
91    /// answers.
92    ///
93    /// `zd_rt = <SampleType>` first, so that every `return` the walk
94    /// builds — [`Julia::give_back`] has no type to hand — can say
95    /// `return zd_freeze(zd_rt, v)`; then `v = zd_def_…()`, Table 9's
96    /// scratch through the generated defaulter; then the walk's own
97    /// statements; then the freeze.
98    fn reading_body(name: &str, sample: jl::Type, body: Vec<jl::Statement>) -> Vec<jl::Statement> {
99        let defaulter = crate::default_julia::defaulter_from_decoder(name);
100        let mut statements = vec![
101            jl::Statement::Assign {
102                place: jl::Expr::name("zd_rt"),
103                value: jl::Expr::Type(Box::new(sample)),
104            },
105            jl::Statement::Assign {
106                place: jl::Expr::name("v"),
107                value: jl::Expr::free_call(&defaulter, Vec::new()),
108            },
109        ];
110        statements.extend(body);
111        statements.push(Self::give_back(None));
112        statements
113    }
114
115    /// A policy value, as the string the runtime accepts.
116    fn text(value: &str) -> jl::Expr {
117        jl::Expr::Literal(Literal::Str(value.to_string()))
118    }
119}
120
121impl CodecTarget for Julia {
122    type Expr = jl::Expr;
123    type Stmt = jl::Statement;
124    type Block = Vec<jl::Statement>;
125    type Type = jl::Type;
126    type Decl = jl::Decl;
127
128    // -- types --------------------------------------------------------
129
130    /// The fixed-width holders `adapter/julia.rs` declares, and `Int64`
131    /// for the walk's own counters and positions.
132    ///
133    /// Never `Int`: that is `Int64` on this host and `Int32` on a
134    /// 32-bit one, for a wire format that fixes every width. A `char`
135    /// is a `UInt8` code unit and a `long double` a `String` holding
136    /// the decimal — both the materialiser's answers, restated here so
137    /// a codec's local and a struct's field are one type.
138    fn scalar(kind: Scalar) -> Self::Type {
139        jl::Type::Primitive(match kind {
140            Scalar::Bool => jl::Primitive::Bool,
141            Scalar::Char | Scalar::UInt8 => jl::Primitive::UInt8,
142            Scalar::Int8 => jl::Primitive::Int8,
143            Scalar::Int16 => jl::Primitive::Int16,
144            Scalar::UInt16 => jl::Primitive::UInt16,
145            Scalar::Int32 => jl::Primitive::Int32,
146            Scalar::UInt32 => jl::Primitive::UInt32,
147            Scalar::UInt64 => jl::Primitive::UInt64,
148            // The walk's counters, byte positions and the enumeration
149            // holder. Signed on purpose: `0:(Int64(n) - 1)` cannot wrap
150            // where an unsigned zero would.
151            Scalar::Int64 | Scalar::Index | Scalar::Size => jl::Primitive::Int64,
152            Scalar::Float => jl::Primitive::Float32,
153            Scalar::Double => jl::Primitive::Float64,
154            Scalar::LongDouble => jl::Primitive::String,
155        })
156    }
157
158    fn named(name: &str) -> Self::Type {
159        jl::Type::named(name)
160    }
161
162    /// `Nothing`, whose one value is what a Julia function with no
163    /// answer returns.
164    fn nothing() -> Self::Type {
165        jl::Type::Primitive(jl::Primitive::Nothing)
166    }
167
168    /// The runtime's handles, by their exported names.
169    fn handle(kind: Handle) -> Self::Type {
170        jl::Type::named(match kind {
171            Handle::DHeader => "ZdDHeader",
172            Handle::EmHeader => "ZdEmHeader",
173            Handle::Pid => "ZdPid",
174            Handle::TryConstruct => "ZdTryConstruct",
175            Handle::ReaderMark => "ZdReaderMark",
176            Handle::PidRead => "ZdPidRead",
177            Handle::EmHeaderRead => "ZdEmHeaderRead",
178        })
179    }
180
181    /// The code unit, unchanged.
182    ///
183    /// `adapter/julia.rs` holds a `char` in a `UInt8` and a `wchar` in
184    /// a `UInt16`, and `runtime/julia`'s `write_char` and `write_wchar`
185    /// take the integer, so there is nothing to convert on either side.
186    fn code_point(value: Self::Expr, _narrow: bool) -> Self::Expr {
187        value
188    }
189
190    /// The runtime's policy values, in the spelling `runtime/julia`
191    /// accepts.
192    ///
193    /// Strings and integers for the wire-form axes — the same spellings
194    /// `runtime/python` takes, because both are
195    /// `WstringLengthPolicy::as_str` and its siblings in
196    /// `crates/ast-layout/src/key.rs` — and the runtime's `@enum`
197    /// instances for the roles, behaviours and statuses. One arm per
198    /// constant and no catch-all: a policy this column has not
199    /// implemented has to be a compile error here rather than a value
200    /// that looks plausible.
201    fn constant(which: Constant) -> Self::Expr {
202        match which {
203            Constant::WcharWidth2 => jl::Expr::int(2),
204            Constant::WcharWidth4 => jl::Expr::int(4),
205            Constant::WcharFixed => Self::text("fixed"),
206            Constant::WcharGiopLengthPrefixed => Self::text("giop-length-prefixed"),
207            Constant::WstringUnitsStream => Self::text("stream"),
208            Constant::WstringUnitsBigEndianAlways => Self::text("big-endian-always"),
209            Constant::WstringUnitsByteOrderMark => Self::text("byte-order-mark"),
210            Constant::WstringUnitsHostUnswapped => Self::text("host-unswapped"),
211            Constant::WstringSpecification => Self::text("specification"),
212            Constant::WstringCharacterCount => Self::text("character-count"),
213            Constant::WstringCharacterCountTerminated => Self::text("character-count-terminated"),
214            Constant::WstringSpecificationWideUnits => Self::text("specification-wide-units"),
215            Constant::WstringRosTypeSupport => Self::text("ros-type-support"),
216            Constant::WstringOctetsTerminated2 => Self::text("octets/terminated/2"),
217            Constant::WstringOctetsTerminated4 => Self::text("octets/terminated/4"),
218            Constant::WstringCharactersTerminated2 => Self::text("characters/terminated/2"),
219            Constant::FixedDeclared => Self::text("declared"),
220            Constant::FixedValuePrecision => Self::text("value-precision"),
221            Constant::FixedValueStringDigits => Self::text("value-string-digits"),
222            Constant::ParameterMember => jl::Expr::name("ZD_PARAMETER_MEMBER"),
223            Constant::ParameterOptional => jl::Expr::name("ZD_PARAMETER_OPTIONAL"),
224            Constant::TryConstructDiscard => jl::Expr::name("ZD_TRY_CONSTRUCT_DISCARD"),
225            Constant::TryConstructTrim => jl::Expr::name("ZD_TRY_CONSTRUCT_TRIM"),
226            Constant::TryConstructUseDefault => jl::Expr::name("ZD_TRY_CONSTRUCT_USE_DEFAULT"),
227            Constant::StatusOk => jl::Expr::name("ZD_STATUS_OK"),
228            Constant::StatusDiscarded => jl::Expr::name("ZD_STATUS_DISCARDED"),
229            Constant::StatusImpossibleLength => jl::Expr::name("ZD_STATUS_IMPOSSIBLE_LENGTH"),
230            Constant::StatusOutOfRange => jl::Expr::name("ZD_STATUS_OUT_OF_RANGE"),
231            Constant::StatusTruncated => jl::Expr::name("ZD_STATUS_TRUNCATED"),
232        }
233    }
234
235    /// The smallest unsigned integer that holds `bits`.
236    fn unsigned_holder(bits: u16) -> Self::Type {
237        jl::Type::Primitive(match bits {
238            0..=8 => jl::Primitive::UInt8,
239            9..=16 => jl::Primitive::UInt16,
240            17..=32 => jl::Primitive::UInt32,
241            _ => jl::Primitive::UInt64,
242        })
243    }
244
245    fn value_type(ast: &Ast, id: Id<TypeNode>) -> Self::Type {
246        julia_value_type(ast, id).unwrap_or(jl::Type::Primitive(jl::Primitive::Any))
247    }
248
249    fn type_name(ast: &Ast, id: Id<TypeNode>) -> String {
250        codec_tag(ast, id, 0)
251    }
252
253    /// The scratch marker and never the materialiser's entry struct:
254    /// the struct is immutable and a reader fills an entry one half at
255    /// a time. See `MAP_ENTRY`.
256    fn map_entry_name(_ast: &Ast, _id: Id<TypeNode>) -> String {
257        MAP_ENTRY.to_string()
258    }
259
260    /// The flattened module path, which in this column *is* the scope:
261    /// `adapter/julia.rs` writes no `module` and every declaration
262    /// carries its path in its name.
263    fn module_prefix(ast: &Ast, id: Id<TypeNode>) -> String {
264        julia_module_prefix(ast, id)
265    }
266
267    /// The field spelling, which escapes keywords only. A field lives
268    /// in the struct's own namespace and shadows nothing —
269    /// `zerodds_lang_cst::julia::escape_field` carries the measurement.
270    fn escape(name: &str) -> String {
271        escape_field(name)
272    }
273
274    /// The base is the first member `parent` (XTypes 1.3 §7.5.1.4.1.3).
275    fn inherited_path(depth: usize) -> Option<String> {
276        Some(format!("{}.", escape_field("parent")).repeat(depth))
277    }
278
279    // -- expressions ---------------------------------------------------
280
281    fn name(text: &str) -> Self::Expr {
282        jl::Expr::name(text)
283    }
284
285    fn int(value: i128) -> Self::Expr {
286        jl::Expr::int(value)
287    }
288
289    /// The number, unsuffixed.
290    ///
291    /// Julia parses a decimal past `typemax(Int64)` as the next wider
292    /// signed type, and a comparison or a typed-local assignment
293    /// converts exactly, so `16045690984833335998` needs no suffix and
294    /// no widening call.
295    fn wide_int(value: i128) -> Self::Expr {
296        jl::Expr::int(value)
297    }
298
299    /// `UInt64(<n>)` — converted, never bare.
300    ///
301    /// A bare hex literal takes the smallest unsigned type it fits, so
302    /// `~0x07` is a `UInt8` whose complement has lost fifty-six bits by
303    /// the time it meets a `UInt64`; and a bare decimal is a platform
304    /// `Int`. The conversion is exact for every `u64`.
305    fn wide_unsigned(value: u64) -> Self::Expr {
306        jl::Expr::convert(
307            jl::Type::Primitive(jl::Primitive::UInt64),
308            jl::Expr::int(i128::from(value)),
309        )
310    }
311
312    /// As [`Self::wide_int`]: the value survives its own width without
313    /// a suffix.
314    fn wide_signed(value: i128) -> Self::Expr {
315        jl::Expr::int(value)
316    }
317
318    /// The number, with no suffix: `2u` is not a Julia token.
319    fn unsigned_int(value: i128) -> Self::Expr {
320        jl::Expr::int(value)
321    }
322
323    /// A literal, with the two raw tokens the walk escapes through
324    /// [`Literal::Int`] rewritten.
325    ///
326    /// `emit/decode.rs`'s `literal::<T>("false")` carries a keyword in
327    /// an integer literal because that variant is the tree's raw-token
328    /// escape. Julia spells the booleans the same way C does, so the
329    /// rewrite only moves them into [`Literal::Bool`], where the
330    /// renderer owns the spelling.
331    fn literal(value: Literal) -> Self::Expr {
332        let Literal::Int(text) = &value else {
333            return jl::Expr::Literal(value);
334        };
335        match text.as_str() {
336            "true" => jl::Expr::Literal(Literal::Bool(true)),
337            "false" => jl::Expr::Literal(Literal::Bool(false)),
338            _ => jl::Expr::Literal(value),
339        }
340    }
341
342    /// A call to a module-level function.
343    ///
344    /// `strcmp` is the one name the walk hands here that is not a
345    /// function this column generates, and it is C's. `Base.cmp` on two
346    /// `String`s compares byte-wise — UTF-8, which C11 §7.24.4's
347    /// `unsigned char` order preserves — and answers the same sign.
348    fn call(callee: &str, args: Vec<Self::Expr>) -> Self::Expr {
349        if callee == "strcmp" {
350            return jl::Expr::free_call("Base.cmp", args);
351        }
352        jl::Expr::free_call(callee, args)
353    }
354
355    /// `write_u32(w, x)` — the runtime is a set of free functions and
356    /// the receiver is the first argument, which is Julia's own calling
357    /// convention.
358    fn runtime(receiver: &str, method: &str, args: Vec<Self::Expr>) -> Self::Expr {
359        // The walk's bare `read_pid()` names the operation that opens
360        // no value block, because the column it was written for opens
361        // one itself. This walk has no such pair — it reads a header
362        // and later calls `finish_pid_read` — so the block has to be
363        // opened by the read, which is what `read_pid_as` does.
364        // §7.4.1.2 makes the difference a wire question: a member's
365        // internal alignment counts from the first octet after its
366        // header.
367        if method == "read_pid" && args.is_empty() {
368            return Self::runtime(
369                receiver,
370                "read_pid_as",
371                vec![Self::constant(Constant::ParameterMember)],
372            );
373        }
374        let mut all = vec![jl::Expr::name(receiver)];
375        all.extend(args);
376        jl::Expr::free_call(method, all)
377    }
378
379    /// A free function of the runtime — `no_dheader`, `utf8_octets`,
380    /// `pid_is_user_member`, the `bit_pattern_*` family — which is
381    /// exactly what `runtime/julia` exports them as.
382    fn runtime_static(method: &str, args: Vec<Self::Expr>) -> Self::Expr {
383        jl::Expr::free_call(method, args)
384    }
385
386    /// `runtime/julia` carries the three HDDS axes:
387    /// `begin_packed_element` / `finish_packed_element` on the writer
388    /// and `reader_begin_packed_element` / `reader_finish_packed_element`
389    /// on the reader, the DHEADER pair for a sequence element's own
390    /// length, and `write_wstring_utf8_octets` /
391    /// `write_wstring_utf8_octets_bounded` / `read_wstring_utf8_octets`.
392    fn carries_hdds_axes() -> bool {
393        true
394    }
395
396    fn member(base: &str, field: &str) -> Self::Expr {
397        jl::Expr::field(jl::Expr::name(base), field)
398    }
399
400    fn whole(base: &str) -> Self::Expr {
401        jl::Expr::name(base)
402    }
403
404    fn field_of(value: Self::Expr, field: &str) -> Self::Expr {
405        jl::Expr::field(value, field)
406    }
407
408    /// `value[(at) + 1]`: the walk counts from zero and Julia from one,
409    /// and the conversion lives at the subscript so the counter itself
410    /// keeps the walk's meaning everywhere else it is read.
411    fn index(value: Self::Expr, at: Self::Expr) -> Self::Expr {
412        jl::Expr::index(
413            value,
414            jl::Expr::Binary {
415                op: "+",
416                lhs: Box::new(jl::Expr::Paren(Box::new(at))),
417                rhs: Box::new(jl::Expr::int(1)),
418            },
419        )
420    }
421
422    /// `Base.length(v)`, or the bound where the materialiser fixed one.
423    ///
424    /// Qualified, because a generated file binds its declarations at
425    /// the scope this codec runs in and an IDL type named `length`
426    /// would take the bare name over.
427    fn count_of(base: &str, fixed: Option<u64>) -> Self::Expr {
428        match fixed {
429            Some(bound) => jl::Expr::int(i128::from(bound)),
430            None => jl::Expr::free_call("Base.length", vec![jl::Expr::name(base)]),
431        }
432    }
433
434    fn elements(base: &str) -> Self::Expr {
435        jl::Expr::name(base)
436    }
437
438    /// `v.field` — a union's selected case member. `adapter/julia.rs`
439    /// declares one `Union{T, Nothing}` field per case on the struct.
440    fn case_member(base: &str, field: &str) -> Self::Expr {
441        Self::member(base, field)
442    }
443
444    /// `.key` and `.value` — one spelling for both directions.
445    ///
446    /// The encoder reaches a real `ZdMapEntry_…` field; the decoder
447    /// reaches the scratch's dictionary through the same property
448    /// syntax, which is what `ZdScratch` overloads `getproperty` for.
449    fn entry_half(entry: Self::Expr, key: bool) -> Self::Expr {
450        jl::Expr::field(entry, if key { "key" } else { "value" })
451    }
452
453    fn absent() -> Self::Expr {
454        jl::Expr::Literal(Literal::Null)
455    }
456
457    /// Nothing: a Julia `String` owns its storage and a read that
458    /// produced one produced it whole.
459    fn materialised(_value: Self::Expr) -> Option<Self::Expr> {
460        None
461    }
462
463    /// The member itself: `Union{T, Nothing}` is the value or
464    /// `nothing`, with no wrapper to unwrap.
465    fn optional_value(_ast: &Ast, _id: Id<TypeNode>, base: &str, field: &str) -> Self::Expr {
466        Self::member(base, field)
467    }
468
469    /// `v.field !== nothing`.
470    ///
471    /// `!==` and not `!=`: `!=` calls `==`, which for `missing`-like
472    /// values and overloaded types answers structurally, while identity
473    /// against the one value of `Nothing` is the question being asked.
474    fn present(base: &str, field: &str) -> Self::Expr {
475        jl::Expr::Binary {
476            op: "!==",
477            lhs: Box::new(Self::member(base, field)),
478            rhs: Box::new(Self::absent()),
479        }
480    }
481
482    /// Nothing on the way in, `v.field = nothing` on the way out:
483    /// writing the member is what makes it present.
484    fn set_present(base: &str, field: &str, present: bool) -> Vec<Self::Stmt> {
485        if present {
486            return Vec::new();
487        }
488        vec![jl::Statement::Assign {
489            place: Self::member(base, field),
490            value: Self::absent(),
491        }]
492    }
493
494    /// `v.field = nothing` for every case: `adapter/julia.rs` declares
495    /// one `Union{T, Nothing}` field per case, and only the selected one
496    /// holds a value. The same statement as an absent
497    /// `@optional` member, [`Self::set_present`] with `false`.
498    fn clear_union_cases(base: &str, fields: &[String]) -> Vec<Self::Stmt> {
499        fields
500            .iter()
501            .flat_map(|field| Self::set_present(base, field, false))
502            .collect()
503    }
504
505    /// The member, with nothing to do first: the scratch's slot exists
506    /// the moment it is named.
507    fn optional_destination(base: &str, field: &str) -> (Vec<Self::Stmt>, Self::Expr) {
508        (Vec::new(), Self::member(base, field))
509    }
510
511    /// The operators, unchanged: Julia spells `&&`, `||`, `==`, `!=`,
512    /// the shifts and the bitwise pair exactly as the walk writes them.
513    /// `=` is left as it is and never reaches a rendered expression:
514    /// [`Self::perform`] is the only thing that receives one and turns
515    /// it into a statement.
516    fn binary(op: &'static str, lhs: Self::Expr, rhs: Self::Expr) -> Self::Expr {
517        jl::Expr::Binary {
518            op,
519            lhs: Box::new(lhs),
520            rhs: Box::new(rhs),
521        }
522    }
523
524    fn unary(op: &'static str, value: Self::Expr) -> Self::Expr {
525        jl::Expr::Unary {
526            op,
527            value: Box::new(value),
528        }
529    }
530
531    fn paren(value: Self::Expr) -> Self::Expr {
532        jl::Expr::Paren(Box::new(value))
533    }
534
535    /// A conversion, written as the constructor call Julia spells one
536    /// with — and three special cases that are not conversions.
537    ///
538    /// - `signed char` is the walk's C spelling of a map key order and
539    ///   becomes the two's-complement arithmetic (`signed_char`);
540    /// - `Int64` goes through the runtime's `zd_i64`, because
541    ///   `Int64(x::UInt64)` throws above `typemax(Int64)` where the
542    ///   walk means the bit pattern — the union overlay reads an
543    ///   eight-octet unsigned arm through exactly this seam;
544    /// - a `Bool` target with an integer literal is the label rewrite
545    ///   at a boolean discriminator: `true`/`false`, not `Bool(1)`.
546    fn cast(type_: Self::Type, value: Self::Expr) -> Self::Expr {
547        if type_ == jl::Type::named(SIGNED_CHAR) {
548            return signed_char(value);
549        }
550        if type_ == jl::Type::Primitive(jl::Primitive::Int64) {
551            return jl::Expr::free_call("zd_i64", vec![value]);
552        }
553        if type_ == jl::Type::Primitive(jl::Primitive::Bool) {
554            if let jl::Expr::Literal(Literal::Int(text)) = &value {
555                return jl::Expr::Literal(Literal::Bool(text != "0"));
556            }
557        }
558        match &type_ {
559            jl::Type::Primitive(
560                jl::Primitive::String | jl::Primitive::Nothing | jl::Primitive::Any,
561            ) => value,
562            jl::Type::Primitive(_) | jl::Type::Named(_) => jl::Expr::convert(type_, value),
563            // No conversion has a collection or a union target in this
564            // walk; the value stands as it is rather than inventing a
565            // constructor call nothing defines.
566            _ => value,
567        }
568    }
569
570    /// The value, unchanged: Julia passes every argument by reference
571    /// to the same object and takes no address.
572    fn handoff(value: Self::Expr, _form: Handoff) -> Self::Expr {
573        value
574    }
575
576    // -- statements ----------------------------------------------------
577
578    /// `local name::T = value`.
579    ///
580    /// Annotated because the annotation is load-bearing twice over: it
581    /// says the width the value keeps — `local zd_n::UInt32` — and it
582    /// converts a later re-assignment, which is what lets `zd_wire =
583    /// <literal>` land in the `Int64` the holder declared.
584    fn declare(type_: Self::Type, name: &str, value: Option<Self::Expr>) -> Self::Stmt {
585        jl::Statement::Local {
586            name: Ident::new(name),
587            type_: Some(type_),
588            value,
589        }
590    }
591
592    /// An expression evaluated for its effect — or, where the walk
593    /// built an assignment, the assignment.
594    ///
595    /// The shared walk's `assign` helper builds `T::binary("=", …)` and
596    /// hands it here, and this is the only place one can arrive. Julia
597    /// would accept `a = b` as an expression, and it is still lifted
598    /// into a statement: an `=` buried inside a larger expression is an
599    /// assignment nobody reads.
600    fn perform(value: Self::Expr) -> Self::Stmt {
601        if let jl::Expr::Binary { op: "=", lhs, rhs } = value {
602            return jl::Statement::Assign {
603                place: *lhs,
604                value: *rhs,
605            };
606        }
607        jl::Statement::Expr(value)
608    }
609
610    /// `return zd_freeze(zd_rt, v)`.
611    ///
612    /// Every `T::give_back` the decode walk builds is `give_back(None)`
613    /// — a `void` decoder in the walk's home column. This column's
614    /// decoders answer with the immutable sample, so every return goes
615    /// through the freeze; `zd_rt` is the local
616    /// `Julia::reading_body` bound to the sample's type, because this
617    /// method has no type parameter of its own.
618    fn give_back(value: Option<Self::Expr>) -> Self::Stmt {
619        jl::Statement::Return(Some(value.unwrap_or_else(|| {
620            jl::Expr::free_call(
621                "zd_freeze",
622                vec![jl::Expr::name("zd_rt"), jl::Expr::name("v")],
623            )
624        })))
625    }
626
627    fn when(
628        condition: Self::Expr,
629        then: Self::Block,
630        otherwise: Option<Self::Block>,
631    ) -> Self::Stmt {
632        jl::Statement::If {
633            condition,
634            then: Self::suite(then),
635            // An `else` the walk built empty is dropped rather than
636            // kept: `else` above `end` says nothing and reads as though
637            // a branch had been forgotten.
638            otherwise: otherwise.filter(|body| !body.is_empty()),
639        }
640    }
641
642    /// `for counter in 0:(Int64(bound) - 1)`.
643    ///
644    /// Through `Int64` first, unconditionally: with an unsigned bound
645    /// of zero, `0:(n - 1)` wraps to `0:0xffffffff` and the loop runs
646    /// four billion times over a stream that said "none".
647    fn count_up(counter: &str, bound: Self::Expr, body: Self::Block) -> Self::Stmt {
648        jl::Statement::For {
649            counter: Ident::new(counter),
650            iter: jl::Expr::range(
651                jl::Expr::int(0),
652                jl::Expr::Paren(Box::new(jl::Expr::Binary {
653                    op: "-",
654                    lhs: Box::new(jl::Expr::free_call("Int64", vec![bound])),
655                    rhs: Box::new(jl::Expr::int(1)),
656                })),
657            ),
658            body: Self::suite(body),
659        }
660    }
661
662    fn until(condition: Self::Expr, body: Self::Block) -> Self::Stmt {
663        jl::Statement::While {
664            condition,
665            body: Self::suite(body),
666        }
667    }
668
669    fn leave() -> Self::Stmt {
670        jl::Statement::Break
671    }
672
673    fn note(doc: Doc) -> Self::Stmt {
674        jl::Statement::Comment(doc)
675    }
676
677    /// The call, and nothing around it: Julia has no discarded-result
678    /// diagnostic and `(Nothing) expr` is not a statement.
679    fn drop_value(value: Self::Expr) -> Self::Stmt {
680        jl::Statement::Expr(value)
681    }
682
683    /// Nothing: Julia does not warn on an unread parameter, and a bare
684    /// `zd_tc` on a line of its own would be a statement that does
685    /// nothing.
686    fn mark_used(_name: &str) -> Vec<Self::Stmt> {
687        Vec::new()
688    }
689
690    /// `place = zd_dec_T(r, zd_tc)`.
691    ///
692    /// A Julia function has no out-parameter — rebinding a parameter
693    /// rebinds the local — so this column's decoders answer with the
694    /// value and the destination is assigned at the call site.
695    fn read_call(
696        callee: &str,
697        place: Self::Expr,
698        _form: Handoff,
699        behaviour: Self::Expr,
700    ) -> Self::Stmt {
701        jl::Statement::Assign {
702            place,
703            value: jl::Expr::free_call(callee, vec![jl::Expr::name("r"), behaviour]),
704        }
705    }
706
707    /// `v = zd_dec_T(r, zd_tc)` — a `typedef`, handed straight on. The
708    /// nested decoder answers a frozen value and the trailing freeze is
709    /// then the identity.
710    fn read_forward(callee: &str) -> Self::Stmt {
711        jl::Statement::Assign {
712            place: jl::Expr::name("v"),
713            value: jl::Expr::free_call(callee, vec![jl::Expr::name("r"), jl::Expr::name("zd_tc")]),
714        }
715    }
716
717    fn block(statements: Vec<Self::Stmt>) -> Self::Block {
718        statements
719    }
720
721    fn into_statements(block: Self::Block) -> Vec<Self::Stmt> {
722        block
723    }
724
725    /// Room for `count` values, and the fill inside it.
726    ///
727    /// **A sequence** (`assigned_whole`): `v = zd_nothing_vector(n)`,
728    /// a `Vector{Any}` of `nothing`, because the element decoder
729    /// assigns each slot outright and `zd_freeze` converts the run to
730    /// the declared `Vector{T}` or `NTuple` at the return.
731    ///
732    /// **A `map`** (not `assigned_whole`): `v = zd_entry_vector(n)`,
733    /// one *distinct* `ZdScratch` per entry, because each entry is
734    /// written half at a time — `v[i].key = …` — and a repeated
735    /// reference would make every entry the last one read.
736    fn make_room(
737        base: &str,
738        _element: Self::Type,
739        count: Self::Expr,
740        assigned_whole: bool,
741        fill: Vec<Self::Stmt>,
742    ) -> Vec<Self::Stmt> {
743        let maker = if assigned_whole {
744            "zd_nothing_vector"
745        } else {
746            "zd_entry_vector"
747        };
748        let mut out = vec![jl::Statement::Assign {
749            place: jl::Expr::name(base),
750            value: jl::Expr::free_call(maker, vec![count]),
751        }];
752        out.extend(fill);
753        out
754    }
755
756    /// `v = v[1:Int64(count)]`.
757    ///
758    /// §7.3.1.2.1.12.3's TRIM for a bounded collection, applied after
759    /// every element has been read. One-based and inclusive, which is
760    /// what "the first `count` elements" is in Julia's own counting.
761    fn keep_first(base: &str, count: Self::Expr) -> Vec<Self::Stmt> {
762        vec![jl::Statement::Assign {
763            place: jl::Expr::name(base),
764            value: jl::Expr::index(
765                jl::Expr::name(base),
766                jl::Expr::range(jl::Expr::int(1), jl::Expr::free_call("Int64", vec![count])),
767            ),
768        }]
769    }
770
771    /// The subscript, with nothing before or after it: a `Vector{Any}`
772    /// slot is an ordinary place at every element type.
773    fn sequence_element(
774        base: &str,
775        index: Self::Expr,
776        _element: Self::Type,
777    ) -> (Vec<Self::Stmt>, Self::Expr, Vec<Self::Stmt>) {
778        (
779            Vec::new(),
780            Self::index(Self::elements(base), index),
781            Vec::new(),
782        )
783    }
784
785    /// A local the body writes *into*.
786    ///
787    /// One caller: `zd_scratch`, the entry of an uncounted `map`, which
788    /// the reader fills a half at a time — a runtime `ZdScratch`, for
789    /// the reason `MAP_ENTRY` gives.
790    fn storage(type_: Self::Type, name: &str) -> Self::Stmt {
791        if type_ == jl::Type::named(MAP_ENTRY) {
792            return jl::Statement::Assign {
793                place: jl::Expr::name(name),
794                value: jl::Expr::free_call("zd_scratch_entry", Vec::new()),
795            };
796        }
797        Self::declare(type_, name, None)
798    }
799
800    /// One field of a header the runtime handed back.
801    ///
802    /// `end` is a Julia keyword and cannot name a struct field, so the
803    /// runtime's `ZdPidRead` spells it `end_` and this seam is where
804    /// the walk's name meets the runtime's.
805    fn header_field(value: Self::Expr, field: &str) -> Self::Expr {
806        let spelled = if field == "end" { "end_" } else { field };
807        jl::Expr::field(value, spelled)
808    }
809
810    // -- assembly ------------------------------------------------------
811
812    /// `function zd_enc_T(w::ZdWriter, v::T) … end` and
813    /// `function zd_dec_T(r::ZdReader, zd_tc::ZdTryConstruct) … end`.
814    ///
815    /// A forward declaration is answered with nothing: Julia resolves a
816    /// global at the call and not at the definition, so a recursive or
817    /// mutually recursive type needs no declaration ahead of its body.
818    fn routine(spec: Routine<Self>) -> Option<Self::Decl> {
819        let body = spec.body?;
820        let mut params = vec![jl::Param {
821            name: Ident::new(if spec.writing { "w" } else { "r" }),
822            type_: Some(jl::Type::named(if spec.writing {
823                "ZdWriter"
824            } else {
825                "ZdReader"
826            })),
827        }];
828        if spec.writing {
829            params.push(jl::Param {
830                name: Ident::new("v"),
831                // Unannotated on purpose. The declared sample type
832                // would be right for every call the walk builds, and
833                // one caller sits outside the walk: the write harness
834                // hands the sample it froze, and an annotation here
835                // would re-convert a value that is already exact.
836                type_: None,
837            });
838        }
839        params.extend(spec.extra.into_iter().map(|(type_, name)| jl::Param {
840            name: Ident::new(&name),
841            type_: Some(type_),
842        }));
843        let body = if spec.writing {
844            Self::suite(body)
845        } else {
846            Self::reading_body(&spec.name, spec.sample, body)
847        };
848        Some(jl::Decl::FunctionDef(jl::FunctionDef {
849            doc: spec.doc,
850            name: Ident::new(&spec.name),
851            params,
852            // No return annotation: a reader's returns already go
853            // through `zd_freeze`, and an annotation would ask Julia to
854            // convert a value that is already the declared type.
855            returns: None,
856            body,
857        }))
858    }
859
860    /// A call to a generated defaulter and not a literal in place;
861    /// [`crate::default_julia`] carries the whole table.
862    ///
863    /// # Errors
864    /// Never; the signature matches the emitters around it.
865    fn default_into(
866        ast: &Ast,
867        id: Id<TypeNode>,
868        place: Self::Expr,
869    ) -> Result<Self::Stmt, EncodeError> {
870        Ok(crate::default_julia::default_into(ast, id, place))
871    }
872
873    /// # Errors
874    /// As [`crate::default_julia::member_default`]: a `@default` this
875    /// column cannot spell as a Julia expression is refused rather than
876    /// silently replaced by Table 9's zero.
877    fn member_default(
878        ast: &Ast,
879        node: &MemberNode,
880        type_: Id<TypeNode>,
881        place: Self::Expr,
882    ) -> Result<Vec<Self::Stmt>, EncodeError> {
883        crate::default_julia::member_default(ast, node, type_, place)
884    }
885
886    /// `a_b_RED` — the flattened constant `adapter/julia.rs` binds an
887    /// enumerator to at module scope.
888    ///
889    /// Through `escape_name` and not [`Self::escape`]: an enumerator is
890    /// a module-scope binding and shadows what a field cannot, so the
891    /// two escapes are different sets and the materialiser used the
892    /// wider one.
893    fn enumerator_constant(ast: &Ast, id: Id<TypeNode>, enumerator: &str) -> Self::Expr {
894        jl::Expr::name(&format!(
895            "{}{}",
896            julia_module_prefix(ast, id),
897            escape_name(enumerator)
898        ))
899    }
900}
901
902/// A code unit read as C's signed `char`.
903///
904/// `((Int64(k) + 128) % 256) - 128`: the two's-complement reading of one
905/// octet, for `MapKeyOrdering::SignedInteger` over an IDL `char` key.
906/// The corpus was recorded on a host where C's `char` is signed, so
907/// `U+00FF` sorts below `U+0000`, and a column comparing unsigned code
908/// units would order the entries differently.
909///
910/// Arithmetic and not a helper in `runtime/julia`, for the reason the
911/// Python column writes down: how wide and how signed C spells a
912/// character is a language-mapping question, and a runtime whose subject
913/// is §7.4 is not where a second column should inherit this column's
914/// host from.
915fn signed_char(value: jl::Expr) -> jl::Expr {
916    let wide = jl::Expr::free_call("Int64", vec![value]);
917    let wrapped = jl::Expr::Binary {
918        op: "%",
919        lhs: Box::new(jl::Expr::Paren(Box::new(jl::Expr::Binary {
920            op: "+",
921            lhs: Box::new(wide),
922            rhs: Box::new(jl::Expr::int(128)),
923        }))),
924        rhs: Box::new(jl::Expr::int(256)),
925    };
926    jl::Expr::Binary {
927        op: "-",
928        lhs: Box::new(jl::Expr::Paren(Box::new(wrapped))),
929        rhs: Box::new(jl::Expr::int(128)),
930    }
931}
932
933/// The bound a `string`, a `sequence` or a `map` carries, as the suffix
934/// that keeps two of them apart.
935///
936/// The tag names the routine a hoisted collection's codec is emitted
937/// under, and one body is emitted per name. With the bound left out,
938/// `sequence<long>` and `sequence<long, 4>` are one name and whichever
939/// was declared first decides: measured on
940/// `@final struct S { sequence<long> b; sequence<long, 4> a; };`, the
941/// generated Julia held no `zd_over` check at all, so `a` accepted a
942/// stream of any length; with the members the other way round both got
943/// the bounded body, so `b` refused — and truncated — a length the IDL
944/// puts no limit on. `string<4>` beside `string` and `wstring<4>`
945/// beside `wstring` collapse the same way, onto the trimming call.
946/// Either way the reader answered with a value the IDL does not
947/// describe and said nothing, which is why the bound is part of the
948/// name. The same suffix the Zig column carries.
949///
950/// An `array` needs nothing here: its `dims` are already in the tag,
951/// under the `Array` arm below.
952fn bound_tag(ast: &Ast, bound: &zerodds_ast::Bound) -> String {
953    match bound.expr() {
954        None => String::new(),
955        // `_b` and not a bare `_n`: `seq_long_4` is what the `Array`
956        // arm answers for an array of four `long`, and the two are
957        // different wire forms.
958        Some(expr) => match ast.eval_integral(expr) {
959            Ok(limit) => format!("_b{limit}"),
960            // As the array's `_x`: a bound that does not evaluate is
961            // still not the unbounded form, and the materialiser
962            // reports the expression that would not evaluate.
963            Err(_) => "_bx".to_string(),
964        },
965    }
966}
967
968/// The name component a codec function takes for a type.
969///
970/// The IDL kind and never the Julia spelling: `octet` and `char` are one
971/// `UInt8` here and one octet apart on no wire, but `double` and every
972/// integer width map one to one — the tag keeps them apart the same way
973/// the Python column's does. An alias is followed through, so a typedef
974/// and its target share one codec and the walk's first-wins
975/// deduplication keeps the target's body.
976///
977/// zerodds-lint: recursion-depth 64 (collection nesting; bounded by `MAX_TYPE_DEPTH`)
978fn codec_tag(ast: &Ast, id: Id<TypeNode>, depth: usize) -> String {
979    if depth > MAX_TYPE_DEPTH {
980        return "deep".to_string();
981    }
982    let Ok(node) = ast.type_(id) else {
983        return "unknown".to_string();
984    };
985    match &node.kind {
986        TypeKind::Primitive(kind) => kind.as_str().replace(' ', "_"),
987        TypeKind::String {
988            width: CharWidth::Char,
989            bound,
990        } => format!("string{}", bound_tag(ast, bound)),
991        TypeKind::String {
992            width: CharWidth::WChar,
993            bound,
994        } => format!("wstring{}", bound_tag(ast, bound)),
995        TypeKind::Fixed { digits, scale } => format!("fixed_{digits}_{scale}"),
996        TypeKind::Sequence { element, bound } => match element.resolved() {
997            Some(element) => format!(
998                "seq{}_{}",
999                bound_tag(ast, bound),
1000                codec_tag(ast, element, depth + 1)
1001            ),
1002            None => format!("seq_unknown{}", bound_tag(ast, bound)),
1003        },
1004        TypeKind::Array { element, dims } => {
1005            let mut name = match element.resolved() {
1006                Some(element) => format!("arr_{}", codec_tag(ast, element, depth + 1)),
1007                None => "arr_unknown".to_string(),
1008            };
1009            for dim in dims {
1010                match ast.eval_integral(*dim) {
1011                    Ok(length) => name.push_str(&format!("_{length}")),
1012                    // No number rather than a zero: the name has to
1013                    // differ from a `[0]` array's.
1014                    Err(_) => name.push_str("_x"),
1015                }
1016            }
1017            name
1018        }
1019        // The entry struct's own name carries no bound — two maps that
1020        // differ only in bound hold the same pairs — but the tag does,
1021        // for the routine-name reason in [`bound_tag`].
1022        TypeKind::Map { bound, .. } => {
1023            {
1024                // The collection's bound goes in front of the entry name:
1025                // after it, a bound on the map and one on its value would
1026                // end in the same suffix.
1027                let entry = julia_map_entry_name(ast, id);
1028                match bound_tag(ast, bound) {
1029                    bound if bound.is_empty() => entry,
1030                    bound => format!("map{bound}_{entry}"),
1031                }
1032            }
1033        }
1034        TypeKind::Alias { aliased } => match aliased.resolved() {
1035            Some(target) => codec_tag(ast, target, depth + 1),
1036            None => julia_type_name(ast, id),
1037        },
1038        _ => julia_type_name(ast, id),
1039    }
1040}