Skip to main content

zerodds_lang_rules/emit/
node.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The Node (plain JavaScript) backend of the shared codec walk.
4//!
5//! DDS-XTypes 1.3 §7.4 from the clause, the same as `runtime/c`,
6//! `runtime/typescript` and the rest do, and none of them calls another.
7//! Where two columns disagree on an octet, one has misread the
8//! specification — which is the only reason to have a second column.
9//!
10//! # The value model, kept from TypeScript
11//!
12//! Plain JavaScript and TypeScript share one runtime: `number` is an
13//! IEEE 754 binary64 and `bigint` is exact and unbounded, which is a
14//! fact about the *language*, not about its type checker. So this
15//! column draws the same boundary [`crate::emit::typescript::TypeScript::scalar`]
16//! does — every integer to `i32` and every `f32`/`f64` is a `number`, a
17//! `long long` and an `unsigned long long` are `bigint`, a `char` and a
18//! `wchar` are a one-character `string`, and a `long double` is
19//! `{ octets: Uint8Array(16) }` and never a `number`, because a binary64
20//! would drop sixty bits of its significand at the assignment. Deriving
21//! a different model from [`crate::emit::zig`] or [`crate::emit::lua`]
22//! instead — a `char` as a code-point integer — would be answering a
23//! question the language has already answered the other way, through no
24//! choice this column makes.
25//!
26//! # Where this column is simpler than TypeScript's, and why
27//!
28//! Every one of these follows from having no compiler holding a
29//! `number`, a `bigint` and a discriminated union's literal types apart
30//! — not from skipping a check TypeScript's runtime still needs:
31//!
32//! - **No union-arm narrowing.** [`crate::emit::typescript::TypeScript::case_member`]
33//!   builds `(v as Extract<typeof v, { one: unknown }>).one` because
34//!   `tsc` refuses `v.one` on a value typed as the whole discriminated
35//!   union. There is no such refusal here: [`Node::case_member`] is
36//!   `v.one`, exactly as [`crate::emit::lua::Lua::case_member`] is
37//!   `v.one` for the same reason.
38//! - **No literal-type rewriting.** TypeScript's `cast` rewrites a union
39//!   label's literal so its *type* matches what `===` narrows on;
40//!   nothing here narrows a type, so a label is the value the walk
41//!   handed over, in the holder [`Node::cast`] already puts it in.
42//! - **No `noUncheckedIndexedAccess` assertion.** [`Node::index`] is
43//!   `v[i]`, not `v[i]!` — that operator does not parse outside
44//!   TypeScript, and a plain JavaScript array answers `v[i]` the same
45//!   way regardless of what a type checker would have said about it.
46//! - **`@external` needs nothing.** Every object this column builds is
47//!   already a reference — there is no pointer to take and no allocator
48//!   to route it through, which is also why the trait's defaults for
49//!   every `external_*` seam are left untouched, exactly as
50//!   [`crate::emit::typescript`] leaves them.
51//!
52//! # Where this column still needs TypeScript's coercion functions
53//!
54//! [`Node::cast`] still calls {@link zdNumber}, {@link zdBigInt} and
55//! {@link zdChar} by name, and [`Node::store`] still wraps every scalar
56//! read in [`Node::cast`] before it reaches its member. Not because
57//! plain JavaScript refuses a mismatched assignment — it does not — but
58//! because the shared decode walk's `primitive_read_at` casts a `wchar`
59//! read to [`crate::emit::Scalar::UInt16`] before this seam ever sees
60//! it, which for a column that holds `wchar` in a `string` is the wrong
61//! holder for the member; [`Node::store`]'s cast is what turns it back.
62//! The exact defect this override closes is `crates/lang-rules/src/emit/typescript.rs`'s
63//! `store` documentation, verified afresh here rather than assumed: this
64//! runtime's `readWchar` and `writeWchar` are one-character strings top
65//! to bottom, and the walk's hard-coded `UInt16` narrowing sits between
66//! two of the four call sites regardless of which column reads it.
67
68use super::{CodecTarget, Constant, Handle, Handoff, Routine, Scalar};
69use crate::adapter::node::field_name;
70use crate::encode::EncodeError;
71use zerodds_ast::{Ast, MemberNode, TypeKind, TypeNode};
72use zerodds_ast_core::Id;
73use zerodds_lang_cst::common::{Doc, Ident, Literal};
74use zerodds_lang_cst::node::{self, Expr, Function, Stat};
75
76/// The Node backend.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct Node;
79
80/// How deep a chain of typedefs and collections is followed.
81const MAX_TYPE_DEPTH: usize = 64;
82
83/// The name the generated file requires the runtime under:
84/// `const zdwire = require("./zdwire.js");`.
85const RUNTIME: &str = "zdwire";
86
87/// The type name [`CodecTarget::named`] is handed for C's signed
88/// `char`; the member is a one-character `string` and the signed
89/// reading is the two's-complement arithmetic [`Node::cast`] writes out.
90const SIGNED_CHAR: &str = "signed char";
91
92/// A JavaScript holder, carried alongside the tree so [`Node::cast`] and
93/// [`Node::store`] know which of the three coercions — or none — a value
94/// needs on its way into a place.
95///
96/// Not a type in any sense the language enforces: plain JavaScript
97/// checks nothing here, and every field but the tag itself is
98/// unreachable at run time. It exists for the same reason
99/// [`crate::emit::lua::LuaType`] does — "the nominal type a local is
100/// annotated with, which in Lua is nothing at all" — with `"boolean"`,
101/// `"number"`, `"bigint"` and `"string"` as the tags [`Node::cast`] acts
102/// on and every other tag passed through unchanged.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct NodeType(pub String);
105
106impl NodeType {
107    fn tag(text: &str) -> Self {
108        Self(text.to_string())
109    }
110}
111
112impl Node {
113    /// `write_u32` as `writeU32`.
114    fn camel(method: &str) -> String {
115        let mut out = String::new();
116        let mut rising = false;
117        for character in method.chars() {
118            if character == '_' {
119                rising = true;
120                continue;
121            }
122            if rising {
123                out.extend(character.to_uppercase());
124                rising = false;
125            } else {
126                out.push(character);
127            }
128        }
129        out
130    }
131
132    /// A dotted path off the runtime: `zdwire.WcharWidth.Octets2`.
133    fn runtime_name(path: &str) -> Expr {
134        let mut segments = path.split('.');
135        let mut expr = Expr::member(Expr::name(RUNTIME), segments.next().unwrap_or(path));
136        for segment in segments {
137            expr = Expr::member(expr, segment);
138        }
139        expr
140    }
141
142    /// A reader's body, with the sample it answers with around it:
143    /// `let v = zd_def_T(); … return v;`. Table 9's value through the
144    /// generated defaulter, so every position the stream does not carry
145    /// is already what the clause says before an octet is read.
146    fn reading_body(name: &str, body: Vec<Stat>) -> Vec<Stat> {
147        let defaulter = crate::default_node::defaulter_from_decoder(name);
148        let mut statements = vec![Stat::Declare {
149            doc: Doc::none(),
150            kind: node::VarKind::Let,
151            name: Ident::new("v"),
152            value: Some(Expr::call(&defaulter, Vec::new())),
153        }];
154        statements.extend(body);
155        statements.push(Stat::Return(Some(Expr::name("v"))));
156        statements
157    }
158}
159
160impl CodecTarget for Node {
161    type Expr = Expr;
162    type Stmt = Stat;
163    type Block = Vec<Stat>;
164    type Type = NodeType;
165    type Decl = Stat;
166
167    // -- types --------------------------------------------------------
168
169    /// `number` to `i32`, `bigint` beyond it, a one-character `string`
170    /// for a `char` — the boundary [`crate::emit::typescript::TypeScript::scalar`]
171    /// draws, kept because it is the language's own and not TypeScript's.
172    fn scalar(kind: Scalar) -> Self::Type {
173        NodeType::tag(match kind {
174            Scalar::Bool => "boolean",
175            Scalar::Char => "string",
176            Scalar::Int8
177            | Scalar::UInt8
178            | Scalar::Int16
179            | Scalar::UInt16
180            | Scalar::Int32
181            | Scalar::UInt32
182            | Scalar::Index
183            | Scalar::Size
184            | Scalar::Float
185            | Scalar::Double => "number",
186            Scalar::Int64 | Scalar::UInt64 => "bigint",
187            Scalar::LongDouble => "float128",
188        })
189    }
190
191    /// The tag, preserved exactly: this is how [`Node::cast`] recognises
192    /// `"signed char"`, and how a call site that names a type by its own
193    /// spelling is answered honestly rather than folded into a generic
194    /// tag that would hide the collision.
195    fn named(name: &str) -> Self::Type {
196        NodeType::tag(name)
197    }
198
199    fn nothing() -> Self::Type {
200        NodeType::tag("nothing")
201    }
202
203    /// Every runtime handle is a plain object; nothing casts one.
204    fn handle(_kind: Handle) -> Self::Type {
205        NodeType::tag("object")
206    }
207
208    /// The member already is the code unit — a one-character `string`
209    /// for both a `char` and a `wchar` — so there is nothing to convert.
210    fn code_point(value: Self::Expr, _narrow: bool) -> Self::Expr {
211        value
212    }
213
214    /// The runtime's policy tables, in `runtime/node/zdwire.js`'s own
215    /// spelling — the same strings `runtime/typescript`'s enums answer
216    /// with, since both runtimes were written against one clause and
217    /// carry the measurements in `crates/ast-layout/src/key.rs` under
218    /// one name apiece.
219    fn constant(which: Constant) -> Self::Expr {
220        Self::runtime_name(match which {
221            Constant::WcharWidth2 => "WcharWidth.Octets2",
222            Constant::WcharWidth4 => "WcharWidth.Octets4",
223            Constant::WcharFixed => "WcharFraming.Fixed",
224            Constant::WcharGiopLengthPrefixed => "WcharFraming.GiopLengthPrefixed",
225            Constant::WstringUnitsStream => "WstringUnitOrder.Stream",
226            Constant::WstringUnitsBigEndianAlways => "WstringUnitOrder.BigEndianAlways",
227            Constant::WstringUnitsByteOrderMark => "WstringUnitOrder.ByteOrderMark",
228            Constant::WstringUnitsHostUnswapped => "WstringUnitOrder.HostUnswapped",
229            Constant::WstringSpecification => "WstringLengthPolicy.Specification",
230            Constant::WstringCharacterCount => "WstringLengthPolicy.CharacterCount",
231            Constant::WstringCharacterCountTerminated => {
232                "WstringLengthPolicy.CharacterCountTerminated"
233            }
234            Constant::WstringSpecificationWideUnits => "WstringLengthPolicy.SpecificationWideUnits",
235            Constant::WstringRosTypeSupport => "WstringLengthPolicy.RosTypeSupport",
236            Constant::WstringOctetsTerminated2 => "WstringLengthPolicy.OctetsTerminated2",
237            Constant::WstringOctetsTerminated4 => "WstringLengthPolicy.OctetsTerminated4",
238            Constant::WstringCharactersTerminated2 => "WstringLengthPolicy.CharactersTerminated2",
239            Constant::FixedDeclared => "FixedDigitCount.Declared",
240            Constant::FixedValuePrecision => "FixedDigitCount.ValuePrecision",
241            Constant::FixedValueStringDigits => "FixedDigitCount.ValueStringDigits",
242            Constant::ParameterMember => "ParameterRole.Member",
243            Constant::ParameterOptional => "ParameterRole.Optional",
244            Constant::TryConstructDiscard => "TryConstruct.Discard",
245            Constant::TryConstructTrim => "TryConstruct.Trim",
246            Constant::TryConstructUseDefault => "TryConstruct.UseDefault",
247            Constant::StatusOk => "Status.Ok",
248            Constant::StatusDiscarded => "Status.Discarded",
249            Constant::StatusImpossibleLength => "Status.ImpossibleLength",
250            Constant::StatusOutOfRange => "Status.OutOfRange",
251            Constant::StatusTruncated => "Status.Truncated",
252        })
253    }
254
255    /// `number` to 32 bits, `bigint` at 64 — the same boundary
256    /// [`Node::scalar`] draws for every other integer.
257    fn unsigned_holder(bits: u16) -> Self::Type {
258        NodeType::tag(if bits > 32 { "bigint" } else { "number" })
259    }
260
261    fn value_type(ast: &Ast, id: Id<TypeNode>) -> Self::Type {
262        value_type_of(ast, id, 0)
263    }
264
265    fn type_name(ast: &Ast, id: Id<TypeNode>) -> String {
266        type_tag(ast, id, 0)
267    }
268
269    fn map_entry_name(_ast: &Ast, _id: Id<TypeNode>) -> String {
270        crate::adapter::node::MAP_ENTRY.to_string()
271    }
272
273    /// Nothing: the declared paths this backend writes go through
274    /// [`Node::enumerator_constant`], which flattens the whole scoped
275    /// name into the one binding it names, so a prefix here would state
276    /// the same thing twice.
277    fn module_prefix(_ast: &Ast, _id: Id<TypeNode>) -> String {
278        String::new()
279    }
280
281    /// The identity, always — [`zerodds_lang_cst::node::escape_member`]'s
282    /// documentation carries why.
283    fn escape(name: &str) -> String {
284        field_name(name)
285    }
286
287    // -- expressions ---------------------------------------------------
288
289    fn name(text: &str) -> Self::Expr {
290        Expr::name(text)
291    }
292
293    fn int(value: i128) -> Self::Expr {
294        Expr::int(value)
295    }
296
297    /// The raw tokens the walk escapes through [`Literal::Int`],
298    /// rewritten into the tree's own boolean where they are one.
299    fn literal(value: Literal) -> Self::Expr {
300        if let Literal::Int(text) = &value {
301            match text.as_str() {
302                "true" => return Expr::bool(true),
303                "false" => return Expr::bool(false),
304                _ => {}
305            }
306        }
307        Expr::Literal(value)
308    }
309
310    /// A `bigint` literal: the number with a trailing `n`. Every value
311    /// the walk hands here is one a `number` cannot carry exactly, so
312    /// the suffix is unconditional.
313    fn wide_int(value: i128) -> Self::Expr {
314        Expr::Literal(Literal::Int(format!("{value}n")))
315    }
316
317    /// The number, with no suffix: JavaScript has one numeric literal
318    /// grammar and no unsigned spelling — `2u` is not a token.
319    fn unsigned_int(value: i128) -> Self::Expr {
320        Expr::int(value)
321    }
322
323    /// `123n`, [`Node::wide_int`]'s spelling: the trait's default is C's
324    /// `UINT64_C(123)`, a macro no JavaScript file declares.
325    fn wide_unsigned(value: u64) -> Self::Expr {
326        Expr::Literal(Literal::Int(format!("{value}n")))
327    }
328
329    /// As [`Node::wide_unsigned`]: a `bigint` is unbounded, so a
330    /// negative pattern is written as the negative number it is.
331    fn wide_signed(value: i128) -> Self::Expr {
332        Expr::Literal(Literal::Int(format!("{value}n")))
333    }
334
335    /// A field of a header the runtime handed back, in the runtime's own
336    /// spelling — the same camelCase table
337    /// `crates/lang-rules/src/emit/typescript.rs`'s override carries,
338    /// because `runtime/node/zdwire.js`'s headers are plain objects with
339    /// the same property names as `runtime/typescript`'s.
340    fn header_field(value: Self::Expr, field: &str) -> Self::Expr {
341        Self::field_of(
342            value,
343            match field {
344                "pid" => "pid",
345                "end" => "end",
346                "ignore" => "ignore",
347                "length" => "length",
348                "member_id" => "memberId",
349                "must_understand" => "mustUnderstand",
350                "extent" => "extent",
351                other => other,
352            },
353        )
354    }
355
356    /// `strcmp` is the walk's name for a three-way code-unit comparison;
357    /// JavaScript's `</>` on strings already compare UTF-16 code units
358    /// (ECMA-262 §7.2.13), so the spelling is a ternary — the same one
359    /// `crates/lang-rules/src/emit/typescript.rs` writes, and the Java
360    /// column's caveat about supplementary code points applies
361    /// unchanged.
362    ///
363    /// A ternary and not the shorter `(l < r) - (l > r)`: subtracting
364    /// two relational results goes through `Boolean`'s numeric
365    /// coercion, where `true - false` is `1` (ECMA-262 §7.1.4), so
366    /// that form returns `+1` for the case C11 §7.24.4 gives `-1`.
367    /// The walk reads the result as `strcmp(..) < 0`, so the inverted
368    /// sign puts a `map`'s entries on the wire in descending key
369    /// order.
370    fn call(callee: &str, args: Vec<Self::Expr>) -> Self::Expr {
371        if callee == "strcmp" {
372            if let [left, right] = args.as_slice() {
373                return Expr::Paren(Box::new(Expr::Ternary {
374                    condition: Box::new(Expr::Binary {
375                        op: "<",
376                        lhs: Box::new(left.clone()),
377                        rhs: Box::new(right.clone()),
378                    }),
379                    then: Box::new(Self::int(-1)),
380                    otherwise: Box::new(Expr::Ternary {
381                        condition: Box::new(Expr::Binary {
382                            op: ">",
383                            lhs: Box::new(left.clone()),
384                            rhs: Box::new(right.clone()),
385                        }),
386                        then: Box::new(Self::int(1)),
387                        otherwise: Box::new(Self::int(0)),
388                    }),
389                }));
390            }
391        }
392        Expr::call(callee, args)
393    }
394
395    fn runtime(receiver: &str, method: &str, args: Vec<Self::Expr>) -> Self::Expr {
396        Expr::Call {
397            callee: Box::new(Expr::member(Expr::name(receiver), &Self::camel(method))),
398            args,
399        }
400    }
401
402    /// `runtime/node` carries the three HDDS axes: `beginPackedElement` and
403    /// `finishPackedElement` on the writer and the reader, the DHEADER pair
404    /// for a sequence element's own length, and `writeWstringUtf8Octets` /
405    /// `readWstringUtf8Octets`.
406    fn carries_hdds_axes() -> bool {
407        true
408    }
409
410    /// A free function the generated file reaches through the runtime's
411    /// own object: `zdwire.<method>(args)`.
412    fn runtime_static(method: &str, args: Vec<Self::Expr>) -> Self::Expr {
413        Expr::Call {
414            callee: Box::new(Expr::member(Expr::name(RUNTIME), &Self::camel(method))),
415            args,
416        }
417    }
418
419    /// A field name with dots is a path through the members that hold a base
420    /// (`parent.x`, XTypes 1.3 §7.5.1.4.1.3): one member step per segment.
421    fn member(base: &str, field: &str) -> Self::Expr {
422        let mut place = Expr::name(base);
423        for segment in field.split('.') {
424            place = Expr::member(place, segment);
425        }
426        place
427    }
428
429    /// The base is the first member `parent` (XTypes 1.3 §7.5.1.4.1.3).
430    fn inherited_path(depth: usize) -> Option<String> {
431        Some("parent.".repeat(depth))
432    }
433
434    fn whole(base: &str) -> Self::Expr {
435        Expr::name(base)
436    }
437
438    fn field_of(value: Self::Expr, field: &str) -> Self::Expr {
439        Expr::member(value, field)
440    }
441
442    /// `v[i]` — a plain subscript; there is no `noUncheckedIndexedAccess`
443    /// assertion to write outside TypeScript.
444    fn index(value: Self::Expr, at: Self::Expr) -> Self::Expr {
445        Expr::Index {
446            object: Box::new(value),
447            index: Box::new(at),
448        }
449    }
450
451    /// `.length`, or the bound where the materialiser fixed one.
452    fn count_of(base: &str, fixed: Option<u64>) -> Self::Expr {
453        match fixed {
454            Some(bound) => Expr::int(i128::from(bound)),
455            None => Expr::member(Expr::name(base), "length"),
456        }
457    }
458
459    fn elements(base: &str) -> Self::Expr {
460        Expr::name(base)
461    }
462
463    /// `v.one` — a union's branches sit beside `_d` on one plain object;
464    /// nothing here needs the narrowing
465    /// [`crate::emit::typescript::TypeScript::case_member`]'s `Extract<>`
466    /// exists for, because nothing checks the type before the property
467    /// is read.
468    fn case_member(base: &str, field: &str) -> Self::Expr {
469        Self::member(base, field)
470    }
471
472    fn entry_half(entry: Self::Expr, key: bool) -> Self::Expr {
473        Expr::member(entry, if key { "key" } else { "value" })
474    }
475
476    fn absent() -> Self::Expr {
477        Expr::Undefined
478    }
479
480    /// Nothing: every string and array this runtime materialises either
481    /// exists or the read has already thrown — there is no allocator
482    /// that can hand back an absent one for a codec to notice, exactly
483    /// as `crates/lang-rules/src/emit/lua.rs`'s answer states for Lua's
484    /// tables.
485    fn materialised(_value: Self::Expr) -> Option<Self::Expr> {
486        None
487    }
488
489    /// The member itself: `v.x` is the value or the key is not there,
490    /// and there is no wrapper to unwrap.
491    fn optional_value(_ast: &Ast, _id: Id<TypeNode>, base: &str, field: &str) -> Self::Expr {
492        Self::member(base, field)
493    }
494
495    fn present(base: &str, field: &str) -> Self::Expr {
496        Expr::Binary {
497            op: "!==",
498            lhs: Box::new(Self::member(base, field)),
499            rhs: Box::new(Self::absent()),
500        }
501    }
502
503    /// `delete v.field` for absence, and nothing for presence — writing
504    /// the member is what makes it present, exactly as
505    /// `crates/lang-rules/src/emit/typescript.rs`'s override reads;
506    /// [`crate::default_node`] writes the same shape for the same
507    /// clause.
508    fn set_present(base: &str, field: &str, present: bool) -> Vec<Self::Stmt> {
509        if present {
510            return Vec::new();
511        }
512        vec![Stat::Expr(Expr::Unary {
513            op: "delete ",
514            value: Box::new(Self::member(base, field)),
515        })]
516    }
517
518    /// `delete v.field` for every case: `adapter/node.rs` puts a union's
519    /// branches beside `_d` on one plain object, and a branch the
520    /// discriminator does not select is a property that is not there. The same statement as an absent
521    /// `@optional` member, [`Self::set_present`] with `false`.
522    fn clear_union_cases(base: &str, fields: &[String]) -> Vec<Self::Stmt> {
523        fields
524            .iter()
525            .flat_map(|field| Self::set_present(base, field, false))
526            .collect()
527    }
528
529    fn optional_destination(base: &str, field: &str) -> (Vec<Self::Stmt>, Self::Expr) {
530        (Vec::new(), Self::member(base, field))
531    }
532
533    /// `==` becomes `===` and `!=` becomes `!==`.
534    ///
535    /// Not house style: JavaScript's loose equality coerces across
536    /// `number` and `bigint` inconsistently — `1 == 1n` is `true` and
537    /// `1 === 1n` is `false` — and a discriminator compared with the
538    /// loose form could match a label of the wrong holder. `&&` and
539    /// `||` are already this language's own spelling and pass through
540    /// unchanged, unlike [`crate::emit::lua::Lua::binary`]'s `and`/`or`.
541    fn binary(op: &'static str, lhs: Self::Expr, rhs: Self::Expr) -> Self::Expr {
542        Expr::Binary {
543            op: match op {
544                "==" => "===",
545                "!=" => "!==",
546                other => other,
547            },
548            lhs: Box::new(lhs),
549            rhs: Box::new(rhs),
550        }
551    }
552
553    /// Every operator as given: JavaScript has a real unary form for
554    /// both `!` and `~`, so neither needs a stand-in.
555    fn unary(op: &'static str, value: Self::Expr) -> Self::Expr {
556        Expr::Unary {
557            op,
558            value: Box::new(value),
559        }
560    }
561
562    fn paren(value: Self::Expr) -> Self::Expr {
563        Expr::Paren(Box::new(value))
564    }
565
566    /// A conversion where the target is one of the three scalar holders
567    /// or the signed-`char` marker, a rewritten literal where the value
568    /// is a boolean discriminator's, and the value unchanged everywhere
569    /// else — there is no `as` to fall back on and none is needed: a
570    /// value this seam does not name a coercion for already carries the
571    /// representation its destination wants.
572    ///
573    /// `"signed char"` is the walk's own reading of a `map`'s `char` key
574    /// order: the member holds the code point as a `string`, so
575    /// `((k.charCodeAt(0) + 128) % 256) - 128` is the two's-complement
576    /// wrap over its one octet — the same arithmetic
577    /// `crates/lang-rules/src/emit/python.rs`'s `signed_char` writes and
578    /// for the same reason.
579    fn cast(type_: Self::Type, value: Self::Expr) -> Self::Expr {
580        if let Expr::Literal(Literal::Int(text)) = &value {
581            if type_.0 == "boolean" {
582                let digits = text.strip_suffix('n').unwrap_or(text);
583                if !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit() || b == b'-') {
584                    return Expr::bool(digits != "0");
585                }
586            }
587        }
588        if type_.0 == SIGNED_CHAR {
589            let point = Expr::Call {
590                callee: Box::new(Expr::member(Expr::Paren(Box::new(value)), "charCodeAt")),
591                args: vec![Self::int(0)],
592            };
593            return Self::binary(
594                "-",
595                Expr::Paren(Box::new(Self::binary(
596                    "%",
597                    Expr::Paren(Box::new(Self::binary("+", point, Self::int(128)))),
598                    Self::int(256),
599                ))),
600                Self::int(128),
601            );
602        }
603        let coercion = match type_.0.as_str() {
604            "number" => Some("zdNumber"),
605            "bigint" => Some("zdBigInt"),
606            "string" => Some("zdChar"),
607            _ => None,
608        };
609        match coercion {
610            Some(callee) if is_call_to(&value, callee) => value,
611            Some(callee) => Self::runtime_static(callee, vec![value]),
612            None => value,
613        }
614    }
615
616    /// The same conversion, at the point a value reaches its member: the
617    /// module documentation says why a `wchar` needs it here as well as
618    /// at [`Node::cast`]'s own call sites.
619    fn store(place: Self::Expr, type_: Self::Type, value: Self::Expr) -> Self::Stmt {
620        Self::perform(Self::binary("=", place, Self::cast(type_, value)))
621    }
622
623    fn handoff(value: Self::Expr, _form: Handoff) -> Self::Expr {
624        value
625    }
626
627    /// `place = zd_dec_T(r, zd_tc);` — this column's decoders answer
628    /// with the value, so the destination is the assignment's left-hand
629    /// side and never an out-parameter.
630    fn read_call(
631        callee: &str,
632        place: Self::Expr,
633        _form: Handoff,
634        behaviour: Self::Expr,
635    ) -> Self::Stmt {
636        Self::perform(Self::binary(
637            "=",
638            place,
639            Self::call(callee, vec![Self::name("r"), behaviour]),
640        ))
641    }
642
643    /// `v = zd_dec_T(r, zd_tc);` — a `typedef`, handed straight on.
644    fn read_forward(callee: &str) -> Self::Stmt {
645        Self::read_call(
646            callee,
647            Self::whole("v"),
648            Handoff::Value,
649            Self::name("zd_tc"),
650        )
651    }
652
653    // -- statements ----------------------------------------------------
654
655    /// `let name = value;`, with an integer literal spelled the way the
656    /// declared holder reads it — `let bits = 0n;` for a `bigint`
657    /// accumulator, exactly as the walk's own
658    /// `T::declare(T::scalar(Scalar::UInt64), "bits", int(0))` for a
659    /// bitset needs.
660    fn declare(type_: Self::Type, name: &str, value: Option<Self::Expr>) -> Self::Stmt {
661        let value = value.map(|value| match &value {
662            Expr::Literal(Literal::Int(text)) if type_.0 == "bigint" && !text.ends_with('n') => {
663                Expr::Literal(Literal::Int(format!("{text}n")))
664            }
665            _ => value,
666        });
667        Stat::Declare {
668            doc: Doc::none(),
669            kind: node::VarKind::Let,
670            name: Ident::new(name),
671            value,
672        }
673    }
674
675    fn perform(value: Self::Expr) -> Self::Stmt {
676        Stat::Expr(value)
677    }
678
679    fn give_back(value: Option<Self::Expr>) -> Self::Stmt {
680        Stat::Return(Some(value.unwrap_or_else(|| Self::whole("v"))))
681    }
682
683    /// Nothing: plain JavaScript has no unread-parameter diagnostic, and
684    /// `zd_tc;` is not worth writing on its own.
685    fn mark_used(_name: &str) -> Vec<Self::Stmt> {
686        Vec::new()
687    }
688
689    /// `void v;` — the one form JavaScript has for "read and discard",
690    /// and the only place the walk needs it.
691    fn drop_value(value: Self::Expr) -> Self::Stmt {
692        Stat::Expr(Expr::Unary {
693            op: "void ",
694            value: Box::new(value),
695        })
696    }
697
698    fn when(
699        condition: Self::Expr,
700        then: Self::Block,
701        otherwise: Option<Self::Block>,
702    ) -> Self::Stmt {
703        Stat::If {
704            condition,
705            then,
706            otherwise,
707        }
708    }
709
710    fn count_up(counter: &str, bound: Self::Expr, body: Self::Block) -> Self::Stmt {
711        Stat::For {
712            counter: Ident::new(counter),
713            bound,
714            body,
715        }
716    }
717
718    fn until(condition: Self::Expr, body: Self::Block) -> Self::Stmt {
719        Stat::While { condition, body }
720    }
721
722    fn leave() -> Self::Stmt {
723        Stat::Break
724    }
725
726    fn note(doc: Doc) -> Self::Stmt {
727        Stat::Comment(doc)
728    }
729
730    fn block(statements: Vec<Self::Stmt>) -> Self::Block {
731        statements
732    }
733
734    fn into_statements(block: Self::Block) -> Vec<Self::Stmt> {
735        block
736    }
737
738    /// `v = new Array(count);`, and — for a `map`, whose entries are
739    /// written half at a time — one `{}` per slot ahead of the fill,
740    /// because `v[i].key = …` needs the slot object to exist first. A
741    /// sequence's element decoder assigns the slot outright and needs
742    /// only the outer array.
743    fn make_room(
744        base: &str,
745        _element: Self::Type,
746        count: Self::Expr,
747        assigned_whole: bool,
748        fill: Vec<Self::Stmt>,
749    ) -> Vec<Self::Stmt> {
750        let mut out = vec![Stat::Assign {
751            doc: Doc::none(),
752            place: Self::whole(base),
753            value: Expr::New {
754                callee: Ident::new("Array"),
755                args: vec![count],
756            },
757        }];
758        if !assigned_whole {
759            out.push(Self::count_up(
760                "zd_e",
761                Self::count_of(base, None),
762                vec![Stat::Assign {
763                    doc: Doc::none(),
764                    place: Expr::Index {
765                        object: Box::new(Self::whole(base)),
766                        index: Box::new(Self::whole("zd_e")),
767                    },
768                    value: Expr::Object(Vec::new()),
769                }],
770            ));
771        }
772        out.extend(fill);
773        out
774    }
775
776    /// `let name = {};` — somewhere to put a value before its halves
777    /// arrive, so `zd_scratch.key = …` does not assign onto `undefined`.
778    fn storage(_type_: Self::Type, name: &str) -> Self::Stmt {
779        Stat::Declare {
780            doc: Doc::none(),
781            kind: node::VarKind::Let,
782            name: Ident::new(name),
783            value: Some(Expr::Object(Vec::new())),
784        }
785    }
786
787    /// `v.length = n;`, which shortens an array in place. §7.3.1.2.1.12.3's
788    /// TRIM.
789    fn keep_first(base: &str, count: Self::Expr) -> Vec<Self::Stmt> {
790        vec![Stat::Assign {
791            doc: Doc::none(),
792            place: Expr::member(Self::whole(base), "length"),
793            value: count,
794        }]
795    }
796
797    /// The subscript, with nothing around it: a JavaScript array element
798    /// is a real place at every element type.
799    fn sequence_element(
800        base: &str,
801        index: Self::Expr,
802        _element: Self::Type,
803    ) -> (Vec<Self::Stmt>, Self::Expr, Vec<Self::Stmt>) {
804        (
805            Vec::new(),
806            Self::index(Self::elements(base), index),
807            Vec::new(),
808        )
809    }
810
811    // -- assembly ------------------------------------------------------
812
813    /// `function zd_enc_T(w, v) { … }`, or the reading form that opens
814    /// from Table 9 and returns the sample.
815    ///
816    /// A top-level declaration, in module scope: nothing this column
817    /// generates is `export`ed, because the probe that runs it is a
818    /// script and not a package.
819    fn routine(spec: Routine<Self>) -> Option<Self::Decl> {
820        let body = spec.body?;
821        let mut params = vec![Ident::new(if spec.writing { "w" } else { "r" })];
822        if spec.writing {
823            params.push(Ident::new("v"));
824        }
825        for (_, name) in &spec.extra {
826            params.push(Ident::new(name));
827        }
828        let body = if spec.writing {
829            body
830        } else {
831            Self::reading_body(&spec.name, body)
832        };
833        Some(Stat::Function(Function {
834            doc: spec.doc,
835            name: Ident::new(&spec.name),
836            params,
837            body,
838        }))
839    }
840
841    /// # Errors
842    /// Never; the signature matches the emitters around it.
843    fn default_into(
844        ast: &Ast,
845        id: Id<TypeNode>,
846        place: Self::Expr,
847    ) -> Result<Self::Stmt, EncodeError> {
848        Ok(crate::default_node::default_into(ast, id, place))
849    }
850
851    /// # Errors
852    /// As [`crate::default_node::member_default`]: a `@default` this
853    /// column cannot spell as a JavaScript expression is refused rather
854    /// than silently replaced by Table 9's zero.
855    fn member_default(
856        ast: &Ast,
857        node: &MemberNode,
858        type_: Id<TypeNode>,
859        place: Self::Expr,
860    ) -> Result<Vec<Self::Stmt>, EncodeError> {
861        crate::default_node::member_default(ast, node, type_, place)
862    }
863
864    /// `a_b_Colour.RED` — the flattened frozen object
865    /// [`crate::adapter::node`] declared, read from the file's own
866    /// top-level scope.
867    ///
868    /// The trait's default answers the bare enumerator name prefixed by
869    /// [`Node::module_prefix`], which is the empty string here — this
870    /// column flattens a declared type's whole scoped name into its one
871    /// binding instead, and the enumerator lives inside that binding's
872    /// own object.
873    fn enumerator_constant(ast: &Ast, id: Id<TypeNode>, enumerator: &str) -> Self::Expr {
874        Expr::member(
875            Expr::name(&crate::adapter::node::node_type_name(ast, id)),
876            &field_name(enumerator),
877        )
878    }
879}
880
881/// Whether `value` is already a call to the unqualified runtime function
882/// `name`.
883fn is_call_to(value: &Expr, name: &str) -> bool {
884    let Expr::Call { callee, .. } = value else {
885        return false;
886    };
887    let Expr::Member {
888        object,
889        name: field,
890    } = callee.as_ref()
891    else {
892        return false;
893    };
894    if field.as_str() != name {
895        return false;
896    }
897    matches!(object.as_ref(), Expr::Name(root) if root.as_str() == RUNTIME)
898}
899
900/// The Node type of a value, structurally.
901///
902/// `number` and `bigint` by the boundary [`Node::scalar`] draws, an
903/// array for either collection, the flattened declared name for an
904/// aggregate that has one, and the aliased type's answer for a typedef —
905/// a `typedef` has no JavaScript form of its own and every position that
906/// reaches one already reads through it.
907///
908/// zerodds-lint: recursion-depth 64 (collection nesting; bounded by `MAX_TYPE_DEPTH`)
909fn value_type_of(ast: &Ast, id: Id<TypeNode>, depth: usize) -> NodeType {
910    use zerodds_ast::PrimitiveKind;
911    if depth > MAX_TYPE_DEPTH {
912        return NodeType::tag("object");
913    }
914    let Ok(node) = ast.type_(id) else {
915        return NodeType::tag("object");
916    };
917    match &node.kind {
918        TypeKind::Primitive(kind) => match kind {
919            PrimitiveKind::Boolean => NodeType::tag("boolean"),
920            PrimitiveKind::Char | PrimitiveKind::WChar => NodeType::tag("string"),
921            PrimitiveKind::Int64 | PrimitiveKind::UInt64 => NodeType::tag("bigint"),
922            PrimitiveKind::Float128 => NodeType::tag("float128"),
923            _ => NodeType::tag("number"),
924        },
925        TypeKind::String { .. } | TypeKind::Fixed { .. } => NodeType::tag("string"),
926        // §7.4.3.5.3 rule (5) caps an enumerator's holder at 32 bits, so
927        // the constant a declaration reads back is always a `number`.
928        TypeKind::Enumeration { .. } => NodeType::tag("number"),
929        TypeKind::Bitmask { .. } => NodeType::tag(if ast.effective_bit_bound(id) > 32 {
930            "bigint"
931        } else {
932            "number"
933        }),
934        TypeKind::Alias { aliased } => aliased.resolved().map_or_else(
935            || NodeType::tag("object"),
936            |target| value_type_of(ast, target, depth + 1),
937        ),
938        _ => NodeType::tag("object"),
939    }
940}
941
942/// The bound a `string`, a `sequence` or a `map` carries, as the suffix
943/// that keeps two of them apart.
944///
945/// The tag names the routine a hoisted collection's codec is emitted
946/// under, and one body is emitted per name. With the bound left out,
947/// `sequence<long>` and `sequence<long, 4>` are one name and whichever
948/// was declared first decides: measured on
949/// `@final struct S { sequence<long> b; sequence<long, 4> a; };`, the
950/// generated Node held no `zd_over` check at all, so `a` accepted a
951/// stream of any length; with the members the other way round both got
952/// the bounded body, so `b` refused — and truncated — a length the IDL
953/// puts no limit on. `string<4>` beside `string` and `wstring<4>`
954/// beside `wstring` collapse the same way, onto the trimming call.
955/// Either way the reader answered with a value the IDL does not
956/// describe and said nothing, which is why the bound is part of the
957/// name. The same suffix the Zig column carries.
958///
959/// An `array` needs nothing here: its `dims` are already in the tag,
960/// under the `Array` arm below.
961fn bound_tag(ast: &Ast, bound: &zerodds_ast::Bound) -> String {
962    match bound.expr() {
963        None => String::new(),
964        // `_b` and not a bare `_n`: `seq_long_4` is what the `Array`
965        // arm answers for an array of four `long`, and the two are
966        // different wire forms.
967        Some(expr) => match ast.eval_integral(expr) {
968            Ok(limit) => format!("_b{limit}"),
969            // As the array's `_x`: a bound that does not evaluate is
970            // still not the unbounded form, and the materialiser
971            // reports the expression that would not evaluate.
972            Err(_) => "_bx".to_string(),
973        },
974    }
975}
976
977/// The tag a codec function is named after, from the IDL kind and never
978/// from the JavaScript spelling — ten of the fifteen primitives are
979/// `number` here, and a tag built from that would give them one codec.
980/// The same scheme [`crate::adapter::lua::type_tag`] uses.
981///
982/// zerodds-lint: recursion-depth 64 (collection nesting; bounded by `MAX_TYPE_DEPTH`)
983fn type_tag(ast: &Ast, id: Id<TypeNode>, depth: usize) -> String {
984    use zerodds_ast::CharWidth;
985    if depth > MAX_TYPE_DEPTH {
986        return "unresolved".to_string();
987    }
988    let Ok(node) = ast.type_(id) else {
989        return "unresolved".to_string();
990    };
991    match &node.kind {
992        TypeKind::Primitive(kind) => kind.as_str().replace(' ', "_"),
993        TypeKind::String {
994            width: CharWidth::Char,
995            bound,
996        } => format!("string{}", bound_tag(ast, bound)),
997        TypeKind::String {
998            width: CharWidth::WChar,
999            bound,
1000        } => format!("wstring{}", bound_tag(ast, bound)),
1001        TypeKind::Fixed { digits, scale } => format!("fixed_{digits}_{scale}"),
1002        TypeKind::Sequence { element, bound } => match element.resolved() {
1003            Some(element) => format!(
1004                "seq{}_{}",
1005                bound_tag(ast, bound),
1006                type_tag(ast, element, depth + 1)
1007            ),
1008            None => format!("seq_unresolved{}", bound_tag(ast, bound)),
1009        },
1010        TypeKind::Array { element, dims } => {
1011            let mut name = match element.resolved() {
1012                Some(element) => format!("arr_{}", type_tag(ast, element, depth + 1)),
1013                None => "arr_unresolved".to_string(),
1014            };
1015            for dim in dims {
1016                match ast.eval_integral(*dim) {
1017                    Ok(length) => name.push_str(&format!("_{length}")),
1018                    Err(_) => name.push_str("_x"),
1019                }
1020            }
1021            name
1022        }
1023        TypeKind::Map { key, value, bound } => {
1024            let half = |half: Option<Id<TypeNode>>| match half {
1025                Some(target) => type_tag(ast, target, depth + 1),
1026                None => "unresolved".to_string(),
1027            };
1028            format!(
1029                "map{}_{}_{}",
1030                bound_tag(ast, bound),
1031                half(key.resolved()),
1032                half(value.resolved())
1033            )
1034        }
1035        TypeKind::Alias { aliased } => match aliased.resolved() {
1036            Some(target) => type_tag(ast, target, depth + 1),
1037            None => "unresolved".to_string(),
1038        },
1039        TypeKind::ValueBox { boxed } => match boxed.resolved() {
1040            Some(target) => format!("box_{}", type_tag(ast, target, depth + 1)),
1041            None => "box_unresolved".to_string(),
1042        },
1043        TypeKind::Structure { .. }
1044        | TypeKind::Union { .. }
1045        | TypeKind::Enumeration { .. }
1046        | TypeKind::Bitmask { .. }
1047        | TypeKind::Bitset { .. }
1048        | TypeKind::Valuetype { .. }
1049        | TypeKind::Native => crate::adapter::node::node_type_name(ast, id),
1050        TypeKind::Any => "any".to_string(),
1051        TypeKind::ValueBase => "valuebase".to_string(),
1052        TypeKind::ObjectRef { .. } => "objectref".to_string(),
1053        TypeKind::AnnotationType { .. } => "annotation".to_string(),
1054    }
1055}