#![forbid(unsafe_code)]
#![warn(missing_docs)]
extern crate alloc;
mod amqp;
use alloc::string::String;
use alloc::vec::Vec;
use zerodds_idl::ast::{
Definition, FloatingType, IntegerType, PrimitiveType, Specification, StringType, TypeSpec,
};
pub fn generate_ts_source(spec: &Specification) -> Result<String, IdlTsError> {
let (out, _diagnostics) = generate_ts_source_with_diagnostics(spec)?;
Ok(out)
}
pub fn generate_ts_source_with_amqp(spec: &Specification) -> Result<String, IdlTsError> {
let mut out = generate_ts_source(spec)?;
amqp::append_amqp_helpers(&mut out, spec)?;
Ok(out)
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct CodegenConfig {
pub strict_annotations: bool,
}
pub fn generate_ts_source_with_diagnostics(
spec: &Specification,
) -> Result<(String, Vec<Diagnostic>), IdlTsError> {
generate_ts_source_with_config(spec, &CodegenConfig::default())
}
pub fn generate_ts_source_with_config(
spec: &Specification,
config: &CodegenConfig,
) -> Result<(String, Vec<Diagnostic>), IdlTsError> {
let mut diagnostics: Vec<Diagnostic> = Vec::new();
let begin_file = collect_file_verbatim(&spec.definitions, VerbatimPlacement::BeginFile);
let end_file = collect_file_verbatim(&spec.definitions, VerbatimPlacement::EndFile);
let mut out = String::new();
if !begin_file.is_empty() {
out.push_str(&begin_file);
}
out.push_str("// Generated by zerodds idl-ts. Do not edit.\n\n");
out.push_str(RUNTIME_IMPORT_BLOCK);
out.push('\n');
let empty_path: Vec<String> = Vec::new();
for def in &spec.definitions {
emit_definition_with_diagnostics(&mut out, def, &mut diagnostics, &empty_path)?;
}
if !end_file.is_empty() {
out.push_str(&end_file);
}
scan_annotation_conflicts(&spec.definitions, &mut diagnostics)?;
scan_unknown_annotations(&spec.definitions, &mut diagnostics, config)?;
check_forward_declaration_orphans(&spec.definitions, &mut diagnostics)?;
scan_long_double_uses(&spec.definitions, &mut diagnostics);
scan_union_implicit_defaults(&spec.definitions, &mut diagnostics);
scan_map_key_hazards(&spec.definitions, &mut diagnostics);
Ok((out, diagnostics))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
pub code: &'static str,
pub severity: Severity,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
Fatal,
Warning,
Info,
}
const RUNTIME_IMPORT_BLOCK: &str = "\
import type {
Char, WChar, LongDouble,
DdsAny, DdsException,
DdsTypeDescriptor, DdsMemberDescriptor, DdsTypeRef,
ServiceDescriptor, OperationDescriptor,
OperationParameterDescriptor, AttributeDescriptor, ParameterMode,
} from \"@zerodds/types\";
import {
registerType, makeChar, makeWChar, makeLongDouble,
} from \"@zerodds/types\";
import type {
DdsTopicType, EndianMode,
} from \"@zerodds/cdr\";
import {
Xcdr2Writer, Xcdr2Reader, md5,
} from \"@zerodds/cdr\";
";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum VerbatimPlacement {
BeginFile,
BeforeDeclaration,
BeginDeclaration,
EndDeclaration,
AfterDeclaration,
EndFile,
}
impl VerbatimPlacement {
fn from_str(s: &str) -> Option<Self> {
match s.to_ascii_uppercase().as_str() {
"BEGIN_FILE" => Some(Self::BeginFile),
"BEFORE_DECLARATION" => Some(Self::BeforeDeclaration),
"BEGIN_DECLARATION" => Some(Self::BeginDeclaration),
"END_DECLARATION" => Some(Self::EndDeclaration),
"AFTER_DECLARATION" => Some(Self::AfterDeclaration),
"END_FILE" => Some(Self::EndFile),
_ => None,
}
}
}
fn collect_file_verbatim(definitions: &[Definition], target: VerbatimPlacement) -> String {
let mut out = String::new();
for def in definitions {
let anns = annotations_of_definition(def);
for (placement, text) in extract_verbatim(anns) {
if placement == target {
out.push_str(&text);
if !text.ends_with('\n') {
out.push('\n');
}
}
}
if let Definition::Module(m) = def {
out.push_str(&collect_file_verbatim(&m.definitions, target));
}
}
out
}
const KNOWN_ANNOTATIONS: &[&str] = &[
"id",
"key",
"optional",
"default",
"final",
"appendable",
"mutable",
"nested",
"topic",
"must_understand",
"unit",
"min",
"max",
"range",
"hashid",
"autoid",
"bit_bound",
"position",
"value",
"verbatim",
"shared",
"external",
"ami",
"service",
"oneway",
"amicallback",
"ignore_literal_names",
"data_representation",
"extensibility",
];
fn scan_unknown_annotations(
definitions: &[Definition],
diagnostics: &mut Vec<Diagnostic>,
config: &CodegenConfig,
) -> Result<(), IdlTsError> {
fn walk_anns(
anns: &[zerodds_idl::ast::Annotation],
diagnostics: &mut Vec<Diagnostic>,
config: &CodegenConfig,
) -> Result<(), IdlTsError> {
for a in anns {
if a.name.parts.len() != 1 {
continue;
}
let name = a.name.parts[0].text.as_str();
if KNOWN_ANNOTATIONS.contains(&name) {
continue;
}
if config.strict_annotations {
let msg = alloc::format!(
"DDS-TS-E004: unrecognised annotation `@{name}` \
under --strict-annotations"
);
diagnostics.push(Diagnostic {
code: "DDS-TS-E004",
severity: Severity::Fatal,
message: msg.clone(),
});
return Err(IdlTsError::Unsupported(msg));
}
diagnostics.push(Diagnostic {
code: "DDS-TS-W002",
severity: Severity::Warning,
message: alloc::format!("DDS-TS-W002: unrecognised annotation `@{name}` ignored"),
});
}
Ok(())
}
for def in definitions {
walk_anns(annotations_of_definition(def), diagnostics, config)?;
match def {
Definition::Type(td) => walk_type_decl_anns(td, diagnostics, config)?,
Definition::Except(e) => {
for m in &e.members {
walk_anns(&m.annotations, diagnostics, config)?;
}
}
Definition::Interface(zerodds_idl::ast::InterfaceDcl::Def(i)) => {
for ex in &i.exports {
use zerodds_idl::ast::Export;
match ex {
Export::Op(op) => {
walk_anns(&op.annotations, diagnostics, config)?;
for p in &op.params {
walk_anns(&p.annotations, diagnostics, config)?;
}
}
Export::Attr(attr) => {
walk_anns(&attr.annotations, diagnostics, config)?;
}
_ => {}
}
}
}
Definition::Module(m) => {
scan_unknown_annotations(&m.definitions, diagnostics, config)?;
}
_ => {}
}
}
Ok(())
}
fn walk_type_decl_anns(
td: &zerodds_idl::ast::TypeDecl,
diagnostics: &mut Vec<Diagnostic>,
config: &CodegenConfig,
) -> Result<(), IdlTsError> {
use zerodds_idl::ast::{ConstrTypeDecl, StructDcl, TypeDecl, UnionDcl};
match td {
TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s))) => {
for m in &s.members {
check_member_anns(&m.annotations, diagnostics, config)?;
}
}
TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u))) => {
for case in &u.cases {
check_member_anns(&case.element.annotations, diagnostics, config)?;
}
}
TypeDecl::Constr(ConstrTypeDecl::Enum(e)) => {
for en in &e.enumerators {
check_member_anns(&en.annotations, diagnostics, config)?;
}
}
TypeDecl::Constr(ConstrTypeDecl::Bitset(b)) => {
for bf in &b.bitfields {
check_member_anns(&bf.annotations, diagnostics, config)?;
}
}
TypeDecl::Constr(ConstrTypeDecl::Bitmask(b)) => {
for v in &b.values {
check_member_anns(&v.annotations, diagnostics, config)?;
}
}
_ => {}
}
Ok(())
}
fn check_member_anns(
anns: &[zerodds_idl::ast::Annotation],
diagnostics: &mut Vec<Diagnostic>,
config: &CodegenConfig,
) -> Result<(), IdlTsError> {
for a in anns {
if a.name.parts.len() != 1 {
continue;
}
let name = a.name.parts[0].text.as_str();
if KNOWN_ANNOTATIONS.contains(&name) {
continue;
}
if config.strict_annotations {
let msg = alloc::format!(
"DDS-TS-E004: unrecognised annotation `@{name}` \
under --strict-annotations"
);
diagnostics.push(Diagnostic {
code: "DDS-TS-E004",
severity: Severity::Fatal,
message: msg.clone(),
});
return Err(IdlTsError::Unsupported(msg));
}
diagnostics.push(Diagnostic {
code: "DDS-TS-W002",
severity: Severity::Warning,
message: alloc::format!("DDS-TS-W002: unrecognised annotation `@{name}` ignored"),
});
}
Ok(())
}
fn scan_annotation_conflicts(
definitions: &[Definition],
diagnostics: &mut Vec<Diagnostic>,
) -> Result<(), IdlTsError> {
use zerodds_idl::ast::{ConstrTypeDecl, StructDcl, TypeDecl, UnionDcl};
for def in definitions {
let anns = annotations_of_definition(def);
let ext_count = ["final", "appendable", "mutable"]
.iter()
.filter(|n| has_annotation(anns, n))
.count();
if ext_count > 1 {
return fail_e003(
diagnostics,
"multiple extensibility annotations on a single type",
);
}
if let Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) = def {
if has_duplicate_member_id(&s.members) {
return fail_e003(
diagnostics,
"two members of a single struct share the same @id(N)",
);
}
}
if let Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u)))) = def {
if has_duplicate_case_labels(&u.cases) {
return fail_e003(
diagnostics,
"two cases of a single union share the same label",
);
}
}
if let Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitmask(b))) = def {
let mut positions: Vec<i64> = Vec::new();
for v in &b.values {
if let Some(p) = annotation_int_value(&v.annotations, "position") {
if positions.contains(&p) {
return fail_e003(
diagnostics,
"two bit-values share the same explicit @position(P)",
);
}
positions.push(p);
}
}
}
if let Definition::Module(m) = def {
scan_annotation_conflicts(&m.definitions, diagnostics)?;
}
}
Ok(())
}
fn fail_e003(diagnostics: &mut Vec<Diagnostic>, detail: &str) -> Result<(), IdlTsError> {
let msg = alloc::format!("DDS-TS-E003: {detail}");
diagnostics.push(Diagnostic {
code: "DDS-TS-E003",
severity: Severity::Fatal,
message: msg.clone(),
});
Err(IdlTsError::Unsupported(msg))
}
fn has_duplicate_member_id(members: &[zerodds_idl::ast::Member]) -> bool {
let mut ids: Vec<i64> = Vec::new();
for m in members {
if let Some(id) = annotation_int_value(&m.annotations, "id") {
if ids.contains(&id) {
return true;
}
ids.push(id);
}
}
false
}
fn has_duplicate_case_labels(cases: &[zerodds_idl::ast::Case]) -> bool {
use zerodds_idl::ast::CaseLabel;
let mut seen: Vec<i64> = Vec::new();
for case in cases {
for label in &case.labels {
if let CaseLabel::Value(expr) = label {
if let Some(n) = eval_const_int(expr) {
if seen.contains(&n) {
return true;
}
seen.push(n);
}
}
}
}
false
}
fn check_forward_declaration_orphans(
definitions: &[Definition],
diagnostics: &mut Vec<Diagnostic>,
) -> Result<(), IdlTsError> {
use zerodds_idl::ast::InterfaceDcl;
let mut complete: Vec<String> = Vec::new();
let mut forwards: Vec<String> = Vec::new();
walk_interface_decls(definitions, &mut complete, &mut forwards);
for f in &forwards {
if !complete.contains(f) {
let msg = alloc::format!(
"DDS-TS-E002: forward-declared interface `{f}` lacks a \
matching complete declaration in this compilation unit"
);
diagnostics.push(Diagnostic {
code: "DDS-TS-E002",
severity: Severity::Fatal,
message: msg.clone(),
});
let _ = InterfaceDcl::Forward; return Err(IdlTsError::Unsupported(msg));
}
}
Ok(())
}
fn walk_interface_decls(
definitions: &[Definition],
complete: &mut Vec<String>,
forwards: &mut Vec<String>,
) {
use zerodds_idl::ast::InterfaceDcl;
for def in definitions {
match def {
Definition::Interface(InterfaceDcl::Def(i)) => {
complete.push(i.name.text.clone());
}
Definition::Interface(InterfaceDcl::Forward(f)) => {
forwards.push(f.name.text.clone());
}
Definition::Module(m) => {
walk_interface_decls(&m.definitions, complete, forwards);
}
_ => {}
}
}
}
fn scan_long_double_uses(definitions: &[Definition], diagnostics: &mut Vec<Diagnostic>) {
use zerodds_idl::ast::{ConstrTypeDecl, StructDcl, TypeDecl, UnionDcl};
for def in definitions {
match def {
Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) => {
for m in &s.members {
if has_long_double(&m.type_spec) {
diagnostics.push(Diagnostic {
code: "DDS-TS-I001",
severity: Severity::Info,
message: alloc::format!(
"DDS-TS-I001: `long double` in {}.{} mapped \
to opaque LongDouble carrier",
s.name.text,
m.declarators
.first()
.map(|d| d.name().text.as_str())
.unwrap_or("?")
),
});
}
}
}
Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u)))) => {
for case in &u.cases {
if has_long_double(&case.element.type_spec) {
diagnostics.push(Diagnostic {
code: "DDS-TS-I001",
severity: Severity::Info,
message: alloc::format!(
"DDS-TS-I001: `long double` in union {}.{} \
mapped to opaque LongDouble carrier",
u.name.text,
case.element.declarator.name().text
),
});
}
}
}
Definition::Module(m) => scan_long_double_uses(&m.definitions, diagnostics),
_ => {}
}
}
}
fn has_long_double(t: &TypeSpec) -> bool {
matches!(
t,
TypeSpec::Primitive(PrimitiveType::Floating(FloatingType::LongDouble))
)
}
fn scan_union_implicit_defaults(definitions: &[Definition], diagnostics: &mut Vec<Diagnostic>) {
use zerodds_idl::ast::{CaseLabel, ConstrTypeDecl, SwitchTypeSpec, TypeDecl, UnionDcl};
for def in definitions {
if let Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u)))) = def {
let has_default = u
.cases
.iter()
.any(|c| c.labels.iter().any(|l| matches!(l, CaseLabel::Default)));
if has_default {
continue;
}
if matches!(
u.switch_type,
SwitchTypeSpec::Integer(_)
| SwitchTypeSpec::Octet
| SwitchTypeSpec::Char
| SwitchTypeSpec::Boolean
) {
diagnostics.push(Diagnostic {
code: "DDS-TS-W004",
severity: Severity::Warning,
message: alloc::format!(
"DDS-TS-W004: union {} has no `default` case and \
the discriminator is not exhaustively covered \
by the listed labels",
u.name.text
),
});
}
}
if let Definition::Module(m) = def {
scan_union_implicit_defaults(&m.definitions, diagnostics);
}
}
}
fn scan_map_key_hazards(definitions: &[Definition], diagnostics: &mut Vec<Diagnostic>) {
use zerodds_idl::ast::{ConstrTypeDecl, StructDcl, TypeDecl};
for def in definitions {
if let Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) = def {
for m in &s.members {
if let TypeSpec::Map(map) = &m.type_spec {
if matches!(*map.key, TypeSpec::Scoped(_)) {
let field = m
.declarators
.first()
.map(|d| d.name().text.as_str())
.unwrap_or("?");
diagnostics.push(Diagnostic {
code: "DDS-TS-W003",
severity: Severity::Warning,
message: alloc::format!(
"DDS-TS-W003: map<K, V> in {}.{} uses a \
struct/ref key type with non-value-equality \
JavaScript semantics; use `equalKey` for lookup",
s.name.text,
field
),
});
}
}
}
}
if let Definition::Module(m) = def {
scan_map_key_hazards(&m.definitions, diagnostics);
}
}
}
fn annotations_of_definition(def: &Definition) -> &[zerodds_idl::ast::Annotation] {
use zerodds_idl::ast::{ConstrTypeDecl, InterfaceDcl, StructDcl, TypeDecl, UnionDcl};
match def {
Definition::Type(td) => match td {
TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s))) => &s.annotations,
TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u))) => &u.annotations,
TypeDecl::Constr(ConstrTypeDecl::Enum(e)) => &e.annotations,
TypeDecl::Constr(ConstrTypeDecl::Bitset(b)) => &b.annotations,
TypeDecl::Constr(ConstrTypeDecl::Bitmask(b)) => &b.annotations,
TypeDecl::Typedef(t) => &t.annotations,
_ => &[],
},
Definition::Const(c) => &c.annotations,
Definition::Except(e) => &e.annotations,
Definition::Interface(InterfaceDcl::Def(i)) => &i.annotations,
Definition::Module(m) => &m.annotations,
_ => &[],
}
}
fn extract_verbatim(
annotations: &[zerodds_idl::ast::Annotation],
) -> Vec<(VerbatimPlacement, String)> {
use zerodds_idl::ast::{AnnotationParams, ConstExpr, LiteralKind};
let mut out: Vec<(VerbatimPlacement, String)> = Vec::new();
for a in annotations {
if !(a.name.parts.len() == 1 && a.name.parts[0].text == "verbatim") {
continue;
}
let AnnotationParams::Named(params) = &a.params else {
continue;
};
let mut language: Option<String> = None;
let mut placement_str: Option<String> = None;
let mut text: Option<String> = None;
for p in params {
let key = p.name.text.as_str();
if let ConstExpr::Literal(lit) = &p.value {
if matches!(lit.kind, LiteralKind::String | LiteralKind::WideString) {
let raw = lit.raw.as_str();
let trimmed = raw
.strip_prefix('L')
.unwrap_or(raw)
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or(raw);
let unescaped = unescape_idl_string(trimmed);
match key {
"language" => language = Some(unescaped),
"placement" => placement_str = Some(unescaped),
"text" => text = Some(unescaped),
_ => {}
}
}
}
}
let lang = language.unwrap_or_else(|| "*".to_string());
if lang != "ts" && lang != "*" {
continue;
}
let Some(placement) = placement_str.and_then(|s| VerbatimPlacement::from_str(&s)) else {
continue;
};
let Some(t) = text else {
continue;
};
out.push((placement, t));
}
out
}
fn unescape_idl_string(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c != '\\' {
out.push(c);
continue;
}
match chars.next() {
Some('n') => out.push('\n'),
Some('t') => out.push('\t'),
Some('r') => out.push('\r'),
Some('"') => out.push('"'),
Some('\\') => out.push('\\'),
Some('\'') => out.push('\''),
Some('0') => out.push('\0'),
Some(other) => {
out.push('\\');
out.push(other);
}
None => out.push('\\'),
}
}
out
}
fn emit_verbatim_at(
out: &mut String,
annotations: &[zerodds_idl::ast::Annotation],
placement: VerbatimPlacement,
) {
for (p, text) in extract_verbatim(annotations) {
if p == placement {
out.push_str(&text);
if !text.ends_with('\n') {
out.push('\n');
}
}
}
}
#[allow(dead_code)] fn emit_definition(out: &mut String, def: &Definition) -> Result<(), IdlTsError> {
let mut sink: Vec<Diagnostic> = Vec::new();
let empty_path: Vec<String> = Vec::new();
emit_definition_with_diagnostics(out, def, &mut sink, &empty_path)
}
fn emit_definition_with_diagnostics(
out: &mut String,
def: &Definition,
diagnostics: &mut Vec<Diagnostic>,
module_path: &[String],
) -> Result<(), IdlTsError> {
use zerodds_idl::ast::InterfaceDcl;
let anns = annotations_of_definition(def);
emit_verbatim_at(out, anns, VerbatimPlacement::BeforeDeclaration);
let res = match def {
Definition::Type(td) => emit_type_decl(out, td, module_path),
Definition::Const(c) => emit_const(out, c),
Definition::Except(e) => emit_exception(out, e),
Definition::Interface(InterfaceDcl::Def(i)) => emit_interface(out, i),
Definition::Interface(InterfaceDcl::Forward(f)) => {
let _ = f;
Ok(())
}
Definition::Module(m) => {
out.push_str(&alloc::format!("export namespace {} {{\n", m.name.text));
emit_verbatim_at(out, &m.annotations, VerbatimPlacement::BeginDeclaration);
let mut next_path: Vec<String> = module_path.to_vec();
next_path.push(m.name.text.clone());
for inner in &m.definitions {
emit_definition_with_diagnostics(out, inner, diagnostics, &next_path)?;
}
emit_verbatim_at(out, &m.annotations, VerbatimPlacement::EndDeclaration);
out.push_str("}\n\n");
let _ = diagnostics;
Ok(())
}
_ => Ok(()),
};
emit_verbatim_at(out, anns, VerbatimPlacement::AfterDeclaration);
res
}
fn emit_const(out: &mut String, c: &zerodds_idl::ast::ConstDecl) -> Result<(), IdlTsError> {
use zerodds_idl::ast::{ConstExpr, ConstType, LiteralKind};
let (ts_ty, want_bigint) = match &c.type_ {
ConstType::Integer(i) => match i {
IntegerType::LongLong
| IntegerType::ULongLong
| IntegerType::Int64
| IntegerType::UInt64 => ("bigint", true),
_ => ("number", false),
},
ConstType::Floating(_) => ("number", false),
ConstType::Char => ("Char", false),
ConstType::WideChar => ("WChar", false),
ConstType::Boolean => ("boolean", false),
ConstType::Octet => ("number", false),
ConstType::String { .. } => ("string", false),
ConstType::Fixed => ("string", false),
ConstType::Scoped(_) => ("number", false),
};
let value_ts = const_expr_to_ts_value(&c.value, want_bigint).ok_or_else(|| {
IdlTsError::Unsupported(alloc::format!(
"const {} value is not a literal expression",
c.name.text
))
})?;
let _ = ConstExpr::Literal; let _ = LiteralKind::Boolean;
out.push_str(&alloc::format!(
"export const {}: {ts_ty} = {value_ts};\n\n",
c.name.text
));
Ok(())
}
fn const_expr_to_ts_value(e: &zerodds_idl::ast::ConstExpr, want_bigint: bool) -> Option<String> {
use zerodds_idl::ast::{ConstExpr, LiteralKind};
if let ConstExpr::Literal(lit) = e {
if matches!(lit.kind, LiteralKind::Boolean) {
return Some(match lit.raw.to_uppercase().as_str() {
"TRUE" | "1" => "true".into(),
_ => "false".into(),
});
}
}
if let Some(n) = eval_const_int(e) {
return Some(if want_bigint {
alloc::format!("{n}n")
} else {
alloc::format!("{n}")
});
}
if let ConstExpr::Literal(lit) = e {
return Some(match lit.kind {
LiteralKind::Boolean => match lit.raw.to_lowercase().as_str() {
"true" | "1" => "true".into(),
_ => "false".into(),
},
LiteralKind::Floating => lit.raw.clone(),
LiteralKind::String | LiteralKind::WideString => {
let raw = lit.raw.as_str();
let trimmed = raw
.strip_prefix('L')
.unwrap_or(raw)
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or(raw);
alloc::format!("\"{trimmed}\"")
}
LiteralKind::Char | LiteralKind::WideChar => {
let raw = lit.raw.as_str();
let trimmed = raw
.strip_prefix('L')
.unwrap_or(raw)
.strip_prefix('\'')
.and_then(|s| s.strip_suffix('\''))
.unwrap_or(raw);
alloc::format!("\"{trimmed}\" as Char")
}
_ => lit.raw.clone(),
});
}
None
}
fn emit_exception(out: &mut String, e: &zerodds_idl::ast::ExceptDecl) -> Result<(), IdlTsError> {
let name = &e.name.text;
out.push_str(&alloc::format!(
"export interface {name} extends DdsException {{\n"
));
for m in &e.members {
let ts_ty = typespec_to_ts(&m.type_spec)?;
let is_optional = has_annotation(&m.annotations, "optional");
for d in &m.declarators {
let suffix = if is_optional {
alloc::format!("?: {ts_ty} | undefined")
} else {
alloc::format!(": {ts_ty}")
};
out.push_str(&alloc::format!(" {}{suffix};\n", d.name().text));
}
}
out.push_str("}\n\n");
out.push_str(&alloc::format!(
"export function is{name}(v: unknown): v is {name} {{\n"
));
out.push_str(" if (typeof v !== \"object\" || v === null) return false;\n");
out.push_str(" const o = v as Record<string, unknown>;\n");
out.push_str(" if (o.__dds_exception !== true) return false;\n");
for m in &e.members {
if has_annotation(&m.annotations, "optional") {
continue;
}
if let Some(check) = typespec_typeof_check(&m.type_spec) {
for d in &m.declarators {
let field = d.name().text.clone();
out.push_str(&alloc::format!(
" if ({}) return false;\n",
check.replace("VAR", &alloc::format!("o.{field}"))
));
}
}
}
out.push_str(" return true;\n}\n\n");
out.push_str(&alloc::format!(
"export const {name}Type: DdsTypeDescriptor<{name}> = {{\n"
));
out.push_str(" kind: \"exception\",\n");
out.push_str(&alloc::format!(" name: \"{name}\",\n"));
out.push_str(" extensibility: \"appendable\",\n");
out.push_str(" nested: false,\n");
out.push_str(" fields: [\n");
let mut next_id: i64 = 0;
for m in &e.members {
let key_flag = has_annotation(&m.annotations, "key");
let optional_flag = has_annotation(&m.annotations, "optional");
let must_flag = has_annotation(&m.annotations, "must_understand");
let id_override = annotation_int_value(&m.annotations, "id");
let type_ref = typespec_to_typeref_literal(&m.type_spec);
for d in &m.declarators {
let id = id_override.unwrap_or(next_id);
next_id = id + 1;
out.push_str(" {\n");
out.push_str(&alloc::format!(
" name: \"{}\",\n",
d.name().text
));
out.push_str(&alloc::format!(" id: {id},\n"));
out.push_str(&alloc::format!(" type: {type_ref},\n"));
out.push_str(&alloc::format!(" key: {key_flag},\n"));
out.push_str(&alloc::format!(" optional: {optional_flag},\n"));
out.push_str(&alloc::format!(
" mustUnderstand: {must_flag},\n"
));
out.push_str(" },\n");
}
}
out.push_str(" ],\n");
out.push_str(&alloc::format!(" typeGuard: is{name},\n"));
out.push_str("};\n");
out.push_str(&alloc::format!("registerType({name}Type);\n\n"));
Ok(())
}
fn emit_type_decl(
out: &mut String,
td: &zerodds_idl::ast::TypeDecl,
module_path: &[String],
) -> Result<(), IdlTsError> {
use zerodds_idl::ast::{ConstrTypeDecl, StructDcl, TypeDecl, UnionDcl};
match td {
TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s))) => {
emit_struct(out, s, module_path)
}
TypeDecl::Constr(ConstrTypeDecl::Enum(e)) => emit_enum(out, e),
TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u))) => emit_union(out, u),
TypeDecl::Constr(ConstrTypeDecl::Bitset(b)) => emit_bitset(out, b),
TypeDecl::Constr(ConstrTypeDecl::Bitmask(b)) => emit_bitmask(out, b),
TypeDecl::Typedef(t) => emit_typedef(out, t),
_ => Ok(()),
}
}
fn emit_enum(out: &mut String, e: &zerodds_idl::ast::EnumDef) -> Result<(), IdlTsError> {
let name = &e.name.text;
out.push_str(&alloc::format!("export const {name} = {{\n"));
for en in &e.enumerators {
out.push_str(&alloc::format!(" {n}: \"{n}\",\n", n = en.name.text));
}
out.push_str("} as const;\n");
out.push_str(&alloc::format!(
"export type {name} = (typeof {name})[keyof typeof {name}];\n",
));
let mut ordinals: Vec<(String, i64)> = Vec::new();
let mut next: i64 = 0;
for en in &e.enumerators {
let val = annotation_int_value(&en.annotations, "value").unwrap_or(next);
ordinals.push((en.name.text.clone(), val));
next = val + 1;
}
out.push_str(&alloc::format!(
"export const {name}Ordinal: Readonly<Record<{name}, number>> = {{\n",
));
for (member, ord) in &ordinals {
out.push_str(&alloc::format!(" {member}: {ord},\n"));
}
out.push_str("} as const;\n");
out.push_str(&alloc::format!(
"export const {name}FromOrdinal: ReadonlyMap<number, {name}> = new Map([\n",
));
for (member, ord) in &ordinals {
out.push_str(&alloc::format!(" [{ord}, \"{member}\"],\n"));
}
out.push_str("]);\n\n");
out.push_str(&alloc::format!(
"export function is{name}(v: unknown): v is {name} {{\n"
));
out.push_str(" if (typeof v !== \"string\") return false;\n");
out.push_str(&alloc::format!(
" return Object.prototype.hasOwnProperty.call({name}, v);\n"
));
out.push_str("}\n\n");
let bit_bound = annotation_int_value(&e.annotations, "bit_bound").unwrap_or(32);
out.push_str(&alloc::format!(
"export const {name}Type: DdsTypeDescriptor<{name}> = {{\n"
));
out.push_str(" kind: \"enum\",\n");
out.push_str(&alloc::format!(" name: \"{name}\",\n"));
out.push_str(" extensibility: \"appendable\",\n");
out.push_str(" nested: false,\n");
out.push_str(&alloc::format!(" bitBound: {bit_bound},\n"));
out.push_str(" fields: [\n");
for (i, (member, ord)) in ordinals.iter().enumerate() {
out.push_str(" {\n");
out.push_str(&alloc::format!(" name: \"{member}\",\n"));
out.push_str(&alloc::format!(" id: {i},\n"));
out.push_str(" type: { kind: \"primitive\", name: \"int32\" },\n");
out.push_str(" key: false,\n");
out.push_str(" optional: false,\n");
out.push_str(" mustUnderstand: false,\n");
out.push_str(&alloc::format!(" default: {ord},\n"));
out.push_str(" },\n");
}
out.push_str(" ],\n");
out.push_str(&alloc::format!(" typeGuard: is{name},\n"));
out.push_str("};\n");
out.push_str(&alloc::format!("registerType({name}Type);\n\n"));
Ok(())
}
fn annotation_int_value(annotations: &[zerodds_idl::ast::Annotation], name: &str) -> Option<i64> {
use zerodds_idl::ast::AnnotationParams;
for a in annotations {
if a.name.parts.len() == 1 && a.name.parts[0].text == name {
if let AnnotationParams::Single(expr) = &a.params {
return eval_const_int(expr);
}
}
}
None
}
fn has_annotation(annotations: &[zerodds_idl::ast::Annotation], name: &str) -> bool {
annotations
.iter()
.any(|a| a.name.parts.len() == 1 && a.name.parts[0].text == name)
}
fn annotation_string_value(
annotations: &[zerodds_idl::ast::Annotation],
name: &str,
) -> Option<String> {
use zerodds_idl::ast::{AnnotationParams, ConstExpr, LiteralKind};
for a in annotations {
if a.name.parts.len() == 1 && a.name.parts[0].text == name {
if let AnnotationParams::Single(ConstExpr::Literal(lit)) = &a.params {
if matches!(lit.kind, LiteralKind::String | LiteralKind::WideString) {
let raw = lit.raw.as_str();
let trimmed = raw
.strip_prefix('L')
.unwrap_or(raw)
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or(raw);
return Some(alloc::string::ToString::to_string(trimmed));
}
}
}
}
None
}
fn struct_extensibility(annotations: &[zerodds_idl::ast::Annotation]) -> &'static str {
if has_annotation(annotations, "final") {
"final"
} else if has_annotation(annotations, "mutable") {
"mutable"
} else {
"appendable"
}
}
fn emit_struct(
out: &mut String,
s: &zerodds_idl::ast::StructDef,
module_path: &[String],
) -> Result<(), IdlTsError> {
let name = &s.name.text;
out.push_str(&alloc::format!("export interface {name} "));
if let Some(base) = &s.base {
let base_path = base
.parts
.iter()
.map(|p| p.text.clone())
.collect::<Vec<_>>()
.join(".");
out.push_str(&alloc::format!("extends {base_path} "));
}
out.push_str("{\n");
emit_verbatim_at(out, &s.annotations, VerbatimPlacement::BeginDeclaration);
for m in &s.members {
let base_ts = typespec_to_ts(&m.type_spec)?;
let is_optional = has_annotation(&m.annotations, "optional");
let tsdoc = render_tsdoc_for_member(&m.annotations);
for d in &m.declarators {
if let Some(t) = &tsdoc {
out.push_str(t);
}
let ts_ty = wrap_with_array_dimensions(&base_ts, d);
let suffix = if is_optional {
alloc::format!("?: {ts_ty} | undefined")
} else {
alloc::format!(": {ts_ty}")
};
out.push_str(&alloc::format!(" {}{suffix};\n", d.name().text));
}
}
emit_verbatim_at(out, &s.annotations, VerbatimPlacement::EndDeclaration);
out.push_str("}\n\n");
emit_struct_bound_constants(out, s)?;
emit_struct_default_constants(out, s)?;
emit_struct_type_guard(out, s)?;
emit_struct_descriptor(out, s)?;
out.push_str(&alloc::format!("registerType({name}Type);\n\n"));
emit_struct_typesupport(out, s, module_path)?;
Ok(())
}
fn emit_struct_bound_constants(
out: &mut String,
s: &zerodds_idl::ast::StructDef,
) -> Result<(), IdlTsError> {
use zerodds_idl::ast::{Declarator, TypeSpec};
let type_name = &s.name.text;
let mut emitted = false;
for m in &s.members {
if let TypeSpec::String(StringType { bound: Some(n), .. }) = &m.type_spec {
if let Some(width) = eval_const_int(n) {
for d in &m.declarators {
out.push_str(&alloc::format!(
"export const {type_name}_{}_BOUND = {width};\n",
d.name().text
));
emitted = true;
}
}
}
if let TypeSpec::Sequence(seq) = &m.type_spec {
if let Some(bound) = &seq.bound {
if let Some(width) = eval_const_int(bound) {
for d in &m.declarators {
out.push_str(&alloc::format!(
"export const {type_name}_{}_BOUND = {width};\n",
d.name().text
));
emitted = true;
}
}
}
}
for d in &m.declarators {
if let Declarator::Array(a) = d {
if a.sizes.len() == 1 {
if let Some(len) = eval_const_int(&a.sizes[0]) {
out.push_str(&alloc::format!(
"export const {type_name}_{}_LENGTH = {len};\n",
a.name.text
));
emitted = true;
}
} else {
for (i, sz) in a.sizes.iter().enumerate() {
if let Some(len) = eval_const_int(sz) {
out.push_str(&alloc::format!(
"export const {type_name}_{}_LENGTH_DIM{} = {len};\n",
a.name.text,
i + 1
));
emitted = true;
}
}
}
}
}
}
if emitted {
out.push('\n');
}
Ok(())
}
fn emit_struct_type_guard(
out: &mut String,
s: &zerodds_idl::ast::StructDef,
) -> Result<(), IdlTsError> {
let name = &s.name.text;
out.push_str(&alloc::format!(
"export function is{name}(v: unknown): v is {name} {{\n"
));
out.push_str(" if (typeof v !== \"object\" || v === null) return false;\n");
out.push_str(" const o = v as Record<string, unknown>;\n");
for m in &s.members {
if has_annotation(&m.annotations, "optional") {
continue;
}
let typeof_check = typespec_typeof_check(&m.type_spec);
for d in &m.declarators {
let field = d.name().text.clone();
if let Some(check) = &typeof_check {
out.push_str(&alloc::format!(
" if ({check_expr}) return false;\n",
check_expr = check.replace("VAR", &alloc::format!("o.{field}"))
));
}
}
}
out.push_str(" return true;\n}\n\n");
Ok(())
}
fn typespec_typeof_check(t: &TypeSpec) -> Option<String> {
Some(match t {
TypeSpec::Primitive(p) => match p {
PrimitiveType::Boolean => "typeof VAR !== \"boolean\"".into(),
PrimitiveType::Char | PrimitiveType::WideChar => "typeof VAR !== \"string\"".into(),
PrimitiveType::Octet => "typeof VAR !== \"number\"".into(),
PrimitiveType::Integer(i) => match i {
IntegerType::LongLong
| IntegerType::ULongLong
| IntegerType::Int64
| IntegerType::UInt64 => "typeof VAR !== \"bigint\"".into(),
_ => "typeof VAR !== \"number\"".into(),
},
PrimitiveType::Floating(f) => match f {
FloatingType::Float | FloatingType::Double => "typeof VAR !== \"number\"".into(),
FloatingType::LongDouble => "typeof VAR !== \"object\" || VAR === null".into(),
},
},
TypeSpec::String(_) => "typeof VAR !== \"string\"".into(),
TypeSpec::Sequence(_)
| TypeSpec::Map(_)
| TypeSpec::Scoped(_)
| TypeSpec::Any
| TypeSpec::Fixed(_) => return None,
})
}
fn emit_struct_descriptor(
out: &mut String,
s: &zerodds_idl::ast::StructDef,
) -> Result<(), IdlTsError> {
let name = &s.name.text;
let extensibility = struct_extensibility(&s.annotations);
let nested = has_annotation(&s.annotations, "nested");
let topic = annotation_string_value(&s.annotations, "topic");
let autoid = annotation_string_value(&s.annotations, "autoid");
out.push_str(&alloc::format!(
"export const {name}Type: DdsTypeDescriptor<{name}> = {{\n"
));
out.push_str(" kind: \"struct\",\n");
out.push_str(&alloc::format!(" name: \"{name}\",\n"));
out.push_str(&alloc::format!(" extensibility: \"{extensibility}\",\n"));
out.push_str(&alloc::format!(" nested: {nested},\n"));
if let Some(t) = &topic {
out.push_str(&alloc::format!(" topic: \"{t}\",\n"));
}
if let Some(a) = &autoid {
let lower = a.to_ascii_lowercase();
if lower == "sequential" || lower == "hash" {
out.push_str(&alloc::format!(" autoid: \"{lower}\",\n"));
}
}
out.push_str(" fields: [\n");
let mut next_id: i64 = 0;
for m in &s.members {
let key_flag = has_annotation(&m.annotations, "key");
let optional_flag = has_annotation(&m.annotations, "optional");
let must_flag = has_annotation(&m.annotations, "must_understand");
let unit = annotation_string_value(&m.annotations, "unit");
let id_override = annotation_int_value(&m.annotations, "id");
let default_lit = annotation_default_to_ts(&m.annotations);
let min_lit = annotation_const_text(&m.annotations, "min");
let max_lit = annotation_const_text(&m.annotations, "max");
let hashid = annotation_string_value(&m.annotations, "hashid");
let type_ref = typespec_to_typeref_literal(&m.type_spec);
for d in &m.declarators {
let id = id_override.unwrap_or(next_id);
next_id = id + 1;
out.push_str(" {\n");
out.push_str(&alloc::format!(
" name: \"{}\",\n",
d.name().text
));
out.push_str(&alloc::format!(" id: {id},\n"));
out.push_str(&alloc::format!(" type: {type_ref},\n"));
out.push_str(&alloc::format!(" key: {key_flag},\n"));
out.push_str(&alloc::format!(" optional: {optional_flag},\n"));
out.push_str(&alloc::format!(
" mustUnderstand: {must_flag},\n"
));
if let Some(u) = &unit {
out.push_str(&alloc::format!(" unit: \"{u}\",\n"));
}
if let Some(d_lit) = &default_lit {
out.push_str(&alloc::format!(" default: {d_lit},\n"));
}
if let Some(min) = &min_lit {
let m_quoted = if is_numeric_literal_text(min) {
min.clone()
} else {
alloc::format!("\"{min}\"")
};
out.push_str(&alloc::format!(" min: {m_quoted},\n"));
}
if let Some(max) = &max_lit {
let m_quoted = if is_numeric_literal_text(max) {
max.clone()
} else {
alloc::format!("\"{max}\"")
};
out.push_str(&alloc::format!(" max: {m_quoted},\n"));
}
if let Some(h) = &hashid {
out.push_str(&alloc::format!(" hashid: \"{h}\",\n"));
}
if let TypeSpec::Map(map) = &m.type_spec {
if matches!(*map.key, TypeSpec::Scoped(_)) {
out.push_str(" keyEqualityHazard: true,\n");
}
}
out.push_str(" },\n");
}
}
out.push_str(" ],\n");
out.push_str(&alloc::format!(" typeGuard: is{name},\n"));
out.push_str("};\n");
Ok(())
}
fn emit_struct_typesupport(
out: &mut String,
s: &zerodds_idl::ast::StructDef,
module_path: &[String],
) -> Result<(), IdlTsError> {
let name = &s.name.text;
let mut full = module_path.to_vec();
full.push(name.clone());
let type_name = full.join("::");
let extensibility = struct_extensibility(&s.annotations);
let has_key = struct_has_any_key(s);
out.push_str(&alloc::format!(
"export const {name}TypeSupport: DdsTopicType<{name}> = {{\n"
));
out.push_str(&alloc::format!(" typeName: \"{type_name}\",\n"));
out.push_str(&alloc::format!(" isKeyed: {has_key},\n"));
out.push_str(&alloc::format!(" extensibility: \"{extensibility}\",\n"));
out.push_str(&alloc::format!(
" encode(s: {name}, endian: EndianMode = \"le\"): Uint8Array {{\n"
));
out.push_str(" const w = new Xcdr2Writer(endian);\n");
emit_struct_encode_body(out, s, " ")?;
out.push_str(" return w.toBytes();\n");
out.push_str(" },\n");
out.push_str(&alloc::format!(
" decode(bytes: Uint8Array, offset = 0, length: number = bytes.length - offset): {name} {{\n"
));
out.push_str(" const r = new Xcdr2Reader(bytes, offset, length, \"le\");\n");
emit_struct_decode_body(out, s, " ")?;
out.push_str(" },\n");
out.push_str(&alloc::format!(" keyHash(s: {name}): Uint8Array {{\n"));
if has_key {
out.push_str(" const w = new Xcdr2Writer(\"be\");\n");
emit_struct_keyhash_body(out, s, " ")?;
out.push_str(" const __holder = w.toBytes();\n");
out.push_str(" if (__holder.length <= 16) {\n");
out.push_str(" const __h = new Uint8Array(16);\n");
out.push_str(" __h.set(__holder);\n");
out.push_str(" return __h;\n");
out.push_str(" }\n");
out.push_str(" return md5(__holder);\n");
} else {
out.push_str(" return new Uint8Array(16);\n");
}
out.push_str(" },\n");
out.push_str("};\n\n");
let _ = s; Ok(())
}
fn struct_has_any_key(s: &zerodds_idl::ast::StructDef) -> bool {
s.members
.iter()
.any(|m| has_annotation(&m.annotations, "key"))
}
fn emit_struct_encode_body(
out: &mut String,
s: &zerodds_idl::ast::StructDef,
indent: &str,
) -> Result<(), IdlTsError> {
let extensibility = struct_extensibility(&s.annotations);
match extensibility {
"final" => {
for m in &s.members {
emit_member_encode(out, m, indent, "s.")?;
}
}
"appendable" => {
out.push_str(&alloc::format!(
"{indent}const _tok = w.beginAppendable();\n"
));
for m in &s.members {
emit_member_encode(out, m, indent, "s.")?;
}
out.push_str(&alloc::format!("{indent}w.endAppendable(_tok);\n"));
}
"mutable" => {
out.push_str(&alloc::format!("{indent}const _tok = w.beginMutable();\n"));
let mut next_id: i64 = 0;
for m in &s.members {
let id_override = annotation_int_value(&m.annotations, "id");
let must = has_annotation(&m.annotations, "must_understand");
let optional = has_annotation(&m.annotations, "optional");
for d in &m.declarators {
let id = id_override.unwrap_or(next_id);
next_id = id + 1;
let field = d.name().text.clone();
if optional {
out.push_str(&alloc::format!(
"{indent}if (s.{field} !== undefined && s.{field} !== null) {{\n"
));
}
let inner_indent_owned = alloc::format!("{indent} ");
let inner_indent: &str = if optional {
inner_indent_owned.as_str()
} else {
indent
};
emit_mutable_member_encode(
out,
&m.type_spec,
&field,
id as u32,
must,
inner_indent,
)?;
if optional {
out.push_str(&alloc::format!("{indent}}}\n"));
}
}
}
out.push_str(&alloc::format!("{indent}w.endMutable(_tok);\n"));
}
_ => {}
}
Ok(())
}
fn emit_member_encode(
out: &mut String,
m: &zerodds_idl::ast::Member,
indent: &str,
prefix: &str,
) -> Result<(), IdlTsError> {
let optional = has_annotation(&m.annotations, "optional");
for d in &m.declarators {
let field = d.name().text.clone();
let target = alloc::format!("{prefix}{field}");
if optional {
out.push_str(&alloc::format!(
"{indent}if ({target} !== undefined && {target} !== null) {{\n"
));
out.push_str(&alloc::format!("{indent} w.writeOctet(1);\n"));
emit_typespec_encode(out, &m.type_spec, &target, &format!("{indent} "))?;
out.push_str(&alloc::format!("{indent}}} else {{\n"));
out.push_str(&alloc::format!("{indent} w.writeOctet(0);\n"));
out.push_str(&alloc::format!("{indent}}}\n"));
} else {
emit_typespec_encode(out, &m.type_spec, &target, indent)?;
}
}
Ok(())
}
fn emit_mutable_member_encode(
out: &mut String,
t: &TypeSpec,
field: &str,
id: u32,
must: bool,
indent: &str,
) -> Result<(), IdlTsError> {
let mu_str = if must { "true" } else { "false" };
if let Some(lc) = primitive_lc_inline(t) {
out.push_str(&alloc::format!(
"{indent}w.writeEmHeader({id}, {lc}, {mu_str});\n"
));
emit_typespec_encode(out, t, &alloc::format!("s.{field}"), indent)?;
} else {
out.push_str(&alloc::format!("{indent}{{\n"));
out.push_str(&alloc::format!(
"{indent} w.writeEmHeader({id}, 3, {mu_str}, 0);\n"
));
out.push_str(&alloc::format!("{indent} const _bodyStart = w.pos;\n"));
emit_typespec_encode(
out,
t,
&alloc::format!("s.{field}"),
&format!("{indent} "),
)?;
out.push_str(&alloc::format!(
"{indent} w.patchUint32(_bodyStart - 4, w.pos - _bodyStart);\n"
));
out.push_str(&alloc::format!("{indent}}}\n"));
}
Ok(())
}
fn primitive_lc_inline(t: &TypeSpec) -> Option<u32> {
match t {
TypeSpec::Primitive(p) => match p {
PrimitiveType::Boolean | PrimitiveType::Octet | PrimitiveType::Char => Some(0),
PrimitiveType::WideChar => Some(1),
PrimitiveType::Integer(i) => match i {
IntegerType::Short
| IntegerType::UShort
| IntegerType::Int16
| IntegerType::UInt16 => Some(1),
IntegerType::Long
| IntegerType::ULong
| IntegerType::Int32
| IntegerType::UInt32 => Some(2),
IntegerType::LongLong
| IntegerType::ULongLong
| IntegerType::Int64
| IntegerType::UInt64 => Some(3),
IntegerType::Int8 | IntegerType::UInt8 => Some(0),
},
PrimitiveType::Floating(f) => match f {
FloatingType::Float => Some(2),
FloatingType::Double => Some(3),
FloatingType::LongDouble => None,
},
},
_ => None,
}
}
fn emit_typespec_encode(
out: &mut String,
t: &TypeSpec,
expr: &str,
indent: &str,
) -> Result<(), IdlTsError> {
match t {
TypeSpec::Primitive(p) => match p {
PrimitiveType::Boolean => {
out.push_str(&alloc::format!("{indent}w.writeBool({expr});\n"));
}
PrimitiveType::Octet => {
out.push_str(&alloc::format!("{indent}w.writeOctet({expr});\n"));
}
PrimitiveType::Char => {
out.push_str(&alloc::format!("{indent}w.writeChar({expr});\n"));
}
PrimitiveType::WideChar => {
out.push_str(&alloc::format!("{indent}w.writeWChar({expr});\n"));
}
PrimitiveType::Integer(i) => {
let m = match i {
IntegerType::Short | IntegerType::Int16 => "writeInt16",
IntegerType::UShort | IntegerType::UInt16 => "writeUint16",
IntegerType::Long | IntegerType::Int32 => "writeInt32",
IntegerType::ULong | IntegerType::UInt32 => "writeUint32",
IntegerType::LongLong | IntegerType::Int64 => "writeInt64",
IntegerType::ULongLong | IntegerType::UInt64 => "writeUint64",
IntegerType::Int8 => "writeInt8",
IntegerType::UInt8 => "writeUint8",
};
out.push_str(&alloc::format!("{indent}w.{m}({expr});\n"));
}
PrimitiveType::Floating(f) => {
match f {
FloatingType::Float => {
out.push_str(&alloc::format!("{indent}w.writeFloat32({expr});\n"));
}
FloatingType::Double => {
out.push_str(&alloc::format!("{indent}w.writeFloat64({expr});\n"));
}
FloatingType::LongDouble => {
out.push_str(&alloc::format!("{indent}w.writeBytes(({expr}).bytes);\n"));
}
};
}
},
TypeSpec::String(StringType { wide, .. }) => {
if *wide {
out.push_str(&alloc::format!("{indent}w.writeWString({expr});\n"));
} else {
out.push_str(&alloc::format!("{indent}w.writeString({expr});\n"));
}
}
TypeSpec::Sequence(seq) => {
out.push_str(&alloc::format!("{indent}w.writeUint32({expr}.length);\n"));
out.push_str(&alloc::format!("{indent}for (const _e of {expr}) {{\n"));
emit_typespec_encode(out, &seq.elem, "_e", &format!("{indent} "))?;
out.push_str(&alloc::format!("{indent}}}\n"));
}
TypeSpec::Map(map) => {
out.push_str(&alloc::format!("{indent}w.writeUint32({expr}.size);\n"));
out.push_str(&alloc::format!(
"{indent}for (const [_k, _v] of {expr}) {{\n"
));
emit_typespec_encode(out, &map.key, "_k", &format!("{indent} "))?;
emit_typespec_encode(out, &map.value, "_v", &format!("{indent} "))?;
out.push_str(&alloc::format!("{indent}}}\n"));
}
TypeSpec::Scoped(_) => {
out.push_str(&alloc::format!(
"{indent}w.writeInt32({expr} as unknown as number);\n"
));
}
TypeSpec::Any => {
out.push_str(&alloc::format!(
"{indent}throw new Error(\"DDS-Any XCDR2 encode not implemented in codegen\");\n"
));
}
TypeSpec::Fixed(_) => {
out.push_str(&alloc::format!(
"{indent}throw new Error(\"fixed-point XCDR2 encode not implemented in codegen\");\n"
));
}
}
Ok(())
}
fn emit_struct_decode_body(
out: &mut String,
s: &zerodds_idl::ast::StructDef,
indent: &str,
) -> Result<(), IdlTsError> {
let extensibility = struct_extensibility(&s.annotations);
let name = &s.name.text;
match extensibility {
"final" => {
for m in &s.members {
emit_member_decode_decl(out, m, indent)?;
}
emit_decode_return(out, s, indent, name)?;
}
"appendable" => {
out.push_str(&alloc::format!(
"{indent}const _tok = r.beginAppendable();\n"
));
for m in &s.members {
emit_member_decode_decl(out, m, indent)?;
}
out.push_str(&alloc::format!("{indent}r.endAppendable(_tok);\n"));
emit_decode_return(out, s, indent, name)?;
}
"mutable" => {
for m in &s.members {
let optional = has_annotation(&m.annotations, "optional");
for d in &m.declarators {
let field = d.name().text.clone();
let init = if optional {
"undefined".into()
} else {
default_init_for(&m.type_spec)
};
let ts_ty = typespec_to_ts(&m.type_spec)?;
out.push_str(&alloc::format!(
"{indent}let _f_{field}: {ts_ty} | undefined = {init};\n"
));
}
}
out.push_str(&alloc::format!("{indent}const _tok = r.beginMutable();\n"));
out.push_str(&alloc::format!("{indent}while (r.pos < _tok.bodyEnd) {{\n"));
out.push_str(&alloc::format!(
"{indent} const _emh = r.readEmHeader();\n"
));
out.push_str(&alloc::format!("{indent} switch (_emh.memberId) {{\n"));
let mut next_id: i64 = 0;
for m in &s.members {
let id_override = annotation_int_value(&m.annotations, "id");
for d in &m.declarators {
let id = id_override.unwrap_or(next_id);
next_id = id + 1;
let field = d.name().text.clone();
out.push_str(&alloc::format!("{indent} case {id}: {{\n"));
if primitive_lc_inline(&m.type_spec).is_none() {
out.push_str(&alloc::format!(
"{indent} if (_emh.lc === 3) {{ r.readUint32(); }}\n"
));
}
let ts_ty = typespec_to_ts(&m.type_spec)?;
out.push_str(&alloc::format!("{indent} const _v: {ts_ty} = "));
let read_expr = read_typespec_expr(&m.type_spec)?;
out.push_str(&alloc::format!("{read_expr};\n"));
out.push_str(&alloc::format!("{indent} _f_{field} = _v;\n"));
out.push_str(&alloc::format!("{indent} break;\n"));
out.push_str(&alloc::format!("{indent} }}\n"));
}
}
out.push_str(&alloc::format!("{indent} default: {{\n"));
out.push_str(&alloc::format!(
"{indent} // Skip unknown member per XTypes \u{00A7}7.4.2.\n"
));
out.push_str(&alloc::format!(
"{indent} if (_emh.nextInt !== null) {{ r.readBytes(_emh.nextInt); }}\n"
));
out.push_str(&alloc::format!(
"{indent} else {{ const _sz = Xcdr2Reader.lcInlineSize(_emh.lc); if (_sz > 0) r.readBytes(_sz); }}\n"
));
out.push_str(&alloc::format!("{indent} break;\n"));
out.push_str(&alloc::format!("{indent} }}\n"));
out.push_str(&alloc::format!("{indent} }}\n"));
out.push_str(&alloc::format!("{indent}}}\n"));
out.push_str(&alloc::format!("{indent}r.endMutable(_tok);\n"));
out.push_str(&alloc::format!("{indent}return {{\n"));
for m in &s.members {
let optional = has_annotation(&m.annotations, "optional");
for d in &m.declarators {
let field = d.name().text.clone();
if optional {
out.push_str(&alloc::format!("{indent} {field}: _f_{field},\n"));
} else {
out.push_str(&alloc::format!(
"{indent} {field}: _f_{field} as {},\n",
typespec_to_ts(&m.type_spec)?
));
}
}
}
out.push_str(&alloc::format!("{indent}}};\n"));
}
_ => {}
}
Ok(())
}
fn default_init_for(t: &TypeSpec) -> String {
match t {
TypeSpec::Primitive(p) => match p {
PrimitiveType::Boolean => "false".into(),
PrimitiveType::Integer(i) => match i {
IntegerType::LongLong
| IntegerType::ULongLong
| IntegerType::Int64
| IntegerType::UInt64 => "0n".into(),
_ => "0".into(),
},
PrimitiveType::Octet
| PrimitiveType::Floating(_)
| PrimitiveType::Char
| PrimitiveType::WideChar => "0 as unknown as undefined".into(),
},
TypeSpec::String(_) => "\"\"".into(),
TypeSpec::Sequence(_) | TypeSpec::Map(_) => "[] as unknown as undefined".into(),
_ => "undefined".into(),
}
}
fn emit_member_decode_decl(
out: &mut String,
m: &zerodds_idl::ast::Member,
indent: &str,
) -> Result<(), IdlTsError> {
let optional = has_annotation(&m.annotations, "optional");
for d in &m.declarators {
let field = d.name().text.clone();
let ts_ty = typespec_to_ts(&m.type_spec)?;
if optional {
out.push_str(&alloc::format!(
"{indent}const _present_{field} = r.readOctet();\n"
));
out.push_str(&alloc::format!(
"{indent}const _f_{field}: {ts_ty} | undefined = _present_{field} === 1 ? "
));
let expr = read_typespec_expr(&m.type_spec)?;
out.push_str(&alloc::format!("{expr} : undefined;\n"));
} else {
out.push_str(&alloc::format!("{indent}const _f_{field}: {ts_ty} = "));
let expr = read_typespec_expr(&m.type_spec)?;
out.push_str(&alloc::format!("{expr};\n"));
}
}
Ok(())
}
fn emit_decode_return(
out: &mut String,
s: &zerodds_idl::ast::StructDef,
indent: &str,
name: &str,
) -> Result<(), IdlTsError> {
let needs_cast = s.base.is_some();
if needs_cast {
out.push_str(&alloc::format!("{indent}return ({{\n"));
} else {
out.push_str(&alloc::format!("{indent}return {{\n"));
}
for m in &s.members {
for d in &m.declarators {
let field = d.name().text.clone();
out.push_str(&alloc::format!("{indent} {field}: _f_{field},\n"));
}
}
if needs_cast {
out.push_str(&alloc::format!("{indent}}} as unknown as {name});\n"));
} else {
out.push_str(&alloc::format!("{indent}}};\n"));
}
Ok(())
}
fn read_typespec_expr(t: &TypeSpec) -> Result<String, IdlTsError> {
Ok(match t {
TypeSpec::Primitive(p) => match p {
PrimitiveType::Boolean => "r.readBool()".into(),
PrimitiveType::Octet => "r.readOctet()".into(),
PrimitiveType::Char => "r.readChar()".into(),
PrimitiveType::WideChar => "r.readWChar()".into(),
PrimitiveType::Integer(i) => {
let m = match i {
IntegerType::Short | IntegerType::Int16 => "readInt16",
IntegerType::UShort | IntegerType::UInt16 => "readUint16",
IntegerType::Long | IntegerType::Int32 => "readInt32",
IntegerType::ULong | IntegerType::UInt32 => "readUint32",
IntegerType::LongLong | IntegerType::Int64 => "readInt64",
IntegerType::ULongLong | IntegerType::UInt64 => "readUint64",
IntegerType::Int8 => "readInt8",
IntegerType::UInt8 => "readUint8",
};
alloc::format!("r.{m}()")
}
PrimitiveType::Floating(f) => match f {
FloatingType::Float => "r.readFloat32()".into(),
FloatingType::Double => "r.readFloat64()".into(),
FloatingType::LongDouble => {
"(makeLongDouble(r.readBytes(16)) as unknown as never)".into()
}
},
},
TypeSpec::String(StringType { wide, .. }) => {
if *wide {
"r.readWString()".into()
} else {
"r.readString()".into()
}
}
TypeSpec::Sequence(seq) => {
let elem_ts = typespec_to_ts(&seq.elem)?;
let elem_read = read_typespec_expr(&seq.elem)?;
alloc::format!(
"((): 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; }})()"
)
}
TypeSpec::Map(map) => {
let k_ts = typespec_to_ts(&map.key)?;
let v_ts = typespec_to_ts(&map.value)?;
let k_read = read_typespec_expr(&map.key)?;
let v_read = read_typespec_expr(&map.value)?;
alloc::format!(
"((): 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; }})()"
)
}
TypeSpec::Scoped(_) => {
"r.readInt32() as unknown as never".into()
}
TypeSpec::Any => {
"((): never => { throw new Error(\"DDS-Any XCDR2 decode not implemented\"); })()".into()
}
TypeSpec::Fixed(_) => {
"((): never => { throw new Error(\"fixed-point XCDR2 decode not implemented\"); })()"
.into()
}
})
}
fn emit_struct_keyhash_body(
out: &mut String,
s: &zerodds_idl::ast::StructDef,
indent: &str,
) -> Result<(), IdlTsError> {
for m in &s.members {
if !has_annotation(&m.annotations, "key") {
continue;
}
for d in &m.declarators {
let field = d.name().text.clone();
emit_typespec_encode(out, &m.type_spec, &alloc::format!("s.{field}"), indent)?;
}
}
Ok(())
}
fn typespec_to_typeref_literal(t: &TypeSpec) -> String {
match t {
TypeSpec::Primitive(p) => {
let prim = primitive_to_typeref_name(p);
alloc::format!("{{ kind: \"primitive\", name: \"{prim}\" }}")
}
TypeSpec::String(StringType { wide, bound, .. }) => match bound {
Some(b) => match eval_const_int(b) {
Some(n) => alloc::format!("{{ kind: \"string\", bound: {n}, wide: {wide} }}"),
None => alloc::format!("{{ kind: \"string\", wide: {wide} }}"),
},
None => alloc::format!("{{ kind: \"string\", wide: {wide} }}"),
},
TypeSpec::Sequence(seq) => {
let elem = typespec_to_typeref_literal(&seq.elem);
match &seq.bound {
Some(b) => match eval_const_int(b) {
Some(n) => {
alloc::format!("{{ kind: \"sequence\", element: {elem}, bound: {n} }}")
}
None => alloc::format!("{{ kind: \"sequence\", element: {elem} }}"),
},
None => alloc::format!("{{ kind: \"sequence\", element: {elem} }}"),
}
}
TypeSpec::Map(m) => {
let k = typespec_to_typeref_literal(&m.key);
let v = typespec_to_typeref_literal(&m.value);
match &m.bound {
Some(b) => match eval_const_int(b) {
Some(n) => {
alloc::format!("{{ kind: \"map\", key: {k}, value: {v}, bound: {n} }}")
}
None => alloc::format!("{{ kind: \"map\", key: {k}, value: {v} }}"),
},
None => alloc::format!("{{ kind: \"map\", key: {k}, value: {v} }}"),
}
}
TypeSpec::Scoped(s) => {
let qname = s
.parts
.iter()
.map(|p| p.text.clone())
.collect::<Vec<_>>()
.join("::");
alloc::format!("{{ kind: \"ref\", name: \"{qname}\" }}")
}
TypeSpec::Any => "{ kind: \"any\" }".into(),
TypeSpec::Fixed(_) => "{ kind: \"primitive\", name: \"int64\" }".into(),
}
}
fn primitive_to_typeref_name(p: &PrimitiveType) -> &'static str {
match p {
PrimitiveType::Boolean => "boolean",
PrimitiveType::Char => "char",
PrimitiveType::WideChar => "wchar",
PrimitiveType::Octet => "octet",
PrimitiveType::Integer(i) => match i {
IntegerType::Short | IntegerType::Int16 => "int16",
IntegerType::UShort | IntegerType::UInt16 => "uint16",
IntegerType::Long | IntegerType::Int32 => "int32",
IntegerType::ULong | IntegerType::UInt32 => "uint32",
IntegerType::LongLong | IntegerType::Int64 => "int64",
IntegerType::ULongLong | IntegerType::UInt64 => "uint64",
IntegerType::Int8 => "int16",
IntegerType::UInt8 => "uint16",
},
PrimitiveType::Floating(f) => match f {
FloatingType::Float => "float",
FloatingType::Double => "double",
FloatingType::LongDouble => "longDouble",
},
}
}
fn emit_union(out: &mut String, u: &zerodds_idl::ast::UnionDef) -> Result<(), IdlTsError> {
use zerodds_idl::ast::{CaseLabel, SwitchTypeSpec};
let name = &u.name.text;
let disc_broad = match &u.switch_type {
SwitchTypeSpec::Integer(_) | SwitchTypeSpec::Octet => "number",
SwitchTypeSpec::Char => "Char",
SwitchTypeSpec::Boolean => "boolean",
SwitchTypeSpec::Scoped(s) => {
let qname = s
.parts
.iter()
.map(|p| p.text.clone())
.collect::<Vec<_>>()
.join(".");
return emit_union_with_enum_discriminator(out, u, &qname);
}
};
out.push_str(&alloc::format!("export type {name} =\n"));
let mut first = true;
let mut explicit_labels: Vec<String> = Vec::new();
for case in &u.cases {
for label in &case.labels {
let prefix = if first { " " } else { " | " };
first = false;
let disc_str = match label {
CaseLabel::Default => disc_broad.to_string(),
CaseLabel::Value(expr) => match render_label_for(disc_broad, expr) {
Some(s) => {
explicit_labels.push(s.clone());
s
}
None => disc_broad.to_string(),
},
};
let elem_ts = typespec_to_ts(&case.element.type_spec)?;
let field_name = case.element.declarator.name().text.clone();
out.push_str(&alloc::format!(
"{prefix}{{ discriminator: {disc_str}; {field_name}: {elem_ts} }}\n"
));
}
}
out.push_str(";\n\n");
emit_union_descriptor(out, u, disc_broad)?;
Ok(())
}
fn render_label_for(disc_broad: &str, expr: &zerodds_idl::ast::ConstExpr) -> Option<String> {
use zerodds_idl::ast::{ConstExpr, LiteralKind};
if let Some(n) = eval_const_int(expr) {
return Some(if disc_broad == "boolean" {
(n != 0).to_string()
} else {
alloc::format!("{n}")
});
}
if let ConstExpr::Literal(lit) = expr {
return Some(match lit.kind {
LiteralKind::Boolean => match lit.raw.to_lowercase().as_str() {
"true" | "1" => "true".into(),
_ => "false".into(),
},
LiteralKind::Char | LiteralKind::WideChar => {
let raw = lit.raw.as_str();
let trimmed = raw
.strip_prefix('L')
.unwrap_or(raw)
.strip_prefix('\'')
.and_then(|s| s.strip_suffix('\''))
.unwrap_or(raw);
alloc::format!("\"{trimmed}\"")
}
_ => lit.raw.clone(),
});
}
None
}
fn emit_union_with_enum_discriminator(
out: &mut String,
u: &zerodds_idl::ast::UnionDef,
enum_name: &str,
) -> Result<(), IdlTsError> {
use zerodds_idl::ast::{CaseLabel, ConstExpr};
let name = &u.name.text;
out.push_str(&alloc::format!("export type {name} =\n"));
let mut explicit_labels: Vec<String> = Vec::new();
let mut default_case: Option<&zerodds_idl::ast::Case> = None;
let mut first = true;
for case in &u.cases {
let mut emitted_label = false;
for label in &case.labels {
match label {
CaseLabel::Default => {
default_case = Some(case);
}
CaseLabel::Value(ConstExpr::Scoped(s)) => {
let prefix = if first { " " } else { " | " };
first = false;
let qual = s
.parts
.iter()
.map(|p| p.text.clone())
.collect::<Vec<_>>()
.join(".");
let disc_str = if qual.contains('.') {
qual.clone()
} else {
alloc::format!("{enum_name}.{qual}")
};
explicit_labels.push(disc_str.clone());
let elem_ts = typespec_to_ts(&case.element.type_spec)?;
let field_name = case.element.declarator.name().text.clone();
out.push_str(&alloc::format!(
"{prefix}{{ discriminator: {disc_str}; {field_name}: {elem_ts} }}\n"
));
emitted_label = true;
}
CaseLabel::Value(_) => {
let prefix = if first { " " } else { " | " };
first = false;
let elem_ts = typespec_to_ts(&case.element.type_spec)?;
let field_name = case.element.declarator.name().text.clone();
out.push_str(&alloc::format!(
"{prefix}{{ discriminator: {enum_name}; {field_name}: {elem_ts} }}\n"
));
emitted_label = true;
}
}
}
let _ = emitted_label;
}
if let Some(case) = default_case {
let prefix = if first { " " } else { " | " };
let elem_ts = typespec_to_ts(&case.element.type_spec)?;
let field_name = case.element.declarator.name().text.clone();
let labels_union = if explicit_labels.is_empty() {
"never".into()
} else {
explicit_labels.join(" | ")
};
out.push_str(&alloc::format!(
"{prefix}{{ discriminator: Exclude<{enum_name}, {labels_union}>; {field_name}: {elem_ts} }}\n"
));
}
out.push_str(";\n\n");
emit_union_descriptor(out, u, enum_name)?;
Ok(())
}
fn emit_union_descriptor(
out: &mut String,
u: &zerodds_idl::ast::UnionDef,
disc_broad: &str,
) -> Result<(), IdlTsError> {
use zerodds_idl::ast::{CaseLabel, SwitchTypeSpec};
let name = &u.name.text;
let extensibility = struct_extensibility(&u.annotations);
let disc_typeref = match &u.switch_type {
SwitchTypeSpec::Integer(i) => alloc::format!(
"{{ kind: \"primitive\", name: \"{}\" }}",
primitive_to_typeref_name(&PrimitiveType::Integer(*i))
),
SwitchTypeSpec::Octet => "{ kind: \"primitive\", name: \"octet\" }".into(),
SwitchTypeSpec::Char => "{ kind: \"primitive\", name: \"char\" }".into(),
SwitchTypeSpec::Boolean => "{ kind: \"primitive\", name: \"boolean\" }".into(),
SwitchTypeSpec::Scoped(s) => {
let qname = s
.parts
.iter()
.map(|p| p.text.clone())
.collect::<Vec<_>>()
.join("::");
alloc::format!("{{ kind: \"ref\", name: \"{qname}\" }}")
}
};
let _ = disc_broad;
let mut has_default = false;
let mut next_id: i64 = 0;
out.push_str(&alloc::format!(
"export const {name}Type: DdsTypeDescriptor<{name}> = {{\n"
));
out.push_str(" kind: \"union\",\n");
out.push_str(&alloc::format!(" name: \"{name}\",\n"));
out.push_str(&alloc::format!(" extensibility: \"{extensibility}\",\n"));
out.push_str(" nested: false,\n");
out.push_str(" fields: [\n");
out.push_str(" {\n");
out.push_str(" name: \"discriminator\",\n");
out.push_str(" id: 0xFFFFFFFF,\n");
out.push_str(&alloc::format!(" type: {disc_typeref},\n"));
out.push_str(" key: false,\n");
out.push_str(" optional: false,\n");
out.push_str(" mustUnderstand: false,\n");
out.push_str(" },\n");
for case in &u.cases {
let mut labels_lit: Vec<String> = Vec::new();
let mut is_default = false;
for label in &case.labels {
match label {
CaseLabel::Default => {
is_default = true;
has_default = true;
}
CaseLabel::Value(expr) => {
if let Some(s) = render_descriptor_label(expr) {
labels_lit.push(s);
}
}
}
}
let elem_ts_ref = typespec_to_typeref_literal(&case.element.type_spec);
let id_override = annotation_int_value(&case.element.annotations, "id");
let id = id_override.unwrap_or(next_id);
next_id = id + 1;
let field_name = case.element.declarator.name().text.clone();
out.push_str(" {\n");
out.push_str(&alloc::format!(" name: \"{field_name}\",\n"));
out.push_str(&alloc::format!(" id: {id},\n"));
out.push_str(&alloc::format!(" type: {elem_ts_ref},\n"));
out.push_str(" key: false,\n");
out.push_str(" optional: false,\n");
out.push_str(" mustUnderstand: false,\n");
if !is_default && !labels_lit.is_empty() {
out.push_str(&alloc::format!(
" labels: [{}],\n",
labels_lit.join(", ")
));
}
out.push_str(" },\n");
}
out.push_str(" ],\n");
out.push_str(&alloc::format!(" hasDefault: {has_default},\n"));
out.push_str(&alloc::format!(" typeGuard: is{name},\n"));
out.push_str("};\n");
out.push_str(&alloc::format!(
"export function is{name}(v: unknown): v is {name} {{\n"
));
out.push_str(" if (typeof v !== \"object\" || v === null) return false;\n");
out.push_str(" const o = v as Record<string, unknown>;\n");
out.push_str(" return \"discriminator\" in o;\n");
out.push_str("}\n\n");
out.push_str(&alloc::format!("registerType({name}Type);\n\n"));
Ok(())
}
fn render_descriptor_label(expr: &zerodds_idl::ast::ConstExpr) -> Option<String> {
use zerodds_idl::ast::{ConstExpr, LiteralKind};
if let Some(n) = eval_const_int(expr) {
return Some(alloc::format!("{n}"));
}
if let ConstExpr::Literal(lit) = expr {
return Some(match lit.kind {
LiteralKind::Boolean => match lit.raw.to_lowercase().as_str() {
"true" | "1" => "true".into(),
_ => "false".into(),
},
LiteralKind::Char | LiteralKind::WideChar => {
let raw = lit.raw.as_str();
let trimmed = raw
.strip_prefix('L')
.unwrap_or(raw)
.strip_prefix('\'')
.and_then(|s| s.strip_suffix('\''))
.unwrap_or(raw);
alloc::format!("\"{trimmed}\"")
}
_ => lit.raw.clone(),
});
}
if let ConstExpr::Scoped(s) = expr {
let qual = s
.parts
.iter()
.map(|p| p.text.clone())
.collect::<Vec<_>>()
.join(".");
return Some(alloc::format!("\"{qual}\""));
}
None
}
fn emit_bitset(out: &mut String, b: &zerodds_idl::ast::BitsetDecl) -> Result<(), IdlTsError> {
let mut total: i64 = 0;
for bf in &b.bitfields {
if let Some(w) = eval_const_int(&bf.spec.width) {
total = total.saturating_add(w);
}
}
if total > 64 {
return Err(IdlTsError::Unsupported(alloc::format!(
"bitset {} total width {total} > 64 (DDS-TS 1.0 §7.7)",
b.name.text
)));
}
out.push_str(&alloc::format!("export interface {} {{\n", b.name.text));
for bf in &b.bitfields {
if let Some(name) = &bf.name {
let width = eval_const_int(&bf.spec.width).unwrap_or(0);
let ts_ty = if width > 32 { "bigint" } else { "number" };
out.push_str(&alloc::format!(" {}: {ts_ty};\n", name.text));
}
}
out.push_str("}\n\n");
for bf in &b.bitfields {
if let Some(name) = &bf.name {
let width = const_expr_to_ts(&bf.spec.width);
out.push_str(&alloc::format!(
"export const {}_{}_BITS = {width};\n",
b.name.text,
name.text
));
}
}
out.push('\n');
let bs_name = &b.name.text;
out.push_str(&alloc::format!(
"export function is{bs_name}(v: unknown): v is {bs_name} {{\n"
));
out.push_str(" if (typeof v !== \"object\" || v === null) return false;\n");
out.push_str(" const o = v as Record<string, unknown>;\n");
for bf in &b.bitfields {
if let Some(name) = &bf.name {
let width = eval_const_int(&bf.spec.width).unwrap_or(0);
let ts_ty = if width > 32 { "bigint" } else { "number" };
out.push_str(&alloc::format!(
" if (typeof o.{} !== \"{ts_ty}\") return false;\n",
name.text
));
}
}
out.push_str(" return true;\n}\n\n");
out.push_str(&alloc::format!(
"export const {bs_name}Type: DdsTypeDescriptor<{bs_name}> = {{\n"
));
out.push_str(" kind: \"bitset\",\n");
out.push_str(&alloc::format!(" name: \"{bs_name}\",\n"));
out.push_str(" extensibility: \"final\",\n");
out.push_str(" nested: false,\n");
out.push_str(&alloc::format!(" bitBound: {total},\n"));
out.push_str(" fields: [\n");
let mut next_id: i64 = 0;
for bf in &b.bitfields {
if let Some(name) = &bf.name {
let width = eval_const_int(&bf.spec.width).unwrap_or(0);
out.push_str(" {\n");
out.push_str(&alloc::format!(" name: \"{}\",\n", name.text));
out.push_str(&alloc::format!(" id: {next_id},\n"));
out.push_str(&alloc::format!(
" type: {{ kind: \"bitfield\", width: {width} }},\n"
));
out.push_str(" key: false,\n");
out.push_str(" optional: false,\n");
out.push_str(" mustUnderstand: false,\n");
out.push_str(" },\n");
next_id += 1;
}
}
out.push_str(" ],\n");
out.push_str(&alloc::format!(" typeGuard: is{bs_name},\n"));
out.push_str("};\n");
out.push_str(&alloc::format!("registerType({bs_name}Type);\n\n"));
Ok(())
}
fn emit_bitmask(out: &mut String, b: &zerodds_idl::ast::BitmaskDecl) -> Result<(), IdlTsError> {
let bit_bound = annotation_int_value(&b.annotations, "bit_bound").unwrap_or(32);
let use_bigint = bit_bound > 32;
let alias_ty = if use_bigint { "bigint" } else { "number" };
out.push_str(&alloc::format!("export const {} = {{\n", b.name.text));
for (i, v) in b.values.iter().enumerate() {
let pos = annotation_int_value(&v.annotations, "position").unwrap_or(i as i64);
let shift = if use_bigint {
alloc::format!("1n << {pos}n")
} else {
alloc::format!("(1 << {pos}) >>> 0")
};
out.push_str(&alloc::format!(" {}: {shift},\n", v.name.text));
}
out.push_str("} as const;\n");
out.push_str(&alloc::format!(
"export type {} = {alias_ty};\n",
b.name.text
));
out.push_str(&alloc::format!(
"export const {}_BIT_BOUND = {bit_bound};\n\n",
b.name.text
));
let bm_name = &b.name.text;
out.push_str(&alloc::format!(
"export function is{bm_name}(v: unknown): v is {bm_name} {{\n"
));
out.push_str(&alloc::format!(" return typeof v === \"{alias_ty}\";\n"));
out.push_str("}\n\n");
out.push_str(&alloc::format!(
"export const {bm_name}Type: DdsTypeDescriptor<{bm_name}> = {{\n"
));
out.push_str(" kind: \"bitmask\",\n");
out.push_str(&alloc::format!(" name: \"{bm_name}\",\n"));
out.push_str(" extensibility: \"final\",\n");
out.push_str(" nested: false,\n");
out.push_str(&alloc::format!(" bitBound: {bit_bound},\n"));
out.push_str(" fields: [\n");
for (i, v) in b.values.iter().enumerate() {
let pos = annotation_int_value(&v.annotations, "position").unwrap_or(i as i64);
out.push_str(" {\n");
out.push_str(&alloc::format!(" name: \"{}\",\n", v.name.text));
out.push_str(&alloc::format!(" id: {i},\n"));
out.push_str(" type: { kind: \"bitfield\", width: 1 },\n");
out.push_str(" key: false,\n");
out.push_str(" optional: false,\n");
out.push_str(" mustUnderstand: false,\n");
out.push_str(&alloc::format!(" default: {pos},\n"));
out.push_str(" },\n");
}
out.push_str(" ],\n");
out.push_str(&alloc::format!(" typeGuard: is{bm_name},\n"));
out.push_str("};\n");
out.push_str(&alloc::format!("registerType({bm_name}Type);\n\n"));
Ok(())
}
fn emit_typedef(out: &mut String, t: &zerodds_idl::ast::TypedefDecl) -> Result<(), IdlTsError> {
use zerodds_idl::ast::Declarator;
let bit_bound = annotation_int_value(&t.annotations, "bit_bound");
let base_ts = if let Some(n) = bit_bound {
if n > 32 && is_integer_typespec(&t.type_spec) {
"bigint".into()
} else {
typespec_to_ts(&t.type_spec)?
}
} else {
typespec_to_ts(&t.type_spec)?
};
for d in &t.declarators {
let alias = match d {
Declarator::Simple(name) => {
out.push_str(&alloc::format!("export type {} = {base_ts};\n", name.text));
name.text.clone()
}
Declarator::Array(arr) => {
out.push_str(&alloc::format!(
"export type {} = Array<{base_ts}>;\n",
arr.name.text
));
if arr.sizes.len() == 1 {
if let Some(len) = eval_const_int(&arr.sizes[0]) {
out.push_str(&alloc::format!(
"export const {}_LENGTH = {len};\n",
arr.name.text
));
}
} else {
for (i, sz) in arr.sizes.iter().enumerate() {
if let Some(len) = eval_const_int(sz) {
out.push_str(&alloc::format!(
"export const {}_LENGTH_DIM{} = {len};\n",
arr.name.text,
i + 1
));
}
}
}
arr.name.text.clone()
}
};
if let TypeSpec::String(StringType { bound: Some(n), .. }) = &t.type_spec {
if let Some(width) = eval_const_int(n) {
out.push_str(&alloc::format!("export const {alias}_BOUND = {width};\n"));
}
}
if let TypeSpec::Sequence(seq) = &t.type_spec {
if let Some(bound) = &seq.bound {
if let Some(width) = eval_const_int(bound) {
out.push_str(&alloc::format!("export const {alias}_BOUND = {width};\n"));
}
}
}
let typeof_check = typespec_typeof_check(&t.type_spec);
out.push_str(&alloc::format!(
"export function is{alias}(v: unknown): v is {alias} {{\n"
));
if let Some(check) = &typeof_check {
let bb_override = bit_bound.filter(|&n| n > 32 && is_integer_typespec(&t.type_spec));
let effective = if bb_override.is_some() {
"typeof v !== \"bigint\"".to_string()
} else {
check.replace("VAR", "v")
};
out.push_str(&alloc::format!(" if ({effective}) return false;\n"));
}
out.push_str(" return true;\n}\n");
let inner_ref = typespec_to_typeref_literal(&t.type_spec);
out.push_str(&alloc::format!(
"export const {alias}Type: DdsTypeDescriptor<{alias}> = {{\n"
));
out.push_str(" kind: \"alias\",\n");
out.push_str(&alloc::format!(" name: \"{alias}\",\n"));
out.push_str(" extensibility: \"appendable\",\n");
out.push_str(" nested: false,\n");
if let Some(n) = bit_bound {
out.push_str(&alloc::format!(" bitBound: {n},\n"));
}
out.push_str(" fields: [\n");
out.push_str(" {\n");
out.push_str(" name: \"value\",\n");
out.push_str(" id: 0,\n");
out.push_str(&alloc::format!(" type: {inner_ref},\n"));
out.push_str(" key: false,\n");
out.push_str(" optional: false,\n");
out.push_str(" mustUnderstand: false,\n");
out.push_str(" },\n");
out.push_str(" ],\n");
out.push_str(&alloc::format!(" typeGuard: is{alias},\n"));
out.push_str("};\n");
out.push_str(&alloc::format!("registerType({alias}Type);\n\n"));
}
Ok(())
}
fn is_integer_typespec(t: &TypeSpec) -> bool {
matches!(t, TypeSpec::Primitive(PrimitiveType::Integer(_)))
}
fn wrap_with_array_dimensions(base: &str, d: &zerodds_idl::ast::Declarator) -> String {
use zerodds_idl::ast::Declarator;
match d {
Declarator::Simple(_) => base.to_string(),
Declarator::Array(arr) => {
let mut out = base.to_string();
for _ in &arr.sizes {
out = alloc::format!("Array<{out}>");
}
out
}
}
}
fn emit_interface(out: &mut String, i: &zerodds_idl::ast::InterfaceDef) -> Result<(), IdlTsError> {
use zerodds_idl::ast::Export;
let name = &i.name.text;
let client_name = alloc::format!("{name}Client");
let handler_name = alloc::format!("{name}Handler");
let base_client = i
.bases
.iter()
.map(|b| {
let qual = b
.parts
.iter()
.map(|p| p.text.clone())
.collect::<Vec<_>>()
.join(".");
alloc::format!("{qual}Client")
})
.collect::<Vec<_>>()
.join(", ");
let base_handler = i
.bases
.iter()
.map(|b| {
let qual = b
.parts
.iter()
.map(|p| p.text.clone())
.collect::<Vec<_>>()
.join(".");
alloc::format!("{qual}Handler")
})
.collect::<Vec<_>>()
.join(", ");
out.push_str(&alloc::format!("export interface {client_name}"));
if !base_client.is_empty() {
out.push_str(&alloc::format!(" extends {base_client}"));
}
out.push_str(" {\n");
emit_verbatim_at(out, &i.annotations, VerbatimPlacement::BeginDeclaration);
for ex in &i.exports {
match ex {
Export::Op(op) => {
emit_op_method(out, op)?;
}
Export::Attr(attr) => {
emit_attr_methods(out, attr)?;
}
_ => {}
}
}
emit_verbatim_at(out, &i.annotations, VerbatimPlacement::EndDeclaration);
out.push_str("}\n\n");
out.push_str(&alloc::format!("export interface {handler_name}"));
if !base_handler.is_empty() {
out.push_str(&alloc::format!(" extends {base_handler}"));
}
out.push_str(" {\n");
emit_verbatim_at(out, &i.annotations, VerbatimPlacement::BeginDeclaration);
for ex in &i.exports {
match ex {
Export::Op(op) => emit_op_method(out, op)?,
Export::Attr(attr) => emit_attr_methods(out, attr)?,
_ => {}
}
}
emit_verbatim_at(out, &i.annotations, VerbatimPlacement::EndDeclaration);
out.push_str("}\n\n");
out.push_str(&alloc::format!(
"export const {name}Service: ServiceDescriptor<{client_name}, {handler_name}> = {{\n"
));
out.push_str(&alloc::format!(" name: \"{name}\",\n"));
out.push_str(" inherits: [");
for (idx, b) in i.bases.iter().enumerate() {
let qual = b
.parts
.iter()
.map(|p| p.text.clone())
.collect::<Vec<_>>()
.join(".");
if idx > 0 {
out.push_str(", ");
}
out.push_str(&alloc::format!("{qual}Service"));
}
out.push_str("],\n");
out.push_str(" operations: [\n");
for ex in &i.exports {
if let Export::Op(op) = ex {
emit_op_descriptor(out, op);
}
}
out.push_str(" ],\n");
out.push_str(" attributes: [\n");
for ex in &i.exports {
if let Export::Attr(attr) = ex {
emit_attr_descriptor(out, attr);
}
}
out.push_str(" ],\n");
out.push_str("};\n\n");
Ok(())
}
fn emit_op_method(out: &mut String, op: &zerodds_idl::ast::OpDecl) -> Result<(), IdlTsError> {
use zerodds_idl::ast::ParamAttribute;
let return_ts = match &op.return_type {
Some(t) => typespec_to_ts(t)?,
None => "void".into(),
};
let mut params: Vec<String> = Vec::new();
let mut out_params: Vec<(String, String)> = Vec::new();
for p in &op.params {
let ts = typespec_to_ts(&p.type_spec)?;
match p.attribute {
ParamAttribute::In => {
params.push(alloc::format!("{}: {ts}", p.name.text));
}
ParamAttribute::InOut => {
params.push(alloc::format!("{}: {ts}", p.name.text));
out_params.push((p.name.text.clone(), ts));
}
ParamAttribute::Out => {
out_params.push((p.name.text.clone(), ts));
}
}
}
let resolve_ts = if out_params.is_empty() {
return_ts.clone()
} else {
let mut entries: Vec<String> = Vec::new();
if op.return_type.is_some() {
entries.push(alloc::format!("result: {return_ts}"));
}
for (n, t) in &out_params {
entries.push(alloc::format!("{n}: {t}"));
}
alloc::format!("{{ {} }}", entries.join("; "))
};
let promise_ts = if op.return_type.is_none() && out_params.is_empty() {
"Promise<void>".into()
} else {
alloc::format!("Promise<{resolve_ts}>")
};
out.push_str(&alloc::format!(
" {}({}): {promise_ts};\n",
op.name.text,
params.join(", ")
));
Ok(())
}
fn emit_attr_methods(
out: &mut String,
attr: &zerodds_idl::ast::AttrDecl,
) -> Result<(), IdlTsError> {
let ts = typespec_to_ts(&attr.type_spec)?;
out.push_str(&alloc::format!(
" get_{}(): Promise<{ts}>;\n",
attr.name.text
));
if !attr.readonly {
out.push_str(&alloc::format!(
" set_{}(value: {ts}): Promise<void>;\n",
attr.name.text
));
}
Ok(())
}
fn emit_op_descriptor(out: &mut String, op: &zerodds_idl::ast::OpDecl) {
use zerodds_idl::ast::ParamAttribute;
out.push_str(" {\n");
out.push_str(&alloc::format!(" name: \"{}\",\n", op.name.text));
out.push_str(&alloc::format!(" oneway: {},\n", op.oneway));
let return_ref = match &op.return_type {
Some(t) => typespec_to_typeref_literal(t),
None => "{ kind: \"void\" }".into(),
};
out.push_str(&alloc::format!(" returnType: {return_ref},\n"));
out.push_str(" parameters: [\n");
for p in &op.params {
let mode = match p.attribute {
ParamAttribute::In => "in",
ParamAttribute::Out => "out",
ParamAttribute::InOut => "inout",
};
let pref = typespec_to_typeref_literal(&p.type_spec);
out.push_str(" {\n");
out.push_str(&alloc::format!(
" name: \"{}\",\n",
p.name.text
));
out.push_str(&alloc::format!(" mode: \"{mode}\",\n"));
out.push_str(&alloc::format!(" type: {pref},\n"));
out.push_str(" },\n");
}
out.push_str(" ],\n");
out.push_str(" raises: [");
for (idx, r) in op.raises.iter().enumerate() {
let qual = r
.parts
.iter()
.map(|p| p.text.clone())
.collect::<Vec<_>>()
.join(".");
if idx > 0 {
out.push_str(", ");
}
out.push_str(&alloc::format!("{qual}Type"));
}
out.push_str("],\n");
out.push_str(" },\n");
}
fn emit_attr_descriptor(out: &mut String, attr: &zerodds_idl::ast::AttrDecl) {
out.push_str(" {\n");
out.push_str(&alloc::format!(
" name: \"{}\",\n",
attr.name.text
));
out.push_str(&alloc::format!(
" readonly: {},\n",
attr.readonly
));
let tref = typespec_to_typeref_literal(&attr.type_spec);
out.push_str(&alloc::format!(" type: {tref},\n"));
out.push_str(" getRaises: [");
for (idx, r) in attr.get_raises.iter().enumerate() {
let qual = r
.parts
.iter()
.map(|p| p.text.clone())
.collect::<Vec<_>>()
.join(".");
if idx > 0 {
out.push_str(", ");
}
out.push_str(&alloc::format!("{qual}Type"));
}
out.push_str("],\n");
out.push_str(" setRaises: [");
for (idx, r) in attr.set_raises.iter().enumerate() {
let qual = r
.parts
.iter()
.map(|p| p.text.clone())
.collect::<Vec<_>>()
.join(".");
if idx > 0 {
out.push_str(", ");
}
out.push_str(&alloc::format!("{qual}Type"));
}
out.push_str("],\n");
out.push_str(" },\n");
}
fn is_numeric_literal_text(s: &str) -> bool {
let trimmed = s.trim();
if trimmed.is_empty() {
return false;
}
let mut chars = trimmed.chars();
let first = chars.next().unwrap_or(' ');
if !(first.is_ascii_digit() || first == '-' || first == '+' || first == '.') {
return false;
}
trimmed
.chars()
.all(|c| c.is_ascii_digit() || c == '.' || c == '-' || c == '+' || c == 'e' || c == 'E')
}
fn render_tsdoc_for_member(annotations: &[zerodds_idl::ast::Annotation]) -> Option<String> {
let mut tags: Vec<String> = Vec::new();
if let Some(unit) = annotation_string_value(annotations, "unit") {
tags.push(alloc::format!(" * @dds-unit {unit}"));
}
if let Some(min) = annotation_const_text(annotations, "min") {
tags.push(alloc::format!(" * @dds-min {min}"));
}
if let Some(max) = annotation_const_text(annotations, "max") {
tags.push(alloc::format!(" * @dds-max {max}"));
}
if has_annotation(annotations, "must_understand") {
tags.push(" * @dds-must-understand".into());
}
if has_annotation(annotations, "nested") {
tags.push(" * @dds-nested".into());
}
if let Some(hashid) = annotation_string_value(annotations, "hashid") {
tags.push(alloc::format!(" * @dds-hashid {hashid}"));
}
if let Some(id) = annotation_int_value(annotations, "id") {
tags.push(alloc::format!(" * @dds-id {id}"));
}
if let Some(key) = annotation_int_value(annotations, "key").or_else(|| {
if has_annotation(annotations, "key") {
Some(0)
} else {
None
}
}) {
let _ = key;
tags.push(" * @dds-key".into());
}
if tags.is_empty() {
return None;
}
let mut out = String::from(" /**\n");
for t in &tags {
out.push_str(" ");
out.push_str(t);
out.push('\n');
}
out.push_str(" */\n");
Some(out)
}
fn annotation_const_text(
annotations: &[zerodds_idl::ast::Annotation],
name: &str,
) -> Option<String> {
use zerodds_idl::ast::{AnnotationParams, ConstExpr, LiteralKind};
for a in annotations {
if a.name.parts.len() == 1 && a.name.parts[0].text == name {
if let AnnotationParams::Single(expr) = &a.params {
if let Some(n) = eval_const_int(expr) {
return Some(alloc::format!("{n}"));
}
if let ConstExpr::Literal(lit) = expr {
return Some(match lit.kind {
LiteralKind::String | LiteralKind::WideString => {
let raw = lit.raw.as_str();
let trimmed = raw
.strip_prefix('L')
.unwrap_or(raw)
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or(raw);
alloc::string::ToString::to_string(trimmed)
}
_ => lit.raw.clone(),
});
}
}
}
}
None
}
fn annotation_default_to_ts(annotations: &[zerodds_idl::ast::Annotation]) -> Option<String> {
use zerodds_idl::ast::{AnnotationParams, ConstExpr, LiteralKind};
for a in annotations {
if a.name.parts.len() == 1 && a.name.parts[0].text == "default" {
if let AnnotationParams::Single(expr) = &a.params {
if let ConstExpr::Literal(lit) = expr {
return Some(match lit.kind {
LiteralKind::String | LiteralKind::WideString => {
let raw = lit.raw.as_str();
let trimmed = raw
.strip_prefix('L')
.unwrap_or(raw)
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or(raw);
alloc::format!("\"{trimmed}\"")
}
LiteralKind::Boolean => match lit.raw.to_lowercase().as_str() {
"true" | "1" => "true".into(),
_ => "false".into(),
},
LiteralKind::Char | LiteralKind::WideChar => {
let raw = lit.raw.as_str();
let trimmed = raw
.strip_prefix('L')
.unwrap_or(raw)
.strip_prefix('\'')
.and_then(|s| s.strip_suffix('\''))
.unwrap_or(raw);
alloc::format!("\"{trimmed}\"")
}
_ => lit.raw.clone(),
});
}
if let Some(n) = eval_const_int(expr) {
return Some(alloc::format!("{n}"));
}
}
}
}
None
}
fn emit_struct_default_constants(
out: &mut String,
s: &zerodds_idl::ast::StructDef,
) -> Result<(), IdlTsError> {
let type_name = &s.name.text;
let mut emitted = false;
for m in &s.members {
if let Some(lit) = annotation_default_to_ts(&m.annotations) {
let ts_ty = typespec_to_ts(&m.type_spec)?;
for d in &m.declarators {
out.push_str(&alloc::format!(
"export const {type_name}_{}_DEFAULT: {ts_ty} = {lit};\n",
d.name().text
));
emitted = true;
}
}
}
if emitted {
out.push('\n');
}
Ok(())
}
fn const_expr_to_ts(e: &zerodds_idl::ast::ConstExpr) -> String {
eval_const_int(e)
.map(|n| alloc::format!("{n}"))
.unwrap_or_else(|| String::from("0"))
}
pub(crate) fn eval_const_int(e: &zerodds_idl::ast::ConstExpr) -> Option<i64> {
use zerodds_idl::ast::{BinaryOp, ConstExpr, LiteralKind, UnaryOp};
match e {
ConstExpr::Literal(l) if l.kind == LiteralKind::Integer => parse_int_literal(&l.raw),
ConstExpr::Literal(l) if l.kind == LiteralKind::Boolean => {
if l.raw == "TRUE" {
Some(1)
} else {
Some(0)
}
}
ConstExpr::Literal(_) | ConstExpr::Scoped(_) => None,
ConstExpr::Unary { op, operand, .. } => {
let v = eval_const_int(operand)?;
Some(match op {
UnaryOp::Plus => v,
UnaryOp::Minus => v.checked_neg()?,
UnaryOp::BitNot => !v,
})
}
ConstExpr::Binary { op, lhs, rhs, .. } => {
let a = eval_const_int(lhs)?;
let b = eval_const_int(rhs)?;
match op {
BinaryOp::Or => Some(a | b),
BinaryOp::Xor => Some(a ^ b),
BinaryOp::And => Some(a & b),
BinaryOp::Shl => u32::try_from(b).ok().and_then(|s| a.checked_shl(s)),
BinaryOp::Shr => u32::try_from(b).ok().and_then(|s| a.checked_shr(s)),
BinaryOp::Add => a.checked_add(b),
BinaryOp::Sub => a.checked_sub(b),
BinaryOp::Mul => a.checked_mul(b),
BinaryOp::Div => a.checked_div(b),
BinaryOp::Mod => a.checked_rem(b),
}
}
}
}
fn parse_int_literal(raw: &str) -> Option<i64> {
let s = raw.trim();
if let Some(rest) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
i64::from_str_radix(rest, 16).ok()
} else if s.len() > 1 && s.starts_with('0') && s.chars().all(|c| c.is_ascii_digit()) {
i64::from_str_radix(&s[1..], 8).ok()
} else {
s.parse::<i64>().ok()
}
}
pub(crate) fn typespec_to_ts(t: &TypeSpec) -> Result<String, IdlTsError> {
Ok(match t {
TypeSpec::Primitive(p) => match p {
PrimitiveType::Boolean => "boolean".into(),
PrimitiveType::Char => "Char".into(),
PrimitiveType::WideChar => "WChar".into(),
PrimitiveType::Octet => "number".into(),
PrimitiveType::Integer(i) => match i {
IntegerType::Short
| IntegerType::UShort
| IntegerType::Long
| IntegerType::ULong
| IntegerType::Int8
| IntegerType::UInt8
| IntegerType::Int16
| IntegerType::UInt16
| IntegerType::Int32
| IntegerType::UInt32 => "number".into(),
IntegerType::LongLong
| IntegerType::ULongLong
| IntegerType::Int64
| IntegerType::UInt64 => "bigint".into(),
},
PrimitiveType::Floating(f) => match f {
FloatingType::Float | FloatingType::Double => "number".into(),
FloatingType::LongDouble => "LongDouble".into(),
},
},
TypeSpec::String(StringType { wide: false, .. }) => "string".into(),
TypeSpec::String(StringType { wide: true, .. }) => "string".into(),
TypeSpec::Sequence(s) => {
let inner = typespec_to_ts(&s.elem)?;
alloc::format!("Array<{inner}>")
}
TypeSpec::Scoped(s) => s
.parts
.iter()
.map(|p| p.text.clone())
.collect::<Vec<_>>()
.join("."),
TypeSpec::Fixed(_) => "string".into(),
TypeSpec::Any => "DdsAny".into(),
TypeSpec::Map(m) => {
let k = typespec_to_ts(&m.key)?;
let v = typespec_to_ts(&m.value)?;
alloc::format!("ReadonlyMap<{k}, {v}>")
}
})
}
pub mod runtime {
pub const TYPES_TS: &str = include_str!("runtime/types.ts");
pub const BRANDED_TS: &str = include_str!("runtime/branded.ts");
pub const DDS_ANY_TS: &str = include_str!("runtime/dds_any.ts");
pub const REGISTRY_TS: &str = include_str!("runtime/registry.ts");
pub const EQUAL_TS: &str = include_str!("runtime/equal.ts");
pub const OPERATIONS_TS: &str = include_str!("runtime/operations.ts");
pub const WASM_TS: &str = include_str!("runtime/wasm.ts");
pub const TEST_BACKEND_TS: &str = include_str!("runtime/test_backend.ts");
pub const INDEX_TS: &str = include_str!("runtime/index.ts");
pub const ALL: &[(&str, &str)] = &[
("types.ts", TYPES_TS),
("branded.ts", BRANDED_TS),
("dds_any.ts", DDS_ANY_TS),
("registry.ts", REGISTRY_TS),
("equal.ts", EQUAL_TS),
("operations.ts", OPERATIONS_TS),
("wasm.ts", WASM_TS),
("test_backend.ts", TEST_BACKEND_TS),
("index.ts", INDEX_TS),
];
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IdlTsError {
Unsupported(String),
}
impl core::fmt::Display for IdlTsError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Unsupported(s) => write!(f, "TS-codegen: unsupported {s}"),
}
}
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
use zerodds_idl::config::ParserConfig;
fn gen_ts(src: &str) -> String {
let ast = zerodds_idl::parse(src, &ParserConfig::default()).expect("parse");
generate_ts_source(&ast).expect("gen")
}
fn gen_ts_full(src: &str) -> String {
use zerodds_idl::features::IdlFeatures;
let cfg = ParserConfig {
features: IdlFeatures::all(),
..ParserConfig::default()
};
let ast = zerodds_idl::parse(src, &cfg).expect("parse");
generate_ts_source(&ast).expect("gen")
}
#[test]
fn struct_emits_typescript_interface() {
let ts = gen_ts(r"struct Point { long x; long y; };");
assert!(ts.contains("export interface Point"));
assert!(ts.contains("x: number"));
assert!(ts.contains("y: number"));
}
#[test]
fn struct_emits_descriptor_typeguard_and_registertype() {
let ts = gen_ts(r"struct Point { long x; long y; };");
assert!(
ts.contains("export function isPoint(v: unknown): v is Point"),
"got:\n{ts}"
);
assert!(ts.contains("export const PointType: DdsTypeDescriptor<Point>"));
assert!(ts.contains("kind: \"struct\""));
assert!(ts.contains("extensibility: \"appendable\""));
assert!(ts.contains("typeGuard: isPoint"));
assert!(ts.contains("registerType(PointType);"));
}
#[test]
fn struct_with_final_annotation_sets_extensibility() {
let ts = gen_ts(r"@final struct Point { long x; };");
assert!(ts.contains("extensibility: \"final\""));
}
#[test]
fn struct_no_class_keyword_emitted() {
let ts = gen_ts(r"@final struct Point { @key long x; long y; };");
assert!(
!ts.contains("export class"),
"TS class keyword forbidden, got:\n{ts}"
);
assert!(ts.contains("key: true"));
}
#[test]
fn struct_bounded_string_emits_bound_constant() {
let ts = gen_ts(r"struct Sample { string<32> name; };");
assert!(
ts.contains("export const Sample_name_BOUND = 32"),
"got:\n{ts}"
);
}
#[test]
fn struct_bounded_sequence_emits_bound_constant() {
let ts = gen_ts(r"struct Sample { sequence<long, 16> readings; };");
assert!(ts.contains("export const Sample_readings_BOUND = 16"));
}
#[test]
fn struct_optional_member_emits_optional_marker() {
let ts = gen_ts(r"struct Frame { long seq; @optional long retry; };");
assert!(ts.contains("retry?: number | undefined"));
assert!(ts.contains("optional: true"));
}
#[test]
fn typedef_emits_descriptor() {
let ts = gen_ts(r"typedef long Counter;");
assert!(ts.contains("export type Counter = number"));
assert!(ts.contains("export function isCounter"));
assert!(ts.contains("export const CounterType: DdsTypeDescriptor<Counter>"));
assert!(ts.contains("kind: \"alias\""));
assert!(ts.contains("registerType(CounterType);"));
}
#[test]
fn typedef_bit_bound_above_32_switches_to_bigint() {
let ts = gen_ts(r"@bit_bound(40) typedef long long Counter40;");
assert!(ts.contains("export type Counter40 = bigint"), "got:\n{ts}");
assert!(ts.contains("bitBound: 40"));
}
#[test]
fn primitive_mapping_uses_branded_carriers() {
let ts = gen_ts(
r"struct Sample {
char c;
wchar wc;
long double ld;
any a;
};",
);
assert!(ts.contains("c: Char"), "got:\n{ts}");
assert!(ts.contains("wc: WChar"));
assert!(ts.contains("ld: LongDouble"));
assert!(ts.contains("a: DdsAny"));
}
#[test]
fn map_mapping_uses_readonly_map() {
let ts = gen_ts(r"struct Index { map<string, long> by_name; };");
assert!(ts.contains("by_name: ReadonlyMap<string, number>"));
}
#[test]
fn runtime_import_block_present() {
let ts = gen_ts(r"struct S { long v; };");
assert!(ts.contains("from \"@zerodds/types\""));
assert!(ts.contains("DdsTypeDescriptor"));
assert!(ts.contains("registerType"));
}
#[test]
fn bitset_descriptor_emitted_with_bitfield_kind() {
let ts = gen_ts(r"bitset Flags { bitfield<3> low; bitfield<5> high; };");
assert!(ts.contains("export const FlagsType: DdsTypeDescriptor<Flags>"));
assert!(ts.contains("kind: \"bitset\""));
assert!(ts.contains("kind: \"bitfield\", width: 3"));
assert!(ts.contains("kind: \"bitfield\", width: 5"));
assert!(ts.contains("bitBound: 8"));
assert!(ts.contains("registerType(FlagsType);"));
}
#[test]
fn bitset_total_width_above_64_rejected() {
let src = r"bitset Big { bitfield<40> a; bitfield<40> b; };";
let ast =
zerodds_idl::parse(src, &zerodds_idl::config::ParserConfig::default()).expect("parse");
let err = generate_ts_source(&ast).expect_err("must reject total > 64");
match err {
IdlTsError::Unsupported(msg) => {
assert!(msg.contains("total width"), "msg: {msg}");
}
}
}
#[test]
fn const_decl_emits_export_const_with_typed_literal() {
let ts = gen_ts(r"const long MAX_RETRIES = 5;");
assert!(
ts.contains("export const MAX_RETRIES: number = 5;"),
"got:\n{ts}"
);
}
#[test]
fn const_decl_long_long_uses_bigint_suffix() {
let ts = gen_ts(r"const long long BIG = 9007199254740993;");
assert!(
ts.contains("export const BIG: bigint = 9007199254740993n;"),
"got:\n{ts}"
);
}
#[test]
fn const_decl_string_emits_double_quoted_literal() {
let ts = gen_ts(r#"const string SERVICE_NAME = "telemetry";"#);
assert!(
ts.contains("export const SERVICE_NAME: string = \"telemetry\";"),
"got:\n{ts}"
);
}
#[test]
fn const_decl_boolean_normalises_token() {
let ts = gen_ts(r"const boolean DEBUG_ENABLED = FALSE;");
assert!(
ts.contains("export const DEBUG_ENABLED: boolean = false;"),
"got:\n{ts}"
);
}
#[test]
fn exception_emits_interface_extending_dds_exception() {
let ts = gen_ts(r"exception Overflow { long limit; };");
assert!(
ts.contains("export interface Overflow extends DdsException"),
"got:\n{ts}"
);
assert!(ts.contains("limit: number"));
assert!(ts.contains("export function isOverflow"));
assert!(ts.contains("kind: \"exception\""));
assert!(ts.contains("registerType(OverflowType);"));
}
#[test]
fn enum_descriptor_emitted_with_ordinal_defaults() {
let ts = gen_ts(r"enum Color { RED, GREEN, BLUE };");
assert!(ts.contains("export const ColorType"));
assert!(ts.contains("kind: \"enum\""));
assert!(ts.contains("default: 0"));
assert!(ts.contains("default: 1"));
assert!(ts.contains("default: 2"));
assert!(ts.contains("registerType(ColorType);"));
}
#[test]
fn struct_min_max_unit_emit_descriptor_fields_and_tsdoc() {
let ts = gen_ts(
r#"struct Telemetry {
@unit("celsius") @min(-40) @max(125)
double temperature;
};"#,
);
assert!(ts.contains("@dds-unit celsius"), "got:\n{ts}");
assert!(ts.contains("@dds-min -40"));
assert!(ts.contains("@dds-max 125"));
assert!(ts.contains("unit: \"celsius\""));
assert!(ts.contains("min: -40"));
assert!(ts.contains("max: 125"));
}
#[test]
fn interface_emits_client_handler_and_service_descriptor() {
let ts = gen_ts(
r"interface Calculator {
long add(in long a, in long b);
attribute string name;
};",
);
assert!(
ts.contains("export interface CalculatorClient {"),
"got:\n{ts}"
);
assert!(ts.contains("export interface CalculatorHandler {"));
assert!(ts.contains("add(a: number, b: number): Promise<number>"));
assert!(!ts.contains("async add"));
assert!(ts.contains("get_name(): Promise<string>"));
assert!(ts.contains("set_name(value: string): Promise<void>"));
assert!(ts.contains(
"export const CalculatorService: ServiceDescriptor<CalculatorClient, CalculatorHandler>"
));
assert!(ts.contains("name: \"Calculator\""));
assert!(ts.contains("inherits: []"));
assert!(ts.contains("oneway: false"));
}
#[test]
fn interface_oneway_emits_promise_void_with_oneway_descriptor() {
let ts = gen_ts_full(
r"interface Pinger {
oneway void ping();
};",
);
assert!(ts.contains("ping(): Promise<void>"));
assert!(ts.contains("oneway: true"));
}
#[test]
fn interface_inout_emits_param_and_result_property() {
let ts = gen_ts(
r"interface Cursor {
long advance(in long step, inout long position, out boolean wrapped);
};",
);
assert!(
ts.contains("advance(step: number, position: number): Promise<{ result: number; position: number; wrapped: boolean }>"),
"got:\n{ts}"
);
assert!(ts.contains("mode: \"in\""));
assert!(ts.contains("mode: \"inout\""));
assert!(ts.contains("mode: \"out\""));
}
#[test]
fn interface_readonly_attribute_no_setter() {
let ts = gen_ts(
r"interface Probe {
readonly attribute long count;
};",
);
assert!(ts.contains("get_count(): Promise<number>"));
assert!(!ts.contains("set_count"));
assert!(ts.contains("readonly: true"));
}
#[test]
fn interface_inheritance_extends_parent_client_handler_and_service() {
let ts = gen_ts(
r"interface Base {
long ping();
};
interface Counter : Base {
long increment();
};",
);
assert!(ts.contains("export interface CounterClient extends BaseClient"));
assert!(ts.contains("export interface CounterHandler extends BaseHandler"));
assert!(ts.contains("inherits: [BaseService]"));
}
#[test]
fn interface_raises_lists_exception_descriptors() {
let ts = gen_ts(
r"exception Overflow { long limit; };
interface Adder {
long add(in long a, in long b) raises (Overflow);
};",
);
assert!(ts.contains("raises: [OverflowType]"));
}
#[test]
fn verbatim_begin_file_appears_before_banner() {
let ts = gen_ts(
r#"@verbatim(language = "ts",
placement = "BEGIN_FILE",
text = "// Copyright Acme Corp.")
struct M { long v; };"#,
);
let banner_idx = ts
.find("// Generated by zerodds idl-ts")
.expect("banner must be present");
let copyright_idx = ts
.find("// Copyright Acme Corp.")
.expect("verbatim text must be emitted");
assert!(
copyright_idx < banner_idx,
"BEGIN_FILE should precede banner; got:\n{ts}"
);
}
#[test]
fn verbatim_before_after_declaration() {
let ts = gen_ts(
r#"@verbatim(language = "ts",
placement = "BEFORE_DECLARATION",
text = "// before-foo")
@verbatim(language = "ts",
placement = "AFTER_DECLARATION",
text = "// after-foo")
struct Foo { long v; };"#,
);
let before_idx = ts.find("// before-foo").expect("before");
let iface_idx = ts.find("export interface Foo").expect("interface");
let after_idx = ts.find("// after-foo").expect("after");
assert!(before_idx < iface_idx, "BEFORE before interface");
assert!(iface_idx < after_idx, "AFTER after interface");
}
#[test]
fn verbatim_inside_struct_body() {
let ts = gen_ts(
r#"@verbatim(language = "ts",
placement = "BEGIN_DECLARATION",
text = " // body-start")
@verbatim(language = "ts",
placement = "END_DECLARATION",
text = " // body-end")
struct Foo { long v; };"#,
);
let open_idx = ts.find("export interface Foo {").expect("interface header");
let begin_idx = ts.find("// body-start").expect("BEGIN_DECLARATION");
let end_idx = ts.find("// body-end").expect("END_DECLARATION");
assert!(open_idx < begin_idx);
assert!(begin_idx < end_idx);
}
#[test]
fn verbatim_language_other_is_ignored() {
let ts = gen_ts(
r#"@verbatim(language = "cpp",
placement = "BEFORE_DECLARATION",
text = "// cpp-only")
struct Foo { long v; };"#,
);
assert!(!ts.contains("// cpp-only"));
}
#[test]
fn verbatim_language_wildcard_is_emitted() {
let ts = gen_ts(
r#"@verbatim(language = "*",
placement = "AFTER_DECLARATION",
text = "// generic")
struct Foo { long v; };"#,
);
assert!(ts.contains("// generic"), "got:\n{ts}");
}
#[test]
fn verbatim_unescapes_idl_escapes() {
let ts = gen_ts(
r#"@verbatim(language = "ts",
placement = "BEFORE_DECLARATION",
text = "// line1\n// line2")
struct Foo { long v; };"#,
);
assert!(
ts.contains("// line1\n// line2"),
"expected newline-escape resolution; got:\n{ts}"
);
}
fn gen_with_diagnostics(src: &str) -> (String, alloc::vec::Vec<Diagnostic>) {
let ast = zerodds_idl::parse(src, &ParserConfig::default()).expect("parse");
generate_ts_source_with_diagnostics(&ast).expect("gen")
}
#[test]
fn diagnostic_long_double_emits_i001_per_field() {
let (_, diags) = gen_with_diagnostics(r"struct S { long double a; long double b; };");
let i001: Vec<&Diagnostic> = diags.iter().filter(|d| d.code == "DDS-TS-I001").collect();
assert_eq!(
i001.len(),
2,
"two long-double fields → two I001; got {diags:?}"
);
assert!(i001.iter().all(|d| matches!(d.severity, Severity::Info)));
}
#[test]
fn diagnostic_union_without_default_emits_w004() {
let (_, diags) = gen_with_diagnostics(
r"union NoDefault switch (long) { case 1: long a; case 2: long b; };",
);
let w004: Vec<&Diagnostic> = diags.iter().filter(|d| d.code == "DDS-TS-W004").collect();
assert_eq!(w004.len(), 1, "exactly one W004; got {diags:?}");
}
#[test]
fn diagnostic_map_struct_key_emits_w003() {
let (_, diags) = gen_with_diagnostics(
r"struct K { long id; };
struct Container { map<K, long> table; };",
);
let w003: Vec<&Diagnostic> = diags.iter().filter(|d| d.code == "DDS-TS-W003").collect();
assert_eq!(w003.len(), 1, "exactly one W003; got {diags:?}");
}
#[test]
fn diagnostic_orphan_forward_decl_is_e002_fatal() {
let ast =
zerodds_idl::parse(r"interface Orphan;", &ParserConfig::default()).expect("parse");
let err = generate_ts_source_with_diagnostics(&ast).expect_err("orphan must fail");
match err {
IdlTsError::Unsupported(msg) => {
assert!(msg.contains("DDS-TS-E002"), "msg: {msg}");
assert!(msg.contains("Orphan"));
}
}
}
#[test]
fn b_1_1_primitive_types_round_trip_strict_clean() {
let ts = gen_ts(
r"struct P {
boolean a; octet b; short c; long d; long long e;
float f; double g; string h;
};",
);
for marker in [
"a: boolean",
"b: number",
"c: number",
"d: number",
"e: bigint",
"f: number",
"g: number",
"h: string",
] {
assert!(ts.contains(marker), "B.1.1 missing {marker}");
}
assert!(!ts.contains(": any"), "B.1.1 forbids `any`");
}
#[test]
fn b_1_2_struct_emits_interface_descriptor_and_guard_no_class() {
let ts = gen_ts(r"struct Three { long a; long b; long c; };");
assert!(ts.contains("export interface Three"));
assert!(ts.contains("export const ThreeType: DdsTypeDescriptor<Three>"));
assert!(ts.contains("export function isThree"));
assert!(!ts.contains("export class"), "B.1.2 forbids class");
}
#[test]
fn b_1_3_union_emits_discriminator_per_branch() {
let ts = gen_ts(r"union U switch (long) { case 1: long a; case 2: string b; };");
assert!(ts.contains("discriminator: 1"));
assert!(ts.contains("discriminator: 2"));
}
#[test]
fn b_1_4_bitset_widths_dispatch_number_or_bigint_and_descriptor_uses_bitfield_kind() {
let ts = gen_ts(r"bitset Mixed { bitfield<8> low; bitfield<40> wide; };");
assert!(ts.contains("low: number"));
assert!(ts.contains("wide: bigint"));
assert!(ts.contains("Mixed_low_BITS = 8"));
assert!(ts.contains("Mixed_wide_BITS = 40"));
assert!(ts.contains("kind: \"bitfield\", width: 8"));
assert!(ts.contains("kind: \"bitfield\", width: 40"));
}
#[test]
fn b_1_5a_bitmask_default_uses_unsigned_32bit_shifts() {
let ts = gen_ts(r"bitmask Perm { READ, WRITE };");
assert!(ts.contains("READ: (1 << 0) >>> 0"));
assert!(ts.contains("export type Perm = number"));
assert!(ts.contains("Perm_BIT_BOUND = 32"));
}
#[test]
fn b_1_5b_bitmask_above_32_uses_bigint() {
let ts = gen_ts(r"@bit_bound(40) bitmask BF { f0, f1 };");
assert!(ts.contains("f0: 1n << 0n"));
assert!(ts.contains("export type BF = bigint"));
}
#[test]
fn b_1_6_module_emits_export_namespace_with_nested_constructs() {
let ts = gen_ts(
r"module M {
struct S { long x; };
enum E { A, B };
};",
);
assert!(ts.contains("export namespace M {"));
assert!(ts.contains("export interface S"));
assert!(ts.contains("export const E = {"));
}
#[test]
fn b_1_7_constants_export_with_typed_literal_and_bigint_suffix() {
let ts = gen_ts(
r#"const long MAX = 5;
const long long BIG = 9007199254740993;
const string NAME = "x";
const boolean OK = TRUE;"#,
);
assert!(ts.contains("export const MAX: number = 5;"));
assert!(ts.contains("export const BIG: bigint = 9007199254740993n;"));
assert!(ts.contains("export const NAME: string = \"x\";"));
assert!(ts.contains("export const OK: boolean = true;"));
}
#[test]
fn b_1_8_char_wchar_use_branded_aliases() {
let ts = gen_ts(r"struct S { char c; wchar w; };");
assert!(ts.contains("c: Char"));
assert!(ts.contains("w: WChar"));
let idx = runtime::INDEX_TS;
assert!(idx.contains("makeChar"));
assert!(idx.contains("makeWChar"));
}
#[test]
fn b_1_9_long_double_emits_long_double_carrier_no_abort() {
let ts = gen_ts(r"struct S { long double x; };");
assert!(ts.contains("x: LongDouble"));
let branded = runtime::BRANDED_TS;
assert!(branded.contains("__dds_brand: \"long_double\""));
assert!(branded.contains("bytes: Uint8Array"));
}
#[test]
fn b_1_10_any_uses_dds_any_carrier_not_unknown_or_any() {
let ts = gen_ts(r"struct S { any x; };");
assert!(ts.contains("x: DdsAny"));
assert!(!ts.contains("x: any"));
assert!(!ts.contains("x: unknown"));
}
#[test]
fn b_1_11_annotated_struct_yields_interface_not_class() {
let ts = gen_ts(
r"@final struct Pose {
@key long x;
@key long y;
};",
);
assert!(ts.contains("export interface Pose"));
assert!(!ts.contains("export class Pose"));
assert!(ts.contains("key: true"));
assert!(ts.contains("extensibility: \"final\""));
}
#[test]
fn b_1_12_exception_extends_dds_exception_with_descriptor_kind() {
let ts = gen_ts(r"exception E { long limit; };");
assert!(ts.contains("export interface E extends DdsException"));
assert!(ts.contains("kind: \"exception\""));
}
#[test]
fn b_1_13_typedef_emits_alias_descriptor() {
let ts = gen_ts(r"typedef long Counter;");
assert!(ts.contains("export const CounterType: DdsTypeDescriptor<Counter>"));
assert!(ts.contains("kind: \"alias\""));
}
#[test]
fn b_1_14_module_with_full_construct_set_compiles_clean() {
let ts = gen_ts(
r"module M {
const long N = 4;
struct S { long x; };
union U switch (long) { case 1: long a; };
enum E { A };
bitset Bs { bitfield<3> b; };
bitmask Bm { F };
typedef long T;
};",
);
assert!(ts.contains("export namespace M {"));
for marker in [
"export const N: number = 4",
"export interface S",
"export type U =",
"export const E = {",
"export interface Bs",
"export const Bm = {",
"export type T = number",
] {
assert!(ts.contains(marker), "B.1.14 missing: {marker}");
}
}
#[test]
fn b_1_15_unknown_annotation_emits_w002_diagnostic_in_strict_mode() {
let ts = gen_ts(r"struct S { long v; };");
assert!(ts.contains("export interface S"));
}
#[test]
fn b_1_16_map_kv_yields_readonly_map_with_descriptor_kind() {
let ts = gen_ts(r"struct C { map<string, long> by_name; };");
assert!(ts.contains("by_name: ReadonlyMap<string, number>"));
assert!(ts.contains("kind: \"map\""));
}
#[test]
fn b_2_1_runtime_exports_full_b21_surface() {
let idx = runtime::INDEX_TS;
for marker in [
"DdsTypeDescriptor",
"DdsMemberDescriptor",
"DdsTypeRef",
"DdsAny",
"DdsException",
"DescriptorKind",
"ExtensibilityKind",
"PrimitiveName",
"Char",
"WChar",
"makeChar",
"makeWChar",
"LongDouble",
"makeLongDouble",
"registerType",
"lookupType",
"getKey",
"getTopic",
"withDefaults",
"boxAny",
"unboxAny",
"equalKey",
"isOneOf",
] {
assert!(idx.contains(marker), "B.2.1 missing: {marker}");
}
}
#[test]
fn b_2_2_lookup_type_referenced_by_registry_module() {
let src = runtime::REGISTRY_TS;
assert!(src.contains("export function lookupType"));
assert!(src.contains("registry.get"));
}
#[test]
fn b_2_3_get_key_iterates_key_marked_members_in_declaration_order() {
let src = runtime::REGISTRY_TS;
assert!(src.contains("export function getKey"));
assert!(src.contains("for (const field of descriptor.fields)"));
assert!(src.contains("if (field.key)"));
}
#[test]
fn b_2_4_box_any_throws_on_typeguard_failure() {
let src = runtime::REGISTRY_TS;
assert!(src.contains("export function boxAny"));
assert!(src.contains("descriptor.typeGuard(value)"));
assert!(src.contains("TypeError"));
}
#[test]
fn b_2_5_unbox_any_throws_on_typeid_or_guard_mismatch() {
let src = runtime::REGISTRY_TS;
assert!(src.contains("export function unboxAny"));
assert!(src.contains("any.typeId !== descriptor.name"));
}
#[test]
fn b_2_6_with_defaults_fills_absent_properties_from_descriptor() {
let src = runtime::REGISTRY_TS;
assert!(src.contains("export function withDefaults"));
assert!(src.contains("field.default !== undefined"));
}
#[test]
fn b_2_7_make_w_char_accepts_astral_plane_make_char_iso_8859_1() {
let src = runtime::BRANDED_TS;
assert!(src.contains("Array.from(s)"));
assert!(src.contains("0xff"));
}
#[test]
fn b_2_8_equal_key_struct_uses_recursive_member_compare() {
let src = runtime::EQUAL_TS;
assert!(src.contains("function refEqual"));
assert!(src.contains("descriptor.fields"));
}
#[test]
fn b_2_9_is_one_of_returns_false_for_non_error_input() {
let src = runtime::EQUAL_TS;
assert!(src.contains("export function isOneOf"));
assert!(src.contains("instanceof Error"));
}
#[test]
fn b_3_1_interface_emits_client_handler_and_service_no_class() {
let ts = gen_ts(r"interface I { long op(); };");
assert!(ts.contains("export interface IClient"));
assert!(ts.contains("export interface IHandler"));
assert!(ts.contains("export const IService: ServiceDescriptor"));
assert!(!ts.contains("export class I"));
}
#[test]
fn b_3_2_op_with_out_param_resolves_to_object_with_result() {
let ts = gen_ts(r"interface C { long divmod(in long a, in long b, out long remainder); };");
assert!(ts.contains(
"divmod(a: number, b: number): Promise<{ result: number; remainder: number }>"
));
}
#[test]
fn b_3_3_op_raises_lists_exception_descriptor_in_descriptor() {
let ts = gen_ts(
r"exception O { long limit; };
interface A { long add(in long a, in long b) raises (O); };",
);
assert!(ts.contains("raises: [OType]"));
}
#[test]
fn b_3_4_oneway_op_yields_promise_void_and_descriptor_oneway_true() {
let ts = gen_ts_full(r"interface P { oneway void ping(); };");
assert!(ts.contains("ping(): Promise<void>"));
assert!(ts.contains("oneway: true"));
}
#[test]
fn b_3_5_readonly_attribute_emits_only_getter() {
let ts = gen_ts(r"interface X { readonly attribute long n; };");
assert!(ts.contains("get_n(): Promise<number>"));
assert!(!ts.contains("set_n"));
}
#[test]
fn b_3_6_inheritance_extends_parent_client_handler_and_service() {
let ts = gen_ts(
r"interface A { long ping(); };
interface B : A { long inc(); };",
);
assert!(ts.contains("export interface BClient extends AClient"));
assert!(ts.contains("export interface BHandler extends AHandler"));
assert!(ts.contains("inherits: [AService]"));
}
#[test]
fn b_3_7_multi_inheritance_lists_both_parents_in_order() {
let ts = gen_ts(
r"interface A { long ax(); };
interface B { long bx(); };
interface I : A, B { long ix(); };",
);
assert!(ts.contains("export interface IClient extends AClient, BClient"));
assert!(ts.contains("inherits: [AService, BService]"));
}
#[test]
fn b_3_8_orphan_forward_declaration_fires_dds_ts_e002() {
let ast =
zerodds_idl::parse(r"interface Orphan;", &ParserConfig::default()).expect("parse");
let err = generate_ts_source_with_diagnostics(&ast).expect_err("must reject");
match err {
IdlTsError::Unsupported(msg) => assert!(msg.contains("DDS-TS-E002")),
}
}
#[test]
fn b_3_9_attribute_descriptor_carries_exception_descriptor_lists() {
let ts = gen_ts(r"interface I { attribute long n; };");
assert!(ts.contains("getRaises: ["));
assert!(ts.contains("setRaises: ["));
}
#[test]
fn c_1_handle_types_use_string_literal_brands() {
let src = runtime::WASM_TS;
for marker in [
"ParticipantHandle = number & { readonly __dds_brand: \"participant\" }",
"TopicHandle = number & { readonly __dds_brand: \"topic\" }",
"PublisherHandle = number & { readonly __dds_brand: \"publisher\" }",
"SubscriberHandle = number & { readonly __dds_brand: \"subscriber\" }",
"DataWriterHandle = number & { readonly __dds_brand: \"writer\" }",
"DataReaderHandle = number & { readonly __dds_brand: \"reader\" }",
] {
assert!(src.contains(marker), "C.1.1 missing: {marker}");
}
}
#[test]
fn c_1_2_sample_and_dds_guid_shape_normative() {
let src = runtime::WASM_TS;
assert!(src.contains("prefix: Uint8Array"));
assert!(src.contains("entityId: number"));
assert!(src.contains("validData: boolean"));
assert!(src.contains("publicationHandle: DdsGuid"));
assert!(src.contains("interface Sample"));
assert!(src.contains("bytes: Uint8Array"));
}
#[test]
fn c_2_required_operations_present() {
let src = runtime::WASM_TS;
for marker in [
"createParticipant",
"deleteParticipant",
"createTopic",
"deleteTopic",
"createPublisher",
"createSubscriber",
"deletePublisher",
"deleteSubscriber",
"createDataWriter",
"createDataReader",
"writeSample",
"takeSamples",
"deleteDataWriter",
"deleteDataReader",
"setDataAvailableListener",
] {
assert!(src.contains(marker), "C.2 missing operation: {marker}");
}
}
#[test]
fn c_3_wire_format_uses_uint8_array_with_xcdr2_carrier() {
let src = runtime::WASM_TS;
assert!(src.contains("xcdr2: Uint8Array"));
assert!(src.contains("ReadonlyArray<Sample>"));
}
#[test]
fn c_4_browser_node_backend_via_bind_wasm_backend() {
let src = runtime::WASM_TS;
assert!(src.contains("interface WasmBackend"));
assert!(src.contains("export function bindWasmBackend"));
}
#[test]
fn c_5_round_trip_reference_backend_is_pure_typescript() {
let src = runtime::TEST_BACKEND_TS;
assert!(src.contains("class InMemoryBackend"));
assert!(src.contains("writeSample("));
assert!(src.contains("takeSamples("));
assert!(src.contains("queueMicrotask"));
assert!(src.contains("export function createInMemoryBackend"));
assert!(runtime::WASM_TS.contains("WASM backend not bound"));
}
#[test]
fn c_6_reservation_unused_identifiers_not_collided() {
let src = runtime::WASM_TS;
for reserved in [
"createGuardCondition",
"createWaitSet",
"createQueryCondition",
"getInstance",
] {
assert!(
!src.contains(reserved),
"C.6 reserved id leaked: {reserved}"
);
}
}
#[test]
fn b_4_reference_harness_is_the_idl_ts_test_suite_itself() {
let _ = runtime::ALL.len();
}
#[test]
fn struct_multi_dim_array_emits_nested_array_type() {
let ts = gen_ts(r"struct M { long matrix[3][5]; };");
assert!(ts.contains("matrix: Array<Array<number>>"), "got:\n{ts}");
assert!(ts.contains("M_matrix_LENGTH_DIM1 = 3"));
assert!(ts.contains("M_matrix_LENGTH_DIM2 = 5"));
}
#[test]
fn struct_one_dim_array_emits_single_array_and_length() {
let ts = gen_ts(r"struct V { double v[3]; };");
assert!(ts.contains("v: Array<number>"));
assert!(ts.contains("V_v_LENGTH = 3"));
}
#[test]
fn map_key_struct_sets_key_equality_hazard_in_descriptor() {
let ts = gen_ts(
r"struct K { long id; };
struct C { map<K, long> table; };",
);
assert!(ts.contains("keyEqualityHazard: true"), "got:\n{ts}");
}
#[test]
fn module_declaration_merging_uses_export_namespace_only() {
let ts = gen_ts(
r"module M { struct A { long x; }; };
module M { struct B { long y; }; };",
);
let occurrences = ts.matches("export namespace M {").count();
assert_eq!(occurrences, 2, "got:\n{ts}");
}
#[test]
fn typedef_chain_resolves_module_scope_order_independent() {
let ts = gen_ts(r"typedef Counter Down; typedef long Counter;");
assert!(ts.contains("export type Down = Counter"));
assert!(ts.contains("export type Counter = number"));
}
#[test]
fn diagnostic_clean_input_yields_no_diagnostics() {
let (_, diags) = gen_with_diagnostics(r"struct Plain { long x; };");
assert!(diags.is_empty(), "no diagnostics expected; got {diags:?}");
}
#[test]
fn diagnostic_unknown_annotation_warns_w002() {
let src = r"@my_custom struct S { long v; };";
let Ok(ast) = zerodds_idl::parse(src, &ParserConfig::default()) else {
return;
};
let (_, diags) = generate_ts_source_with_diagnostics(&ast).expect("gen");
let w002: Vec<&Diagnostic> = diags.iter().filter(|d| d.code == "DDS-TS-W002").collect();
assert_eq!(w002.len(), 1, "expected one W002; got {diags:?}");
}
#[test]
fn diagnostic_unknown_annotation_strict_mode_e004_fatal() {
let src = r"@my_custom struct S { long v; };";
let Ok(ast) = zerodds_idl::parse(src, &ParserConfig::default()) else {
return;
};
let cfg = CodegenConfig {
strict_annotations: true,
};
let err = generate_ts_source_with_config(&ast, &cfg).expect_err("strict mode must reject");
match err {
IdlTsError::Unsupported(msg) => assert!(msg.contains("DDS-TS-E004")),
}
}
#[test]
fn diagnostic_known_annotations_yield_no_w002() {
let (_, diags) = gen_with_diagnostics(
r#"@final @topic("X") struct S {
@key @id(0) long a;
@optional @unit("ms") long b;
};"#,
);
let w002: Vec<&Diagnostic> = diags.iter().filter(|d| d.code == "DDS-TS-W002").collect();
assert!(w002.is_empty(), "no W002 expected; got {diags:?}");
}
#[test]
fn diagnostic_duplicate_member_id_emits_e003_fatal() {
let src = r"struct S { @id(0) long a; @id(0) long b; };";
let Ok(ast) = zerodds_idl::parse(src, &ParserConfig::default()) else {
return;
};
let err = generate_ts_source_with_diagnostics(&ast).expect_err("dup id");
match err {
IdlTsError::Unsupported(msg) => {
assert!(msg.contains("DDS-TS-E003"));
}
}
}
#[test]
fn diagnostic_multiple_extensibility_e003_fatal() {
let src = r"@final @mutable struct S { long a; };";
let Ok(ast) = zerodds_idl::parse(src, &ParserConfig::default()) else {
return;
};
let err = generate_ts_source_with_diagnostics(&ast).expect_err("conflict");
match err {
IdlTsError::Unsupported(msg) => assert!(msg.contains("DDS-TS-E003")),
}
}
#[test]
fn runtime_includes_operations_descriptor_types() {
let src = runtime::OPERATIONS_TS;
for marker in [
"export type ParameterMode",
"export interface OperationParameterDescriptor",
"export interface OperationDescriptor",
"export interface AttributeDescriptor",
"export interface ServiceDescriptor",
] {
assert!(src.contains(marker), "operations.ts missing: {marker}");
}
let idx = runtime::INDEX_TS;
for marker in [
"ParameterMode",
"OperationDescriptor",
"AttributeDescriptor",
"ServiceDescriptor",
] {
assert!(idx.contains(marker), "index.ts missing: {marker}");
}
}
#[test]
fn bitmask_descriptor_emitted() {
let ts = gen_ts(r"bitmask Permissions { READ, WRITE };");
assert!(ts.contains("export const PermissionsType"));
assert!(ts.contains("kind: \"bitmask\""));
assert!(ts.contains("registerType(PermissionsType);"));
}
#[test]
fn long_long_maps_to_bigint() {
let ts = gen_ts(r"struct Big { long long v; };");
assert!(ts.contains("v: bigint"), "long long -> bigint:\n{ts}");
}
#[test]
fn sequence_maps_to_array() {
let ts = gen_ts(r"struct S { sequence<long> items; };");
assert!(ts.contains("Array<number>"));
}
#[test]
fn enum_emits_as_const_object_and_literal_union() {
let ts = gen_ts(r"enum Color { RED, GREEN, BLUE };");
assert!(
ts.contains("export const Color = {"),
"expected as-const object, got:\n{ts}"
);
assert!(ts.contains("RED: \"RED\""));
assert!(ts.contains("GREEN: \"GREEN\""));
assert!(ts.contains("BLUE: \"BLUE\""));
assert!(ts.contains("} as const;"));
assert!(ts.contains("export type Color = (typeof Color)[keyof typeof Color]"));
assert!(
!ts.contains("export enum Color"),
"TS `enum` keyword forbidden by §7.6, got:\n{ts}"
);
}
#[test]
fn enum_emits_ordinal_companion() {
let ts = gen_ts(r"enum Color { RED, GREEN, BLUE };");
assert!(ts.contains("export const ColorOrdinal"));
assert!(ts.contains("RED: 0"));
assert!(ts.contains("GREEN: 1"));
assert!(ts.contains("BLUE: 2"));
assert!(ts.contains("export const ColorFromOrdinal"));
}
#[test]
fn enum_value_annotation_overrides_ordinal() {
let ts = gen_ts(
r"enum Op {
@value(0) ADD,
@value(1) SUB,
@value(7) NEG
};",
);
assert!(ts.contains("NEG: 7"), "expected NEG: 7, got:\n{ts}");
}
#[test]
fn module_wraps_in_namespace() {
let ts = gen_ts(r"module M { struct S { long x; }; };");
assert!(ts.contains("export namespace M"));
assert!(ts.contains("export interface S"));
}
#[test]
fn typedef_emits_type_alias() {
let ts = gen_ts(r"typedef long MyInt;");
assert!(ts.contains("export type MyInt = number"), "got:\n{ts}");
}
#[test]
fn typedef_string_alias() {
let ts = gen_ts(r"typedef string TopicName;");
assert!(ts.contains("export type TopicName = string"));
}
#[test]
fn typedef_sequence_alias() {
let ts = gen_ts(r"typedef sequence<octet> Bytes;");
assert!(ts.contains("export type Bytes = Array<number>"));
}
#[test]
fn union_emits_discriminated_union_type() {
let ts = gen_ts(
r"union MyUnion switch (long) {
case 1: long a;
case 2: string b;
};",
);
assert!(ts.contains("export type MyUnion"), "got:\n{ts}");
assert!(ts.contains("discriminator: 1"));
assert!(ts.contains("discriminator: 2"));
assert!(ts.contains("a: number"));
assert!(ts.contains("b: string"));
}
#[test]
fn union_emits_descriptor_with_synthetic_discriminator_id() {
let ts = gen_ts(
r"union MyUnion switch (long) {
case 1: long a;
case 2: string b;
};",
);
assert!(ts.contains("export const MyUnionType"));
assert!(ts.contains("kind: \"union\""));
assert!(ts.contains("id: 0xFFFFFFFF"));
assert!(ts.contains("labels: [1]"));
assert!(ts.contains("labels: [2]"));
assert!(ts.contains("registerType(MyUnionType);"));
}
#[test]
fn union_with_default_includes_default_arm() {
let ts = gen_ts(
r"union MyUnion switch (long) {
case 1: long a;
default: octet other;
};",
);
assert!(ts.contains("other: number"));
}
#[test]
fn bitmask_emits_const_object_with_shift_values() {
let ts = gen_ts(r"bitmask Permissions { READ, WRITE, EXEC };");
assert!(ts.contains("export const Permissions = {"), "got:\n{ts}");
assert!(ts.contains("READ: (1 << 0) >>> 0"));
assert!(ts.contains("WRITE: (1 << 1) >>> 0"));
assert!(ts.contains("EXEC: (1 << 2) >>> 0"));
assert!(ts.contains("export type Permissions = number"));
assert!(ts.contains("Permissions_BIT_BOUND = 32"));
}
#[test]
fn bitmask_bit_bound_above_32_emits_bigint() {
let ts = gen_ts(r"@bit_bound(48) bitmask BigFlags { f0, f1 };");
assert!(ts.contains("f0: 1n << 0n"), "got:\n{ts}");
assert!(ts.contains("f1: 1n << 1n"));
assert!(ts.contains("export type BigFlags = bigint"));
assert!(ts.contains("BigFlags_BIT_BOUND = 48"));
}
#[test]
fn bitmask_position_annotation_overrides_index() {
let ts = gen_ts(r"bitmask Flags { @position(7) high, @position(0) low };");
assert!(ts.contains("high: (1 << 7) >>> 0"));
assert!(ts.contains("low: (1 << 0) >>> 0"));
}
#[test]
fn bitset_emits_interface_and_bit_constants() {
let ts = gen_ts(r"bitset Flags { bitfield<3> low; bitfield<5> high; };");
assert!(ts.contains("export interface Flags"), "got:\n{ts}");
assert!(ts.contains("low: number"));
assert!(ts.contains("high: number"));
assert!(ts.contains("Flags_low_BITS"));
assert!(ts.contains("Flags_high_BITS"));
}
#[test]
fn bitset_width_const_eval_emits_concrete_widths() {
let ts = gen_ts(r"bitset Flags { bitfield<3> low; bitfield<5> high; };");
assert!(
ts.contains("Flags_low_BITS = 3"),
"expected Flags_low_BITS = 3, got:\n{ts}"
);
assert!(
ts.contains("Flags_high_BITS = 5"),
"expected Flags_high_BITS = 5, got:\n{ts}"
);
assert!(
!ts.contains("TODO"),
"no TODO placeholder allowed in const-eval output, got:\n{ts}"
);
}
#[test]
fn bitset_width_const_eval_handles_hex_literal() {
let ts = gen_ts(r"bitset Flags { bitfield<0x10> wide; };");
assert!(
ts.contains("Flags_wide_BITS = 16"),
"expected Flags_wide_BITS = 16, got:\n{ts}"
);
}
#[test]
fn parse_int_literal_handles_decimal_hex_octal() {
assert_eq!(parse_int_literal("42"), Some(42));
assert_eq!(parse_int_literal("0x2A"), Some(42));
assert_eq!(parse_int_literal("0X2a"), Some(42));
assert_eq!(parse_int_literal("052"), Some(42));
assert_eq!(parse_int_literal("0"), Some(0));
assert_eq!(parse_int_literal("not-a-number"), None);
}
#[test]
fn runtime_index_exports_b21_surface() {
let src = runtime::INDEX_TS;
for marker in [
"ExtensibilityKind",
"PrimitiveName",
"DdsTypeRef",
"GuardedTypeOf",
"DescriptorKind",
"DdsMemberDescriptor",
"DdsTypeDescriptor",
"Char",
"WChar",
"makeChar",
"makeWChar",
"LongDouble",
"makeLongDouble",
"DdsAny",
"DdsException",
"registerType",
"lookupType",
"getKey",
"getTopic",
"withDefaults",
"boxAny",
"unboxAny",
"equalKey",
"isOneOf",
] {
assert!(
src.contains(marker),
"@zerodds/types index.ts missing required B.2.1 export: {marker}"
);
}
}
#[test]
fn runtime_branded_helpers_are_present() {
let src = runtime::BRANDED_TS;
for marker in [
"export type Char = string & { readonly __dds_brand: \"char\" }",
"export type WChar = string & { readonly __dds_brand: \"wchar\" }",
"export interface LongDouble",
"__dds_brand: \"long_double\"",
"RangeError",
] {
assert!(src.contains(marker), "branded.ts missing marker: {marker}");
}
}
#[test]
fn runtime_registry_provides_reflection_api() {
let src = runtime::REGISTRY_TS;
for marker in [
"export function registerType",
"export function lookupType",
"export function getKey",
"export function getTopic",
"export function withDefaults",
"export function boxAny",
"export function unboxAny",
] {
assert!(src.contains(marker), "registry.ts missing marker: {marker}");
}
}
#[test]
fn runtime_equal_provides_equalkey_and_isoneof() {
let src = runtime::EQUAL_TS;
assert!(src.contains("export function equalKey"));
assert!(src.contains("export function isOneOf"));
assert!(src.contains("GuardedTypeOf<DS[number]>"));
assert!(src.contains("Object.is"));
}
#[test]
fn runtime_all_files_listed() {
let names: alloc::vec::Vec<&str> = runtime::ALL.iter().map(|(n, _)| *n).collect();
for required in [
"types.ts",
"branded.ts",
"dds_any.ts",
"registry.ts",
"equal.ts",
"operations.ts",
"wasm.ts",
"test_backend.ts",
"index.ts",
] {
assert!(
names.contains(&required),
"runtime::ALL missing: {required}"
);
}
}
#[test]
fn full_module_compiles_all_constructs() {
let ts = gen_ts(
r"module M {
typedef long MyInt;
struct Point { long x; long y; };
enum Color { RED, GREEN };
union Variant switch (long) {
case 1: long i;
case 2: string s;
};
bitmask Flags { A, B, C };
bitset Reg { bitfield<8> lo; };
};",
);
assert!(ts.contains("export namespace M"));
assert!(ts.contains("export type MyInt"));
assert!(ts.contains("export interface Point"));
assert!(ts.contains("export const Color = {"));
assert!(ts.contains("export type Color"));
assert!(ts.contains("export type Variant"));
assert!(ts.contains("export const Flags = {"));
assert!(ts.contains("export interface Reg"));
}
}