Skip to main content

zerodds_lang_rules/emit/
ocaml.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The OCaml 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/go` and the rest do, and none of them
7//! calls another: `runtime/ocaml/zdwire.ml` is a further reading of the
8//! specification, and this file only spells the walk's decisions in
9//! OCaml.
10//!
11//! # The five decisions this column turns on
12//!
13//! **There are no statements, so a body is a `unit` expression.** Every
14//! [`Stmt`] this backend builds is rendered by
15//! `zerodds_lang_render::ocaml::codec_chain` under `;` sequencing, a
16//! local is a `ref` cell, and the two control transfers C spells
17//! `return` and `break` are the runtime's `Zd_return` and `Zd_break`
18//! exceptions — OCaml has neither keyword.
19//!
20//! **There are no implicit integer conversions, so kinds are data.**
21//! `adapter/ocaml.rs` maps `long` onto `int32`, `unsigned long` and
22//! `long long` onto `int64`, everything of at most 16 bits onto `int`,
23//! and `unsigned long long` onto decimal text. A `+` is `Int64.add` on
24//! one pair and `+` on the next, and only the renderer's typing
25//! environment can say which — so every [`CodecType`] carries a
26//! [`ValueKind`] and the conversion sites carry [`CodeExpr::Convert`]
27//! rather than a spelled cast.
28//!
29//! **Records are immutable, so a member store is a functional update.**
30//! The reader's sample lives in a `ref` cell seeded from the Table 9
31//! defaulter, and `v.x = e` becomes `v := { !v with x = e }`. Arrays
32//! are the one mutable shape OCaml gives this column, and every
33//! collection slot is stored with `<-`.
34//!
35//! **An enumeration is a variant, so its number lives in a function.**
36//! The adapter emits `int32_of_<name>` and `<name>_of_int32` beside
37//! every enumeration, and this backend reaches the numbering through
38//! them — a cast of a variant to `Int64` is
39//! `Int64.of_int32 (int32_of_<name> v)`, resolved by kind.
40//!
41//! **A union branch is an `option` field.** The value of a selected
42//! branch is `Option.get v.f` on the way out and `Some …` on the way
43//! in, which is [`CodeExpr::Member`]'s `case` flag; a discriminator
44//! that selects no branch leaves every branch `None`, which is the
45//! record shape `adapter/ocaml.rs` argues for.
46
47use super::{CodecTarget, Constant, Handle, Handoff, Routine, Scalar};
48use crate::adapter::ocaml::{
49    ocaml_enumerator_name, ocaml_map_entry_name, ocaml_type_name, ocaml_value_type,
50};
51use crate::encode::EncodeError;
52use zerodds_ast::{Ast, CharWidth, MemberNode, PrimitiveKind, TypeKind, TypeNode};
53use zerodds_ast_core::Id;
54use zerodds_lang_cst::common::{Doc, Literal};
55use zerodds_lang_cst::ocaml::{self, CodeExpr, CodecFn, CodecType, Stmt, ValueKind};
56
57/// The OCaml backend.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub struct OCaml;
60
61/// How deep a chain of typedefs and collections is followed.
62const MAX_TYPE_DEPTH: usize = 64;
63
64impl OCaml {
65    fn type_(text: &str, kind: ValueKind, default: &str) -> CodecType {
66        CodecType {
67            text: text.to_string(),
68            kind,
69            default: default.to_string(),
70        }
71    }
72
73    fn constant_text(text: &str, kind: ValueKind) -> CodeExpr {
74        CodeExpr::RuntimeConst {
75            text: text.to_string(),
76            kind,
77        }
78    }
79
80    fn policy(text: &str) -> CodeExpr {
81        CodeExpr::TextLit(text.to_string())
82    }
83}
84
85/// The written OCaml type of the adapter's tree — a local copy of the
86/// renderer's `type_text`, because this crate does not depend on
87/// `zerodds-lang-render` and the postfix grammar is four arms.
88pub fn ocaml_type_text(node: &ocaml::Type) -> String {
89    match node {
90        ocaml::Type::Primitive(primitive) => primitive.as_str().to_string(),
91        ocaml::Type::Named(name) => name.as_str().to_string(),
92        ocaml::Type::Array(element) => format!("{} array", ocaml_type_text(element)),
93        ocaml::Type::Option(inner) => format!("{} option", ocaml_type_text(inner)),
94        ocaml::Type::Tuple(parts) => {
95            let rendered: Vec<String> = parts.iter().map(ocaml_type_text).collect();
96            format!("({})", rendered.join(" * "))
97        }
98        ocaml::Type::Qualified { module, name } => {
99            let mut out = String::new();
100            for segment in module {
101                out.push_str(segment.as_str());
102                out.push('.');
103            }
104            out.push_str(name.as_str());
105            out
106        }
107    }
108}
109
110/// The [`ValueKind`] a value of `id` has in this column, following
111/// typedefs — the memory-shape table of `adapter/ocaml.rs`, reduced to
112/// what conversions care about.
113///
114/// zerodds-lint: recursion-depth 64 (alias chains; bounded by `MAX_TYPE_DEPTH`)
115pub fn kind_of(ast: &Ast, id: Id<TypeNode>, depth: usize) -> ValueKind {
116    if depth > MAX_TYPE_DEPTH {
117        return ValueKind::Aggregate;
118    }
119    let Ok(node) = ast.type_(id) else {
120        return ValueKind::Aggregate;
121    };
122    match &node.kind {
123        TypeKind::Primitive(kind) => match kind {
124            PrimitiveKind::Boolean => ValueKind::Bool,
125            PrimitiveKind::Octet
126            | PrimitiveKind::UInt8
127            | PrimitiveKind::Char
128            | PrimitiveKind::Int8
129            | PrimitiveKind::Int16
130            | PrimitiveKind::UInt16
131            | PrimitiveKind::WChar => ValueKind::Int,
132            PrimitiveKind::Int32 => ValueKind::Int32,
133            PrimitiveKind::UInt32 | PrimitiveKind::Int64 => ValueKind::Int64,
134            PrimitiveKind::UInt64 | PrimitiveKind::Float128 => ValueKind::Text,
135            PrimitiveKind::Float32 | PrimitiveKind::Float64 => ValueKind::Float,
136        },
137        TypeKind::String { .. } | TypeKind::Fixed { .. } => ValueKind::Text,
138        TypeKind::Enumeration { .. } => ValueKind::Enum(ocaml_type_name(ast, id)),
139        TypeKind::Bitmask { .. } => {
140            if ast.effective_bit_bound(id) <= 32 {
141                ValueKind::Int32
142            } else {
143                ValueKind::Int64
144            }
145        }
146        TypeKind::Alias { aliased } => aliased.resolved().map_or(ValueKind::Aggregate, |target| {
147            kind_of(ast, target, depth + 1)
148        }),
149        // The structural kinds carry their parts, so the renderer can
150        // type a member access without the model in hand. Capped low —
151        // a codec body reaches at most a few accessors deep from `v`,
152        // and a recursive type's kind tree would otherwise grow with
153        // the alias bound rather than with what any body reads.
154        _ if depth > 8 => ValueKind::Aggregate,
155        TypeKind::Sequence { element, .. } => {
156            element.resolved().map_or(ValueKind::Aggregate, |target| {
157                ValueKind::ArrayOf(Box::new(kind_of(ast, target, depth + 1)))
158            })
159        }
160        TypeKind::Array { element, dims } => match element.resolved() {
161            Some(target) => {
162                let mut kind = kind_of(ast, target, depth + 1);
163                for _ in dims {
164                    kind = ValueKind::ArrayOf(Box::new(kind));
165                }
166                kind
167            }
168            None => ValueKind::Aggregate,
169        },
170        TypeKind::Map { key, value, .. } => {
171            let half = |half: Option<Id<TypeNode>>| {
172                half.map_or(ValueKind::Aggregate, |target| {
173                    kind_of(ast, target, depth + 1)
174                })
175            };
176            ValueKind::ArrayOf(Box::new(ValueKind::RecordOf(vec![
177                ("key".to_string(), half(key.resolved())),
178                ("value".to_string(), half(value.resolved())),
179            ])))
180        }
181        TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
182            let mut fields = Vec::new();
183            // The base is the first member `parent` (XTypes 1.3 §7.5.1.4.1.3).
184            if let Some(base) = crate::adapter::c::struct_base(ast, id) {
185                fields.push((ocaml::lower_ident("parent"), kind_of(ast, base, depth + 1)));
186            }
187            for member in ast.members_of(id).iter().copied() {
188                let Ok(member_node) = ast.member(member) else {
189                    continue;
190                };
191                let Some(target) = member_node.type_.resolved() else {
192                    continue;
193                };
194                let mut kind = kind_of(ast, target, depth + 1);
195                if member_node.facets.is_optional() {
196                    kind = ValueKind::OptionOf(Box::new(kind));
197                }
198                fields.push((ocaml::lower_ident(&member_node.name), kind));
199            }
200            ValueKind::RecordOf(fields)
201        }
202        TypeKind::Union { discriminator, .. } => {
203            let mut fields = Vec::new();
204            if let Some(disc) = discriminator.resolved() {
205                fields.push(("_d".to_string(), kind_of(ast, disc, depth + 1)));
206            }
207            for case in ast.cases_of(id) {
208                let Ok(case_node) = ast.case(*case) else {
209                    continue;
210                };
211                let Ok(member) = ast.member(case_node.member) else {
212                    continue;
213                };
214                let Some(target) = member.type_.resolved() else {
215                    continue;
216                };
217                fields.push((
218                    ocaml::lower_ident(&member.name),
219                    ValueKind::OptionOf(Box::new(kind_of(ast, target, depth + 1))),
220                ));
221            }
222            ValueKind::RecordOf(fields)
223        }
224        TypeKind::Bitset { .. } => {
225            let mut fields = Vec::new();
226            for field in &ast.all_bitfields_of(id) {
227                let Ok(field_node) = ast.bitfield(*field) else {
228                    continue;
229                };
230                let Some(field_name) = field_node.name.as_ref() else {
231                    continue;
232                };
233                let kind = match crate::adapter::ocaml::ocaml_holder(field_node.bits) {
234                    ocaml::Primitive::Int32 => ValueKind::Int32,
235                    ocaml::Primitive::Int64 => ValueKind::Int64,
236                    _ => ValueKind::Int,
237                };
238                fields.push((ocaml::lower_ident(field_name), kind));
239            }
240            ValueKind::RecordOf(fields)
241        }
242        _ => ValueKind::Aggregate,
243    }
244}
245
246/// The placeholder expression a value of `id` starts as — a literal
247/// zero for the scalar kinds, and the generated Table 9 defaulter for
248/// everything with structure. What a `ref` cell or an `Array.make`
249/// slot holds until the decoder overwrites it; never observed by a
250/// completed read.
251pub fn default_of(ast: &Ast, id: Id<TypeNode>) -> String {
252    match kind_of(ast, id, 0) {
253        ValueKind::Bool => "false".to_string(),
254        ValueKind::Int => "0".to_string(),
255        ValueKind::Int32 => "0l".to_string(),
256        ValueKind::Int64 => "0L".to_string(),
257        ValueKind::Float => "0.0".to_string(),
258        ValueKind::Text => "\"\"".to_string(),
259        _ => format!("{} ()", crate::default_ocaml::defaulter_name(ast, id)),
260    }
261}
262
263impl CodecTarget for OCaml {
264    type Expr = CodeExpr;
265    type Stmt = Stmt;
266    type Block = Vec<Stmt>;
267    type Type = CodecType;
268    type Decl = CodecFn;
269
270    // -- types --------------------------------------------------------
271
272    /// The OCaml type each scalar local takes. `Index` and `Size` are
273    /// `int` — a counter and a stream position — and so is `UInt32`,
274    /// because every length local the walks declare with it holds a
275    /// value an OCaml `int` carries exactly on the 64-bit targets this
276    /// column requires. The 64-bit widths are `int64`, matching the
277    /// member table; the decimal holders are `string`.
278    fn scalar(kind: Scalar) -> Self::Type {
279        match kind {
280            Scalar::Bool => Self::type_("bool", ValueKind::Bool, "false"),
281            Scalar::Char
282            | Scalar::Int8
283            | Scalar::UInt8
284            | Scalar::Int16
285            | Scalar::UInt16
286            | Scalar::UInt32
287            | Scalar::Index
288            | Scalar::Size => Self::type_("int", ValueKind::Int, "0"),
289            Scalar::Int32 => Self::type_("int32", ValueKind::Int32, "0l"),
290            Scalar::Int64 => Self::type_("int64", ValueKind::Int64, "0L"),
291            // The same OCaml type, under the runtime's alias for it, so
292            // that a widening the walk asks for as unsigned keeps the
293            // bit pattern where one asked for as signed keeps the sign.
294            // `zerodds_lang_cst::ocaml::UNSIGNED_HOLDER` carries why.
295            Scalar::UInt64 => Self::type_(ocaml::UNSIGNED_HOLDER, ValueKind::Int64, "0L"),
296            Scalar::Float | Scalar::Double => Self::type_("float", ValueKind::Float, "0.0"),
297            Scalar::LongDouble => Self::type_("string", ValueKind::Text, "\"0.0\""),
298        }
299    }
300
301    /// A named type; only the `signed char` marker ever reaches a
302    /// rendered conversion, and a default that is asked for renders as
303    /// an unbound name so `ocamlc` reports the site.
304    fn named(name: &str) -> Self::Type {
305        Self::type_(name, ValueKind::Aggregate, "zd_unsupported_default")
306    }
307
308    fn nothing() -> Self::Type {
309        Self::type_("unit", ValueKind::UnitValue, "()")
310    }
311
312    /// The runtime's handles, with the inert value each `ref` cell
313    /// starts from.
314    fn handle(kind: Handle) -> Self::Type {
315        match kind {
316            Handle::DHeader => Self::type_(
317                "Zdwire.dheader",
318                ValueKind::Handle("dheader"),
319                "Zdwire.no_dheader ()",
320            ),
321            Handle::EmHeader => Self::type_(
322                "Zdwire.emheader",
323                ValueKind::Handle("emheader"),
324                "Zdwire.emheader_none",
325            ),
326            Handle::Pid => Self::type_("Zdwire.pid", ValueKind::Handle("pid"), "Zdwire.pid_none"),
327            Handle::TryConstruct => Self::type_(
328                "Zdwire.try_construct",
329                ValueKind::Handle("try_construct"),
330                "Zdwire.Discard",
331            ),
332            Handle::ReaderMark => Self::type_(
333                "Zdwire.reader_mark",
334                ValueKind::Handle("reader_mark"),
335                "Zdwire.mark_none",
336            ),
337            Handle::PidRead => Self::type_(
338                "Zdwire.pid_read",
339                ValueKind::Handle("pid_read"),
340                "Zdwire.pid_read_none",
341            ),
342            Handle::EmHeaderRead => Self::type_(
343                "Zdwire.emheader_read",
344                ValueKind::Handle("emheader_read"),
345                "Zdwire.emheader_read_none",
346            ),
347        }
348    }
349
350    /// The member already holds the code point as an `int`, so there is
351    /// nothing to convert.
352    fn code_point(value: Self::Expr, _narrow: bool) -> Self::Expr {
353        value
354    }
355
356    /// The runtime's policy values. Strings and integers where
357    /// `runtime/ocaml` takes them so — the spellings are
358    /// `WstringLengthPolicy::as_str` and its siblings in
359    /// `crates/ast-layout/src/key.rs` — and variant constructors for
360    /// the enumerated kinds. One arm per constant and no catch-all.
361    fn constant(which: Constant) -> Self::Expr {
362        match which {
363            Constant::WcharWidth2 => CodeExpr::Int(2),
364            Constant::WcharWidth4 => CodeExpr::Int(4),
365            Constant::WcharFixed => Self::policy("fixed"),
366            Constant::WcharGiopLengthPrefixed => Self::policy("giop-length-prefixed"),
367            Constant::WstringUnitsStream => Self::policy("stream"),
368            Constant::WstringUnitsBigEndianAlways => Self::policy("big-endian-always"),
369            Constant::WstringUnitsByteOrderMark => Self::policy("byte-order-mark"),
370            Constant::WstringUnitsHostUnswapped => Self::policy("host-unswapped"),
371            Constant::WstringSpecification => Self::policy("specification"),
372            Constant::WstringCharacterCount => Self::policy("character-count"),
373            Constant::WstringCharacterCountTerminated => Self::policy("character-count-terminated"),
374            Constant::WstringSpecificationWideUnits => Self::policy("specification-wide-units"),
375            Constant::WstringRosTypeSupport => Self::policy("ros-type-support"),
376            Constant::WstringOctetsTerminated2 => Self::policy("octets/terminated/2"),
377            Constant::WstringOctetsTerminated4 => Self::policy("octets/terminated/4"),
378            Constant::WstringCharactersTerminated2 => Self::policy("characters/terminated/2"),
379            Constant::FixedDeclared => Self::policy("declared"),
380            Constant::FixedValuePrecision => Self::policy("value-precision"),
381            Constant::FixedValueStringDigits => Self::policy("value-string-digits"),
382            Constant::ParameterMember => {
383                Self::constant_text("Zdwire.Role_member", ValueKind::Handle("role"))
384            }
385            Constant::ParameterOptional => {
386                Self::constant_text("Zdwire.Role_optional", ValueKind::Handle("role"))
387            }
388            Constant::TryConstructDiscard => {
389                Self::constant_text("Zdwire.Discard", ValueKind::Handle("try_construct"))
390            }
391            Constant::TryConstructTrim => {
392                Self::constant_text("Zdwire.Trim", ValueKind::Handle("try_construct"))
393            }
394            Constant::TryConstructUseDefault => {
395                Self::constant_text("Zdwire.Use_default", ValueKind::Handle("try_construct"))
396            }
397            Constant::StatusOk => Self::constant_text("Zdwire.St_ok", ValueKind::Handle("status")),
398            Constant::StatusDiscarded => {
399                Self::constant_text("Zdwire.St_discarded", ValueKind::Handle("status"))
400            }
401            Constant::StatusImpossibleLength => {
402                Self::constant_text("Zdwire.St_impossible_length", ValueKind::Handle("status"))
403            }
404            Constant::StatusOutOfRange => {
405                Self::constant_text("Zdwire.St_out_of_range", ValueKind::Handle("status"))
406            }
407            Constant::StatusTruncated => {
408                Self::constant_text("Zdwire.St_truncated", ValueKind::Handle("status"))
409            }
410        }
411    }
412
413    /// Whether `left` is less than `right` as unsigned 64-bit values.
414    /// The keys arrive as their decimal holders, and the runtime's
415    /// `u64_less` spells `Int64.unsigned_compare` over their patterns.
416    fn unsigned_less(_octets: u8, left: Self::Expr, right: Self::Expr) -> Self::Expr {
417        CodeExpr::Static {
418            method: "u64_less".to_string(),
419            args: vec![left, right],
420        }
421    }
422
423    // `bool_less` keeps the default `<`: OCaml's polymorphic order has
424    // `false < true`, measured in the toplevel before this column was
425    // written.
426
427    /// `runtime/ocaml` carries the three HDDS axes: `begin_packed_element`
428    /// / `finish_packed_element` on the writer and
429    /// `reader_begin_packed_element` / `reader_finish_packed_element` on the
430    /// reader, the DHEADER pair for a sequence element's own length, and
431    /// `write_wstring_utf8_octets` / `read_wstring_utf8_octets`.
432    fn carries_hdds_axes() -> bool {
433        true
434    }
435
436    /// The holder a bit set is assembled in: the adapter's own answer.
437    fn unsigned_holder(bits: u16) -> Self::Type {
438        match crate::adapter::ocaml::ocaml_holder(bits) {
439            ocaml::Primitive::Int32 => Self::type_("int32", ValueKind::Int32, "0l"),
440            ocaml::Primitive::Int64 => Self::type_("int64", ValueKind::Int64, "0L"),
441            _ => Self::type_("int", ValueKind::Int, "0"),
442        }
443    }
444
445    fn value_type(ast: &Ast, id: Id<TypeNode>) -> Self::Type {
446        let text = match ocaml_value_type(ast, id) {
447            Ok(type_) => ocaml_type_text(&type_),
448            // A type with no OCaml form was refused by the materialiser
449            // before a codec walk ran; the unbound name makes `ocamlc`
450            // fail loudly rather than compile a plausible member.
451            Err(_) => "zd_no_ocaml_form".to_string(),
452        };
453        CodecType {
454            text,
455            kind: kind_of(ast, id, 0),
456            default: default_of(ast, id),
457        }
458    }
459
460    fn type_name(ast: &Ast, id: Id<TypeNode>) -> String {
461        type_tag(ast, id, 0)
462    }
463
464    fn map_entry_name(ast: &Ast, id: Id<TypeNode>) -> String {
465        ocaml_map_entry_name(ast, id)
466    }
467
468    /// The entry record's type, with an inline default the reader's
469    /// `Array.make` can replicate: the slots are replaced by functional
470    /// update, so one shared record value is sound.
471    fn map_entry_type(ast: &Ast, id: Id<TypeNode>) -> Self::Type {
472        let name = ocaml_map_entry_name(ast, id);
473        let default = match ast.type_(id).map(|node| node.kind.clone()) {
474            Ok(TypeKind::Map { key, value, .. }) => match (key.resolved(), value.resolved()) {
475                (Some(key), Some(value)) => format!(
476                    "({{ key = {}; value = {} }} : {name})",
477                    default_of(ast, key),
478                    default_of(ast, value)
479                ),
480                _ => "zd_unsupported_default".to_string(),
481            },
482            _ => "zd_unsupported_default".to_string(),
483        };
484        CodecType {
485            text: name,
486            kind: ValueKind::Aggregate,
487            default,
488        }
489    }
490
491    /// Nothing: [`Self::enumerator_constant`] is overridden and no
492    /// other seam reads a prefix in this column.
493    fn module_prefix(_ast: &Ast, _id: Id<TypeNode>) -> String {
494        String::new()
495    }
496
497    fn escape(name: &str) -> String {
498        ocaml::lower_ident(name)
499    }
500
501    /// The base is the first member `parent` (XTypes 1.3 §7.5.1.4.1.3).
502    fn inherited_path(depth: usize) -> Option<String> {
503        Some(format!("{}.", ocaml::lower_ident("parent")).repeat(depth))
504    }
505
506    // -- expressions ---------------------------------------------------
507
508    fn name(text: &str) -> Self::Expr {
509        CodeExpr::Name(text.to_string())
510    }
511
512    fn int(value: i128) -> Self::Expr {
513        CodeExpr::Int(value)
514    }
515
516    /// A widthless literal; the renderer spells it in the width of
517    /// whatever it meets, which is what OCaml's suffixed literals need.
518    fn wide_int(value: i128) -> Self::Expr {
519        CodeExpr::Int(value)
520    }
521
522    /// The number with no suffix: `5u` is not an OCaml token.
523    fn unsigned_int(value: i128) -> Self::Expr {
524        CodeExpr::Int(value)
525    }
526
527    /// The 64-bit pattern as an `int64` literal — the sign is the top
528    /// bit's, because OCaml has no unsigned integer to spell it in.
529    #[allow(clippy::cast_possible_wrap)]
530    fn wide_unsigned(value: u64) -> Self::Expr {
531        CodeExpr::Int64Lit(value as i64)
532    }
533
534    #[allow(clippy::cast_possible_truncation)]
535    fn wide_signed(value: i128) -> Self::Expr {
536        CodeExpr::Int64Lit(value as i64)
537    }
538
539    /// The two raw tokens the walk escapes through [`Literal::Int`],
540    /// rewritten into what OCaml spells them as.
541    fn literal(value: Literal) -> Self::Expr {
542        match value {
543            Literal::Int(text) => match text.as_str() {
544                "true" => CodeExpr::BoolLit(true),
545                "false" => CodeExpr::BoolLit(false),
546                other => other
547                    .parse::<i128>()
548                    .map_or_else(|_| CodeExpr::TextLit(other.to_string()), CodeExpr::Int),
549            },
550            Literal::Bool(flag) => CodeExpr::BoolLit(flag),
551            Literal::Str(text) => CodeExpr::TextLit(text),
552            Literal::Float(text) => CodeExpr::FloatLit(text),
553            Literal::Char(ch) => CodeExpr::Int(i128::from(u32::from(ch))),
554            Literal::Bytes(_) | Literal::Null => CodeExpr::Absent,
555        }
556    }
557
558    /// A call to a generated function; `strcmp` is the one name the
559    /// walk hands here that is the runtime's.
560    fn call(callee: &str, args: Vec<Self::Expr>) -> Self::Expr {
561        if callee == "strcmp" {
562            return CodeExpr::Static {
563                method: "strcmp".to_string(),
564                args,
565            };
566        }
567        CodeExpr::Call {
568            callee: callee.to_string(),
569            args,
570        }
571    }
572
573    /// `Zdwire.<method> <receiver> <args>` — the receiver-first shape a
574    /// language without methods gives an operation on the writer or the
575    /// reader.
576    fn runtime(receiver: &str, method: &str, args: Vec<Self::Expr>) -> Self::Expr {
577        // The walk's bare `read_pid()` reads a header *and* opens its
578        // value block — `read_pid_as` in this runtime, the same rewrite
579        // the Python and Go backends make and for the same §7.4.1.2
580        // reason.
581        if method == "read_pid" && args.is_empty() {
582            return Self::runtime(
583                receiver,
584                "read_pid_as",
585                vec![Self::constant(Constant::ParameterMember)],
586            );
587        }
588        let receiver = if receiver == "r" { "r" } else { "w" };
589        CodeExpr::Runtime {
590            receiver,
591            method: method.to_string(),
592            args,
593        }
594    }
595
596    /// A free function of the runtime.
597    fn runtime_static(method: &str, args: Vec<Self::Expr>) -> Self::Expr {
598        CodeExpr::Static {
599            method: method.to_string(),
600            args,
601        }
602    }
603
604    fn member(base: &str, field: &str) -> Self::Expr {
605        CodeExpr::Member {
606            base: base.to_string(),
607            field: field.to_string(),
608            case: false,
609        }
610    }
611
612    fn whole(base: &str) -> Self::Expr {
613        CodeExpr::Whole(base.to_string())
614    }
615
616    fn field_of(value: Self::Expr, field: &str) -> Self::Expr {
617        CodeExpr::HeaderField {
618            value: Box::new(value),
619            field: field.to_string(),
620        }
621    }
622
623    fn index(value: Self::Expr, at: Self::Expr) -> Self::Expr {
624        CodeExpr::Index {
625            base: Box::new(value),
626            index: Box::new(at),
627        }
628    }
629
630    fn count_of(base: &str, fixed: Option<u64>) -> Self::Expr {
631        match fixed {
632            Some(bound) => CodeExpr::Int(i128::from(bound)),
633            None => CodeExpr::CountOf(base.to_string()),
634        }
635    }
636
637    fn elements(base: &str) -> Self::Expr {
638        CodeExpr::Elements(base.to_string())
639    }
640
641    /// A union branch, whose field is an `option`: `Option.get` as a
642    /// value, `Some` as a destination.
643    fn case_member(base: &str, field: &str) -> Self::Expr {
644        CodeExpr::Member {
645            base: base.to_string(),
646            field: field.to_string(),
647            case: true,
648        }
649    }
650
651    fn entry_half(entry: Self::Expr, key: bool) -> Self::Expr {
652        CodeExpr::EntryHalf {
653            entry: Box::new(entry),
654            key,
655        }
656    }
657
658    fn absent() -> Self::Expr {
659        CodeExpr::Absent
660    }
661
662    /// Nothing: an OCaml string owns its storage and a read that could
663    /// not allocate has already raised.
664    fn materialised(_value: Self::Expr) -> Option<Self::Expr> {
665        None
666    }
667
668    /// `Option.get v.f` — only ever reached inside a branch the
669    /// presence test guarded.
670    fn optional_value(_ast: &Ast, _id: Id<TypeNode>, base: &str, field: &str) -> Self::Expr {
671        CodeExpr::OptionGet(Box::new(Self::member(base, field)))
672    }
673
674    /// `v.f <> None`.
675    fn present(base: &str, field: &str) -> Self::Expr {
676        CodeExpr::IsSome {
677            base: base.to_string(),
678            field: field.to_string(),
679        }
680    }
681
682    /// Nothing on the way in — the `Some` store is what makes the
683    /// member present — and `None` on the way out.
684    fn set_present(base: &str, field: &str, present: bool) -> Vec<Self::Stmt> {
685        if present {
686            return Vec::new();
687        }
688        vec![Stmt::Assign {
689            place: Self::member(base, field),
690            value: CodeExpr::Absent,
691        }]
692    }
693
694    /// `None` in every case: a branch field is an `option`, `None` unless
695    /// the discriminator selects it. The same statement as an absent
696    /// `@optional` member, [`Self::set_present`] with `false`.
697    fn clear_union_cases(base: &str, fields: &[String]) -> Vec<Self::Stmt> {
698        fields
699            .iter()
700            .flat_map(|field| Self::set_present(base, field, false))
701            .collect()
702    }
703
704    /// The marked slot; the store wraps the decoded value in `Some`.
705    fn optional_destination(base: &str, field: &str) -> (Vec<Self::Stmt>, Self::Expr) {
706        (
707            Vec::new(),
708            CodeExpr::OptionSlot {
709                base: base.to_string(),
710                field: field.to_string(),
711            },
712        )
713    }
714
715    fn binary(op: &'static str, lhs: Self::Expr, rhs: Self::Expr) -> Self::Expr {
716        CodeExpr::Binary {
717            op,
718            lhs: Box::new(lhs),
719            rhs: Box::new(rhs),
720        }
721    }
722
723    fn unary(op: &'static str, value: Self::Expr) -> Self::Expr {
724        CodeExpr::Unary {
725            op,
726            value: Box::new(value),
727        }
728    }
729
730    fn paren(value: Self::Expr) -> Self::Expr {
731        CodeExpr::Paren(Box::new(value))
732    }
733
734    /// A conversion, deferred: the renderer resolves it from the
735    /// operand's kind, because OCaml's conversions are functions and
736    /// which function depends on both ends.
737    fn cast(type_: Self::Type, value: Self::Expr) -> Self::Expr {
738        CodeExpr::Convert {
739            target: type_,
740            value: Box::new(value),
741        }
742    }
743
744    /// The value, unchanged: OCaml passes every value as itself.
745    fn handoff(value: Self::Expr, _form: Handoff) -> Self::Expr {
746        value
747    }
748
749    // -- statements ----------------------------------------------------
750
751    /// A `ref` cell: OCaml's one shape for a local the body assigns.
752    fn declare(type_: Self::Type, name: &str, value: Option<Self::Expr>) -> Self::Stmt {
753        Stmt::Let {
754            type_,
755            name: name.to_string(),
756            value,
757        }
758    }
759
760    /// An expression evaluated for its effect — or, where the walk
761    /// built an assignment, the assignment: this is the only place the
762    /// walk's `=` arrives.
763    fn perform(value: Self::Expr) -> Self::Stmt {
764        if let CodeExpr::Binary { op: "=", lhs, rhs } = value {
765            return Stmt::Assign {
766                place: *lhs,
767                value: *rhs,
768            };
769        }
770        Stmt::Perform(value)
771    }
772
773    /// `raise Zdwire.Zd_return`: OCaml has no `return`, and the codec
774    /// function's own `try` is the landing site. The reader's `!v`
775    /// after it is what goes back.
776    fn give_back(_value: Option<Self::Expr>) -> Self::Stmt {
777        Stmt::Return
778    }
779
780    fn when(
781        condition: Self::Expr,
782        then: Self::Block,
783        otherwise: Option<Self::Block>,
784    ) -> Self::Stmt {
785        Stmt::When {
786            condition,
787            then,
788            otherwise: otherwise.filter(|body| !body.is_empty()),
789        }
790    }
791
792    fn count_up(counter: &str, bound: Self::Expr, body: Self::Block) -> Self::Stmt {
793        Stmt::For {
794            counter: counter.to_string(),
795            bound,
796            body,
797        }
798    }
799
800    fn until(condition: Self::Expr, body: Self::Block) -> Self::Stmt {
801        Stmt::While { condition, body }
802    }
803
804    /// `raise Zdwire.Zd_break`: OCaml has no `break`, and every
805    /// generated loop is rendered inside its own handler.
806    fn leave() -> Self::Stmt {
807        Stmt::Leave
808    }
809
810    fn note(doc: Doc) -> Self::Stmt {
811        Stmt::Note(doc)
812    }
813
814    /// `ignore e` — a value read for its effect says so.
815    fn drop_value(value: Self::Expr) -> Self::Stmt {
816        Stmt::Discard(value)
817    }
818
819    /// `ignore <name>`: quiet under every warning set, and honest about
820    /// a parameter or a latch nothing else reads.
821    fn mark_used(name: &str) -> Vec<Self::Stmt> {
822        vec![Stmt::Discard(CodeExpr::Name(name.to_string()))]
823    }
824
825    /// `place <- zd_dec_T r behaviour` — this column's decoders answer
826    /// with the value, and the destination decides its own store shape.
827    fn read_call(
828        callee: &str,
829        place: Self::Expr,
830        _form: Handoff,
831        behaviour: Self::Expr,
832    ) -> Self::Stmt {
833        Stmt::ReadInto {
834            callee: callee.to_string(),
835            place,
836            behaviour,
837        }
838    }
839
840    /// `v := zd_dec_T r zd_tc` — a typedef, handed straight on.
841    fn read_forward(callee: &str) -> Self::Stmt {
842        Stmt::ReadInto {
843            callee: callee.to_string(),
844            place: CodeExpr::Whole("v".to_string()),
845            behaviour: CodeExpr::Name("zd_tc".to_string()),
846        }
847    }
848
849    fn block(statements: Vec<Self::Stmt>) -> Self::Block {
850        statements
851    }
852
853    fn into_statements(block: Self::Block) -> Vec<Self::Stmt> {
854        block
855    }
856
857    /// `base := Array.make n <element default>`, and the fill after it.
858    /// The slots hold one shared placeholder until the fill replaces
859    /// them — sound for a whole-slot store, and sound for a map's
860    /// half-at-a-time store too, because a half store is a functional
861    /// update that makes the slot its own record.
862    fn make_room(
863        base: &str,
864        element: Self::Type,
865        count: Self::Expr,
866        _assigned_whole: bool,
867        fill: Vec<Self::Stmt>,
868    ) -> Vec<Self::Stmt> {
869        let mut out = vec![Stmt::Alloc {
870            base: base.to_string(),
871            element,
872            count,
873        }];
874        out.extend(fill);
875        out
876    }
877
878    /// `base := Array.sub !base 0 n` — §7.3.1.2.1.12.3's TRIM.
879    fn keep_first(base: &str, count: Self::Expr) -> Vec<Self::Stmt> {
880        vec![Stmt::KeepFirst {
881            base: base.to_string(),
882            count,
883        }]
884    }
885
886    /// The subscript, with nothing around it: an OCaml array element is
887    /// a real place at every element type.
888    fn sequence_element(
889        base: &str,
890        index: Self::Expr,
891        _element: Self::Type,
892    ) -> (Vec<Self::Stmt>, Self::Expr, Vec<Self::Stmt>) {
893        (
894            Vec::new(),
895            Self::index(Self::elements(base), index),
896            Vec::new(),
897        )
898    }
899
900    /// The map's scratch entry is not declared at all: the counting
901    /// pass only consumes the stream, so every store into it renders as
902    /// `ignore`. Any other storage request is an ordinary cell.
903    fn storage(type_: Self::Type, name: &str) -> Self::Stmt {
904        if type_.text.starts_with("zd_map_entry_") {
905            return Stmt::Scratch {
906                name: name.to_string(),
907            };
908        }
909        Stmt::Let {
910            type_,
911            name: name.to_string(),
912            value: None,
913        }
914    }
915
916    // -- assembly ------------------------------------------------------
917
918    /// One function of the generated `let rec … and …` chain. A forward
919    /// declaration is answered with nothing: the chain resolves every
920    /// name in it, whatever order the walk visited the types in.
921    fn routine(spec: Routine<Self>) -> Option<Self::Decl> {
922        let body = spec.body?;
923        let defaulter = if spec.writing {
924            None
925        } else {
926            Some(crate::default_ocaml::defaulter_from_decoder(&spec.name))
927        };
928        Some(CodecFn {
929            doc: spec.doc,
930            name: spec.name,
931            sample: spec.sample,
932            reading: !spec.writing,
933            defaulter,
934            extra: spec.extra,
935            body,
936        })
937    }
938
939    /// A call to the generated defaulter, which is Table 9 once per
940    /// type rather than once per position.
941    ///
942    /// # Errors
943    /// Never; the signature matches the emitters around it.
944    fn default_into(
945        ast: &Ast,
946        id: Id<TypeNode>,
947        place: Self::Expr,
948    ) -> Result<Self::Stmt, EncodeError> {
949        Ok(Stmt::Assign {
950            place,
951            value: CodeExpr::Call {
952                callee: crate::default_ocaml::defaulter_name(ast, id),
953                args: Vec::new(),
954            },
955        })
956    }
957
958    /// # Errors
959    /// As [`crate::default_ocaml::member_default`]: a `@default` this
960    /// column cannot spell is refused rather than silently replaced by
961    /// Table 9's zero.
962    fn member_default(
963        ast: &Ast,
964        node: &MemberNode,
965        type_: Id<TypeNode>,
966        place: Self::Expr,
967    ) -> Result<Vec<Self::Stmt>, EncodeError> {
968        crate::default_ocaml::member_default(ast, node, type_, place)
969    }
970
971    /// The constructor, in the adapter's spelling — the one place the
972    /// numbering's names live.
973    fn enumerator_constant(ast: &Ast, id: Id<TypeNode>, enumerator: &str) -> Self::Expr {
974        CodeExpr::Construct(ocaml_enumerator_name(ast, id, enumerator))
975    }
976}
977
978/// The bound a `string`, a `sequence` or a `map` carries, as the suffix
979/// that keeps two of them apart.
980///
981/// The tag names the routine a hoisted collection's codec is emitted
982/// under, and one body is emitted per name. With the bound left out,
983/// `sequence<long>` and `sequence<long, 4>` are one name and whichever
984/// was declared first decides: measured on
985/// `@final struct S { sequence<long> b; sequence<long, 4> a; };`, the
986/// generated OCaml held no `zd_over` check at all, so `a` accepted a
987/// stream of any length; with the members the other way round both got
988/// the bounded body, so `b` refused — and truncated — a length the IDL
989/// puts no limit on. `string<4>` beside `string` and `wstring<4>`
990/// beside `wstring` collapse the same way, onto the trimming call.
991/// Either way the reader answered with a value the IDL does not
992/// describe and said nothing, which is why the bound is part of the
993/// name. The same suffix the Zig column carries.
994///
995/// An `array` needs nothing here: its `dims` are already in the tag,
996/// under the `Array` arm below.
997fn bound_tag(ast: &Ast, bound: &zerodds_ast::Bound) -> String {
998    match bound.expr() {
999        None => String::new(),
1000        // `_b` and not a bare `_n`: `seq_long_4` is what the `Array`
1001        // arm answers for an array of four `long`, and the two are
1002        // different wire forms.
1003        Some(expr) => match ast.eval_integral(expr) {
1004            Ok(limit) => format!("_b{limit}"),
1005            // As the array's `_x`: a bound that does not evaluate is
1006            // still not the unbounded form, and the materialiser
1007            // reports the expression that would not evaluate.
1008            Err(_) => "_bx".to_string(),
1009        },
1010    }
1011}
1012
1013/// The name a codec function takes for the type it codes.
1014///
1015/// The IDL kind and not the OCaml spelling, for the reason the Python
1016/// column's `type_tag` gives at length: `octet` and `wchar` are one
1017/// OCaml type here and two wire forms, and a tag built from the OCaml
1018/// type would give them one codec. A declared type takes its flattened
1019/// OCaml name, which two IDL declarations cannot share —
1020/// `adapter/ocaml.rs`'s `remember` refuses the collision first.
1021///
1022/// zerodds-lint: recursion-depth 64 (collection nesting; bounded by `MAX_TYPE_DEPTH`)
1023#[must_use]
1024pub fn type_tag(ast: &Ast, id: Id<TypeNode>, depth: usize) -> String {
1025    if depth > MAX_TYPE_DEPTH {
1026        return "unresolved".to_string();
1027    }
1028    let node = match ast.type_(id) {
1029        Ok(node) => node,
1030        Err(_) => return "unresolved".to_string(),
1031    };
1032    match &node.kind {
1033        TypeKind::Primitive(kind) => kind.as_str().replace(' ', "_"),
1034        TypeKind::String {
1035            width: CharWidth::Char,
1036            bound,
1037        } => format!("string{}", bound_tag(ast, bound)),
1038        TypeKind::String {
1039            width: CharWidth::WChar,
1040            bound,
1041        } => format!("wstring{}", bound_tag(ast, bound)),
1042        TypeKind::Fixed { digits, scale } => format!("fixed_{digits}_{scale}"),
1043        TypeKind::Sequence { element, bound } => match element.resolved() {
1044            Some(element) => format!(
1045                "seq{}_{}",
1046                bound_tag(ast, bound),
1047                type_tag(ast, element, depth + 1)
1048            ),
1049            None => format!("seq_unresolved{}", bound_tag(ast, bound)),
1050        },
1051        TypeKind::Array { element, dims } => {
1052            let mut name = match element.resolved() {
1053                Some(element) => format!("arr_{}", type_tag(ast, element, depth + 1)),
1054                None => "arr_unresolved".to_string(),
1055            };
1056            for dim in dims {
1057                match ast.eval_integral(*dim) {
1058                    Ok(length) => name.push_str(&format!("_{length}")),
1059                    Err(_) => name.push_str("_x"),
1060                }
1061            }
1062            name
1063        }
1064        TypeKind::Map { key, value, bound } => {
1065            let half = |half: Option<Id<TypeNode>>| match half {
1066                Some(target) => type_tag(ast, target, depth + 1),
1067                None => "unresolved".to_string(),
1068            };
1069            format!(
1070                "map{}_{}_{}",
1071                bound_tag(ast, bound),
1072                half(key.resolved()),
1073                half(value.resolved())
1074            )
1075        }
1076        TypeKind::Alias { aliased } => match aliased.resolved() {
1077            Some(target) => type_tag(ast, target, depth + 1),
1078            None => "unresolved".to_string(),
1079        },
1080        TypeKind::ValueBox { boxed } => match boxed.resolved() {
1081            Some(target) => format!("box_{}", type_tag(ast, target, depth + 1)),
1082            None => "box_unresolved".to_string(),
1083        },
1084        TypeKind::Structure { .. }
1085        | TypeKind::Union { .. }
1086        | TypeKind::Enumeration { .. }
1087        | TypeKind::Bitmask { .. }
1088        | TypeKind::Bitset { .. }
1089        | TypeKind::Valuetype { .. }
1090        | TypeKind::Native => ocaml_type_name(ast, id),
1091        TypeKind::Any => "any".to_string(),
1092        TypeKind::ValueBase => "valuebase".to_string(),
1093        TypeKind::ObjectRef { .. } => "objectref".to_string(),
1094        TypeKind::AnnotationType { .. } => "annotation".to_string(),
1095    }
1096}