Skip to main content

zerodds_lang_rules/emit/
fsharp.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The F# backend of the shared codec walk.
4//!
5//! DDS-XTypes 1.3 §7.4 from the clause, the same as every column before
6//! it, and none of them calls another. Where two columns disagree on an
7//! octet, one has misread the specification.
8//!
9//! # The decisions this column turns on
10//!
11//! **No error union and no `try` at the call site.** Zig has no
12//! exceptions, so [`crate::emit::zig::Zig`] threads a `ZigExpr::Fallible`
13//! wrapper through every seam and prepends `try`. .NET has exceptions,
14//! and `runtime/fsharp/ZdWire.fs`'s module documentation is where the
15//! decision to use them is argued: a fallible runtime operation there
16//! simply `raise`s `ZdWire.WireError` and unwinds with no syntax at the
17//! call site, so this backend's `Self::Expr` is a plain
18//! [`FsharpExpr`] with no fallible variant at all — the trait's own
19//! defaults for [`crate::emit::CodecTarget::call`] and
20//! [`crate::emit::CodecTarget::runtime`] already assume exactly this
21//! shape and this backend uses them unchanged.
22//!
23//! **Decoders answer their value, not an out-parameter.** The trait's
24//! documented default for [`crate::emit::CodecTarget::read_call`] is
25//! C's: an address is passed and the callee writes through it. F# has
26//! no address to pass, so — as C#, D, Go, Java, Python and TypeScript
27//! already do, and as Zig does for its own different reason — this
28//! backend overrides it: `place = zd_dec_T(r, behaviour)`.
29//!
30//! **`@external` needs nothing.** `crates/lang-rules/src/adapter/
31//! fsharp.rs`'s module documentation carries why: an F# class is a
32//! reference already, so every `external_*` seam takes the trait's
33//! default, which is the plain member access every other seam already
34//! reaches.
35//!
36//! **`@optional` is `'T option`, and the read direction wraps in
37//! `Some`.** A decoded `T` does not coerce into a `'T option` the way it
38//! coerces into Zig's `?T`; [`FsharpExpr::OptionalSlot`] is the marker
39//! that tells [`Fsharp::read_call`] and [`Fsharp::default_into`] to
40//! write `Some(value)` rather than `value`.
41//!
42//! **There is no one cast operator, so [`CodecTarget::cast`] chooses.**
43//! `enum<T> v` into an enumeration — the walk casts a union label into
44//! its discriminator's type — a bare numeric conversion function
45//! (`int`, `uint32`, `int64`, …) between integer widths, and
46//! `ZdWire.asI64`/`ZdWire.asU64` where the walk widens a value it
47//! cannot see the type of.
48//!
49//! **F#'s own `<` already answers every ordering question.** Unlike
50//! Java, whose `long` holding an IDL `unsigned long long` sorts signed,
51//! F#'s `uint64` is a real unsigned type and its `<` is already the
52//! ascending-key order §7.3.1.2.1.19 wants; unlike C#, C++ and
53//! TypeScript, F#'s `<` on two `bool` values compiles and answers
54//! `false` before `true` — measured (`fsc` 9.0.106) — so neither
55//! [`CodecTarget::unsigned_less`] nor [`CodecTarget::bool_less`] is
56//! overridden here; the trait's own `binary("<", …)` default is exactly
57//! right.
58//!
59//! **No unused-parameter or discard machinery, unlike Zig's.** Zig's
60//! `routine` runs a whole assembly pass to decide `var` against `const`
61//! and to keep exactly the discards a body needs, because Zig refuses
62//! both an unused parameter and a pointless discard. Measured
63//! (`fsc` 9.0.106, `--warnaserror+ --warn:5`): neither an unused
64//! function parameter nor an unused `let` binding is diagnosed at all.
65//! [`Fsharp::routine`] is therefore the simple assembly Zig's own
66//! documentation says a target with no such rule gets.
67
68use super::{CodecTarget, Constant, Handle, Handoff, Routine, Scalar};
69use crate::adapter::fsharp::{
70    MODULE_NAME, field_name, fsharp_map_entry_name, fsharp_type_name, fsharp_value_type, typed_int,
71};
72use crate::default_fsharp;
73use crate::encode::EncodeError;
74use zerodds_ast::{Ast, CharWidth, MemberNode, TypeKind, TypeNode};
75use zerodds_ast_core::Id;
76use zerodds_lang_cst::common::{Doc, Ident, Literal};
77use zerodds_lang_cst::fsharp;
78
79/// The F# backend.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct Fsharp;
82
83/// How deep a chain of typedefs and collections is followed.
84const MAX_TYPE_DEPTH: usize = 64;
85
86/// The name the generated file's `ZdWire` value is reached through — the
87/// module `runtime/fsharp/ZdWire.fs` declares, `#load`ed by the
88/// generator's `Main.fsx` beside the generated `Types.fs`.
89const RUNTIME: &str = "ZdWire";
90
91/// The type name [`CodecTarget::named`] is handed for C's signed
92/// `char`; this column's member is a `byte` and the signed reading is
93/// `sbyte (int8 v)` — a two-step conversion through the same width, the
94/// bit pattern read back as signed.
95const SIGNED_CHAR: &str = "signed char";
96
97/// An expression, with the one property the walk's seams cannot carry
98/// on a bare [`fsharp::Expr`]: whether the destination is an
99/// `@optional` member's `'T option` slot, which needs the decoded value
100/// wrapped in `Some`.
101#[derive(Debug, Clone)]
102pub enum FsharpExpr {
103    /// An ordinary expression.
104    Plain(fsharp::Expr),
105    /// The `'T option` field of an `@optional` member, as a place a
106    /// decoded `T` is about to be written into.
107    OptionalSlot(fsharp::Expr),
108}
109
110/// An F# type, with the two properties [`CodecTarget::cast`] needs and
111/// the tree cannot carry.
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct FsharpType {
114    /// The type as written.
115    pub tree: fsharp::Type,
116    /// Whether a cast into it converts from an integer with `enum<T>`.
117    pub enum_like: bool,
118    /// The holder, where the type names an integer under its own name
119    /// — a bitmask, whose F# form is `type Flags = uint16`. The tree
120    /// says `Named("common_Flags")` and F#'s numeric conversion
121    /// functions (`uint16`, …) know nothing of that name, only of the
122    /// primitive underneath it, so [`CodecTarget::cast`] needs the
123    /// width carried here to spell `uint16(zd_bits)` and not a
124    /// conversion function that does not exist.
125    pub bitmask_holder: Option<fsharp::Primitive>,
126}
127
128impl FsharpType {
129    fn plain(tree: fsharp::Type) -> Self {
130        Self {
131            tree,
132            enum_like: false,
133            bitmask_holder: None,
134        }
135    }
136}
137
138/// The amount of a shift, as `int`.
139///
140/// F# types the shift amount as `int` whatever the shifted value is —
141/// `(<<<) : ^a -> int -> ^a` — while the walk hands the amount in the
142/// width of the value beside it, because every other column wants the
143/// two operands to agree. A bitset offset therefore arrives as `0UL`,
144/// and `zdBits >>> 0UL` is `error FS0001` ("expected int32", measured
145/// with `dotnet fsi`, .NET SDK 9.0.106). A literal drops the suffix
146/// `crate::adapter::fsharp::typed_int` gave it — a bare integer
147/// literal is `int32` in F# — and anything else goes through `int`.
148fn shift_amount(value: fsharp::Expr) -> fsharp::Expr {
149    if let fsharp::Expr::Literal(Literal::Int(digits)) = &value {
150        let bare = digits.trim_end_matches(char::is_alphabetic);
151        if !bare.is_empty() && bare != "-" {
152            return fsharp::Expr::Literal(Literal::Int(bare.to_string()));
153        }
154    }
155    fsharp::Expr::call("int", vec![value])
156}
157
158fn plain(e: fsharp::Expr) -> FsharpExpr {
159    FsharpExpr::Plain(e)
160}
161
162/// The expression inside, whichever form it took.
163fn inner(expr: FsharpExpr) -> fsharp::Expr {
164    match expr {
165        FsharpExpr::Plain(e) | FsharpExpr::OptionalSlot(e) => e,
166    }
167}
168
169impl Fsharp {
170    /// A runtime name, qualified: `ZdWire.X`.
171    fn runtime_name(name: &str) -> fsharp::Expr {
172        fsharp::Expr::dot(fsharp::Expr::Name(Ident::new(RUNTIME)), name)
173    }
174
175    /// A runtime **method**'s F# spelling: `write_u32` is `WriteU32`,
176    /// every `_`-separated segment with its first letter raised — the
177    /// method names `runtime/fsharp/ZdWire.fs`'s `Writer`/`Reader`
178    /// classes publish, .NET's own convention and the one this runtime
179    /// was written against.
180    fn pascal(method: &str) -> String {
181        let mut out = String::new();
182        for segment in method.split('_') {
183            let mut chars = segment.chars();
184            if let Some(first) = chars.next() {
185                out.extend(first.to_uppercase());
186                out.push_str(chars.as_str());
187            }
188        }
189        out
190    }
191
192    /// A runtime **free function**'s F# spelling: `bit_pattern_int` is
193    /// `bitPatternInt` — every segment after the first with its first
194    /// letter raised, the first left alone. F#'s own convention for a
195    /// module-level `let` binding, unlike a method's, and the one
196    /// `runtime/fsharp/ZdWire.fs`'s free functions — `noDheader`,
197    /// `strcmp`, `boolLess`, `checkedChar`, `asI64`, … — are all
198    /// spelled in.
199    fn camel(method: &str) -> String {
200        let mut out = String::new();
201        for (index, segment) in method.split('_').enumerate() {
202            if index == 0 {
203                out.push_str(segment);
204                continue;
205            }
206            let mut chars = segment.chars();
207            if let Some(first) = chars.next() {
208                out.extend(first.to_uppercase());
209                out.push_str(chars.as_str());
210            }
211        }
212        out
213    }
214
215    fn text(value: &str) -> FsharpExpr {
216        plain(fsharp::Expr::Literal(Literal::Str(value.to_string())))
217    }
218
219    /// One value of a runtime union: `ZdWire.ParameterRole.Member`. This
220    /// column's runtime types are F# unions, not enumerations, so a
221    /// case is reached the same way a value would be — `ZdWire.Case`,
222    /// with no accessor between them.
223    fn runtime_case(type_: &str, case: &str) -> FsharpExpr {
224        let _ = type_;
225        plain(Self::runtime_name(case))
226    }
227
228    /// Whether the resolved type behind `id` is an enumeration.
229    fn is_enumeration(ast: &Ast, id: Id<TypeNode>) -> bool {
230        let mut current = id;
231        for _ in 0..MAX_TYPE_DEPTH {
232            match ast.type_(current).map(|node| node.kind.clone()) {
233                Ok(TypeKind::Enumeration { .. }) => return true,
234                Ok(TypeKind::Alias { aliased }) => match aliased.resolved() {
235                    Some(target) => current = target,
236                    None => return false,
237                },
238                _ => return false,
239            }
240        }
241        false
242    }
243
244    /// The bitmask holder behind `id`, resolved through any typedef, or
245    /// `None` where `id` is not a bitmask at all.
246    fn bitmask_holder(ast: &Ast, id: Id<TypeNode>) -> Option<fsharp::Primitive> {
247        let mut current = id;
248        for _ in 0..MAX_TYPE_DEPTH {
249            match ast.type_(current).map(|node| node.kind.clone()) {
250                Ok(TypeKind::Bitmask { .. }) => {
251                    let bound = ast.effective_bit_bound(current);
252                    return Some(crate::adapter::fsharp::fsharp_holder(bound));
253                }
254                Ok(TypeKind::Alias { aliased }) => match aliased.resolved() {
255                    Some(target) => current = target,
256                    None => return None,
257                },
258                _ => return None,
259            }
260        }
261        None
262    }
263
264    /// An integer literal given the width of the slot it is declared
265    /// into — `0` into a `uint32` local is `0u`. Anything that is not a
266    /// bare integer literal, and any slot that is not a primitive, is
267    /// left as it stands: the walk's own `cast` seam already answers
268    /// those.
269    fn widen_literal(tree: &fsharp::Type, value: fsharp::Expr) -> fsharp::Expr {
270        let (fsharp::Type::Primitive(primitive), fsharp::Expr::Literal(Literal::Int(_))) =
271            (tree, &value)
272        else {
273            return value;
274        };
275        typed_int(*primitive, value)
276    }
277
278    /// What a slot the walk declared without an initialiser holds until
279    /// the statement that fills it runs.
280    fn empty_value(tree: &fsharp::Type) -> fsharp::Expr {
281        match tree {
282            fsharp::Type::Primitive(primitive) => match primitive {
283                fsharp::Primitive::Bool => fsharp::Expr::Literal(Literal::Bool(false)),
284                fsharp::Primitive::Float32 | fsharp::Primitive::Double => {
285                    fsharp::Expr::Literal(Literal::Float("0.0".to_string()))
286                }
287                fsharp::Primitive::String => fsharp::Expr::Literal(Literal::Str(String::new())),
288                fsharp::Primitive::Unit => fsharp::Expr::Name(Ident::new("()")),
289                other => typed_int(*other, fsharp::Expr::int(0)),
290            },
291            fsharp::Type::Named(name) if name.as_str() == "ZdWire.PidRead" => {
292                Self::runtime_nothing("noPidRead")
293            }
294            fsharp::Type::Named(name) if name.as_str() == "ZdWire.EmHeaderRead" => {
295                Self::runtime_nothing("noEmheaderRead")
296            }
297            _ => fsharp::Expr::None_,
298        }
299    }
300
301    /// `ZdWire.f ()` — a runtime free function of no arguments, curried
302    /// exactly as [`CodecTarget::runtime_static`] renders one.
303    fn runtime_nothing(name: &str) -> fsharp::Expr {
304        fsharp::Expr::CurriedCall {
305            func: Box::new(Self::runtime_name(name)),
306            args: Vec::new(),
307        }
308    }
309
310    /// Whether `tree` is a fixed-width integer this column has a plain
311    /// conversion function for.
312    fn integer_conversion(tree: &fsharp::Type) -> Option<&'static str> {
313        let fsharp::Type::Primitive(primitive) = tree else {
314            return None;
315        };
316        use fsharp::Primitive as P;
317        Some(match primitive {
318            P::SByte => "sbyte",
319            P::Byte => "byte",
320            P::Int16 => "int16",
321            P::UInt16 => "uint16",
322            P::Int32 => "int32",
323            P::UInt32 => "uint32",
324            P::Int64 => "int64",
325            P::UInt64 => "uint64",
326            _ => return None,
327        })
328    }
329}
330
331impl CodecTarget for Fsharp {
332    type Expr = FsharpExpr;
333    type Stmt = fsharp::Stmt;
334    type Block = Vec<fsharp::Stmt>;
335    type Type = FsharpType;
336    type Decl = fsharp::Decl;
337
338    // -- types --------------------------------------------------------
339
340    /// The fixed-width F# type each scalar local takes. `Index` is
341    /// `uint32` — a sequence length is a `uint32` on the wire — and
342    /// `Size` is `int32`, which is what a .NET array's own `Length` and
343    /// a byte position are held in.
344    fn scalar(kind: Scalar) -> Self::Type {
345        use fsharp::Primitive as P;
346        FsharpType::plain(match kind {
347            Scalar::Bool => fsharp::Type::Primitive(P::Bool),
348            Scalar::Char | Scalar::UInt8 => fsharp::Type::Primitive(P::Byte),
349            Scalar::Int8 => fsharp::Type::Primitive(P::SByte),
350            Scalar::Int16 => fsharp::Type::Primitive(P::Int16),
351            Scalar::UInt16 => fsharp::Type::Primitive(P::UInt16),
352            Scalar::Int32 => fsharp::Type::Primitive(P::Int32),
353            Scalar::UInt32 | Scalar::Index => fsharp::Type::Primitive(P::UInt32),
354            Scalar::Int64 => fsharp::Type::Primitive(P::Int64),
355            Scalar::UInt64 => fsharp::Type::Primitive(P::UInt64),
356            Scalar::Float => fsharp::Type::Primitive(P::Float32),
357            Scalar::Double => fsharp::Type::Primitive(P::Double),
358            // The decimal text; the module documentation of
359            // `crates/lang-cst/src/fsharp/mod.rs` says why .NET has no
360            // narrower workaround needed and no wider type to use.
361            Scalar::LongDouble => fsharp::Type::text(),
362            Scalar::Size => fsharp::Type::Primitive(P::Int32),
363        })
364    }
365
366    fn named(name: &str) -> Self::Type {
367        FsharpType::plain(fsharp::Type::named(name))
368    }
369
370    fn nothing() -> Self::Type {
371        FsharpType::plain(fsharp::Type::Primitive(fsharp::Primitive::Unit))
372    }
373
374    /// The runtime's handles, qualified.
375    fn handle(kind: Handle) -> Self::Type {
376        FsharpType::plain(fsharp::Type::named(match kind {
377            Handle::DHeader => "ZdWire.DHeader",
378            Handle::EmHeader => "ZdWire.EmHeader",
379            Handle::Pid => "ZdWire.Pid",
380            Handle::TryConstruct => "ZdWire.TryConstruct",
381            Handle::ReaderMark => "ZdWire.ReaderMark",
382            Handle::PidRead => "ZdWire.PidRead",
383            Handle::EmHeaderRead => "ZdWire.EmHeaderRead",
384        }))
385    }
386
387    /// A `char`'s member already is the runtime's own type — `byte`,
388    /// which is what `ZdWire.Writer.WriteChar` takes — and there is
389    /// nothing to convert.
390    ///
391    /// A `wchar`'s member is a `uint16` and `WriteWchar` takes a code
392    /// point as `int`, because the four-octet form of §7.4.1.1.1 Table
393    /// 30 is not a `uint16` on the wire. F# converts no numeric type
394    /// implicitly, so the conversion is written here — the same
395    /// asymmetry the walk spells out in the other direction, where
396    /// `crates/lang-rules/src/emit/decode.rs` casts `read_wchar`'s
397    /// answer back to a `uint16` itself.
398    fn code_point(value: Self::Expr, narrow: bool) -> Self::Expr {
399        if narrow {
400            return value;
401        }
402        plain(fsharp::Expr::call("int", vec![inner(value)]))
403    }
404
405    /// The runtime's policy values: numbers and strings where
406    /// `ZdWire.fs` takes them so, and the runtime's own union cases for
407    /// the enumerated kinds.
408    fn constant(which: Constant) -> Self::Expr {
409        match which {
410            Constant::WcharWidth2 => plain(fsharp::Expr::int(2)),
411            Constant::WcharWidth4 => plain(fsharp::Expr::int(4)),
412            Constant::WcharFixed => plain(fsharp::Expr::Literal(Literal::Bool(false))),
413            Constant::WcharGiopLengthPrefixed => plain(fsharp::Expr::Literal(Literal::Bool(true))),
414            Constant::WstringUnitsStream => Self::text("stream"),
415            Constant::WstringUnitsBigEndianAlways => Self::text("big-endian-always"),
416            Constant::WstringUnitsByteOrderMark => Self::text("byte-order-mark"),
417            Constant::WstringUnitsHostUnswapped => Self::text("host-unswapped"),
418            Constant::WstringSpecification => Self::text("specification"),
419            Constant::WstringCharacterCount => Self::text("character-count"),
420            Constant::WstringCharacterCountTerminated => Self::text("character-count-terminated"),
421            Constant::WstringSpecificationWideUnits => Self::text("specification-wide-units"),
422            Constant::WstringRosTypeSupport => Self::text("ros-type-support"),
423            Constant::WstringOctetsTerminated2 => Self::text("octets/terminated/2"),
424            Constant::WstringOctetsTerminated4 => Self::text("octets/terminated/4"),
425            Constant::WstringCharactersTerminated2 => Self::text("characters/terminated/2"),
426            Constant::FixedDeclared => Self::text("declared"),
427            Constant::FixedValuePrecision => Self::text("value-precision"),
428            Constant::FixedValueStringDigits => Self::text("value-string-digits"),
429            Constant::ParameterMember => Self::runtime_case("ParameterRole", "Member"),
430            Constant::ParameterOptional => Self::runtime_case("ParameterRole", "Optional"),
431            Constant::TryConstructDiscard => Self::runtime_case("TryConstruct", "Discard"),
432            Constant::TryConstructTrim => Self::runtime_case("TryConstruct", "Trim"),
433            Constant::TryConstructUseDefault => Self::runtime_case("TryConstruct", "UseDefault"),
434            Constant::StatusOk => Self::runtime_case("Status", "Ok"),
435            Constant::StatusDiscarded => Self::runtime_case("Status", "Discarded"),
436            Constant::StatusImpossibleLength => Self::runtime_case("Status", "ImpossibleLength"),
437            Constant::StatusOutOfRange => Self::runtime_case("Status", "OutOfRange"),
438            Constant::StatusTruncated => Self::runtime_case("Status", "Truncated"),
439        }
440    }
441
442    /// The unsigned type wide enough for `bits` of a bit set — the same
443    /// answer the materialiser's `fsharp_holder` gives its fields.
444    fn unsigned_holder(bits: u16) -> Self::Type {
445        FsharpType::plain(fsharp::Type::Primitive(
446            crate::adapter::fsharp::fsharp_holder(bits),
447        ))
448    }
449
450    /// `runtime/fsharp` carries the three HDDS axes: `BeginPackedElement`
451    /// and `FinishPackedElement` on the writer and `ReaderBeginPackedElement`
452    /// / `ReaderFinishPackedElement` on the reader, the DHEADER pair for a
453    /// sequence element's own length, and `WriteWstringUtf8Octets` /
454    /// `ReadWstringUtf8Octets`.
455    fn carries_hdds_axes() -> bool {
456        true
457    }
458
459    fn value_type(ast: &Ast, id: Id<TypeNode>) -> Self::Type {
460        // A type with no F# form was refused by the materialiser before a
461        // codec walk ran — `fsharp_items` answers `Unevaluated` for the
462        // same constructs, and `generate::source` calls `declarations`
463        // before `codec`. The trait's signature carries no error, so the
464        // unreachable arm names a type F# does not have: `fsc` then stops
465        // on an unbound name. `unit` would have been a *valid* type, and a
466        // member declared `unit` writes nothing and reads nothing — the
467        // member would leave the wire with the file still compiling. The
468        // OCaml column answers the same shape the same way
469        // (`emit/ocaml.rs`, `zd_no_ocaml_form`).
470        let tree = fsharp_value_type(ast, id, "codec")
471            .unwrap_or_else(|_| fsharp::Type::named("ZdNoFsharpForm"));
472        FsharpType {
473            tree,
474            enum_like: Self::is_enumeration(ast, id),
475            bitmask_holder: Self::bitmask_holder(ast, id),
476        }
477    }
478
479    fn type_name(ast: &Ast, id: Id<TypeNode>) -> String {
480        type_tag(ast, id, 0)
481    }
482
483    /// The entry name the materialiser declared, and a distinct one
484    /// per type when its tag walk refuses: the slot keeps two refused
485    /// entries from collapsing onto one declaration, which would merge
486    /// two map types under one name.
487    fn map_entry_name(ast: &Ast, id: Id<TypeNode>) -> String {
488        fsharp_map_entry_name(ast, id).unwrap_or_else(|_| format!("ZdUnresolvedEntry{}", id.slot()))
489    }
490
491    /// Nothing: a declared type's F# name is already flat — the adapter
492    /// folds the modules in — and an enumerator lives in its enum's own
493    /// scope, which [`CodecTarget::enumerator_constant`]'s override
494    /// reaches.
495    fn module_prefix(_ast: &Ast, _id: Id<TypeNode>) -> String {
496        String::new()
497    }
498
499    fn escape(name: &str) -> String {
500        field_name(name)
501    }
502
503    /// The base is the first member `parent` (XTypes 1.3 §7.5.1.4.1.3).
504    fn inherited_path(depth: usize) -> Option<String> {
505        Some(format!("{}.", field_name("parent")).repeat(depth))
506    }
507
508    // -- expressions ---------------------------------------------------
509
510    fn name(text: &str) -> Self::Expr {
511        plain(fsharp::Expr::Name(Ident::new(text)))
512    }
513
514    fn int(value: i128) -> Self::Expr {
515        plain(fsharp::Expr::int(value))
516    }
517
518    /// The raw tokens the walk escapes through [`Literal::Int`],
519    /// rewritten into the tree's own boolean where they are one.
520    fn literal(value: Literal) -> Self::Expr {
521        let Literal::Int(text) = &value else {
522            return plain(fsharp::Expr::Literal(value));
523        };
524        match text.as_str() {
525            "true" => plain(fsharp::Expr::Literal(Literal::Bool(true))),
526            "false" => plain(fsharp::Expr::Literal(Literal::Bool(false))),
527            _ => plain(fsharp::Expr::Literal(value)),
528        }
529    }
530
531    /// `int64(v)` or `uint64(v)`, whichever the value's own range needs
532    /// — a union label the size of `16045690984833335998` does not fit
533    /// an `int64`, and the trait's own documentation names that exact
534    /// value. F# gives neither width bare literal syntax: measured
535    /// (`fsi`, .NET SDK 9.0.106), `let x : int64 = 5` is `error FS0001`
536    /// with no suffix, so this seam wraps in the conversion function
537    /// `crate::adapter::fsharp::typed_int` names, not a literal suffix
538    /// — `crates/lang-cst/src/fsharp/mod.rs`'s tree carries no signed
539    /// arbitrary-precision literal form the way Zig's `comptime_int` is
540    /// one.
541    fn wide_int(value: i128) -> Self::Expr {
542        if let Ok(narrow) = i64::try_from(value) {
543            plain(typed_int(
544                fsharp::Primitive::Int64,
545                fsharp::Expr::int(narrow.into()),
546            ))
547        } else {
548            plain(typed_int(
549                fsharp::Primitive::UInt64,
550                fsharp::Expr::int(value),
551            ))
552        }
553    }
554
555    /// `uint32(v)` — a count, an element index and a member id are all
556    /// `uint32` on the wire, and this column's own `Scalar::Index`
557    /// answers the same width for the same reason.
558    fn unsigned_int(value: i128) -> Self::Expr {
559        plain(typed_int(
560            fsharp::Primitive::UInt32,
561            fsharp::Expr::int(value),
562        ))
563    }
564
565    /// `uint64(v)`.
566    fn wide_unsigned(value: u64) -> Self::Expr {
567        plain(typed_int(
568            fsharp::Primitive::UInt64,
569            fsharp::Expr::int(i128::from(value)),
570        ))
571    }
572
573    /// `int64(v)`.
574    fn wide_signed(value: i128) -> Self::Expr {
575        plain(typed_int(
576            fsharp::Primitive::Int64,
577            fsharp::Expr::int(value),
578        ))
579    }
580
581    /// `v.x` — and `v._d` as it stands: this column keeps the IDL-facing
582    /// spelling, underscore and all.
583    fn member(base: &str, field: &str) -> Self::Expr {
584        plain(fsharp::Expr::dot(
585            fsharp::Expr::Name(Ident::new(base)),
586            field,
587        ))
588    }
589
590    fn whole(base: &str) -> Self::Expr {
591        plain(fsharp::Expr::Name(Ident::new(base)))
592    }
593
594    fn field_of(value: Self::Expr, field: &str) -> Self::Expr {
595        plain(fsharp::Expr::dot(inner(value), field))
596    }
597
598    /// The runtime's own spelling for a header field the walk asks for
599    /// by its wire name.
600    ///
601    /// `runtime/fsharp/ZdWire.fs` holds `PidRead` and `EmHeaderRead` as
602    /// records, and a record field is a name in the language: `end` is a
603    /// reserved word of the F# specification §3.4 and `zd_p.end` is
604    /// `error FS0599`, "missing qualification after '.'". The other
605    /// names differ from the wire's for the same reason the C# column's
606    /// do — a .NET member is `PascalCase` — so the mapping is written
607    /// once here rather than bent into the runtime.
608    fn header_field(value: Self::Expr, field: &str) -> Self::Expr {
609        plain(fsharp::Expr::dot(
610            inner(value),
611            match field {
612                "pid" | "member_id" => "Id",
613                "end" => "ListEnd",
614                "ignore" => "Ignore",
615                "length" => "Length",
616                "must_understand" => "MustUnderstand",
617                "extent" => "Extent",
618                other => other,
619            },
620        ))
621    }
622
623    /// `value.[int at]` — an F# array index is always `int`, and this
624    /// walk's own counters are `uint32` ([`Self::count_up`]'s own
625    /// documentation says why); measured (`fsi`, .NET SDK 9.0.106),
626    /// `v.[i]` with `i: uint32` is `error FS0001`. The cast is written
627    /// here and not at every call site, because every index this walk
628    /// ever builds ends up as an array subscript.
629    fn index(value: Self::Expr, at: Self::Expr) -> Self::Expr {
630        plain(fsharp::Expr::Index {
631            value: Box::new(inner(value)),
632            index: Box::new(fsharp::Expr::call("int", vec![inner(at)])),
633        })
634    }
635
636    /// `uint32 v.len.Length`, or the bound where the materialiser fixed
637    /// one: `.Length` is an `int32` and a sequence's count is a
638    /// `uint32` on the wire.
639    fn count_of(base: &str, fixed: Option<u64>) -> Self::Expr {
640        match fixed {
641            Some(bound) => plain(typed_int(
642                fsharp::Primitive::UInt32,
643                fsharp::Expr::int(i128::from(bound)),
644            )),
645            None => plain(fsharp::Expr::call(
646                "uint32",
647                vec![fsharp::Expr::dot(
648                    fsharp::Expr::Name(Ident::new(base)),
649                    "Length",
650                )],
651            )),
652        }
653    }
654
655    /// The array itself: an F# array value is the elements.
656    fn elements(base: &str) -> Self::Expr {
657        plain(fsharp::Expr::Name(Ident::new(base)))
658    }
659
660    /// `v.x` — a union is a class carrying its branches directly.
661    fn case_member(base: &str, field: &str) -> Self::Expr {
662        Self::member(base, field)
663    }
664
665    /// `entry.Key` and `entry.Value`, the two properties the
666    /// materialiser gives a map's entry class.
667    fn entry_half(entry: Self::Expr, key: bool) -> Self::Expr {
668        plain(fsharp::Expr::dot(
669            inner(entry),
670            if key { "Key" } else { "Value" },
671        ))
672    }
673
674    fn absent() -> Self::Expr {
675        plain(fsharp::Expr::None_)
676    }
677
678    /// Nothing: an F# array and string always exist once the runtime
679    /// answered — an allocation failure is a `.NET` `OutOfMemoryException`
680    /// no code here is built to survive anyway — so there is no absent
681    /// state for the guard to test.
682    fn materialised(_value: Self::Expr) -> Option<Self::Expr> {
683        None
684    }
685
686    /// `v.x.Value` — the value a `'T option` holds, given that it holds
687    /// one. Only ever reached inside a branch the presence test
688    /// guarded, where the unwrap cannot trip.
689    fn optional_value(_ast: &Ast, _id: Id<TypeNode>, base: &str, field: &str) -> Self::Expr {
690        plain(fsharp::Expr::dot(inner(Self::member(base, field)), "Value"))
691    }
692
693    /// `v.x.IsSome`.
694    fn present(base: &str, field: &str) -> Self::Expr {
695        plain(fsharp::Expr::dot(
696            inner(Self::member(base, field)),
697            "IsSome",
698        ))
699    }
700
701    /// Nothing on the way in — assigning `Some value` is what makes the
702    /// member present, through [`Self::optional_destination`]'s marker
703    /// — and `v.x <- None` on the way out.
704    fn set_present(base: &str, field: &str, present: bool) -> Vec<Self::Stmt> {
705        if present {
706            return Vec::new();
707        }
708        vec![fsharp::Stmt::Assign {
709            place: inner(Self::member(base, field)),
710            value: fsharp::Expr::None_,
711        }]
712    }
713
714    /// The marked place; [`Self::read_call`] wraps the decoded `T` in
715    /// `Some` before the assignment, because unlike Zig's `?T` an F#
716    /// `'T option` does not coerce from a bare `T`.
717    fn optional_destination(base: &str, field: &str) -> (Vec<Self::Stmt>, Self::Expr) {
718        (
719            Vec::new(),
720            FsharpExpr::OptionalSlot(inner(Self::member(base, field))),
721        )
722    }
723
724    // `external_value`, `external_destination`, `external_present`,
725    // `external_set_present`, `external_case_value` and
726    // `external_case_destination` all take the trait's default: an F#
727    // class is a reference already, so `@external` reaches the plain
728    // member or case accessor unchanged. The module documentation of
729    // `crates/lang-rules/src/adapter/fsharp.rs` carries the reasoning.
730
731    /// The operators, unchanged: F# spells every one of them the same
732    /// as the walk does, `&&`/`||` included. `=` never reaches a
733    /// rendered expression — [`Self::perform`] turns it into an
734    /// assignment statement.
735    /// The operator, with the spellings F# does not share with the
736    /// walk's own C-family default: the shifts and the three bitwise
737    /// operators take a third symbol — `<<` is `<<<`, `>>` is `>>>`,
738    /// `&` is `&&&`, `|` is `|||`, `^` is `^^^` — because F# still
739    /// parses a bare `&`/`|` as its own long-deprecated boolean
740    /// `and`/`or` (measured, `fsc` 9.0.106: `zdBits & mask` on two
741    /// `uint64` is `error FS0001`, "expected bool"). `==` is `=`,
742    /// F#'s equality operator — `==` is reference equality on a class
743    /// and refuses two `int32 []` outright — and `!=` is `<>`.
744    fn binary(op: &'static str, lhs: Self::Expr, rhs: Self::Expr) -> Self::Expr {
745        let spelled = match op {
746            "<<" => "<<<",
747            ">>" => ">>>",
748            "&" => "&&&",
749            "|" => "|||",
750            "^" => "^^^",
751            "==" => "=",
752            "!=" => "<>",
753            other => other,
754        };
755        let right = match op {
756            "<<" | ">>" => shift_amount(inner(rhs)),
757            _ => inner(rhs),
758        };
759        plain(fsharp::Expr::Binary {
760            op: spelled,
761            lhs: Box::new(inner(lhs)),
762            rhs: Box::new(right),
763        })
764    }
765
766    /// `not value` for `!`, `-value` for `-`, `~~~value` for `~` — F#'s
767    /// own bitwise complement, spelled with three tildes and not one.
768    fn unary(op: &'static str, value: Self::Expr) -> Self::Expr {
769        let operand = inner(value);
770        let spelled = match op {
771            "!" => "not ",
772            "~" => "~~~",
773            other => other,
774        };
775        plain(fsharp::Expr::Unary {
776            op: spelled,
777            value: Box::new(operand),
778        })
779    }
780
781    fn paren(value: Self::Expr) -> Self::Expr {
782        plain(fsharp::Expr::Paren(Box::new(inner(value))))
783    }
784
785    /// The conversion, spelled by what it converts into. A `bool`
786    /// target with an integer literal becomes the boolean literal, as
787    /// in the sibling columns.
788    fn cast(type_: Self::Type, value: Self::Expr) -> Self::Expr {
789        let operand = inner(value);
790        if type_.tree == fsharp::Type::named(SIGNED_CHAR) {
791            return plain(fsharp::Expr::call("sbyte", vec![operand]));
792        }
793        if type_.tree == fsharp::Type::Primitive(fsharp::Primitive::Bool) {
794            if let fsharp::Expr::Literal(Literal::Int(text)) = &operand {
795                return plain(fsharp::Expr::Literal(Literal::Bool(text != "0")));
796            }
797        }
798        if type_.enum_like {
799            let fsharp::Type::Named(name) = &type_.tree else {
800                return plain(operand);
801            };
802            // `enum<T>` takes only `int32`: measured (`fsi`, .NET SDK
803            // 9.0.106), `enum<Mode> (int64 0)` is `error FS0001`,
804            // "expected 'int32' … 'int64'" — unlike the numeric
805            // conversion functions, `enum<T>`'s argument is not widened
806            // through .NET's usual numeric SRTP. The value this walk
807            // hands a union label through is `Self::wide_int`'s
808            // `int64`/`uint64`, so it is narrowed here first.
809            return plain(fsharp::Expr::call(
810                &format!("enum<{}>", name.as_str()),
811                vec![fsharp::Expr::call("int", vec![operand])],
812            ));
813        }
814        if type_.tree == fsharp::Type::Primitive(fsharp::Primitive::Int64) {
815            return plain(fsharp::Expr::Call {
816                func: Box::new(Self::runtime_name("asI64")),
817                args: vec![operand],
818            });
819        }
820        if type_.tree == fsharp::Type::Primitive(fsharp::Primitive::UInt64) {
821            return plain(fsharp::Expr::Call {
822                func: Box::new(Self::runtime_name("asU64")),
823                args: vec![operand],
824            });
825        }
826        if let Some(conversion) = Self::integer_conversion(&type_.tree) {
827            return plain(fsharp::Expr::call(conversion, vec![operand]));
828        }
829        // A bitmask's own name (`common_Flags`) is a type abbreviation
830        // and not a conversion function; [`Self::bitmask_holder`]
831        // carries the primitive underneath it, which `typed_int` knows
832        // how to spell — [`crate::emit::CodecTarget::bitmask_value`]'s
833        // default already routes here through this seam.
834        if let Some(holder) = type_.bitmask_holder {
835            return plain(typed_int(holder, operand));
836        }
837        plain(operand)
838    }
839
840    /// The value, unchanged: an F# array and a class both hand by
841    /// header/reference already.
842    fn handoff(value: Self::Expr, _form: Handoff) -> Self::Expr {
843        value
844    }
845
846    /// A call to a generated codec — a curried `let` function, not a
847    /// .NET method: [`fsharp::Expr::CurriedCall`]'s documentation
848    /// carries the measurement that `f(a, b)` does not compile against
849    /// one. `strcmp` is the runtime's, which spells C11 §7.24.4's
850    /// unsigned-octet order over two UTF-8 strings, and is a free
851    /// function of `runtime/fsharp/ZdWire.fs` too.
852    fn call(callee: &str, args: Vec<Self::Expr>) -> Self::Expr {
853        let arguments: Vec<fsharp::Expr> = args.into_iter().map(inner).collect();
854        if callee == "strcmp" {
855            return plain(fsharp::Expr::CurriedCall {
856                func: Box::new(Self::runtime_name("strcmp")),
857                args: arguments,
858            });
859        }
860        plain(fsharp::Expr::curried_call(callee, arguments))
861    }
862
863    /// `w.WriteU32(x)` — a method on the writer or the reader.
864    fn runtime(receiver: &str, method: &str, args: Vec<Self::Expr>) -> Self::Expr {
865        plain(fsharp::Expr::Call {
866            func: Box::new(fsharp::Expr::dot(
867                fsharp::Expr::Name(Ident::new(receiver)),
868                &Self::pascal(method),
869            )),
870            args: args.into_iter().map(inner).collect(),
871        })
872    }
873
874    /// A free function of the runtime, qualified — except
875    /// `pid_is_user_member`, which is a property on the header the
876    /// reader handed back.
877    fn runtime_static(method: &str, args: Vec<Self::Expr>) -> Self::Expr {
878        if method == "pid_is_user_member" {
879            let mut arguments = args.into_iter().map(inner);
880            if let Some(header) = arguments.next() {
881                return plain(fsharp::Expr::dot(header, "IsUserMember"));
882            }
883            return plain(fsharp::Expr::dot(
884                Self::runtime_name("PidRead"),
885                "IsUserMember",
886            ));
887        }
888        // A free function of `ZdWire`, curried, exactly as
889        // [`Self::call`] answers for a generated codec — the module
890        // documentation of [`fsharp::Expr::CurriedCall`] carries why.
891        let call = fsharp::Expr::CurriedCall {
892            func: Box::new(Self::runtime_name(&Self::camel(method))),
893            args: args.into_iter().map(inner).collect(),
894        };
895        plain(call)
896    }
897
898    // -- statements ----------------------------------------------------
899
900    /// `let name : T = value` — the type written, so a reader of the
901    /// generated file sees the width. Every local the body reassigns is
902    /// written `let mutable` by [`Self::routine`]'s assembly pass;
903    /// [`Self::declare`] itself answers `false` because most locals the
904    /// walk declares are never reassigned, and the pass corrects the
905    /// minority that are.
906    ///
907    /// Two coercions the other columns need none of, both because F#
908    /// converts no numeric type implicitly and admits no null in a
909    /// record:
910    ///
911    /// - **A bare integer initialiser takes the slot's own width.** The
912    ///   walk writes its zeros and ones through one `int` seam — C's
913    ///   `uint32_t zd_before = 0` and C#'s `uint zd_before = 0` both
914    ///   compile — and F#'s `let zd_before : uint32 = 0` is
915    ///   `error FS0001`. [`crate::adapter::fsharp::typed_int`] gives the
916    ///   literal its suffix instead.
917    /// - **A slot with no initialiser gets the runtime's empty handle.**
918    ///   `crates/lang-rules/src/emit/decode.rs` declares `zd_p` and
919    ///   `zd_h` with no value, meaning "a header the loop is about to
920    ///   read into"; C leaves the struct uninitialised and F# has no
921    ///   such state, so the runtime's `noPidRead ()`/`noEmheaderRead ()`
922    ///   stand in — the same answer [`Handle::DHeader`] already gets
923    ///   from `ZdWire.noDheader ()` through the walk's own seam.
924    fn declare(type_: Self::Type, name: &str, value: Option<Self::Expr>) -> Self::Stmt {
925        let value = match value {
926            Some(value) => Self::widen_literal(&type_.tree, inner(value)),
927            None => Self::empty_value(&type_.tree),
928        };
929        fsharp::Stmt::Let {
930            name: Ident::new(name),
931            type_: Some(type_.tree),
932            value,
933            mutable: false,
934        }
935    }
936
937    fn perform(value: Self::Expr) -> Self::Stmt {
938        match value {
939            FsharpExpr::Plain(fsharp::Expr::Binary { op: "=", lhs, rhs }) => fsharp::Stmt::Assign {
940                place: *lhs,
941                value: *rhs,
942            },
943            other => fsharp::Stmt::Expr(inner(other)),
944        }
945    }
946
947    /// The body's trailing expression: an F# decoder's last line is the
948    /// sample it built, with no `return` keyword to write.
949    fn give_back(value: Option<Self::Expr>) -> Self::Stmt {
950        fsharp::Stmt::Return(match value {
951            Some(value) => inner(value),
952            None => fsharp::Expr::Name(Ident::new("v")),
953        })
954    }
955
956    fn when(
957        condition: Self::Expr,
958        then: Self::Block,
959        otherwise: Option<Self::Block>,
960    ) -> Self::Stmt {
961        fsharp::Stmt::If {
962            condition: inner(condition),
963            then,
964            otherwise: otherwise.filter(|body| !body.is_empty()),
965        }
966    }
967
968    /// `for i = 0 to (int bound) - 1 do …` — F#'s own counted loop,
969    /// scoped by the language; the module documentation of
970    /// `crates/lang-cst/src/fsharp/mod.rs` says why this needs none of
971    /// Zig's block-wrapping trick. The counter is `int32` and not
972    /// `uint32` — unlike a `let`-bound local, F#'s `for … = … to … do`
973    /// syntax takes only `int`: measured (`fsi`, .NET SDK 9.0.106),
974    /// `for i = 0u to n - 1u do` is `error FS0001`, "This expression was
975    /// expected to have type 'int' … 'uint32'". `bound` is cast to
976    /// `int` because [`Self::count_of`]'s own answer is `uint32`, the
977    /// wire's own width for a count.
978    ///
979    /// The walk nevertheless reads a counter as a **count**, at the
980    /// width [`Scalar::Index`] gives: `crates/lang-rules/src/emit/
981    /// encode.rs`'s `ascending_by_key` declares `zd_before` at
982    /// [`Scalar::UInt32`] and compares it against the enclosing
983    /// counter, which C# gets away with because C# promotes both to
984    /// `long` and F# does not — measured, `if zd_before = zd_rank then`
985    /// with an `int` counter is `error FS0001`. So the loop counts in
986    /// `int`, as the syntax requires, and the body opens with
987    /// `let i = uint32 i`: F# admits the shadow inside the loop's own
988    /// scope (measured, `fsi`, .NET SDK 9.0.106) and every use behind it
989    /// then sees the width the walk assumed. An index into a .NET array
990    /// crosses `Self::element_at`'s own `int` conversion either way.
991    fn count_up(counter: &str, bound: Self::Expr, body: Self::Block) -> Self::Stmt {
992        let mut inside = Vec::with_capacity(body.len() + 1);
993        inside.push(fsharp::Stmt::Let {
994            name: Ident::new(counter),
995            type_: Some(fsharp::Type::Primitive(fsharp::Primitive::UInt32)),
996            value: fsharp::Expr::call("uint32", vec![fsharp::Expr::Name(Ident::new(counter))]),
997            mutable: false,
998        });
999        inside.extend(body);
1000        fsharp::Stmt::CountUp {
1001            counter: Ident::new(counter),
1002            type_: Box::new(fsharp::Type::Primitive(fsharp::Primitive::Int32)),
1003            bound: Box::new(fsharp::Expr::call("int", vec![inner(bound)])),
1004            body: inside,
1005        }
1006    }
1007
1008    fn until(condition: Self::Expr, body: Self::Block) -> Self::Stmt {
1009        fsharp::Stmt::Until {
1010            condition: inner(condition),
1011            body,
1012        }
1013    }
1014
1015    fn leave() -> Self::Stmt {
1016        fsharp::Stmt::Leave
1017    }
1018
1019    fn note(doc: Doc) -> Self::Stmt {
1020        fsharp::Stmt::Comment(doc)
1021    }
1022
1023    /// `value |> ignore` — F#'s spelling of a value read for its effect
1024    /// and not kept.
1025    fn drop_value(value: Self::Expr) -> Self::Stmt {
1026        fsharp::Stmt::Expr(fsharp::Expr::Call {
1027            func: Box::new(fsharp::Expr::Name(Ident::new("ignore"))),
1028            args: vec![inner(value)],
1029        })
1030    }
1031
1032    /// No statement: F# diagnoses neither an unused parameter nor an
1033    /// unused local (measured, `fsc` 9.0.106,
1034    /// `--warnaserror+ --warn:5`), so there is nothing here to say.
1035    fn mark_used(_name: &str) -> Vec<Self::Stmt> {
1036        Vec::new()
1037    }
1038
1039    /// `place <- zd_dec_T r behaviour` — through `Some` where the place
1040    /// is an `@optional` member's slot: the decoder answers a bare `T`
1041    /// and F#'s `'T option` needs it wrapped. Curried, as every call to
1042    /// a generated codec is — [`fsharp::Expr::CurriedCall`]'s
1043    /// documentation carries why.
1044    fn read_call(
1045        callee: &str,
1046        place: Self::Expr,
1047        _form: Handoff,
1048        behaviour: Self::Expr,
1049    ) -> Self::Stmt {
1050        let decoded = fsharp::Expr::curried_call(
1051            callee,
1052            vec![fsharp::Expr::Name(Ident::new("r")), inner(behaviour)],
1053        );
1054        match place {
1055            FsharpExpr::OptionalSlot(field) => fsharp::Stmt::Assign {
1056                place: field,
1057                value: fsharp::Expr::Some_(Box::new(decoded)),
1058            },
1059            FsharpExpr::Plain(field) => fsharp::Stmt::Assign {
1060                place: field,
1061                value: decoded,
1062            },
1063        }
1064    }
1065
1066    /// `v <- zd_dec_T(r, zd_tc);` — a `typedef`, handed straight on.
1067    ///
1068    /// The trait's default is C's out-parameter shape; this column's
1069    /// decoders answer their value, exactly as Zig's, C#'s, D's, Go's,
1070    /// Java's, Python's and TypeScript's do, for each backend's own
1071    /// reason.
1072    fn read_forward(callee: &str) -> Self::Stmt {
1073        Self::read_call(
1074            callee,
1075            Self::whole("v"),
1076            Handoff::Value,
1077            Self::name("zd_tc"),
1078        )
1079    }
1080
1081    fn block(statements: Vec<Self::Stmt>) -> Self::Block {
1082        statements
1083    }
1084
1085    fn into_statements(block: Self::Block) -> Vec<Self::Stmt> {
1086        block
1087    }
1088
1089    /// `v <- Array.zeroCreate<T> (int count)`, and the fill after it.
1090    /// `Array.zeroCreate` answers an array of `T`'s own zero value —
1091    /// `null` for a class-typed `T`, which the fill then overwrites
1092    /// slot by slot before anything reads it, exactly as C's
1093    /// allocate-then-fill does.
1094    /// `Array.zeroCreate<T> n`, and — where the elements are written
1095    /// into rather than assigned whole — one constructed instance per
1096    /// slot.
1097    ///
1098    /// `Array.zeroCreate` gives every slot `Unchecked.defaultof<T>`,
1099    /// which for the classes every aggregate becomes is `null`. A
1100    /// decoder that then writes `v.[i].Key <- …` dereferences it:
1101    /// measured on `matrix/cases/map-key-long`, whose three entries came
1102    /// back as `System.NullReferenceException` and cost the read
1103    /// direction of the cell. The C# column's `make_room` fills the
1104    /// slots for the same reason, and `assigned_whole` is the walk's own
1105    /// answer for when it need not — a primitive element is assigned as
1106    /// a whole and has no null to dereference.
1107    fn make_room(
1108        base: &str,
1109        element: Self::Type,
1110        count: Self::Expr,
1111        assigned_whole: bool,
1112        fill: Vec<Self::Stmt>,
1113    ) -> Vec<Self::Stmt> {
1114        let mut out = vec![fsharp::Stmt::Assign {
1115            place: fsharp::Expr::Name(Ident::new(base)),
1116            value: fsharp::Expr::ArrayZeroCreate {
1117                element: Box::new(element.tree.clone()),
1118                count: Box::new(fsharp::Expr::call("int", vec![inner(count)])),
1119            },
1120        }];
1121        if !assigned_whole {
1122            if let fsharp::Type::Named(name) = &element.tree {
1123                out.push(Self::count_up(
1124                    "zd_e",
1125                    Self::count_of(base, None),
1126                    vec![fsharp::Stmt::Assign {
1127                        place: fsharp::Expr::Index {
1128                            value: Box::new(fsharp::Expr::Name(Ident::new(base))),
1129                            // `int zd_e`: [`Self::count_up`] gives its
1130                            // counter the width the walk reads a count
1131                            // at, and a .NET array indexes by `int`.
1132                            index: Box::new(fsharp::Expr::call(
1133                                "int",
1134                                vec![fsharp::Expr::Name(Ident::new("zd_e"))],
1135                            )),
1136                        },
1137                        value: fsharp::Expr::New {
1138                            type_: name.clone(),
1139                            args: Vec::new(),
1140                        },
1141                    }],
1142                ));
1143            }
1144        }
1145        out.extend(fill);
1146        out
1147    }
1148
1149    /// `v <- Array.sub v 0 (int count)` — §7.3.1.2.1.12.3's TRIM,
1150    /// applied after every element has been read. `Array.sub` is
1151    /// curried, like every F# standard-library array function.
1152    fn keep_first(base: &str, count: Self::Expr) -> Vec<Self::Stmt> {
1153        vec![fsharp::Stmt::Assign {
1154            place: fsharp::Expr::Name(Ident::new(base)),
1155            value: fsharp::Expr::curried_call(
1156                "Array.sub",
1157                vec![
1158                    fsharp::Expr::Name(Ident::new(base)),
1159                    fsharp::Expr::int(0),
1160                    fsharp::Expr::call("int", vec![inner(count)]),
1161                ],
1162            ),
1163        }]
1164    }
1165
1166    /// The subscript, with nothing around it: an F# array element is a
1167    /// real settable place at every element type.
1168    fn sequence_element(
1169        base: &str,
1170        index: Self::Expr,
1171        _element: Self::Type,
1172    ) -> (Vec<Self::Stmt>, Self::Expr, Vec<Self::Stmt>) {
1173        (
1174            Vec::new(),
1175            Self::index(Self::elements(base), index),
1176            Vec::new(),
1177        )
1178    }
1179
1180    // -- assembly ------------------------------------------------------
1181
1182    /// `let zd_enc_T (w: ZdWire.Writer) (v: T) : unit = …` and
1183    /// `let zd_dec_T (r: ZdWire.Reader) (zd_tc: ZdWire.TryConstruct) : T
1184    /// = …`.
1185    ///
1186    /// A forward declaration is answered with nothing: `module rec`
1187    /// gives every `let` in the file mutual visibility regardless of
1188    /// where it is written, so a recursive type needs no declaration
1189    /// ahead of its body — the module documentation of
1190    /// `crates/lang-cst/src/fsharp/mod.rs` carries the measurement.
1191    ///
1192    /// Unlike [`crate::emit::zig::Zig::routine`], this does not scan the
1193    /// body for mutability or unused names: F# diagnoses neither, so
1194    /// every local the walk declares is a plain `let` — a reassignment
1195    /// [`Self::perform`] built as an [`fsharp::Stmt::Assign`] against a
1196    /// `let`-bound name is exactly the case the assembly pass below
1197    /// corrects, by finding every name a body assigns through and
1198    /// widening its declaration to `let mutable`.
1199    fn routine(spec: Routine<Self>) -> Option<Self::Decl> {
1200        let body = spec.body?;
1201        let mut params = vec![fsharp::FnParam {
1202            name: Ident::new(if spec.writing { "w" } else { "r" }),
1203            type_: fsharp::Type::named(if spec.writing {
1204                "ZdWire.Writer"
1205            } else {
1206                "ZdWire.Reader"
1207            }),
1208        }];
1209        if spec.writing {
1210            params.push(fsharp::FnParam {
1211                name: Ident::new("v"),
1212                type_: spec.sample.tree.clone(),
1213            });
1214        }
1215        for (type_, name) in spec.extra {
1216            params.push(fsharp::FnParam {
1217                name: Ident::new(&name),
1218                type_: type_.tree,
1219            });
1220        }
1221        let (returns, body) = if spec.writing {
1222            (fsharp::Type::Primitive(fsharp::Primitive::Unit), body)
1223        } else {
1224            // The sample starts as Table 9's value, through the
1225            // generated defaulter's call — `Routine` carries no `Ast`
1226            // and no `Id<TypeNode>`, only the already-materialised
1227            // type, so the callee is named from the decoder's own
1228            // name, exactly as `crate::emit::zig::Zig::routine` derives
1229            // its own defaulter call. `default_fsharp`'s module
1230            // documentation carries why.
1231            let defaulter = default_fsharp::defaulter_from_decoder(&spec.name);
1232            let mut statements = vec![fsharp::Stmt::Let {
1233                name: Ident::new("v"),
1234                type_: Some(spec.sample.tree.clone()),
1235                value: fsharp::Expr::call(&defaulter, Vec::new()),
1236                mutable: true,
1237            }];
1238            // A `return` the shared walk wrote inside a loop body is the
1239            // one shape `hoist_early_returns` cannot reach: pushing the
1240            // routine's tail into a loop body would run the tail once
1241            // per iteration. Those bodies take the exception instead —
1242            // every `Return` in them becomes `raise (ZdWire.GiveBack())`
1243            // and the whole body goes inside one `try`, with `v` as the
1244            // trailing expression after it. Only those: a `try` around
1245            // a body that never raises would be noise in every other
1246            // decoder in the file, and the hoisting pass is exact for
1247            // every other shape.
1248            if returns_inside_a_loop(&body) {
1249                let mut guarded = body;
1250                give_back_instead_of_returning(&mut guarded);
1251                statements.push(fsharp::Stmt::Guarded { body: guarded });
1252            } else {
1253                statements.extend(body);
1254            }
1255            statements.push(fsharp::Stmt::Return(fsharp::Expr::Name(Ident::new("v"))));
1256            (spec.sample.tree, statements)
1257        };
1258        let body = hoist_early_returns(body);
1259        let mut body = body;
1260        let assigned = assigned_roots(&body);
1261        fix_mutability(&mut body, &assigned);
1262        Some(fsharp::Decl::Let(fsharp::LetDecl {
1263            doc: spec.doc,
1264            name: Ident::new(&spec.name),
1265            params,
1266            type_: Some(returns),
1267            body,
1268        }))
1269    }
1270
1271    /// `place <- zd_def_T ()` — the generated defaulter, not the value
1272    /// inlined, so a member the stream skipped and one this same
1273    /// decoder's `v` already started as (`Self::routine`'s own call to
1274    /// the same function) always agree, by construction, on what
1275    /// Table 9's value is.
1276    ///
1277    /// # Errors
1278    /// Never; the signature matches the emitters around it.
1279    fn default_into(
1280        ast: &Ast,
1281        id: Id<TypeNode>,
1282        place: Self::Expr,
1283    ) -> Result<Self::Stmt, EncodeError> {
1284        let value = fsharp::Expr::call(&default_fsharp::defaulter_name(ast, id), Vec::new());
1285        Ok(match place {
1286            FsharpExpr::OptionalSlot(field) => fsharp::Stmt::Assign {
1287                place: field,
1288                value: fsharp::Expr::Some_(Box::new(value)),
1289            },
1290            FsharpExpr::Plain(field) => fsharp::Stmt::Assign {
1291                place: field,
1292                value,
1293            },
1294        })
1295    }
1296
1297    /// # Errors
1298    /// As [`default_fsharp::member_value`]: a `@default` this column
1299    /// cannot spell is refused rather than silently replaced by
1300    /// Table 9's zero.
1301    fn member_default(
1302        ast: &Ast,
1303        node: &MemberNode,
1304        type_: Id<TypeNode>,
1305        place: Self::Expr,
1306    ) -> Result<Vec<Self::Stmt>, EncodeError> {
1307        let value = default_fsharp::member_value(ast, node, type_)?;
1308        Ok(match place {
1309            FsharpExpr::OptionalSlot(field) => vec![fsharp::Stmt::Assign {
1310                place: field,
1311                value: fsharp::Expr::Some_(Box::new(value)),
1312            }],
1313            FsharpExpr::Plain(field) => vec![fsharp::Stmt::Assign {
1314                place: field,
1315                value,
1316            }],
1317        })
1318    }
1319
1320    /// `Mode.IDLE` — the enum's own scope, which is where the
1321    /// materialiser declared the case.
1322    fn enumerator_constant(ast: &Ast, id: Id<TypeNode>, enumerator: &str) -> Self::Expr {
1323        plain(fsharp::Expr::dot(
1324            fsharp::Expr::Name(Ident::new(&fsharp_type_name(ast, id))),
1325            &field_name(enumerator),
1326        ))
1327    }
1328}
1329
1330// -- the assembly pass ---------------------------------------------------
1331
1332/// F# has no early `return`: a function's value is its body's last
1333/// expression, and nothing before that point can hand the function's
1334/// answer back and stop. The shared codec walk's decoders do exactly
1335/// that — an enumeration's decoder, for one, builds a single
1336/// `if`/`elif`/…/`else` covering every enumerator *and* every
1337/// §7.2.2.7 try-construct fallback, where some arms end with
1338/// `v <- <the decoded value>` (falling through to a trailing
1339/// `give_back`) and others end with a mid-body
1340/// [`crate::emit::CodecTarget::give_back`] of their own — a
1341/// [`fsharp::Stmt::Return`] that is not the whole chain's last
1342/// statement, with a plain `v` still to come after the `if`/`elif`
1343/// chain closes.
1344///
1345/// This pass turns that into what F# can run: [`splice_tail`] pushes
1346/// whatever textually followed a `Return`-containing `if` down into
1347/// every one of its branches that does not already end in one,
1348/// recursively through an `elif` chain's nested `otherwise`s and
1349/// through loop bodies, until nothing in the function is left dangling
1350/// after a statement that was supposed to have ended it.
1351///
1352/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
1353/// The tree walked here is the one this generator emitted and owns, so
1354/// it holds no cycle; the depth is that body's height, and a defaulter
1355/// body is a handful of statements deep.
1356fn hoist_early_returns(body: Vec<fsharp::Stmt>) -> Vec<fsharp::Stmt> {
1357    // A right fold: the tail spliced into statement `i` is everything
1358    // that followed it, already folded — so a chain of several
1359    // early-return `if`s each gets exactly what came after *it*, not
1360    // what came after the first one.
1361    let mut acc: Vec<fsharp::Stmt> = Vec::new();
1362    for stmt in body.into_iter().rev() {
1363        let stmt = recurse_into_nested(stmt);
1364        // A statement with no `Return` anywhere inside it neither ends
1365        // the function nor leaves anything dangling after itself, so
1366        // the tail belongs after it and nowhere else. Handing it to
1367        // `splice_tail` would reach the same arrangement by copying the
1368        // whole tail, and this pass runs once per statement, so the
1369        // copy is what turns a decoder's body quadratic in its own
1370        // length for no gain.
1371        if contains_return(core::slice::from_ref(&stmt)) {
1372            acc = splice_tail(vec![stmt], &acc);
1373        } else {
1374            acc.insert(0, stmt);
1375        }
1376    }
1377    acc
1378}
1379
1380/// Applies [`hoist_early_returns`] inside a statement's own nested
1381/// blocks — an `if`'s branches, a loop's body — without touching the
1382/// statement's place in whatever list holds it.
1383///
1384/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
1385/// The tree walked here is the one this generator emitted and owns, so
1386/// it holds no cycle; the depth is that body's height, and a defaulter
1387/// body is a handful of statements deep.
1388fn recurse_into_nested(stmt: fsharp::Stmt) -> fsharp::Stmt {
1389    match stmt {
1390        fsharp::Stmt::If {
1391            condition,
1392            then,
1393            otherwise,
1394        } => fsharp::Stmt::If {
1395            condition,
1396            then: hoist_early_returns(then),
1397            otherwise: otherwise.map(hoist_early_returns),
1398        },
1399        fsharp::Stmt::CountUp {
1400            counter,
1401            type_,
1402            bound,
1403            body,
1404        } => fsharp::Stmt::CountUp {
1405            counter,
1406            type_,
1407            bound,
1408            body: hoist_early_returns(body),
1409        },
1410        fsharp::Stmt::Until { condition, body } => fsharp::Stmt::Until {
1411            condition,
1412            body: hoist_early_returns(body),
1413        },
1414        other => other,
1415    }
1416}
1417
1418/// Appends `tail` to `body`, unless `body` already
1419/// [`always_returns`] — in which case `tail` is unreachable and is
1420/// dropped — pushing `tail` down into an unterminated `if`'s branches
1421/// instead of after it when `body`'s last statement is one.
1422///
1423/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
1424/// The tree walked here is the one this generator emitted and owns, so
1425/// it holds no cycle; the depth is that body's height, and a defaulter
1426/// body is a handful of statements deep.
1427fn splice_tail(mut body: Vec<fsharp::Stmt>, tail: &[fsharp::Stmt]) -> Vec<fsharp::Stmt> {
1428    if tail.is_empty() || always_returns(&body) {
1429        return body;
1430    }
1431    match body.pop() {
1432        Some(fsharp::Stmt::If {
1433            condition,
1434            then,
1435            otherwise,
1436        }) => {
1437            // Only an `if` that can hand the function its answer needs
1438            // the tail pushed inside it. An `if` with no `Return` on any
1439            // path is a plain unit-valued statement that falls through,
1440            // and `if c then A else B` followed by `T` runs exactly what
1441            // `if c then A; T else B; T` runs — but the second spelling
1442            // writes `T` twice, and doing that once per statement is a
1443            // body that doubles per `if`.
1444            //
1445            // The decoders that meet this are every unannotated
1446            // aggregate's: DDS-XTypes 1.3 §7.3.1.2.1.8 makes an
1447            // aggregate with no extensibility annotation APPENDABLE, and
1448            // an APPENDABLE member is read under a guard on whether the
1449            // stream still carries it, so the walk emits several such
1450            // fall-through `if`s per member and the copies compound
1451            // across all of them.
1452            let returns_inside = contains_return(&then)
1453                || otherwise
1454                    .as_ref()
1455                    .is_some_and(|otherwise| contains_return(otherwise));
1456            if !returns_inside {
1457                body.push(fsharp::Stmt::If {
1458                    condition,
1459                    then,
1460                    otherwise,
1461                });
1462                body.extend_from_slice(tail);
1463                return body;
1464            }
1465            let then = splice_tail(then, tail);
1466            let otherwise = splice_tail(otherwise.unwrap_or_default(), tail);
1467            body.push(fsharp::Stmt::If {
1468                condition,
1469                then,
1470                otherwise: Some(otherwise),
1471            });
1472            body
1473        }
1474        Some(last) => {
1475            body.push(last);
1476            body.extend_from_slice(tail);
1477            body
1478        }
1479        None => tail.to_vec(),
1480    }
1481}
1482
1483/// Whether any path through `body` reaches a [`fsharp::Stmt::Return`]
1484/// at all, however deeply nested.
1485///
1486/// The weaker sibling of [`always_returns`], and the question
1487/// [`splice_tail`] has to ask before it copies: a statement that cannot
1488/// return needs nothing pushed into it, because F# runs whatever
1489/// follows it either way. A loop body counts, because a `Return` there
1490/// is just as much the function's answer as one at the top level and
1491/// F# has no more of a way to express it.
1492///
1493/// zerodds-lint: recursion-depth 64 (statement nesting, which the walk
1494/// adds one level of per type level and so bounds by `MAX_TYPE_DEPTH`)
1495fn contains_return(body: &[fsharp::Stmt]) -> bool {
1496    body.iter().any(|statement| match statement {
1497        fsharp::Stmt::Return(_) => true,
1498        fsharp::Stmt::If {
1499            then, otherwise, ..
1500        } => {
1501            contains_return(then)
1502                || otherwise
1503                    .as_ref()
1504                    .is_some_and(|otherwise| contains_return(otherwise))
1505        }
1506        fsharp::Stmt::CountUp { body, .. }
1507        | fsharp::Stmt::Until { body, .. }
1508        | fsharp::Stmt::Guarded { body } => contains_return(body),
1509        fsharp::Stmt::Let { .. }
1510        | fsharp::Stmt::Assign { .. }
1511        | fsharp::Stmt::Expr(_)
1512        | fsharp::Stmt::Leave
1513        // Already the raise, so no longer a `Return` for this pass to
1514        // find; `routine` rewrites the ones it cannot hoist and does it
1515        // after `hoist_early_returns` has run.
1516        | fsharp::Stmt::GiveBack
1517        | fsharp::Stmt::Comment(_) => false,
1518    })
1519}
1520
1521/// Whether any [`fsharp::Stmt::Return`] in `body` sits inside a loop.
1522///
1523/// The question [`Fsharp::routine`] asks before it reaches for
1524/// [`fsharp::Stmt::Guarded`]. [`hoist_early_returns`] answers every
1525/// other shape by pushing what followed a return down into the branches
1526/// that could reach it; a loop body is where that stops working, because
1527/// what followed the loop must run once and not once per iteration.
1528///
1529/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
1530/// The tree walked here is the one this generator emitted and owns, so
1531/// it holds no cycle; the depth is that body's height.
1532fn returns_inside_a_loop(body: &[fsharp::Stmt]) -> bool {
1533    body.iter().any(|statement| match statement {
1534        fsharp::Stmt::CountUp { body, .. } | fsharp::Stmt::Until { body, .. } => {
1535            contains_return(body)
1536        }
1537        fsharp::Stmt::If {
1538            then, otherwise, ..
1539        } => {
1540            returns_inside_a_loop(then)
1541                || otherwise
1542                    .as_ref()
1543                    .is_some_and(|otherwise| returns_inside_a_loop(otherwise))
1544        }
1545        fsharp::Stmt::Guarded { body } => returns_inside_a_loop(body),
1546        _ => false,
1547    })
1548}
1549
1550/// Turns every [`fsharp::Stmt::Return`] in `body` into
1551/// [`fsharp::Stmt::GiveBack`].
1552///
1553/// All of them and not only the ones inside a loop: the guard is around
1554/// the whole body, so once it exists the cheapest correct rule is that
1555/// nothing inside it is a trailing expression. Every one of the shared
1556/// walk's thirteen early returns is `give_back(None)`, which
1557/// [`Fsharp::give_back`] spells `v` — the same value
1558/// [`Fsharp::routine`] writes after the guard — so the routine answers
1559/// what it answered before.
1560///
1561/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
1562/// The tree walked here is the one this generator emitted and owns, so
1563/// it holds no cycle; the depth is that body's height.
1564fn give_back_instead_of_returning(body: &mut [fsharp::Stmt]) {
1565    for statement in body.iter_mut() {
1566        match statement {
1567            fsharp::Stmt::Return(_) => *statement = fsharp::Stmt::GiveBack,
1568            fsharp::Stmt::If {
1569                then, otherwise, ..
1570            } => {
1571                give_back_instead_of_returning(then);
1572                if let Some(otherwise) = otherwise {
1573                    give_back_instead_of_returning(otherwise);
1574                }
1575            }
1576            fsharp::Stmt::CountUp { body, .. }
1577            | fsharp::Stmt::Until { body, .. }
1578            | fsharp::Stmt::Guarded { body } => give_back_instead_of_returning(body),
1579            _ => {}
1580        }
1581    }
1582}
1583
1584/// Whether `body` is certain, on every path through it, to end in a
1585/// [`fsharp::Stmt::Return`] — so that anything spliced after it would
1586/// be unreachable.
1587///
1588/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
1589/// The tree walked here is the one this generator emitted and owns, so
1590/// it holds no cycle; the depth is that body's height, and a defaulter
1591/// body is a handful of statements deep.
1592fn always_returns(body: &[fsharp::Stmt]) -> bool {
1593    match body.last() {
1594        Some(fsharp::Stmt::Return(_)) => true,
1595        Some(fsharp::Stmt::If {
1596            then,
1597            otherwise: Some(otherwise),
1598            ..
1599        }) => always_returns(then) && always_returns(otherwise),
1600        _ => false,
1601    }
1602}
1603
1604/// Every base name the body assigns through — `v` for `v.x <- …` and
1605/// `v.[i] <- …` alike — which is what decides `let mutable`.
1606fn assigned_roots(body: &[fsharp::Stmt]) -> Vec<String> {
1607    let mut out = Vec::new();
1608    collect_assigned(body, &mut out);
1609    out
1610}
1611
1612/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
1613/// The tree walked here is the one this generator emitted and owns, so
1614/// it holds no cycle; the depth is that body's height, and a defaulter
1615/// body is a handful of statements deep.
1616fn collect_assigned(body: &[fsharp::Stmt], out: &mut Vec<String>) {
1617    for statement in body {
1618        match statement {
1619            fsharp::Stmt::Assign { place, .. } => {
1620                if let Some(root) = root_name(place) {
1621                    if !out.contains(&root) {
1622                        out.push(root);
1623                    }
1624                }
1625            }
1626            fsharp::Stmt::If {
1627                then, otherwise, ..
1628            } => {
1629                collect_assigned(then, out);
1630                if let Some(otherwise) = otherwise {
1631                    collect_assigned(otherwise, out);
1632                }
1633            }
1634            fsharp::Stmt::CountUp { body, .. }
1635            | fsharp::Stmt::Until { body, .. }
1636            | fsharp::Stmt::Guarded { body } => {
1637                collect_assigned(body, out);
1638            }
1639            _ => {}
1640        }
1641    }
1642}
1643
1644/// The base a place is reached from: `v` for `v.x.[i]`.
1645///
1646/// zerodds-lint: recursion-depth 64 (place-expression nesting the walk
1647/// builds). The tree walked here is the one this generator emitted and
1648/// owns, so it holds no cycle; the depth is that expression's height.
1649fn root_name(place: &fsharp::Expr) -> Option<String> {
1650    match place {
1651        fsharp::Expr::Name(name) => Some(name.as_str().to_string()),
1652        fsharp::Expr::Dot { value, .. }
1653        | fsharp::Expr::Index { value, .. }
1654        | fsharp::Expr::Paren(value) => root_name(value),
1655        _ => None,
1656    }
1657}
1658
1659/// Turn every `let` the body assigns through into `let mutable`: F#
1660/// refuses `<-` against a binding that is not one.
1661///
1662/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
1663/// The tree walked here is the one this generator emitted and owns, so
1664/// it holds no cycle; the depth is that body's height, and a defaulter
1665/// body is a handful of statements deep.
1666fn fix_mutability(body: &mut [fsharp::Stmt], assigned: &[String]) {
1667    for statement in body {
1668        match statement {
1669            fsharp::Stmt::Let { name, mutable, .. } => {
1670                if assigned.iter().any(|root| root == name.as_str()) {
1671                    *mutable = true;
1672                }
1673            }
1674            fsharp::Stmt::If {
1675                then, otherwise, ..
1676            } => {
1677                fix_mutability(then, assigned);
1678                if let Some(otherwise) = otherwise {
1679                    fix_mutability(otherwise, assigned);
1680                }
1681            }
1682            fsharp::Stmt::CountUp { body, .. }
1683            | fsharp::Stmt::Until { body, .. }
1684            | fsharp::Stmt::Guarded { body } => {
1685                fix_mutability(body, assigned);
1686            }
1687            _ => {}
1688        }
1689    }
1690}
1691
1692/// The bound a `string`, a `sequence` or a `map` carries, as the suffix
1693/// that keeps two of them apart.
1694///
1695/// The tag names the routine a hoisted collection's codec is emitted
1696/// under, and one body is emitted per name. With the bound left out,
1697/// `sequence<long>` and `sequence<long, 4>` are one name and whichever
1698/// was declared first decides: measured on
1699/// `@final struct S { sequence<long> b; sequence<long, 4> a; };`, the
1700/// generated F# held no `zd_over` check at all, so `a` accepted a
1701/// stream of any length; with the members the other way round both got
1702/// the bounded body, so `b` refused — and truncated — a length the IDL
1703/// puts no limit on. `string<4>` beside `string` and `wstring<4>`
1704/// beside `wstring` collapse the same way, onto the trimming call.
1705/// Either way the reader answered with a value the IDL does not
1706/// describe and said nothing, which is why the bound is part of the
1707/// name. The same suffix the Zig column carries.
1708///
1709/// An `array` needs nothing here: its `dims` are already in the tag,
1710/// under the `Array` arm below.
1711fn bound_tag(ast: &Ast, bound: &zerodds_ast::Bound) -> String {
1712    match bound.expr() {
1713        None => String::new(),
1714        // `_b` and not a bare `_n`: `seq_long_4` is what the `Array`
1715        // arm answers for an array of four `long`, and the two are
1716        // different wire forms.
1717        Some(expr) => match ast.eval_integral(expr) {
1718            Ok(limit) => format!("_b{limit}"),
1719            // As the array's `_x`: a bound that does not evaluate is
1720            // still not the unbounded form, and the materialiser
1721            // reports the expression that would not evaluate.
1722            Err(_) => "_bx".to_string(),
1723        },
1724    }
1725}
1726
1727/// The name a codec function takes for the type it codes: the IDL kind
1728/// and not the F# spelling, structurally for a collection and the
1729/// flattened F# name for a declaration — the same scheme
1730/// [`crate::emit::zig::type_tag`] carries, for the reasons its own
1731/// documentation states.
1732///
1733/// zerodds-lint: recursion-depth 64 (collection nesting; bounded by `MAX_TYPE_DEPTH`)
1734#[must_use]
1735pub fn type_tag(ast: &Ast, id: Id<TypeNode>, depth: usize) -> String {
1736    if depth > MAX_TYPE_DEPTH {
1737        return "unresolved".to_string();
1738    }
1739    let node = match ast.type_(id) {
1740        Ok(node) => node,
1741        Err(_) => return "unresolved".to_string(),
1742    };
1743    match &node.kind {
1744        TypeKind::Primitive(kind) => kind.as_str().replace(' ', "_"),
1745        TypeKind::String {
1746            width: CharWidth::Char,
1747            bound,
1748        } => format!("string{}", bound_tag(ast, bound)),
1749        TypeKind::String {
1750            width: CharWidth::WChar,
1751            bound,
1752        } => format!("wstring{}", bound_tag(ast, bound)),
1753        TypeKind::Fixed { digits, scale } => format!("fixed_{digits}_{scale}"),
1754        TypeKind::Sequence { element, bound } => match element.resolved() {
1755            Some(element) => format!(
1756                "seq{}_{}",
1757                bound_tag(ast, bound),
1758                type_tag(ast, element, depth + 1)
1759            ),
1760            None => format!("seq_unresolved{}", bound_tag(ast, bound)),
1761        },
1762        TypeKind::Array { element, dims } => {
1763            let mut name = match element.resolved() {
1764                Some(element) => format!("arr_{}", type_tag(ast, element, depth + 1)),
1765                None => "arr_unresolved".to_string(),
1766            };
1767            for dim in dims {
1768                match ast.eval_integral(*dim) {
1769                    Ok(length) => name.push_str(&format!("_{length}")),
1770                    Err(_) => name.push_str("_x"),
1771                }
1772            }
1773            name
1774        }
1775        TypeKind::Map { key, value, bound } => {
1776            let half = |half: Option<Id<TypeNode>>| match half {
1777                Some(target) => type_tag(ast, target, depth + 1),
1778                None => "unresolved".to_string(),
1779            };
1780            format!(
1781                "map{}_{}_{}",
1782                bound_tag(ast, bound),
1783                half(key.resolved()),
1784                half(value.resolved())
1785            )
1786        }
1787        TypeKind::Alias { aliased } => match aliased.resolved() {
1788            Some(target) => type_tag(ast, target, depth + 1),
1789            None => "unresolved".to_string(),
1790        },
1791        TypeKind::ValueBox { boxed } => match boxed.resolved() {
1792            Some(target) => format!("box_{}", type_tag(ast, target, depth + 1)),
1793            None => "box_unresolved".to_string(),
1794        },
1795        TypeKind::Structure { .. }
1796        | TypeKind::Union { .. }
1797        | TypeKind::Enumeration { .. }
1798        | TypeKind::Bitmask { .. }
1799        | TypeKind::Bitset { .. }
1800        | TypeKind::Valuetype { .. }
1801        | TypeKind::Native => fsharp_type_name(ast, id),
1802        TypeKind::Any => "any".to_string(),
1803        TypeKind::ValueBase => "valuebase".to_string(),
1804        TypeKind::ObjectRef { .. } => "objectref".to_string(),
1805        TypeKind::AnnotationType { .. } => "annotation".to_string(),
1806    }
1807}
1808
1809/// The module the generator's `Types.fs` declares, named identically
1810/// with `crates/lang-rules/src/adapter/fsharp.rs`'s own [`MODULE_NAME`]
1811/// so a generated `main.fsx` opens the one the adapter actually wrote.
1812#[must_use]
1813pub fn module_name() -> &'static str {
1814    MODULE_NAME
1815}