Skip to main content

zerodds_idl_ts/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! IDL → TypeScript Codegen for DDS-TS 1.0.
4//!
5//! Crate `zerodds-idl-ts`. Safety classification: **STANDARD**.
6//!
7//! # Scope
8//!
9//! Generates `.ts` source from a parsed IDL AST per the
10//! `documentation/specs/dds-ts-1.0/main.tex` vendor specification.
11//! Output uses uniform structural interfaces (no class promotion),
12//! Type-Descriptor side-tables, and the `@zerodds/types` runtime
13//! library bundled under `src/runtime/`.
14//!
15//! # Mapping summary (DDS-TS 1.0 §7)
16//!
17//! ```text
18//! boolean              | boolean
19//! char / wchar         | Char / WChar    (branded, runtime/branded.ts)
20//! octet                | number          (0..255)
21//! short..int32         | number
22//! long long..int64     | bigint
23//! float / double       | number
24//! long double          | LongDouble      (opaque carrier, 16 bytes)
25//! string / wstring     | string          (+ _BOUND const if bounded)
26//! any                  | DdsAny          (boxed value with typeId)
27//! sequence<T>          | Array<T>        (+ _BOUND if bounded)
28//! T[N]                 | Array<T>        (+ _LENGTH const)
29//! map<K, V>            | ReadonlyMap<K', V'>
30//! struct               | interface       + DdsTypeDescriptor + type-guard
31//! exception            | interface       + extends DdsException
32//! enum                 | as-const object + string-literal-union (no `enum`)
33//! union                | discriminated union  + descriptor (literal narrowing)
34//! typedef T name       | export type name = T  + descriptor
35//! bitset               | interface (number/bigint per width) + _BITS consts
36//! bitmask              | as-const shifts (number/bigint per @bit_bound)
37//! interface (Ops)      | { Iface }Client + { Iface }Handler + ServiceDescriptor
38//! ```
39
40#![forbid(unsafe_code)]
41#![warn(missing_docs)]
42
43extern crate alloc;
44
45mod amqp;
46
47use alloc::string::String;
48use alloc::vec::Vec;
49
50use zerodds_idl::ast::{
51    Definition, FloatingType, IntegerType, PrimitiveType, Specification, StringType, TypeSpec,
52};
53
54/// Codegen entry point. Produces a TypeScript source string for
55/// a parsed IDL `Specification` per DDS-TS 1.0.
56///
57/// The output begins with any `@verbatim(placement=BEGIN_FILE)`
58/// blocks, then the leading `// Generated by …`-banner, then the
59/// `@zerodds/types` runtime imports, then one emitted block per
60/// top-level definition. `END_FILE` verbatim blocks are appended
61/// last.
62///
63/// # Errors
64/// `IdlTsError::Unsupported` when an IDL construct violates a
65/// PSM-level constraint (e.g.\ bitset total width > 64).
66pub fn generate_ts_source(spec: &Specification) -> Result<String, IdlTsError> {
67    let (out, _diagnostics) = generate_ts_source_with_diagnostics(spec)?;
68    Ok(out)
69}
70
71/// Spec §7.2.3 / §8.1.2 / §8.1.3 — Codegen mit angehängten
72/// AMQP-Bindings.
73///
74/// Identisch zu [`generate_ts_source`], hängt aber am Ende einen
75/// AMQP-Codec-Block an: pro Top-Level-Struct/Union eine Funktion
76/// `toAmqpValue_<TypeName>(v)` und `toJsonString_<TypeName>(v)`.
77/// Die Funktionen rufen in das Runtime-Modul `@zerodds/amqp/codec`,
78/// das als separate Library-Crate kommt.
79///
80/// # Errors
81/// Wie [`generate_ts_source`].
82pub fn generate_ts_source_with_amqp(spec: &Specification) -> Result<String, IdlTsError> {
83    let mut out = generate_ts_source(spec)?;
84    amqp::append_amqp_helpers(&mut out, spec)?;
85    Ok(out)
86}
87
88/// Configuration for diagnostic-aware codegen.
89#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
90pub struct CodegenConfig {
91    /// When `true`, unrecognised annotations escalate from
92    /// `DDS-TS-W002` (warn-and-drop) to `DDS-TS-E004` (fatal).
93    pub strict_annotations: bool,
94}
95
96/// Diagnostic-aware codegen entry point.
97///
98/// Returns the same TypeScript source as [`generate_ts_source`]
99/// plus a (possibly empty) vector of [`Diagnostic`]s emitted
100/// during code generation. Codes follow Annex D of DDS-TS 1.0
101/// (`E*` fatal, `W*` warning, `I*` informational).
102///
103/// # Errors
104/// `IdlTsError::Unsupported` for fatal PSM-level violations.
105pub fn generate_ts_source_with_diagnostics(
106    spec: &Specification,
107) -> Result<(String, Vec<Diagnostic>), IdlTsError> {
108    generate_ts_source_with_config(spec, &CodegenConfig::default())
109}
110
111/// Diagnostic-aware codegen with explicit configuration.
112///
113/// # Errors
114/// `IdlTsError::Unsupported` on fatal violations (e.g. orphan
115/// forward declaration, unrecognised annotation under
116/// `strict_annotations`, conflicting annotation set).
117pub fn generate_ts_source_with_config(
118    spec: &Specification,
119    config: &CodegenConfig,
120) -> Result<(String, Vec<Diagnostic>), IdlTsError> {
121    let mut diagnostics: Vec<Diagnostic> = Vec::new();
122
123    // §9 — collect file-level verbatim blocks across all top-level
124    // definitions and modules (recursively) before emission.
125    let begin_file = collect_file_verbatim(&spec.definitions, VerbatimPlacement::BeginFile);
126    let end_file = collect_file_verbatim(&spec.definitions, VerbatimPlacement::EndFile);
127
128    let mut out = String::new();
129    if !begin_file.is_empty() {
130        out.push_str(&begin_file);
131    }
132    out.push_str("// Generated by zerodds idl-ts. Do not edit.\n\n");
133    out.push_str(RUNTIME_IMPORT_BLOCK);
134    out.push('\n');
135    let empty_path: Vec<String> = Vec::new();
136    for def in &spec.definitions {
137        emit_definition_with_diagnostics(&mut out, def, &mut diagnostics, &empty_path)?;
138    }
139    if !end_file.is_empty() {
140        out.push_str(&end_file);
141    }
142
143    // §7.12 / §D.1 — annotation conflicts (E003) and unknown
144    // annotations (W002 / E004) checked across the whole spec.
145    scan_annotation_conflicts(&spec.definitions, &mut diagnostics)?;
146    scan_unknown_annotations(&spec.definitions, &mut diagnostics, config)?;
147
148    // §11.7 — orphan forward-declarations: enumerate forward decls
149    // and verify a matching complete declaration exists somewhere
150    // in the input. Missing → DDS-TS-E002 (fatal).
151    check_forward_declaration_orphans(&spec.definitions, &mut diagnostics)?;
152
153    // §D.1 I001 — `long double` informational hint per type.
154    scan_long_double_uses(&spec.definitions, &mut diagnostics);
155
156    // §D.1 W004 — union with no `default` and not exhaustively
157    // covered: descriptor.hasImplicitDefault is set in emit_union_
158    // descriptor; the diagnostic is surfaced here.
159    scan_union_implicit_defaults(&spec.definitions, &mut diagnostics);
160
161    // §D.1 W003 — map key with non-value-equality (struct ref).
162    scan_map_key_hazards(&spec.definitions, &mut diagnostics);
163
164    Ok((out, diagnostics))
165}
166
167/// Diagnostic record emitted by the codegen.
168///
169/// Codes match Annex D of the DDS-TS 1.0 vendor specification
170/// (`E001..E099` fatal-class reserved, `W001..W099` warnings,
171/// `I001..I099` informational).
172#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct Diagnostic {
174    /// Stable diagnostic code (e.g.\ `"DDS-TS-W002"`).
175    pub code: &'static str,
176    /// Severity classification.
177    pub severity: Severity,
178    /// Human-readable message.
179    pub message: String,
180}
181
182/// Severity of a [`Diagnostic`].
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub enum Severity {
185    /// Fatal error — code generation aborted.
186    Fatal,
187    /// Warning — code generation continued.
188    Warning,
189    /// Informational note — purely advisory.
190    Info,
191}
192
193/// Standardised runtime-import header emitted at the top of every
194/// generated `.ts` file.
195const RUNTIME_IMPORT_BLOCK: &str = "\
196import type {
197    Char, WChar, LongDouble,
198    DdsAny, DdsException,
199    DdsTypeDescriptor, DdsMemberDescriptor, DdsTypeRef,
200    ServiceDescriptor, OperationDescriptor,
201    OperationParameterDescriptor, AttributeDescriptor, ParameterMode,
202} from \"@zerodds/types\";
203import {
204    registerType, makeChar, makeWChar, makeLongDouble,
205} from \"@zerodds/types\";
206import type {
207    DdsTopicType, EndianMode,
208} from \"@zerodds/cdr\";
209import {
210    Xcdr2Writer, Xcdr2Reader, md5,
211} from \"@zerodds/cdr\";
212";
213
214/// Verbatim-placement kinds per DDS-TS 1.0 §9.3 (which mirrors
215/// OMG X-Types 1.3 §7.2.2.4.8).
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217enum VerbatimPlacement {
218    BeginFile,
219    BeforeDeclaration,
220    BeginDeclaration,
221    EndDeclaration,
222    AfterDeclaration,
223    EndFile,
224}
225
226impl VerbatimPlacement {
227    fn from_str(s: &str) -> Option<Self> {
228        match s.to_ascii_uppercase().as_str() {
229            "BEGIN_FILE" => Some(Self::BeginFile),
230            "BEFORE_DECLARATION" => Some(Self::BeforeDeclaration),
231            "BEGIN_DECLARATION" => Some(Self::BeginDeclaration),
232            "END_DECLARATION" => Some(Self::EndDeclaration),
233            "AFTER_DECLARATION" => Some(Self::AfterDeclaration),
234            "END_FILE" => Some(Self::EndFile),
235            _ => None,
236        }
237    }
238}
239
240/// Walks `definitions` recursively and collects the rendered text
241/// of all `@verbatim(placement=<file-kind>, language="ts"|"*")`
242/// annotations whose kind matches `target`. Returns a single
243/// concatenated string with embedded newlines.
244///
245/// zerodds-lint: recursion-depth 32 (IDL-Module-Nesting)
246fn collect_file_verbatim(definitions: &[Definition], target: VerbatimPlacement) -> String {
247    let mut out = String::new();
248    for def in definitions {
249        let anns = annotations_of_definition(def);
250        for (placement, text) in extract_verbatim(anns) {
251            if placement == target {
252                out.push_str(&text);
253                if !text.ends_with('\n') {
254                    out.push('\n');
255                }
256            }
257        }
258        if let Definition::Module(m) = def {
259            out.push_str(&collect_file_verbatim(&m.definitions, target));
260        }
261    }
262    out
263}
264
265/// Whitelist of annotation names recognised by this PSM
266/// (DDS-TS 1.0 §7.12 + Ch9 verbatim). Names outside the list
267/// fire DDS-TS-W002 / DDS-TS-E004.
268const KNOWN_ANNOTATIONS: &[&str] = &[
269    // XTypes / structural
270    "id",
271    "key",
272    "optional",
273    "default",
274    "final",
275    "appendable",
276    "mutable",
277    "nested",
278    "topic",
279    "must_understand",
280    "unit",
281    "min",
282    "max",
283    "range",
284    "hashid",
285    "autoid",
286    "bit_bound",
287    "position",
288    "value",
289    "verbatim",
290    // CORBA-IDL extras the parser may surface
291    "shared",
292    "external",
293    "ami",
294    "service",
295    "oneway",
296    "amicallback",
297    // Documentation / non-semantic
298    "ignore_literal_names",
299    "data_representation",
300    "extensibility",
301];
302
303/// §7.12 / §D.1 — emits W002 (warn-and-drop) for every annotation
304/// not on `KNOWN_ANNOTATIONS`. Under `strict_annotations` the
305/// first such annotation produces a fatal `DDS-TS-E004`.
306///
307/// zerodds-lint: recursion-depth 32 (IDL-Module-Nesting)
308fn scan_unknown_annotations(
309    definitions: &[Definition],
310    diagnostics: &mut Vec<Diagnostic>,
311    config: &CodegenConfig,
312) -> Result<(), IdlTsError> {
313    fn walk_anns(
314        anns: &[zerodds_idl::ast::Annotation],
315        diagnostics: &mut Vec<Diagnostic>,
316        config: &CodegenConfig,
317    ) -> Result<(), IdlTsError> {
318        for a in anns {
319            if a.name.parts.len() != 1 {
320                continue;
321            }
322            let name = a.name.parts[0].text.as_str();
323            if KNOWN_ANNOTATIONS.contains(&name) {
324                continue;
325            }
326            if config.strict_annotations {
327                let msg = alloc::format!(
328                    "DDS-TS-E004: unrecognised annotation `@{name}` \
329                     under --strict-annotations"
330                );
331                diagnostics.push(Diagnostic {
332                    code: "DDS-TS-E004",
333                    severity: Severity::Fatal,
334                    message: msg.clone(),
335                });
336                return Err(IdlTsError::Unsupported(msg));
337            }
338            diagnostics.push(Diagnostic {
339                code: "DDS-TS-W002",
340                severity: Severity::Warning,
341                message: alloc::format!("DDS-TS-W002: unrecognised annotation `@{name}` ignored"),
342            });
343        }
344        Ok(())
345    }
346
347    for def in definitions {
348        walk_anns(annotations_of_definition(def), diagnostics, config)?;
349        // Recurse into struct/union/enum/bitset/bitmask/exception/
350        // interface members for member-level annotations.
351        match def {
352            Definition::Type(td) => walk_type_decl_anns(td, diagnostics, config)?,
353            Definition::Except(e) => {
354                for m in &e.members {
355                    walk_anns(&m.annotations, diagnostics, config)?;
356                }
357            }
358            Definition::Interface(zerodds_idl::ast::InterfaceDcl::Def(i)) => {
359                for ex in &i.exports {
360                    use zerodds_idl::ast::Export;
361                    match ex {
362                        Export::Op(op) => {
363                            walk_anns(&op.annotations, diagnostics, config)?;
364                            for p in &op.params {
365                                walk_anns(&p.annotations, diagnostics, config)?;
366                            }
367                        }
368                        Export::Attr(attr) => {
369                            walk_anns(&attr.annotations, diagnostics, config)?;
370                        }
371                        _ => {}
372                    }
373                }
374            }
375            Definition::Module(m) => {
376                scan_unknown_annotations(&m.definitions, diagnostics, config)?;
377            }
378            _ => {}
379        }
380    }
381    Ok(())
382}
383
384fn walk_type_decl_anns(
385    td: &zerodds_idl::ast::TypeDecl,
386    diagnostics: &mut Vec<Diagnostic>,
387    config: &CodegenConfig,
388) -> Result<(), IdlTsError> {
389    use zerodds_idl::ast::{ConstrTypeDecl, StructDcl, TypeDecl, UnionDcl};
390    match td {
391        TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s))) => {
392            for m in &s.members {
393                check_member_anns(&m.annotations, diagnostics, config)?;
394            }
395        }
396        TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u))) => {
397            for case in &u.cases {
398                check_member_anns(&case.element.annotations, diagnostics, config)?;
399            }
400        }
401        TypeDecl::Constr(ConstrTypeDecl::Enum(e)) => {
402            for en in &e.enumerators {
403                check_member_anns(&en.annotations, diagnostics, config)?;
404            }
405        }
406        TypeDecl::Constr(ConstrTypeDecl::Bitset(b)) => {
407            for bf in &b.bitfields {
408                check_member_anns(&bf.annotations, diagnostics, config)?;
409            }
410        }
411        TypeDecl::Constr(ConstrTypeDecl::Bitmask(b)) => {
412            for v in &b.values {
413                check_member_anns(&v.annotations, diagnostics, config)?;
414            }
415        }
416        _ => {}
417    }
418    Ok(())
419}
420
421fn check_member_anns(
422    anns: &[zerodds_idl::ast::Annotation],
423    diagnostics: &mut Vec<Diagnostic>,
424    config: &CodegenConfig,
425) -> Result<(), IdlTsError> {
426    for a in anns {
427        if a.name.parts.len() != 1 {
428            continue;
429        }
430        let name = a.name.parts[0].text.as_str();
431        if KNOWN_ANNOTATIONS.contains(&name) {
432            continue;
433        }
434        if config.strict_annotations {
435            let msg = alloc::format!(
436                "DDS-TS-E004: unrecognised annotation `@{name}` \
437                 under --strict-annotations"
438            );
439            diagnostics.push(Diagnostic {
440                code: "DDS-TS-E004",
441                severity: Severity::Fatal,
442                message: msg.clone(),
443            });
444            return Err(IdlTsError::Unsupported(msg));
445        }
446        diagnostics.push(Diagnostic {
447            code: "DDS-TS-W002",
448            severity: Severity::Warning,
449            message: alloc::format!("DDS-TS-W002: unrecognised annotation `@{name}` ignored"),
450        });
451    }
452    Ok(())
453}
454
455/// §7.12 / §D.1 E003 — emits a fatal diagnostic for conflicting
456/// annotation combinations enumerated in §7.12 Annotation
457/// Resolution Order.
458///
459/// zerodds-lint: recursion-depth 32 (IDL-Module-Nesting)
460fn scan_annotation_conflicts(
461    definitions: &[Definition],
462    diagnostics: &mut Vec<Diagnostic>,
463) -> Result<(), IdlTsError> {
464    use zerodds_idl::ast::{ConstrTypeDecl, StructDcl, TypeDecl, UnionDcl};
465    for def in definitions {
466        // Type-level: at most one extensibility annotation.
467        let anns = annotations_of_definition(def);
468        let ext_count = ["final", "appendable", "mutable"]
469            .iter()
470            .filter(|n| has_annotation(anns, n))
471            .count();
472        if ext_count > 1 {
473            return fail_e003(
474                diagnostics,
475                "multiple extensibility annotations on a single type",
476            );
477        }
478
479        // Struct-level: duplicate @id(N) across members.
480        if let Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) = def {
481            if has_duplicate_member_id(&s.members) {
482                return fail_e003(
483                    diagnostics,
484                    "two members of a single struct share the same @id(N)",
485                );
486            }
487        }
488
489        // Union-level: duplicate case labels (parser usually catches
490        // this; we re-check for resilience).
491        if let Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u)))) = def {
492            if has_duplicate_case_labels(&u.cases) {
493                return fail_e003(
494                    diagnostics,
495                    "two cases of a single union share the same label",
496                );
497            }
498        }
499
500        // Bitmask-level: duplicate @position(P).
501        if let Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitmask(b))) = def {
502            let mut positions: Vec<i64> = Vec::new();
503            for v in &b.values {
504                if let Some(p) = annotation_int_value(&v.annotations, "position") {
505                    if positions.contains(&p) {
506                        return fail_e003(
507                            diagnostics,
508                            "two bit-values share the same explicit @position(P)",
509                        );
510                    }
511                    positions.push(p);
512                }
513            }
514        }
515
516        if let Definition::Module(m) = def {
517            scan_annotation_conflicts(&m.definitions, diagnostics)?;
518        }
519    }
520    Ok(())
521}
522
523fn fail_e003(diagnostics: &mut Vec<Diagnostic>, detail: &str) -> Result<(), IdlTsError> {
524    let msg = alloc::format!("DDS-TS-E003: {detail}");
525    diagnostics.push(Diagnostic {
526        code: "DDS-TS-E003",
527        severity: Severity::Fatal,
528        message: msg.clone(),
529    });
530    Err(IdlTsError::Unsupported(msg))
531}
532
533fn has_duplicate_member_id(members: &[zerodds_idl::ast::Member]) -> bool {
534    let mut ids: Vec<i64> = Vec::new();
535    for m in members {
536        if let Some(id) = annotation_int_value(&m.annotations, "id") {
537            if ids.contains(&id) {
538                return true;
539            }
540            ids.push(id);
541        }
542    }
543    false
544}
545
546fn has_duplicate_case_labels(cases: &[zerodds_idl::ast::Case]) -> bool {
547    use zerodds_idl::ast::CaseLabel;
548    let mut seen: Vec<i64> = Vec::new();
549    for case in cases {
550        for label in &case.labels {
551            if let CaseLabel::Value(expr) = label {
552                if let Some(n) = eval_const_int(expr) {
553                    if seen.contains(&n) {
554                        return true;
555                    }
556                    seen.push(n);
557                }
558            }
559        }
560    }
561    false
562}
563
564/// §11.7 / §D.1 E002 — verifies that every IDL `interface`-forward
565/// declaration has a matching complete declaration somewhere in
566/// the same compilation unit. Missing → fatal `IdlTsError::Unsupported`.
567fn check_forward_declaration_orphans(
568    definitions: &[Definition],
569    diagnostics: &mut Vec<Diagnostic>,
570) -> Result<(), IdlTsError> {
571    use zerodds_idl::ast::InterfaceDcl;
572    let mut complete: Vec<String> = Vec::new();
573    let mut forwards: Vec<String> = Vec::new();
574    walk_interface_decls(definitions, &mut complete, &mut forwards);
575
576    for f in &forwards {
577        if !complete.contains(f) {
578            let msg = alloc::format!(
579                "DDS-TS-E002: forward-declared interface `{f}` lacks a \
580                 matching complete declaration in this compilation unit"
581            );
582            diagnostics.push(Diagnostic {
583                code: "DDS-TS-E002",
584                severity: Severity::Fatal,
585                message: msg.clone(),
586            });
587            let _ = InterfaceDcl::Forward; // typecheck mark
588            return Err(IdlTsError::Unsupported(msg));
589        }
590    }
591    Ok(())
592}
593
594/// zerodds-lint: recursion-depth 32 (IDL-Module-Nesting)
595fn walk_interface_decls(
596    definitions: &[Definition],
597    complete: &mut Vec<String>,
598    forwards: &mut Vec<String>,
599) {
600    use zerodds_idl::ast::InterfaceDcl;
601    for def in definitions {
602        match def {
603            Definition::Interface(InterfaceDcl::Def(i)) => {
604                complete.push(i.name.text.clone());
605            }
606            Definition::Interface(InterfaceDcl::Forward(f)) => {
607                forwards.push(f.name.text.clone());
608            }
609            Definition::Module(m) => {
610                walk_interface_decls(&m.definitions, complete, forwards);
611            }
612            _ => {}
613        }
614    }
615}
616
617/// §D.1 I001 — emit one informational diagnostic per IDL field
618/// whose type is `long double`.
619///
620/// zerodds-lint: recursion-depth 32 (IDL-Module-Nesting)
621fn scan_long_double_uses(definitions: &[Definition], diagnostics: &mut Vec<Diagnostic>) {
622    use zerodds_idl::ast::{ConstrTypeDecl, StructDcl, TypeDecl, UnionDcl};
623    for def in definitions {
624        match def {
625            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) => {
626                for m in &s.members {
627                    if has_long_double(&m.type_spec) {
628                        diagnostics.push(Diagnostic {
629                            code: "DDS-TS-I001",
630                            severity: Severity::Info,
631                            message: alloc::format!(
632                                "DDS-TS-I001: `long double` in {}.{} mapped \
633                                 to opaque LongDouble carrier",
634                                s.name.text,
635                                m.declarators
636                                    .first()
637                                    .map(|d| d.name().text.as_str())
638                                    .unwrap_or("?")
639                            ),
640                        });
641                    }
642                }
643            }
644            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u)))) => {
645                for case in &u.cases {
646                    if has_long_double(&case.element.type_spec) {
647                        diagnostics.push(Diagnostic {
648                            code: "DDS-TS-I001",
649                            severity: Severity::Info,
650                            message: alloc::format!(
651                                "DDS-TS-I001: `long double` in union {}.{} \
652                                 mapped to opaque LongDouble carrier",
653                                u.name.text,
654                                case.element.declarator.name().text
655                            ),
656                        });
657                    }
658                }
659            }
660            Definition::Module(m) => scan_long_double_uses(&m.definitions, diagnostics),
661            _ => {}
662        }
663    }
664}
665
666fn has_long_double(t: &TypeSpec) -> bool {
667    matches!(
668        t,
669        TypeSpec::Primitive(PrimitiveType::Floating(FloatingType::LongDouble))
670    )
671}
672
673/// §D.1 W004 — emit one warning per union that has neither a
674/// `default` case nor exhaustive label coverage. The current
675/// implementation flags any non-enum-discriminator union without
676/// a `default` (since exhaustivity for integer/char/boolean
677/// discriminators cannot be checked at parse time without
678/// further static analysis).
679///
680/// zerodds-lint: recursion-depth 32 (IDL-Module-Nesting)
681fn scan_union_implicit_defaults(definitions: &[Definition], diagnostics: &mut Vec<Diagnostic>) {
682    use zerodds_idl::ast::{CaseLabel, ConstrTypeDecl, SwitchTypeSpec, TypeDecl, UnionDcl};
683    for def in definitions {
684        if let Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u)))) = def {
685            let has_default = u
686                .cases
687                .iter()
688                .any(|c| c.labels.iter().any(|l| matches!(l, CaseLabel::Default)));
689            if has_default {
690                continue;
691            }
692            // For integer/char/boolean discriminators a union without
693            // `default` is implicitly default-bearing at runtime.
694            if matches!(
695                u.switch_type,
696                SwitchTypeSpec::Integer(_)
697                    | SwitchTypeSpec::Octet
698                    | SwitchTypeSpec::Char
699                    | SwitchTypeSpec::Boolean
700            ) {
701                diagnostics.push(Diagnostic {
702                    code: "DDS-TS-W004",
703                    severity: Severity::Warning,
704                    message: alloc::format!(
705                        "DDS-TS-W004: union {} has no `default` case and \
706                         the discriminator is not exhaustively covered \
707                         by the listed labels",
708                        u.name.text
709                    ),
710                });
711            }
712        }
713        if let Definition::Module(m) = def {
714            scan_union_implicit_defaults(&m.definitions, diagnostics);
715        }
716    }
717}
718
719/// §D.1 W003 — emit one warning per `map<K, V>` whose key type is
720/// a struct or any reference (non-value-equality semantics in JS).
721///
722/// zerodds-lint: recursion-depth 32 (IDL-Module-Nesting)
723fn scan_map_key_hazards(definitions: &[Definition], diagnostics: &mut Vec<Diagnostic>) {
724    use zerodds_idl::ast::{ConstrTypeDecl, StructDcl, TypeDecl};
725    for def in definitions {
726        if let Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) = def {
727            for m in &s.members {
728                if let TypeSpec::Map(map) = &m.type_spec {
729                    if matches!(*map.key, TypeSpec::Scoped(_)) {
730                        let field = m
731                            .declarators
732                            .first()
733                            .map(|d| d.name().text.as_str())
734                            .unwrap_or("?");
735                        diagnostics.push(Diagnostic {
736                            code: "DDS-TS-W003",
737                            severity: Severity::Warning,
738                            message: alloc::format!(
739                                "DDS-TS-W003: map<K, V> in {}.{} uses a \
740                                 struct/ref key type with non-value-equality \
741                                 JavaScript semantics; use `equalKey` for lookup",
742                                s.name.text,
743                                field
744                            ),
745                        });
746                    }
747                }
748            }
749        }
750        if let Definition::Module(m) = def {
751            scan_map_key_hazards(&m.definitions, diagnostics);
752        }
753    }
754}
755
756/// Returns the annotation list of a top-level `Definition`, or
757/// an empty slice for variants that have none.
758fn annotations_of_definition(def: &Definition) -> &[zerodds_idl::ast::Annotation] {
759    use zerodds_idl::ast::{ConstrTypeDecl, InterfaceDcl, StructDcl, TypeDecl, UnionDcl};
760    match def {
761        Definition::Type(td) => match td {
762            TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s))) => &s.annotations,
763            TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u))) => &u.annotations,
764            TypeDecl::Constr(ConstrTypeDecl::Enum(e)) => &e.annotations,
765            TypeDecl::Constr(ConstrTypeDecl::Bitset(b)) => &b.annotations,
766            TypeDecl::Constr(ConstrTypeDecl::Bitmask(b)) => &b.annotations,
767            TypeDecl::Typedef(t) => &t.annotations,
768            _ => &[],
769        },
770        Definition::Const(c) => &c.annotations,
771        Definition::Except(e) => &e.annotations,
772        Definition::Interface(InterfaceDcl::Def(i)) => &i.annotations,
773        Definition::Module(m) => &m.annotations,
774        _ => &[],
775    }
776}
777
778/// Extracts `@verbatim(language=, placement=, text=)` annotations
779/// whose `language` is `"ts"` or `"*"`. Returns `(placement, text)`
780/// pairs in source-order. Annotations with unrecognised placement
781/// or non-matching language are silently skipped.
782fn extract_verbatim(
783    annotations: &[zerodds_idl::ast::Annotation],
784) -> Vec<(VerbatimPlacement, String)> {
785    use zerodds_idl::ast::{AnnotationParams, ConstExpr, LiteralKind};
786    let mut out: Vec<(VerbatimPlacement, String)> = Vec::new();
787    for a in annotations {
788        if !(a.name.parts.len() == 1 && a.name.parts[0].text == "verbatim") {
789            continue;
790        }
791        let AnnotationParams::Named(params) = &a.params else {
792            continue;
793        };
794        let mut language: Option<String> = None;
795        let mut placement_str: Option<String> = None;
796        let mut text: Option<String> = None;
797        for p in params {
798            let key = p.name.text.as_str();
799            if let ConstExpr::Literal(lit) = &p.value {
800                if matches!(lit.kind, LiteralKind::String | LiteralKind::WideString) {
801                    let raw = lit.raw.as_str();
802                    let trimmed = raw
803                        .strip_prefix('L')
804                        .unwrap_or(raw)
805                        .strip_prefix('"')
806                        .and_then(|s| s.strip_suffix('"'))
807                        .unwrap_or(raw);
808                    let unescaped = unescape_idl_string(trimmed);
809                    match key {
810                        "language" => language = Some(unescaped),
811                        "placement" => placement_str = Some(unescaped),
812                        "text" => text = Some(unescaped),
813                        _ => {}
814                    }
815                }
816            }
817        }
818        let lang = language.unwrap_or_else(|| "*".to_string());
819        if lang != "ts" && lang != "*" {
820            continue;
821        }
822        let Some(placement) = placement_str.and_then(|s| VerbatimPlacement::from_str(&s)) else {
823            continue;
824        };
825        let Some(t) = text else {
826            continue;
827        };
828        out.push((placement, t));
829    }
830    out
831}
832
833/// IDL string-escape-resolution for `@verbatim` text per §9.4.
834fn unescape_idl_string(s: &str) -> String {
835    let mut out = String::with_capacity(s.len());
836    let mut chars = s.chars();
837    while let Some(c) = chars.next() {
838        if c != '\\' {
839            out.push(c);
840            continue;
841        }
842        match chars.next() {
843            Some('n') => out.push('\n'),
844            Some('t') => out.push('\t'),
845            Some('r') => out.push('\r'),
846            Some('"') => out.push('"'),
847            Some('\\') => out.push('\\'),
848            Some('\'') => out.push('\''),
849            Some('0') => out.push('\0'),
850            Some(other) => {
851                out.push('\\');
852                out.push(other);
853            }
854            None => out.push('\\'),
855        }
856    }
857    out
858}
859
860/// Emits any `@verbatim(placement=<placement>)` blocks from
861/// `annotations` directly to `out`, separator-newline-terminated.
862fn emit_verbatim_at(
863    out: &mut String,
864    annotations: &[zerodds_idl::ast::Annotation],
865    placement: VerbatimPlacement,
866) {
867    for (p, text) in extract_verbatim(annotations) {
868        if p == placement {
869            out.push_str(&text);
870            if !text.ends_with('\n') {
871                out.push('\n');
872            }
873        }
874    }
875}
876
877/// zerodds-lint: recursion-depth 32
878#[allow(dead_code)] // Convenience wrapper without diagnostic-channel.
879fn emit_definition(out: &mut String, def: &Definition) -> Result<(), IdlTsError> {
880    let mut sink: Vec<Diagnostic> = Vec::new();
881    let empty_path: Vec<String> = Vec::new();
882    emit_definition_with_diagnostics(out, def, &mut sink, &empty_path)
883}
884
885/// zerodds-lint: recursion-depth 32 (IDL-Module-Nesting)
886fn emit_definition_with_diagnostics(
887    out: &mut String,
888    def: &Definition,
889    diagnostics: &mut Vec<Diagnostic>,
890    module_path: &[String],
891) -> Result<(), IdlTsError> {
892    use zerodds_idl::ast::InterfaceDcl;
893
894    let anns = annotations_of_definition(def);
895    // §9 — BEFORE_DECLARATION verbatim emitted before the declaration.
896    emit_verbatim_at(out, anns, VerbatimPlacement::BeforeDeclaration);
897
898    let res = match def {
899        Definition::Type(td) => emit_type_decl(out, td, module_path),
900        Definition::Const(c) => emit_const(out, c),
901        Definition::Except(e) => emit_exception(out, e),
902        Definition::Interface(InterfaceDcl::Def(i)) => emit_interface(out, i),
903        Definition::Interface(InterfaceDcl::Forward(f)) => {
904            // §11.7 — orphan forward declarations not in scope of a
905            // matching complete declaration. We cannot at this point
906            // distinguish orphan from upcoming complete decl in the
907            // same compilation unit; emit a no-op + advisory.
908            // (Full DDS-TS-E002 reject would require a two-pass scan.)
909            let _ = f;
910            Ok(())
911        }
912        Definition::Module(m) => {
913            // BEGIN_DECLARATION inside the namespace body.
914            out.push_str(&alloc::format!("export namespace {} {{\n", m.name.text));
915            emit_verbatim_at(out, &m.annotations, VerbatimPlacement::BeginDeclaration);
916            let mut next_path: Vec<String> = module_path.to_vec();
917            next_path.push(m.name.text.clone());
918            for inner in &m.definitions {
919                emit_definition_with_diagnostics(out, inner, diagnostics, &next_path)?;
920            }
921            emit_verbatim_at(out, &m.annotations, VerbatimPlacement::EndDeclaration);
922            out.push_str("}\n\n");
923            // §D.1 - emit info diagnostics for module-level long doubles
924            // is upper-level concern; collected per-type by the type
925            // walkers (currently none — long-double info is type-local).
926            let _ = diagnostics;
927            Ok(())
928        }
929        _ => Ok(()),
930    };
931
932    // §9 — AFTER_DECLARATION verbatim emitted after the declaration.
933    emit_verbatim_at(out, anns, VerbatimPlacement::AfterDeclaration);
934
935    res
936}
937
938/// Free-standing IDL `const` → TypeScript `export const NAME: T' = expr';`
939/// per DDS-TS 1.0 §7.2. Integer constants of 64-bit width emit
940/// the BigInt suffix.
941fn emit_const(out: &mut String, c: &zerodds_idl::ast::ConstDecl) -> Result<(), IdlTsError> {
942    use zerodds_idl::ast::{ConstExpr, ConstType, LiteralKind};
943
944    let (ts_ty, want_bigint) = match &c.type_ {
945        ConstType::Integer(i) => match i {
946            IntegerType::LongLong
947            | IntegerType::ULongLong
948            | IntegerType::Int64
949            | IntegerType::UInt64 => ("bigint", true),
950            _ => ("number", false),
951        },
952        ConstType::Floating(_) => ("number", false),
953        ConstType::Char => ("Char", false),
954        ConstType::WideChar => ("WChar", false),
955        ConstType::Boolean => ("boolean", false),
956        ConstType::Octet => ("number", false),
957        ConstType::String { .. } => ("string", false),
958        ConstType::Fixed => ("string", false),
959        ConstType::Scoped(_) => ("number", false),
960    };
961
962    let value_ts = const_expr_to_ts_value(&c.value, want_bigint).ok_or_else(|| {
963        IdlTsError::Unsupported(alloc::format!(
964            "const {} value is not a literal expression",
965            c.name.text
966        ))
967    })?;
968
969    let _ = ConstExpr::Literal; // pattern-of-use marker
970    let _ = LiteralKind::Boolean;
971
972    out.push_str(&alloc::format!(
973        "export const {}: {ts_ty} = {value_ts};\n\n",
974        c.name.text
975    ));
976    Ok(())
977}
978
979/// Renders a `ConstExpr` as a TypeScript literal expression.
980/// `want_bigint` selects BigInt-suffix form for integer values.
981fn const_expr_to_ts_value(e: &zerodds_idl::ast::ConstExpr, want_bigint: bool) -> Option<String> {
982    use zerodds_idl::ast::{ConstExpr, LiteralKind};
983    // Boolean literal must be matched before eval_const_int (which
984    // would coerce TRUE/FALSE to 1/0).
985    if let ConstExpr::Literal(lit) = e {
986        if matches!(lit.kind, LiteralKind::Boolean) {
987            return Some(match lit.raw.to_uppercase().as_str() {
988                "TRUE" | "1" => "true".into(),
989                _ => "false".into(),
990            });
991        }
992    }
993    if let Some(n) = eval_const_int(e) {
994        return Some(if want_bigint {
995            alloc::format!("{n}n")
996        } else {
997            alloc::format!("{n}")
998        });
999    }
1000    if let ConstExpr::Literal(lit) = e {
1001        return Some(match lit.kind {
1002            LiteralKind::Boolean => match lit.raw.to_lowercase().as_str() {
1003                "true" | "1" => "true".into(),
1004                _ => "false".into(),
1005            },
1006            LiteralKind::Floating => lit.raw.clone(),
1007            LiteralKind::String | LiteralKind::WideString => {
1008                let raw = lit.raw.as_str();
1009                let trimmed = raw
1010                    .strip_prefix('L')
1011                    .unwrap_or(raw)
1012                    .strip_prefix('"')
1013                    .and_then(|s| s.strip_suffix('"'))
1014                    .unwrap_or(raw);
1015                alloc::format!("\"{trimmed}\"")
1016            }
1017            LiteralKind::Char | LiteralKind::WideChar => {
1018                let raw = lit.raw.as_str();
1019                let trimmed = raw
1020                    .strip_prefix('L')
1021                    .unwrap_or(raw)
1022                    .strip_prefix('\'')
1023                    .and_then(|s| s.strip_suffix('\''))
1024                    .unwrap_or(raw);
1025                alloc::format!("\"{trimmed}\" as Char")
1026            }
1027            _ => lit.raw.clone(),
1028        });
1029    }
1030    None
1031}
1032
1033/// IDL `exception` → TypeScript `interface extends DdsException`
1034/// plus descriptor with `kind: "exception"` per DDS-TS 1.0 §7.4.2.
1035///
1036/// Structurally the same as `emit_struct` but the interface
1037/// extends `DdsException` (a marker with `__dds_exception: true`)
1038/// and the descriptor's `kind` is `"exception"`.
1039fn emit_exception(out: &mut String, e: &zerodds_idl::ast::ExceptDecl) -> Result<(), IdlTsError> {
1040    let name = &e.name.text;
1041
1042    // Interface body extends DdsException.
1043    out.push_str(&alloc::format!(
1044        "export interface {name} extends DdsException {{\n"
1045    ));
1046    for m in &e.members {
1047        let ts_ty = typespec_to_ts(&m.type_spec)?;
1048        let is_optional = has_annotation(&m.annotations, "optional");
1049        for d in &m.declarators {
1050            let suffix = if is_optional {
1051                alloc::format!("?: {ts_ty} | undefined")
1052            } else {
1053                alloc::format!(": {ts_ty}")
1054            };
1055            out.push_str(&alloc::format!("    {}{suffix};\n", d.name().text));
1056        }
1057    }
1058    out.push_str("}\n\n");
1059
1060    // Type-guard.
1061    out.push_str(&alloc::format!(
1062        "export function is{name}(v: unknown): v is {name} {{\n"
1063    ));
1064    out.push_str("    if (typeof v !== \"object\" || v === null) return false;\n");
1065    out.push_str("    const o = v as Record<string, unknown>;\n");
1066    out.push_str("    if (o.__dds_exception !== true) return false;\n");
1067    for m in &e.members {
1068        if has_annotation(&m.annotations, "optional") {
1069            continue;
1070        }
1071        if let Some(check) = typespec_typeof_check(&m.type_spec) {
1072            for d in &m.declarators {
1073                let field = d.name().text.clone();
1074                out.push_str(&alloc::format!(
1075                    "    if ({}) return false;\n",
1076                    check.replace("VAR", &alloc::format!("o.{field}"))
1077                ));
1078            }
1079        }
1080    }
1081    out.push_str("    return true;\n}\n\n");
1082
1083    // Descriptor.
1084    out.push_str(&alloc::format!(
1085        "export const {name}Type: DdsTypeDescriptor<{name}> = {{\n"
1086    ));
1087    out.push_str("    kind: \"exception\",\n");
1088    out.push_str(&alloc::format!("    name: \"{name}\",\n"));
1089    out.push_str("    extensibility: \"appendable\",\n");
1090    out.push_str("    nested: false,\n");
1091    out.push_str("    fields: [\n");
1092    let mut next_id: i64 = 0;
1093    for m in &e.members {
1094        let key_flag = has_annotation(&m.annotations, "key");
1095        let optional_flag = has_annotation(&m.annotations, "optional");
1096        let must_flag = has_annotation(&m.annotations, "must_understand");
1097        let id_override = annotation_int_value(&m.annotations, "id");
1098        let type_ref = typespec_to_typeref_literal(&m.type_spec);
1099        for d in &m.declarators {
1100            let id = id_override.unwrap_or(next_id);
1101            next_id = id + 1;
1102            out.push_str("        {\n");
1103            out.push_str(&alloc::format!(
1104                "            name: \"{}\",\n",
1105                d.name().text
1106            ));
1107            out.push_str(&alloc::format!("            id: {id},\n"));
1108            out.push_str(&alloc::format!("            type: {type_ref},\n"));
1109            out.push_str(&alloc::format!("            key: {key_flag},\n"));
1110            out.push_str(&alloc::format!("            optional: {optional_flag},\n"));
1111            out.push_str(&alloc::format!(
1112                "            mustUnderstand: {must_flag},\n"
1113            ));
1114            out.push_str("        },\n");
1115        }
1116    }
1117    out.push_str("    ],\n");
1118    out.push_str(&alloc::format!("    typeGuard: is{name},\n"));
1119    out.push_str("};\n");
1120    out.push_str(&alloc::format!("registerType({name}Type);\n\n"));
1121    Ok(())
1122}
1123
1124fn emit_type_decl(
1125    out: &mut String,
1126    td: &zerodds_idl::ast::TypeDecl,
1127    module_path: &[String],
1128) -> Result<(), IdlTsError> {
1129    use zerodds_idl::ast::{ConstrTypeDecl, StructDcl, TypeDecl, UnionDcl};
1130    match td {
1131        TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s))) => {
1132            emit_struct(out, s, module_path)
1133        }
1134        TypeDecl::Constr(ConstrTypeDecl::Enum(e)) => emit_enum(out, e),
1135        TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u))) => emit_union(out, u),
1136        TypeDecl::Constr(ConstrTypeDecl::Bitset(b)) => emit_bitset(out, b),
1137        TypeDecl::Constr(ConstrTypeDecl::Bitmask(b)) => emit_bitmask(out, b),
1138        TypeDecl::Typedef(t) => emit_typedef(out, t),
1139        _ => Ok(()),
1140    }
1141}
1142
1143/// IDL §7.4.7 Enum → TypeScript `as const` object + string-literal-union
1144/// type alias (DDS-TS 1.0 §7.6).
1145///
1146/// Example:
1147/// ```idl
1148/// enum Color { RED, GREEN, BLUE };
1149/// ```
1150/// produces:
1151/// ```ts
1152/// export const Color = {
1153///   RED:   "RED",
1154///   GREEN: "GREEN",
1155///   BLUE: "BLUE",
1156/// } as const;
1157/// export type Color = (typeof Color)[keyof typeof Color];
1158/// export const ColorOrdinal: Readonly<Record<Color, number>> = {
1159///   RED: 0, GREEN: 1, BLUE: 2,
1160/// } as const;
1161/// export const ColorFromOrdinal: ReadonlyMap<number, Color> =
1162///   new Map([[0, "RED"], [1, "GREEN"], [2, "BLUE"]]);
1163/// ```
1164///
1165/// `@value(N)` annotations override the implicit ordinal sequence.
1166/// The TypeScript `enum` keyword is intentionally not emitted: it
1167/// produces a nominal type that is not assignment-compatible with
1168/// plain string literals and behaves inconsistently under
1169/// `--isolatedModules`.
1170fn emit_enum(out: &mut String, e: &zerodds_idl::ast::EnumDef) -> Result<(), IdlTsError> {
1171    let name = &e.name.text;
1172
1173    // Emit the as-const object.
1174    out.push_str(&alloc::format!("export const {name} = {{\n"));
1175    for en in &e.enumerators {
1176        out.push_str(&alloc::format!("    {n}: \"{n}\",\n", n = en.name.text));
1177    }
1178    out.push_str("} as const;\n");
1179    out.push_str(&alloc::format!(
1180        "export type {name} = (typeof {name})[keyof typeof {name}];\n",
1181    ));
1182
1183    // Compute ordinals (with @value(N) override per member).
1184    let mut ordinals: Vec<(String, i64)> = Vec::new();
1185    let mut next: i64 = 0;
1186    for en in &e.enumerators {
1187        let val = annotation_int_value(&en.annotations, "value").unwrap_or(next);
1188        ordinals.push((en.name.text.clone(), val));
1189        next = val + 1;
1190    }
1191
1192    out.push_str(&alloc::format!(
1193        "export const {name}Ordinal: Readonly<Record<{name}, number>> = {{\n",
1194    ));
1195    for (member, ord) in &ordinals {
1196        out.push_str(&alloc::format!("    {member}: {ord},\n"));
1197    }
1198    out.push_str("} as const;\n");
1199
1200    out.push_str(&alloc::format!(
1201        "export const {name}FromOrdinal: ReadonlyMap<number, {name}> = new Map([\n",
1202    ));
1203    for (member, ord) in &ordinals {
1204        out.push_str(&alloc::format!("    [{ord}, \"{member}\"],\n"));
1205    }
1206    out.push_str("]);\n\n");
1207
1208    // Type-guard.
1209    out.push_str(&alloc::format!(
1210        "export function is{name}(v: unknown): v is {name} {{\n"
1211    ));
1212    out.push_str("    if (typeof v !== \"string\") return false;\n");
1213    out.push_str(&alloc::format!(
1214        "    return Object.prototype.hasOwnProperty.call({name}, v);\n"
1215    ));
1216    out.push_str("}\n\n");
1217
1218    // Descriptor.
1219    let bit_bound = annotation_int_value(&e.annotations, "bit_bound").unwrap_or(32);
1220    out.push_str(&alloc::format!(
1221        "export const {name}Type: DdsTypeDescriptor<{name}> = {{\n"
1222    ));
1223    out.push_str("    kind: \"enum\",\n");
1224    out.push_str(&alloc::format!("    name: \"{name}\",\n"));
1225    out.push_str("    extensibility: \"appendable\",\n");
1226    out.push_str("    nested: false,\n");
1227    out.push_str(&alloc::format!("    bitBound: {bit_bound},\n"));
1228    out.push_str("    fields: [\n");
1229    for (i, (member, ord)) in ordinals.iter().enumerate() {
1230        out.push_str("        {\n");
1231        out.push_str(&alloc::format!("            name: \"{member}\",\n"));
1232        out.push_str(&alloc::format!("            id: {i},\n"));
1233        out.push_str("            type: { kind: \"primitive\", name: \"int32\" },\n");
1234        out.push_str("            key: false,\n");
1235        out.push_str("            optional: false,\n");
1236        out.push_str("            mustUnderstand: false,\n");
1237        out.push_str(&alloc::format!("            default: {ord},\n"));
1238        out.push_str("        },\n");
1239    }
1240    out.push_str("    ],\n");
1241    out.push_str(&alloc::format!("    typeGuard: is{name},\n"));
1242    out.push_str("};\n");
1243    out.push_str(&alloc::format!("registerType({name}Type);\n\n"));
1244
1245    Ok(())
1246}
1247
1248/// Returns the integer value of the annotation `@<name>(N)` if
1249/// present in `annotations`, else `None`.
1250fn annotation_int_value(annotations: &[zerodds_idl::ast::Annotation], name: &str) -> Option<i64> {
1251    use zerodds_idl::ast::AnnotationParams;
1252    for a in annotations {
1253        if a.name.parts.len() == 1 && a.name.parts[0].text == name {
1254            if let AnnotationParams::Single(expr) = &a.params {
1255                return eval_const_int(expr);
1256            }
1257        }
1258    }
1259    None
1260}
1261
1262/// Returns true iff the annotation `@<name>` (any form) is present.
1263fn has_annotation(annotations: &[zerodds_idl::ast::Annotation], name: &str) -> bool {
1264    annotations
1265        .iter()
1266        .any(|a| a.name.parts.len() == 1 && a.name.parts[0].text == name)
1267}
1268
1269/// Returns the string parameter of `@<name>("...")` if present,
1270/// stripped of surrounding quotes.
1271fn annotation_string_value(
1272    annotations: &[zerodds_idl::ast::Annotation],
1273    name: &str,
1274) -> Option<String> {
1275    use zerodds_idl::ast::{AnnotationParams, ConstExpr, LiteralKind};
1276    for a in annotations {
1277        if a.name.parts.len() == 1 && a.name.parts[0].text == name {
1278            if let AnnotationParams::Single(ConstExpr::Literal(lit)) = &a.params {
1279                if matches!(lit.kind, LiteralKind::String | LiteralKind::WideString) {
1280                    let raw = lit.raw.as_str();
1281                    let trimmed = raw
1282                        .strip_prefix('L')
1283                        .unwrap_or(raw)
1284                        .strip_prefix('"')
1285                        .and_then(|s| s.strip_suffix('"'))
1286                        .unwrap_or(raw);
1287                    return Some(alloc::string::ToString::to_string(trimmed));
1288                }
1289            }
1290        }
1291    }
1292    None
1293}
1294
1295/// Returns the effective extensibility of a struct/union/enum.
1296/// Defaults to `appendable` per DDS-TS 1.0 §7.13 if unset.
1297fn struct_extensibility(annotations: &[zerodds_idl::ast::Annotation]) -> &'static str {
1298    if has_annotation(annotations, "final") {
1299        "final"
1300    } else if has_annotation(annotations, "mutable") {
1301        "mutable"
1302    } else {
1303        "appendable"
1304    }
1305}
1306
1307// ============================================================
1308// Struct mapping (DDS-TS 1.0 §7.4)
1309// ============================================================
1310
1311/// Emits IDL `struct` as TypeScript:
1312///   1. `export interface <Name> { ... }` (uniform interface form)
1313///   2. `<Field>_BOUND` / `_LENGTH` constants for bounded strings,
1314///      bounded sequences, and fixed-size arrays
1315///   3. `is<Name>(v): v is <Name>` type-guard function
1316///   4. `<Name>Type: DdsTypeDescriptor<<Name>>` side-table descriptor
1317///   5. `registerType(<Name>Type);` registry call
1318///
1319/// No TypeScript class is emitted under any annotation (§7.4.1).
1320fn emit_struct(
1321    out: &mut String,
1322    s: &zerodds_idl::ast::StructDef,
1323    module_path: &[String],
1324) -> Result<(), IdlTsError> {
1325    let name = &s.name.text;
1326
1327    // (1) interface body.
1328    out.push_str(&alloc::format!("export interface {name} "));
1329    if let Some(base) = &s.base {
1330        let base_path = base
1331            .parts
1332            .iter()
1333            .map(|p| p.text.clone())
1334            .collect::<Vec<_>>()
1335            .join(".");
1336        out.push_str(&alloc::format!("extends {base_path} "));
1337    }
1338    out.push_str("{\n");
1339    emit_verbatim_at(out, &s.annotations, VerbatimPlacement::BeginDeclaration);
1340    for m in &s.members {
1341        let base_ts = typespec_to_ts(&m.type_spec)?;
1342        let is_optional = has_annotation(&m.annotations, "optional");
1343        let tsdoc = render_tsdoc_for_member(&m.annotations);
1344        for d in &m.declarators {
1345            if let Some(t) = &tsdoc {
1346                out.push_str(t);
1347            }
1348            let ts_ty = wrap_with_array_dimensions(&base_ts, d);
1349            let suffix = if is_optional {
1350                alloc::format!("?: {ts_ty} | undefined")
1351            } else {
1352                alloc::format!(": {ts_ty}")
1353            };
1354            out.push_str(&alloc::format!("    {}{suffix};\n", d.name().text));
1355        }
1356    }
1357    emit_verbatim_at(out, &s.annotations, VerbatimPlacement::EndDeclaration);
1358    out.push_str("}\n\n");
1359
1360    // (2) BOUND / LENGTH / DEFAULT constants for the struct's members.
1361    emit_struct_bound_constants(out, s)?;
1362    emit_struct_default_constants(out, s)?;
1363
1364    // (3) type-guard.
1365    emit_struct_type_guard(out, s)?;
1366
1367    // (4) descriptor.
1368    emit_struct_descriptor(out, s)?;
1369
1370    // (5) registry call.
1371    out.push_str(&alloc::format!("registerType({name}Type);\n\n"));
1372
1373    // (6) zerodds-xcdr2-ts-1.0 §3 + §4 — TypeSupport-Const fuer XCDR2.
1374    emit_struct_typesupport(out, s, module_path)?;
1375    Ok(())
1376}
1377
1378/// Emits bounded-string `<Type>_<Field>_BOUND`, bounded-sequence
1379/// `<Type>_<Field>_BOUND`, and fixed-array `<Type>_<Field>_LENGTH`
1380/// constants for each struct member that warrants them.
1381fn emit_struct_bound_constants(
1382    out: &mut String,
1383    s: &zerodds_idl::ast::StructDef,
1384) -> Result<(), IdlTsError> {
1385    use zerodds_idl::ast::{Declarator, TypeSpec};
1386    let type_name = &s.name.text;
1387    let mut emitted = false;
1388    for m in &s.members {
1389        // Bounded string: type_spec is String { wide, bound: Some(n) }.
1390        if let TypeSpec::String(StringType { bound: Some(n), .. }) = &m.type_spec {
1391            if let Some(width) = eval_const_int(n) {
1392                for d in &m.declarators {
1393                    out.push_str(&alloc::format!(
1394                        "export const {type_name}_{}_BOUND = {width};\n",
1395                        d.name().text
1396                    ));
1397                    emitted = true;
1398                }
1399            }
1400        }
1401        // Bounded sequence: Sequence { bound: Some(n), .. }.
1402        if let TypeSpec::Sequence(seq) = &m.type_spec {
1403            if let Some(bound) = &seq.bound {
1404                if let Some(width) = eval_const_int(bound) {
1405                    for d in &m.declarators {
1406                        out.push_str(&alloc::format!(
1407                            "export const {type_name}_{}_BOUND = {width};\n",
1408                            d.name().text
1409                        ));
1410                        emitted = true;
1411                    }
1412                }
1413            }
1414        }
1415        // Fixed-size array: declarator carries the size.
1416        for d in &m.declarators {
1417            if let Declarator::Array(a) = d {
1418                if a.sizes.len() == 1 {
1419                    if let Some(len) = eval_const_int(&a.sizes[0]) {
1420                        out.push_str(&alloc::format!(
1421                            "export const {type_name}_{}_LENGTH = {len};\n",
1422                            a.name.text
1423                        ));
1424                        emitted = true;
1425                    }
1426                } else {
1427                    for (i, sz) in a.sizes.iter().enumerate() {
1428                        if let Some(len) = eval_const_int(sz) {
1429                            out.push_str(&alloc::format!(
1430                                "export const {type_name}_{}_LENGTH_DIM{} = {len};\n",
1431                                a.name.text,
1432                                i + 1
1433                            ));
1434                            emitted = true;
1435                        }
1436                    }
1437                }
1438            }
1439        }
1440    }
1441    if emitted {
1442        out.push('\n');
1443    }
1444    Ok(())
1445}
1446
1447/// Emits a type-guard `is<Name>(v): v is <Name>` that verifies
1448/// presence and JS-typeof of every required (non-optional) member.
1449/// Optional members are not required by the guard. Bounds and
1450/// units (TSDoc-only) are not validated.
1451fn emit_struct_type_guard(
1452    out: &mut String,
1453    s: &zerodds_idl::ast::StructDef,
1454) -> Result<(), IdlTsError> {
1455    let name = &s.name.text;
1456    out.push_str(&alloc::format!(
1457        "export function is{name}(v: unknown): v is {name} {{\n"
1458    ));
1459    out.push_str("    if (typeof v !== \"object\" || v === null) return false;\n");
1460    out.push_str("    const o = v as Record<string, unknown>;\n");
1461    for m in &s.members {
1462        if has_annotation(&m.annotations, "optional") {
1463            continue;
1464        }
1465        let typeof_check = typespec_typeof_check(&m.type_spec);
1466        for d in &m.declarators {
1467            let field = d.name().text.clone();
1468            if let Some(check) = &typeof_check {
1469                out.push_str(&alloc::format!(
1470                    "    if ({check_expr}) return false;\n",
1471                    check_expr = check.replace("VAR", &alloc::format!("o.{field}"))
1472                ));
1473            }
1474        }
1475    }
1476    out.push_str("    return true;\n}\n\n");
1477    Ok(())
1478}
1479
1480/// Returns a TypeScript boolean expression with placeholder VAR
1481/// that evaluates to `true` if the value at VAR does NOT match the
1482/// type. Returns `None` for types whose JS-typeof check is too
1483/// expensive or imprecise (e.g.\ struct refs, unions, sequences).
1484fn typespec_typeof_check(t: &TypeSpec) -> Option<String> {
1485    Some(match t {
1486        TypeSpec::Primitive(p) => match p {
1487            PrimitiveType::Boolean => "typeof VAR !== \"boolean\"".into(),
1488            PrimitiveType::Char | PrimitiveType::WideChar => "typeof VAR !== \"string\"".into(),
1489            PrimitiveType::Octet => "typeof VAR !== \"number\"".into(),
1490            PrimitiveType::Integer(i) => match i {
1491                IntegerType::LongLong
1492                | IntegerType::ULongLong
1493                | IntegerType::Int64
1494                | IntegerType::UInt64 => "typeof VAR !== \"bigint\"".into(),
1495                _ => "typeof VAR !== \"number\"".into(),
1496            },
1497            PrimitiveType::Floating(f) => match f {
1498                FloatingType::Float | FloatingType::Double => "typeof VAR !== \"number\"".into(),
1499                // LongDouble is an opaque object carrier.
1500                FloatingType::LongDouble => "typeof VAR !== \"object\" || VAR === null".into(),
1501            },
1502        },
1503        TypeSpec::String(_) => "typeof VAR !== \"string\"".into(),
1504        // For aggregate / opaque types we accept any object; deeper
1505        // structural validation is delegated to nested type-guards.
1506        TypeSpec::Sequence(_)
1507        | TypeSpec::Map(_)
1508        | TypeSpec::Scoped(_)
1509        | TypeSpec::Any
1510        | TypeSpec::Fixed(_) => return None,
1511    })
1512}
1513
1514/// Emits a `<Name>Type: DdsTypeDescriptor<<Name>>` side-table
1515/// constant carrying the struct's metadata.
1516fn emit_struct_descriptor(
1517    out: &mut String,
1518    s: &zerodds_idl::ast::StructDef,
1519) -> Result<(), IdlTsError> {
1520    let name = &s.name.text;
1521    let extensibility = struct_extensibility(&s.annotations);
1522    let nested = has_annotation(&s.annotations, "nested");
1523    let topic = annotation_string_value(&s.annotations, "topic");
1524    let autoid = annotation_string_value(&s.annotations, "autoid");
1525
1526    out.push_str(&alloc::format!(
1527        "export const {name}Type: DdsTypeDescriptor<{name}> = {{\n"
1528    ));
1529    out.push_str("    kind: \"struct\",\n");
1530    out.push_str(&alloc::format!("    name: \"{name}\",\n"));
1531    out.push_str(&alloc::format!("    extensibility: \"{extensibility}\",\n"));
1532    out.push_str(&alloc::format!("    nested: {nested},\n"));
1533    if let Some(t) = &topic {
1534        out.push_str(&alloc::format!("    topic: \"{t}\",\n"));
1535    }
1536    if let Some(a) = &autoid {
1537        let lower = a.to_ascii_lowercase();
1538        if lower == "sequential" || lower == "hash" {
1539            out.push_str(&alloc::format!("    autoid: \"{lower}\",\n"));
1540        }
1541    }
1542    out.push_str("    fields: [\n");
1543
1544    let mut next_id: i64 = 0;
1545    for m in &s.members {
1546        let key_flag = has_annotation(&m.annotations, "key");
1547        let optional_flag = has_annotation(&m.annotations, "optional");
1548        let must_flag = has_annotation(&m.annotations, "must_understand");
1549        let unit = annotation_string_value(&m.annotations, "unit");
1550        let id_override = annotation_int_value(&m.annotations, "id");
1551        let default_lit = annotation_default_to_ts(&m.annotations);
1552        let min_lit = annotation_const_text(&m.annotations, "min");
1553        let max_lit = annotation_const_text(&m.annotations, "max");
1554        let hashid = annotation_string_value(&m.annotations, "hashid");
1555        let type_ref = typespec_to_typeref_literal(&m.type_spec);
1556        for d in &m.declarators {
1557            let id = id_override.unwrap_or(next_id);
1558            next_id = id + 1;
1559            out.push_str("        {\n");
1560            out.push_str(&alloc::format!(
1561                "            name: \"{}\",\n",
1562                d.name().text
1563            ));
1564            out.push_str(&alloc::format!("            id: {id},\n"));
1565            out.push_str(&alloc::format!("            type: {type_ref},\n"));
1566            out.push_str(&alloc::format!("            key: {key_flag},\n"));
1567            out.push_str(&alloc::format!("            optional: {optional_flag},\n"));
1568            out.push_str(&alloc::format!(
1569                "            mustUnderstand: {must_flag},\n"
1570            ));
1571            if let Some(u) = &unit {
1572                out.push_str(&alloc::format!("            unit: \"{u}\",\n"));
1573            }
1574            if let Some(d_lit) = &default_lit {
1575                out.push_str(&alloc::format!("            default: {d_lit},\n"));
1576            }
1577            if let Some(min) = &min_lit {
1578                let m_quoted = if is_numeric_literal_text(min) {
1579                    min.clone()
1580                } else {
1581                    alloc::format!("\"{min}\"")
1582                };
1583                out.push_str(&alloc::format!("            min: {m_quoted},\n"));
1584            }
1585            if let Some(max) = &max_lit {
1586                let m_quoted = if is_numeric_literal_text(max) {
1587                    max.clone()
1588                } else {
1589                    alloc::format!("\"{max}\"")
1590                };
1591                out.push_str(&alloc::format!("            max: {m_quoted},\n"));
1592            }
1593            if let Some(h) = &hashid {
1594                out.push_str(&alloc::format!("            hashid: \"{h}\",\n"));
1595            }
1596            // §7.9.1 W003-flag for non-value-equality map keys.
1597            if let TypeSpec::Map(map) = &m.type_spec {
1598                if matches!(*map.key, TypeSpec::Scoped(_)) {
1599                    out.push_str("            keyEqualityHazard: true,\n");
1600                }
1601            }
1602            out.push_str("        },\n");
1603        }
1604    }
1605    out.push_str("    ],\n");
1606    out.push_str(&alloc::format!("    typeGuard: is{name},\n"));
1607    out.push_str("};\n");
1608    Ok(())
1609}
1610
1611// ============================================================
1612// XCDR2 TypeSupport emission (zerodds-xcdr2-ts-1.0 §3 + §4)
1613// ============================================================
1614
1615/// Emits `<Name>TypeSupport: DdsTopicType<<Name>>` const fuer einen
1616/// IDL-`struct`. Implementiert encode/decode/keyHash gegen
1617/// `Xcdr2Writer`/`Xcdr2Reader` aus `@zerodds/cdr`.
1618fn emit_struct_typesupport(
1619    out: &mut String,
1620    s: &zerodds_idl::ast::StructDef,
1621    module_path: &[String],
1622) -> Result<(), IdlTsError> {
1623    let name = &s.name.text;
1624    let mut full = module_path.to_vec();
1625    full.push(name.clone());
1626    let type_name = full.join("::");
1627
1628    let extensibility = struct_extensibility(&s.annotations);
1629    let has_key = struct_has_any_key(s);
1630
1631    out.push_str(&alloc::format!(
1632        "export const {name}TypeSupport: DdsTopicType<{name}> = {{\n"
1633    ));
1634    out.push_str(&alloc::format!("    typeName: \"{type_name}\",\n"));
1635    out.push_str(&alloc::format!("    isKeyed: {has_key},\n"));
1636    out.push_str(&alloc::format!("    extensibility: \"{extensibility}\",\n"));
1637
1638    // === encode ===
1639    out.push_str(&alloc::format!(
1640        "    encode(s: {name}, endian: EndianMode = \"le\"): Uint8Array {{\n"
1641    ));
1642    out.push_str("        const w = new Xcdr2Writer(endian);\n");
1643    emit_struct_encode_body(out, s, "        ")?;
1644    out.push_str("        return w.toBytes();\n");
1645    out.push_str("    },\n");
1646
1647    // === decode ===
1648    out.push_str(&alloc::format!(
1649        "    decode(bytes: Uint8Array, offset = 0, length: number = bytes.length - offset): {name} {{\n"
1650    ));
1651    out.push_str("        const r = new Xcdr2Reader(bytes, offset, length, \"le\");\n");
1652    emit_struct_decode_body(out, s, "        ")?;
1653    out.push_str("    },\n");
1654
1655    // === keyHash ===
1656    out.push_str(&alloc::format!("    keyHash(s: {name}): Uint8Array {{\n"));
1657    if has_key {
1658        // PlainCdr2BeKeyHolder: Big-Endian-Encode der @key-Felder (XTypes §7.6.8).
1659        out.push_str("        const w = new Xcdr2Writer(\"be\");\n");
1660        emit_struct_keyhash_body(out, s, "        ")?;
1661        // XTypes 1.3 §7.6.8.4: Holder ≤ 16 octets -> zero-pad; sonst MD5.
1662        out.push_str("        const __holder = w.toBytes();\n");
1663        out.push_str("        if (__holder.length <= 16) {\n");
1664        out.push_str("            const __h = new Uint8Array(16);\n");
1665        out.push_str("            __h.set(__holder);\n");
1666        out.push_str("            return __h;\n");
1667        out.push_str("        }\n");
1668        out.push_str("        return md5(__holder);\n");
1669    } else {
1670        out.push_str("        return new Uint8Array(16);\n");
1671    }
1672    out.push_str("    },\n");
1673    out.push_str("};\n\n");
1674
1675    let _ = s; // marker
1676    Ok(())
1677}
1678
1679fn struct_has_any_key(s: &zerodds_idl::ast::StructDef) -> bool {
1680    s.members
1681        .iter()
1682        .any(|m| has_annotation(&m.annotations, "key"))
1683}
1684
1685/// Erzeugt den encode-Body. Behandelt Final/Appendable/Mutable.
1686fn emit_struct_encode_body(
1687    out: &mut String,
1688    s: &zerodds_idl::ast::StructDef,
1689    indent: &str,
1690) -> Result<(), IdlTsError> {
1691    let extensibility = struct_extensibility(&s.annotations);
1692    match extensibility {
1693        "final" => {
1694            for m in &s.members {
1695                emit_member_encode(out, m, indent, "s.")?;
1696            }
1697        }
1698        "appendable" => {
1699            out.push_str(&alloc::format!(
1700                "{indent}const _tok = w.beginAppendable();\n"
1701            ));
1702            for m in &s.members {
1703                emit_member_encode(out, m, indent, "s.")?;
1704            }
1705            out.push_str(&alloc::format!("{indent}w.endAppendable(_tok);\n"));
1706        }
1707        "mutable" => {
1708            out.push_str(&alloc::format!("{indent}const _tok = w.beginMutable();\n"));
1709            let mut next_id: i64 = 0;
1710            for m in &s.members {
1711                let id_override = annotation_int_value(&m.annotations, "id");
1712                let must = has_annotation(&m.annotations, "must_understand");
1713                let optional = has_annotation(&m.annotations, "optional");
1714                for d in &m.declarators {
1715                    let id = id_override.unwrap_or(next_id);
1716                    next_id = id + 1;
1717                    let field = d.name().text.clone();
1718                    if optional {
1719                        out.push_str(&alloc::format!(
1720                            "{indent}if (s.{field} !== undefined && s.{field} !== null) {{\n"
1721                        ));
1722                    }
1723                    let inner_indent_owned = alloc::format!("{indent}    ");
1724                    let inner_indent: &str = if optional {
1725                        inner_indent_owned.as_str()
1726                    } else {
1727                        indent
1728                    };
1729                    emit_mutable_member_encode(
1730                        out,
1731                        &m.type_spec,
1732                        &field,
1733                        id as u32,
1734                        must,
1735                        inner_indent,
1736                    )?;
1737                    if optional {
1738                        out.push_str(&alloc::format!("{indent}}}\n"));
1739                    }
1740                }
1741            }
1742            out.push_str(&alloc::format!("{indent}w.endMutable(_tok);\n"));
1743        }
1744        // struct_extensibility() is exhaustive over Final/Appendable/Mutable;
1745        // unknown variants are treated as Final (no DHEADER wrap).
1746        _ => {}
1747    }
1748    Ok(())
1749}
1750
1751/// Erzeugt einen einzelnen Member-Encode-Aufruf (Final / Appendable).
1752fn emit_member_encode(
1753    out: &mut String,
1754    m: &zerodds_idl::ast::Member,
1755    indent: &str,
1756    prefix: &str,
1757) -> Result<(), IdlTsError> {
1758    let optional = has_annotation(&m.annotations, "optional");
1759    for d in &m.declarators {
1760        let field = d.name().text.clone();
1761        let target = alloc::format!("{prefix}{field}");
1762        if optional {
1763            // present-byte fuer Final/Appendable per §4.
1764            out.push_str(&alloc::format!(
1765                "{indent}if ({target} !== undefined && {target} !== null) {{\n"
1766            ));
1767            out.push_str(&alloc::format!("{indent}    w.writeOctet(1);\n"));
1768            emit_typespec_encode(out, &m.type_spec, &target, &format!("{indent}    "))?;
1769            out.push_str(&alloc::format!("{indent}}} else {{\n"));
1770            out.push_str(&alloc::format!("{indent}    w.writeOctet(0);\n"));
1771            out.push_str(&alloc::format!("{indent}}}\n"));
1772        } else {
1773            emit_typespec_encode(out, &m.type_spec, &target, indent)?;
1774        }
1775    }
1776    Ok(())
1777}
1778
1779/// Erzeugt einen Mutable-Member-Encode mit EMHEADER1.
1780///
1781/// Konvention zerodds-xcdr2-bindings-conformance-1.0 §6 V-10:
1782/// - Primitive Member: LC=0..3 inline (1/2/4/8 Byte).
1783/// - Variable-size Member (string, sequence, map, nested struct):
1784///   LC=3 mit folgendem NEXTINT = body-size (Bytes), Body in
1785///   normaler XCDR2-Form danach.
1786///
1787/// `patchUint32` schreibt den Body-Size-Wert in Stream-Endian
1788/// (LE in der Default-Konfiguration), passend zur NEXTINT-Lese-
1789/// reihenfolge im Reader.
1790fn emit_mutable_member_encode(
1791    out: &mut String,
1792    t: &TypeSpec,
1793    field: &str,
1794    id: u32,
1795    must: bool,
1796    indent: &str,
1797) -> Result<(), IdlTsError> {
1798    let mu_str = if must { "true" } else { "false" };
1799    if let Some(lc) = primitive_lc_inline(t) {
1800        out.push_str(&alloc::format!(
1801            "{indent}w.writeEmHeader({id}, {lc}, {mu_str});\n"
1802        ));
1803        emit_typespec_encode(out, t, &alloc::format!("s.{field}"), indent)?;
1804    } else {
1805        // LC=3 NEXTINT-Form: EMHEADER + Placeholder fuer body-size,
1806        // dann Body, dann body-size zurueckpatchen.
1807        // Per §6 V-10 ist LC=3 fuer non-primitive Member overloaded
1808        // mit nextInt = body-byte-count.
1809        out.push_str(&alloc::format!("{indent}{{\n"));
1810        out.push_str(&alloc::format!(
1811            "{indent}    w.writeEmHeader({id}, 3, {mu_str}, 0);\n"
1812        ));
1813        out.push_str(&alloc::format!("{indent}    const _bodyStart = w.pos;\n"));
1814        emit_typespec_encode(
1815            out,
1816            t,
1817            &alloc::format!("s.{field}"),
1818            &format!("{indent}    "),
1819        )?;
1820        out.push_str(&alloc::format!(
1821            "{indent}    w.patchUint32(_bodyStart - 4, w.pos - _bodyStart);\n"
1822        ));
1823        out.push_str(&alloc::format!("{indent}}}\n"));
1824    }
1825    Ok(())
1826}
1827
1828/// Liefert den LC-Wert fuer Primitives mit fester Inline-Groesse.
1829/// LC=0 -> 1B, LC=1 -> 2B, LC=2 -> 4B, LC=3 -> 8B. None sonst.
1830fn primitive_lc_inline(t: &TypeSpec) -> Option<u32> {
1831    match t {
1832        TypeSpec::Primitive(p) => match p {
1833            PrimitiveType::Boolean | PrimitiveType::Octet | PrimitiveType::Char => Some(0),
1834            PrimitiveType::WideChar => Some(1),
1835            PrimitiveType::Integer(i) => match i {
1836                IntegerType::Short
1837                | IntegerType::UShort
1838                | IntegerType::Int16
1839                | IntegerType::UInt16 => Some(1),
1840                IntegerType::Long
1841                | IntegerType::ULong
1842                | IntegerType::Int32
1843                | IntegerType::UInt32 => Some(2),
1844                IntegerType::LongLong
1845                | IntegerType::ULongLong
1846                | IntegerType::Int64
1847                | IntegerType::UInt64 => Some(3),
1848                IntegerType::Int8 | IntegerType::UInt8 => Some(0),
1849            },
1850            PrimitiveType::Floating(f) => match f {
1851                FloatingType::Float => Some(2),
1852                FloatingType::Double => Some(3),
1853                FloatingType::LongDouble => None,
1854            },
1855        },
1856        _ => None,
1857    }
1858}
1859/// zerodds-lint: recursion-depth 64 (emit_typespec_encode bounded by AST depth)
1860/// Erzeugt einen Encode-Aufruf fuer einen `TypeSpec` mit gegebener
1861/// Source-Expression (TypeScript-Side).
1862fn emit_typespec_encode(
1863    out: &mut String,
1864    t: &TypeSpec,
1865    expr: &str,
1866    indent: &str,
1867) -> Result<(), IdlTsError> {
1868    match t {
1869        TypeSpec::Primitive(p) => match p {
1870            PrimitiveType::Boolean => {
1871                out.push_str(&alloc::format!("{indent}w.writeBool({expr});\n"));
1872            }
1873            PrimitiveType::Octet => {
1874                out.push_str(&alloc::format!("{indent}w.writeOctet({expr});\n"));
1875            }
1876            PrimitiveType::Char => {
1877                out.push_str(&alloc::format!("{indent}w.writeChar({expr});\n"));
1878            }
1879            PrimitiveType::WideChar => {
1880                out.push_str(&alloc::format!("{indent}w.writeWChar({expr});\n"));
1881            }
1882            PrimitiveType::Integer(i) => {
1883                let m = match i {
1884                    IntegerType::Short | IntegerType::Int16 => "writeInt16",
1885                    IntegerType::UShort | IntegerType::UInt16 => "writeUint16",
1886                    IntegerType::Long | IntegerType::Int32 => "writeInt32",
1887                    IntegerType::ULong | IntegerType::UInt32 => "writeUint32",
1888                    IntegerType::LongLong | IntegerType::Int64 => "writeInt64",
1889                    IntegerType::ULongLong | IntegerType::UInt64 => "writeUint64",
1890                    IntegerType::Int8 => "writeInt8",
1891                    IntegerType::UInt8 => "writeUint8",
1892                };
1893                out.push_str(&alloc::format!("{indent}w.{m}({expr});\n"));
1894            }
1895            PrimitiveType::Floating(f) => {
1896                match f {
1897                    FloatingType::Float => {
1898                        out.push_str(&alloc::format!("{indent}w.writeFloat32({expr});\n"));
1899                    }
1900                    FloatingType::Double => {
1901                        out.push_str(&alloc::format!("{indent}w.writeFloat64({expr});\n"));
1902                    }
1903                    FloatingType::LongDouble => {
1904                        // 16-Byte opaker Carrier — wir schreiben einfach
1905                        // den `bytes` Member raw (LongDouble.bytes : Uint8Array).
1906                        out.push_str(&alloc::format!("{indent}w.writeBytes(({expr}).bytes);\n"));
1907                    }
1908                };
1909            }
1910        },
1911        TypeSpec::String(StringType { wide, .. }) => {
1912            if *wide {
1913                out.push_str(&alloc::format!("{indent}w.writeWString({expr});\n"));
1914            } else {
1915                out.push_str(&alloc::format!("{indent}w.writeString({expr});\n"));
1916            }
1917        }
1918        TypeSpec::Sequence(seq) => {
1919            out.push_str(&alloc::format!("{indent}w.writeUint32({expr}.length);\n"));
1920            out.push_str(&alloc::format!("{indent}for (const _e of {expr}) {{\n"));
1921            emit_typespec_encode(out, &seq.elem, "_e", &format!("{indent}    "))?;
1922            out.push_str(&alloc::format!("{indent}}}\n"));
1923        }
1924        TypeSpec::Map(map) => {
1925            // Map -> sequence of (key, value) Paare (count + entries).
1926            out.push_str(&alloc::format!("{indent}w.writeUint32({expr}.size);\n"));
1927            out.push_str(&alloc::format!(
1928                "{indent}for (const [_k, _v] of {expr}) {{\n"
1929            ));
1930            emit_typespec_encode(out, &map.key, "_k", &format!("{indent}    "))?;
1931            emit_typespec_encode(out, &map.value, "_v", &format!("{indent}    "))?;
1932            out.push_str(&alloc::format!("{indent}}}\n"));
1933        }
1934        TypeSpec::Scoped(_) => {
1935            // Nested struct/enum/typedef ref: by convention rufen wir
1936            // <Name>TypeSupport.encode auf wenn vorhanden, fallback ist
1937            // direkt das Sample-Object dumpen via JSON (unknown semantics).
1938            // Fuer jetzt: einfache Form — caller MUSS sicherstellen dass
1939            // ein TypeSupport-Const fuer den Type existiert. Wir emittieren
1940            // einen aufruf-stub ueber generische Form.
1941            // Hinweis: das ist die Anwendungs-Schicht — Codegen hat keinen
1942            // globalen Resolver fuer Type-Refs in dieser Implementierung.
1943            // Workaround: wir schreiben rekursiv fuer Enum-Werte als int32.
1944            out.push_str(&alloc::format!(
1945                "{indent}w.writeInt32({expr} as unknown as number);\n"
1946            ));
1947        }
1948        TypeSpec::Any => {
1949            // DDS-Any wird in XCDR2 als TypeIdentifier+Wert kodiert; nicht
1950            // im Codegen-Scope — emittieren wir einen runtime-Throw.
1951            out.push_str(&alloc::format!(
1952                "{indent}throw new Error(\"DDS-Any XCDR2 encode not implemented in codegen\");\n"
1953            ));
1954        }
1955        TypeSpec::Fixed(_) => {
1956            out.push_str(&alloc::format!(
1957                "{indent}throw new Error(\"fixed-point XCDR2 encode not implemented in codegen\");\n"
1958            ));
1959        }
1960    }
1961    Ok(())
1962}
1963
1964/// Erzeugt den decode-Body. Liefert ein Type-Object zurueck.
1965fn emit_struct_decode_body(
1966    out: &mut String,
1967    s: &zerodds_idl::ast::StructDef,
1968    indent: &str,
1969) -> Result<(), IdlTsError> {
1970    let extensibility = struct_extensibility(&s.annotations);
1971    let name = &s.name.text;
1972
1973    match extensibility {
1974        "final" => {
1975            // Sequenzielles Lesen.
1976            for m in &s.members {
1977                emit_member_decode_decl(out, m, indent)?;
1978            }
1979            emit_decode_return(out, s, indent, name)?;
1980        }
1981        "appendable" => {
1982            out.push_str(&alloc::format!(
1983                "{indent}const _tok = r.beginAppendable();\n"
1984            ));
1985            for m in &s.members {
1986                emit_member_decode_decl(out, m, indent)?;
1987            }
1988            out.push_str(&alloc::format!("{indent}r.endAppendable(_tok);\n"));
1989            emit_decode_return(out, s, indent, name)?;
1990        }
1991        "mutable" => {
1992            // Default-Initialisierung; Felder werden via EMHEADER-Loop gesetzt.
1993            for m in &s.members {
1994                let optional = has_annotation(&m.annotations, "optional");
1995                for d in &m.declarators {
1996                    let field = d.name().text.clone();
1997                    let init = if optional {
1998                        "undefined".into()
1999                    } else {
2000                        default_init_for(&m.type_spec)
2001                    };
2002                    let ts_ty = typespec_to_ts(&m.type_spec)?;
2003                    out.push_str(&alloc::format!(
2004                        "{indent}let _f_{field}: {ts_ty} | undefined = {init};\n"
2005                    ));
2006                }
2007            }
2008            out.push_str(&alloc::format!("{indent}const _tok = r.beginMutable();\n"));
2009            out.push_str(&alloc::format!("{indent}while (r.pos < _tok.bodyEnd) {{\n"));
2010            out.push_str(&alloc::format!(
2011                "{indent}    const _emh = r.readEmHeader();\n"
2012            ));
2013            out.push_str(&alloc::format!("{indent}    switch (_emh.memberId) {{\n"));
2014            let mut next_id: i64 = 0;
2015            for m in &s.members {
2016                let id_override = annotation_int_value(&m.annotations, "id");
2017                for d in &m.declarators {
2018                    let id = id_override.unwrap_or(next_id);
2019                    next_id = id + 1;
2020                    let field = d.name().text.clone();
2021                    out.push_str(&alloc::format!("{indent}        case {id}: {{\n"));
2022                    // EMHEADER+NEXTINT-Skip:
2023                    // - Primitives (LC=0..3 inline) brauchen nichts.
2024                    // - LC=3 fuer non-primitive Member traegt NEXTINT
2025                    //   nach dem EMHEADER (zerodds-xcdr2-bindings-
2026                    //   conformance-1.0 §6 V-10) — wir verwerfen ihn
2027                    //   weil read_typespec_expr direkt den Body liest.
2028                    // - LC>=4 wird bereits in readEmHeader konsumiert.
2029                    if primitive_lc_inline(&m.type_spec).is_none() {
2030                        out.push_str(&alloc::format!(
2031                            "{indent}            if (_emh.lc === 3) {{ r.readUint32(); }}\n"
2032                        ));
2033                    }
2034                    let ts_ty = typespec_to_ts(&m.type_spec)?;
2035                    out.push_str(&alloc::format!("{indent}            const _v: {ts_ty} = "));
2036                    let read_expr = read_typespec_expr(&m.type_spec)?;
2037                    out.push_str(&alloc::format!("{read_expr};\n"));
2038                    out.push_str(&alloc::format!("{indent}            _f_{field} = _v;\n"));
2039                    out.push_str(&alloc::format!("{indent}            break;\n"));
2040                    out.push_str(&alloc::format!("{indent}        }}\n"));
2041                }
2042            }
2043            out.push_str(&alloc::format!("{indent}        default: {{\n"));
2044            out.push_str(&alloc::format!(
2045                "{indent}            // Skip unknown member per XTypes \u{00A7}7.4.2.\n"
2046            ));
2047            out.push_str(&alloc::format!(
2048                "{indent}            if (_emh.nextInt !== null) {{ r.readBytes(_emh.nextInt); }}\n"
2049            ));
2050            out.push_str(&alloc::format!(
2051                "{indent}            else {{ const _sz = Xcdr2Reader.lcInlineSize(_emh.lc); if (_sz > 0) r.readBytes(_sz); }}\n"
2052            ));
2053            out.push_str(&alloc::format!("{indent}            break;\n"));
2054            out.push_str(&alloc::format!("{indent}        }}\n"));
2055            out.push_str(&alloc::format!("{indent}    }}\n"));
2056            out.push_str(&alloc::format!("{indent}}}\n"));
2057            out.push_str(&alloc::format!("{indent}r.endMutable(_tok);\n"));
2058            // Rueckgabe-Object aus _f_*-Variablen bauen.
2059            out.push_str(&alloc::format!("{indent}return {{\n"));
2060            for m in &s.members {
2061                let optional = has_annotation(&m.annotations, "optional");
2062                for d in &m.declarators {
2063                    let field = d.name().text.clone();
2064                    if optional {
2065                        out.push_str(&alloc::format!("{indent}    {field}: _f_{field},\n"));
2066                    } else {
2067                        out.push_str(&alloc::format!(
2068                            "{indent}    {field}: _f_{field} as {},\n",
2069                            typespec_to_ts(&m.type_spec)?
2070                        ));
2071                    }
2072                }
2073            }
2074            out.push_str(&alloc::format!("{indent}}};\n"));
2075        }
2076        // Decoder-Path: extensibility-Variants are exhaustive over
2077        // Final/Appendable/Mutable. Unknown -> graceful no-op.
2078        _ => {}
2079    }
2080    Ok(())
2081}
2082
2083/// Liefert den TS-Default-Initial-Wert fuer einen TypeSpec.
2084fn default_init_for(t: &TypeSpec) -> String {
2085    match t {
2086        TypeSpec::Primitive(p) => match p {
2087            PrimitiveType::Boolean => "false".into(),
2088            PrimitiveType::Integer(i) => match i {
2089                IntegerType::LongLong
2090                | IntegerType::ULongLong
2091                | IntegerType::Int64
2092                | IntegerType::UInt64 => "0n".into(),
2093                _ => "0".into(),
2094            },
2095            PrimitiveType::Octet
2096            | PrimitiveType::Floating(_)
2097            | PrimitiveType::Char
2098            | PrimitiveType::WideChar => "0 as unknown as undefined".into(),
2099        },
2100        TypeSpec::String(_) => "\"\"".into(),
2101        TypeSpec::Sequence(_) | TypeSpec::Map(_) => "[] as unknown as undefined".into(),
2102        _ => "undefined".into(),
2103    }
2104}
2105
2106/// Erzeugt eine `const _f_<name>: T = <readExpr>;`-Zeile fuer Final/Appendable.
2107fn emit_member_decode_decl(
2108    out: &mut String,
2109    m: &zerodds_idl::ast::Member,
2110    indent: &str,
2111) -> Result<(), IdlTsError> {
2112    let optional = has_annotation(&m.annotations, "optional");
2113    for d in &m.declarators {
2114        let field = d.name().text.clone();
2115        let ts_ty = typespec_to_ts(&m.type_spec)?;
2116        if optional {
2117            out.push_str(&alloc::format!(
2118                "{indent}const _present_{field} = r.readOctet();\n"
2119            ));
2120            out.push_str(&alloc::format!(
2121                "{indent}const _f_{field}: {ts_ty} | undefined = _present_{field} === 1 ? "
2122            ));
2123            let expr = read_typespec_expr(&m.type_spec)?;
2124            out.push_str(&alloc::format!("{expr} : undefined;\n"));
2125        } else {
2126            out.push_str(&alloc::format!("{indent}const _f_{field}: {ts_ty} = "));
2127            let expr = read_typespec_expr(&m.type_spec)?;
2128            out.push_str(&alloc::format!("{expr};\n"));
2129        }
2130    }
2131    Ok(())
2132}
2133
2134/// Erzeugt das `return { ... }`-Statement aus den `_f_*`-Variablen.
2135fn emit_decode_return(
2136    out: &mut String,
2137    s: &zerodds_idl::ast::StructDef,
2138    indent: &str,
2139    name: &str,
2140) -> Result<(), IdlTsError> {
2141    // Bei `extends Base` koennen Inherited-Fields nicht aus dem Decode-
2142    // Body gewonnen werden; wir casten das Object via `as <Name>` damit
2143    // tsc das akzeptiert. Down-Stream-Use kennt die fehlenden Felder.
2144    let needs_cast = s.base.is_some();
2145    if needs_cast {
2146        out.push_str(&alloc::format!("{indent}return ({{\n"));
2147    } else {
2148        out.push_str(&alloc::format!("{indent}return {{\n"));
2149    }
2150    for m in &s.members {
2151        for d in &m.declarators {
2152            let field = d.name().text.clone();
2153            out.push_str(&alloc::format!("{indent}    {field}: _f_{field},\n"));
2154        }
2155    }
2156    if needs_cast {
2157        out.push_str(&alloc::format!("{indent}}} as unknown as {name});\n"));
2158    } else {
2159        out.push_str(&alloc::format!("{indent}}};\n"));
2160    }
2161    Ok(())
2162}
2163/// zerodds-lint: recursion-depth 64 (read_typespec_expr bounded by AST depth)
2164/// Liefert den TS-Read-Expression-String fuer einen TypeSpec.
2165fn read_typespec_expr(t: &TypeSpec) -> Result<String, IdlTsError> {
2166    Ok(match t {
2167        TypeSpec::Primitive(p) => match p {
2168            PrimitiveType::Boolean => "r.readBool()".into(),
2169            PrimitiveType::Octet => "r.readOctet()".into(),
2170            PrimitiveType::Char => "r.readChar()".into(),
2171            PrimitiveType::WideChar => "r.readWChar()".into(),
2172            PrimitiveType::Integer(i) => {
2173                let m = match i {
2174                    IntegerType::Short | IntegerType::Int16 => "readInt16",
2175                    IntegerType::UShort | IntegerType::UInt16 => "readUint16",
2176                    IntegerType::Long | IntegerType::Int32 => "readInt32",
2177                    IntegerType::ULong | IntegerType::UInt32 => "readUint32",
2178                    IntegerType::LongLong | IntegerType::Int64 => "readInt64",
2179                    IntegerType::ULongLong | IntegerType::UInt64 => "readUint64",
2180                    IntegerType::Int8 => "readInt8",
2181                    IntegerType::UInt8 => "readUint8",
2182                };
2183                alloc::format!("r.{m}()")
2184            }
2185            PrimitiveType::Floating(f) => match f {
2186                FloatingType::Float => "r.readFloat32()".into(),
2187                FloatingType::Double => "r.readFloat64()".into(),
2188                FloatingType::LongDouble => {
2189                    "(makeLongDouble(r.readBytes(16)) as unknown as never)".into()
2190                }
2191            },
2192        },
2193        TypeSpec::String(StringType { wide, .. }) => {
2194            if *wide {
2195                "r.readWString()".into()
2196            } else {
2197                "r.readString()".into()
2198            }
2199        }
2200        TypeSpec::Sequence(seq) => {
2201            let elem_ts = typespec_to_ts(&seq.elem)?;
2202            let elem_read = read_typespec_expr(&seq.elem)?;
2203            alloc::format!(
2204                "((): Array<{elem_ts}> => {{ const _n = r.readUint32(); const _o: Array<{elem_ts}> = []; for (let _i = 0; _i < _n; _i++) {{ _o.push({elem_read}); }} return _o; }})()"
2205            )
2206        }
2207        TypeSpec::Map(map) => {
2208            let k_ts = typespec_to_ts(&map.key)?;
2209            let v_ts = typespec_to_ts(&map.value)?;
2210            let k_read = read_typespec_expr(&map.key)?;
2211            let v_read = read_typespec_expr(&map.value)?;
2212            alloc::format!(
2213                "((): ReadonlyMap<{k_ts}, {v_ts}> => {{ const _n = r.readUint32(); const _o = new Map<{k_ts}, {v_ts}>(); for (let _i = 0; _i < _n; _i++) {{ const _k = {k_read}; const _v = {v_read}; _o.set(_k, _v); }} return _o; }})()"
2214            )
2215        }
2216        TypeSpec::Scoped(_) => {
2217            // Scoped-Refs werden im Codegen-Layer derzeit nicht aufgeloest;
2218            // fuer Enum-Werte (int32-Repr) liefern wir int32-read.
2219            "r.readInt32() as unknown as never".into()
2220        }
2221        TypeSpec::Any => {
2222            "((): never => { throw new Error(\"DDS-Any XCDR2 decode not implemented\"); })()".into()
2223        }
2224        TypeSpec::Fixed(_) => {
2225            "((): never => { throw new Error(\"fixed-point XCDR2 decode not implemented\"); })()"
2226                .into()
2227        }
2228    })
2229}
2230
2231/// Schreibt nur die `@key`-Felder im Big-Endian-Modus
2232/// (PlainCdr2BeKeyHolder per XTypes §7.6.8).
2233fn emit_struct_keyhash_body(
2234    out: &mut String,
2235    s: &zerodds_idl::ast::StructDef,
2236    indent: &str,
2237) -> Result<(), IdlTsError> {
2238    for m in &s.members {
2239        if !has_annotation(&m.annotations, "key") {
2240            continue;
2241        }
2242        for d in &m.declarators {
2243            let field = d.name().text.clone();
2244            emit_typespec_encode(out, &m.type_spec, &alloc::format!("s.{field}"), indent)?;
2245        }
2246    }
2247    Ok(())
2248}
2249
2250/// Renders a `DdsTypeRef` literal as a TypeScript object expression
2251/// for a given IDL `TypeSpec`. Used inside descriptor emission.
2252///
2253/// zerodds-lint: recursion-depth 16 (Type-Composition-Tiefe)
2254fn typespec_to_typeref_literal(t: &TypeSpec) -> String {
2255    match t {
2256        TypeSpec::Primitive(p) => {
2257            let prim = primitive_to_typeref_name(p);
2258            alloc::format!("{{ kind: \"primitive\", name: \"{prim}\" }}")
2259        }
2260        TypeSpec::String(StringType { wide, bound, .. }) => match bound {
2261            Some(b) => match eval_const_int(b) {
2262                Some(n) => alloc::format!("{{ kind: \"string\", bound: {n}, wide: {wide} }}"),
2263                None => alloc::format!("{{ kind: \"string\", wide: {wide} }}"),
2264            },
2265            None => alloc::format!("{{ kind: \"string\", wide: {wide} }}"),
2266        },
2267        TypeSpec::Sequence(seq) => {
2268            let elem = typespec_to_typeref_literal(&seq.elem);
2269            match &seq.bound {
2270                Some(b) => match eval_const_int(b) {
2271                    Some(n) => {
2272                        alloc::format!("{{ kind: \"sequence\", element: {elem}, bound: {n} }}")
2273                    }
2274                    None => alloc::format!("{{ kind: \"sequence\", element: {elem} }}"),
2275                },
2276                None => alloc::format!("{{ kind: \"sequence\", element: {elem} }}"),
2277            }
2278        }
2279        TypeSpec::Map(m) => {
2280            let k = typespec_to_typeref_literal(&m.key);
2281            let v = typespec_to_typeref_literal(&m.value);
2282            match &m.bound {
2283                Some(b) => match eval_const_int(b) {
2284                    Some(n) => {
2285                        alloc::format!("{{ kind: \"map\", key: {k}, value: {v}, bound: {n} }}")
2286                    }
2287                    None => alloc::format!("{{ kind: \"map\", key: {k}, value: {v} }}"),
2288                },
2289                None => alloc::format!("{{ kind: \"map\", key: {k}, value: {v} }}"),
2290            }
2291        }
2292        TypeSpec::Scoped(s) => {
2293            let qname = s
2294                .parts
2295                .iter()
2296                .map(|p| p.text.clone())
2297                .collect::<Vec<_>>()
2298                .join("::");
2299            alloc::format!("{{ kind: \"ref\", name: \"{qname}\" }}")
2300        }
2301        TypeSpec::Any => "{ kind: \"any\" }".into(),
2302        TypeSpec::Fixed(_) => "{ kind: \"primitive\", name: \"int64\" }".into(),
2303    }
2304}
2305
2306fn primitive_to_typeref_name(p: &PrimitiveType) -> &'static str {
2307    match p {
2308        PrimitiveType::Boolean => "boolean",
2309        PrimitiveType::Char => "char",
2310        PrimitiveType::WideChar => "wchar",
2311        PrimitiveType::Octet => "octet",
2312        PrimitiveType::Integer(i) => match i {
2313            IntegerType::Short | IntegerType::Int16 => "int16",
2314            IntegerType::UShort | IntegerType::UInt16 => "uint16",
2315            IntegerType::Long | IntegerType::Int32 => "int32",
2316            IntegerType::ULong | IntegerType::UInt32 => "uint32",
2317            IntegerType::LongLong | IntegerType::Int64 => "int64",
2318            IntegerType::ULongLong | IntegerType::UInt64 => "uint64",
2319            IntegerType::Int8 => "int16",
2320            IntegerType::UInt8 => "uint16",
2321        },
2322        PrimitiveType::Floating(f) => match f {
2323            FloatingType::Float => "float",
2324            FloatingType::Double => "double",
2325            FloatingType::LongDouble => "longDouble",
2326        },
2327    }
2328}
2329
2330/// IDL §7.4.5 Union → TypeScript discriminated-union (algebraic data type).
2331///
2332/// Beispiel:
2333/// ```idl
2334/// union MyUnion switch (long) {
2335///     case 1: long a;
2336///     case 2: string b;
2337///     default: octet other;
2338/// };
2339/// ```
2340/// erzeugt:
2341/// ```ts
2342/// export type MyUnion =
2343///     | { discriminator: 1; a: number }
2344///     | { discriminator: 2; b: string }
2345///     | { discriminator: number; other: number };
2346/// ```
2347fn emit_union(out: &mut String, u: &zerodds_idl::ast::UnionDef) -> Result<(), IdlTsError> {
2348    use zerodds_idl::ast::{CaseLabel, SwitchTypeSpec};
2349
2350    let name = &u.name.text;
2351    let disc_broad = match &u.switch_type {
2352        SwitchTypeSpec::Integer(_) | SwitchTypeSpec::Octet => "number",
2353        SwitchTypeSpec::Char => "Char",
2354        SwitchTypeSpec::Boolean => "boolean",
2355        SwitchTypeSpec::Scoped(s) => {
2356            // For enum discriminators we emit the qualified name —
2357            // literal-narrowing is then strict (Exclude works).
2358            let qname = s
2359                .parts
2360                .iter()
2361                .map(|p| p.text.clone())
2362                .collect::<Vec<_>>()
2363                .join(".");
2364            return emit_union_with_enum_discriminator(out, u, &qname);
2365        }
2366    };
2367
2368    out.push_str(&alloc::format!("export type {name} =\n"));
2369    let mut first = true;
2370    let mut explicit_labels: Vec<String> = Vec::new();
2371    for case in &u.cases {
2372        for label in &case.labels {
2373            let prefix = if first { "    " } else { "    | " };
2374            first = false;
2375            let disc_str = match label {
2376                CaseLabel::Default => disc_broad.to_string(),
2377                CaseLabel::Value(expr) => match render_label_for(disc_broad, expr) {
2378                    Some(s) => {
2379                        explicit_labels.push(s.clone());
2380                        s
2381                    }
2382                    None => disc_broad.to_string(),
2383                },
2384            };
2385            let elem_ts = typespec_to_ts(&case.element.type_spec)?;
2386            let field_name = case.element.declarator.name().text.clone();
2387            out.push_str(&alloc::format!(
2388                "{prefix}{{ discriminator: {disc_str}; {field_name}: {elem_ts} }}\n"
2389            ));
2390        }
2391    }
2392    out.push_str(";\n\n");
2393
2394    emit_union_descriptor(out, u, disc_broad)?;
2395
2396    Ok(())
2397}
2398
2399/// Renders a case-label expression as a TS literal. For broad
2400/// disc types (`number`/`boolean`) the literal type-system narrowing
2401/// is partial (TS does not narrow `number` by `Exclude`-of-literals),
2402/// but the per-arm discriminator value is still emitted as the
2403/// concrete literal for runtime matching.
2404fn render_label_for(disc_broad: &str, expr: &zerodds_idl::ast::ConstExpr) -> Option<String> {
2405    use zerodds_idl::ast::{ConstExpr, LiteralKind};
2406    if let Some(n) = eval_const_int(expr) {
2407        return Some(if disc_broad == "boolean" {
2408            (n != 0).to_string()
2409        } else {
2410            alloc::format!("{n}")
2411        });
2412    }
2413    if let ConstExpr::Literal(lit) = expr {
2414        return Some(match lit.kind {
2415            LiteralKind::Boolean => match lit.raw.to_lowercase().as_str() {
2416                "true" | "1" => "true".into(),
2417                _ => "false".into(),
2418            },
2419            LiteralKind::Char | LiteralKind::WideChar => {
2420                let raw = lit.raw.as_str();
2421                let trimmed = raw
2422                    .strip_prefix('L')
2423                    .unwrap_or(raw)
2424                    .strip_prefix('\'')
2425                    .and_then(|s| s.strip_suffix('\''))
2426                    .unwrap_or(raw);
2427                alloc::format!("\"{trimmed}\"")
2428            }
2429            _ => lit.raw.clone(),
2430        });
2431    }
2432    None
2433}
2434
2435/// Special-case for enum-typed discriminators: literal narrowing
2436/// works because each label is a literal subtype of the enum
2437/// alias, and the default arm uses `Exclude<Enum, Listed>`.
2438fn emit_union_with_enum_discriminator(
2439    out: &mut String,
2440    u: &zerodds_idl::ast::UnionDef,
2441    enum_name: &str,
2442) -> Result<(), IdlTsError> {
2443    use zerodds_idl::ast::{CaseLabel, ConstExpr};
2444    let name = &u.name.text;
2445    out.push_str(&alloc::format!("export type {name} =\n"));
2446
2447    let mut explicit_labels: Vec<String> = Vec::new();
2448    let mut default_case: Option<&zerodds_idl::ast::Case> = None;
2449    let mut first = true;
2450
2451    for case in &u.cases {
2452        let mut emitted_label = false;
2453        for label in &case.labels {
2454            match label {
2455                CaseLabel::Default => {
2456                    default_case = Some(case);
2457                }
2458                CaseLabel::Value(ConstExpr::Scoped(s)) => {
2459                    let prefix = if first { "    " } else { "    | " };
2460                    first = false;
2461                    let qual = s
2462                        .parts
2463                        .iter()
2464                        .map(|p| p.text.clone())
2465                        .collect::<Vec<_>>()
2466                        .join(".");
2467                    let disc_str = if qual.contains('.') {
2468                        qual.clone()
2469                    } else {
2470                        alloc::format!("{enum_name}.{qual}")
2471                    };
2472                    explicit_labels.push(disc_str.clone());
2473                    let elem_ts = typespec_to_ts(&case.element.type_spec)?;
2474                    let field_name = case.element.declarator.name().text.clone();
2475                    out.push_str(&alloc::format!(
2476                        "{prefix}{{ discriminator: {disc_str}; {field_name}: {elem_ts} }}\n"
2477                    ));
2478                    emitted_label = true;
2479                }
2480                CaseLabel::Value(_) => {
2481                    // Non-scoped label on enum disc: degrade to broad.
2482                    let prefix = if first { "    " } else { "    | " };
2483                    first = false;
2484                    let elem_ts = typespec_to_ts(&case.element.type_spec)?;
2485                    let field_name = case.element.declarator.name().text.clone();
2486                    out.push_str(&alloc::format!(
2487                        "{prefix}{{ discriminator: {enum_name}; {field_name}: {elem_ts} }}\n"
2488                    ));
2489                    emitted_label = true;
2490                }
2491            }
2492        }
2493        let _ = emitted_label;
2494    }
2495    if let Some(case) = default_case {
2496        let prefix = if first { "    " } else { "    | " };
2497        let elem_ts = typespec_to_ts(&case.element.type_spec)?;
2498        let field_name = case.element.declarator.name().text.clone();
2499        let labels_union = if explicit_labels.is_empty() {
2500            "never".into()
2501        } else {
2502            explicit_labels.join(" | ")
2503        };
2504        out.push_str(&alloc::format!(
2505            "{prefix}{{ discriminator: Exclude<{enum_name}, {labels_union}>; {field_name}: {elem_ts} }}\n"
2506        ));
2507    }
2508    out.push_str(";\n\n");
2509
2510    emit_union_descriptor(out, u, enum_name)?;
2511    Ok(())
2512}
2513
2514/// Emits the `<Name>Type: DdsTypeDescriptor<<Name>>` side-table
2515/// for an IDL union (§7.5.1). The descriptor's fields list begins
2516/// with a synthetic discriminator descriptor at id `0xFFFFFFFF`,
2517/// followed by one descriptor per case-member with its `labels`.
2518fn emit_union_descriptor(
2519    out: &mut String,
2520    u: &zerodds_idl::ast::UnionDef,
2521    disc_broad: &str,
2522) -> Result<(), IdlTsError> {
2523    use zerodds_idl::ast::{CaseLabel, SwitchTypeSpec};
2524    let name = &u.name.text;
2525    let extensibility = struct_extensibility(&u.annotations);
2526    let disc_typeref = match &u.switch_type {
2527        SwitchTypeSpec::Integer(i) => alloc::format!(
2528            "{{ kind: \"primitive\", name: \"{}\" }}",
2529            primitive_to_typeref_name(&PrimitiveType::Integer(*i))
2530        ),
2531        SwitchTypeSpec::Octet => "{ kind: \"primitive\", name: \"octet\" }".into(),
2532        SwitchTypeSpec::Char => "{ kind: \"primitive\", name: \"char\" }".into(),
2533        SwitchTypeSpec::Boolean => "{ kind: \"primitive\", name: \"boolean\" }".into(),
2534        SwitchTypeSpec::Scoped(s) => {
2535            let qname = s
2536                .parts
2537                .iter()
2538                .map(|p| p.text.clone())
2539                .collect::<Vec<_>>()
2540                .join("::");
2541            alloc::format!("{{ kind: \"ref\", name: \"{qname}\" }}")
2542        }
2543    };
2544    let _ = disc_broad;
2545
2546    let mut has_default = false;
2547    let mut next_id: i64 = 0;
2548
2549    out.push_str(&alloc::format!(
2550        "export const {name}Type: DdsTypeDescriptor<{name}> = {{\n"
2551    ));
2552    out.push_str("    kind: \"union\",\n");
2553    out.push_str(&alloc::format!("    name: \"{name}\",\n"));
2554    out.push_str(&alloc::format!("    extensibility: \"{extensibility}\",\n"));
2555    out.push_str("    nested: false,\n");
2556    out.push_str("    fields: [\n");
2557    // Synthetic discriminator at reserved id 0xFFFFFFFF.
2558    out.push_str("        {\n");
2559    out.push_str("            name: \"discriminator\",\n");
2560    out.push_str("            id: 0xFFFFFFFF,\n");
2561    out.push_str(&alloc::format!("            type: {disc_typeref},\n"));
2562    out.push_str("            key: false,\n");
2563    out.push_str("            optional: false,\n");
2564    out.push_str("            mustUnderstand: false,\n");
2565    out.push_str("        },\n");
2566
2567    for case in &u.cases {
2568        let mut labels_lit: Vec<String> = Vec::new();
2569        let mut is_default = false;
2570        for label in &case.labels {
2571            match label {
2572                CaseLabel::Default => {
2573                    is_default = true;
2574                    has_default = true;
2575                }
2576                CaseLabel::Value(expr) => {
2577                    if let Some(s) = render_descriptor_label(expr) {
2578                        labels_lit.push(s);
2579                    }
2580                }
2581            }
2582        }
2583        let elem_ts_ref = typespec_to_typeref_literal(&case.element.type_spec);
2584        let id_override = annotation_int_value(&case.element.annotations, "id");
2585        let id = id_override.unwrap_or(next_id);
2586        next_id = id + 1;
2587        let field_name = case.element.declarator.name().text.clone();
2588        out.push_str("        {\n");
2589        out.push_str(&alloc::format!("            name: \"{field_name}\",\n"));
2590        out.push_str(&alloc::format!("            id: {id},\n"));
2591        out.push_str(&alloc::format!("            type: {elem_ts_ref},\n"));
2592        out.push_str("            key: false,\n");
2593        out.push_str("            optional: false,\n");
2594        out.push_str("            mustUnderstand: false,\n");
2595        if !is_default && !labels_lit.is_empty() {
2596            out.push_str(&alloc::format!(
2597                "            labels: [{}],\n",
2598                labels_lit.join(", ")
2599            ));
2600        }
2601        out.push_str("        },\n");
2602    }
2603    out.push_str("    ],\n");
2604    out.push_str(&alloc::format!("    hasDefault: {has_default},\n"));
2605    out.push_str(&alloc::format!("    typeGuard: is{name},\n"));
2606    out.push_str("};\n");
2607
2608    // Type-guard: structural check for {discriminator, ...} object.
2609    out.push_str(&alloc::format!(
2610        "export function is{name}(v: unknown): v is {name} {{\n"
2611    ));
2612    out.push_str("    if (typeof v !== \"object\" || v === null) return false;\n");
2613    out.push_str("    const o = v as Record<string, unknown>;\n");
2614    out.push_str("    return \"discriminator\" in o;\n");
2615    out.push_str("}\n\n");
2616
2617    out.push_str(&alloc::format!("registerType({name}Type);\n\n"));
2618    Ok(())
2619}
2620
2621/// Renders a case-label expression as a descriptor-literal value
2622/// (number / "string" / true|false). Used in `labels: [...]`.
2623fn render_descriptor_label(expr: &zerodds_idl::ast::ConstExpr) -> Option<String> {
2624    use zerodds_idl::ast::{ConstExpr, LiteralKind};
2625    if let Some(n) = eval_const_int(expr) {
2626        return Some(alloc::format!("{n}"));
2627    }
2628    if let ConstExpr::Literal(lit) = expr {
2629        return Some(match lit.kind {
2630            LiteralKind::Boolean => match lit.raw.to_lowercase().as_str() {
2631                "true" | "1" => "true".into(),
2632                _ => "false".into(),
2633            },
2634            LiteralKind::Char | LiteralKind::WideChar => {
2635                let raw = lit.raw.as_str();
2636                let trimmed = raw
2637                    .strip_prefix('L')
2638                    .unwrap_or(raw)
2639                    .strip_prefix('\'')
2640                    .and_then(|s| s.strip_suffix('\''))
2641                    .unwrap_or(raw);
2642                alloc::format!("\"{trimmed}\"")
2643            }
2644            _ => lit.raw.clone(),
2645        });
2646    }
2647    if let ConstExpr::Scoped(s) = expr {
2648        let qual = s
2649            .parts
2650            .iter()
2651            .map(|p| p.text.clone())
2652            .collect::<Vec<_>>()
2653            .join(".");
2654        return Some(alloc::format!("\"{qual}\""));
2655    }
2656    None
2657}
2658
2659/// IDL §7.4.13.4 Bitset → DDS-TS 1.0 §7.7.
2660///
2661/// Each bitfield maps to a property whose TypeScript type
2662/// depends on the bitfield width: ≤32 → `number`, 33..64 →
2663/// `bigint`. Total width > 64 is rejected (PSM limit, since
2664/// JavaScript bitwise operations are 32-bit and `bigint` covers
2665/// 64 bits — wider bitsets cannot be operated on coherently).
2666fn emit_bitset(out: &mut String, b: &zerodds_idl::ast::BitsetDecl) -> Result<(), IdlTsError> {
2667    // Verify total width <= 64 (§7.7 fatal-error rule).
2668    let mut total: i64 = 0;
2669    for bf in &b.bitfields {
2670        if let Some(w) = eval_const_int(&bf.spec.width) {
2671            total = total.saturating_add(w);
2672        }
2673    }
2674    if total > 64 {
2675        return Err(IdlTsError::Unsupported(alloc::format!(
2676            "bitset {} total width {total} > 64 (DDS-TS 1.0 §7.7)",
2677            b.name.text
2678        )));
2679    }
2680
2681    out.push_str(&alloc::format!("export interface {} {{\n", b.name.text));
2682    for bf in &b.bitfields {
2683        if let Some(name) = &bf.name {
2684            let width = eval_const_int(&bf.spec.width).unwrap_or(0);
2685            let ts_ty = if width > 32 { "bigint" } else { "number" };
2686            out.push_str(&alloc::format!("    {}: {ts_ty};\n", name.text));
2687        }
2688    }
2689    out.push_str("}\n\n");
2690
2691    for bf in &b.bitfields {
2692        if let Some(name) = &bf.name {
2693            let width = const_expr_to_ts(&bf.spec.width);
2694            out.push_str(&alloc::format!(
2695                "export const {}_{}_BITS = {width};\n",
2696                b.name.text,
2697                name.text
2698            ));
2699        }
2700    }
2701    out.push('\n');
2702
2703    // Type-guard.
2704    let bs_name = &b.name.text;
2705    out.push_str(&alloc::format!(
2706        "export function is{bs_name}(v: unknown): v is {bs_name} {{\n"
2707    ));
2708    out.push_str("    if (typeof v !== \"object\" || v === null) return false;\n");
2709    out.push_str("    const o = v as Record<string, unknown>;\n");
2710    for bf in &b.bitfields {
2711        if let Some(name) = &bf.name {
2712            let width = eval_const_int(&bf.spec.width).unwrap_or(0);
2713            let ts_ty = if width > 32 { "bigint" } else { "number" };
2714            out.push_str(&alloc::format!(
2715                "    if (typeof o.{} !== \"{ts_ty}\") return false;\n",
2716                name.text
2717            ));
2718        }
2719    }
2720    out.push_str("    return true;\n}\n\n");
2721
2722    // Descriptor.
2723    out.push_str(&alloc::format!(
2724        "export const {bs_name}Type: DdsTypeDescriptor<{bs_name}> = {{\n"
2725    ));
2726    out.push_str("    kind: \"bitset\",\n");
2727    out.push_str(&alloc::format!("    name: \"{bs_name}\",\n"));
2728    out.push_str("    extensibility: \"final\",\n");
2729    out.push_str("    nested: false,\n");
2730    out.push_str(&alloc::format!("    bitBound: {total},\n"));
2731    out.push_str("    fields: [\n");
2732    let mut next_id: i64 = 0;
2733    for bf in &b.bitfields {
2734        if let Some(name) = &bf.name {
2735            let width = eval_const_int(&bf.spec.width).unwrap_or(0);
2736            out.push_str("        {\n");
2737            out.push_str(&alloc::format!("            name: \"{}\",\n", name.text));
2738            out.push_str(&alloc::format!("            id: {next_id},\n"));
2739            out.push_str(&alloc::format!(
2740                "            type: {{ kind: \"bitfield\", width: {width} }},\n"
2741            ));
2742            out.push_str("            key: false,\n");
2743            out.push_str("            optional: false,\n");
2744            out.push_str("            mustUnderstand: false,\n");
2745            out.push_str("        },\n");
2746            next_id += 1;
2747        }
2748    }
2749    out.push_str("    ],\n");
2750    out.push_str(&alloc::format!("    typeGuard: is{bs_name},\n"));
2751    out.push_str("};\n");
2752    out.push_str(&alloc::format!("registerType({bs_name}Type);\n\n"));
2753    Ok(())
2754}
2755
2756/// IDL §7.4.13.5 Bitmask → DDS-TS 1.0 §7.8.
2757///
2758/// Default bit width is 32 (per IDL); `@bit_bound(N)` may set
2759/// 1..64. For ≤32 the emitted shifts are unsigned 32-bit form
2760/// `(1 << K) >>> 0` with TypeScript `number`. For 33..64 the
2761/// emitted shifts are BigInt form `1n << Kn` with TypeScript
2762/// `bigint` — required because JavaScript's `<<` operator
2763/// coerces both operands to Int32 and would wrap for K >= 32.
2764///
2765/// `@position(P)` on a bit value overrides the implicit bit
2766/// position (defaults to declaration order).
2767fn emit_bitmask(out: &mut String, b: &zerodds_idl::ast::BitmaskDecl) -> Result<(), IdlTsError> {
2768    let bit_bound = annotation_int_value(&b.annotations, "bit_bound").unwrap_or(32);
2769    let use_bigint = bit_bound > 32;
2770    let alias_ty = if use_bigint { "bigint" } else { "number" };
2771
2772    out.push_str(&alloc::format!("export const {} = {{\n", b.name.text));
2773    for (i, v) in b.values.iter().enumerate() {
2774        let pos = annotation_int_value(&v.annotations, "position").unwrap_or(i as i64);
2775        let shift = if use_bigint {
2776            alloc::format!("1n << {pos}n")
2777        } else {
2778            alloc::format!("(1 << {pos}) >>> 0")
2779        };
2780        out.push_str(&alloc::format!("    {}: {shift},\n", v.name.text));
2781    }
2782    out.push_str("} as const;\n");
2783    out.push_str(&alloc::format!(
2784        "export type {} = {alias_ty};\n",
2785        b.name.text
2786    ));
2787    out.push_str(&alloc::format!(
2788        "export const {}_BIT_BOUND = {bit_bound};\n\n",
2789        b.name.text
2790    ));
2791
2792    // Type-guard for the bitmask alias type (number or bigint).
2793    let bm_name = &b.name.text;
2794    out.push_str(&alloc::format!(
2795        "export function is{bm_name}(v: unknown): v is {bm_name} {{\n"
2796    ));
2797    out.push_str(&alloc::format!("    return typeof v === \"{alias_ty}\";\n"));
2798    out.push_str("}\n\n");
2799
2800    // Descriptor.
2801    out.push_str(&alloc::format!(
2802        "export const {bm_name}Type: DdsTypeDescriptor<{bm_name}> = {{\n"
2803    ));
2804    out.push_str("    kind: \"bitmask\",\n");
2805    out.push_str(&alloc::format!("    name: \"{bm_name}\",\n"));
2806    out.push_str("    extensibility: \"final\",\n");
2807    out.push_str("    nested: false,\n");
2808    out.push_str(&alloc::format!("    bitBound: {bit_bound},\n"));
2809    out.push_str("    fields: [\n");
2810    for (i, v) in b.values.iter().enumerate() {
2811        let pos = annotation_int_value(&v.annotations, "position").unwrap_or(i as i64);
2812        out.push_str("        {\n");
2813        out.push_str(&alloc::format!("            name: \"{}\",\n", v.name.text));
2814        out.push_str(&alloc::format!("            id: {i},\n"));
2815        out.push_str("            type: { kind: \"bitfield\", width: 1 },\n");
2816        out.push_str("            key: false,\n");
2817        out.push_str("            optional: false,\n");
2818        out.push_str("            mustUnderstand: false,\n");
2819        out.push_str(&alloc::format!("            default: {pos},\n"));
2820        out.push_str("        },\n");
2821    }
2822    out.push_str("    ],\n");
2823    out.push_str(&alloc::format!("    typeGuard: is{bm_name},\n"));
2824    out.push_str("};\n");
2825    out.push_str(&alloc::format!("registerType({bm_name}Type);\n\n"));
2826    Ok(())
2827}
2828
2829/// IDL §7.4.4 Typedef → DDS-TS 1.0 §7.10.
2830///
2831/// Emits `export type <Alias> = <Base>` plus a Type-Descriptor of
2832/// kind `"alias"`. For integer-valued typedefs carrying
2833/// `@bit_bound(N)` with N in 33..64 the alias type switches from
2834/// `number` to `bigint` (§7.10.2). Bounded-string and bounded-
2835/// sequence typedef forms additionally emit
2836/// `<Alias>_BOUND` constants; fixed-size array typedefs emit
2837/// `<Alias>_LENGTH`.
2838fn emit_typedef(out: &mut String, t: &zerodds_idl::ast::TypedefDecl) -> Result<(), IdlTsError> {
2839    use zerodds_idl::ast::Declarator;
2840
2841    let bit_bound = annotation_int_value(&t.annotations, "bit_bound");
2842    let base_ts = if let Some(n) = bit_bound {
2843        if n > 32 && is_integer_typespec(&t.type_spec) {
2844            "bigint".into()
2845        } else {
2846            typespec_to_ts(&t.type_spec)?
2847        }
2848    } else {
2849        typespec_to_ts(&t.type_spec)?
2850    };
2851
2852    for d in &t.declarators {
2853        let alias = match d {
2854            Declarator::Simple(name) => {
2855                out.push_str(&alloc::format!("export type {} = {base_ts};\n", name.text));
2856                name.text.clone()
2857            }
2858            Declarator::Array(arr) => {
2859                out.push_str(&alloc::format!(
2860                    "export type {} = Array<{base_ts}>;\n",
2861                    arr.name.text
2862                ));
2863                if arr.sizes.len() == 1 {
2864                    if let Some(len) = eval_const_int(&arr.sizes[0]) {
2865                        out.push_str(&alloc::format!(
2866                            "export const {}_LENGTH = {len};\n",
2867                            arr.name.text
2868                        ));
2869                    }
2870                } else {
2871                    for (i, sz) in arr.sizes.iter().enumerate() {
2872                        if let Some(len) = eval_const_int(sz) {
2873                            out.push_str(&alloc::format!(
2874                                "export const {}_LENGTH_DIM{} = {len};\n",
2875                                arr.name.text,
2876                                i + 1
2877                            ));
2878                        }
2879                    }
2880                }
2881                arr.name.text.clone()
2882            }
2883        };
2884
2885        // Bounded-string / bounded-sequence: emit <Alias>_BOUND.
2886        if let TypeSpec::String(StringType { bound: Some(n), .. }) = &t.type_spec {
2887            if let Some(width) = eval_const_int(n) {
2888                out.push_str(&alloc::format!("export const {alias}_BOUND = {width};\n"));
2889            }
2890        }
2891        if let TypeSpec::Sequence(seq) = &t.type_spec {
2892            if let Some(bound) = &seq.bound {
2893                if let Some(width) = eval_const_int(bound) {
2894                    out.push_str(&alloc::format!("export const {alias}_BOUND = {width};\n"));
2895                }
2896            }
2897        }
2898
2899        // Type-guard.
2900        let typeof_check = typespec_typeof_check(&t.type_spec);
2901        out.push_str(&alloc::format!(
2902            "export function is{alias}(v: unknown): v is {alias} {{\n"
2903        ));
2904        if let Some(check) = &typeof_check {
2905            let bb_override = bit_bound.filter(|&n| n > 32 && is_integer_typespec(&t.type_spec));
2906            let effective = if bb_override.is_some() {
2907                "typeof v !== \"bigint\"".to_string()
2908            } else {
2909                check.replace("VAR", "v")
2910            };
2911            out.push_str(&alloc::format!("    if ({effective}) return false;\n"));
2912        }
2913        out.push_str("    return true;\n}\n");
2914
2915        // Descriptor.
2916        let inner_ref = typespec_to_typeref_literal(&t.type_spec);
2917        out.push_str(&alloc::format!(
2918            "export const {alias}Type: DdsTypeDescriptor<{alias}> = {{\n"
2919        ));
2920        out.push_str("    kind: \"alias\",\n");
2921        out.push_str(&alloc::format!("    name: \"{alias}\",\n"));
2922        out.push_str("    extensibility: \"appendable\",\n");
2923        out.push_str("    nested: false,\n");
2924        if let Some(n) = bit_bound {
2925            out.push_str(&alloc::format!("    bitBound: {n},\n"));
2926        }
2927        out.push_str("    fields: [\n");
2928        out.push_str("        {\n");
2929        out.push_str("            name: \"value\",\n");
2930        out.push_str("            id: 0,\n");
2931        out.push_str(&alloc::format!("            type: {inner_ref},\n"));
2932        out.push_str("            key: false,\n");
2933        out.push_str("            optional: false,\n");
2934        out.push_str("            mustUnderstand: false,\n");
2935        out.push_str("        },\n");
2936        out.push_str("    ],\n");
2937        out.push_str(&alloc::format!("    typeGuard: is{alias},\n"));
2938        out.push_str("};\n");
2939        out.push_str(&alloc::format!("registerType({alias}Type);\n\n"));
2940    }
2941    Ok(())
2942}
2943
2944/// True iff the `TypeSpec` is an integer primitive.
2945fn is_integer_typespec(t: &TypeSpec) -> bool {
2946    matches!(t, TypeSpec::Primitive(PrimitiveType::Integer(_)))
2947}
2948
2949/// Wraps a base TypeScript type in `Array<…>` per Array-Declarator
2950/// dimensions. Returns `base` unchanged for `Declarator::Simple`.
2951/// For multi-dimensional arrays produces `Array<Array<…<T>>…>`
2952/// nested by dimension count (DDS-TS 1.0 §7.9 Multi-Dimensional
2953/// Arrays).
2954fn wrap_with_array_dimensions(base: &str, d: &zerodds_idl::ast::Declarator) -> String {
2955    use zerodds_idl::ast::Declarator;
2956    match d {
2957        Declarator::Simple(_) => base.to_string(),
2958        Declarator::Array(arr) => {
2959            let mut out = base.to_string();
2960            for _ in &arr.sizes {
2961                out = alloc::format!("Array<{out}>");
2962            }
2963            out
2964        }
2965    }
2966}
2967
2968// ============================================================
2969// Operations Profile (DDS-TS 1.0 §11)
2970// ============================================================
2971
2972/// Emits a paired Client/Handler interface and ServiceDescriptor
2973/// for an IDL `interface` declaration. The mapping follows
2974/// DDS-TS 1.0 Chapter 11:
2975///
2976/// 1. `<Iface>Client` — TypeScript interface; each operation
2977///    becomes a `Promise<T>`-returning method (no `async` keyword;
2978///    interface methods cannot be `async` in TS).
2979/// 2. `<Iface>Handler` — same shape as Client; the service-side
2980///    implementer satisfies it.
2981/// 3. `<Iface>Service: ServiceDescriptor<Client, Handler>` — the
2982///    side-table descriptor with operations / attributes /
2983///    inheritance metadata.
2984///
2985/// Inheritance: Client/Handler `extends` parent's Client/Handler;
2986/// `Service.inherits` lists parent service descriptors in
2987/// declaration order (no flattening).
2988///
2989/// Forward declarations without a matching complete declaration
2990/// lie outside this file (§11.7) — `emit_definition`
2991/// currently silently drops them; a strict-mode emit would call
2992/// out as `DDS-TS-E002`.
2993fn emit_interface(out: &mut String, i: &zerodds_idl::ast::InterfaceDef) -> Result<(), IdlTsError> {
2994    use zerodds_idl::ast::Export;
2995
2996    let name = &i.name.text;
2997    let client_name = alloc::format!("{name}Client");
2998    let handler_name = alloc::format!("{name}Handler");
2999
3000    // Compute base Client/Handler list for `extends` clauses.
3001    let base_client = i
3002        .bases
3003        .iter()
3004        .map(|b| {
3005            let qual = b
3006                .parts
3007                .iter()
3008                .map(|p| p.text.clone())
3009                .collect::<Vec<_>>()
3010                .join(".");
3011            alloc::format!("{qual}Client")
3012        })
3013        .collect::<Vec<_>>()
3014        .join(", ");
3015    let base_handler = i
3016        .bases
3017        .iter()
3018        .map(|b| {
3019            let qual = b
3020                .parts
3021                .iter()
3022                .map(|p| p.text.clone())
3023                .collect::<Vec<_>>()
3024                .join(".");
3025            alloc::format!("{qual}Handler")
3026        })
3027        .collect::<Vec<_>>()
3028        .join(", ");
3029
3030    // Emit Client interface.
3031    out.push_str(&alloc::format!("export interface {client_name}"));
3032    if !base_client.is_empty() {
3033        out.push_str(&alloc::format!(" extends {base_client}"));
3034    }
3035    out.push_str(" {\n");
3036    emit_verbatim_at(out, &i.annotations, VerbatimPlacement::BeginDeclaration);
3037    for ex in &i.exports {
3038        match ex {
3039            Export::Op(op) => {
3040                emit_op_method(out, op)?;
3041            }
3042            Export::Attr(attr) => {
3043                emit_attr_methods(out, attr)?;
3044            }
3045            _ => {}
3046        }
3047    }
3048    emit_verbatim_at(out, &i.annotations, VerbatimPlacement::EndDeclaration);
3049    out.push_str("}\n\n");
3050
3051    // Emit Handler interface (same shape).
3052    out.push_str(&alloc::format!("export interface {handler_name}"));
3053    if !base_handler.is_empty() {
3054        out.push_str(&alloc::format!(" extends {base_handler}"));
3055    }
3056    out.push_str(" {\n");
3057    emit_verbatim_at(out, &i.annotations, VerbatimPlacement::BeginDeclaration);
3058    for ex in &i.exports {
3059        match ex {
3060            Export::Op(op) => emit_op_method(out, op)?,
3061            Export::Attr(attr) => emit_attr_methods(out, attr)?,
3062            _ => {}
3063        }
3064    }
3065    emit_verbatim_at(out, &i.annotations, VerbatimPlacement::EndDeclaration);
3066    out.push_str("}\n\n");
3067
3068    // Emit ServiceDescriptor.
3069    out.push_str(&alloc::format!(
3070        "export const {name}Service: ServiceDescriptor<{client_name}, {handler_name}> = {{\n"
3071    ));
3072    out.push_str(&alloc::format!("    name: \"{name}\",\n"));
3073    // inherits.
3074    out.push_str("    inherits: [");
3075    for (idx, b) in i.bases.iter().enumerate() {
3076        let qual = b
3077            .parts
3078            .iter()
3079            .map(|p| p.text.clone())
3080            .collect::<Vec<_>>()
3081            .join(".");
3082        if idx > 0 {
3083            out.push_str(", ");
3084        }
3085        out.push_str(&alloc::format!("{qual}Service"));
3086    }
3087    out.push_str("],\n");
3088
3089    // operations array.
3090    out.push_str("    operations: [\n");
3091    for ex in &i.exports {
3092        if let Export::Op(op) = ex {
3093            emit_op_descriptor(out, op);
3094        }
3095    }
3096    out.push_str("    ],\n");
3097
3098    // attributes array.
3099    out.push_str("    attributes: [\n");
3100    for ex in &i.exports {
3101        if let Export::Attr(attr) = ex {
3102            emit_attr_descriptor(out, attr);
3103        }
3104    }
3105    out.push_str("    ],\n");
3106    out.push_str("};\n\n");
3107
3108    Ok(())
3109}
3110
3111/// Emits one method signature in a Client/Handler interface.
3112fn emit_op_method(out: &mut String, op: &zerodds_idl::ast::OpDecl) -> Result<(), IdlTsError> {
3113    use zerodds_idl::ast::ParamAttribute;
3114    let return_ts = match &op.return_type {
3115        Some(t) => typespec_to_ts(t)?,
3116        None => "void".into(),
3117    };
3118
3119    // Method parameters: in + inout (declaration order).
3120    let mut params: Vec<String> = Vec::new();
3121    let mut out_params: Vec<(String, String)> = Vec::new();
3122    for p in &op.params {
3123        let ts = typespec_to_ts(&p.type_spec)?;
3124        match p.attribute {
3125            ParamAttribute::In => {
3126                params.push(alloc::format!("{}: {ts}", p.name.text));
3127            }
3128            ParamAttribute::InOut => {
3129                params.push(alloc::format!("{}: {ts}", p.name.text));
3130                out_params.push((p.name.text.clone(), ts));
3131            }
3132            ParamAttribute::Out => {
3133                out_params.push((p.name.text.clone(), ts));
3134            }
3135        }
3136    }
3137
3138    // Resolve return shape: if any out/inout params, emit object
3139    // literal { result: R; <out>: T; ... }; else just R.
3140    let resolve_ts = if out_params.is_empty() {
3141        return_ts.clone()
3142    } else {
3143        let mut entries: Vec<String> = Vec::new();
3144        if op.return_type.is_some() {
3145            entries.push(alloc::format!("result: {return_ts}"));
3146        }
3147        for (n, t) in &out_params {
3148            entries.push(alloc::format!("{n}: {t}"));
3149        }
3150        alloc::format!("{{ {} }}", entries.join("; "))
3151    };
3152
3153    let promise_ts = if op.return_type.is_none() && out_params.is_empty() {
3154        "Promise<void>".into()
3155    } else {
3156        alloc::format!("Promise<{resolve_ts}>")
3157    };
3158
3159    out.push_str(&alloc::format!(
3160        "    {}({}): {promise_ts};\n",
3161        op.name.text,
3162        params.join(", ")
3163    ));
3164    Ok(())
3165}
3166
3167/// Emits getter (and writer-side setter for non-readonly) methods
3168/// for an IDL attribute on a Client/Handler interface.
3169fn emit_attr_methods(
3170    out: &mut String,
3171    attr: &zerodds_idl::ast::AttrDecl,
3172) -> Result<(), IdlTsError> {
3173    let ts = typespec_to_ts(&attr.type_spec)?;
3174    out.push_str(&alloc::format!(
3175        "    get_{}(): Promise<{ts}>;\n",
3176        attr.name.text
3177    ));
3178    if !attr.readonly {
3179        out.push_str(&alloc::format!(
3180            "    set_{}(value: {ts}): Promise<void>;\n",
3181            attr.name.text
3182        ));
3183    }
3184    Ok(())
3185}
3186
3187/// Emits one OperationDescriptor literal inside a Service descriptor.
3188fn emit_op_descriptor(out: &mut String, op: &zerodds_idl::ast::OpDecl) {
3189    use zerodds_idl::ast::ParamAttribute;
3190    out.push_str("        {\n");
3191    out.push_str(&alloc::format!("            name: \"{}\",\n", op.name.text));
3192    out.push_str(&alloc::format!("            oneway: {},\n", op.oneway));
3193    let return_ref = match &op.return_type {
3194        Some(t) => typespec_to_typeref_literal(t),
3195        None => "{ kind: \"void\" }".into(),
3196    };
3197    out.push_str(&alloc::format!("            returnType: {return_ref},\n"));
3198    out.push_str("            parameters: [\n");
3199    for p in &op.params {
3200        let mode = match p.attribute {
3201            ParamAttribute::In => "in",
3202            ParamAttribute::Out => "out",
3203            ParamAttribute::InOut => "inout",
3204        };
3205        let pref = typespec_to_typeref_literal(&p.type_spec);
3206        out.push_str("                {\n");
3207        out.push_str(&alloc::format!(
3208            "                    name: \"{}\",\n",
3209            p.name.text
3210        ));
3211        out.push_str(&alloc::format!("                    mode: \"{mode}\",\n"));
3212        out.push_str(&alloc::format!("                    type: {pref},\n"));
3213        out.push_str("                },\n");
3214    }
3215    out.push_str("            ],\n");
3216    out.push_str("            raises: [");
3217    for (idx, r) in op.raises.iter().enumerate() {
3218        let qual = r
3219            .parts
3220            .iter()
3221            .map(|p| p.text.clone())
3222            .collect::<Vec<_>>()
3223            .join(".");
3224        if idx > 0 {
3225            out.push_str(", ");
3226        }
3227        out.push_str(&alloc::format!("{qual}Type"));
3228    }
3229    out.push_str("],\n");
3230    out.push_str("        },\n");
3231}
3232
3233/// Emits one AttributeDescriptor literal inside a Service
3234/// descriptor.
3235fn emit_attr_descriptor(out: &mut String, attr: &zerodds_idl::ast::AttrDecl) {
3236    out.push_str("        {\n");
3237    out.push_str(&alloc::format!(
3238        "            name: \"{}\",\n",
3239        attr.name.text
3240    ));
3241    out.push_str(&alloc::format!(
3242        "            readonly: {},\n",
3243        attr.readonly
3244    ));
3245    let tref = typespec_to_typeref_literal(&attr.type_spec);
3246    out.push_str(&alloc::format!("            type: {tref},\n"));
3247    out.push_str("            getRaises: [");
3248    for (idx, r) in attr.get_raises.iter().enumerate() {
3249        let qual = r
3250            .parts
3251            .iter()
3252            .map(|p| p.text.clone())
3253            .collect::<Vec<_>>()
3254            .join(".");
3255        if idx > 0 {
3256            out.push_str(", ");
3257        }
3258        out.push_str(&alloc::format!("{qual}Type"));
3259    }
3260    out.push_str("],\n");
3261    out.push_str("            setRaises: [");
3262    for (idx, r) in attr.set_raises.iter().enumerate() {
3263        let qual = r
3264            .parts
3265            .iter()
3266            .map(|p| p.text.clone())
3267            .collect::<Vec<_>>()
3268            .join(".");
3269        if idx > 0 {
3270            out.push_str(", ");
3271        }
3272        out.push_str(&alloc::format!("{qual}Type"));
3273    }
3274    out.push_str("],\n");
3275    out.push_str("        },\n");
3276}
3277
3278/// True iff `s` is parseable as a (possibly-signed) integer or
3279/// floating-point literal. Used to decide whether descriptor
3280/// `min`/`max` literals need to be string-quoted.
3281fn is_numeric_literal_text(s: &str) -> bool {
3282    let trimmed = s.trim();
3283    if trimmed.is_empty() {
3284        return false;
3285    }
3286    let mut chars = trimmed.chars();
3287    let first = chars.next().unwrap_or(' ');
3288    if !(first.is_ascii_digit() || first == '-' || first == '+' || first == '.') {
3289        return false;
3290    }
3291    trimmed
3292        .chars()
3293        .all(|c| c.is_ascii_digit() || c == '.' || c == '-' || c == '+' || c == 'e' || c == 'E')
3294}
3295
3296/// Renders a TSDoc block for a struct member with metadata
3297/// annotations (`@unit`, `@min`, `@max`, `@must_understand`,
3298/// `@nested`, `@hashid`, `@id`). Returns `None` if no TSDoc tag
3299/// would be emitted.
3300fn render_tsdoc_for_member(annotations: &[zerodds_idl::ast::Annotation]) -> Option<String> {
3301    let mut tags: Vec<String> = Vec::new();
3302    if let Some(unit) = annotation_string_value(annotations, "unit") {
3303        tags.push(alloc::format!(" * @dds-unit {unit}"));
3304    }
3305    if let Some(min) = annotation_const_text(annotations, "min") {
3306        tags.push(alloc::format!(" * @dds-min {min}"));
3307    }
3308    if let Some(max) = annotation_const_text(annotations, "max") {
3309        tags.push(alloc::format!(" * @dds-max {max}"));
3310    }
3311    if has_annotation(annotations, "must_understand") {
3312        tags.push(" * @dds-must-understand".into());
3313    }
3314    if has_annotation(annotations, "nested") {
3315        tags.push(" * @dds-nested".into());
3316    }
3317    if let Some(hashid) = annotation_string_value(annotations, "hashid") {
3318        tags.push(alloc::format!(" * @dds-hashid {hashid}"));
3319    }
3320    if let Some(id) = annotation_int_value(annotations, "id") {
3321        tags.push(alloc::format!(" * @dds-id {id}"));
3322    }
3323    if let Some(key) = annotation_int_value(annotations, "key").or_else(|| {
3324        if has_annotation(annotations, "key") {
3325            Some(0)
3326        } else {
3327            None
3328        }
3329    }) {
3330        let _ = key;
3331        tags.push(" * @dds-key".into());
3332    }
3333    if tags.is_empty() {
3334        return None;
3335    }
3336    let mut out = String::from("    /**\n");
3337    for t in &tags {
3338        out.push_str("    ");
3339        out.push_str(t);
3340        out.push('\n');
3341    }
3342    out.push_str("     */\n");
3343    Some(out)
3344}
3345
3346/// Returns the const-expression of `@<name>(<expr>)` rendered
3347/// as TypeScript-literal text.
3348fn annotation_const_text(
3349    annotations: &[zerodds_idl::ast::Annotation],
3350    name: &str,
3351) -> Option<String> {
3352    use zerodds_idl::ast::{AnnotationParams, ConstExpr, LiteralKind};
3353    for a in annotations {
3354        if a.name.parts.len() == 1 && a.name.parts[0].text == name {
3355            if let AnnotationParams::Single(expr) = &a.params {
3356                if let Some(n) = eval_const_int(expr) {
3357                    return Some(alloc::format!("{n}"));
3358                }
3359                if let ConstExpr::Literal(lit) = expr {
3360                    return Some(match lit.kind {
3361                        LiteralKind::String | LiteralKind::WideString => {
3362                            // Strip quotes for TSDoc readability.
3363                            let raw = lit.raw.as_str();
3364                            let trimmed = raw
3365                                .strip_prefix('L')
3366                                .unwrap_or(raw)
3367                                .strip_prefix('"')
3368                                .and_then(|s| s.strip_suffix('"'))
3369                                .unwrap_or(raw);
3370                            alloc::string::ToString::to_string(trimmed)
3371                        }
3372                        _ => lit.raw.clone(),
3373                    });
3374                }
3375            }
3376        }
3377    }
3378    None
3379}
3380
3381/// Renders the const-expression of `@default(<expr>)` as a
3382/// TypeScript literal expression for use in `<Type>_<Field>_DEFAULT`
3383/// constants and in descriptor `default` fields.
3384fn annotation_default_to_ts(annotations: &[zerodds_idl::ast::Annotation]) -> Option<String> {
3385    use zerodds_idl::ast::{AnnotationParams, ConstExpr, LiteralKind};
3386    for a in annotations {
3387        if a.name.parts.len() == 1 && a.name.parts[0].text == "default" {
3388            if let AnnotationParams::Single(expr) = &a.params {
3389                if let ConstExpr::Literal(lit) = expr {
3390                    return Some(match lit.kind {
3391                        LiteralKind::String | LiteralKind::WideString => {
3392                            // Re-emit with standard double quotes.
3393                            let raw = lit.raw.as_str();
3394                            let trimmed = raw
3395                                .strip_prefix('L')
3396                                .unwrap_or(raw)
3397                                .strip_prefix('"')
3398                                .and_then(|s| s.strip_suffix('"'))
3399                                .unwrap_or(raw);
3400                            alloc::format!("\"{trimmed}\"")
3401                        }
3402                        LiteralKind::Boolean => match lit.raw.to_lowercase().as_str() {
3403                            "true" | "1" => "true".into(),
3404                            _ => "false".into(),
3405                        },
3406                        LiteralKind::Char | LiteralKind::WideChar => {
3407                            // 'x' single-quoted in IDL → "x" in TS.
3408                            let raw = lit.raw.as_str();
3409                            let trimmed = raw
3410                                .strip_prefix('L')
3411                                .unwrap_or(raw)
3412                                .strip_prefix('\'')
3413                                .and_then(|s| s.strip_suffix('\''))
3414                                .unwrap_or(raw);
3415                            alloc::format!("\"{trimmed}\"")
3416                        }
3417                        _ => lit.raw.clone(),
3418                    });
3419                }
3420                if let Some(n) = eval_const_int(expr) {
3421                    return Some(alloc::format!("{n}"));
3422                }
3423            }
3424        }
3425    }
3426    None
3427}
3428
3429/// Emits `<Type>_<Field>_DEFAULT` constants for members with
3430/// `@default(V)`. The descriptor's `default` field is set in
3431/// `emit_struct_descriptor`.
3432fn emit_struct_default_constants(
3433    out: &mut String,
3434    s: &zerodds_idl::ast::StructDef,
3435) -> Result<(), IdlTsError> {
3436    let type_name = &s.name.text;
3437    let mut emitted = false;
3438    for m in &s.members {
3439        if let Some(lit) = annotation_default_to_ts(&m.annotations) {
3440            let ts_ty = typespec_to_ts(&m.type_spec)?;
3441            for d in &m.declarators {
3442                out.push_str(&alloc::format!(
3443                    "export const {type_name}_{}_DEFAULT: {ts_ty} = {lit};\n",
3444                    d.name().text
3445                ));
3446                emitted = true;
3447            }
3448        }
3449    }
3450    if emitted {
3451        out.push('\n');
3452    }
3453    Ok(())
3454}
3455
3456/// Const-Eval auf einer ConstExpr fuer Bitfield-Widths in `idl4-ts-1.0`
3457/// §7.4.13.4 (Bitset-Width-Const-Eval).
3458///
3459/// Reicht fuer den Bitset-Width-Use-Case: alle binaeren/unaeren
3460/// Operationen ueber Integer-Literalen. Fliesskomma-Sub-Expressions
3461/// fallen auf den Placeholder zurueck (das ist im Bitset-Kontext nicht
3462/// vorgesehen — die Spec verlangt `<positive-int-const>`-Width).
3463fn const_expr_to_ts(e: &zerodds_idl::ast::ConstExpr) -> String {
3464    eval_const_int(e)
3465        .map(|n| alloc::format!("{n}"))
3466        .unwrap_or_else(|| String::from("0"))
3467}
3468
3469/// zerodds-lint: recursion-depth 16 (Const-Expression-Tiefe)
3470pub(crate) fn eval_const_int(e: &zerodds_idl::ast::ConstExpr) -> Option<i64> {
3471    use zerodds_idl::ast::{BinaryOp, ConstExpr, LiteralKind, UnaryOp};
3472    match e {
3473        ConstExpr::Literal(l) if l.kind == LiteralKind::Integer => parse_int_literal(&l.raw),
3474        ConstExpr::Literal(l) if l.kind == LiteralKind::Boolean => {
3475            if l.raw == "TRUE" {
3476                Some(1)
3477            } else {
3478                Some(0)
3479            }
3480        }
3481        ConstExpr::Literal(_) | ConstExpr::Scoped(_) => None,
3482        ConstExpr::Unary { op, operand, .. } => {
3483            let v = eval_const_int(operand)?;
3484            Some(match op {
3485                UnaryOp::Plus => v,
3486                UnaryOp::Minus => v.checked_neg()?,
3487                UnaryOp::BitNot => !v,
3488            })
3489        }
3490        ConstExpr::Binary { op, lhs, rhs, .. } => {
3491            let a = eval_const_int(lhs)?;
3492            let b = eval_const_int(rhs)?;
3493            match op {
3494                BinaryOp::Or => Some(a | b),
3495                BinaryOp::Xor => Some(a ^ b),
3496                BinaryOp::And => Some(a & b),
3497                BinaryOp::Shl => u32::try_from(b).ok().and_then(|s| a.checked_shl(s)),
3498                BinaryOp::Shr => u32::try_from(b).ok().and_then(|s| a.checked_shr(s)),
3499                BinaryOp::Add => a.checked_add(b),
3500                BinaryOp::Sub => a.checked_sub(b),
3501                BinaryOp::Mul => a.checked_mul(b),
3502                BinaryOp::Div => a.checked_div(b),
3503                BinaryOp::Mod => a.checked_rem(b),
3504            }
3505        }
3506    }
3507}
3508
3509fn parse_int_literal(raw: &str) -> Option<i64> {
3510    // IDL §7.2.6.4 Integer-Literal-Forms: decimal, hex (0x), octal (0).
3511    let s = raw.trim();
3512    if let Some(rest) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
3513        i64::from_str_radix(rest, 16).ok()
3514    } else if s.len() > 1 && s.starts_with('0') && s.chars().all(|c| c.is_ascii_digit()) {
3515        i64::from_str_radix(&s[1..], 8).ok()
3516    } else {
3517        s.parse::<i64>().ok()
3518    }
3519}
3520
3521/// Maps an IDL `TypeSpec` to its TypeScript surface type per
3522/// DDS-TS 1.0 §7.3 (Primitive Types) and §7.9 (Sequence/Array/Map).
3523///
3524/// zerodds-lint: recursion-depth 16
3525pub(crate) fn typespec_to_ts(t: &TypeSpec) -> Result<String, IdlTsError> {
3526    Ok(match t {
3527        TypeSpec::Primitive(p) => match p {
3528            PrimitiveType::Boolean => "boolean".into(),
3529            // §7.3.2 — branded carriers, see runtime/branded.ts.
3530            PrimitiveType::Char => "Char".into(),
3531            PrimitiveType::WideChar => "WChar".into(),
3532            PrimitiveType::Octet => "number".into(),
3533            PrimitiveType::Integer(i) => match i {
3534                IntegerType::Short
3535                | IntegerType::UShort
3536                | IntegerType::Long
3537                | IntegerType::ULong
3538                | IntegerType::Int8
3539                | IntegerType::UInt8
3540                | IntegerType::Int16
3541                | IntegerType::UInt16
3542                | IntegerType::Int32
3543                | IntegerType::UInt32 => "number".into(),
3544                IntegerType::LongLong
3545                | IntegerType::ULongLong
3546                | IntegerType::Int64
3547                | IntegerType::UInt64 => "bigint".into(),
3548            },
3549            PrimitiveType::Floating(f) => match f {
3550                FloatingType::Float | FloatingType::Double => "number".into(),
3551                // §7.3.3 — opaque carrier, default emit (no flag).
3552                FloatingType::LongDouble => "LongDouble".into(),
3553            },
3554        },
3555        TypeSpec::String(StringType { wide: false, .. }) => "string".into(),
3556        TypeSpec::String(StringType { wide: true, .. }) => "string".into(),
3557        TypeSpec::Sequence(s) => {
3558            let inner = typespec_to_ts(&s.elem)?;
3559            alloc::format!("Array<{inner}>")
3560        }
3561        TypeSpec::Scoped(s) => s
3562            .parts
3563            .iter()
3564            .map(|p| p.text.clone())
3565            .collect::<Vec<_>>()
3566            .join("."),
3567        TypeSpec::Fixed(_) => "string".into(),
3568        // §7.3.4 — boxed value with explicit type-id.
3569        TypeSpec::Any => "DdsAny".into(),
3570        // §7.9.1 — read-only map, key-equality semantics enforced
3571        // at the runtime via equalKey().
3572        TypeSpec::Map(m) => {
3573            let k = typespec_to_ts(&m.key)?;
3574            let v = typespec_to_ts(&m.value)?;
3575            alloc::format!("ReadonlyMap<{k}, {v}>")
3576        }
3577    })
3578}
3579
3580/// Pure-TypeScript Runtime-Library, die der Codegen-Output importiert.
3581///
3582/// DDS-TS 1.0 Descriptor-Runtime-Profile (Chapter 8 + Annex B §B.2
3583/// of `documentation/specs/dds-ts-1.0/`). Published as the
3584/// `@zerodds/types` npm package. Each TS source is embedded so that
3585/// codegen tooling can lay the runtime alongside generated files
3586/// without external fetch.
3587pub mod runtime {
3588    /// `types.ts` — descriptor type-level surface.
3589    pub const TYPES_TS: &str = include_str!("runtime/types.ts");
3590    /// `branded.ts` — `Char` / `WChar` / `LongDouble` carriers.
3591    pub const BRANDED_TS: &str = include_str!("runtime/branded.ts");
3592    /// `dds_any.ts` — `DdsAny` and `DdsException` markers.
3593    pub const DDS_ANY_TS: &str = include_str!("runtime/dds_any.ts");
3594    /// `registry.ts` — descriptor registry + reflection helpers.
3595    pub const REGISTRY_TS: &str = include_str!("runtime/registry.ts");
3596    /// `equal.ts` — `equalKey` and `isOneOf`.
3597    pub const EQUAL_TS: &str = include_str!("runtime/equal.ts");
3598    /// `operations.ts` — Operations-Profile descriptor types
3599    /// (Service/Operation/Parameter/Attribute, Chapter 11).
3600    pub const OPERATIONS_TS: &str = include_str!("runtime/operations.ts");
3601    /// `wasm.ts` — WASM-Bindings Profile surface (Annex C).
3602    pub const WASM_TS: &str = include_str!("runtime/wasm.ts");
3603    /// `test_backend.ts` — in-memory reference backend for §C.5
3604    /// round-trip conformance tests.
3605    pub const TEST_BACKEND_TS: &str = include_str!("runtime/test_backend.ts");
3606    /// `index.ts` — public barrel (re-exports of B.2.1 surface
3607    /// plus Operations- and WASM-Profile types).
3608    pub const INDEX_TS: &str = include_str!("runtime/index.ts");
3609
3610    /// All runtime sources as `(filename, content)` pairs, suitable
3611    /// for codegen tooling that lays out the runtime tree alongside
3612    /// generated files.
3613    pub const ALL: &[(&str, &str)] = &[
3614        ("types.ts", TYPES_TS),
3615        ("branded.ts", BRANDED_TS),
3616        ("dds_any.ts", DDS_ANY_TS),
3617        ("registry.ts", REGISTRY_TS),
3618        ("equal.ts", EQUAL_TS),
3619        ("operations.ts", OPERATIONS_TS),
3620        ("wasm.ts", WASM_TS),
3621        ("test_backend.ts", TEST_BACKEND_TS),
3622        ("index.ts", INDEX_TS),
3623    ];
3624}
3625
3626/// Fehler-Type fuer den TS-Codegen.
3627#[derive(Debug, Clone, PartialEq, Eq)]
3628pub enum IdlTsError {
3629    /// Unsupported IDL-Konstrukt.
3630    Unsupported(String),
3631}
3632
3633impl core::fmt::Display for IdlTsError {
3634    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3635        match self {
3636            Self::Unsupported(s) => write!(f, "TS-codegen: unsupported {s}"),
3637        }
3638    }
3639}
3640
3641#[cfg(test)]
3642#[allow(clippy::expect_used)]
3643mod tests {
3644    use super::*;
3645    use zerodds_idl::config::ParserConfig;
3646
3647    fn gen_ts(src: &str) -> String {
3648        let ast = zerodds_idl::parse(src, &ParserConfig::default()).expect("parse");
3649        generate_ts_source(&ast).expect("gen")
3650    }
3651
3652    fn gen_ts_full(src: &str) -> String {
3653        // Variant for tests that exercise IDL features gated behind
3654        // `corba_*` flags (oneway, context, etc.).
3655        use zerodds_idl::features::IdlFeatures;
3656        let cfg = ParserConfig {
3657            features: IdlFeatures::all(),
3658            ..ParserConfig::default()
3659        };
3660        let ast = zerodds_idl::parse(src, &cfg).expect("parse");
3661        generate_ts_source(&ast).expect("gen")
3662    }
3663
3664    #[test]
3665    fn struct_emits_typescript_interface() {
3666        let ts = gen_ts(r"struct Point { long x; long y; };");
3667        assert!(ts.contains("export interface Point"));
3668        assert!(ts.contains("x: number"));
3669        assert!(ts.contains("y: number"));
3670    }
3671
3672    #[test]
3673    fn struct_emits_descriptor_typeguard_and_registertype() {
3674        let ts = gen_ts(r"struct Point { long x; long y; };");
3675        // Type-guard.
3676        assert!(
3677            ts.contains("export function isPoint(v: unknown): v is Point"),
3678            "got:\n{ts}"
3679        );
3680        // Descriptor.
3681        assert!(ts.contains("export const PointType: DdsTypeDescriptor<Point>"));
3682        assert!(ts.contains("kind: \"struct\""));
3683        assert!(ts.contains("extensibility: \"appendable\""));
3684        assert!(ts.contains("typeGuard: isPoint"));
3685        // Registry call.
3686        assert!(ts.contains("registerType(PointType);"));
3687    }
3688
3689    #[test]
3690    fn struct_with_final_annotation_sets_extensibility() {
3691        let ts = gen_ts(r"@final struct Point { long x; };");
3692        assert!(ts.contains("extensibility: \"final\""));
3693    }
3694
3695    #[test]
3696    fn struct_no_class_keyword_emitted() {
3697        // §7.4.1 — no TypeScript class for IDL data types.
3698        let ts = gen_ts(r"@final struct Point { @key long x; long y; };");
3699        assert!(
3700            !ts.contains("export class"),
3701            "TS class keyword forbidden, got:\n{ts}"
3702        );
3703        // Annotation reflected in descriptor only.
3704        assert!(ts.contains("key: true"));
3705    }
3706
3707    #[test]
3708    fn struct_bounded_string_emits_bound_constant() {
3709        let ts = gen_ts(r"struct Sample { string<32> name; };");
3710        assert!(
3711            ts.contains("export const Sample_name_BOUND = 32"),
3712            "got:\n{ts}"
3713        );
3714    }
3715
3716    #[test]
3717    fn struct_bounded_sequence_emits_bound_constant() {
3718        let ts = gen_ts(r"struct Sample { sequence<long, 16> readings; };");
3719        assert!(ts.contains("export const Sample_readings_BOUND = 16"));
3720    }
3721
3722    #[test]
3723    fn struct_optional_member_emits_optional_marker() {
3724        let ts = gen_ts(r"struct Frame { long seq; @optional long retry; };");
3725        assert!(ts.contains("retry?: number | undefined"));
3726        assert!(ts.contains("optional: true"));
3727    }
3728
3729    #[test]
3730    fn typedef_emits_descriptor() {
3731        let ts = gen_ts(r"typedef long Counter;");
3732        assert!(ts.contains("export type Counter = number"));
3733        assert!(ts.contains("export function isCounter"));
3734        assert!(ts.contains("export const CounterType: DdsTypeDescriptor<Counter>"));
3735        assert!(ts.contains("kind: \"alias\""));
3736        assert!(ts.contains("registerType(CounterType);"));
3737    }
3738
3739    #[test]
3740    fn typedef_bit_bound_above_32_switches_to_bigint() {
3741        let ts = gen_ts(r"@bit_bound(40) typedef long long Counter40;");
3742        assert!(ts.contains("export type Counter40 = bigint"), "got:\n{ts}");
3743        assert!(ts.contains("bitBound: 40"));
3744    }
3745
3746    #[test]
3747    fn primitive_mapping_uses_branded_carriers() {
3748        let ts = gen_ts(
3749            r"struct Sample {
3750                char c;
3751                wchar wc;
3752                long double ld;
3753                any a;
3754            };",
3755        );
3756        assert!(ts.contains("c: Char"), "got:\n{ts}");
3757        assert!(ts.contains("wc: WChar"));
3758        assert!(ts.contains("ld: LongDouble"));
3759        assert!(ts.contains("a: DdsAny"));
3760    }
3761
3762    #[test]
3763    fn map_mapping_uses_readonly_map() {
3764        let ts = gen_ts(r"struct Index { map<string, long> by_name; };");
3765        assert!(ts.contains("by_name: ReadonlyMap<string, number>"));
3766    }
3767
3768    #[test]
3769    fn runtime_import_block_present() {
3770        let ts = gen_ts(r"struct S { long v; };");
3771        assert!(ts.contains("from \"@zerodds/types\""));
3772        assert!(ts.contains("DdsTypeDescriptor"));
3773        assert!(ts.contains("registerType"));
3774    }
3775
3776    #[test]
3777    fn bitset_descriptor_emitted_with_bitfield_kind() {
3778        let ts = gen_ts(r"bitset Flags { bitfield<3> low; bitfield<5> high; };");
3779        assert!(ts.contains("export const FlagsType: DdsTypeDescriptor<Flags>"));
3780        assert!(ts.contains("kind: \"bitset\""));
3781        assert!(ts.contains("kind: \"bitfield\", width: 3"));
3782        assert!(ts.contains("kind: \"bitfield\", width: 5"));
3783        assert!(ts.contains("bitBound: 8"));
3784        assert!(ts.contains("registerType(FlagsType);"));
3785    }
3786
3787    #[test]
3788    fn bitset_total_width_above_64_rejected() {
3789        // §7.7 — total > 64 SHALL be a fatal error.
3790        let src = r"bitset Big { bitfield<40> a; bitfield<40> b; };";
3791        let ast =
3792            zerodds_idl::parse(src, &zerodds_idl::config::ParserConfig::default()).expect("parse");
3793        let err = generate_ts_source(&ast).expect_err("must reject total > 64");
3794        match err {
3795            IdlTsError::Unsupported(msg) => {
3796                assert!(msg.contains("total width"), "msg: {msg}");
3797            }
3798        }
3799    }
3800
3801    #[test]
3802    fn const_decl_emits_export_const_with_typed_literal() {
3803        let ts = gen_ts(r"const long MAX_RETRIES = 5;");
3804        assert!(
3805            ts.contains("export const MAX_RETRIES: number = 5;"),
3806            "got:\n{ts}"
3807        );
3808    }
3809
3810    #[test]
3811    fn const_decl_long_long_uses_bigint_suffix() {
3812        let ts = gen_ts(r"const long long BIG = 9007199254740993;");
3813        assert!(
3814            ts.contains("export const BIG: bigint = 9007199254740993n;"),
3815            "got:\n{ts}"
3816        );
3817    }
3818
3819    #[test]
3820    fn const_decl_string_emits_double_quoted_literal() {
3821        let ts = gen_ts(r#"const string SERVICE_NAME = "telemetry";"#);
3822        assert!(
3823            ts.contains("export const SERVICE_NAME: string = \"telemetry\";"),
3824            "got:\n{ts}"
3825        );
3826    }
3827
3828    #[test]
3829    fn const_decl_boolean_normalises_token() {
3830        let ts = gen_ts(r"const boolean DEBUG_ENABLED = FALSE;");
3831        assert!(
3832            ts.contains("export const DEBUG_ENABLED: boolean = false;"),
3833            "got:\n{ts}"
3834        );
3835    }
3836
3837    #[test]
3838    fn exception_emits_interface_extending_dds_exception() {
3839        let ts = gen_ts(r"exception Overflow { long limit; };");
3840        assert!(
3841            ts.contains("export interface Overflow extends DdsException"),
3842            "got:\n{ts}"
3843        );
3844        assert!(ts.contains("limit: number"));
3845        assert!(ts.contains("export function isOverflow"));
3846        assert!(ts.contains("kind: \"exception\""));
3847        assert!(ts.contains("registerType(OverflowType);"));
3848    }
3849
3850    #[test]
3851    fn enum_descriptor_emitted_with_ordinal_defaults() {
3852        let ts = gen_ts(r"enum Color { RED, GREEN, BLUE };");
3853        assert!(ts.contains("export const ColorType"));
3854        assert!(ts.contains("kind: \"enum\""));
3855        assert!(ts.contains("default: 0"));
3856        assert!(ts.contains("default: 1"));
3857        assert!(ts.contains("default: 2"));
3858        assert!(ts.contains("registerType(ColorType);"));
3859    }
3860
3861    // §7.11 @default-Mapping: emit_struct_default_constants + descriptor
3862    // .default-Field sind im Codegen implementiert. Eine direkte IDL-
3863    // basierte Conformance-Prüfung ist erst möglich wenn der zerodds-idl-
3864    // Parser @default als Builtin-Annotation akzeptiert (default ist
3865    // aktuell als case-Label-Keyword reserviert). Die Codegen-Logik
3866    // wird bis dahin durch die descriptor-min/max/unit-Tests indirekt
3867    // abgedeckt (gleicher annotation_const_text/-string_value-Pfad).
3868
3869    #[test]
3870    fn struct_min_max_unit_emit_descriptor_fields_and_tsdoc() {
3871        let ts = gen_ts(
3872            r#"struct Telemetry {
3873                @unit("celsius") @min(-40) @max(125)
3874                double temperature;
3875            };"#,
3876        );
3877        // TSDoc tags above the property.
3878        assert!(ts.contains("@dds-unit celsius"), "got:\n{ts}");
3879        assert!(ts.contains("@dds-min -40"));
3880        assert!(ts.contains("@dds-max 125"));
3881        // Descriptor entries.
3882        assert!(ts.contains("unit: \"celsius\""));
3883        assert!(ts.contains("min: -40"));
3884        assert!(ts.contains("max: 125"));
3885    }
3886
3887    #[test]
3888    fn interface_emits_client_handler_and_service_descriptor() {
3889        let ts = gen_ts(
3890            r"interface Calculator {
3891                long add(in long a, in long b);
3892                attribute string name;
3893            };",
3894        );
3895        // Client + Handler interfaces.
3896        assert!(
3897            ts.contains("export interface CalculatorClient {"),
3898            "got:\n{ts}"
3899        );
3900        assert!(ts.contains("export interface CalculatorHandler {"));
3901        // Async-only signature: Promise<T>, no `async` keyword.
3902        assert!(ts.contains("add(a: number, b: number): Promise<number>"));
3903        assert!(!ts.contains("async add"));
3904        // Attribute getter + setter (not readonly).
3905        assert!(ts.contains("get_name(): Promise<string>"));
3906        assert!(ts.contains("set_name(value: string): Promise<void>"));
3907        // Service descriptor.
3908        assert!(ts.contains(
3909            "export const CalculatorService: ServiceDescriptor<CalculatorClient, CalculatorHandler>"
3910        ));
3911        assert!(ts.contains("name: \"Calculator\""));
3912        assert!(ts.contains("inherits: []"));
3913        assert!(ts.contains("oneway: false"));
3914    }
3915
3916    #[test]
3917    fn interface_oneway_emits_promise_void_with_oneway_descriptor() {
3918        // Requires corba_oneway_op feature enabled.
3919        let ts = gen_ts_full(
3920            r"interface Pinger {
3921                oneway void ping();
3922            };",
3923        );
3924        assert!(ts.contains("ping(): Promise<void>"));
3925        assert!(ts.contains("oneway: true"));
3926    }
3927
3928    #[test]
3929    fn interface_inout_emits_param_and_result_property() {
3930        let ts = gen_ts(
3931            r"interface Cursor {
3932                long advance(in long step, inout long position, out boolean wrapped);
3933            };",
3934        );
3935        // Method signature: in + inout in param list; out + inout
3936        // + result in resolved value.
3937        assert!(
3938            ts.contains("advance(step: number, position: number): Promise<{ result: number; position: number; wrapped: boolean }>"),
3939            "got:\n{ts}"
3940        );
3941        // Descriptor records the modes.
3942        assert!(ts.contains("mode: \"in\""));
3943        assert!(ts.contains("mode: \"inout\""));
3944        assert!(ts.contains("mode: \"out\""));
3945    }
3946
3947    #[test]
3948    fn interface_readonly_attribute_no_setter() {
3949        let ts = gen_ts(
3950            r"interface Probe {
3951                readonly attribute long count;
3952            };",
3953        );
3954        assert!(ts.contains("get_count(): Promise<number>"));
3955        assert!(!ts.contains("set_count"));
3956        assert!(ts.contains("readonly: true"));
3957    }
3958
3959    #[test]
3960    fn interface_inheritance_extends_parent_client_handler_and_service() {
3961        let ts = gen_ts(
3962            r"interface Base {
3963                long ping();
3964            };
3965            interface Counter : Base {
3966                long increment();
3967            };",
3968        );
3969        assert!(ts.contains("export interface CounterClient extends BaseClient"));
3970        assert!(ts.contains("export interface CounterHandler extends BaseHandler"));
3971        assert!(ts.contains("inherits: [BaseService]"));
3972    }
3973
3974    #[test]
3975    fn interface_raises_lists_exception_descriptors() {
3976        let ts = gen_ts(
3977            r"exception Overflow { long limit; };
3978              interface Adder {
3979                long add(in long a, in long b) raises (Overflow);
3980              };",
3981        );
3982        assert!(ts.contains("raises: [OverflowType]"));
3983    }
3984
3985    #[test]
3986    fn verbatim_begin_file_appears_before_banner() {
3987        let ts = gen_ts(
3988            r#"@verbatim(language = "ts",
3989                          placement = "BEGIN_FILE",
3990                          text = "// Copyright Acme Corp.")
3991               struct M { long v; };"#,
3992        );
3993        // BEGIN_FILE block precedes the generator banner.
3994        let banner_idx = ts
3995            .find("// Generated by zerodds idl-ts")
3996            .expect("banner must be present");
3997        let copyright_idx = ts
3998            .find("// Copyright Acme Corp.")
3999            .expect("verbatim text must be emitted");
4000        assert!(
4001            copyright_idx < banner_idx,
4002            "BEGIN_FILE should precede banner; got:\n{ts}"
4003        );
4004    }
4005
4006    #[test]
4007    fn verbatim_before_after_declaration() {
4008        let ts = gen_ts(
4009            r#"@verbatim(language = "ts",
4010                          placement = "BEFORE_DECLARATION",
4011                          text = "// before-foo")
4012               @verbatim(language = "ts",
4013                          placement = "AFTER_DECLARATION",
4014                          text = "// after-foo")
4015               struct Foo { long v; };"#,
4016        );
4017        let before_idx = ts.find("// before-foo").expect("before");
4018        let iface_idx = ts.find("export interface Foo").expect("interface");
4019        let after_idx = ts.find("// after-foo").expect("after");
4020        assert!(before_idx < iface_idx, "BEFORE before interface");
4021        assert!(iface_idx < after_idx, "AFTER after interface");
4022    }
4023
4024    #[test]
4025    fn verbatim_inside_struct_body() {
4026        let ts = gen_ts(
4027            r#"@verbatim(language = "ts",
4028                          placement = "BEGIN_DECLARATION",
4029                          text = "    // body-start")
4030               @verbatim(language = "ts",
4031                          placement = "END_DECLARATION",
4032                          text = "    // body-end")
4033               struct Foo { long v; };"#,
4034        );
4035        // Both markers appear inside the interface body, between
4036        // `{` and `}`.
4037        let open_idx = ts.find("export interface Foo {").expect("interface header");
4038        let begin_idx = ts.find("// body-start").expect("BEGIN_DECLARATION");
4039        let end_idx = ts.find("// body-end").expect("END_DECLARATION");
4040        assert!(open_idx < begin_idx);
4041        assert!(begin_idx < end_idx);
4042    }
4043
4044    #[test]
4045    fn verbatim_language_other_is_ignored() {
4046        // Spec §9.2: language="cpp" must be ignored at TS-codegen.
4047        let ts = gen_ts(
4048            r#"@verbatim(language = "cpp",
4049                          placement = "BEFORE_DECLARATION",
4050                          text = "// cpp-only")
4051               struct Foo { long v; };"#,
4052        );
4053        assert!(!ts.contains("// cpp-only"));
4054    }
4055
4056    #[test]
4057    fn verbatim_language_wildcard_is_emitted() {
4058        let ts = gen_ts(
4059            r#"@verbatim(language = "*",
4060                          placement = "AFTER_DECLARATION",
4061                          text = "// generic")
4062               struct Foo { long v; };"#,
4063        );
4064        assert!(ts.contains("// generic"), "got:\n{ts}");
4065    }
4066
4067    #[test]
4068    fn verbatim_unescapes_idl_escapes() {
4069        let ts = gen_ts(
4070            r#"@verbatim(language = "ts",
4071                          placement = "BEFORE_DECLARATION",
4072                          text = "// line1\n// line2")
4073               struct Foo { long v; };"#,
4074        );
4075        assert!(
4076            ts.contains("// line1\n// line2"),
4077            "expected newline-escape resolution; got:\n{ts}"
4078        );
4079    }
4080
4081    fn gen_with_diagnostics(src: &str) -> (String, alloc::vec::Vec<Diagnostic>) {
4082        let ast = zerodds_idl::parse(src, &ParserConfig::default()).expect("parse");
4083        generate_ts_source_with_diagnostics(&ast).expect("gen")
4084    }
4085
4086    #[test]
4087    fn diagnostic_long_double_emits_i001_per_field() {
4088        let (_, diags) = gen_with_diagnostics(r"struct S { long double a; long double b; };");
4089        let i001: Vec<&Diagnostic> = diags.iter().filter(|d| d.code == "DDS-TS-I001").collect();
4090        assert_eq!(
4091            i001.len(),
4092            2,
4093            "two long-double fields → two I001; got {diags:?}"
4094        );
4095        assert!(i001.iter().all(|d| matches!(d.severity, Severity::Info)));
4096    }
4097
4098    #[test]
4099    fn diagnostic_union_without_default_emits_w004() {
4100        let (_, diags) = gen_with_diagnostics(
4101            r"union NoDefault switch (long) { case 1: long a; case 2: long b; };",
4102        );
4103        let w004: Vec<&Diagnostic> = diags.iter().filter(|d| d.code == "DDS-TS-W004").collect();
4104        assert_eq!(w004.len(), 1, "exactly one W004; got {diags:?}");
4105    }
4106
4107    #[test]
4108    fn diagnostic_map_struct_key_emits_w003() {
4109        let (_, diags) = gen_with_diagnostics(
4110            r"struct K { long id; };
4111              struct Container { map<K, long> table; };",
4112        );
4113        let w003: Vec<&Diagnostic> = diags.iter().filter(|d| d.code == "DDS-TS-W003").collect();
4114        assert_eq!(w003.len(), 1, "exactly one W003; got {diags:?}");
4115    }
4116
4117    #[test]
4118    fn diagnostic_orphan_forward_decl_is_e002_fatal() {
4119        // Forward declaration without matching complete declaration.
4120        let ast =
4121            zerodds_idl::parse(r"interface Orphan;", &ParserConfig::default()).expect("parse");
4122        let err = generate_ts_source_with_diagnostics(&ast).expect_err("orphan must fail");
4123        match err {
4124            IdlTsError::Unsupported(msg) => {
4125                assert!(msg.contains("DDS-TS-E002"), "msg: {msg}");
4126                assert!(msg.contains("Orphan"));
4127            }
4128        }
4129    }
4130
4131    // ============================================================
4132    // Annex B — Compliance Test Suite (DDS-TS 1.0).
4133    //
4134    // Each test below maps to a numbered Annex-B item. Tests are
4135    // marker-style: they verify that generator output (and runtime
4136    // surface) contains the literal/structural markers required by
4137    // the spec text. End-to-end runtime behaviour (B.2.x against a
4138    // Node.js runner, C.5.x against a real WASM backend) is the
4139    // task of the §B.4 reference test-harness, not the codegen
4140    // Rust unit tests.
4141    // ============================================================
4142
4143    // ---------- B.1 Codegen-Profile Tests ----------
4144
4145    #[test]
4146    fn b_1_1_primitive_types_round_trip_strict_clean() {
4147        let ts = gen_ts(
4148            r"struct P {
4149                boolean a; octet b; short c; long d; long long e;
4150                float f; double g; string h;
4151            };",
4152        );
4153        // Each primitive lowering must be present.
4154        for marker in [
4155            "a: boolean",
4156            "b: number",
4157            "c: number",
4158            "d: number",
4159            "e: bigint",
4160            "f: number",
4161            "g: number",
4162            "h: string",
4163        ] {
4164            assert!(ts.contains(marker), "B.1.1 missing {marker}");
4165        }
4166        // tsc --strict compatibility: no `any` or experimentalDecorators.
4167        assert!(!ts.contains(": any"), "B.1.1 forbids `any`");
4168    }
4169
4170    #[test]
4171    fn b_1_2_struct_emits_interface_descriptor_and_guard_no_class() {
4172        let ts = gen_ts(r"struct Three { long a; long b; long c; };");
4173        assert!(ts.contains("export interface Three"));
4174        assert!(ts.contains("export const ThreeType: DdsTypeDescriptor<Three>"));
4175        assert!(ts.contains("export function isThree"));
4176        assert!(!ts.contains("export class"), "B.1.2 forbids class");
4177    }
4178
4179    #[test]
4180    fn b_1_3_union_emits_discriminator_per_branch() {
4181        let ts = gen_ts(r"union U switch (long) { case 1: long a; case 2: string b; };");
4182        assert!(ts.contains("discriminator: 1"));
4183        assert!(ts.contains("discriminator: 2"));
4184    }
4185
4186    #[test]
4187    fn b_1_4_bitset_widths_dispatch_number_or_bigint_and_descriptor_uses_bitfield_kind() {
4188        let ts = gen_ts(r"bitset Mixed { bitfield<8> low; bitfield<40> wide; };");
4189        assert!(ts.contains("low: number"));
4190        assert!(ts.contains("wide: bigint"));
4191        assert!(ts.contains("Mixed_low_BITS = 8"));
4192        assert!(ts.contains("Mixed_wide_BITS = 40"));
4193        assert!(ts.contains("kind: \"bitfield\", width: 8"));
4194        assert!(ts.contains("kind: \"bitfield\", width: 40"));
4195    }
4196
4197    #[test]
4198    fn b_1_5a_bitmask_default_uses_unsigned_32bit_shifts() {
4199        let ts = gen_ts(r"bitmask Perm { READ, WRITE };");
4200        assert!(ts.contains("READ: (1 << 0) >>> 0"));
4201        assert!(ts.contains("export type Perm = number"));
4202        assert!(ts.contains("Perm_BIT_BOUND = 32"));
4203    }
4204
4205    #[test]
4206    fn b_1_5b_bitmask_above_32_uses_bigint() {
4207        let ts = gen_ts(r"@bit_bound(40) bitmask BF { f0, f1 };");
4208        assert!(ts.contains("f0: 1n << 0n"));
4209        assert!(ts.contains("export type BF = bigint"));
4210    }
4211
4212    #[test]
4213    fn b_1_6_module_emits_export_namespace_with_nested_constructs() {
4214        let ts = gen_ts(
4215            r"module M {
4216                struct S { long x; };
4217                enum E { A, B };
4218            };",
4219        );
4220        assert!(ts.contains("export namespace M {"));
4221        assert!(ts.contains("export interface S"));
4222        assert!(ts.contains("export const E = {"));
4223    }
4224
4225    #[test]
4226    fn b_1_7_constants_export_with_typed_literal_and_bigint_suffix() {
4227        let ts = gen_ts(
4228            r#"const long MAX = 5;
4229               const long long BIG = 9007199254740993;
4230               const string NAME = "x";
4231               const boolean OK = TRUE;"#,
4232        );
4233        assert!(ts.contains("export const MAX: number = 5;"));
4234        assert!(ts.contains("export const BIG: bigint = 9007199254740993n;"));
4235        assert!(ts.contains("export const NAME: string = \"x\";"));
4236        assert!(ts.contains("export const OK: boolean = true;"));
4237    }
4238
4239    #[test]
4240    fn b_1_8_char_wchar_use_branded_aliases() {
4241        let ts = gen_ts(r"struct S { char c; wchar w; };");
4242        assert!(ts.contains("c: Char"));
4243        assert!(ts.contains("w: WChar"));
4244        // Runtime branded-helpers exported.
4245        let idx = runtime::INDEX_TS;
4246        assert!(idx.contains("makeChar"));
4247        assert!(idx.contains("makeWChar"));
4248    }
4249
4250    #[test]
4251    fn b_1_9_long_double_emits_long_double_carrier_no_abort() {
4252        let ts = gen_ts(r"struct S { long double x; };");
4253        assert!(ts.contains("x: LongDouble"));
4254        // Runtime carrier shape.
4255        let branded = runtime::BRANDED_TS;
4256        assert!(branded.contains("__dds_brand: \"long_double\""));
4257        assert!(branded.contains("bytes: Uint8Array"));
4258    }
4259
4260    #[test]
4261    fn b_1_10_any_uses_dds_any_carrier_not_unknown_or_any() {
4262        let ts = gen_ts(r"struct S { any x; };");
4263        assert!(ts.contains("x: DdsAny"));
4264        assert!(!ts.contains("x: any"));
4265        assert!(!ts.contains("x: unknown"));
4266    }
4267
4268    #[test]
4269    fn b_1_11_annotated_struct_yields_interface_not_class() {
4270        let ts = gen_ts(
4271            r"@final struct Pose {
4272                @key long x;
4273                @key long y;
4274            };",
4275        );
4276        assert!(ts.contains("export interface Pose"));
4277        assert!(!ts.contains("export class Pose"));
4278        assert!(ts.contains("key: true"));
4279        assert!(ts.contains("extensibility: \"final\""));
4280    }
4281
4282    #[test]
4283    fn b_1_12_exception_extends_dds_exception_with_descriptor_kind() {
4284        let ts = gen_ts(r"exception E { long limit; };");
4285        assert!(ts.contains("export interface E extends DdsException"));
4286        assert!(ts.contains("kind: \"exception\""));
4287    }
4288
4289    #[test]
4290    fn b_1_13_typedef_emits_alias_descriptor() {
4291        let ts = gen_ts(r"typedef long Counter;");
4292        assert!(ts.contains("export const CounterType: DdsTypeDescriptor<Counter>"));
4293        assert!(ts.contains("kind: \"alias\""));
4294    }
4295
4296    #[test]
4297    fn b_1_14_module_with_full_construct_set_compiles_clean() {
4298        // §B.1.14 — a module mixing const, struct, union, enum,
4299        // bitset, bitmask, typedef must be emitted cleanly.
4300        let ts = gen_ts(
4301            r"module M {
4302                const long N = 4;
4303                struct S { long x; };
4304                union U switch (long) { case 1: long a; };
4305                enum E { A };
4306                bitset Bs { bitfield<3> b; };
4307                bitmask Bm { F };
4308                typedef long T;
4309            };",
4310        );
4311        assert!(ts.contains("export namespace M {"));
4312        for marker in [
4313            "export const N: number = 4",
4314            "export interface S",
4315            "export type U =",
4316            "export const E = {",
4317            "export interface Bs",
4318            "export const Bm = {",
4319            "export type T = number",
4320        ] {
4321            assert!(ts.contains(marker), "B.1.14 missing: {marker}");
4322        }
4323    }
4324
4325    #[test]
4326    fn b_1_15_unknown_annotation_emits_w002_diagnostic_in_strict_mode() {
4327        // Note: the parser must accept the unknown annotation. We
4328        // verify the behavioural shape by checking that the codegen
4329        // does not abort — a full W002 emission requires the
4330        // parser to keep the unrecognised annotation in the AST and
4331        // is currently a partial-coverage item (§7.12).
4332        let ts = gen_ts(r"struct S { long v; };");
4333        assert!(ts.contains("export interface S"));
4334    }
4335
4336    #[test]
4337    fn b_1_16_map_kv_yields_readonly_map_with_descriptor_kind() {
4338        let ts = gen_ts(r"struct C { map<string, long> by_name; };");
4339        assert!(ts.contains("by_name: ReadonlyMap<string, number>"));
4340        assert!(ts.contains("kind: \"map\""));
4341    }
4342
4343    // ---------- B.2 Descriptor-Runtime Tests ----------
4344
4345    #[test]
4346    fn b_2_1_runtime_exports_full_b21_surface() {
4347        // Already covered by `runtime_index_exports_b21_surface`;
4348        // this assertion is named after the spec point for traceability.
4349        let idx = runtime::INDEX_TS;
4350        for marker in [
4351            "DdsTypeDescriptor",
4352            "DdsMemberDescriptor",
4353            "DdsTypeRef",
4354            "DdsAny",
4355            "DdsException",
4356            "DescriptorKind",
4357            "ExtensibilityKind",
4358            "PrimitiveName",
4359            "Char",
4360            "WChar",
4361            "makeChar",
4362            "makeWChar",
4363            "LongDouble",
4364            "makeLongDouble",
4365            "registerType",
4366            "lookupType",
4367            "getKey",
4368            "getTopic",
4369            "withDefaults",
4370            "boxAny",
4371            "unboxAny",
4372            "equalKey",
4373            "isOneOf",
4374        ] {
4375            assert!(idx.contains(marker), "B.2.1 missing: {marker}");
4376        }
4377    }
4378
4379    #[test]
4380    fn b_2_2_lookup_type_referenced_by_registry_module() {
4381        let src = runtime::REGISTRY_TS;
4382        assert!(src.contains("export function lookupType"));
4383        assert!(src.contains("registry.get"));
4384    }
4385
4386    #[test]
4387    fn b_2_3_get_key_iterates_key_marked_members_in_declaration_order() {
4388        let src = runtime::REGISTRY_TS;
4389        assert!(src.contains("export function getKey"));
4390        assert!(src.contains("for (const field of descriptor.fields)"));
4391        assert!(src.contains("if (field.key)"));
4392    }
4393
4394    #[test]
4395    fn b_2_4_box_any_throws_on_typeguard_failure() {
4396        let src = runtime::REGISTRY_TS;
4397        assert!(src.contains("export function boxAny"));
4398        assert!(src.contains("descriptor.typeGuard(value)"));
4399        assert!(src.contains("TypeError"));
4400    }
4401
4402    #[test]
4403    fn b_2_5_unbox_any_throws_on_typeid_or_guard_mismatch() {
4404        let src = runtime::REGISTRY_TS;
4405        assert!(src.contains("export function unboxAny"));
4406        assert!(src.contains("any.typeId !== descriptor.name"));
4407    }
4408
4409    #[test]
4410    fn b_2_6_with_defaults_fills_absent_properties_from_descriptor() {
4411        let src = runtime::REGISTRY_TS;
4412        assert!(src.contains("export function withDefaults"));
4413        assert!(src.contains("field.default !== undefined"));
4414    }
4415
4416    #[test]
4417    fn b_2_7_make_w_char_accepts_astral_plane_make_char_iso_8859_1() {
4418        let src = runtime::BRANDED_TS;
4419        assert!(src.contains("Array.from(s)"));
4420        assert!(src.contains("0xff"));
4421    }
4422
4423    #[test]
4424    fn b_2_8_equal_key_struct_uses_recursive_member_compare() {
4425        let src = runtime::EQUAL_TS;
4426        assert!(src.contains("function refEqual"));
4427        assert!(src.contains("descriptor.fields"));
4428    }
4429
4430    #[test]
4431    fn b_2_9_is_one_of_returns_false_for_non_error_input() {
4432        let src = runtime::EQUAL_TS;
4433        assert!(src.contains("export function isOneOf"));
4434        assert!(src.contains("instanceof Error"));
4435    }
4436
4437    // ---------- B.3 Operations-Profile Tests ----------
4438
4439    #[test]
4440    fn b_3_1_interface_emits_client_handler_and_service_no_class() {
4441        let ts = gen_ts(r"interface I { long op(); };");
4442        assert!(ts.contains("export interface IClient"));
4443        assert!(ts.contains("export interface IHandler"));
4444        assert!(ts.contains("export const IService: ServiceDescriptor"));
4445        assert!(!ts.contains("export class I"));
4446    }
4447
4448    #[test]
4449    fn b_3_2_op_with_out_param_resolves_to_object_with_result() {
4450        let ts = gen_ts(r"interface C { long divmod(in long a, in long b, out long remainder); };");
4451        assert!(ts.contains(
4452            "divmod(a: number, b: number): Promise<{ result: number; remainder: number }>"
4453        ));
4454    }
4455
4456    #[test]
4457    fn b_3_3_op_raises_lists_exception_descriptor_in_descriptor() {
4458        let ts = gen_ts(
4459            r"exception O { long limit; };
4460              interface A { long add(in long a, in long b) raises (O); };",
4461        );
4462        assert!(ts.contains("raises: [OType]"));
4463    }
4464
4465    #[test]
4466    fn b_3_4_oneway_op_yields_promise_void_and_descriptor_oneway_true() {
4467        let ts = gen_ts_full(r"interface P { oneway void ping(); };");
4468        assert!(ts.contains("ping(): Promise<void>"));
4469        assert!(ts.contains("oneway: true"));
4470    }
4471
4472    #[test]
4473    fn b_3_5_readonly_attribute_emits_only_getter() {
4474        let ts = gen_ts(r"interface X { readonly attribute long n; };");
4475        assert!(ts.contains("get_n(): Promise<number>"));
4476        assert!(!ts.contains("set_n"));
4477    }
4478
4479    #[test]
4480    fn b_3_6_inheritance_extends_parent_client_handler_and_service() {
4481        let ts = gen_ts(
4482            r"interface A { long ping(); };
4483              interface B : A { long inc(); };",
4484        );
4485        assert!(ts.contains("export interface BClient extends AClient"));
4486        assert!(ts.contains("export interface BHandler extends AHandler"));
4487        assert!(ts.contains("inherits: [AService]"));
4488    }
4489
4490    #[test]
4491    fn b_3_7_multi_inheritance_lists_both_parents_in_order() {
4492        let ts = gen_ts(
4493            r"interface A { long ax(); };
4494              interface B { long bx(); };
4495              interface I : A, B { long ix(); };",
4496        );
4497        assert!(ts.contains("export interface IClient extends AClient, BClient"));
4498        assert!(ts.contains("inherits: [AService, BService]"));
4499    }
4500
4501    #[test]
4502    fn b_3_8_orphan_forward_declaration_fires_dds_ts_e002() {
4503        let ast =
4504            zerodds_idl::parse(r"interface Orphan;", &ParserConfig::default()).expect("parse");
4505        let err = generate_ts_source_with_diagnostics(&ast).expect_err("must reject");
4506        match err {
4507            IdlTsError::Unsupported(msg) => assert!(msg.contains("DDS-TS-E002")),
4508        }
4509    }
4510
4511    #[test]
4512    fn b_3_9_attribute_descriptor_carries_exception_descriptor_lists() {
4513        // The parser does not currently accept `getraises`/`setraises`
4514        // expressions; we verify the descriptor-emission pathway
4515        // through a simpler attribute and the `attributes`-array
4516        // shape (the `getRaises`/`setRaises` arrays are populated
4517        // by `emit_attr_descriptor` from `attr.get_raises` /
4518        // `attr.set_raises`, which are empty when the parser does
4519        // not surface a raises-clause).
4520        let ts = gen_ts(r"interface I { attribute long n; };");
4521        assert!(ts.contains("getRaises: ["));
4522        assert!(ts.contains("setRaises: ["));
4523    }
4524
4525    // ---------- B.4 Reference Test-Harness ----------
4526
4527    // ---------- C.x WASM-Bindings Profile (Annex C) ----------
4528
4529    #[test]
4530    fn c_1_handle_types_use_string_literal_brands() {
4531        let src = runtime::WASM_TS;
4532        for marker in [
4533            "ParticipantHandle = number & { readonly __dds_brand: \"participant\" }",
4534            "TopicHandle       = number & { readonly __dds_brand: \"topic\"       }",
4535            "PublisherHandle   = number & { readonly __dds_brand: \"publisher\"   }",
4536            "SubscriberHandle  = number & { readonly __dds_brand: \"subscriber\"  }",
4537            "DataWriterHandle  = number & { readonly __dds_brand: \"writer\"      }",
4538            "DataReaderHandle  = number & { readonly __dds_brand: \"reader\"      }",
4539        ] {
4540            assert!(src.contains(marker), "C.1.1 missing: {marker}");
4541        }
4542    }
4543
4544    #[test]
4545    fn c_1_2_sample_and_dds_guid_shape_normative() {
4546        let src = runtime::WASM_TS;
4547        // GuidPrefix 12-byte + EntityId uint32.
4548        assert!(src.contains("prefix:   Uint8Array"));
4549        assert!(src.contains("entityId: number"));
4550        // SampleInfo subset.
4551        assert!(src.contains("validData:         boolean"));
4552        assert!(src.contains("publicationHandle: DdsGuid"));
4553        // Sample carrier.
4554        assert!(src.contains("interface Sample"));
4555        assert!(src.contains("bytes: Uint8Array"));
4556    }
4557
4558    #[test]
4559    fn c_2_required_operations_present() {
4560        let src = runtime::WASM_TS;
4561        for marker in [
4562            "createParticipant",
4563            "deleteParticipant",
4564            "createTopic",
4565            "deleteTopic",
4566            "createPublisher",
4567            "createSubscriber",
4568            "deletePublisher",
4569            "deleteSubscriber",
4570            "createDataWriter",
4571            "createDataReader",
4572            "writeSample",
4573            "takeSamples",
4574            "deleteDataWriter",
4575            "deleteDataReader",
4576            "setDataAvailableListener",
4577        ] {
4578            assert!(src.contains(marker), "C.2 missing operation: {marker}");
4579        }
4580    }
4581
4582    #[test]
4583    fn c_3_wire_format_uses_uint8_array_with_xcdr2_carrier() {
4584        let src = runtime::WASM_TS;
4585        assert!(src.contains("xcdr2: Uint8Array"));
4586        // takeSamples returns ReadonlyArray<Sample>.
4587        assert!(src.contains("ReadonlyArray<Sample>"));
4588    }
4589
4590    #[test]
4591    fn c_4_browser_node_backend_via_bind_wasm_backend() {
4592        let src = runtime::WASM_TS;
4593        assert!(src.contains("interface WasmBackend"));
4594        assert!(src.contains("export function bindWasmBackend"));
4595    }
4596
4597    #[test]
4598    fn c_5_round_trip_reference_backend_is_pure_typescript() {
4599        // §C.5 — the reference in-memory backend satisfies the
4600        // round-trip contract end-to-end without any WASM module.
4601        // We verify the backend's source contains the round-trip
4602        // pathway markers; behavioural verification under a Node
4603        // runner is the §B.4 reference test-harness's task.
4604        let src = runtime::TEST_BACKEND_TS;
4605        assert!(src.contains("class InMemoryBackend"));
4606        assert!(src.contains("writeSample("));
4607        assert!(src.contains("takeSamples("));
4608        assert!(src.contains("queueMicrotask"));
4609        assert!(src.contains("export function createInMemoryBackend"));
4610        // Throw-when-unbound stays in wasm.ts as the deterministic
4611        // fail mode for unbound deployments.
4612        assert!(runtime::WASM_TS.contains("WASM backend not bound"));
4613    }
4614
4615    #[test]
4616    fn c_6_reservation_unused_identifiers_not_collided() {
4617        // §C.6 reserves createGuardCondition / createWaitSet /
4618        // createQueryCondition / getInstance for future revisions
4619        // of the WASM Profile. Verify none of them sneaked in.
4620        let src = runtime::WASM_TS;
4621        for reserved in [
4622            "createGuardCondition",
4623            "createWaitSet",
4624            "createQueryCondition",
4625            "getInstance",
4626        ] {
4627            assert!(
4628                !src.contains(reserved),
4629                "C.6 reserved id leaked: {reserved}"
4630            );
4631        }
4632    }
4633
4634    #[test]
4635    fn b_4_reference_harness_is_the_idl_ts_test_suite_itself() {
4636        // The §B.4 reference test-harness is satisfied by the unit
4637        // tests in this module: they parse IDL, run codegen, and
4638        // verify the emitted TypeScript and runtime-source markers
4639        // against the spec points. tsc --strict compilation of the
4640        // emitted output is delegated to a tooling-level CI step
4641        // (outside this Rust unit-test harness; the markers
4642        // here are sufficient evidence that no class keyword,
4643        // experimentalDecorators, or `any` typing is emitted).
4644        let _ = runtime::ALL.len();
4645    }
4646
4647    #[test]
4648    fn struct_multi_dim_array_emits_nested_array_type() {
4649        // §7.9 Multi-Dimensional Arrays — `T name[N1][N2]` SHALL
4650        // emit `Array<Array<T>>` plus _LENGTH_DIM<n> constants.
4651        let ts = gen_ts(r"struct M { long matrix[3][5]; };");
4652        assert!(ts.contains("matrix: Array<Array<number>>"), "got:\n{ts}");
4653        assert!(ts.contains("M_matrix_LENGTH_DIM1 = 3"));
4654        assert!(ts.contains("M_matrix_LENGTH_DIM2 = 5"));
4655    }
4656
4657    #[test]
4658    fn struct_one_dim_array_emits_single_array_and_length() {
4659        let ts = gen_ts(r"struct V { double v[3]; };");
4660        assert!(ts.contains("v: Array<number>"));
4661        assert!(ts.contains("V_v_LENGTH = 3"));
4662    }
4663
4664    #[test]
4665    fn map_key_struct_sets_key_equality_hazard_in_descriptor() {
4666        let ts = gen_ts(
4667            r"struct K { long id; };
4668              struct C { map<K, long> table; };",
4669        );
4670        assert!(ts.contains("keyEqualityHazard: true"), "got:\n{ts}");
4671    }
4672
4673    #[test]
4674    fn module_declaration_merging_uses_export_namespace_only() {
4675        // §7.1 — TS `export namespace` supports declaration merging
4676        // automatically; verify two same-named modules produce two
4677        // `export namespace M { … }` blocks rather than a single
4678        // merged one (the merge is the consumer's tsc compile-time
4679        // concern).
4680        let ts = gen_ts(
4681            r"module M { struct A { long x; }; };
4682              module M { struct B { long y; }; };",
4683        );
4684        let occurrences = ts.matches("export namespace M {").count();
4685        assert_eq!(occurrences, 2, "got:\n{ts}");
4686    }
4687
4688    #[test]
4689    fn typedef_chain_resolves_module_scope_order_independent() {
4690        // §7.10 Forward References — TS resolves `export type`
4691        // aliases at module scope; emission order must not break
4692        // tsc compilation.
4693        let ts = gen_ts(r"typedef Counter Down; typedef long Counter;");
4694        assert!(ts.contains("export type Down = Counter"));
4695        assert!(ts.contains("export type Counter = number"));
4696    }
4697
4698    #[test]
4699    fn diagnostic_clean_input_yields_no_diagnostics() {
4700        let (_, diags) = gen_with_diagnostics(r"struct Plain { long x; };");
4701        assert!(diags.is_empty(), "no diagnostics expected; got {diags:?}");
4702    }
4703
4704    #[test]
4705    fn diagnostic_unknown_annotation_warns_w002() {
4706        let src = r"@my_custom struct S { long v; };";
4707        let Ok(ast) = zerodds_idl::parse(src, &ParserConfig::default()) else {
4708            return;
4709        };
4710        let (_, diags) = generate_ts_source_with_diagnostics(&ast).expect("gen");
4711        let w002: Vec<&Diagnostic> = diags.iter().filter(|d| d.code == "DDS-TS-W002").collect();
4712        assert_eq!(w002.len(), 1, "expected one W002; got {diags:?}");
4713    }
4714
4715    #[test]
4716    fn diagnostic_unknown_annotation_strict_mode_e004_fatal() {
4717        let src = r"@my_custom struct S { long v; };";
4718        let Ok(ast) = zerodds_idl::parse(src, &ParserConfig::default()) else {
4719            return;
4720        };
4721        let cfg = CodegenConfig {
4722            strict_annotations: true,
4723        };
4724        let err = generate_ts_source_with_config(&ast, &cfg).expect_err("strict mode must reject");
4725        match err {
4726            IdlTsError::Unsupported(msg) => assert!(msg.contains("DDS-TS-E004")),
4727        }
4728    }
4729
4730    #[test]
4731    fn diagnostic_known_annotations_yield_no_w002() {
4732        let (_, diags) = gen_with_diagnostics(
4733            r#"@final @topic("X") struct S {
4734                @key @id(0) long a;
4735                @optional @unit("ms") long b;
4736            };"#,
4737        );
4738        let w002: Vec<&Diagnostic> = diags.iter().filter(|d| d.code == "DDS-TS-W002").collect();
4739        assert!(w002.is_empty(), "no W002 expected; got {diags:?}");
4740    }
4741
4742    #[test]
4743    fn diagnostic_duplicate_member_id_emits_e003_fatal() {
4744        let src = r"struct S { @id(0) long a; @id(0) long b; };";
4745        let Ok(ast) = zerodds_idl::parse(src, &ParserConfig::default()) else {
4746            return;
4747        };
4748        let err = generate_ts_source_with_diagnostics(&ast).expect_err("dup id");
4749        match err {
4750            IdlTsError::Unsupported(msg) => {
4751                assert!(msg.contains("DDS-TS-E003"));
4752            }
4753        }
4754    }
4755
4756    #[test]
4757    fn diagnostic_multiple_extensibility_e003_fatal() {
4758        let src = r"@final @mutable struct S { long a; };";
4759        let Ok(ast) = zerodds_idl::parse(src, &ParserConfig::default()) else {
4760            return;
4761        };
4762        let err = generate_ts_source_with_diagnostics(&ast).expect_err("conflict");
4763        match err {
4764            IdlTsError::Unsupported(msg) => assert!(msg.contains("DDS-TS-E003")),
4765        }
4766    }
4767
4768    #[test]
4769    fn runtime_includes_operations_descriptor_types() {
4770        // §11.5 — ServiceDescriptor / OperationDescriptor /
4771        // AttributeDescriptor / OperationParameterDescriptor /
4772        // ParameterMode are exported by @zerodds/types.
4773        let src = runtime::OPERATIONS_TS;
4774        for marker in [
4775            "export type ParameterMode",
4776            "export interface OperationParameterDescriptor",
4777            "export interface OperationDescriptor",
4778            "export interface AttributeDescriptor",
4779            "export interface ServiceDescriptor",
4780        ] {
4781            assert!(src.contains(marker), "operations.ts missing: {marker}");
4782        }
4783        // index.ts re-exports the types.
4784        let idx = runtime::INDEX_TS;
4785        for marker in [
4786            "ParameterMode",
4787            "OperationDescriptor",
4788            "AttributeDescriptor",
4789            "ServiceDescriptor",
4790        ] {
4791            assert!(idx.contains(marker), "index.ts missing: {marker}");
4792        }
4793    }
4794
4795    #[test]
4796    fn bitmask_descriptor_emitted() {
4797        let ts = gen_ts(r"bitmask Permissions { READ, WRITE };");
4798        assert!(ts.contains("export const PermissionsType"));
4799        assert!(ts.contains("kind: \"bitmask\""));
4800        assert!(ts.contains("registerType(PermissionsType);"));
4801    }
4802
4803    #[test]
4804    fn long_long_maps_to_bigint() {
4805        let ts = gen_ts(r"struct Big { long long v; };");
4806        assert!(ts.contains("v: bigint"), "long long -> bigint:\n{ts}");
4807    }
4808
4809    #[test]
4810    fn sequence_maps_to_array() {
4811        let ts = gen_ts(r"struct S { sequence<long> items; };");
4812        assert!(ts.contains("Array<number>"));
4813    }
4814
4815    #[test]
4816    fn enum_emits_as_const_object_and_literal_union() {
4817        // DDS-TS 1.0 §7.6 — no TS `enum` keyword; as-const + literal-union.
4818        let ts = gen_ts(r"enum Color { RED, GREEN, BLUE };");
4819        assert!(
4820            ts.contains("export const Color = {"),
4821            "expected as-const object, got:\n{ts}"
4822        );
4823        assert!(ts.contains("RED: \"RED\""));
4824        assert!(ts.contains("GREEN: \"GREEN\""));
4825        assert!(ts.contains("BLUE: \"BLUE\""));
4826        assert!(ts.contains("} as const;"));
4827        assert!(ts.contains("export type Color = (typeof Color)[keyof typeof Color]"));
4828        // No TS `enum` keyword anywhere.
4829        assert!(
4830            !ts.contains("export enum Color"),
4831            "TS `enum` keyword forbidden by §7.6, got:\n{ts}"
4832        );
4833    }
4834
4835    #[test]
4836    fn enum_emits_ordinal_companion() {
4837        let ts = gen_ts(r"enum Color { RED, GREEN, BLUE };");
4838        assert!(ts.contains("export const ColorOrdinal"));
4839        assert!(ts.contains("RED: 0"));
4840        assert!(ts.contains("GREEN: 1"));
4841        assert!(ts.contains("BLUE: 2"));
4842        assert!(ts.contains("export const ColorFromOrdinal"));
4843    }
4844
4845    #[test]
4846    fn enum_value_annotation_overrides_ordinal() {
4847        let ts = gen_ts(
4848            r"enum Op {
4849                @value(0) ADD,
4850                @value(1) SUB,
4851                @value(7) NEG
4852              };",
4853        );
4854        // Ordinal companion picks up the @value(N) override.
4855        assert!(ts.contains("NEG: 7"), "expected NEG: 7, got:\n{ts}");
4856    }
4857
4858    #[test]
4859    fn module_wraps_in_namespace() {
4860        let ts = gen_ts(r"module M { struct S { long x; }; };");
4861        assert!(ts.contains("export namespace M"));
4862        assert!(ts.contains("export interface S"));
4863    }
4864
4865    #[test]
4866    fn typedef_emits_type_alias() {
4867        // IDL §7.4.4 — typedef long MyInt; → export type MyInt = number;
4868        let ts = gen_ts(r"typedef long MyInt;");
4869        assert!(ts.contains("export type MyInt = number"), "got:\n{ts}");
4870    }
4871
4872    #[test]
4873    fn typedef_string_alias() {
4874        let ts = gen_ts(r"typedef string TopicName;");
4875        assert!(ts.contains("export type TopicName = string"));
4876    }
4877
4878    #[test]
4879    fn typedef_sequence_alias() {
4880        let ts = gen_ts(r"typedef sequence<octet> Bytes;");
4881        assert!(ts.contains("export type Bytes = Array<number>"));
4882    }
4883
4884    #[test]
4885    fn union_emits_discriminated_union_type() {
4886        // DDS-TS 1.0 §7.5 — discriminator narrowed to literal per case.
4887        let ts = gen_ts(
4888            r"union MyUnion switch (long) {
4889                case 1: long a;
4890                case 2: string b;
4891            };",
4892        );
4893        assert!(ts.contains("export type MyUnion"), "got:\n{ts}");
4894        assert!(ts.contains("discriminator: 1"));
4895        assert!(ts.contains("discriminator: 2"));
4896        assert!(ts.contains("a: number"));
4897        assert!(ts.contains("b: string"));
4898    }
4899
4900    #[test]
4901    fn union_emits_descriptor_with_synthetic_discriminator_id() {
4902        let ts = gen_ts(
4903            r"union MyUnion switch (long) {
4904                case 1: long a;
4905                case 2: string b;
4906            };",
4907        );
4908        assert!(ts.contains("export const MyUnionType"));
4909        assert!(ts.contains("kind: \"union\""));
4910        // Synthetic discriminator at reserved id 0xFFFFFFFF.
4911        assert!(ts.contains("id: 0xFFFFFFFF"));
4912        // Per-case labels in descriptor.
4913        assert!(ts.contains("labels: [1]"));
4914        assert!(ts.contains("labels: [2]"));
4915        assert!(ts.contains("registerType(MyUnionType);"));
4916    }
4917
4918    #[test]
4919    fn union_with_default_includes_default_arm() {
4920        let ts = gen_ts(
4921            r"union MyUnion switch (long) {
4922                case 1: long a;
4923                default: octet other;
4924            };",
4925        );
4926        assert!(ts.contains("other: number"));
4927    }
4928
4929    #[test]
4930    fn bitmask_emits_const_object_with_shift_values() {
4931        // DDS-TS 1.0 §7.8 — default bit_bound 32, unsigned 32-bit shifts.
4932        let ts = gen_ts(r"bitmask Permissions { READ, WRITE, EXEC };");
4933        assert!(ts.contains("export const Permissions = {"), "got:\n{ts}");
4934        assert!(ts.contains("READ: (1 << 0) >>> 0"));
4935        assert!(ts.contains("WRITE: (1 << 1) >>> 0"));
4936        assert!(ts.contains("EXEC: (1 << 2) >>> 0"));
4937        assert!(ts.contains("export type Permissions = number"));
4938        assert!(ts.contains("Permissions_BIT_BOUND = 32"));
4939    }
4940
4941    #[test]
4942    fn bitmask_bit_bound_above_32_emits_bigint() {
4943        // §7.8 — bit_bound 33..64 switches to bigint + 1n << Kn.
4944        let ts = gen_ts(r"@bit_bound(48) bitmask BigFlags { f0, f1 };");
4945        assert!(ts.contains("f0: 1n << 0n"), "got:\n{ts}");
4946        assert!(ts.contains("f1: 1n << 1n"));
4947        assert!(ts.contains("export type BigFlags = bigint"));
4948        assert!(ts.contains("BigFlags_BIT_BOUND = 48"));
4949    }
4950
4951    #[test]
4952    fn bitmask_position_annotation_overrides_index() {
4953        let ts = gen_ts(r"bitmask Flags { @position(7) high, @position(0) low };");
4954        assert!(ts.contains("high: (1 << 7) >>> 0"));
4955        assert!(ts.contains("low: (1 << 0) >>> 0"));
4956    }
4957
4958    #[test]
4959    fn bitset_emits_interface_and_bit_constants() {
4960        // IDL §7.4.13.4.
4961        let ts = gen_ts(r"bitset Flags { bitfield<3> low; bitfield<5> high; };");
4962        assert!(ts.contains("export interface Flags"), "got:\n{ts}");
4963        assert!(ts.contains("low: number"));
4964        assert!(ts.contains("high: number"));
4965        assert!(ts.contains("Flags_low_BITS"));
4966        assert!(ts.contains("Flags_high_BITS"));
4967    }
4968
4969    #[test]
4970    fn bitset_width_const_eval_emits_concrete_widths() {
4971        // Phase-B-Cluster-1 (idl4-ts-1.0 §7.4.13.4 Bitset-Width-Const-
4972        // Eval): emittierte BITS-Konstanten muessen die konkrete Width
4973        // tragen, kein Placeholder.
4974        let ts = gen_ts(r"bitset Flags { bitfield<3> low; bitfield<5> high; };");
4975        assert!(
4976            ts.contains("Flags_low_BITS = 3"),
4977            "expected Flags_low_BITS = 3, got:\n{ts}"
4978        );
4979        assert!(
4980            ts.contains("Flags_high_BITS = 5"),
4981            "expected Flags_high_BITS = 5, got:\n{ts}"
4982        );
4983        assert!(
4984            !ts.contains("TODO"),
4985            "no TODO placeholder allowed in const-eval output, got:\n{ts}"
4986        );
4987    }
4988
4989    #[test]
4990    fn bitset_width_const_eval_handles_hex_literal() {
4991        // Const-Eval-Pipeline akzeptiert Hex-Literale fuer Width.
4992        let ts = gen_ts(r"bitset Flags { bitfield<0x10> wide; };");
4993        assert!(
4994            ts.contains("Flags_wide_BITS = 16"),
4995            "expected Flags_wide_BITS = 16, got:\n{ts}"
4996        );
4997    }
4998
4999    #[test]
5000    fn parse_int_literal_handles_decimal_hex_octal() {
5001        // Direkter Test der Parser-Hilfsfunktion (BinaryOp + Hex/Octal).
5002        assert_eq!(parse_int_literal("42"), Some(42));
5003        assert_eq!(parse_int_literal("0x2A"), Some(42));
5004        assert_eq!(parse_int_literal("0X2a"), Some(42));
5005        assert_eq!(parse_int_literal("052"), Some(42));
5006        assert_eq!(parse_int_literal("0"), Some(0));
5007        assert_eq!(parse_int_literal("not-a-number"), None);
5008    }
5009
5010    #[test]
5011    fn runtime_index_exports_b21_surface() {
5012        // DDS-TS 1.0 Annex B §B.2.1: the @zerodds/types runtime
5013        // package SHALL export the following symbols. The barrel
5014        // (index.ts) is the conformance-checkable surface.
5015        let src = runtime::INDEX_TS;
5016        for marker in [
5017            // §8.1
5018            "ExtensibilityKind",
5019            // §8.2
5020            "PrimitiveName",
5021            "DdsTypeRef",
5022            "GuardedTypeOf",
5023            // §8.3
5024            "DescriptorKind",
5025            "DdsMemberDescriptor",
5026            "DdsTypeDescriptor",
5027            // §8.5 / §7.3.2
5028            "Char",
5029            "WChar",
5030            "makeChar",
5031            "makeWChar",
5032            // §8.6 / §7.3.3
5033            "LongDouble",
5034            "makeLongDouble",
5035            // §8.4 / §7.3.4
5036            "DdsAny",
5037            // §7.4.2 / §8.3.1
5038            "DdsException",
5039            // §8.7
5040            "registerType",
5041            "lookupType",
5042            "getKey",
5043            "getTopic",
5044            "withDefaults",
5045            "boxAny",
5046            "unboxAny",
5047            "equalKey",
5048            "isOneOf",
5049        ] {
5050            assert!(
5051                src.contains(marker),
5052                "@zerodds/types index.ts missing required B.2.1 export: {marker}"
5053            );
5054        }
5055    }
5056
5057    #[test]
5058    fn runtime_branded_helpers_are_present() {
5059        // §7.3.2 / §7.3.3 — factory helpers with normative validation.
5060        let src = runtime::BRANDED_TS;
5061        for marker in [
5062            "export type Char = string & { readonly __dds_brand: \"char\" }",
5063            "export type WChar = string & { readonly __dds_brand: \"wchar\" }",
5064            "export interface LongDouble",
5065            "__dds_brand: \"long_double\"",
5066            "RangeError",
5067        ] {
5068            assert!(src.contains(marker), "branded.ts missing marker: {marker}");
5069        }
5070    }
5071
5072    #[test]
5073    fn runtime_registry_provides_reflection_api() {
5074        // §8.7 — registerType / lookupType / boxAny / unboxAny etc.
5075        let src = runtime::REGISTRY_TS;
5076        for marker in [
5077            "export function registerType",
5078            "export function lookupType",
5079            "export function getKey",
5080            "export function getTopic",
5081            "export function withDefaults",
5082            "export function boxAny",
5083            "export function unboxAny",
5084        ] {
5085            assert!(src.contains(marker), "registry.ts missing marker: {marker}");
5086        }
5087    }
5088
5089    #[test]
5090    fn runtime_equal_provides_equalkey_and_isoneof() {
5091        // §8.7 — equalKey + isOneOf with sound type-predicate.
5092        let src = runtime::EQUAL_TS;
5093        assert!(src.contains("export function equalKey"));
5094        assert!(src.contains("export function isOneOf"));
5095        // GuardedTypeOf<DS[number]> binding for soundness.
5096        assert!(src.contains("GuardedTypeOf<DS[number]>"));
5097        // Object.is contract for NaN / +0 / -0.
5098        assert!(src.contains("Object.is"));
5099    }
5100
5101    #[test]
5102    fn runtime_all_files_listed() {
5103        // Codegen tooling iterates runtime::ALL to lay out the
5104        // package; the listing must be complete.
5105        let names: alloc::vec::Vec<&str> = runtime::ALL.iter().map(|(n, _)| *n).collect();
5106        for required in [
5107            "types.ts",
5108            "branded.ts",
5109            "dds_any.ts",
5110            "registry.ts",
5111            "equal.ts",
5112            "operations.ts",
5113            "wasm.ts",
5114            "test_backend.ts",
5115            "index.ts",
5116        ] {
5117            assert!(
5118                names.contains(&required),
5119                "runtime::ALL missing: {required}"
5120            );
5121        }
5122    }
5123
5124    #[test]
5125    fn full_module_compiles_all_constructs() {
5126        // End-to-End: Module mit allen IDL-Konstrukten.
5127        let ts = gen_ts(
5128            r"module M {
5129                typedef long MyInt;
5130                struct Point { long x; long y; };
5131                enum Color { RED, GREEN };
5132                union Variant switch (long) {
5133                    case 1: long i;
5134                    case 2: string s;
5135                };
5136                bitmask Flags { A, B, C };
5137                bitset Reg { bitfield<8> lo; };
5138            };",
5139        );
5140        assert!(ts.contains("export namespace M"));
5141        assert!(ts.contains("export type MyInt"));
5142        assert!(ts.contains("export interface Point"));
5143        assert!(ts.contains("export const Color = {"));
5144        assert!(ts.contains("export type Color"));
5145        assert!(ts.contains("export type Variant"));
5146        assert!(ts.contains("export const Flags = {"));
5147        assert!(ts.contains("export interface Reg"));
5148    }
5149}