Skip to main content

zerodds_lang_rules/emit/
typescript.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The TypeScript backend of the shared codec walk.
4//!
5//! DDS-XTypes 1.3 §7.4 from the clause, the same as `runtime/c`,
6//! `runtime/java` 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 three decisions this column turns on
11//!
12//! **A `number` is a binary64.** So every integer up to `i32` is one
13//! and everything wider is a `bigint`. The boundary is the type's and
14//! not the wire's: a binary64 holds every `i32` exactly and not every
15//! `i64`, and a `long long` that arrived as a `number` would already
16//! have lost its low bits before any octet was written.
17//!
18//! **A member is a property.** `v.field`, not `v->field` and not a
19//! getter, which is what `adapter/typescript.rs` declares; the two have
20//! to agree and a third reading is the one that would quietly diverge.
21//!
22//! **A `long double` never becomes a `number`.** `ZdFloat128` carries
23//! the sixteen octets, because a binary64 would drop sixty bits of the
24//! significand at the assignment.
25
26use super::{CodecTarget, Constant, Handle, Handoff, Routine, Scalar};
27use crate::encode::EncodeError;
28use zerodds_ast::{Ast, MemberNode, TypeNode};
29use zerodds_ast_core::Id;
30use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
31use zerodds_lang_cst::typescript as ts;
32
33/// The TypeScript backend.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct TypeScript;
36
37impl TypeScript {
38    /// `write_u32` as `writeU32`.
39    ///
40    /// The runtime's methods are named by TypeScript's convention and
41    /// the walk asks in the wire's, so one of the two has to convert.
42    /// Doing it here means a new operation is added to the walk under
43    /// one name.
44    fn camel(method: &str) -> String {
45        let mut out = String::new();
46        let mut rising = false;
47        for character in method.chars() {
48            if character == '_' {
49                rising = true;
50                continue;
51            }
52            if rising {
53                out.extend(character.to_uppercase());
54                rising = false;
55            } else {
56                out.push(character);
57            }
58        }
59        out
60    }
61
62    fn path(name: &str) -> Path {
63        Path::new(&name.split('.').collect::<Vec<_>>())
64    }
65
66    /// A reader's body, with the sample it answers with around it.
67    ///
68    /// `let v: T = zd_def_T(); … return v;`. Two things are settled here
69    /// and neither is a spelling:
70    ///
71    /// - **Where the sample comes from.** Table 9's value, through the
72    ///   generated defaulter, and not `new TImpl()` and not `undefined`.
73    ///   Every position the stream does not carry is then already what
74    ///   DDS-XTypes 1.3 Table 9 says it is, without the walk having to
75    ///   reach each one; a `new TImpl()` would be a second answer to
76    ///   Table 9 beside [`crate::default_typescript`], and the two can
77    ///   part.
78    /// - **What the name is.** The defaulter and the decoder are keyed
79    ///   the same way — [`crate::emit::decode::decoder_name`] and
80    ///   [`crate::default_typescript::defaulter_name`] differ only in
81    ///   the three letters after `zd_` — so the one is the other with
82    ///   the verb changed. A name that does not resolve is a `tsc` error
83    ///   naming the function.
84    ///
85    /// Without this the walk's own `return v` referred to a binding
86    /// nothing declared: the generated decoder named `v` in every
87    /// statement and never introduced it.
88    fn reading_body(name: &str, sample: ts::Type, body: ts::Block) -> ts::Block {
89        if let Some((key, value)) = readonly_map_halves(&sample) {
90            return Self::reading_map_body(key, value, body);
91        }
92        let defaulter = crate::default_typescript::defaulter_from_decoder(name);
93        let mut statements = vec![ts::Statement::Declare {
94            // `let` and not `const`: the walk assigns `v` whole for a
95            // primitive and for a collection it re-seats.
96            kind: ts::VarKind::Let,
97            name: Ident::new("v"),
98            type_: Some(sample),
99            value: Some(Self::call(&defaulter, Vec::new())),
100        }];
101        statements.extend(body.statements);
102        statements.push(ts::Statement::Return(Some(Self::whole("v"))));
103        ts::Block { statements }
104    }
105
106    /// A map reader's body: the entry array the walk fills, turned into
107    /// the `ReadonlyMap` §7.9.1 says the surface is.
108    ///
109    /// XTypes 1.3 §7.4.1.1.4 puts a map on the wire as a *sequence of
110    /// entries*, and the shared walk reads one that way — `v[i].key`,
111    /// `v[i].value`, `v.length`. A `Map` has none of those, and the read
112    /// direction could not be written against one at all: an entry is
113    /// filled in two steps, and there is no half-built entry in a `Map`
114    /// to fill. So the walk keeps its array and the conversion happens
115    /// once, here, at the boundary where the value leaves the codec.
116    ///
117    /// Every `return v` the walk emitted — the refusals of
118    /// `CodecTarget::give_back` as well as the one at the end — is
119    /// rewritten, because each of them is answering with the map.
120    fn reading_map_body(key: ts::Type, value: ts::Type, body: ts::Block) -> ts::Block {
121        let mut statements = vec![ts::Statement::Declare {
122            kind: ts::VarKind::Let,
123            name: Ident::new("v"),
124            type_: Some(ts::Type::Array(Box::new(entry_object(key, value)))),
125            value: Some(ts::Expr::Array(Vec::new())),
126        }];
127        statements.extend(body.statements.into_iter().map(answer_with_map));
128        statements.push(ts::Statement::Return(Some(map_of_entries())));
129        ts::Block { statements }
130    }
131}
132
133/// `{ key: K; value: V }` — the entry the codec stages a map through.
134///
135/// Structural and not the generated `ZdEntry_k_v` interface by name: the
136/// two are the same type to `tsc`, and this one can be built from the
137/// `ReadonlyMap`'s own arguments without asking the materialiser for a
138/// name a second time.
139fn entry_object(key: ts::Type, value: ts::Type) -> ts::Type {
140    let property = |name: &str, type_: ts::Type| {
141        ts::Signature::Property(ts::Property {
142            doc: Doc::none(),
143            readonly: false,
144            name: ts::PropertyName::ident(name),
145            optional: false,
146            type_,
147        })
148    };
149    ts::Type::Object(vec![property("key", key), property("value", value)])
150}
151
152/// `new Map(v.map((zd_p) => [zd_p.key, zd_p.value] as const))`
153///
154/// `as const` on the pair and not a bare array literal: the `Map`
155/// constructor takes `readonly (readonly [K, V])[]`, and `[a, b]` infers
156/// as `(K | V)[]` — which is not a pair and constructs nothing.
157fn map_of_entries() -> ts::Expr {
158    let half = |name: &str| ts::Expr::Member {
159        object: Box::new(ts::Expr::Path(Path::new(&["zd_p"]))),
160        name: Ident::new(name),
161    };
162    ts::Expr::New {
163        callee: Path::new(&["Map"]),
164        args: vec![ts::Expr::Call {
165            callee: Box::new(ts::Expr::Member {
166                object: Box::new(ts::Expr::Path(Path::new(&["v"]))),
167                name: Ident::new("map"),
168            }),
169            args: vec![ts::Expr::Arrow {
170                params: vec![Ident::new("zd_p")],
171                body: Box::new(ts::Expr::AsConst(Box::new(ts::Expr::Array(vec![
172                    half("key"),
173                    half("value"),
174                ])))),
175            }],
176        }],
177    }
178}
179
180/// `Array.from(zd_map).map((zd_p) => ({ key: zd_p[0], value: zd_p[1] }))`
181///
182/// The write direction's conversion: the walk indexes an entry array and
183/// the parameter is the `ReadonlyMap` a caller holds, so the pairs are
184/// staged once at the top of the function.
185fn entries_of_map() -> ts::Expr {
186    entries_of(ts::Expr::Path(Path::new(&["zd_map"])))
187}
188
189/// `Array.from(source).map((zd_p) => ({ key: zd_p[0], value: zd_p[1] }))`
190///
191/// The same conversion for any source expression, not just the writer's
192/// `zd_map` parameter. A map reader's `UseDefault` branch needs it: the
193/// defaulter answers the public `ReadonlyMap`, while `v` holds the pair
194/// array the walk fills, and assigning one to the other does not
195/// type-check.
196fn entries_of(source: ts::Expr) -> ts::Expr {
197    let half = |index: i128| ts::Expr::Index {
198        object: Box::new(ts::Expr::Path(Path::new(&["zd_p"]))),
199        index: Box::new(ts::Expr::Literal(Literal::int(index))),
200    };
201    ts::Expr::Call {
202        callee: Box::new(ts::Expr::Member {
203            object: Box::new(ts::Expr::Call {
204                callee: Box::new(ts::Expr::Member {
205                    object: Box::new(ts::Expr::Path(Path::new(&["Array"]))),
206                    name: Ident::new("from"),
207                }),
208                args: vec![source],
209            }),
210            name: Ident::new("map"),
211        }),
212        args: vec![ts::Expr::Arrow {
213            params: vec![Ident::new("zd_p")],
214            // A concise arrow body starting with `{` is a *block*, so
215            // the object literal is parenthesised.
216            body: Box::new(ts::Expr::Parenthesised(Box::new(ts::Expr::Object(vec![
217                (ts::PropertyName::ident("key"), half(0)),
218                (ts::PropertyName::ident("value"), half(1)),
219            ])))),
220        }],
221    }
222}
223
224/// The two arguments of a `ReadonlyMap<K, V>`, or `None` for any other
225/// type.
226fn readonly_map_halves(type_: &ts::Type) -> Option<(ts::Type, ts::Type)> {
227    let ts::Type::Named { path, args } = type_ else {
228        return None;
229    };
230    let [key, value] = args.as_slice() else {
231        return None;
232    };
233    let [head] = path.segments() else {
234        return None;
235    };
236    if head.as_str() != "ReadonlyMap" {
237        return None;
238    }
239    Some((key.clone(), value.clone()))
240}
241
242/// Whether an expression assigned to a map reader's `v` answers the
243/// public `ReadonlyMap` rather than the pair array.
244///
245/// Only the Table 9 defaulter does. The other assignment in the same
246/// body is `v = new Array(zd_n)`, which reserves the pair array's
247/// capacity and is already the right shape — converting that one would
248/// build `zd_n` objects with undefined halves for the loop below it to
249/// overwrite.
250fn answers_a_map(value: &ts::Expr) -> bool {
251    let ts::Expr::Call { callee, .. } = value else {
252        return false;
253    };
254    let ts::Expr::Path(path) = callee.as_ref() else {
255        return false;
256    };
257    path.segments()
258        .last()
259        .is_some_and(|segment| segment.as_str().starts_with("zd_def_"))
260}
261
262/// `return v` as `return new Map(…)`, wherever it appears in a map
263/// reader's body.
264///
265/// zerodds-lint: recursion-depth 64 (statement nesting; bounded by the walk)
266fn answer_with_map(statement: ts::Statement) -> ts::Statement {
267    let rewrite = |block: ts::Block| ts::Block {
268        statements: block.statements.into_iter().map(answer_with_map).collect(),
269    };
270    match statement {
271        ts::Statement::Return(Some(ts::Expr::Path(ref path)))
272            if path.segments().len() == 1
273                && path
274                    .segments()
275                    .first()
276                    .is_some_and(|segment| segment.as_str() == "v") =>
277        {
278            ts::Statement::Return(Some(map_of_entries()))
279        }
280        // `v = zd_def_X()`: the defaulter answers the public
281        // `ReadonlyMap`, `v` is the pair array. Without the conversion the
282        // branch does not type-check — TS2740, "Type 'ReadonlyMap' is
283        // missing the following properties from type '{ key; value; }[]'".
284        // Dropping the statement instead would return whatever was read
285        // before the overflow rather than the default.
286        ts::Statement::Expr(ts::Expr::Binary { op: "=", lhs, rhs })
287            if matches!(
288                lhs.as_ref(),
289                ts::Expr::Path(path)
290                    if path.segments().len() == 1
291                        && path
292                            .segments()
293                            .first()
294                            .is_some_and(|segment| segment.as_str() == "v")
295            ) && answers_a_map(rhs.as_ref()) =>
296        {
297            ts::Statement::Expr(ts::Expr::Binary {
298                op: "=",
299                lhs,
300                rhs: Box::new(entries_of(*rhs)),
301            })
302        }
303        ts::Statement::If {
304            condition,
305            then,
306            otherwise,
307        } => ts::Statement::If {
308            condition,
309            then: rewrite(then),
310            otherwise: otherwise.map(rewrite),
311        },
312        ts::Statement::For {
313            counter,
314            bound,
315            body,
316        } => ts::Statement::For {
317            counter,
318            bound,
319            body: rewrite(body),
320        },
321        ts::Statement::While { condition, body } => ts::Statement::While {
322            condition,
323            body: rewrite(body),
324        },
325        ts::Statement::ForOf {
326            kind,
327            binding,
328            range,
329            body,
330        } => ts::Statement::ForOf {
331            kind,
332            binding,
333            range,
334            body: rewrite(body),
335        },
336        ts::Statement::Switch { value, cases } => ts::Statement::Switch {
337            value,
338            cases: cases
339                .into_iter()
340                .map(|case| ts::SwitchCase {
341                    labels: case.labels,
342                    body: rewrite(case.body),
343                    breaks: case.breaks,
344                })
345                .collect(),
346        },
347        other => other,
348    }
349}
350
351impl CodecTarget for TypeScript {
352    type Expr = ts::Expr;
353    type Stmt = ts::Statement;
354    type Block = ts::Block;
355    type Type = ts::Type;
356    type Decl = ts::Decl;
357
358    // -- types --------------------------------------------------------
359
360    /// `number` to `i32`, `bigint` beyond it.
361    ///
362    /// Not a style: `Number.MAX_SAFE_INTEGER` is 2^53 − 1, so a
363    /// `number` holds every `i32` and every `u32` exactly and neither
364    /// `i64` nor `u64`. An index and a size are `number` because a
365    /// collection's length is one.
366    ///
367    /// The same boundary `adapter/typescript.rs` draws for a member's
368    /// declared type and `runtime/typescript` for its parameters.
369    /// Three places have to agree.
370    ///
371    /// [`ts::Type::Keyword`] and not a named path spelled `"number"`.
372    /// The two render the same text and are not the same tree, and the
373    /// materialiser builds the keyword: a backend that built the path
374    /// answered `false` to every `type == ts::Type::number()` a caller
375    /// asked, which is how a holder decision gets made twice and
376    /// differently.
377    fn scalar(kind: Scalar) -> Self::Type {
378        match kind {
379            Scalar::Bool => ts::Type::Keyword(ts::Keyword::Boolean),
380            Scalar::Char => ts::Type::string(),
381            Scalar::Int8
382            | Scalar::UInt8
383            | Scalar::Int16
384            | Scalar::UInt16
385            | Scalar::Int32
386            | Scalar::UInt32
387            | Scalar::Index
388            | Scalar::Size
389            | Scalar::Float
390            | Scalar::Double => ts::Type::number(),
391            Scalar::Int64 | Scalar::UInt64 => ts::Type::bigint(),
392            Scalar::LongDouble => ts::Type::named(Path::new(&["ZdFloat128"])),
393        }
394    }
395
396    fn named(name: &str) -> Self::Type {
397        ts::Type::named(Self::path(name))
398    }
399
400    fn nothing() -> Self::Type {
401        ts::Type::Keyword(ts::Keyword::Void)
402    }
403
404    /// The runtime's handles, by their exported names.
405    ///
406    /// A generated file imports them, so the simple name is what it
407    /// writes; `runtime/typescript` exports each as an interface rather
408    /// than nesting it in the writer, because TypeScript has no nested
409    /// type in a class the way Java does.
410    fn handle(kind: Handle) -> Self::Type {
411        ts::Type::named(Path::new(&[match kind {
412            Handle::DHeader => "DHeader",
413            Handle::EmHeader => "EmHeader",
414            Handle::Pid => "Pid",
415            Handle::TryConstruct => "TryConstruct",
416            Handle::ReaderMark => "ReaderMark",
417            Handle::PidRead => "PidRead",
418            Handle::EmHeaderRead => "EmHeaderRead",
419        }]))
420    }
421
422    /// A `string` of one character is what a `char` is here.
423    ///
424    /// `codePointAt(0)` is the runtime's, not the generated file's: the
425    /// narrowing a `char` needs belongs where the octet is written.
426    fn code_point(value: Self::Expr, _narrow: bool) -> Self::Expr {
427        value
428    }
429
430    /// The runtime's enumerations, in TypeScript's naming.
431    ///
432    /// One member per constant and no catch-all: a policy this column
433    /// has not implemented has to be a compile error here rather than a
434    /// value that looks plausible.
435    fn constant(which: Constant) -> Self::Expr {
436        ts::Expr::Path(Self::path(match which {
437            Constant::WcharWidth2 => "WcharWidth.Octets2",
438            Constant::WcharWidth4 => "WcharWidth.Octets4",
439            Constant::WcharFixed => "WcharFraming.Fixed",
440            Constant::WcharGiopLengthPrefixed => "WcharFraming.GiopLengthPrefixed",
441            Constant::WstringUnitsStream => "WstringUnitOrder.Stream",
442            Constant::WstringUnitsBigEndianAlways => "WstringUnitOrder.BigEndianAlways",
443            Constant::WstringUnitsByteOrderMark => "WstringUnitOrder.ByteOrderMark",
444            Constant::WstringUnitsHostUnswapped => "WstringUnitOrder.HostUnswapped",
445            Constant::WstringSpecification => "WstringLengthPolicy.Specification",
446            Constant::WstringCharacterCount => "WstringLengthPolicy.CharacterCount",
447            Constant::WstringCharacterCountTerminated => {
448                "WstringLengthPolicy.CharacterCountTerminated"
449            }
450            Constant::WstringSpecificationWideUnits => "WstringLengthPolicy.SpecificationWideUnits",
451            Constant::WstringRosTypeSupport => "WstringLengthPolicy.RosTypeSupport",
452            Constant::WstringOctetsTerminated2 => "WstringLengthPolicy.OctetsTerminated2",
453            Constant::WstringOctetsTerminated4 => "WstringLengthPolicy.OctetsTerminated4",
454            Constant::WstringCharactersTerminated2 => "WstringLengthPolicy.CharactersTerminated2",
455            Constant::FixedDeclared => "FixedDigitCount.Declared",
456            Constant::FixedValuePrecision => "FixedDigitCount.ValuePrecision",
457            Constant::FixedValueStringDigits => "FixedDigitCount.ValueStringDigits",
458            Constant::ParameterMember => "ParameterRole.Member",
459            Constant::ParameterOptional => "ParameterRole.Optional",
460            Constant::TryConstructDiscard => "TryConstruct.Discard",
461            Constant::TryConstructTrim => "TryConstruct.Trim",
462            Constant::TryConstructUseDefault => "TryConstruct.UseDefault",
463            Constant::StatusOk => "ZdReaderStatus.Ok",
464            Constant::StatusDiscarded => "ZdReaderStatus.Discarded",
465            Constant::StatusImpossibleLength => "ZdReaderStatus.ImpossibleLength",
466            Constant::StatusOutOfRange => "ZdReaderStatus.OutOfRange",
467            Constant::StatusTruncated => "ZdReaderStatus.Truncated",
468        }))
469    }
470
471    /// An unsigned holder is a `number` to 32 bits and a `bigint` at
472    /// 64, for the reason [`Self::scalar`] gives.
473    fn unsigned_holder(bits: u16) -> Self::Type {
474        crate::adapter::typescript::unsigned_holder(bits)
475    }
476
477    /// `zdNumber(zd_bits) as Flags` — the holder conversion first, the
478    /// alias after.
479    ///
480    /// The trait's one cast is `zd_bits as Flags`, and `Flags` here is
481    /// `type Flags = number` for a bound to 32 bits while the read
482    /// answers a `bigint`: `as` between the two is TS2352, because `as`
483    /// is erased and would have left the value a `bigint` besides. The
484    /// runtime conversion through [`Self::unsigned_holder`]'s keyword is
485    /// the same one every bitset field takes.
486    fn bitmask_value(ast: &Ast, id: Id<TypeNode>, value: Self::Expr) -> Self::Expr {
487        let bound = ast.effective_bit_bound(id);
488        Self::cast(
489            Self::value_type(ast, id),
490            Self::cast(Self::unsigned_holder(bound), value),
491        )
492    }
493
494    /// The TypeScript type of a value of `id`.
495    ///
496    /// Not [`Self::type_name`], which is the codec's tag —
497    /// `p_long`, `seq_long` — and names a function, not a type. A
498    /// parameter declared `v: long` is a reference to a type
499    /// TypeScript does not have, and `tsc` says so; it was what the
500    /// first generated codec of this column wrote.
501    ///
502    /// Not `adapter/typescript.rs`'s `ts_member_type` either: that one
503    /// needs the rule application to spell a *member's* declared type,
504    /// which a codec does not have and does not need. A structural
505    /// walk is enough here, and the two agree on every shape because
506    /// both read the same model.
507    fn value_type(ast: &Ast, id: Id<TypeNode>) -> Self::Type {
508        value_type_of(ast, id, 0)
509    }
510
511    fn type_name(ast: &Ast, id: Id<TypeNode>) -> String {
512        crate::adapter::typescript::type_tag(ast, id, 0)
513            .unwrap_or_else(|_| format!("unresolved_type_{}", id.slot()))
514    }
515
516    fn map_entry_name(ast: &Ast, id: Id<TypeNode>) -> String {
517        crate::adapter::typescript::map_entry_name(ast, id)
518            .unwrap_or_else(|_| format!("UnresolvedEntry{}", id.slot()))
519    }
520
521    fn module_prefix(_ast: &Ast, _id: Id<TypeNode>) -> String {
522        // TypeScript has namespaces and the materialiser nests in them,
523        // so a declaration's own name is enough inside its namespace.
524        String::new()
525    }
526
527    fn escape(name: &str) -> String {
528        ts::escape_member(name)
529    }
530
531    /// The base is the first member `parent` (XTypes 1.3 §7.5.1.4.1.3).
532    fn inherited_path(depth: usize) -> Option<String> {
533        Some("parent.".repeat(depth))
534    }
535
536    // -- expressions ---------------------------------------------------
537
538    fn name(text: &str) -> Self::Expr {
539        ts::Expr::Path(Self::path(text))
540    }
541
542    fn int(value: i128) -> Self::Expr {
543        ts::Expr::Literal(Literal::int(value))
544    }
545
546    /// A `bigint` literal, which is a number with an `n` after it.
547    ///
548    /// Every value the walk hands here is one a `number` cannot carry —
549    /// that is what makes it wide — so the suffix is unconditional.
550    fn wide_int(value: i128) -> Self::Expr {
551        ts::Expr::Literal(Literal::Int(format!("{value}n")))
552    }
553
554    fn literal(value: Literal) -> Self::Expr {
555        ts::Expr::Literal(value)
556    }
557
558    /// The number, with no suffix.
559    ///
560    /// The trait's default is C's `123u`. TypeScript has one numeric
561    /// type and no suffix for it, and `2u` is not an expression at all:
562    /// `tsc` calls it TS1351, "an identifier or keyword cannot
563    /// immediately follow a numeric literal". Every array bound and
564    /// every string bound the decode walk writes goes through here.
565    fn unsigned_int(value: i128) -> Self::Expr {
566        ts::Expr::Literal(Literal::int(value))
567    }
568
569    /// A field of a header the runtime handed back, in the runtime's
570    /// spelling.
571    ///
572    /// The walk names a header's parts in the wire's convention —
573    /// `member_id`, `must_understand` — and `runtime/typescript` spells
574    /// them as TypeScript properties. The names are listed rather than
575    /// run through `Self::camel` so that a part the runtime does not
576    /// publish is visible here: it becomes a `tsc` error naming the
577    /// property, which is where a header that grew a part shows up.
578    fn header_field(value: Self::Expr, field: &str) -> Self::Expr {
579        Self::field_of(
580            value,
581            match field {
582                "pid" => "pid",
583                "end" => "end",
584                "ignore" => "ignore",
585                "length" => "length",
586                "member_id" => "memberId",
587                "must_understand" => "mustUnderstand",
588                "extent" => "extent",
589                other => other,
590            },
591        )
592    }
593
594    /// `strcmp` is the walk's name for a three-way code-unit
595    /// comparison; TypeScript's `<`/`>` on strings compare UTF-16 code
596    /// units, so the spelling is a ternary — the Java column's caveat
597    /// about supplementary code points applies unchanged.
598    fn call(callee: &str, args: Vec<Self::Expr>) -> Self::Expr {
599        // A slice pattern rather than a length check and two `next()`
600        // calls: the pattern binds both arguments or neither, so no
601        // branch is left over to claim it cannot be reached.
602        if callee == "strcmp" {
603            if let [left, right] = args.as_slice() {
604                return ts::Expr::Paren(Box::new(ts::Expr::Ternary {
605                    condition: Box::new(ts::Expr::Binary {
606                        op: "<",
607                        lhs: Box::new(left.clone()),
608                        rhs: Box::new(right.clone()),
609                    }),
610                    then: Box::new(Self::int(-1)),
611                    otherwise: Box::new(ts::Expr::Ternary {
612                        condition: Box::new(ts::Expr::Binary {
613                            op: ">",
614                            lhs: Box::new(left.clone()),
615                            rhs: Box::new(right.clone()),
616                        }),
617                        then: Box::new(Self::int(1)),
618                        otherwise: Box::new(Self::int(0)),
619                    }),
620                }));
621            }
622        }
623        ts::Expr::Call {
624            callee: Box::new(ts::Expr::Path(Self::path(callee))),
625            args,
626        }
627    }
628
629    fn runtime(receiver: &str, method: &str, args: Vec<Self::Expr>) -> Self::Expr {
630        ts::Expr::Call {
631            callee: Box::new(ts::Expr::Member {
632                object: Box::new(ts::Expr::Path(Self::path(receiver))),
633                name: Ident::new(&Self::camel(method)),
634            }),
635            args,
636        }
637    }
638
639    /// `runtime/typescript` carries the three HDDS axes: `beginPackedElement`
640    /// and `finishPackedElement` on the writer and the reader, the DHEADER
641    /// pair for a sequence element's own length, and
642    /// `writeWstringUtf8Octets` / `readWstringUtf8Octets`.
643    fn carries_hdds_axes() -> bool {
644        true
645    }
646
647    /// A free function the generated file imports from the runtime.
648    ///
649    /// TypeScript has module-level functions, so unlike Java there is
650    /// no class to hang them on.
651    fn runtime_static(method: &str, args: Vec<Self::Expr>) -> Self::Expr {
652        Self::call(&Self::camel(method), args)
653    }
654
655    fn member(base: &str, field: &str) -> Self::Expr {
656        ts::Expr::Member {
657            object: Box::new(ts::Expr::Path(Self::path(base))),
658            name: Ident::new(field),
659        }
660    }
661
662    fn whole(base: &str) -> Self::Expr {
663        ts::Expr::Path(Self::path(base))
664    }
665
666    fn field_of(value: Self::Expr, field: &str) -> Self::Expr {
667        ts::Expr::Member {
668            object: Box::new(value),
669            name: Ident::new(field),
670        }
671    }
672
673    /// `v[i]!`, with the non-null assertion.
674    ///
675    /// `runtime/typescript/tsconfig.json` compiles under
676    /// `noUncheckedIndexedAccess`, where `v[i]` is `T | undefined`
677    /// however the loop is bounded — the checker does not relate `i` to
678    /// `v.length`. Every index the walk builds is inside a loop it
679    /// bounded itself with that very length, so the assertion states
680    /// something the generated code has already established.
681    ///
682    /// Written here rather than turning the setting off, because a
683    /// generated file that only compiles under looser settings is one a
684    /// consumer who has that setting on cannot use. It is the one place
685    /// this column writes a `!`.
686    ///
687    /// The read direction will need an lvalue form: `v[i]! = x` is not
688    /// an assignment target. `emit/typescript.rs` refuses to generate a
689    /// decoder at all today — `default_into` says so by name — and the
690    /// module that lifts that refusal has this to solve.
691    fn index(value: Self::Expr, at: Self::Expr) -> Self::Expr {
692        ts::Expr::NonNull(Box::new(ts::Expr::Index {
693            object: Box::new(value),
694            index: Box::new(at),
695        }))
696    }
697
698    /// `.length`, or the bound where the materialiser fixed one.
699    fn count_of(base: &str, fixed: Option<u64>) -> Self::Expr {
700        match fixed {
701            Some(bound) => ts::Expr::Literal(Literal::int(i128::from(bound))),
702            None => ts::Expr::Member {
703                object: Box::new(Self::whole(base)),
704                name: Ident::new("length"),
705            },
706        }
707    }
708
709    fn elements(base: &str) -> Self::Expr {
710        Self::whole(base)
711    }
712
713    /// `(v as Extract<typeof v, { one: unknown }>).one`
714    ///
715    /// Not `v.one`. A union is a *discriminated union* here and not a
716    /// struct with every branch's field in it, which is what the C and
717    /// Java columns have — so a case member lives on one arm only and
718    /// reaching it needs the value narrowed to that arm. **Both
719    /// directions need it, for different reasons**, and the two were
720    /// found independently by the read and the write halves of this
721    /// column before they were merged:
722    ///
723    /// - **Reading.** TypeScript refuses `v.one = …` on a union-typed
724    ///   `v`: `one` is in one arm and `_d` is in all of them, so the
725    ///   discriminator assigns and the selected member does not. TS2339
726    ///   on every union the walk reads.
727    /// - **Writing.** Narrowing on the discriminator is what the walk's
728    ///   `if` already writes, and it is not enough for either of the two
729    ///   shapes the corpus has: a `default:` arm carries the
730    ///   discriminator's **whole** type, so `v._d === 1` leaves both
731    ///   arms live — TypeScript cannot spell "a `number` that is not 1"
732    ///   — and the no-case-selected arm `{ _d: number }` has the same
733    ///   effect on every union with no `default:`.
734    ///
735    /// So this is not a spelling: it is the shape `adapter/typescript.rs`
736    /// deliberately chose so that a reader of the value narrows on `_d`.
737    /// `Extract<typeof v, { <field>: unknown }>` names the one arm that
738    /// has the field, so the access is checked against the member's own
739    /// declared type rather than through an `any` that would check
740    /// nothing. `typeof v` is the *declared* type and not the narrowed
741    /// one, which is what makes it the same expression in both
742    /// directions.
743    fn case_member(base: &str, field: &str) -> Self::Expr {
744        let arm = ts::Type::generic(
745            Path::new(&["Extract"]),
746            vec![
747                ts::Type::TypeOf(Path::new(&[base])),
748                ts::Type::Object(vec![ts::Signature::Property(ts::Property {
749                    doc: Doc::none(),
750                    readonly: false,
751                    name: ts::PropertyName::ident(field),
752                    optional: false,
753                    type_: ts::Type::Keyword(ts::Keyword::Unknown),
754                })]),
755            ],
756        );
757        ts::Expr::Member {
758            // `Paren` and not `Parenthesised`. The CST has both and the
759            // renderer writes `( … )` for each, which is why the two
760            // halves of this column reached for a different one and
761            // neither noticed. `Paren` is the one the rest of this file
762            // already uses.
763            object: Box::new(ts::Expr::Paren(Box::new(ts::Expr::As {
764                value: Box::new(Self::whole(base)),
765                type_: Box::new(arm),
766            }))),
767            name: Ident::new(field),
768        }
769    }
770
771    fn entry_half(entry: Self::Expr, key: bool) -> Self::Expr {
772        ts::Expr::Member {
773            object: Box::new(entry),
774            name: Ident::new(if key { "key" } else { "value" }),
775        }
776    }
777
778    /// `v.discriminator`, which is the name DDS-TS 1.0 §7.5 gives the
779    /// property and the one `adapter/typescript.rs` declares. The
780    /// trait's default is `_d`, which is what the other columns'
781    /// materialisers call it.
782    fn discriminant(base: &str) -> Self::Expr {
783        Self::member(base, crate::adapter::typescript::DISCRIMINANT_PROPERTY)
784    }
785
786    /// `a.b.StatusOrdinal[v]` — the wire value an enumerator carries.
787    ///
788    /// §7.6 makes an enumeration a string-literal union, so this column
789    /// has no integer in the variable at all: the other columns' answer
790    /// (the value itself) would put the first code unit of the
791    /// enumerator's name on the wire. Measured against the ledger,
792    /// `enum-sparse-values` wrote `70 79 87` for `FAIL OK WARN` before
793    /// this override existed, where the recorded octets are
794    /// `65535 1 100`.
795    ///
796    /// The table is the materialiser's — `adapter/typescript.rs`'s
797    /// `ordinal_path` names it — so the codec looks the value up rather
798    /// than deriving it a second time from the model.
799    ///
800    /// The identity answer is **required** and not a safety net. Two of
801    /// the three callers pass a type that is often not an enumeration
802    /// at all: `encode::write_union_overlay`'s suppressed arm hands
803    /// every `BitPatternSource::Int` through this seam — a `long` arm
804    /// among them — and `encode::map_key_less` hands a map key through
805    /// it whenever the ordering is by enumerator value. For those the
806    /// integer is already in the variable and the table would name
807    /// nothing.
808    ///
809    /// The remaining door — an enumeration whose table cannot be
810    /// named — is a model `adapter::typescript::ts_items` has already
811    /// refused: `ordinal_path` fails there only through `qualified`,
812    /// and `assemble`'s `insert` walks the same `module_chain` for
813    /// every declaration before a file exists.
814    fn enumerator_ordinal(ast: &Ast, id: Id<TypeNode>, value: Self::Expr) -> Self::Expr {
815        let Ok(table) = crate::adapter::typescript::ordinal_path(ast, id) else {
816            return value;
817        };
818        ts::Expr::Index {
819            object: Box::new(ts::Expr::Path(table)),
820            index: Box::new(value),
821        }
822    }
823
824    /// `a.b.Tag.TAG_A` for an enumeration discriminator, and the trait's
825    /// narrowed literal for every other kind.
826    ///
827    /// §7.6 makes an enumeration a string-literal union, so the ordinal
828    /// is not a value of the type: `v.discriminator === 1` against a
829    /// `"TAG_A" | "TAG_B"` is TS2367, "the types have no overlap", on
830    /// every enumeration-discriminated union. The constant is also what
831    /// narrows the arm — comparison against a literal-typed `const` is
832    /// what TypeScript reads as a discriminant test.
833    ///
834    /// A label that names no enumerator falls back to the trait's
835    /// answer, which `tsc` then reports: there is no member for it, and
836    /// a number written in its place would compile against no arm at
837    /// all.
838    fn case_label(ast: &Ast, discriminator: Id<TypeNode>, value: i128) -> Self::Expr {
839        crate::default_typescript::case_label(ast, discriminator, value).unwrap_or_else(|| {
840            Self::cast(Self::value_type(ast, discriminator), Self::wide_int(value))
841        })
842    }
843
844    fn absent() -> Self::Expr {
845        ts::Expr::Path(Path::new(&["undefined"]))
846    }
847
848    /// `!== undefined`, which is what an absent `@optional` member is
849    /// here: `adapter/typescript.rs` declares one `field?: T`, so the
850    /// property is missing rather than null.
851    fn materialised(value: Self::Expr) -> Option<Self::Expr> {
852        Some(ts::Expr::Binary {
853            op: "!==",
854            lhs: Box::new(value),
855            rhs: Box::new(Self::absent()),
856        })
857    }
858
859    /// The member itself: `field?: T` is the value or nothing, and
860    /// there is no wrapper to unwrap the way Java's boxed field has.
861    fn optional_value(_ast: &Ast, _id: Id<TypeNode>, base: &str, field: &str) -> Self::Expr {
862        Self::member(base, field)
863    }
864
865    fn present(base: &str, field: &str) -> Self::Expr {
866        ts::Expr::Binary {
867            op: "!==",
868            lhs: Box::new(Self::member(base, field)),
869            rhs: Box::new(Self::absent()),
870        }
871    }
872
873    /// `delete v.field` for absence, and nothing for presence.
874    ///
875    /// Not `v.field = undefined`. The declaration is `field?: T` and the
876    /// tree compiles under `exactOptionalPropertyTypes`, where assigning
877    /// `undefined` to an optional property is TS2412 — the property
878    /// being *gone* and the property holding `undefined` are two things
879    /// there. `delete` is also what [`crate::default_typescript`] writes
880    /// for the same clause, so a decoder and a defaulter cannot part
881    /// over what absent means.
882    fn set_present(base: &str, field: &str, present: bool) -> Vec<Self::Stmt> {
883        if present {
884            return Vec::new();
885        }
886        vec![ts::Statement::Expr(ts::Expr::Unary {
887            op: "delete ",
888            value: Box::new(Self::member(base, field)),
889        })]
890    }
891
892    /// `delete (v as { field?: unknown }).field` for every case.
893    ///
894    /// DDS-TS 1.0 §7.5 makes a union a discriminated union of object
895    /// types, one per case, so a branch the discriminator does not
896    /// select is a property the object does not have. The Table 9
897    /// default the decoder starts from carries the default branch's
898    /// property, and without this a sample read with another
899    /// discriminator kept it (measured: `idl-conformance/_interop`
900    /// `combo`, `reading` came back with `idleTicks: 0` beside
901    /// `activeRate`). The union type names no property shared by every
902    /// variant, so the operand is widened to an object whose one
903    /// property is optional — the form `delete` accepts (TS2790).
904    fn clear_union_cases(base: &str, fields: &[String]) -> Vec<Self::Stmt> {
905        fields
906            .iter()
907            .map(|field| {
908                let optional = ts::Type::Object(vec![ts::Signature::Property(ts::Property {
909                    doc: Doc::none(),
910                    readonly: false,
911                    name: ts::PropertyName::ident(field),
912                    optional: true,
913                    type_: ts::Type::Keyword(ts::Keyword::Unknown),
914                })]);
915                ts::Statement::Expr(ts::Expr::Unary {
916                    op: "delete ",
917                    value: Box::new(ts::Expr::Member {
918                        object: Box::new(ts::Expr::Paren(Box::new(ts::Expr::As {
919                            value: Box::new(Self::whole(base)),
920                            type_: Box::new(optional),
921                        }))),
922                        name: Ident::new(field),
923                    }),
924                })
925            })
926            .collect()
927    }
928
929    fn optional_destination(base: &str, field: &str) -> (Vec<Self::Stmt>, Self::Expr) {
930        (Vec::new(), Self::member(base, field))
931    }
932
933    /// `==` becomes `===` and `!=` becomes `!==`.
934    ///
935    /// Not house style. TypeScript narrows a discriminated union on the
936    /// **strict** comparison of its discriminant against a literal, and
937    /// a union case member is only reachable through that narrowing:
938    /// `if (v._d == 1) { w.writeI32(v.whole) }` is
939    /// `TS2339: Property 'whole' does not exist`, and the same line with
940    /// `===` compiles. Measured on `array-1d-union`.
941    ///
942    /// The loose form is also a different operator — `0 == "0"` is true
943    /// — so a discriminator that arrived as a string would select an
944    /// arm no other column selects.
945    fn binary(op: &'static str, lhs: Self::Expr, rhs: Self::Expr) -> Self::Expr {
946        ts::Expr::Binary {
947            op: match op {
948                "==" => "===",
949                "!=" => "!==",
950                other => other,
951            },
952            lhs: Box::new(lhs),
953            rhs: Box::new(rhs),
954        }
955    }
956
957    /// `!a` as written, and every other operator as itself.
958    ///
959    /// TypeScript has a real unary form for both `!` and `~`, so
960    /// neither needs the `0 <op> a` stand-in that cost the C++ column
961    /// twenty-one cases.
962    fn unary(op: &'static str, value: Self::Expr) -> Self::Expr {
963        ts::Expr::Unary {
964            op,
965            value: Box::new(value),
966        }
967    }
968
969    fn paren(value: Self::Expr) -> Self::Expr {
970        // The renderer does not bracket by precedence, and `&` binds
971        // more loosely than `!==` here as it does in Java.
972        ts::Expr::Paren(Box::new(value))
973    }
974
975    /// `false` before `true`, spelled as the conjunction — TypeScript's
976    /// `<` does not take two booleans (TS2365).
977    fn bool_less(left: Self::Expr, right: Self::Expr) -> Self::Expr {
978        Self::binary(
979            "&&",
980            ts::Expr::Paren(Box::new(Self::binary(
981                "==",
982                left,
983                ts::Expr::Literal(Literal::Bool(false)),
984            ))),
985            right,
986        )
987    }
988
989    /// A conversion where the target is one of the three scalar holders,
990    /// a rewritten literal where the value is one, and `as T` elsewhere.
991    ///
992    /// `as` is **erased**. Every other column's narrowing is a real
993    /// operation — `(int)` in Java, `static_cast` in C++ — and writing
994    /// `as` for those would be two defects at once. `tsc` refuses the
995    /// erased form outright where the holders differ:
996    /// `zd_bits as number` on a `bigint` is TS2352, which the decode
997    /// walk emits for every bitmask and every bitset field and the
998    /// encode walk for every bitmask, bitset and union case. And where
999    /// it does not refuse — `x as unknown as number` — the value would
1000    /// go into the field still a `bigint`, and every later comparison
1001    /// against it would be false.
1002    ///
1003    /// So the three holders take a runtime conversion:
1004    /// `zdNumber`, `zdBigInt` and `zdChar` in `runtime/typescript`,
1005    /// which carry the same value into `number`, `bigint` and the
1006    /// one-character `string` an IDL `char` is here. Each is the
1007    /// identity on a value already in its holder.
1008    ///
1009    /// A **literal** is rewritten at generation time instead, and that
1010    /// is not an optimisation. Two things need it, and both are the
1011    /// encode walk's union labels — `cast(value_type(discriminator),
1012    /// wide_int(label))`:
1013    ///
1014    /// - a `boolean` or enumeration discriminator reaches **no**
1015    ///   coercion arm, because its holder is `boolean` or the
1016    ///   materialiser's `type Colour = number`, and `1n as boolean` and
1017    ///   `1n as a.b.Colour` are both TS2352;
1018    /// - a literal type is what a union arm is narrowed on, and
1019    ///   `v._d === zdNumber(1n)` narrows nothing while `v._d === 1`
1020    ///   narrows to the arm.
1021    ///
1022    /// The four forms are the four `adapter/typescript.rs`'s
1023    /// `discriminant_literal` writes, read off the target type rather
1024    /// than off the model — a third reading of that mapping is how the
1025    /// declaration and the comparison come to describe different arms.
1026    fn cast(type_: Self::Type, value: Self::Expr) -> Self::Expr {
1027        if let ts::Expr::Literal(Literal::Int(text)) = &value {
1028            if let Some(literal) = literal_as(&type_, text) {
1029                return ts::Expr::Literal(literal);
1030            }
1031        }
1032        // The walk's `signed char` — the C spelling of a map key's
1033        // signed order — has no type here: an IDL `char` is a
1034        // one-character `string`. `((k.charCodeAt(0) + 128) % 256) -
1035        // 128` is the two's complement reading of its one octet, the
1036        // same arithmetic the Python column writes and for the same
1037        // reason (see `python.rs::signed_char`).
1038        if type_ == ts::Type::named(Self::path("signed char")) {
1039            let point = ts::Expr::Call {
1040                callee: Box::new(ts::Expr::Member {
1041                    object: Box::new(ts::Expr::Paren(Box::new(value))),
1042                    name: Ident::new("charCodeAt"),
1043                }),
1044                args: vec![Self::int(0)],
1045            };
1046            return Self::binary(
1047                "-",
1048                ts::Expr::Paren(Box::new(Self::binary(
1049                    "%",
1050                    ts::Expr::Paren(Box::new(Self::binary("+", point, Self::int(128)))),
1051                    Self::int(256),
1052                ))),
1053                Self::int(128),
1054            );
1055        }
1056        let coercion = match &type_ {
1057            ts::Type::Keyword(ts::Keyword::Number) => Some("zdNumber"),
1058            ts::Type::Keyword(ts::Keyword::BigInt) => Some("zdBigInt"),
1059            ts::Type::Keyword(ts::Keyword::String) => Some("zdChar"),
1060            _ => None,
1061        };
1062        match coercion {
1063            // Not twice. The walk narrows and [`Self::store`] narrows
1064            // again at the destination, and `zdChar(zdChar(x))` is right
1065            // but says the same thing twice — which reads as though the
1066            // two were different narrowings.
1067            Some(callee) if is_call_to(&value, callee) => value,
1068            Some(callee) => Self::call(callee, vec![value]),
1069            None => ts::Expr::As {
1070                value: Box::new(value),
1071                type_: Box::new(type_),
1072            },
1073        }
1074    }
1075
1076    /// The same conversion, at the point a value reaches its member.
1077    ///
1078    /// [`Self::cast`] answers the walk where the walk asked for a
1079    /// narrowing; this answers where it did not. A `wchar` is the one
1080    /// that needs it: the walk narrows the runtime's answer to the
1081    /// sixteen-bit storage every other column keeps a `wchar` in, and
1082    /// this column keeps it in a `string`, so the value arrives as a
1083    /// `number` and the destination is a `string`. Without the second
1084    /// conversion that is a `tsc` error on every `wchar` member.
1085    fn store(place: Self::Expr, type_: Self::Type, value: Self::Expr) -> Self::Stmt {
1086        Self::perform(Self::binary(
1087            "=",
1088            assignable(place),
1089            Self::cast(type_, value),
1090        ))
1091    }
1092
1093    fn handoff(value: Self::Expr, _form: Handoff) -> Self::Expr {
1094        value
1095    }
1096
1097    /// `place = zd_dec_T(r, zd_tc);`
1098    ///
1099    /// The trait's default hands the destination *to* the callee, which
1100    /// is C's shape: its decoder takes a `T *` and fills it. A
1101    /// TypeScript function has no out-parameter, so this column's
1102    /// decoders answer with the value — [`Self::routine`] declares them
1103    /// `(r, …extra)` — and the destination is assigned at the call site.
1104    ///
1105    /// Without this override the generated call was
1106    /// `zd_dec_p_long(r, v.a, TryConstruct.Discard)`: three arguments to
1107    /// a two-parameter function, and — had it compiled — a member read
1108    /// off the wire and thrown away, leaving Table 9's value in `v.a`
1109    /// with nothing to say so.
1110    fn read_call(
1111        callee: &str,
1112        place: Self::Expr,
1113        _form: Handoff,
1114        behaviour: Self::Expr,
1115    ) -> Self::Stmt {
1116        Self::perform(Self::binary(
1117            "=",
1118            assignable(place),
1119            Self::call(callee, vec![Self::name("r"), behaviour]),
1120        ))
1121    }
1122
1123    /// `v = zd_dec_T(r, zd_tc);` — a `typedef`, handed straight on.
1124    fn read_forward(callee: &str) -> Self::Stmt {
1125        Self::read_call(
1126            callee,
1127            Self::whole("v"),
1128            Handoff::Value,
1129            Self::name("zd_tc"),
1130        )
1131    }
1132
1133    // -- statements ----------------------------------------------------
1134
1135    /// `let name: T = value`, with an integer literal spelled the way
1136    /// `T` reads it.
1137    ///
1138    /// `let bits: bigint = 0` does not typecheck — `0` is a `number` —
1139    /// and the walk declares exactly that for a bitset's accumulator:
1140    /// `T::declare(T::scalar(Scalar::UInt64), "bits", int(0))`. The `n`
1141    /// is not a suffix the walk can add, because in C and Java the same
1142    /// declaration is right as it stands. `adapter/typescript.rs` writes
1143    /// `k: bigint = 0n` for a member of the same type and for the same
1144    /// reason.
1145    fn declare(type_: Self::Type, name: &str, value: Option<Self::Expr>) -> Self::Stmt {
1146        let value = value.map(|value| match &value {
1147            ts::Expr::Literal(Literal::Int(text)) => {
1148                literal_as(&type_, text).map_or_else(|| value.clone(), ts::Expr::Literal)
1149            }
1150            _ => value,
1151        });
1152        ts::Statement::Declare {
1153            kind: ts::VarKind::Let,
1154            name: Ident::new(name),
1155            type_: Some(type_),
1156            value,
1157        }
1158    }
1159
1160    fn perform(value: Self::Expr) -> Self::Stmt {
1161        ts::Statement::Expr(value)
1162    }
1163
1164    fn give_back(value: Option<Self::Expr>) -> Self::Stmt {
1165        ts::Statement::Return(Some(value.unwrap_or_else(|| Self::whole("v"))))
1166    }
1167
1168    /// Nothing. TypeScript has no unread-parameter diagnostic under the
1169    /// settings this tree compiles with, and `zd_tc;` is not a
1170    /// statement worth writing.
1171    fn mark_used(_name: &str) -> Vec<Self::Stmt> {
1172        Vec::new()
1173    }
1174
1175    /// `void v;` — the one form TypeScript has for "read and
1176    /// discard", and the only place the walk needs it.
1177    fn drop_value(value: Self::Expr) -> Self::Stmt {
1178        ts::Statement::Expr(ts::Expr::Unary {
1179            op: "void ",
1180            value: Box::new(value),
1181        })
1182    }
1183
1184    fn when(
1185        condition: Self::Expr,
1186        then: Self::Block,
1187        otherwise: Option<Self::Block>,
1188    ) -> Self::Stmt {
1189        ts::Statement::If {
1190            condition,
1191            then,
1192            otherwise,
1193        }
1194    }
1195
1196    fn count_up(counter: &str, bound: Self::Expr, body: Self::Block) -> Self::Stmt {
1197        ts::Statement::For {
1198            counter: Ident::new(counter),
1199            bound: Box::new(bound),
1200            body,
1201        }
1202    }
1203
1204    fn until(condition: Self::Expr, body: Self::Block) -> Self::Stmt {
1205        ts::Statement::While {
1206            condition: Box::new(condition),
1207            body,
1208        }
1209    }
1210
1211    fn leave() -> Self::Stmt {
1212        ts::Statement::Break
1213    }
1214
1215    fn note(doc: Doc) -> Self::Stmt {
1216        ts::Statement::Comment(doc)
1217    }
1218
1219    fn block(statements: Vec<Self::Stmt>) -> Self::Block {
1220        ts::Block { statements }
1221    }
1222
1223    fn into_statements(block: Self::Block) -> Vec<Self::Stmt> {
1224        block.statements
1225    }
1226
1227    /// A new array of `count` elements, which a TypeScript collection
1228    /// is: there is no reserve-then-fill, so the fill is the loop the
1229    /// walk hands over.
1230    ///
1231    /// `new Array(n)` is `n` holes. A sequence's element decoder answers
1232    /// with a value and the slot is assigned outright, so nothing more
1233    /// is needed; a `map`'s two halves are written *into* the entry at
1234    /// `v[i]`, and an entry that was never made is a `TypeError` at the
1235    /// first key — measured on `map-key-long`, "Cannot set properties
1236    /// of undefined". That is what `assigned_whole` separates, exactly
1237    /// as the Java column's `make_room` says, and the seed is the empty
1238    /// object whose halves the loop assigns before anything reads them.
1239    fn make_room(
1240        base: &str,
1241        element: Self::Type,
1242        count: Self::Expr,
1243        assigned_whole: bool,
1244        fill: Vec<Self::Stmt>,
1245    ) -> Vec<Self::Stmt> {
1246        let mut out = vec![ts::Statement::Expr(ts::Expr::Binary {
1247            op: "=",
1248            lhs: Box::new(Self::whole(base)),
1249            rhs: Box::new(ts::Expr::New {
1250                callee: Path::new(&["Array"]),
1251                args: vec![count],
1252            }),
1253        })];
1254        if !assigned_whole {
1255            out.push(Self::count_up(
1256                "zd_e",
1257                Self::count_of(base, None),
1258                ts::Block {
1259                    statements: vec![ts::Statement::Expr(ts::Expr::Binary {
1260                        op: "=",
1261                        lhs: Box::new(ts::Expr::Index {
1262                            object: Box::new(Self::whole(base)),
1263                            index: Box::new(Self::whole("zd_e")),
1264                        }),
1265                        rhs: Box::new(ts::Expr::As {
1266                            value: Box::new(ts::Expr::Object(Vec::new())),
1267                            type_: Box::new(element),
1268                        }),
1269                    })],
1270                },
1271            ));
1272        }
1273        out.extend(fill);
1274        out
1275    }
1276
1277    /// `let name: T = {} as T;` — somewhere to put a value before its
1278    /// halves arrive.
1279    ///
1280    /// The trait's default declares without a value, which TypeScript
1281    /// under `strict` reads as "used before assigned" the moment a
1282    /// map's counting pass writes `zd_scratch.key` — and at run time
1283    /// the property assignment lands on `undefined`. The empty object
1284    /// is a seed whose properties the very next statements fill.
1285    fn storage(type_: Self::Type, name: &str) -> Self::Stmt {
1286        ts::Statement::Declare {
1287            kind: ts::VarKind::Let,
1288            name: Ident::new(name),
1289            type_: Some(type_.clone()),
1290            value: Some(ts::Expr::As {
1291                value: Box::new(ts::Expr::Object(Vec::new())),
1292                type_: Box::new(type_),
1293            }),
1294        }
1295    }
1296
1297    /// `v.length = n`, which shortens an array in place.
1298    ///
1299    /// §7.3.1.2.1.12.3's TRIM. A TypeScript array's length is writable
1300    /// and assigning a smaller one drops the tail, which is exactly the
1301    /// operation and is why this column needs no `splice`.
1302    fn keep_first(base: &str, count: Self::Expr) -> Vec<Self::Stmt> {
1303        vec![ts::Statement::Expr(ts::Expr::Binary {
1304            op: "=",
1305            lhs: Box::new(ts::Expr::Member {
1306                object: Box::new(Self::whole(base)),
1307                name: Ident::new("length"),
1308            }),
1309            rhs: Box::new(count),
1310        })]
1311    }
1312
1313    fn sequence_element(
1314        base: &str,
1315        index: Self::Expr,
1316        _element: Self::Type,
1317    ) -> (Vec<Self::Stmt>, Self::Expr, Vec<Self::Stmt>) {
1318        (
1319            Vec::new(),
1320            Self::index(Self::elements(base), index),
1321            Vec::new(),
1322        )
1323    }
1324
1325    // -- assembly ------------------------------------------------------
1326
1327    /// `export function encodeT(w: Writer, v: T): void`.
1328    ///
1329    /// Exported. A generated file is one module, and a module-level
1330    /// function that is not exported is reachable from nothing outside
1331    /// that file: `wireprobe-zerodds-typescript-gen` never noticed
1332    /// because it appends its own `main` to the same module, and an
1333    /// application generated by `idlc generate` has no way to call a
1334    /// single one of them. The same defect the Java column carried as
1335    /// `private static` and was measured out of; the declarations
1336    /// beside these — `newT`, `isT`, the interfaces — were exported all
1337    /// along.
1338    fn routine(spec: Routine<Self>) -> Option<Self::Decl> {
1339        let body = spec.body?;
1340        let mut params = vec![ts::Param {
1341            name: Ident::new(if spec.writing { "w" } else { "r" }),
1342            type_: Some(ts::Type::named(Path::new(&[if spec.writing {
1343                "Writer"
1344            } else {
1345                "Reader"
1346            }]))),
1347            optional: false,
1348            default: None,
1349            rest: false,
1350        }];
1351        // A map's parameter is the `ReadonlyMap` §7.9.1 declares and the
1352        // walk's body is written against the entry array XTypes 1.3
1353        // §7.4.1.1.4 puts on the wire, so the pairs are staged under the
1354        // name the body uses and the parameter takes another.
1355        let staged = spec
1356            .writing
1357            .then(|| readonly_map_halves(&spec.sample))
1358            .flatten();
1359        if spec.writing {
1360            params.push(ts::Param {
1361                name: Ident::new(if staged.is_some() { "zd_map" } else { "v" }),
1362                type_: Some(spec.sample.clone()),
1363                optional: false,
1364                default: None,
1365                rest: false,
1366            });
1367        }
1368        params.extend(spec.extra.into_iter().map(|(type_, name)| ts::Param {
1369            name: Ident::new(&name),
1370            type_: Some(type_),
1371            optional: false,
1372            default: None,
1373            rest: false,
1374        }));
1375        let (returns, body) = if spec.writing {
1376            let body = match staged {
1377                Some((key, value)) => {
1378                    let mut statements = vec![ts::Statement::Declare {
1379                        kind: ts::VarKind::Const,
1380                        name: Ident::new("v"),
1381                        type_: Some(ts::Type::Array(Box::new(entry_object(key, value)))),
1382                        value: Some(entries_of_map()),
1383                    }];
1384                    statements.extend(body.statements);
1385                    ts::Block { statements }
1386                }
1387                None => body,
1388            };
1389            (Self::nothing(), body)
1390        } else {
1391            (
1392                spec.sample.clone(),
1393                Self::reading_body(&spec.name, spec.sample, body),
1394            )
1395        };
1396        Some(ts::Decl::Function(ts::Function {
1397            doc: spec.doc,
1398            exported: true,
1399            name: Ident::new(&spec.name),
1400            generics: Vec::new(),
1401            params,
1402            returns: Some(returns),
1403            body: Some(body),
1404        }))
1405    }
1406
1407    /// A call to a generated defaulter and not a literal in place.
1408    ///
1409    /// A literal cannot carry an aggregate, and one written at every
1410    /// position a member can go missing would put the same answer in the
1411    /// file as many times as the type is reached — where two copies can
1412    /// differ. [`crate::default_typescript`] carries the whole table and
1413    /// the argument.
1414    ///
1415    /// # Errors
1416    /// Never; the signature matches the emitters around it.
1417    fn default_into(
1418        ast: &Ast,
1419        id: Id<TypeNode>,
1420        place: Self::Expr,
1421    ) -> Result<Self::Stmt, EncodeError> {
1422        Ok(crate::default_typescript::default_into(ast, id, place))
1423    }
1424
1425    /// # Errors
1426    /// As [`crate::default_typescript::member_default`]: a `@default`
1427    /// this column cannot spell as a TypeScript expression is refused
1428    /// rather than silently replaced by Table 9's zero.
1429    fn member_default(
1430        ast: &Ast,
1431        node: &MemberNode,
1432        type_: Id<TypeNode>,
1433        place: Self::Expr,
1434    ) -> Result<Vec<Self::Stmt>, EncodeError> {
1435        crate::default_typescript::member_default(ast, node, type_, place)
1436    }
1437
1438    /// `a.b.Colour.RED` — the `as const` object the materialiser
1439    /// declared, written from the file root.
1440    ///
1441    /// The trait's default answers the bare enumerator name, which is
1442    /// right for a target that flattens its modules into a prefix and
1443    /// wrong here: the TypeScript materialiser nests each declaration in
1444    /// a namespace per IDL module and a generated codec sits at the file
1445    /// root, so `RED` there resolves to nothing — or, worse, to whatever
1446    /// else happened to be in scope.
1447    fn enumerator_constant(ast: &Ast, id: Id<TypeNode>, enumerator: &str) -> Self::Expr {
1448        crate::default_typescript::enumerator_constant(ast, id, enumerator)
1449    }
1450
1451    /// `123n`, which is how TypeScript spells a 64-bit constant.
1452    ///
1453    /// The trait's default is C's `UINT64_C(123)`, which is a macro no
1454    /// TypeScript file declares. Both of these reach the generated
1455    /// source through the decode walk alone — the bitmask's
1456    /// undeclared-bit check and the enumerator chain — so a column that
1457    /// did not override them produced a decoder that did not parse.
1458    fn wide_unsigned(value: u64) -> Self::Expr {
1459        Self::literal(Literal::Int(format!("{value}n")))
1460    }
1461
1462    /// As [`Self::wide_unsigned`], and the value rather than the
1463    /// two's-complement pattern: a `bigint` is unbounded, so a negative
1464    /// enumerator pattern is written as the negative number it is.
1465    fn wide_signed(value: i128) -> Self::Expr {
1466        Self::literal(Literal::Int(format!("{value}n")))
1467    }
1468}
1469
1470/// How deep a chain of typedefs and collections is followed.
1471const MAX_TYPE_DEPTH: usize = 64;
1472
1473/// The same place, as something that can be assigned to.
1474///
1475/// [`TypeScript::index`] writes `v[i]!` because `v[i]` is
1476/// `T | undefined` under `noUncheckedIndexedAccess`. That assertion is
1477/// right where the element is **read** and is not an expression that can
1478/// be assigned to: `v[i]! = x` is a syntax error, where `v[i]!.key = x`
1479/// is fine because the assertion is on the object of a member access.
1480///
1481/// So the assertion is stripped at exactly the two points a place is
1482/// written — [`TypeScript::store`] and [`TypeScript::read_call`] — and
1483/// nowhere else. Nothing is asserted away: a destination is not read, so
1484/// there is no `undefined` to rule out.
1485fn assignable(place: ts::Expr) -> ts::Expr {
1486    match place {
1487        ts::Expr::NonNull(inner) => *inner,
1488        other => other,
1489    }
1490}
1491
1492/// Whether `value` is already a call to the unqualified function `name`.
1493fn is_call_to(value: &ts::Expr, name: &str) -> bool {
1494    let ts::Expr::Call { callee, .. } = value else {
1495        return false;
1496    };
1497    let ts::Expr::Path(path) = callee.as_ref() else {
1498        return false;
1499    };
1500    path.segments().len() == 1
1501        && path
1502            .segments()
1503            .first()
1504            .is_some_and(|segment| segment.as_str() == name)
1505}
1506
1507/// An integer literal in the spelling `target` calls for, or `None`
1508/// where the literal is not the thing to change.
1509///
1510/// The counterpart of `adapter/typescript.rs`'s `discriminant_literal`,
1511/// read off the target type instead of the model because that is all a
1512/// cast has. The two have to produce the same four forms: the
1513/// declaration says `{ _d: true; … }` and the comparison has to say
1514/// `v._d === true`, or the arm is unreachable and its member is a
1515/// property TypeScript says does not exist.
1516///
1517/// A **named** target answers like `number`, and that is the enumeration
1518/// case: the materialiser declares `type Colour = number` beside its
1519/// `as const` object, so a label cast to `Colour` is the number.
1520/// `unknown`, `void` and `any` answer `None` — a literal reaching one of
1521/// those is not a discriminator and rewriting it would hide whatever put
1522/// it there.
1523///
1524/// **The text has to be an integer.** [`Literal::Int`] is not only an
1525/// integer: `emit/decode.rs`'s `literal::<T>("false")` carries the
1526/// keyword in one, because that variant is the tree's raw-token escape
1527/// and `false` is what C, Java and TypeScript all spell a false boolean
1528/// with. Rewriting it here read `"false"` as "not the digit zero" and
1529/// declared `let zd_seen0: boolean = true` — every `@mutable` member
1530/// marked as already seen, so no member the stream omitted took
1531/// Table 9's value and the decoder answered with a plausible sample.
1532/// `crates/lang-rules/tests/typescript_reader_generates.rs` caught it by
1533/// type-checking what it generates:
1534/// `TS2367: This comparison appears to be unintentional because the
1535/// types 'true' and 'false' have no overlap`.
1536fn literal_as(target: &ts::Type, text: &str) -> Option<Literal> {
1537    let digits = text.strip_suffix('n').unwrap_or(text);
1538    let body = digits.strip_prefix('-').unwrap_or(digits);
1539    if body.is_empty() || !body.bytes().all(|byte| byte.is_ascii_digit()) {
1540        return None;
1541    }
1542    match target {
1543        ts::Type::Keyword(ts::Keyword::BigInt) => Some(Literal::Int(format!("{digits}n"))),
1544        ts::Type::Keyword(ts::Keyword::Boolean) => Some(Literal::Bool(digits != "0")),
1545        // A `char` or `wchar` discriminator. The label evaluated to a
1546        // code point and the member holds the character.
1547        ts::Type::Keyword(ts::Keyword::String) => digits
1548            .parse::<u32>()
1549            .ok()
1550            .and_then(char::from_u32)
1551            .map(Literal::Char),
1552        ts::Type::Keyword(ts::Keyword::Number) | ts::Type::Named { .. } => {
1553            Some(Literal::Int(digits.to_string()))
1554        }
1555        _ => None,
1556    }
1557}
1558
1559/// The TypeScript type of a value, structurally.
1560///
1561/// `number` and `bigint` by the boundary [`TypeScript::scalar`] draws,
1562/// an array for either collection, the declared name for an aggregate,
1563/// and the aliased type for a typedef — a `typedef` has no TypeScript
1564/// form of its own, and `type Ids = number[]` beside it would be a
1565/// second name for one type. TypeScript's types are structural, so a
1566/// codec that resolves through an alias and a member declared as the
1567/// alias are the same type.
1568///
1569/// Every named answer is the path from the **file root** and not the
1570/// declaration's own name. The materialiser nests a declaration in a
1571/// namespace per IDL module and a generated codec sits at the root, so
1572/// `Point` there names nothing; `module_prefix` answers the empty string
1573/// because a target with namespaces has no prefix to add, and that is
1574/// not the same as having nothing to qualify. Measured from both
1575/// directions before they were merged: `empty-struct`
1576/// (`module m { struct S {}; }`) through the write probe is
1577/// `error TS2304: Cannot find name 'S'`.
1578///
1579/// An enumeration, a bitmask and a bitset are named too and not spelled
1580/// `number`. A `@bit_bound(64)` bitmask is a `bigint` and a bitset is an
1581/// object with one property per bitfield, so `number` was wrong for two
1582/// of the three.
1583///
1584/// zerodds-lint: recursion-depth 64 (collection and typedef nesting;
1585/// bounded by `MAX_TYPE_DEPTH`, past which the spelling is
1586/// `unknown` — the same answer a type that does not resolve takes)
1587fn value_type_of(ast: &Ast, id: Id<TypeNode>, depth: usize) -> ts::Type {
1588    use zerodds_ast::{PrimitiveKind, TypeKind};
1589    let unknown = || ts::Type::Keyword(ts::Keyword::Unknown);
1590    if depth > MAX_TYPE_DEPTH {
1591        return unknown();
1592    }
1593    let Ok(node) = ast.type_(id) else {
1594        return unknown();
1595    };
1596    let declared = || {
1597        crate::adapter::typescript::declared_path(ast, id)
1598            .map_or_else(|_| unknown(), ts::Type::named)
1599    };
1600    match &node.kind {
1601        TypeKind::Primitive(kind) => match kind {
1602            PrimitiveKind::Boolean => TypeScript::scalar(Scalar::Bool),
1603            PrimitiveKind::Char | PrimitiveKind::WChar => TypeScript::scalar(Scalar::Char),
1604            PrimitiveKind::Int64 | PrimitiveKind::UInt64 => TypeScript::scalar(Scalar::Int64),
1605            PrimitiveKind::Float128 => TypeScript::scalar(Scalar::LongDouble),
1606            _ => TypeScript::scalar(Scalar::Int32),
1607        },
1608        TypeKind::String { .. } | TypeKind::Fixed { .. } => ts::Type::string(),
1609        // An enumeration is declared as `type E = number` beside its
1610        // `as const` object, and a bitmask as `type M = <holder>`. The
1611        // *declared name* and not the holder it aliases: the two are the
1612        // same type to `tsc` and only one of them says what the member
1613        // is.
1614        TypeKind::Enumeration { .. } | TypeKind::Bitmask { .. } => declared(),
1615        // A bitset is an object with one property per named bitfield,
1616        // not an integer. The anchor is the codec's business and never
1617        // appears in the type — a `number` here made every bitset member
1618        // an assignment `tsc` refuses.
1619        TypeKind::Bitset { .. }
1620        | TypeKind::Structure { .. }
1621        | TypeKind::Union { .. }
1622        | TypeKind::Valuetype { .. }
1623        | TypeKind::Native => declared(),
1624        TypeKind::Sequence { element, .. } => {
1625            let inner = element
1626                .resolved()
1627                .map_or_else(unknown, |target| value_type_of(ast, target, depth + 1));
1628            ts::Type::Array(Box::new(inner))
1629        }
1630        // One `[]` per dimension. `long a[2][3]` is `number[][]`, which
1631        // is what the materialiser declares and what the walk indexes
1632        // twice; a single `[]` here made every multi-dimensional array
1633        // a `number[]` whose elements were then indexed again.
1634        TypeKind::Array { element, dims } => {
1635            let mut out = element
1636                .resolved()
1637                .map_or_else(unknown, |target| value_type_of(ast, target, depth + 1));
1638            for _ in dims {
1639                out = ts::Type::Array(Box::new(out));
1640            }
1641            out
1642        }
1643        // DDS-TS 1.0 §7.9.1 makes a map a `ReadonlyMap<K', V'>` at the
1644        // TypeScript surface, which is what a member holds and what a
1645        // codec function therefore takes and answers with. The entry
1646        // array XTypes 1.3 §7.4.1.1.4 puts on the wire is the codec's
1647        // own staging and never leaves it; [`TypeScript::routine`]
1648        // converts at the two ends.
1649        TypeKind::Map { key, value, .. } => {
1650            let half = |half: &zerodds_ast_core::Ref<TypeNode>| {
1651                half.resolved()
1652                    .map_or_else(unknown, |target| value_type_of(ast, target, depth + 1))
1653            };
1654            ts::Type::generic(Path::new(&["ReadonlyMap"]), vec![half(key), half(value)])
1655        }
1656        TypeKind::Alias { aliased } => aliased
1657            .resolved()
1658            .map_or_else(unknown, |target| value_type_of(ast, target, depth + 1)),
1659        // A boxed value is a reference, absent when it is not there, and
1660        // `undefined` is what TypeScript's absent is.
1661        TypeKind::ValueBox { boxed } => {
1662            let inner = boxed
1663                .resolved()
1664                .map_or_else(unknown, |target| value_type_of(ast, target, depth + 1));
1665            ts::Type::Union(vec![inner, ts::Type::Keyword(ts::Keyword::Undefined)])
1666        }
1667        // No TypeScript data form at all; `adapter/typescript.rs`
1668        // refuses a member carrying one by name. `unknown` and not `any`
1669        // so that a value of it has to be narrowed before it is used.
1670        TypeKind::Any
1671        | TypeKind::ValueBase
1672        | TypeKind::ObjectRef { .. }
1673        | TypeKind::AnnotationType { .. } => unknown(),
1674    }
1675}