1#![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
54pub fn generate_ts_source(spec: &Specification) -> Result<String, IdlTsError> {
67 let (out, _diagnostics) = generate_ts_source_with_diagnostics(spec)?;
68 Ok(out)
69}
70
71pub 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
90pub struct CodegenConfig {
91 pub strict_annotations: bool,
94}
95
96pub 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
111pub 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 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 scan_annotation_conflicts(&spec.definitions, &mut diagnostics)?;
146 scan_unknown_annotations(&spec.definitions, &mut diagnostics, config)?;
147
148 check_forward_declaration_orphans(&spec.definitions, &mut diagnostics)?;
152
153 scan_long_double_uses(&spec.definitions, &mut diagnostics);
155
156 scan_union_implicit_defaults(&spec.definitions, &mut diagnostics);
160
161 scan_map_key_hazards(&spec.definitions, &mut diagnostics);
163
164 Ok((out, diagnostics))
165}
166
167#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct Diagnostic {
174 pub code: &'static str,
176 pub severity: Severity,
178 pub message: String,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub enum Severity {
185 Fatal,
187 Warning,
189 Info,
191}
192
193const 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#[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
240fn 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
265const KNOWN_ANNOTATIONS: &[&str] = &[
269 "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 "shared",
292 "external",
293 "ami",
294 "service",
295 "oneway",
296 "amicallback",
297 "ignore_literal_names",
299 "data_representation",
300 "extensibility",
301];
302
303fn 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 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
455fn 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 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 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 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 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
564fn 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; return Err(IdlTsError::Unsupported(msg));
589 }
590 }
591 Ok(())
592}
593
594fn 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
617fn 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
673fn 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 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
719fn 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
756fn 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
778fn 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
833fn 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
860fn 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#[allow(dead_code)] fn 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
885fn 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 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 let _ = f;
910 Ok(())
911 }
912 Definition::Module(m) => {
913 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 let _ = diagnostics;
927 Ok(())
928 }
929 _ => Ok(()),
930 };
931
932 emit_verbatim_at(out, anns, VerbatimPlacement::AfterDeclaration);
934
935 res
936}
937
938fn 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; 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
979fn const_expr_to_ts_value(e: &zerodds_idl::ast::ConstExpr, want_bigint: bool) -> Option<String> {
982 use zerodds_idl::ast::{ConstExpr, LiteralKind};
983 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
1033fn emit_exception(out: &mut String, e: &zerodds_idl::ast::ExceptDecl) -> Result<(), IdlTsError> {
1040 let name = &e.name.text;
1041
1042 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 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 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
1143fn emit_enum(out: &mut String, e: &zerodds_idl::ast::EnumDef) -> Result<(), IdlTsError> {
1171 let name = &e.name.text;
1172
1173 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 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 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 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
1248fn 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
1262fn 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
1269fn 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
1295fn 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
1307fn 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 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 emit_struct_bound_constants(out, s)?;
1362 emit_struct_default_constants(out, s)?;
1363
1364 emit_struct_type_guard(out, s)?;
1366
1367 emit_struct_descriptor(out, s)?;
1369
1370 out.push_str(&alloc::format!("registerType({name}Type);\n\n"));
1372
1373 emit_struct_typesupport(out, s, module_path)?;
1375 Ok(())
1376}
1377
1378fn 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 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 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 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
1447fn 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
1480fn 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 FloatingType::LongDouble => "typeof VAR !== \"object\" || VAR === null".into(),
1501 },
1502 },
1503 TypeSpec::String(_) => "typeof VAR !== \"string\"".into(),
1504 TypeSpec::Sequence(_)
1507 | TypeSpec::Map(_)
1508 | TypeSpec::Scoped(_)
1509 | TypeSpec::Any
1510 | TypeSpec::Fixed(_) => return None,
1511 })
1512}
1513
1514fn 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 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
1611fn 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 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 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 out.push_str(&alloc::format!(" keyHash(s: {name}): Uint8Array {{\n"));
1657 if has_key {
1658 out.push_str(" const w = new Xcdr2Writer(\"be\");\n");
1660 emit_struct_keyhash_body(out, s, " ")?;
1661 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; 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
1685fn 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 _ => {}
1747 }
1748 Ok(())
1749}
1750
1751fn 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 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
1779fn 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 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
1828fn 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}
1859fn 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 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 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 out.push_str(&alloc::format!(
1945 "{indent}w.writeInt32({expr} as unknown as number);\n"
1946 ));
1947 }
1948 TypeSpec::Any => {
1949 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
1964fn 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 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 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 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 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 _ => {}
2079 }
2080 Ok(())
2081}
2082
2083fn 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
2106fn 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
2134fn emit_decode_return(
2136 out: &mut String,
2137 s: &zerodds_idl::ast::StructDef,
2138 indent: &str,
2139 name: &str,
2140) -> Result<(), IdlTsError> {
2141 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}
2163fn 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 "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
2231fn 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
2250fn 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
2330fn 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 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
2399fn 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
2435fn 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 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
2514fn 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 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 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
2621fn 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
2659fn emit_bitset(out: &mut String, b: &zerodds_idl::ast::BitsetDecl) -> Result<(), IdlTsError> {
2667 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 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 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
2756fn 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 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 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
2829fn 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 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 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 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
2944fn is_integer_typespec(t: &TypeSpec) -> bool {
2946 matches!(t, TypeSpec::Primitive(PrimitiveType::Integer(_)))
2947}
2948
2949fn 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
2968fn 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 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 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 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 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 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 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 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
3111fn 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 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 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
3167fn 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
3187fn 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
3233fn 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
3278fn 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
3296fn 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
3346fn 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 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
3381fn 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 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 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
3429fn 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
3456fn 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
3469pub(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 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
3521pub(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 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 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 TypeSpec::Any => "DdsAny".into(),
3570 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
3580pub mod runtime {
3588 pub const TYPES_TS: &str = include_str!("runtime/types.ts");
3590 pub const BRANDED_TS: &str = include_str!("runtime/branded.ts");
3592 pub const DDS_ANY_TS: &str = include_str!("runtime/dds_any.ts");
3594 pub const REGISTRY_TS: &str = include_str!("runtime/registry.ts");
3596 pub const EQUAL_TS: &str = include_str!("runtime/equal.ts");
3598 pub const OPERATIONS_TS: &str = include_str!("runtime/operations.ts");
3601 pub const WASM_TS: &str = include_str!("runtime/wasm.ts");
3603 pub const TEST_BACKEND_TS: &str = include_str!("runtime/test_backend.ts");
3606 pub const INDEX_TS: &str = include_str!("runtime/index.ts");
3609
3610 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#[derive(Debug, Clone, PartialEq, Eq)]
3628pub enum IdlTsError {
3629 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 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 assert!(
3677 ts.contains("export function isPoint(v: unknown): v is Point"),
3678 "got:\n{ts}"
3679 );
3680 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 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 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 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 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 #[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 assert!(ts.contains("@dds-unit celsius"), "got:\n{ts}");
3879 assert!(ts.contains("@dds-min -40"));
3880 assert!(ts.contains("@dds-max 125"));
3881 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 assert!(
3897 ts.contains("export interface CalculatorClient {"),
3898 "got:\n{ts}"
3899 );
3900 assert!(ts.contains("export interface CalculatorHandler {"));
3901 assert!(ts.contains("add(a: number, b: number): Promise<number>"));
3903 assert!(!ts.contains("async add"));
3904 assert!(ts.contains("get_name(): Promise<string>"));
3906 assert!(ts.contains("set_name(value: string): Promise<void>"));
3907 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 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 assert!(
3938 ts.contains("advance(step: number, position: number): Promise<{ result: number; position: number; wrapped: boolean }>"),
3939 "got:\n{ts}"
3940 );
3941 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 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 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 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 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 #[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 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 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 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 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 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 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 #[test]
4346 fn b_2_1_runtime_exports_full_b21_surface() {
4347 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 #[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 let ts = gen_ts(r"interface I { attribute long n; };");
4521 assert!(ts.contains("getRaises: ["));
4522 assert!(ts.contains("setRaises: ["));
4523 }
4524
4525 #[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 assert!(src.contains("prefix: Uint8Array"));
4549 assert!(src.contains("entityId: number"));
4550 assert!(src.contains("validData: boolean"));
4552 assert!(src.contains("publicationHandle: DdsGuid"));
4553 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 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 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 assert!(runtime::WASM_TS.contains("WASM backend not bound"));
4613 }
4614
4615 #[test]
4616 fn c_6_reservation_unused_identifiers_not_collided() {
4617 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 let _ = runtime::ALL.len();
4645 }
4646
4647 #[test]
4648 fn struct_multi_dim_array_emits_nested_array_type() {
4649 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 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 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 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 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 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 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 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 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 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 assert!(ts.contains("id: 0xFFFFFFFF"));
4912 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 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 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 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 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 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 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 let src = runtime::INDEX_TS;
5016 for marker in [
5017 "ExtensibilityKind",
5019 "PrimitiveName",
5021 "DdsTypeRef",
5022 "GuardedTypeOf",
5023 "DescriptorKind",
5025 "DdsMemberDescriptor",
5026 "DdsTypeDescriptor",
5027 "Char",
5029 "WChar",
5030 "makeChar",
5031 "makeWChar",
5032 "LongDouble",
5034 "makeLongDouble",
5035 "DdsAny",
5037 "DdsException",
5039 "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 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 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 let src = runtime::EQUAL_TS;
5093 assert!(src.contains("export function equalKey"));
5094 assert!(src.contains("export function isOneOf"));
5095 assert!(src.contains("GuardedTypeOf<DS[number]>"));
5097 assert!(src.contains("Object.is"));
5099 }
5100
5101 #[test]
5102 fn runtime_all_files_listed() {
5103 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 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}