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}