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}