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