Skip to main content

zerodds_lang_render/
ocaml.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Rendering the OCaml CST. Spec §5.
4//!
5//! One style, the one `ocamlformat` produces with its defaults: two
6//! spaces per level, a leading `|` on every variant constructor and
7//! every match arm, `(** … *)` for documentation.
8//!
9//! Two things are decided here that are not decided in the sibling
10//! renderers, and both are lexical:
11//!
12//! 1. **A comment's text is sanitised.** OCaml comments nest *and*
13//!    contain string literals: the manual's "Lexical conventions"
14//!    require a comment to hold well-formed strings, and `ocamlc`
15//!    reports `This comment contains an unterminated string literal`
16//!    for a `(* … " … *)`. A `*)` inside a comment closes it early and
17//!    a `(*` opens a nested one that then never closes. None of those
18//!    is a hazard in C, Go, Rust or Java, and a documentation comment
19//!    here is assembled from IDL text. [`comment_safe`] is where the
20//!    three are neutralised.
21//! 2. **Types are postfix and left-associative.** `int32 array option`
22//!    is an option of an array; `int32 option array` is an array of
23//!    options. Nesting therefore needs no brackets at all — except
24//!    around a tuple, which is why [`Type::Tuple`] always renders
25//!    parenthesised.
26//!
27//! Nothing else is decided. What is a group and what is a single
28//! declaration is settled in `zerodds_lang_rules::adapter::ocaml`,
29//! because it is a property of the type graph and not of the text.
30
31use crate::writer::{Writer, escape_string, verbatim_lines};
32use alloc::string::{String, ToString};
33use alloc::vec::Vec;
34use zerodds_lang_cst::common::{Doc, Ident, Literal};
35use zerodds_lang_cst::ocaml::{
36    Arm, ClassTypeDecl, Constructor, Decl, Expr, Field, File, LetDecl, Method, Pattern, Type,
37    TypeBody, TypeDecl, TypeGroup,
38};
39
40/// Render a whole file.
41#[must_use]
42pub fn file(file: &File) -> String {
43    let mut w = Writer::new("  ");
44    doc(&mut w, &file.doc);
45    for decl in &file.decls {
46        w.blank_line();
47        render_decl(&mut w, decl);
48    }
49    w.finish()
50}
51
52/// Render one declaration, for a caller that has only one.
53#[must_use]
54pub fn decl(node: &Decl) -> String {
55    let mut w = Writer::new("  ");
56    render_decl(&mut w, node);
57    w.finish()
58}
59
60/// Render one type, for a caller that has only one.
61#[must_use]
62pub fn type_(node: &Type) -> String {
63    type_text(node)
64}
65
66/// The identifier a name renders as, for a caller assembling one.
67#[must_use]
68pub fn ident(name: &Ident) -> String {
69    name.as_str().to_string()
70}
71
72fn render_decl(w: &mut Writer, node: &Decl) {
73    match node {
74        Decl::Types(group) => render_group(w, group),
75        Decl::Let(binding) => render_let(w, binding),
76        Decl::ClassType(class) => render_class_type(w, class),
77        Decl::Comment(lines) => doc(w, lines),
78        Decl::Verbatim(text) => verbatim_lines(w, text),
79    }
80}
81
82/// `type a = … and b = …`.
83///
84/// The keyword changes at the first item and only there. A group of one
85/// is an ordinary declaration; a group of more is the only way OCaml
86/// writes a cycle, because a type name resolves against what precedes it
87/// and against its own group and nothing else.
88fn render_group(w: &mut Writer, group: &TypeGroup) {
89    doc(w, &group.doc);
90    for (index, item) in group.items.iter().enumerate() {
91        if index > 0 {
92            w.blank_line();
93        }
94        render_type_decl(w, item, if index == 0 { "type" } else { "and" });
95    }
96}
97
98fn render_type_decl(w: &mut Writer, node: &TypeDecl, keyword: &str) {
99    doc(w, &node.doc);
100    let mut head = String::from(keyword);
101    head.push(' ');
102    head.push_str(node.name.as_str());
103    match &node.body {
104        TypeBody::Abstract => w.line(&head),
105        TypeBody::Alias(target) => {
106            head.push_str(" = ");
107            head.push_str(&type_text(target));
108            w.line(&head);
109        }
110        TypeBody::Record(fields) => {
111            head.push_str(" = {");
112            w.line(&head);
113            w.indent();
114            for text in &node.body_text.begin {
115                verbatim_lines(w, text);
116            }
117            for field in fields {
118                render_field(w, field);
119            }
120            for text in &node.body_text.end {
121                verbatim_lines(w, text);
122            }
123            w.dedent();
124            w.line("}");
125        }
126        TypeBody::Variant(constructors) => {
127            head.push_str(" =");
128            w.line(&head);
129            w.indent();
130            for constructor in constructors {
131                render_constructor(w, constructor);
132            }
133            w.dedent();
134        }
135    }
136}
137
138fn render_field(w: &mut Writer, node: &Field) {
139    doc(w, &node.doc);
140    let mut line = String::from(node.name.as_str());
141    line.push_str(" : ");
142    line.push_str(&type_text(&node.type_));
143    // A trailing `;` on the last field too. OCaml permits it, and a
144    // field added later is then a one-line diff rather than two.
145    line.push(';');
146    w.line(&line);
147}
148
149fn render_constructor(w: &mut Writer, node: &Constructor) {
150    doc(w, &node.doc);
151    let mut line = String::from("| ");
152    line.push_str(node.name.as_str());
153    if !node.args.is_empty() {
154        line.push_str(" of ");
155        let parts: Vec<String> = node.args.iter().map(type_text).collect();
156        line.push_str(&parts.join(" * "));
157    }
158    w.line(&line);
159}
160
161fn render_let(w: &mut Writer, node: &LetDecl) {
162    doc(w, &node.doc);
163    let mut head = String::from("let ");
164    head.push_str(node.name.as_str());
165    if let Some(type_) = &node.type_ {
166        head.push_str(" : ");
167        head.push_str(&type_text(type_));
168    }
169    head.push_str(" = ");
170    match &node.body {
171        // A `function` is the one expression that spans lines: its arms
172        // are written one per line so that an enumeration with fifty
173        // enumerators is readable and diffs one arm at a time.
174        Expr::Function(arms) => {
175            head.push_str("function");
176            w.line(&head);
177            w.indent();
178            for arm in arms {
179                render_arm(w, arm);
180            }
181            w.dedent();
182        }
183        other => {
184            head.push_str(&expr_text(other));
185            w.line(&head);
186        }
187    }
188}
189
190fn render_arm(w: &mut Writer, node: &Arm) {
191    let mut line = String::from("| ");
192    line.push_str(&pattern_text(&node.pattern));
193    line.push_str(" -> ");
194    line.push_str(&expr_text(&node.body));
195    w.line(&line);
196}
197
198fn render_class_type(w: &mut Writer, node: &ClassTypeDecl) {
199    doc(w, &node.doc);
200    let mut head = String::from("class type ");
201    head.push_str(node.name.as_str());
202    head.push_str(" = object");
203    if node.methods.is_empty() {
204        // `object end` on one line. An interface declaring no operation
205        // is legal IDL and `object\nend` would read as an omission.
206        head.push_str(" end");
207        w.line(&head);
208        return;
209    }
210    w.line(&head);
211    w.indent();
212    for method in &node.methods {
213        render_method(w, method);
214    }
215    w.dedent();
216    w.line("end");
217}
218
219fn render_method(w: &mut Writer, node: &Method) {
220    doc(w, &node.doc);
221    let mut line = String::from("method ");
222    line.push_str(node.name.as_str());
223    line.push_str(" : ");
224    for param in &node.params {
225        line.push_str(&type_text(param));
226        line.push_str(" -> ");
227    }
228    line.push_str(&type_text(&node.result));
229    w.line(&line);
230}
231
232/// zerodds-lint: recursion-depth 64 (one level per collection, alias or
233/// generic argument of the spelled type; `MAX_TYPE_DEPTH` in
234/// `lang-rules/src/emit/ocaml.rs` refuses a type that nests deeper)
235fn type_text(node: &Type) -> String {
236    match node {
237        Type::Primitive(primitive) => primitive.as_str().to_string(),
238        Type::Named(name) => name.as_str().to_string(),
239        // Postfix and left-associative, so no bracket is needed at any
240        // depth: `int32 array array` is an array of arrays and
241        // `int32 array option` an option of an array.
242        Type::Array(element) => {
243            let mut out = type_text(element);
244            out.push_str(" array");
245            out
246        }
247        Type::Option(inner) => {
248            let mut out = type_text(inner);
249            out.push_str(" option");
250            out
251        }
252        // Always bracketed. `a * b array` is `a * (b array)`, so a tuple
253        // written without brackets changes meaning the moment it appears
254        // under a postfix constructor.
255        Type::Tuple(parts) => {
256            let mut out = String::from("(");
257            let rendered: Vec<String> = parts.iter().map(type_text).collect();
258            out.push_str(&rendered.join(" * "));
259            out.push(')');
260            out
261        }
262        Type::Qualified { module, name } => {
263            let mut out = String::new();
264            for segment in module {
265                out.push_str(segment.as_str());
266                out.push('.');
267            }
268            out.push_str(name.as_str());
269            out
270        }
271    }
272}
273
274/// zerodds-lint: recursion-depth = the built expression's nesting. A walk
275/// calls a routine per type rather than nesting one, so the one operand
276/// tree that grows with the IDL is an array's default value: one composite
277/// literal per dimension, built in `lang-rules/src/default_ocaml.rs`, and
278/// nothing bounds the dimension count. No number is claimed: this
279/// signature returns no error.
280fn expr_text(node: &Expr) -> String {
281    match node {
282        Expr::Literal(literal) => literal_text(literal),
283        Expr::Name(name) => name.as_str().to_string(),
284        Expr::Construct(name) => name.as_str().to_string(),
285        Expr::Apply { func, args } => {
286            let mut out = expr_text(func);
287            for arg in args {
288                out.push(' ');
289                out.push_str(&expr_text(arg));
290            }
291            out
292        }
293        // Reached only where a `function` is not the whole right-hand
294        // side of a `let`; [`render_let`] writes that case over several
295        // lines.
296        Expr::Function(arms) => {
297            let mut out = String::from("function ");
298            let rendered: Vec<String> = arms
299                .iter()
300                .map(|arm| {
301                    let mut text = pattern_text(&arm.pattern);
302                    text.push_str(" -> ");
303                    text.push_str(&expr_text(&arm.body));
304                    text
305                })
306                .collect();
307            out.push_str(&rendered.join(" | "));
308            out
309        }
310        Expr::Paren(inner) => {
311            let mut out = String::from("(");
312            out.push_str(&expr_text(inner));
313            out.push(')');
314            out
315        }
316    }
317}
318
319fn pattern_text(node: &Pattern) -> String {
320    match node {
321        Pattern::Literal(literal) => literal_text(literal),
322        Pattern::Construct(name) | Pattern::Bind(name) => name.as_str().to_string(),
323    }
324}
325
326fn literal_text(literal: &Literal) -> String {
327    match literal {
328        // The suffix an `int32` or an `int64` needs is already in the
329        // text: `zerodds_lang_cst::ocaml::int32_literal` puts it there,
330        // because the width is a property of the value's type and not of
331        // the rendering.
332        Literal::Int(text) | Literal::Float(text) => text.clone(),
333        Literal::Bool(value) => {
334            if *value {
335                "true".to_string()
336            } else {
337                "false".to_string()
338            }
339        }
340        // An OCaml `char` is one octet, and an IDL `char` maps onto
341        // `int` in this column, so a character literal is written as its
342        // code point. `'\xff'` would be the other spelling and would be
343        // a `char` where the member is an `int`.
344        Literal::Char(ch) => alloc::format!("{}", u32::from(*ch)),
345        Literal::Str(text) => {
346            let mut out = String::from("\"");
347            out.push_str(&escape_string(text));
348            out.push('"');
349            out
350        }
351        // OCaml has no byte-string literal; a `string` holds arbitrary
352        // octets, so the escaped form is the byte string.
353        Literal::Bytes(bytes) => {
354            let mut out = String::from("\"");
355            for byte in bytes {
356                out.push_str(&alloc::format!("\\x{byte:02x}"));
357            }
358            out.push('"');
359            out
360        }
361        Literal::Null => "None".to_string(),
362    }
363}
364
365/// Write a documentation comment, or nothing when there is none.
366///
367/// One `(** … *)` block for the whole comment rather than one per line.
368/// Several consecutive documentation comments attached to one
369/// declaration are what `ocamlc`'s warning 50 is about, and a block is
370/// also what a reader gets from `ocamldoc`.
371fn doc(w: &mut Writer, doc: &Doc) {
372    let lines = doc.as_lines();
373    let Some((first, rest)) = lines.split_first() else {
374        return;
375    };
376    if rest.is_empty() {
377        let mut only = String::from("(** ");
378        only.push_str(&comment_safe(first));
379        only.push_str(" *)");
380        w.line(&only);
381        return;
382    }
383    let mut head = String::from("(** ");
384    head.push_str(&comment_safe(first));
385    w.line(&head);
386    for (index, line) in rest.iter().enumerate() {
387        let last = index + 1 == rest.len();
388        // A blank line inside the block gets no indentation: trailing
389        // whitespace is what a formatter strips and what a byte
390        // comparison then reports as a difference.
391        if line.is_empty() && !last {
392            w.line("");
393            continue;
394        }
395        let mut text = String::from("    ");
396        text.push_str(&comment_safe(line));
397        if last {
398            text.push_str(" *)");
399        }
400        w.line(&text);
401    }
402}
403
404/// Text that cannot end, nest or unbalance the comment holding it.
405///
406/// Three substitutions, and each is a way an OCaml comment differs from
407/// a `//` or a `/* */` one:
408///
409/// - `"` becomes `'`. A comment must contain well-formed string
410///   literals (OCaml manual, "Lexical conventions"); `ocamlc` reports
411///   `This comment contains an unterminated string literal` and refuses
412///   the file. A C or Go comment has no such rule.
413/// - `*)` becomes `* )`, because it would close the comment early and
414///   leave the rest of the sentence as source.
415/// - `(*` becomes `( *`, because OCaml comments nest and it would open
416///   one that never closes.
417#[must_use]
418pub fn comment_safe(text: &str) -> String {
419    text.replace('"', "'")
420        .replace("*)", "* )")
421        .replace("(*", "( *")
422}
423
424// -- the codec layer -------------------------------------------------------
425//
426// Rendering `zerodds_lang_cst::ocaml`'s codec nodes. Unlike the
427// declaration layer above, this half carries a typing environment: OCaml
428// has no implicit conversion between `int`, `int32` and `int64`, no
429// statements, no `return` and no `break`, so what a C renderer writes
430// down verbatim has to be *typed* here before it can be spelled. The
431// environment is built from the declarations the body itself makes —
432// every local is a `ref` cell with a declared `CodecType` — plus the
433// function's own parameters, and the runtime's signatures are the table
434// below.
435//
436// A store shape this renderer does not know is rendered as the unbound
437// name `zd_unsupported_store`, so the generated file fails to compile
438// naming the site — the same loudness the Python backend chooses for a
439// walk that changed underneath it.
440
441use alloc::format;
442use zerodds_lang_cst::ocaml::{CodeExpr, CodecFn, Stmt, UNSIGNED_HOLDER, ValueFn, ValueKind};
443
444/// How a name in scope is reached.
445#[derive(Debug, Clone, Copy, PartialEq, Eq)]
446enum EnvForm {
447    /// A parameter or loop counter: read as `x`.
448    Plain,
449    /// A `ref` cell: read as `!x`, stored as `x := …`.
450    Cell,
451    /// The discarded scratch entry of an uncounted map's counting pass:
452    /// stores into it are `ignore`.
453    Scratch,
454}
455
456/// The typing environment: name, kind, form. A `Vec` and not a map —
457/// bodies hold tens of names — with the *latest* binding winning, which
458/// is OCaml's own shadowing order.
459type Env = Vec<(String, ValueKind, EnvForm)>;
460
461fn env_get<'a>(env: &'a Env, name: &str) -> Option<(&'a ValueKind, EnvForm)> {
462    env.iter()
463        .rev()
464        .find(|(bound, _, _)| bound == name)
465        .map(|(_, kind, form)| (kind, *form))
466}
467
468/// The runtime's signature for one operation: the parameter kinds after
469/// the receiver, and the result kind. The authority is
470/// `runtime/ocaml/zdwire.ml`; an operation missing here renders with
471/// its arguments unconverted, and `ocamlc` names the mismatch.
472#[allow(clippy::too_many_lines)]
473fn runtime_signature(method: &str) -> Option<(&'static [ValueKind], ValueKind)> {
474    use ValueKind as K;
475    const I: K = K::Int;
476    const I64: K = K::Int64;
477    const I32: K = K::Int32;
478    const F: K = K::Float;
479    const B: K = K::Bool;
480    const T: K = K::Text;
481    const U: K = K::UnitValue;
482    Some(match method {
483        "write_bool" => (&[B], U),
484        "write_u8" | "write_i8" | "write_char" | "write_i16" | "write_u16" | "write_u32" => {
485            (&[I], U)
486        }
487        "write_wchar" => (&[I, I, T, T], U),
488        "write_i32" => (&[I32], U),
489        "write_i64" => (&[I64], U),
490        "write_u64" | "write_string" | "write_f128" => (&[T], U),
491        "write_string_bounded" => (&[T, I], U),
492        "write_f32" | "write_f64" => (&[F], U),
493        "write_wstring_utf8" => (&[T, T, T], U),
494        "write_wstring_bounded" => (&[T, I, T, T], U),
495        "check_sequence_bound" | "check_map_bound" => (&[I, I], U),
496        "write_fixed" => (&[T, I, I, T], U),
497        "write_enum" => (&[I, I64], U),
498        "write_bitmask" | "write_bitset" => (&[I, I64], U),
499        "write_union_overlay" => (&[I64, I], U),
500        "bitfield" => (&[I64, I, I], I64),
501        "write_dheader" => (&[], K::Handle("dheader")),
502        "finish_dheader" => (&[K::Handle("dheader")], U),
503        "write_emheader" => (&[I, I, B], K::Handle("emheader")),
504        "finish_emheader" => (&[K::Handle("emheader")], U),
505        "write_pid" | "write_pid_extended" => (&[I, K::Handle("role"), B], K::Handle("pid")),
506        "finish_pid" => (&[K::Handle("pid")], U),
507        "write_sentinel" => (&[], U),
508        "read_bool" => (&[], B),
509        "read_u8" | "read_i8" | "read_char" | "read_i16" | "read_u16" | "read_u32" => (&[], I),
510        "read_wchar" => (&[I, T, T], I),
511        "read_i32" => (&[], I32),
512        "read_i64" => (&[], I64),
513        "read_u64" | "read_string_alloc" | "read_f128" => (&[], T),
514        "read_f32" | "read_f64" => (&[], F),
515        "read_wstring_utf8" => (&[T, T], T),
516        "read_fixed" => (&[I, I, T], T),
517        "read_enum" | "read_bitmask" => (&[I], I64),
518        "read_bitset" => (&[I], I64),
519        "read_dheader" | "reader_position" | "reader_remaining" => (&[], I),
520        "reader_status" => (&[], K::Handle("status")),
521        "reader_refuse" => (&[K::Handle("status")], U),
522        "reader_clear_discard" => (&[], B),
523        "reader_skip_to" => (&[I], U),
524        "reader_save" => (&[], K::Handle("reader_mark")),
525        "reader_restore" => (&[K::Handle("reader_mark")], U),
526        "read_pid" => (&[], K::Handle("pid_read")),
527        "read_pid_as" => (&[K::Handle("role")], K::Handle("pid_read")),
528        "finish_pid_read" => (&[K::Handle("pid_read")], U),
529        "read_emheader" => (&[], K::Handle("emheader_read")),
530        "trim_utf8" | "trim_utf16" => (&[T, I], T),
531        // The free functions, reached with no receiver.
532        "no_dheader" => (&[], K::Handle("dheader")),
533        "utf8_octets" | "utf16_units" => (&[T], I),
534        "pid_is_user_member" => (&[K::Handle("pid_read")], B),
535        "bit_pattern_int" => (&[I64, I], I64),
536        "bit_pattern_bool" => (&[B], I64),
537        "bit_pattern_f32" | "bit_pattern_f64" => (&[F], I64),
538        "u64_less" | "strcmp" => (&[T, T], if method == "strcmp" { I } else { B }),
539        "u64_of_dec" => (&[T], I64),
540        _ => return None,
541    })
542}
543
544/// The runtime's field for a walk-named header field, per handle kind.
545fn header_field_name(kind: &ValueKind, field: &str) -> String {
546    let mapped = match (kind, field) {
547        (ValueKind::Handle("emheader_read"), "member_id") => "em_member_id",
548        (ValueKind::Handle("emheader_read"), "length_code") => "em_length_code",
549        (ValueKind::Handle("emheader_read"), "must_understand") => "em_must_understand",
550        (ValueKind::Handle("emheader_read"), "extent") => "em_extent",
551        (ValueKind::Handle("pid_read"), "pid") => "pr_pid",
552        (ValueKind::Handle("pid_read"), "length") => "pr_length",
553        (ValueKind::Handle("pid_read"), "end") => "pr_end",
554        (ValueKind::Handle("pid_read"), "must_understand") => "pr_must_understand",
555        (ValueKind::Handle("pid_read"), "ignore") => "pr_ignore",
556        // A field on a header the environment could not type: the
557        // unbound name makes `ocamlc` report the site.
558        _ => return format!("zd_unknown_header_field_{field}"),
559    };
560    mapped.to_string()
561}
562
563fn header_field_kind(field: &str) -> ValueKind {
564    match field {
565        "must_understand" | "end" | "ignore" => ValueKind::Bool,
566        _ => ValueKind::Int,
567    }
568}
569
570/// A literal integer in the spelling `kind` reads.
571fn literal_in(value: i128, kind: &ValueKind) -> String {
572    match kind {
573        ValueKind::Int32 => format!("({value}l)"),
574        ValueKind::Int64 => format!("({value}L)"),
575        ValueKind::Text => format!("\"{value}\""),
576        ValueKind::Bool => (if value != 0 { "true" } else { "false" }).to_string(),
577        ValueKind::Float => format!("({value}.0)"),
578        ValueKind::Enum(name) => format!("({name}_of_int32 ({value}l))"),
579        _ => {
580            if value < 0 {
581                format!("({value})")
582            } else {
583                format!("{value}")
584            }
585        }
586    }
587}
588
589/// `text` (of kind `from`) as a value of kind `to`, through the one
590/// conversion OCaml has for the pair. Kinds with no conversion between
591/// them pass through unchanged: the pair agrees by construction or
592/// `ocamlc` names the site.
593fn convert(text: String, from: &ValueKind, to: &ValueKind) -> String {
594    use ValueKind as K;
595    if from == to {
596        return text;
597    }
598    match (from, to) {
599        (K::Int, K::Int64) => format!("(Int64.of_int {text})"),
600        (K::Int, K::Int32) => format!("(Int32.of_int {text})"),
601        (K::Int32, K::Int64) => format!("(Int64.of_int32 {text})"),
602        (K::Int32, K::Int) => format!("(Int32.to_int {text})"),
603        (K::Int64, K::Int) => format!("(Int64.to_int {text})"),
604        (K::Int64, K::Int32) => format!("(Int64.to_int32 {text})"),
605        (K::Enum(name), K::Int64) => format!("(Int64.of_int32 (int32_of_{name} {text}))"),
606        (K::Enum(name), K::Int32) => format!("(int32_of_{name} {text})"),
607        (K::Enum(name), K::Int) => format!("(Int32.to_int (int32_of_{name} {text}))"),
608        (K::Int32, K::Enum(name)) => format!("({name}_of_int32 {text})"),
609        (K::Int, K::Enum(name)) => format!("({name}_of_int32 (Int32.of_int {text}))"),
610        (K::Int64, K::Enum(name)) => format!("({name}_of_int32 (Int64.to_int32 {text}))"),
611        // The decimal holder of an `unsigned long long` against its
612        // 64-bit pattern.
613        (K::Text, K::Int64) => format!("(Zdwire.u64_of_dec {text})"),
614        (K::Int64, K::Text) => format!("(Zdwire.u64_to_dec {text})"),
615        _ => text,
616    }
617}
618
619/// The kind an expression naturally has, before any expectation.
620///
621/// zerodds-lint: recursion-depth = the built expression's nesting. A walk
622/// calls a routine per type rather than nesting one, so the one operand
623/// tree that grows with the IDL is an array's default value: one composite
624/// literal per dimension, built in `lang-rules/src/default_ocaml.rs`, and
625/// nothing bounds the dimension count. No number is claimed: this
626/// signature returns no error.
627fn natural_kind(env: &Env, expr: &CodeExpr) -> ValueKind {
628    match expr {
629        CodeExpr::Name(name) => {
630            env_get(env, name).map_or(ValueKind::Aggregate, |(kind, _)| kind.clone())
631        }
632        CodeExpr::Whole(name) => {
633            env_get(env, name).map_or(ValueKind::Aggregate, |(kind, _)| kind.clone())
634        }
635        CodeExpr::Int(_) => ValueKind::Int,
636        CodeExpr::Int64Lit(_) => ValueKind::Int64,
637        CodeExpr::BoolLit(_) => ValueKind::Bool,
638        CodeExpr::TextLit(_) => ValueKind::Text,
639        CodeExpr::FloatLit(_) => ValueKind::Float,
640        CodeExpr::Runtime { method, .. } | CodeExpr::Static { method, .. } => {
641            runtime_signature(method).map_or(ValueKind::Aggregate, |(_, ret)| ret)
642        }
643        CodeExpr::HeaderField { field, .. } => header_field_kind(field),
644        CodeExpr::CountOf(_) => ValueKind::Int,
645        CodeExpr::IsSome { .. } => ValueKind::Bool,
646        CodeExpr::Absent => ValueKind::Aggregate,
647        CodeExpr::Binary { op, lhs, rhs } => match *op {
648            "==" | "!=" | "<" | ">" | "<=" | ">=" | "&&" | "||" => ValueKind::Bool,
649            // An arithmetic node's kind is its operands' joint one —
650            // whichever side knows, a bare literal deferring to the
651            // other.
652            _ => {
653                let left = natural_kind(env, lhs);
654                if left == ValueKind::Aggregate || matches!(lhs.as_ref(), CodeExpr::Int(_)) {
655                    natural_kind(env, rhs)
656                } else {
657                    left
658                }
659            }
660        },
661        CodeExpr::Unary { op, value } => {
662            if *op == "!" {
663                ValueKind::Bool
664            } else {
665                natural_kind(env, value)
666            }
667        }
668        CodeExpr::Paren(inner) => natural_kind(env, inner),
669        CodeExpr::Convert { target, .. } => target.kind.clone(),
670        CodeExpr::RuntimeConst { kind, .. } | CodeExpr::Raw { kind, .. } => kind.clone(),
671        // The structural kinds: a member access, a subscript and an
672        // entry half read their answer off the base's kind tree, which
673        // is how a bitset field's `Int64.of_int` and a union arm's
674        // `int32_of_<enum>` get their operand kind without the model.
675        CodeExpr::Member { base, field, case } => {
676            let record = env_get(env, base).map(|(kind, _)| kind.clone());
677            // A field with dots is a path through the members that hold a base
678            // (`parent.x`, XTypes 1.3 §7.5.1.4.1.3).
679            let field_kind = field.split('.').fold(record, |kind, segment| match kind {
680                Some(ValueKind::RecordOf(fields)) => fields
681                    .iter()
682                    .find(|(name, _)| name == segment)
683                    .map(|(_, kind)| kind.clone()),
684                _ => None,
685            });
686            let field_kind = field_kind.unwrap_or(ValueKind::Aggregate);
687            if *case {
688                match field_kind {
689                    ValueKind::OptionOf(inner) => *inner,
690                    other => other,
691                }
692            } else {
693                field_kind
694            }
695        }
696        CodeExpr::OptionGet(inner) => match natural_kind(env, inner) {
697            ValueKind::OptionOf(kind) => *kind,
698            other => other,
699        },
700        CodeExpr::Index { base, .. } => match natural_kind(env, base) {
701            ValueKind::ArrayOf(element) => *element,
702            _ => ValueKind::Aggregate,
703        },
704        CodeExpr::EntryHalf { entry, key } => match natural_kind(env, entry) {
705            ValueKind::RecordOf(fields) => {
706                let half = if *key { "key" } else { "value" };
707                fields
708                    .iter()
709                    .find(|(name, _)| name == half)
710                    .map_or(ValueKind::Aggregate, |(_, kind)| kind.clone())
711            }
712            _ => ValueKind::Aggregate,
713        },
714        CodeExpr::Elements(base) => {
715            env_get(env, base).map_or(ValueKind::Aggregate, |(kind, _)| kind.clone())
716        }
717        CodeExpr::OptionSlot { .. } | CodeExpr::Construct(_) | CodeExpr::Call { .. } => {
718            ValueKind::Aggregate
719        }
720    }
721}
722
723/// The value a name reads as.
724fn name_value(env: &Env, name: &str) -> String {
725    match env_get(env, name) {
726        Some((_, EnvForm::Cell)) => format!("(!{name})"),
727        _ => name.to_string(),
728    }
729}
730
731/// An OCaml string literal of `text`'s bytes.
732fn ocaml_string_literal(text: &str) -> String {
733    let mut out = String::from("\"");
734    for byte in text.bytes() {
735        match byte {
736            b'"' => out.push_str("\\\""),
737            b'\\' => out.push_str("\\\\"),
738            b'\n' => out.push_str("\\n"),
739            b'\r' => out.push_str("\\r"),
740            b'\t' => out.push_str("\\t"),
741            byte if !(0x20..0x7F).contains(&byte) => out.push_str(&format!("\\x{byte:02x}")),
742            byte => out.push(byte as char),
743        }
744    }
745    out.push('"');
746    out
747}
748
749/// The arithmetic spelling of `op` over `kind`.
750fn arithmetic(op: &str, kind: &ValueKind, lhs: &str, rhs: &str) -> String {
751    match kind {
752        ValueKind::Int64 => {
753            let name = match op {
754                "+" => "Int64.add",
755                "-" => "Int64.sub",
756                "*" => "Int64.mul",
757                "/" => "Int64.div",
758                "%" => "Int64.rem",
759                "&" => "Int64.logand",
760                "|" => "Int64.logor",
761                "^" => "Int64.logxor",
762                "<<" => "Int64.shift_left",
763                ">>" => "Int64.shift_right_logical",
764                other => other,
765            };
766            format!("({name} {lhs} {rhs})")
767        }
768        ValueKind::Float => {
769            let symbol = match op {
770                "+" => "+.",
771                "-" => "-.",
772                "*" => "*.",
773                "/" => "/.",
774                other => other,
775            };
776            format!("({lhs} {symbol} {rhs})")
777        }
778        _ => {
779            let symbol = match op {
780                "%" => "mod",
781                "&" => "land",
782                "|" => "lor",
783                "^" => "lxor",
784                "<<" => "lsl",
785                ">>" => "lsr",
786                other => other,
787            };
788            format!("({lhs} {symbol} {rhs})")
789        }
790    }
791}
792
793/// Render one expression as a value, converted into `expected` where
794/// one is given.
795///
796/// zerodds-lint: recursion-depth = the built expression's nesting. A walk
797/// calls a routine per type rather than nesting one, so the one operand
798/// tree that grows with the IDL is an array's default value: one composite
799/// literal per dimension, built in `lang-rules/src/default_ocaml.rs`, and
800/// nothing bounds the dimension count. No number is claimed: this
801/// signature returns no error.
802#[allow(clippy::too_many_lines)]
803fn value(env: &Env, expr: &CodeExpr, expected: Option<&ValueKind>) -> String {
804    // A widthless literal takes the expected width directly, with no
805    // conversion function around it.
806    if let (CodeExpr::Int(number), Some(kind)) = (expr, expected) {
807        return literal_in(*number, kind);
808    }
809    // An `int64` literal a site wants as an `int` — a shift distance —
810    // is the digits, not a conversion around a suffix.
811    if let (CodeExpr::Int64Lit(number), Some(ValueKind::Int)) = (expr, expected) {
812        return literal_in(i128::from(*number), &ValueKind::Int);
813    }
814    let natural = natural_kind(env, expr);
815    let text = match expr {
816        CodeExpr::Name(name) => name_value(env, name),
817        CodeExpr::Whole(name) => name_value(env, name),
818        CodeExpr::Int(number) => {
819            if *number < 0 {
820                format!("({number})")
821            } else {
822                format!("{number}")
823            }
824        }
825        CodeExpr::Int64Lit(number) => format!("({number}L)"),
826        CodeExpr::BoolLit(flag) => (if *flag { "true" } else { "false" }).to_string(),
827        CodeExpr::TextLit(text) => ocaml_string_literal(text),
828        CodeExpr::FloatLit(text) => format!("({text})"),
829        CodeExpr::Runtime {
830            receiver,
831            method,
832            args,
833        } => {
834            let mut out = format!("(Zdwire.{method} {receiver}");
835            let params = runtime_signature(method).map(|(params, _)| params);
836            for (index, arg) in args.iter().enumerate() {
837                let expected = params.and_then(|params| params.get(index));
838                out.push(' ');
839                out.push_str(&value(env, arg, expected));
840            }
841            out.push(')');
842            out
843        }
844        CodeExpr::Static { method, args } => {
845            let mut out = format!("(Zdwire.{method}");
846            let params = runtime_signature(method).map(|(params, _)| params);
847            if args.is_empty() {
848                out.push_str(" ()");
849            }
850            for (index, arg) in args.iter().enumerate() {
851                let expected = params.and_then(|params| params.get(index));
852                out.push(' ');
853                out.push_str(&value(env, arg, expected));
854            }
855            out.push(')');
856            out
857        }
858        CodeExpr::Call { callee, args } => {
859            let mut out = format!("({callee}");
860            if args.is_empty() {
861                out.push_str(" ()");
862            }
863            for arg in args {
864                out.push(' ');
865                out.push_str(&value(env, arg, None));
866            }
867            out.push(')');
868            out
869        }
870        CodeExpr::Member { base, field, case } => {
871            let read = format!("{}.{field}", name_value(env, base));
872            if *case {
873                format!("(Option.get {read})")
874            } else {
875                read
876            }
877        }
878        CodeExpr::OptionSlot { base, field } => {
879            // Read through the marked slot: the value, unwrapped.
880            format!("(Option.get {}.{field})", name_value(env, base))
881        }
882        CodeExpr::OptionGet(inner) => format!("(Option.get {})", value(env, inner, None)),
883        CodeExpr::IsSome { base, field } => {
884            format!("({}.{field} <> None)", name_value(env, base))
885        }
886        CodeExpr::Absent => "None".to_string(),
887        CodeExpr::Index { base, index } => format!(
888            "{}.({})",
889            value(env, base, None),
890            value(env, index, Some(&ValueKind::Int))
891        ),
892        CodeExpr::EntryHalf { entry, key } => format!(
893            "{}.{}",
894            value(env, entry, None),
895            if *key { "key" } else { "value" }
896        ),
897        CodeExpr::HeaderField {
898            value: header,
899            field,
900        } => {
901            let kind = natural_kind(env, header);
902            format!(
903                "{}.Zdwire.{}",
904                value(env, header, None),
905                header_field_name(&kind, field)
906            )
907        }
908        CodeExpr::CountOf(base) => format!("(Array.length {})", name_value(env, base)),
909        CodeExpr::Elements(base) => name_value(env, base),
910        CodeExpr::Construct(name) => name.clone(),
911        CodeExpr::Binary { op, lhs, rhs } => match *op {
912            "&&" | "||" => format!(
913                "({} {op} {})",
914                value(env, lhs, Some(&ValueKind::Bool)),
915                value(env, rhs, Some(&ValueKind::Bool))
916            ),
917            "==" | "!=" | "<" | ">" | "<=" | ">=" => {
918                // The comparison's operand kind: whichever side knows.
919                let left_kind = natural_kind(env, lhs);
920                let right_kind = natural_kind(env, rhs);
921                let joint = if left_kind == ValueKind::Aggregate
922                    || matches!(lhs.as_ref(), CodeExpr::Int(_))
923                {
924                    right_kind
925                } else {
926                    left_kind
927                };
928                let expect = if joint == ValueKind::Aggregate {
929                    None
930                } else {
931                    Some(&joint)
932                };
933                let symbol = match *op {
934                    "==" => "=",
935                    "!=" => "<>",
936                    other => other,
937                };
938                format!(
939                    "({} {symbol} {})",
940                    value(env, lhs, expect),
941                    value(env, rhs, expect)
942                )
943            }
944            op => {
945                // Arithmetic: the operand kind is whichever side knows,
946                // and a shift's distance is an `int` whatever the value
947                // is.
948                let left_kind = natural_kind(env, lhs);
949                let right_kind = natural_kind(env, rhs);
950                let joint = if left_kind == ValueKind::Aggregate
951                    || matches!(lhs.as_ref(), CodeExpr::Int(_))
952                {
953                    right_kind.clone()
954                } else {
955                    left_kind.clone()
956                };
957                let shift = op == "<<" || op == ">>";
958                let right_expect = if shift && joint == ValueKind::Int64 {
959                    ValueKind::Int
960                } else {
961                    joint.clone()
962                };
963                arithmetic(
964                    op,
965                    &joint,
966                    &value(env, lhs, Some(&joint)),
967                    &value(env, rhs, Some(&right_expect)),
968                )
969            }
970        },
971        CodeExpr::Unary { op, value: operand } => match *op {
972            "!" => format!("(not {})", value(env, operand, Some(&ValueKind::Bool))),
973            "~" => {
974                let kind = natural_kind(env, operand);
975                if kind == ValueKind::Int64 {
976                    format!("(Int64.lognot {})", value(env, operand, None))
977                } else {
978                    format!("(lnot {})", value(env, operand, None))
979                }
980            }
981            op => format!("({op} {})", value(env, operand, None)),
982        },
983        CodeExpr::Paren(inner) => value(env, inner, None),
984        CodeExpr::Convert {
985            target,
986            value: operand,
987        } => {
988            if target.text == "signed char" {
989                // C's signed reading of one code-point octet, spelled
990                // as arithmetic: (((cp + 128) mod 256) - 128).
991                let inner = value(env, operand, Some(&ValueKind::Int));
992                format!("((({inner} + 128) mod 256) - 128)")
993            } else if let CodeExpr::Int(number) = operand.as_ref() {
994                literal_in(*number, &target.kind)
995            } else if target.text == UNSIGNED_HOLDER {
996                // The walk's two unsigned widenings: `write_bitmask`'s
997                // pattern and `bitfield`'s value. A bitmask of at most
998                // 32 bits is held in an `int32` — Table 33 gives the
999                // width and OCaml has no unsigned integer — so a bound
1000                // of 32 with bit 31 set is negative there, and
1001                // `Int64.of_int32` would set bits 32..63 as well. The
1002                // runtime then measures that against 2^32 and refuses a
1003                // value that is inside the bound. Masking to the
1004                // holder's width is the widening that keeps the
1005                // pattern.
1006                let from = natural_kind(env, operand);
1007                let inner = value(env, operand, None);
1008                match from {
1009                    ValueKind::Int32 => {
1010                        format!("(Int64.logand (Int64.of_int32 {inner}) 0xFFFFFFFFL)")
1011                    }
1012                    other => convert(inner, &other, &ValueKind::Int64),
1013                }
1014            } else {
1015                let from = natural_kind(env, operand);
1016                convert(value(env, operand, None), &from, &target.kind)
1017            }
1018        }
1019        CodeExpr::RuntimeConst { text, .. } => text.clone(),
1020        CodeExpr::Raw { text, .. } => text.clone(),
1021    };
1022    match expected {
1023        Some(kind) if *kind != ValueKind::Aggregate => convert(text, &natural, kind),
1024        _ => text,
1025    }
1026}
1027
1028/// The functional update of a record held in the cell `base`: `{ !v with x = e }`, and for a
1029/// field with dots, a path through the members that hold a base (`parent.x`, XTypes 1.3
1030/// §7.5.1.4.1.3), `{ !v with parent = { (!v).parent with x = e } }`.
1031fn record_update(base: &str, field: &str, value_text: &str) -> String {
1032    /// zerodds-lint: recursion-depth = the dots of a field path, the depth of the base chain.
1033    fn update(record: &str, segments: &[&str], value_text: &str) -> String {
1034        match segments {
1035            [] => value_text.to_string(),
1036            [last] => format!("{{ {record} with {last} = {value_text} }}"),
1037            [head, rest @ ..] => {
1038                let inner = update(&format!("({record}).{head}"), rest, value_text);
1039                format!("{{ {record} with {head} = {inner} }}")
1040            }
1041        }
1042    }
1043    let segments: Vec<&str> = field.split('.').collect();
1044    update(&format!("!{base}"), &segments, value_text)
1045}
1046
1047/// One store, in the shape the place calls for.
1048fn store(env: &Env, place: &CodeExpr, value_text: &str, value_is_absent: bool) -> String {
1049    match place {
1050        CodeExpr::Whole(name) | CodeExpr::Name(name) => match env_get(env, name) {
1051            Some((_, EnvForm::Scratch)) => format!("ignore {value_text};"),
1052            _ => format!("{name} := {value_text};"),
1053        },
1054        CodeExpr::Member { base, field, case } => {
1055            let wrapped = if *case && !value_is_absent {
1056                format!("Some {value_text}")
1057            } else {
1058                value_text.to_string()
1059            };
1060            format!("{base} := {};", record_update(base, field, &wrapped))
1061        }
1062        CodeExpr::OptionSlot { base, field } => {
1063            let wrapped = if value_is_absent {
1064                value_text.to_string()
1065            } else {
1066                format!("Some {value_text}")
1067            };
1068            format!("{base} := {};", record_update(base, field, &wrapped))
1069        }
1070        CodeExpr::Index { base, index } => format!(
1071            "{}.({}) <- {value_text};",
1072            value(env, base, None),
1073            value(env, index, Some(&ValueKind::Int))
1074        ),
1075        CodeExpr::EntryHalf { entry, key } => {
1076            let half = if *key { "key" } else { "value" };
1077            match entry.as_ref() {
1078                // A store into the discarded scratch entry consumes the
1079                // stream and keeps nothing.
1080                CodeExpr::Name(name)
1081                    if matches!(env_get(env, name), Some((_, EnvForm::Scratch))) =>
1082                {
1083                    format!("ignore {value_text};")
1084                }
1085                CodeExpr::Index { base, index } => {
1086                    let array = value(env, base, None);
1087                    let at = value(env, index, Some(&ValueKind::Int));
1088                    format!("{array}.({at}) <- {{ {array}.({at}) with {half} = {value_text} }};")
1089                }
1090                _ => format!("zd_unsupported_store {value_text};"),
1091            }
1092        }
1093        _ => format!("zd_unsupported_store {value_text};"),
1094    }
1095}
1096
1097/// The kind a store's place expects, where one is knowable.
1098fn place_kind(env: &Env, place: &CodeExpr) -> Option<ValueKind> {
1099    match place {
1100        CodeExpr::Whole(name) | CodeExpr::Name(name) => {
1101            env_get(env, name).map(|(kind, _)| kind.clone())
1102        }
1103        _ => None,
1104    }
1105}
1106
1107/// Append `indent` spaces of depth `depth`.
1108fn pad(out: &mut String, depth: usize) {
1109    for _ in 0..depth {
1110        out.push_str("  ");
1111    }
1112}
1113
1114/// Render a block of statements as a `unit` expression body, ending in
1115/// `()` so the sequence is closed whatever the walk emitted.
1116///
1117/// zerodds-lint: recursion-depth = the generated body's block nesting.
1118/// The codec walk emits a routine per type rather than inlining one
1119/// (`lang-rules/src/emit/encode.rs`), so nothing here follows the type
1120/// graph. What does grow with the IDL is the dispatch chain: a union's
1121/// cases, an enumeration's enumerators and a `@mutable` aggregate's
1122/// members each become one `If` per arm, in the previous arm's `else`.
1123/// The model bounds none of the three counts, and OCaml nests the arms in
1124/// its own output too, so they cannot be walked iteratively the way
1125/// `c.rs` walks them. No number is claimed: this signature returns no
1126/// error, so nothing here could report one.
1127#[allow(clippy::too_many_lines)]
1128fn block(env: &mut Env, statements: &[Stmt], depth: usize, out: &mut String) {
1129    for statement in statements {
1130        match statement {
1131            Stmt::Let {
1132                type_,
1133                name,
1134                value: init,
1135            } => {
1136                let rendered = match init {
1137                    Some(expr) => value(env, expr, Some(&type_.kind)),
1138                    None => format!("({})", type_.default),
1139                };
1140                pad(out, depth);
1141                out.push_str(&format!(
1142                    "let {name} : {} ref = ref {rendered} in\n",
1143                    type_.text
1144                ));
1145                env.push((name.clone(), type_.kind.clone(), EnvForm::Cell));
1146            }
1147            Stmt::Scratch { name } => {
1148                pad(out, depth);
1149                out.push_str(&format!(
1150                    "(* {name}: the counting pass reads and discards *)\n"
1151                ));
1152                env.push((name.clone(), ValueKind::Aggregate, EnvForm::Scratch));
1153            }
1154            Stmt::Assign {
1155                place,
1156                value: given,
1157            } => {
1158                let expected = place_kind(env, place);
1159                let text = value(env, given, expected.as_ref());
1160                pad(out, depth);
1161                out.push_str(&store(env, place, &text, matches!(given, CodeExpr::Absent)));
1162                out.push('\n');
1163            }
1164            Stmt::Store {
1165                place,
1166                type_,
1167                value: given,
1168            } => {
1169                let text = value(env, given, Some(&type_.kind));
1170                pad(out, depth);
1171                out.push_str(&store(env, place, &text, matches!(given, CodeExpr::Absent)));
1172                out.push('\n');
1173            }
1174            Stmt::ReadInto {
1175                callee,
1176                place,
1177                behaviour,
1178            } => {
1179                let call = format!("({callee} r {})", value(env, behaviour, None));
1180                pad(out, depth);
1181                out.push_str(&store(env, place, &call, false));
1182                out.push('\n');
1183            }
1184            Stmt::Perform(expr) => {
1185                let unit_valued = match expr {
1186                    CodeExpr::Runtime { method, .. } | CodeExpr::Static { method, .. } => {
1187                        runtime_signature(method)
1188                            .is_some_and(|(_, ret)| ret == ValueKind::UnitValue)
1189                    }
1190                    // A generated encoder answers unit; nothing else is
1191                    // performed bare.
1192                    CodeExpr::Call { .. } => true,
1193                    _ => false,
1194                };
1195                pad(out, depth);
1196                if unit_valued {
1197                    out.push_str(&format!("{};\n", value(env, expr, None)));
1198                } else {
1199                    out.push_str(&format!("ignore {};\n", value(env, expr, None)));
1200                }
1201            }
1202            Stmt::Discard(expr) => {
1203                pad(out, depth);
1204                out.push_str(&format!("ignore {};\n", value(env, expr, None)));
1205            }
1206            Stmt::Return => {
1207                pad(out, depth);
1208                out.push_str("raise Zdwire.Zd_return;\n");
1209            }
1210            Stmt::When {
1211                condition,
1212                then,
1213                otherwise,
1214            } => {
1215                pad(out, depth);
1216                out.push_str(&format!(
1217                    "if {} then begin\n",
1218                    value(env, condition, Some(&ValueKind::Bool))
1219                ));
1220                let mut inner = env.clone();
1221                block(&mut inner, then, depth + 1, out);
1222                match otherwise {
1223                    Some(other) => {
1224                        pad(out, depth);
1225                        out.push_str("end\n");
1226                        pad(out, depth);
1227                        out.push_str("else begin\n");
1228                        let mut inner = env.clone();
1229                        block(&mut inner, other, depth + 1, out);
1230                        pad(out, depth);
1231                        out.push_str("end;\n");
1232                    }
1233                    None => {
1234                        pad(out, depth);
1235                        out.push_str("end;\n");
1236                    }
1237                }
1238            }
1239            Stmt::For {
1240                counter,
1241                bound,
1242                body,
1243            } => {
1244                pad(out, depth);
1245                out.push_str("(try\n");
1246                pad(out, depth + 1);
1247                out.push_str(&format!(
1248                    "for {counter} = 0 to {} - 1 do\n",
1249                    value(env, bound, Some(&ValueKind::Int))
1250                ));
1251                let mut inner = env.clone();
1252                inner.push((counter.clone(), ValueKind::Int, EnvForm::Plain));
1253                block(&mut inner, body, depth + 2, out);
1254                pad(out, depth + 1);
1255                out.push_str("done\n");
1256                pad(out, depth);
1257                out.push_str("with Zdwire.Zd_break -> ());\n");
1258            }
1259            Stmt::While { condition, body } => {
1260                pad(out, depth);
1261                out.push_str("(try\n");
1262                pad(out, depth + 1);
1263                out.push_str(&format!(
1264                    "while {} do\n",
1265                    value(env, condition, Some(&ValueKind::Bool))
1266                ));
1267                let mut inner = env.clone();
1268                block(&mut inner, body, depth + 2, out);
1269                pad(out, depth + 1);
1270                out.push_str("done\n");
1271                pad(out, depth);
1272                out.push_str("with Zdwire.Zd_break -> ());\n");
1273            }
1274            Stmt::Leave => {
1275                pad(out, depth);
1276                out.push_str("raise Zdwire.Zd_break;\n");
1277            }
1278            Stmt::Note(doc) => {
1279                for line in doc.as_lines() {
1280                    pad(out, depth);
1281                    out.push_str(&format!("(* {} *)\n", comment_safe(line.as_str())));
1282                }
1283            }
1284            Stmt::Alloc {
1285                base,
1286                element,
1287                count,
1288            } => {
1289                pad(out, depth);
1290                out.push_str(&format!(
1291                    "{base} := Array.make {} ({});\n",
1292                    value(env, count, Some(&ValueKind::Int)),
1293                    element.default
1294                ));
1295            }
1296            Stmt::KeepFirst { base, count } => {
1297                pad(out, depth);
1298                out.push_str(&format!(
1299                    "{base} := Array.sub (!{base}) 0 {};\n",
1300                    value(env, count, Some(&ValueKind::Int))
1301                ));
1302            }
1303        }
1304    }
1305    pad(out, depth);
1306    out.push_str("()\n");
1307}
1308
1309/// One codec function, under `keyword` (`let rec` or `and`).
1310fn codec_fn(function: &CodecFn, keyword: &str, out: &mut String) {
1311    for line in function.doc.as_lines() {
1312        out.push_str(&format!("(* {} *)\n", comment_safe(line.as_str())));
1313    }
1314    let mut env: Env = Vec::new();
1315    let mut head = format!("{keyword} {}", function.name);
1316    if function.reading {
1317        head.push_str(" (r : Zdwire.reader)");
1318        env.push(("r".to_string(), ValueKind::Handle("reader"), EnvForm::Plain));
1319    } else {
1320        head.push_str(" (w : Zdwire.writer)");
1321        env.push(("w".to_string(), ValueKind::Handle("writer"), EnvForm::Plain));
1322        head.push_str(&format!(" (v : {})", function.sample.text));
1323        env.push((
1324            "v".to_string(),
1325            function.sample.kind.clone(),
1326            EnvForm::Plain,
1327        ));
1328    }
1329    for (type_, name) in &function.extra {
1330        head.push_str(&format!(" ({name} : {})", type_.text));
1331        env.push((name.clone(), type_.kind.clone(), EnvForm::Plain));
1332    }
1333    if function.reading {
1334        head.push_str(&format!(" : {} =\n", function.sample.text));
1335        out.push_str(&head);
1336        let seed = function
1337            .defaulter
1338            .clone()
1339            .unwrap_or_else(|| function.sample.default.clone());
1340        // Seeded from Table 9, so every position the stream does not
1341        // carry is already what the clause says it is.
1342        out.push_str(&format!("  let v = ref ({seed} ()) in\n"));
1343        env.push(("v".to_string(), function.sample.kind.clone(), EnvForm::Cell));
1344        out.push_str("  (try\n");
1345        block(&mut env, &function.body, 2, out);
1346        out.push_str("  with Zdwire.Zd_return -> ());\n");
1347        out.push_str("  !v\n");
1348    } else {
1349        head.push_str(" : unit =\n");
1350        out.push_str(&head);
1351        block(&mut env, &function.body, 1, out);
1352    }
1353}
1354
1355/// A chain of codec functions: `let rec f … and g …`. One chain, so a
1356/// recursive or mutually recursive type's codecs resolve whatever order
1357/// the walk visited them in.
1358#[must_use]
1359pub fn codec_chain(functions: &[CodecFn]) -> String {
1360    let mut out = String::new();
1361    for (index, function) in functions.iter().enumerate() {
1362        out.push('\n');
1363        codec_fn(
1364            function,
1365            if index == 0 { "let rec" } else { "and" },
1366            &mut out,
1367        );
1368    }
1369    out
1370}
1371
1372/// A chain of value functions — the Table 9 defaulters.
1373#[must_use]
1374pub fn value_chain(functions: &[ValueFn]) -> String {
1375    let mut out = String::new();
1376    for (index, function) in functions.iter().enumerate() {
1377        out.push('\n');
1378        for line in function.doc.as_lines() {
1379            out.push_str(&format!("(* {} *)\n", comment_safe(line.as_str())));
1380        }
1381        let keyword = if index == 0 { "let rec" } else { "and" };
1382        out.push_str(&format!(
1383            "{keyword} {} () : {} =\n  {}\n",
1384            function.name, function.result, function.body
1385        ));
1386    }
1387    out
1388}
1389
1390#[cfg(test)]
1391mod tests {
1392    #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
1393    use super::*;
1394    use zerodds_lang_cst::ocaml::{Primitive, int32_literal};
1395
1396    fn record(name: &str, fields: Vec<Field>) -> TypeDecl {
1397        TypeDecl {
1398            body_text: zerodds_lang_cst::common::BodyText::none(),
1399            doc: Doc::none(),
1400            name: Ident::new(name),
1401            body: TypeBody::Record(fields),
1402        }
1403    }
1404
1405    fn field(name: &str, type_: Type) -> Field {
1406        Field {
1407            doc: Doc::none(),
1408            name: Ident::new(name),
1409            type_,
1410        }
1411    }
1412
1413    #[test]
1414    fn a_record_writes_one_field_per_line_with_a_trailing_semicolon() {
1415        let text = decl(&Decl::Types(TypeGroup::one(record(
1416            "pose",
1417            alloc::vec![
1418                field("x", Type::Primitive(Primitive::Int32)),
1419                field("y", Type::Primitive(Primitive::Float)),
1420            ],
1421        ))));
1422        assert_eq!(text, "type pose = {\n  x : int32;\n  y : float;\n}\n");
1423    }
1424
1425    #[test]
1426    fn a_group_of_two_joins_them_with_and() {
1427        // The only way OCaml writes a cycle: a type name resolves
1428        // against what precedes it and against its own group.
1429        let group = TypeGroup {
1430            doc: Doc::none(),
1431            items: alloc::vec![
1432                record("a", alloc::vec![field("b", Type::named("b"))]),
1433                record("b", alloc::vec![field("a", Type::named("a"))]),
1434            ],
1435        };
1436        let text = decl(&Decl::Types(group));
1437        assert!(text.starts_with("type a = {"), "{text}");
1438        assert!(text.contains("\nand b = {"), "{text}");
1439    }
1440
1441    #[test]
1442    fn a_postfix_type_needs_no_brackets_at_any_depth() {
1443        let nested = Type::option(Type::array(Type::array(Type::Primitive(Primitive::Int32))));
1444        assert_eq!(type_(&nested), "int32 array array option");
1445        let other = Type::array(Type::option(Type::Primitive(Primitive::String)));
1446        assert_eq!(type_(&other), "string option array");
1447    }
1448
1449    #[test]
1450    fn a_tuple_is_always_bracketed_because_star_binds_loosest() {
1451        let tuple = Type::array(Type::Tuple(alloc::vec![
1452            Type::Primitive(Primitive::Int32),
1453            Type::Primitive(Primitive::String),
1454        ]));
1455        assert_eq!(type_(&tuple), "(int32 * string) array");
1456    }
1457
1458    #[test]
1459    fn a_variant_writes_a_leading_bar_per_constructor() {
1460        let text = decl(&Decl::Types(TypeGroup::one(TypeDecl {
1461            body_text: zerodds_lang_cst::common::BodyText::none(),
1462            doc: Doc::none(),
1463            name: Ident::new("mode"),
1464            body: TypeBody::Variant(alloc::vec![
1465                Constructor {
1466                    doc: Doc::none(),
1467                    name: Ident::new("IDLE"),
1468                    args: Vec::new(),
1469                },
1470                Constructor {
1471                    doc: Doc::none(),
1472                    name: Ident::new("ACTIVE"),
1473                    args: Vec::new(),
1474                },
1475            ]),
1476        })));
1477        assert_eq!(text, "type mode =\n  | IDLE\n  | ACTIVE\n");
1478    }
1479
1480    #[test]
1481    fn a_function_binding_writes_one_arm_per_line() {
1482        let text = decl(&Decl::Let(LetDecl {
1483            doc: Doc::none(),
1484            name: Ident::new("int32_of_mode"),
1485            type_: None,
1486            body: Expr::Function(alloc::vec![
1487                Arm {
1488                    pattern: Pattern::Construct(Ident::new("IDLE")),
1489                    body: Expr::Literal(int32_literal(0)),
1490                },
1491                Arm {
1492                    pattern: Pattern::Construct(Ident::new("ACTIVE")),
1493                    body: Expr::Literal(int32_literal(4)),
1494                },
1495            ]),
1496        }));
1497        assert_eq!(
1498            text,
1499            "let int32_of_mode = function\n  | IDLE -> 0l\n  | ACTIVE -> 4l\n"
1500        );
1501    }
1502
1503    #[test]
1504    fn an_abstract_type_writes_only_its_name() {
1505        let text = decl(&Decl::Types(TypeGroup::one(TypeDecl {
1506            body_text: zerodds_lang_cst::common::BodyText::none(),
1507            doc: Doc::none(),
1508            name: Ident::new("handle"),
1509            body: TypeBody::Abstract,
1510        })));
1511        assert_eq!(text, "type handle\n");
1512    }
1513
1514    #[test]
1515    fn an_empty_class_type_stays_on_one_line() {
1516        let text = decl(&Decl::ClassType(ClassTypeDecl {
1517            doc: Doc::none(),
1518            name: Ident::new("nothing"),
1519            methods: Vec::new(),
1520        }));
1521        assert_eq!(text, "class type nothing = object end\n");
1522    }
1523
1524    #[test]
1525    fn a_method_writes_its_parameters_as_arrows() {
1526        let text = decl(&Decl::ClassType(ClassTypeDecl {
1527            doc: Doc::none(),
1528            name: Ident::new("control"),
1529            methods: alloc::vec![
1530                Method {
1531                    doc: Doc::none(),
1532                    name: Ident::new("go"),
1533                    params: alloc::vec![Type::named("pose")],
1534                    result: Type::Primitive(Primitive::Unit),
1535                },
1536                Method {
1537                    doc: Doc::none(),
1538                    name: Ident::new("status"),
1539                    params: Vec::new(),
1540                    result: Type::Primitive(Primitive::Int32),
1541                },
1542            ],
1543        }));
1544        assert!(text.contains("method go : pose -> unit"), "{text}");
1545        assert!(text.contains("method status : int32"), "{text}");
1546    }
1547
1548    #[test]
1549    fn a_quote_in_a_comment_is_replaced_because_ocamlc_refuses_the_file() {
1550        // `ocamlc` reports "This comment contains an unterminated string
1551        // literal" and stops. No other column in this crate has that
1552        // hazard.
1553        assert_eq!(comment_safe("the \"wire\" form"), "the 'wire' form");
1554    }
1555
1556    #[test]
1557    fn a_comment_delimiter_inside_a_comment_is_neutralised() {
1558        assert_eq!(
1559            comment_safe("ends here *) and then"),
1560            "ends here * ) and then"
1561        );
1562        assert_eq!(comment_safe("opens (* here"), "opens ( * here");
1563    }
1564
1565    #[test]
1566    fn a_multi_line_comment_is_one_block_and_not_one_block_per_line() {
1567        let mut w = Writer::new("  ");
1568        doc(&mut w, &Doc::lines(&["first", "second"]));
1569        assert_eq!(w.finish(), "(** first\n    second *)\n");
1570    }
1571}