use crate::adapter::c::ordered_types;
use crate::adapter::constant;
use crate::adapter::cst::{MaterialiseError, no_spelling_collision};
use crate::apply::Applied;
use crate::verbatim;
use seesaw_tgg::ident::GhostId;
use zerodds_ast::{Ast, CaseLabel, CharWidth, ModuleNode, PrimitiveKind, TypeKind, TypeNode};
use zerodds_ast_core::{Id, NodeId};
use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
use zerodds_lang_cst::typescript as ts;
pub const FLOAT128: &str = "ZdFloat128";
pub const ANY_BOX: &str = "DdsAny";
const ENTRY_PREFIX: &str = "ZdEntry_";
const FACTORY_PREFIX: &str = "new";
const RESERVED_NAMES: &[&str] = &[FLOAT128, ANY_BOX, "Array", "Uint8Array"];
const MAX_MODULE_DEPTH: usize = 64;
const MAX_TYPE_DEPTH: usize = 64;
const MAX_SAFE_INTEGER: i128 = 9_007_199_254_740_991;
#[must_use]
pub fn prelude() -> Vec<ts::Decl> {
vec![
ts::Decl::Interface(ts::Interface {
doc: Doc::lines(&[
"An IDL `long double`, as the sixteen octets it is.",
"",
"TypeScript has one numeric type and it is an IEEE 754 binary64, so",
"there is nothing to hold a binary128 in. A `number` member would put",
"eight bytes on the wire where the type says sixteen; the octets are",
"the one form that carries the value without deciding what it means.",
]),
exported: true,
name: Ident::new(FLOAT128),
generics: Vec::new(),
extends: Vec::new(),
members: vec![ts::Signature::Property(ts::Property {
doc: Doc::line("The sixteen octets, in the order the wire carries them."),
readonly: true,
name: ts::PropertyName::ident("octets"),
optional: false,
type_: ts::Type::named(Path::single("Uint8Array")),
})],
}),
ts::Decl::Interface(ts::Interface {
doc: Doc::lines(&[
"An IDL `any`, as the value and the identity of its type.",
"",
"DDS-TS 1.0 ยง7.3.4. `typeId` is the qualified IDL name and indexes",
"the descriptor registry, so `lookupType(box.typeId)` answers with the",
"descriptor that describes `value`. Structurally the `DdsAny` of",
"`runtime/typescript/src/types.ts`, declared here rather than imported",
"so that a generated file typechecks on its own.",
]),
exported: true,
name: Ident::new(ANY_BOX),
generics: Vec::new(),
extends: Vec::new(),
members: vec![
ts::Signature::Property(ts::Property {
doc: Doc::line("The qualified IDL name of the boxed value's type."),
readonly: true,
name: ts::PropertyName::ident("typeId"),
optional: false,
type_: ts::Type::string(),
}),
ts::Signature::Property(ts::Property {
doc: Doc::line("The payload, narrowed through the descriptor."),
readonly: true,
name: ts::PropertyName::ident("value"),
optional: false,
type_: ts::Type::Keyword(ts::Keyword::Unknown),
}),
],
}),
]
}
pub fn ts_items(ast: &Ast, applied: &Applied) -> Result<Vec<ts::Decl>, MaterialiseError> {
let (structural, items) = ts_pending(ast, applied)?;
Ok(verbatim::weave_file(
ast,
verbatim::aliases::TYPESCRIPT,
assemble(ast, structural, items)?,
))
}
pub fn ts_items_with_descriptors(
ast: &Ast,
applied: &Applied,
) -> Result<Vec<ts::Decl>, MaterialiseError> {
let (structural, mut items) = ts_pending(ast, applied)?;
items.extend(descriptor_pending(ast, applied)?);
Ok(verbatim::weave_file(
ast,
verbatim::aliases::TYPESCRIPT,
assemble(ast, structural, items)?,
))
}
impl verbatim::Carrier for ts::Decl {
fn verbatim(text: String) -> Self {
Self::Verbatim(text)
}
fn take_into_body(
&mut self,
name: &str,
begin: &mut Vec<String>,
end: &mut Vec<String>,
) -> bool {
match self {
Self::Interface(node) if node.name.as_str() == name => {
let mut members: Vec<ts::Signature> =
begin.drain(..).map(ts::Signature::Verbatim).collect();
members.append(&mut node.members);
members.extend(end.drain(..).map(ts::Signature::Verbatim));
node.members = members;
true
}
Self::Class(node) if node.name.as_str() == name => {
let mut members: Vec<ts::ClassMember> =
begin.drain(..).map(ts::ClassMember::Verbatim).collect();
members.append(&mut node.members);
members.extend(end.drain(..).map(ts::ClassMember::Verbatim));
node.members = members;
true
}
_ => false,
}
}
}
fn ts_pending(
ast: &Ast,
applied: &Applied,
) -> Result<(Vec<ts::Decl>, Vec<Pending>), MaterialiseError> {
let mut structural: Vec<ts::Decl> = Vec::new();
let mut emitted: Vec<String> = Vec::new();
let mut items: Vec<Pending> = Vec::new();
for id in ordered_types(ast) {
let start = items.len();
declare(ast, applied, id, &mut items, &mut structural, &mut emitted)?;
let Ok(node) = ast.type_(id) else {
continue;
};
let module = node.module;
let name = ts::escape_keyword(&node.name);
verbatim::weave_with(
verbatim::declaration_slots(ast, id, verbatim::aliases::TYPESCRIPT),
start,
&mut items,
|text| Pending {
module,
decl: ts::Decl::Verbatim(text),
},
|declared, begin, end| {
for pending in declared {
if verbatim::Carrier::take_into_body(&mut pending.decl, &name, begin, end) {
return;
}
}
},
);
}
for (id, node) in ast.constants() {
if ghost_created_for(applied, node.node_id, "TsConst").is_none() {
continue;
}
let _ = id;
let value = ast
.eval_const(node.expr)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "constant's value".to_string(),
at: node.name.clone(),
})?;
let declared = node.type_.resolved();
let type_ = match declared {
Some(target) => ts_member_type(ast, applied, target, &node.name)?,
None => ts::Type::number(),
};
let value = constant_expr(ast, declared, &value, &node.name)?;
items.push(Pending {
module: node.module,
decl: ts::Decl::Variable(ts::Variable {
doc: renamed_doc(&node.name, Doc::none()),
exported: true,
declare: false,
kind: ts::VarKind::Const,
name: Ident::new(&ts::escape_keyword(&node.name)),
type_: Some(type_),
value: Some(value),
}),
});
}
for (id, node) in ast.exceptions() {
if ghost_created_for(applied, node.node_id, "TsError").is_none() {
continue;
}
let _ = id;
let mut fields = Vec::new();
for member in &node.members {
let Ok(member_node) = ast.member(*member) else {
return Err(MaterialiseError::Unevaluated {
construct: "exception member the model listed".to_string(),
at: node.name.clone(),
});
};
let Some(target) = member_node.type_.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "exception member type".to_string(),
at: member_node.name.clone(),
});
};
let container = ts::escape_keyword(&node.name);
for decl in bound_constants(ast, &container, Some(&member_node.name), target)? {
items.push(Pending {
module: node.module,
decl,
});
}
if let Some(decl) = default_constant(ast, applied, &container, member_node, target)? {
items.push(Pending {
module: node.module,
decl,
});
}
fields.push(Slot {
name: member_node.name.clone(),
type_: ts_member_type(ast, applied, target, &member_node.name)?,
optional: member_node.facets.is_optional(),
default: default_expr(ast, applied, target, &member_node.name, 0)?,
doc: Doc::none(),
});
}
push_pair(
&mut items,
node.module,
&node.name,
fields,
Doc::lines(&[
"An exception, as the structure its members make.",
"",
"A data holder and not an `Error`: `Error` carries a `message` and a",
"`stack` that nothing on the wire decides, and a generated subclass",
"would have to invent both.",
]),
);
}
Ok((structural, items))
}
fn assemble(
ast: &Ast,
structural: Vec<ts::Decl>,
items: Vec<Pending>,
) -> Result<Vec<ts::Decl>, MaterialiseError> {
let mut root = Scope::default();
for item in items {
insert(ast, &mut root, item)?;
}
let mut decls = structural;
decls.extend(scope_decls(root, 0)?);
check_names(&decls, &top_level_names(&decls), 0)?;
Ok(decls)
}
struct Pending {
module: Id<ModuleNode>,
decl: ts::Decl,
}
struct Slot {
name: String,
type_: ts::Type,
optional: bool,
default: ts::Expr,
doc: Doc,
}
#[derive(Default)]
struct Scope {
decls: Vec<ts::Decl>,
children: Vec<(Id<ModuleNode>, String, String, Scope)>,
}
fn insert(ast: &Ast, root: &mut Scope, item: Pending) -> Result<(), MaterialiseError> {
let chain = module_chain(ast, item.module)?;
let mut scope = root;
for (id, name, idl) in chain {
let at = match scope
.children
.iter()
.position(|(seen, _, _, _)| *seen == id)
{
Some(at) => at,
None => {
scope.children.push((id, name, idl, Scope::default()));
scope.children.len() - 1
}
};
scope = &mut scope
.children
.get_mut(at)
.ok_or_else(|| MaterialiseError::NoModule {
at: module_handle(item.module),
})?
.3;
}
scope.decls.push(item.decl);
Ok(())
}
fn scope_decls(scope: Scope, depth: usize) -> Result<Vec<ts::Decl>, MaterialiseError> {
if depth > MAX_MODULE_DEPTH {
return Err(MaterialiseError::NoModule {
at: format!("the nesting runs past {MAX_MODULE_DEPTH}"),
});
}
let mut out = scope.decls;
for (_, name, idl, child) in scope.children {
out.push(ts::Decl::Namespace(ts::Namespace {
doc: renamed_doc(
&idl,
Doc::line("An IDL module, as the namespace its declarations nest in."),
),
exported: true,
declare: false,
name: Ident::new(&name),
decls: scope_decls(child, depth + 1)?,
}));
}
Ok(out)
}
fn module_chain(
ast: &Ast,
module: Id<ModuleNode>,
) -> Result<Vec<(Id<ModuleNode>, String, String)>, MaterialiseError> {
let mut out = Vec::new();
let mut current = Some(module);
let mut depth = 0;
while let Some(id) = current {
if depth > MAX_MODULE_DEPTH {
return Err(MaterialiseError::NoModule {
at: format!(
"{} โ the parent chain runs past {MAX_MODULE_DEPTH}",
module_handle(module)
),
});
}
let node = ast.module(id).map_err(|_| MaterialiseError::NoModule {
at: module_handle(id),
})?;
if node.parent.is_none() {
break;
}
out.push((id, ts::escape_keyword(&node.name), node.name.clone()));
current = node.parent;
depth += 1;
}
out.reverse();
Ok(out)
}
fn module_handle(module: Id<ModuleNode>) -> String {
format!("#{}@{}", module.slot(), module.generation())
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Space {
Type,
Value,
Both,
}
impl Space {
const fn overlaps(self, other: Self) -> bool {
matches!(
(self, other),
(Self::Both, _)
| (_, Self::Both)
| (Self::Type, Self::Type)
| (Self::Value, Self::Value)
)
}
}
fn declared_name(decl: &ts::Decl) -> Option<(&str, Space)> {
match decl {
ts::Decl::Statement(_) => None,
ts::Decl::Interface(node) => Some((node.name.as_str(), Space::Type)),
ts::Decl::TypeAlias(node) => Some((node.name.as_str(), Space::Type)),
ts::Decl::Class(node) => Some((node.name.as_str(), Space::Both)),
ts::Decl::Enum(node) => Some((node.name.as_str(), Space::Both)),
ts::Decl::Variable(node) => Some((node.name.as_str(), Space::Value)),
ts::Decl::Function(node) => Some((node.name.as_str(), Space::Value)),
ts::Decl::Namespace(node) => Some((node.name.as_str(), Space::Value)),
ts::Decl::Comment(_) | ts::Decl::Verbatim(_) => None,
}
}
fn top_level_names(decls: &[ts::Decl]) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for decl in decls {
if let Some((name, _)) = declared_name(decl) {
out.push(name.to_string());
}
}
out
}
fn check_names(
decls: &[ts::Decl],
top_level: &[String],
depth: usize,
) -> Result<(), MaterialiseError> {
if depth > MAX_MODULE_DEPTH {
return Err(MaterialiseError::NoModule {
at: format!("the nesting runs past {MAX_MODULE_DEPTH}"),
});
}
let mut seen: Vec<(String, Space)> = Vec::new();
for decl in decls {
let Some((name, space)) = declared_name(decl) else {
continue;
};
if seen
.iter()
.any(|(taken, other)| taken == name && space.overlaps(*other))
{
return Err(MaterialiseError::NoLanguageForm {
construct: format!(
"a second declaration named `{name}` in one scope, which TypeScript \
merges into one rather than reporting"
),
language: "TypeScript",
at: name.to_string(),
});
}
seen.push((name.to_string(), space));
if RESERVED_NAMES.contains(&name) {
return Err(MaterialiseError::NoLanguageForm {
construct: format!(
"a declaration named `{name}`, which this materialiser writes into \
the file itself โ the generated one would shadow it for every \
scope inside"
),
language: "TypeScript",
at: name.to_string(),
});
}
if depth > 0 && top_level.iter().any(|taken| taken == name) {
return Err(MaterialiseError::NoLanguageForm {
construct: format!(
"a nested declaration named `{name}`, which would capture the first \
segment of every qualified reference written inside it โ TypeScript \
has no root operator to write past it with"
),
language: "TypeScript",
at: name.to_string(),
});
}
if let ts::Decl::Namespace(namespace) = decl {
check_names(&namespace.decls, top_level, depth + 1)?;
}
}
Ok(())
}
#[allow(clippy::too_many_lines)]
fn declare(
ast: &Ast,
applied: &Applied,
id: Id<TypeNode>,
items: &mut Vec<Pending>,
structural: &mut Vec<ts::Decl>,
emitted: &mut Vec<String>,
) -> Result<(), MaterialiseError> {
let Ok(node) = ast.type_(id) else {
return Ok(());
};
match &node.kind {
TypeKind::Primitive(_)
| TypeKind::String { .. }
| TypeKind::Fixed { .. }
| TypeKind::Sequence { .. } => {}
TypeKind::Array { dims, .. } => {
for dim in dims {
ast.eval_integral(*dim)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "array length".to_string(),
at: named(&node.name, node.node_id),
})?;
}
}
TypeKind::Map { key, value, .. } => {
let (Some(key), Some(value)) = (key.resolved(), value.resolved()) else {
return Err(MaterialiseError::Unevaluated {
construct: "map key or value".to_string(),
at: named(&node.name, node.node_id),
});
};
let name = map_entry_name(ast, id)?;
if emitted.contains(&name) {
return Ok(());
}
emitted.push(name.clone());
let at = named(&node.name, node.node_id);
let slots = vec![
Slot {
name: "key".to_string(),
type_: ts_member_type(ast, applied, key, &at)?,
optional: false,
default: default_expr(ast, applied, key, &at, 0)?,
doc: Doc::none(),
},
Slot {
name: "value".to_string(),
type_: ts_member_type(ast, applied, value, &at)?,
optional: false,
default: default_expr(ast, applied, value, &at, 0)?,
doc: Doc::none(),
},
];
structural.extend(pair(
&name,
&name,
slots,
Doc::line(
"One entry of a map. XTypes 1.3 7.4.1.1.4 defines a map as a \
sequence of these, and the rules write the two halves with no \
framing between them. An array of these and not a `Map`: the \
entry order is the sample's, and it is what the MapEntryOrder \
axis measures โ and a `Map` keyed by a struct would compare its \
keys by reference.",
),
));
}
TypeKind::Alias { aliased } => {
if !emits_alias(ast, applied, id) {
return Ok(());
}
let Some(target) = aliased.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "typedef target".to_string(),
at: node.name.clone(),
});
};
for decl in bound_constants(ast, &ts::escape_keyword(&node.name), None, target)? {
items.push(Pending {
module: node.module,
decl,
});
}
items.push(Pending {
module: node.module,
decl: ts::Decl::TypeAlias(ts::TypeAlias {
doc: renamed_doc(
&node.name,
Doc::line("An IDL typedef, as the type alias it is."),
),
exported: true,
name: Ident::new(&ts::escape_keyword(&node.name)),
generics: Vec::new(),
type_: type_at(ast, applied, target, &node.name, 0)?,
}),
});
}
TypeKind::Enumeration { enumerators } => {
if ghost_created_for(applied, node.node_id, "TsEnum").is_none() {
return Ok(());
}
let mut entries = Vec::new();
let mut ordinals: Vec<(String, i128)> = Vec::new();
for enumerator in enumerators {
let Ok(enum_node) = ast.enumerator(*enumerator) else {
return Err(MaterialiseError::Unevaluated {
construct: "enumerator the model listed".to_string(),
at: node.name.clone(),
});
};
if ghost_created_for(applied, enum_node.node_id, "TsEnumerator").is_none() {
continue;
}
let value = ast.effective_enumerator_value(*enumerator).ok_or_else(|| {
MaterialiseError::Unevaluated {
construct: "enumerator value".to_string(),
at: enum_node.name.clone(),
}
})?;
let value = in_range(
value,
i128::from(i32::MIN),
i128::from(u32::MAX),
"32-bit enumerated literal",
&enum_node.name,
)?;
let member = ts::escape_member(&enum_node.name);
entries.push((
ts::PropertyName::ident(&member),
ts::Expr::Literal(Literal::Str(member.clone())),
));
ordinals.push((member, value));
}
items.extend(const_object(
node.module,
&node.name,
entries,
enum_alias_type(&node.name),
Doc::line(
"An enumeration, as the `as const` object its enumerators name and \
the string-literal union a member of it holds (DDS-TS 1.0 ยง7.6). \
Not a TypeScript `enum`: since 5.0 an `enum`-typed slot rejects a \
value that is not one of its members, and an @appendable writer \
that added an enumerator sends exactly such a value.",
),
));
items.extend(enum_ordinals(node.module, &node.name, &ordinals));
}
TypeKind::Bitmask { bits } => {
if ghost_created_for(applied, node.node_id, "TsBitmask").is_none() {
return Ok(());
}
let bound = ast.effective_bit_bound(id);
let holder = unsigned_holder(bound);
let wide = holder == ts::Type::bigint();
let mut entries = Vec::new();
for bit in bits {
let Ok(bit_node) = ast.bit_value(*bit) else {
return Err(MaterialiseError::Unevaluated {
construct: "bitmask bit the model listed".to_string(),
at: node.name.clone(),
});
};
if ghost_created_for(applied, bit_node.node_id, "TsBitConst").is_none() {
continue;
}
let position = ast.effective_bit_position(*bit).ok_or_else(|| {
MaterialiseError::Unevaluated {
construct: "bit position".to_string(),
at: bit_node.name.clone(),
}
})?;
entries.push((
ts::PropertyName::ident(&ts::escape_member(&bit_node.name)),
ts::Expr::Literal(bit_literal(position, wide)),
));
}
items.extend(const_object(
node.module,
&node.name,
entries,
holder,
Doc::line(
"A bitmask, as the `as const` object its bits name. Each value is \
written as the number it is and not as `1 << n`: JavaScript's `<<` \
is a signed 32-bit operation, so `1 << 31` is -2147483648 and a \
shift past 31 wraps โ neither of which is the bit the position \
names.",
),
));
items.push(Pending {
module: node.module,
decl: side_constant(
format!("{}_BIT_BOUND", ts::escape_keyword(&node.name)),
None,
ts::Expr::Literal(Literal::int(i128::from(bound))),
Doc::line(
"The effective `@bit_bound` of this bitmask, which is what \
decides whether its values are a `number` or a `bigint`. \
DDS-TS 1.0 ยง7.8.",
),
),
});
}
TypeKind::Bitset { .. } => {
if ghost_created_for(applied, node.node_id, "TsBitset").is_none() {
return Ok(());
}
let mut slots = Vec::new();
for bitfield in &ast.all_bitfields_of(id) {
let Ok(field_node) = ast.bitfield(*bitfield) else {
return Err(MaterialiseError::Unevaluated {
construct: "bitset field the model listed".to_string(),
at: node.name.clone(),
});
};
let Some(field_name) = field_node.name.as_ref() else {
continue;
};
let holder = unsigned_holder(field_node.bits);
let default = zero_of(&holder);
items.push(Pending {
module: node.module,
decl: side_constant(
format!("{}_{field_name}_BITS", ts::escape_keyword(&node.name)),
None,
ts::Expr::Literal(Literal::int(i128::from(field_node.bits))),
Doc::line("The width of this bitfield, in bits. DDS-TS 1.0 ยง7.7."),
),
});
slots.push(Slot {
name: field_name.clone(),
type_: holder,
optional: false,
default,
doc: Doc::none(),
});
}
push_pair(
items,
node.module,
&node.name,
slots,
Doc::line(
"A bitset. The fields are separate integers here; the packing into \
the anchor is the codec's.",
),
);
}
TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
let created = if matches!(node.kind, TypeKind::Valuetype { .. }) {
"TsValue"
} else {
"TsStruct"
};
if ghost_created_for(applied, node.node_id, created).is_none() {
return Ok(());
}
let mut slots = Vec::new();
let mut spelled = Vec::new();
if let Some(base_type) = super::c::struct_base(ast, id) {
spelled.push(("parent".to_string(), "parent".to_string()));
slots.push(Slot {
name: "parent".to_string(),
type_: ts_member_type(ast, applied, base_type, "parent")?,
optional: false,
default: default_expr(ast, applied, base_type, "parent", 0)?,
doc: Doc::line(
"The base structure, the first member (XTypes 1.3 7.5.1.4.1.3).",
),
});
}
for member in ast.members_of(id).to_vec() {
let Ok(member_node) = ast.member(member) else {
return Err(MaterialiseError::Unevaluated {
construct: "member the model listed".to_string(),
at: node.name.clone(),
});
};
let Some(target) = member_node.type_.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "member type".to_string(),
at: member_node.name.clone(),
});
};
let optional = member_node.facets.is_optional();
let container = ts::escape_keyword(&node.name);
for decl in bound_constants(ast, &container, Some(&member_node.name), target)? {
items.push(Pending {
module: node.module,
decl,
});
}
if let Some(decl) = default_constant(ast, applied, &container, member_node, target)?
{
items.push(Pending {
module: node.module,
decl,
});
}
spelled.push((
ts::escape_member(&member_node.name),
member_node.name.clone(),
));
slots.push(Slot {
name: member_node.name.clone(),
type_: ts_member_type(ast, applied, target, &member_node.name)?,
optional,
default: if optional {
ts::Expr::Undefined
} else {
default_expr(ast, applied, target, &member_node.name, 0)?
},
doc: if optional {
Doc::line(
"An @optional member: absent rather than present-and-null. In \
a @mutable type an absent member is not written at all; \
outside one its absence is written โ a zero is_present byte \
under XCDR2 (rule 20) or a parameter header declaring a \
length of zero under XCDR1 (7.4.1.2).",
)
} else {
Doc::none()
},
});
}
no_spelling_collision(
spelled
.iter()
.map(|(field, declared)| (field.as_str(), declared.as_str())),
"TypeScript",
&node.name,
)?;
push_pair(items, node.module, &node.name, slots, Doc::none());
}
TypeKind::Union { discriminator, .. } => {
if ghost_created_for(applied, node.node_id, "TsUnion").is_none() {
return Ok(());
}
let Some(disc) = discriminator.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "union discriminator".to_string(),
at: node.name.clone(),
});
};
let disc_type = ts_member_type(ast, applied, disc, &node.name)?;
let explicit = explicit_labels(ast, id, disc, &node.name)?;
let mut arms: Vec<ts::Type> = Vec::new();
for case in ast.cases_of(id) {
let Ok(case_node) = ast.case(*case) else {
return Err(MaterialiseError::Unevaluated {
construct: "union case the model listed".to_string(),
at: node.name.clone(),
});
};
if ghost_created_for(applied, case_node.node_id, "TsCase").is_none() {
continue;
}
let Ok(member) = ast.member(case_node.member) else {
return Err(MaterialiseError::Unevaluated {
construct: "member behind a union case the model listed".to_string(),
at: node.name.clone(),
});
};
let Some(target) = member.type_.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "union case type".to_string(),
at: member.name.clone(),
});
};
for decl in bound_constants(
ast,
&ts::escape_keyword(&node.name),
Some(&member.name),
target,
)? {
items.push(Pending {
module: node.module,
decl,
});
}
arms.push(ts::Type::Object(vec![
ts::Signature::Property(ts::Property {
doc: Doc::none(),
readonly: false,
name: ts::PropertyName::ident(DISCRIMINANT_PROPERTY),
optional: false,
type_: discriminant_type(
ast,
disc,
&disc_type,
&case_node.labels,
&explicit,
&member.name,
)?,
}),
ts::Signature::Property(ts::Property {
doc: Doc::none(),
readonly: false,
name: ts::PropertyName::ident(&ts::escape_member(&member.name)),
optional: false,
type_: ts_member_type(ast, applied, target, &member.name)?,
}),
]));
}
let has_default = ast.cases_of(id).iter().any(|case| {
ast.case(*case).is_ok_and(|case_node| {
case_node
.labels
.iter()
.any(|label| matches!(label, CaseLabel::Default))
})
});
if !arms.is_empty() && !has_default {
arms.push(ts::Type::Object(vec![ts::Signature::Property(
ts::Property {
doc: Doc::none(),
readonly: false,
name: ts::PropertyName::ident(DISCRIMINANT_PROPERTY),
optional: false,
type_: disc_type.clone(),
},
)]));
}
if arms.is_empty() {
arms.push(ts::Type::Object(vec![ts::Signature::Property(
ts::Property {
doc: Doc::none(),
readonly: false,
name: ts::PropertyName::ident(DISCRIMINANT_PROPERTY),
optional: false,
type_: disc_type,
},
)]));
}
items.push(Pending {
module: node.module,
decl: ts::Decl::TypeAlias(ts::TypeAlias {
doc: renamed_doc(
&node.name,
Doc::line(
"A union, as the discriminated union TypeScript narrows on \
`discriminator` (DDS-TS 1.0 ยง7.5). A `default:` case takes \
the discriminator's type minus the labels the other cases \
claim, because that is what it selects.",
),
),
exported: true,
name: Ident::new(&ts::escape_keyword(&node.name)),
generics: Vec::new(),
type_: ts::Type::Union(arms),
}),
});
}
TypeKind::Native => {
if ghost_created_for(applied, node.node_id, "TsNative").is_none() {
return Ok(());
}
items.push(Pending {
module: node.module,
decl: ts::Decl::TypeAlias(ts::TypeAlias {
doc: renamed_doc(
&node.name,
Doc::line(
"A native type. `unknown` unless a mapping facet named \
something: `unknown` is opaque and has to be narrowed \
before it is used, where `any` would switch the checking \
off.",
),
),
exported: true,
name: Ident::new(&ts::escape_keyword(&node.name)),
generics: Vec::new(),
type_: native_type(ast, id),
}),
});
}
TypeKind::ValueBox { .. } => {}
TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. }
| TypeKind::Any
| TypeKind::ValueBase => {}
}
Ok(())
}
fn emits_alias(ast: &Ast, applied: &Applied, id: Id<TypeNode>) -> bool {
let Ok(node) = ast.type_(id) else {
return false;
};
if node.name.is_empty() {
return false;
}
ghost_created_for(applied, node.node_id, "TsAlias").is_some()
}
fn pair(name: &str, idl_name: &str, slots: Vec<Slot>, doc: Doc) -> Vec<ts::Decl> {
let interface_name = ts::escape_keyword(name);
let mut factory_name = String::from(FACTORY_PREFIX);
factory_name.push_str(&interface_name);
let members: Vec<ts::Signature> = slots
.iter()
.map(|slot| {
ts::Signature::Property(ts::Property {
doc: renamed_member_doc(&slot.name, slot.doc.clone()),
readonly: false,
name: ts::PropertyName::ident(&ts::escape_member(&slot.name)),
optional: slot.optional,
type_: if slot.optional {
optional_of(slot.type_.clone())
} else {
slot.type_.clone()
},
})
})
.collect();
let entries: Vec<(ts::PropertyName, ts::Expr)> = slots
.iter()
.filter(|slot| !slot.optional)
.map(|slot| {
(
ts::PropertyName::ident(&ts::escape_member(&slot.name)),
slot.default.clone(),
)
})
.collect();
vec![
ts::Decl::Interface(ts::Interface {
doc: renamed_doc(idl_name, doc),
exported: true,
name: Ident::new(&interface_name),
generics: Vec::new(),
extends: Vec::new(),
members,
}),
ts::Decl::Function(ts::Function {
doc: Doc::lines(&[
"A value of this type, with every member at the value Table 9 of",
"DDS-XTypes 1.3 gives it.",
"",
"An interface is erased, so nothing can be constructed from one and a",
"decoder that opens with `let v: T = โฆ` needs a `T` from somewhere.",
"This is that somewhere. A function and not a class: DDS-TS 1.0 ยง7.4",
"emits no TypeScript class for an IDL data type, and a caller typed as",
"the interface depends on no implementation because there is none.",
]),
exported: true,
name: Ident::new(&factory_name),
generics: Vec::new(),
params: Vec::new(),
returns: Some(ts::Type::named(Path::single(&interface_name))),
body: Some(ts::Block {
statements: vec![ts::Statement::Return(Some(ts::Expr::Object(entries)))],
}),
}),
]
}
fn push_pair(
items: &mut Vec<Pending>,
module: Id<ModuleNode>,
name: &str,
slots: Vec<Slot>,
doc: Doc,
) {
for decl in pair(name, name, slots, doc) {
items.push(Pending { module, decl });
}
}
fn const_object(
module: Id<ModuleNode>,
name: &str,
entries: Vec<(ts::PropertyName, ts::Expr)>,
holder: ts::Type,
doc: Doc,
) -> Vec<Pending> {
let escaped = ts::escape_keyword(name);
vec![
Pending {
module,
decl: ts::Decl::Variable(ts::Variable {
doc: renamed_doc(name, doc),
exported: true,
declare: false,
kind: ts::VarKind::Const,
name: Ident::new(&escaped),
type_: None,
value: Some(ts::Expr::AsConst(Box::new(ts::Expr::Object(entries)))),
}),
},
Pending {
module,
decl: ts::Decl::TypeAlias(ts::TypeAlias {
doc: Doc::line("What a member of this type holds."),
exported: true,
name: Ident::new(&escaped),
generics: Vec::new(),
type_: holder,
}),
},
]
}
fn side_constant(name: String, type_: Option<ts::Type>, value: ts::Expr, doc: Doc) -> ts::Decl {
ts::Decl::Variable(ts::Variable {
doc,
exported: true,
declare: false,
kind: ts::VarKind::Const,
name: Ident::new(&name),
type_,
value: Some(value),
})
}
fn bound_constants(
ast: &Ast,
container: &str,
field: Option<&str>,
target: Id<TypeNode>,
) -> Result<Vec<ts::Decl>, MaterialiseError> {
let stem = match field {
Some(field) => format!("{container}_{field}"),
None => container.to_string(),
};
let at = || stem.clone();
let Ok(node) = ast.type_(target) else {
return Ok(Vec::new());
};
let bounded =
|bound: zerodds_ast::Bound, what: &str| -> Result<Vec<ts::Decl>, MaterialiseError> {
Ok(match evaluated_bound(ast, bound, &at())? {
Some(value) => vec![side_constant(
format!("{stem}_BOUND"),
None,
ts::Expr::Literal(Literal::int(value)),
Doc::line(what),
)],
None => Vec::new(),
})
};
Ok(match &node.kind {
TypeKind::String { bound, .. } => bounded(
*bound,
"The declared maximum length of this string, in characters. DDS-TS 1.0 \
ยง7.3.1: a TypeScript `string` carries no bound structurally, so the \
limit is stated beside the declaration and checked at the \
runtime-validation layer.",
)?,
TypeKind::Sequence { bound, .. } => bounded(
*bound,
"The declared maximum number of elements. DDS-TS 1.0 ยง7.9: a TypeScript \
`Array` carries no bound structurally.",
)?,
TypeKind::Map { bound, .. } => bounded(
*bound,
"The declared maximum number of entries. DDS-TS 1.0 ยง7.9.1.",
)?,
TypeKind::Array { dims, .. } => {
let mut lengths = Vec::new();
for dim in dims {
lengths.push(ast.eval_integral(*dim).map_err(|_| {
MaterialiseError::Unevaluated {
construct: "array length".to_string(),
at: at(),
}
})?);
}
match lengths.as_slice() {
[] => Vec::new(),
[only] => vec![side_constant(
format!("{stem}_LENGTH"),
None,
ts::Expr::Literal(Literal::int(*only)),
Doc::line(
"The exact length of this array. DDS-TS 1.0 ยง7.9: a TypeScript \
`Array` type states no length.",
),
)],
many => many
.iter()
.enumerate()
.map(|(index, length)| {
side_constant(
format!("{stem}_LENGTH_DIM{}", index + 1),
None,
ts::Expr::Literal(Literal::int(*length)),
Doc::line(
"One dimension of this array, in declaration order. \
DDS-TS 1.0 ยง7.9.",
),
)
})
.collect(),
}
}
_ => Vec::new(),
})
}
fn default_constant(
ast: &Ast,
applied: &Applied,
container: &str,
member: &zerodds_ast::MemberNode,
target: Id<TypeNode>,
) -> Result<Option<ts::Decl>, MaterialiseError> {
let Some(expr) = member.facets.xtypes.as_ref().and_then(|x| x.default_value) else {
return Ok(None);
};
let value = crate::default_typescript::explicit_default(ast, expr, target, &member.name)
.map_err(|error| MaterialiseError::NoLanguageForm {
construct: format!("a `@default` this column cannot spell ({error})"),
language: "TypeScript",
at: member.name.clone(),
})?;
Ok(Some(side_constant(
format!("{container}_{}_DEFAULT", member.name),
Some(ts_member_type(ast, applied, target, &member.name)?),
value,
Doc::line(
"The value `@default` gave this member. DDS-TS 1.0 ยง7.11: the annotation \
changes no interface shape, so it is carried here and in the descriptor's \
`default` property.",
),
)))
}
fn enum_alias_type(name: &str) -> ts::Type {
let path = Path::single(&ts::escape_keyword(name));
ts::Type::Indexed {
object: Box::new(ts::Type::Parenthesised(Box::new(ts::Type::TypeOf(
path.clone(),
)))),
index: Box::new(ts::Type::KeyOf(Box::new(ts::Type::TypeOf(path)))),
}
}
fn enum_ordinals(module: Id<ModuleNode>, name: &str, ordinals: &[(String, i128)]) -> Vec<Pending> {
let escaped = ts::escape_keyword(name);
let alias = ts::Type::named(Path::single(&escaped));
let forward: Vec<(ts::PropertyName, ts::Expr)> = ordinals
.iter()
.map(|(member, value)| {
(
ts::PropertyName::ident(member),
ts::Expr::Literal(Literal::int(*value)),
)
})
.collect();
let backward: Vec<ts::Expr> = ordinals
.iter()
.map(|(member, value)| {
ts::Expr::Array(vec![
ts::Expr::Literal(Literal::int(*value)),
ts::Expr::Path(Path::new(&[escaped.as_str(), member.as_str()])),
])
})
.collect();
vec![
Pending {
module,
decl: ts::Decl::Variable(ts::Variable {
doc: Doc::line(
"The numeric value each enumerator carries on the wire. DDS-TS 1.0 \
ยง7.6: the ordinals are the declaration's, whether `@value(N)` \
stated them or the position did.",
),
exported: true,
declare: false,
kind: ts::VarKind::Const,
name: Ident::new(&format!("{escaped}Ordinal")),
type_: Some(ts::Type::generic(
Path::single("Readonly"),
vec![ts::Type::generic(
Path::single("Record"),
vec![alias.clone(), ts::Type::number()],
)],
)),
value: Some(ts::Expr::AsConst(Box::new(ts::Expr::Object(forward)))),
}),
},
Pending {
module,
decl: ts::Decl::Variable(ts::Variable {
doc: Doc::line(
"The enumerator each wire value names โ the inverse of \
`<Enum>Ordinal`, for a reader that has the number.",
),
exported: true,
declare: false,
kind: ts::VarKind::Const,
name: Ident::new(&format!("{escaped}FromOrdinal")),
type_: Some(ts::Type::generic(
Path::single("ReadonlyMap"),
vec![ts::Type::number(), alias],
)),
value: Some(ts::Expr::New {
callee: Path::single("Map"),
args: vec![ts::Expr::AsConst(Box::new(ts::Expr::Array(backward)))],
}),
}),
},
]
}
pub fn ts_member_type(
ast: &Ast,
applied: &Applied,
target: Id<TypeNode>,
at: &str,
) -> Result<ts::Type, MaterialiseError> {
type_at(ast, applied, target, at, 0)
}
fn type_at(
ast: &Ast,
applied: &Applied,
id: Id<TypeNode>,
at: &str,
depth: usize,
) -> Result<ts::Type, MaterialiseError> {
if depth > MAX_TYPE_DEPTH {
return Err(MaterialiseError::Unevaluated {
construct: format!("type, whose TypeScript spelling nests past {MAX_TYPE_DEPTH}"),
at: at.to_string(),
});
}
let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
construct: "type".to_string(),
at: at.to_string(),
})?;
let refused = match &node.kind {
TypeKind::ValueBase => Some("ValueBase"),
TypeKind::ObjectRef { .. } => Some("Object"),
TypeKind::AnnotationType { .. } => Some("annotation type"),
_ => None,
};
if let Some(construct) = refused {
return Err(MaterialiseError::NoLanguageForm {
construct: construct.to_string(),
language: "TypeScript",
at: at.to_string(),
});
}
Ok(match &node.kind {
TypeKind::Primitive(PrimitiveKind::Float128) => ts::Type::named(Path::single(FLOAT128)),
TypeKind::Primitive(kind) => ts::Type::Keyword(ts_primitive(*kind)),
TypeKind::String { .. } | TypeKind::Fixed { .. } => ts::Type::string(),
TypeKind::Sequence { element, .. } => {
let Some(element) = element.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "sequence element".to_string(),
at: at.to_string(),
});
};
array_of(type_at(ast, applied, element, at, depth + 1)?)
}
TypeKind::Array { element, dims } => {
let Some(element) = element.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "array element".to_string(),
at: at.to_string(),
});
};
let mut out = type_at(ast, applied, element, at, depth + 1)?;
for _ in dims {
out = array_of(out);
}
out
}
TypeKind::Map { key, value, .. } => {
let (Some(key), Some(value)) = (key.resolved(), value.resolved()) else {
return Err(MaterialiseError::Unevaluated {
construct: "map key or value".to_string(),
at: at.to_string(),
});
};
ts::Type::generic(
Path::single("ReadonlyMap"),
vec![
type_at(ast, applied, key, at, depth + 1)?,
type_at(ast, applied, value, at, depth + 1)?,
],
)
}
TypeKind::Alias { aliased } => {
let Some(target) = aliased.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "typedef target".to_string(),
at: at.to_string(),
});
};
if emits_alias(ast, applied, id) {
ts::Type::named(qualified(ast, id)?)
} else {
type_at(ast, applied, target, at, depth + 1)?
}
}
TypeKind::Structure { .. }
| TypeKind::Union { .. }
| TypeKind::Bitset { .. }
| TypeKind::Valuetype { .. }
| TypeKind::Enumeration { .. }
| TypeKind::Bitmask { .. }
| TypeKind::Native => ts::Type::named(qualified(ast, id)?),
TypeKind::ValueBox { boxed: inner } => {
let Some(target) = inner.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "value box target".to_string(),
at: at.to_string(),
});
};
optional_of(type_at(ast, applied, target, at, depth + 1)?)
}
TypeKind::Any => ts::Type::named(Path::single(ANY_BOX)),
TypeKind::ValueBase | TypeKind::ObjectRef { .. } | TypeKind::AnnotationType { .. } => {
return Err(MaterialiseError::NoLanguageForm {
construct: node.kind.as_str().to_string(),
language: "TypeScript",
at: at.to_string(),
});
}
})
}
#[must_use]
pub const fn ts_primitive(kind: PrimitiveKind) -> ts::Keyword {
match kind {
PrimitiveKind::Boolean => ts::Keyword::Boolean,
PrimitiveKind::Char | PrimitiveKind::WChar => ts::Keyword::String,
PrimitiveKind::Int8
| PrimitiveKind::Octet
| PrimitiveKind::UInt8
| PrimitiveKind::Int16
| PrimitiveKind::UInt16
| PrimitiveKind::Int32
| PrimitiveKind::UInt32 => ts::Keyword::Number,
PrimitiveKind::Int64 | PrimitiveKind::UInt64 => ts::Keyword::BigInt,
PrimitiveKind::Float32 | PrimitiveKind::Float64 | PrimitiveKind::Float128 => {
ts::Keyword::Number
}
}
}
#[must_use]
pub fn unsigned_holder(bits: u16) -> ts::Type {
if bits <= 32 {
ts::Type::number()
} else {
ts::Type::bigint()
}
}
fn array_of(inner: ts::Type) -> ts::Type {
match inner {
ts::Type::Union(_) | ts::Type::Intersection(_) | ts::Type::Function { .. } => {
ts::Type::Array(Box::new(ts::Type::Parenthesised(Box::new(inner))))
}
other => ts::Type::Array(Box::new(other)),
}
}
fn optional_of(inner: ts::Type) -> ts::Type {
let mut arms = match inner {
ts::Type::Union(existing) => existing,
other => vec![other],
};
let undefined = ts::Type::Keyword(ts::Keyword::Undefined);
if !arms.contains(&undefined) {
arms.push(undefined);
}
ts::Type::Union(arms)
}
fn native_type(ast: &Ast, id: Id<TypeNode>) -> ts::Type {
let mapping = ast
.any_of_type(id)
.ok()
.and_then(|any| ast.facets(any))
.and_then(|facets| facets.corba.as_ref())
.and_then(|corba| corba.native_mapping.clone());
match mapping {
Some(spelling) => ts::Type::named(Path::new(&spelling.split('.').collect::<Vec<&str>>())),
None => ts::Type::Keyword(ts::Keyword::Unknown),
}
}
fn qualified(ast: &Ast, id: Id<TypeNode>) -> Result<Path, MaterialiseError> {
let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
construct: "type".to_string(),
at: "the file root".to_string(),
})?;
let mut segments = Vec::new();
for (_, name, _) in module_chain(ast, node.module)? {
segments.push(name);
}
segments.push(ts::escape_keyword(&node.name));
Ok(Path::new(
&segments.iter().map(String::as_str).collect::<Vec<&str>>(),
))
}
pub fn declared_path(ast: &Ast, id: Id<TypeNode>) -> Result<Path, MaterialiseError> {
qualified(ast, id)
}
pub fn factory_path(ast: &Ast, id: Id<TypeNode>) -> Result<Path, MaterialiseError> {
let path = qualified(ast, id)?;
let mut segments: Vec<String> = path
.segments()
.iter()
.map(|segment| segment.as_str().to_string())
.collect();
let Some(tail) = segments.last_mut() else {
return Err(MaterialiseError::Unevaluated {
construct: "the name of the type a value is constructed from".to_string(),
at: "the file root".to_string(),
});
};
*tail = format!("{FACTORY_PREFIX}{tail}");
Ok(Path::new(
&segments.iter().map(String::as_str).collect::<Vec<&str>>(),
))
}
pub fn ordinal_path(ast: &Ast, id: Id<TypeNode>) -> Result<Path, MaterialiseError> {
let Some(id) = resolved_enumeration(ast, id, 0) else {
return Err(MaterialiseError::NoLanguageForm {
construct: "an ordinal table for a type that is not an enumeration".to_string(),
language: "TypeScript",
at: "the file root".to_string(),
});
};
let path = qualified(ast, id)?;
let mut segments: Vec<String> = path
.segments()
.iter()
.map(|segment| segment.as_str().to_string())
.collect();
let Some(tail) = segments.last_mut() else {
return Err(MaterialiseError::Unevaluated {
construct: "the name of the enumeration an ordinal is looked up in".to_string(),
at: "the file root".to_string(),
});
};
tail.push_str("Ordinal");
Ok(Path::new(
&segments.iter().map(String::as_str).collect::<Vec<&str>>(),
))
}
pub fn map_entry_name(ast: &Ast, id: Id<TypeNode>) -> Result<String, MaterialiseError> {
let Ok(node) = ast.type_(id) else {
return Err(MaterialiseError::Unevaluated {
construct: "map".to_string(),
at: "the file root".to_string(),
});
};
let TypeKind::Map { key, value, .. } = &node.kind else {
return Err(MaterialiseError::Unevaluated {
construct: "map".to_string(),
at: named(&node.name, node.node_id),
});
};
let (Some(key), Some(value)) = (key.resolved(), value.resolved()) else {
return Err(MaterialiseError::Unevaluated {
construct: "map key or value".to_string(),
at: named(&node.name, node.node_id),
});
};
Ok(format!(
"{ENTRY_PREFIX}{}_{}",
type_tag(ast, key, 0)?,
type_tag(ast, value, 0)?
))
}
fn bound_tag(ast: &Ast, bound: &zerodds_ast::Bound) -> String {
match bound.expr() {
None => String::new(),
Some(expr) => match ast.eval_integral(expr) {
Ok(limit) => format!("_b{limit}"),
Err(_) => "_bx".to_string(),
},
}
}
pub fn type_tag(ast: &Ast, id: Id<TypeNode>, depth: usize) -> Result<String, MaterialiseError> {
if depth > MAX_TYPE_DEPTH {
return Err(MaterialiseError::Unevaluated {
construct: format!("type, whose name nests past {MAX_TYPE_DEPTH}"),
at: "the file root".to_string(),
});
}
let Ok(node) = ast.type_(id) else {
return Ok("unknown".to_string());
};
Ok(match &node.kind {
TypeKind::Primitive(kind) => kind.as_str().replace(' ', "_"),
TypeKind::String {
width: CharWidth::Char,
bound,
} => format!("string{}", bound_tag(ast, bound)),
TypeKind::String {
width: CharWidth::WChar,
bound,
} => format!("wstring{}", bound_tag(ast, bound)),
TypeKind::Fixed { digits, scale } => format!("fixed_{digits}_{scale}"),
TypeKind::Sequence { element, bound } => match element.resolved() {
Some(element) => format!(
"seq{}_{}",
bound_tag(ast, bound),
type_tag(ast, element, depth + 1)?
),
None => format!("seq_unknown{}", bound_tag(ast, bound)),
},
TypeKind::Array { element, dims } => {
let mut name = match element.resolved() {
Some(element) => format!("arr_{}", type_tag(ast, element, depth + 1)?),
None => "arr_unknown".to_string(),
};
for dim in dims {
match ast.eval_integral(*dim) {
Ok(length) => name.push_str(&format!("_{length}")),
Err(_) => name.push_str("_x"),
}
}
name
}
TypeKind::Map { key, value, bound } => {
let key = match key.resolved() {
Some(key) => type_tag(ast, key, depth + 1)?,
None => "unknown".to_string(),
};
let value = match value.resolved() {
Some(value) => type_tag(ast, value, depth + 1)?,
None => "unknown".to_string(),
};
format!("map{}_{key}_{value}", bound_tag(ast, bound))
}
TypeKind::Alias { aliased } => match aliased.resolved() {
Some(target) => type_tag(ast, target, depth + 1)?,
None => "unknown".to_string(),
},
TypeKind::ValueBox { boxed: inner } => match inner.resolved() {
Some(target) => format!("box_{}", type_tag(ast, target, depth + 1)?),
None => "box_unknown".to_string(),
},
TypeKind::Structure { .. }
| TypeKind::Union { .. }
| TypeKind::Enumeration { .. }
| TypeKind::Bitmask { .. }
| TypeKind::Bitset { .. }
| TypeKind::Valuetype { .. }
| TypeKind::Native => {
let mut segments = Vec::new();
for (_, name, _) in module_chain(ast, node.module)? {
segments.push(name);
}
segments.push(ts::escape_keyword(&node.name));
segments.join("_")
}
TypeKind::Any => "any".to_string(),
TypeKind::ValueBase => "valuebase".to_string(),
TypeKind::ObjectRef { .. } => "objectref".to_string(),
TypeKind::AnnotationType { .. } => "annotation".to_string(),
})
}
fn discriminant_type(
ast: &Ast,
discriminator: Id<TypeNode>,
disc_type: &ts::Type,
labels: &[CaseLabel],
explicit: &[ts::Type],
at: &str,
) -> Result<ts::Type, MaterialiseError> {
let mut arms: Vec<ts::Type> = Vec::new();
for label in labels {
match label {
CaseLabel::Default => return Ok(excluding(disc_type, explicit)),
CaseLabel::Value(expr) => {
let value =
ast.eval_integral(*expr)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "union case label".to_string(),
at: at.to_string(),
})?;
let literal = discriminant_type_of(ast, discriminator, value)?;
if !arms.contains(&literal) {
arms.push(literal);
}
}
}
}
Ok(match arms.len() {
0 => disc_type.clone(),
1 => arms.into_iter().next().unwrap_or_else(|| disc_type.clone()),
_ => ts::Type::Union(arms),
})
}
fn excluding(disc_type: &ts::Type, explicit: &[ts::Type]) -> ts::Type {
let removed = match explicit.len() {
0 => return disc_type.clone(),
1 => explicit
.first()
.cloned()
.unwrap_or_else(|| disc_type.clone()),
_ => ts::Type::Union(explicit.to_vec()),
};
ts::Type::generic(Path::single("Exclude"), vec![disc_type.clone(), removed])
}
fn explicit_labels(
ast: &Ast,
id: Id<TypeNode>,
discriminator: Id<TypeNode>,
at: &str,
) -> Result<Vec<ts::Type>, MaterialiseError> {
let mut out: Vec<ts::Type> = Vec::new();
for case in ast.cases_of(id) {
let Ok(case_node) = ast.case(*case) else {
continue;
};
for label in &case_node.labels {
let CaseLabel::Value(expr) = label else {
continue;
};
let value = ast
.eval_integral(*expr)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "union case label".to_string(),
at: at.to_string(),
})?;
let literal = discriminant_type_of(ast, discriminator, value)?;
if !out.contains(&literal) {
out.push(literal);
}
}
}
Ok(out)
}
fn discriminant_type_of(
ast: &Ast,
discriminator: Id<TypeNode>,
value: i128,
) -> Result<ts::Type, MaterialiseError> {
let Some(enumeration) = resolved_enumeration(ast, discriminator, 0) else {
return Ok(ts::Type::Literal(discriminant_literal(
ast,
discriminator,
value,
)));
};
let name = enumerator_named_by(ast, enumeration, value).ok_or_else(|| {
MaterialiseError::Unevaluated {
construct: format!("union case label {value}, which names no enumerator"),
at: "a union case label".to_string(),
}
})?;
let mut segments: Vec<String> = qualified(ast, enumeration)?
.segments()
.iter()
.map(|segment| segment.as_str().to_string())
.collect();
segments.push(ts::escape_member(&name));
Ok(ts::Type::TypeOf(Path::new(
&segments.iter().map(String::as_str).collect::<Vec<&str>>(),
)))
}
fn discriminant_expr(
ast: &Ast,
discriminator: Id<TypeNode>,
value: i128,
) -> Result<ts::Expr, MaterialiseError> {
let Some(enumeration) = resolved_enumeration(ast, discriminator, 0) else {
return Ok(ts::Expr::Literal(discriminant_literal(
ast,
discriminator,
value,
)));
};
let name = enumerator_named_by(ast, enumeration, value).ok_or_else(|| {
MaterialiseError::Unevaluated {
construct: format!("union discriminator value {value}, which names no enumerator"),
at: "a union discriminator".to_string(),
}
})?;
enumerator_path(ast, enumeration, &name)
}
fn discriminant_literal(ast: &Ast, discriminator: Id<TypeNode>, value: i128) -> Literal {
match resolved_kind(ast, discriminator, 0) {
Some(PrimitiveKind::Boolean) => Literal::Bool(value != 0),
Some(PrimitiveKind::Char | PrimitiveKind::WChar) => u32::try_from(value)
.ok()
.and_then(char::from_u32)
.map_or_else(|| Literal::int(value), Literal::Char),
Some(PrimitiveKind::Int64 | PrimitiveKind::UInt64) => Literal::Int(format!("{value}n")),
_ => Literal::int(value),
}
}
fn resolved_kind(ast: &Ast, id: Id<TypeNode>, depth: usize) -> Option<PrimitiveKind> {
if depth > MAX_TYPE_DEPTH {
return None;
}
match &ast.type_(id).ok()?.kind {
TypeKind::Primitive(kind) => Some(*kind),
TypeKind::Alias { aliased } => resolved_kind(ast, aliased.resolved()?, depth + 1),
_ => None,
}
}
fn default_expr(
ast: &Ast,
applied: &Applied,
id: Id<TypeNode>,
at: &str,
depth: usize,
) -> Result<ts::Expr, MaterialiseError> {
if depth > MAX_TYPE_DEPTH {
return Err(MaterialiseError::Unevaluated {
construct: format!("default value, which nests past {MAX_TYPE_DEPTH}"),
at: at.to_string(),
});
}
let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
construct: "type".to_string(),
at: at.to_string(),
})?;
Ok(match &node.kind {
TypeKind::Primitive(PrimitiveKind::Float128) => ts::Expr::Object(vec![(
ts::PropertyName::ident("octets"),
ts::Expr::New {
callee: Path::single("Uint8Array"),
args: vec![ts::Expr::Literal(Literal::int(16))],
},
)]),
TypeKind::Primitive(PrimitiveKind::Boolean) => ts::Expr::Literal(Literal::Bool(false)),
TypeKind::Primitive(PrimitiveKind::Char | PrimitiveKind::WChar) => {
ts::Expr::Literal(Literal::Char('\0'))
}
TypeKind::Primitive(PrimitiveKind::Int64 | PrimitiveKind::UInt64) => {
ts::Expr::Literal(Literal::Int("0n".to_string()))
}
TypeKind::Primitive(_) => ts::Expr::Literal(Literal::int(0)),
TypeKind::String { .. } | TypeKind::Fixed { .. } => ts::Expr::Literal(Literal::string("")),
TypeKind::Sequence { .. } => ts::Expr::Array(Vec::new()),
TypeKind::Map { .. } => ts::Expr::New {
callee: Path::single("Map"),
args: Vec::new(),
},
TypeKind::Array { element, dims } => {
let Some(element) = element.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "array element".to_string(),
at: at.to_string(),
});
};
let mut lengths = Vec::new();
for dim in dims {
lengths.push(ast.eval_integral(*dim).map_err(|_| {
MaterialiseError::Unevaluated {
construct: "array length".to_string(),
at: at.to_string(),
}
})?);
}
let mut out = default_expr(ast, applied, element, at, depth + 1)?;
for length in lengths.into_iter().rev() {
out = filled_array(length, out);
}
out
}
TypeKind::Alias { aliased } => {
let Some(target) = aliased.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "typedef target".to_string(),
at: at.to_string(),
});
};
default_expr(ast, applied, target, at, depth + 1)?
}
TypeKind::Enumeration { .. } => enum_default_literal(ast, id, at)?,
TypeKind::Bitmask { .. } => zero_of(&unsigned_holder(ast.effective_bit_bound(id))),
TypeKind::Structure { .. } | TypeKind::Valuetype { .. } | TypeKind::Bitset { .. } => {
ts::Expr::Call {
callee: Box::new(ts::Expr::Path(factory_path(ast, id)?)),
args: Vec::new(),
}
}
TypeKind::Union { discriminator, .. } => {
let Some(disc) = discriminator.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "union discriminator".to_string(),
at: at.to_string(),
});
};
union_default(ast, applied, id, disc, at, depth)?
}
TypeKind::ValueBox { .. } | TypeKind::Native => ts::Expr::Undefined,
TypeKind::Any => ts::Expr::Object(vec![
(
ts::PropertyName::ident("typeId"),
ts::Expr::Literal(Literal::string("")),
),
(ts::PropertyName::ident("value"), ts::Expr::Undefined),
]),
TypeKind::ValueBase | TypeKind::ObjectRef { .. } | TypeKind::AnnotationType { .. } => {
return Err(MaterialiseError::NoLanguageForm {
construct: node.kind.as_str().to_string(),
language: "TypeScript",
at: at.to_string(),
});
}
})
}
fn union_default(
ast: &Ast,
applied: &Applied,
id: Id<TypeNode>,
discriminator: Id<TypeNode>,
at: &str,
depth: usize,
) -> Result<ts::Expr, MaterialiseError> {
let selector = discriminator_default_value(ast, discriminator);
let mut fallback: Option<(&zerodds_ast::CaseNode, i128)> = None;
let mut first: Option<(&zerodds_ast::CaseNode, i128)> = None;
let mut chosen: Option<(&zerodds_ast::CaseNode, i128)> = None;
for case in ast.cases_of(id) {
let Ok(case_node) = ast.case(*case) else {
continue;
};
let mut label_value = None;
let mut is_default = false;
for label in &case_node.labels {
match label {
CaseLabel::Default => is_default = true,
CaseLabel::Value(expr) => {
if let Ok(value) = ast.eval_integral(*expr) {
if label_value.is_none() {
label_value = Some(value);
}
if Some(value) == selector && chosen.is_none() {
chosen = Some((case_node, value));
}
}
}
}
}
let value = label_value.or(selector).unwrap_or(0);
if is_default && fallback.is_none() {
fallback = Some((case_node, value));
}
if first.is_none() {
first = Some((case_node, value));
}
}
let Some((case_node, value)) = chosen.or(fallback).or(first) else {
return Ok(ts::Expr::Object(vec![(
ts::PropertyName::ident(DISCRIMINANT_PROPERTY),
discriminant_expr(ast, discriminator, selector.unwrap_or(0))?,
)]));
};
let Ok(member) = ast.member(case_node.member) else {
return Err(MaterialiseError::Unevaluated {
construct: "member behind a union case the model listed".to_string(),
at: at.to_string(),
});
};
let Some(target) = member.type_.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "union case type".to_string(),
at: member.name.clone(),
});
};
Ok(ts::Expr::Object(vec![
(
ts::PropertyName::ident(DISCRIMINANT_PROPERTY),
discriminant_expr(ast, discriminator, value)?,
),
(
ts::PropertyName::ident(&ts::escape_member(&member.name)),
default_expr(ast, applied, target, &member.name, depth + 1)?,
),
]))
}
fn discriminator_default_value(ast: &Ast, discriminator: Id<TypeNode>) -> Option<i128> {
match &ast.type_(discriminator).ok()?.kind {
TypeKind::Alias { aliased } => discriminator_default_value(ast, aliased.resolved()?),
TypeKind::Enumeration { .. } => Some(enum_default_value(ast, discriminator)),
TypeKind::Primitive(_) => Some(0),
_ => None,
}
}
fn enumerator_path(
ast: &Ast,
id: Id<TypeNode>,
enumerator: &str,
) -> Result<ts::Expr, MaterialiseError> {
let mut segments: Vec<String> = qualified(ast, id)?
.segments()
.iter()
.map(|segment| segment.as_str().to_string())
.collect();
segments.push(ts::escape_member(enumerator));
Ok(ts::Expr::Path(Path::new(
&segments.iter().map(String::as_str).collect::<Vec<&str>>(),
)))
}
fn enum_default_literal(
ast: &Ast,
id: Id<TypeNode>,
at: &str,
) -> Result<ts::Expr, MaterialiseError> {
let Some(enumerator) = crate::emit::default_enumerator_name(ast, id) else {
return Err(MaterialiseError::Unevaluated {
construct: "enumeration with no enumerator to take as its default".to_string(),
at: at.to_string(),
});
};
enumerator_path(ast, id, &enumerator)
}
fn enumerator_named_by(ast: &Ast, id: Id<TypeNode>, value: i128) -> Option<String> {
for enumerator in ast.enumerators_of(id) {
if ast.effective_enumerator_value(*enumerator) == Some(value) {
return ast
.enumerator(*enumerator)
.ok()
.map(|node| node.name.clone());
}
}
None
}
fn resolved_enumeration(ast: &Ast, id: Id<TypeNode>, depth: usize) -> Option<Id<TypeNode>> {
if depth > MAX_TYPE_DEPTH {
return None;
}
match &ast.type_(id).ok()?.kind {
TypeKind::Enumeration { .. } => Some(id),
TypeKind::Alias { aliased } => resolved_enumeration(ast, aliased.resolved()?, depth + 1),
_ => None,
}
}
fn enum_default_value(ast: &Ast, id: Id<TypeNode>) -> i128 {
let mut first = None;
for enumerator in ast.enumerators_of(id) {
let Ok(node) = ast.enumerator(*enumerator) else {
continue;
};
let Some(value) = ast.effective_enumerator_value(*enumerator) else {
continue;
};
if node
.facets
.xtypes
.as_ref()
.is_some_and(|x| x.is_default_literal)
{
return value;
}
if first.is_none() {
first = Some(value);
}
}
first.unwrap_or(0)
}
fn filled_array(length: i128, element: ts::Expr) -> ts::Expr {
let element = match element {
ts::Expr::Object(_) => ts::Expr::Parenthesised(Box::new(element)),
other => other,
};
ts::Expr::Call {
callee: Box::new(ts::Expr::Member {
object: Box::new(ts::Expr::Path(Path::single("Array"))),
name: Ident::new("from"),
}),
args: vec![
ts::Expr::Object(vec![(
ts::PropertyName::ident("length"),
ts::Expr::Literal(Literal::int(length)),
)]),
ts::Expr::Arrow {
params: Vec::new(),
body: Box::new(element),
},
],
}
}
fn zero_of(holder: &ts::Type) -> ts::Expr {
if *holder == ts::Type::bigint() {
ts::Expr::Literal(Literal::Int("0n".to_string()))
} else {
ts::Expr::Literal(Literal::int(0))
}
}
fn bit_literal(position: u16, wide: bool) -> Literal {
let value = i128::from(1u8) << u32::from(position);
if wide {
Literal::Int(format!("{value}n"))
} else {
Literal::int(value)
}
}
fn constant_expr(
ast: &Ast,
declared: Option<Id<TypeNode>>,
value: &zerodds_ast::ConstValue,
at: &str,
) -> Result<ts::Expr, MaterialiseError> {
use zerodds_ast::ConstValue;
let refuse = |construct: String| MaterialiseError::NoLanguageForm {
construct,
language: "TypeScript",
at: at.to_string(),
};
let float128 = declared
.and_then(|id| resolved_kind(ast, id, 0))
.is_some_and(|kind| matches!(kind, PrimitiveKind::Float128));
match value {
ConstValue::Integer(number) => integer_literal(ast, declared, *number, at),
ConstValue::Boolean(flag) => Ok(ts::Expr::Literal(Literal::Bool(*flag))),
ConstValue::Char(character) => Ok(ts::Expr::Literal(Literal::Char(*character))),
ConstValue::WChar(point) => match constant::wchar_char(*point) {
Some(character) => Ok(ts::Expr::Literal(Literal::Char(character))),
None => Err(refuse(format!(
"the code point U+{point:04X}, which is not a Unicode scalar value"
))),
},
ConstValue::Float { mantissa, exponent } => {
if float128 {
return Err(refuse(String::from(
"a `long double` constant, which has no TypeScript literal",
)));
}
Ok(ts::Expr::Literal(Literal::Float(constant::float_text(
mantissa,
exponent.as_deref(),
))))
}
ConstValue::Fixed { digits, scale } => Ok(ts::Expr::Literal(Literal::Str(
constant::fixed_decimal(digits, *scale),
))),
ConstValue::String(text) => Ok(ts::Expr::Literal(Literal::Str(text.clone()))),
ConstValue::WString(points) => match constant::wstring_utf8(points) {
Some(text) => Ok(ts::Expr::Literal(Literal::Str(text))),
None => Err(refuse(String::from(
"a `wstring` constant holding a lone surrogate",
))),
},
_ => Err(refuse(String::from(
"a constant form this binding has no literal for",
))),
}
}
fn integer_literal(
ast: &Ast,
declared: Option<Id<TypeNode>>,
value: i128,
at: &str,
) -> Result<ts::Expr, MaterialiseError> {
if let Some(enumeration) = declared.and_then(|id| resolved_enumeration(ast, id, 0)) {
let Some(name) = enumerator_named_by(ast, enumeration, value) else {
return Err(MaterialiseError::NoLanguageForm {
construct: format!("the value {value}, which names no enumerator"),
language: "TypeScript",
at: at.to_string(),
});
};
return enumerator_path(ast, enumeration, &name);
}
let kind = declared.and_then(|id| resolved_kind(ast, id, 0));
Ok(match kind {
Some(PrimitiveKind::Boolean) => ts::Expr::Literal(Literal::Bool(value != 0)),
Some(PrimitiveKind::Char | PrimitiveKind::WChar) => {
match u32::try_from(value).ok().and_then(char::from_u32) {
Some(character) => ts::Expr::Literal(Literal::Char(character)),
None => {
return Err(MaterialiseError::NoLanguageForm {
construct: format!(
"the value {value}, which is not a Unicode scalar value and \
so is not a one-unit TypeScript `string`"
),
language: "TypeScript",
at: at.to_string(),
});
}
}
}
Some(PrimitiveKind::Int64 | PrimitiveKind::UInt64) => {
ts::Expr::Literal(Literal::Int(format!("{value}n")))
}
_ => ts::Expr::Literal(Literal::int(in_range(
value,
-MAX_SAFE_INTEGER,
MAX_SAFE_INTEGER,
"number",
at,
)?)),
})
}
fn in_range(
value: i128,
low: i128,
high: i128,
type_: &str,
at: &str,
) -> Result<i128, MaterialiseError> {
if value < low || value > high {
return Err(MaterialiseError::NoLanguageForm {
construct: format!(
"the value {value}, which a TypeScript `{type_}` does not hold as itself"
),
language: "TypeScript",
at: at.to_string(),
});
}
Ok(value)
}
fn renamed_doc(idl_name: &str, doc: Doc) -> Doc {
if !ts::is_reserved(idl_name) {
return doc;
}
let mut out = doc;
out.push(&format!(
"IDL `{idl_name}`, renamed: it is a reserved word in a TypeScript binding."
));
out
}
fn renamed_member_doc(idl_name: &str, doc: Doc) -> Doc {
if !ts::is_reserved_member(idl_name) {
return doc;
}
let mut out = doc;
out.push(&format!(
"IDL `{idl_name}`, renamed: a class body parses `{idl_name}` as the \
constructor, so `{idl_name}: T` is a syntax error."
));
out
}
fn named(name: &str, node: NodeId) -> String {
if name.is_empty() {
format!("{node:?}")
} else {
name.to_string()
}
}
fn ghost_created_for(applied: &Applied, node: NodeId, type_name: &str) -> Option<GhostId> {
let anchor = applied
.origin
.iter()
.find(|(_, model)| **model == node)
.map(|(ghost, _)| *ghost)?;
let typ = applied.graph.types.lookup(type_name)?;
applied
.graph
.nodes_of_type(typ)
.map(|created| created.id)
.find(|created| {
applied.graph.connected(&anchor, created)
|| applied
.graph
.parts(&anchor)
.any(|part| applied.graph.connected(&part.other, created))
})
}
pub const DEFAULT_RUNTIME_PACKAGE: &str = "@zerodds/wire";
const ANCHOR_BITS: u16 = 64;
const DISCRIMINATOR_ID: u32 = 0xffff_ffff;
const PARENT_ID: u32 = 0xffff_fffe;
pub const DISCRIMINANT_PROPERTY: &str = "discriminator";
#[must_use]
pub fn descriptor_import(module: &str) -> ts::Import {
ts::Import {
module: module.to_string(),
type_only: false,
default: None,
namespace: None,
names: vec![
ts::ImportName {
name: Ident::new("registerType"),
alias: None,
type_only: false,
},
ts::ImportName {
name: Ident::new("DdsTypeDescriptor"),
alias: None,
type_only: true,
},
],
}
}
pub fn ts_descriptors(ast: &Ast, applied: &Applied) -> Result<Vec<ts::Decl>, MaterialiseError> {
let items = descriptor_pending(ast, applied)?;
let mut root = Scope::default();
for item in items {
insert(ast, &mut root, item)?;
}
scope_decls(root, 0)
}
fn descriptor_pending(ast: &Ast, applied: &Applied) -> Result<Vec<Pending>, MaterialiseError> {
let mut items: Vec<Pending> = Vec::new();
for id in ordered_types(ast) {
describe_type(ast, applied, id, &mut items)?;
}
for (_, node) in ast.exceptions() {
if ghost_created_for(applied, node.node_id, "TsError").is_none() {
continue;
}
let mut fields = Vec::new();
for (position, member) in node.members.iter().enumerate() {
fields.push(member_field(ast, applied, *member, position, &node.name)?);
}
let spec = TypeSpec {
kind: "exception",
local: ts::escape_keyword(&node.name),
guard: guard_name(&node.name),
qualified: qualified_idl_name(ast, node.module, &node.name)?,
extensibility: extensibility_word(
node.facets
.extensibility()
.unwrap_or(zerodds_ast::DEFAULT_EXTENSIBILITY),
),
nested: is_nested(&node.facets),
topic: topic_of(&node.facets, &node.name),
autoid: autoid_word(&node.facets),
bit_bound: None,
has_default: None,
has_implicit_default: None,
fields,
guard_shape: GuardShape::Object,
};
push_descriptor(&mut items, node.module, &spec);
}
Ok(items)
}
struct TypeSpec {
kind: &'static str,
local: String,
guard: String,
qualified: String,
extensibility: &'static str,
nested: bool,
topic: Option<String>,
autoid: Option<&'static str>,
bit_bound: Option<u16>,
has_default: Option<bool>,
has_implicit_default: Option<bool>,
fields: Vec<FieldSpec>,
guard_shape: GuardShape,
}
struct FieldSpec {
name: String,
id: u32,
type_ref: ts::Expr,
key: bool,
optional: bool,
must_understand: bool,
default: Option<ts::Expr>,
unit: Option<String>,
min: Option<ts::Expr>,
max: Option<ts::Expr>,
hashid: Option<String>,
labels: Option<Option<Vec<ts::Expr>>>,
key_equality_hazard: bool,
base: bool,
check: Option<ts::Expr>,
}
enum GuardShape {
Object,
TypeOf(&'static str),
Delegate(Box<ts::Expr>),
}
fn describe_type(
ast: &Ast,
applied: &Applied,
id: Id<TypeNode>,
items: &mut Vec<Pending>,
) -> Result<(), MaterialiseError> {
let Ok(node) = ast.type_(id) else {
return Ok(());
};
let Some(spec) = type_spec(ast, applied, id, node)? else {
return Ok(());
};
push_descriptor(items, node.module, &spec);
Ok(())
}
#[allow(clippy::too_many_lines)]
fn type_spec(
ast: &Ast,
applied: &Applied,
id: Id<TypeNode>,
node: &TypeNode,
) -> Result<Option<TypeSpec>, MaterialiseError> {
let base =
|kind: &'static str, guard_shape: GuardShape| -> Result<TypeSpec, MaterialiseError> {
Ok(TypeSpec {
kind,
local: ts::escape_keyword(&node.name),
guard: guard_name(&node.name),
qualified: qualified_idl_name(ast, node.module, &node.name)?,
extensibility: extensibility_word(ast.effective_extensibility(id)),
nested: is_nested(&node.facets),
topic: topic_of(&node.facets, &node.name),
autoid: autoid_word(&node.facets),
bit_bound: None,
has_default: None,
has_implicit_default: None,
fields: Vec::new(),
guard_shape,
})
};
Ok(match &node.kind {
TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
let created = if matches!(node.kind, TypeKind::Valuetype { .. }) {
"TsValue"
} else {
"TsStruct"
};
if ghost_created_for(applied, node.node_id, created).is_none() {
return Ok(None);
}
let mut spec = base("struct", GuardShape::Object)?;
if let Some(base_type) = super::c::struct_base(ast, id) {
spec.fields
.push(parent_field(ast, applied, base_type, &node.name)?);
}
let own = ast.members_of(id).to_vec();
for (position, member) in ast.all_members_of(id).into_iter().enumerate() {
if own.contains(&member) {
spec.fields
.push(member_field(ast, applied, member, position, &node.name)?);
}
}
Some(spec)
}
TypeKind::Union { discriminator, .. } => {
if ghost_created_for(applied, node.node_id, "TsUnion").is_none() {
return Ok(None);
}
let Some(disc) = discriminator.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "union discriminator".to_string(),
at: node.name.clone(),
});
};
let mut spec = base("union", GuardShape::Object)?;
spec.fields.push(FieldSpec {
name: DISCRIMINANT_PROPERTY.to_string(),
id: DISCRIMINATOR_ID,
type_ref: type_ref_expr(ast, applied, disc, &node.name, 0)?,
key: false,
optional: false,
must_understand: false,
default: None,
unit: None,
min: None,
max: None,
hashid: None,
labels: None,
key_equality_hazard: false,
base: false,
check: guard_check(ast, applied, disc, &member_access(DISCRIMINANT_PROPERTY))?,
});
let mut declared_default = false;
let mut covered: Vec<i128> = Vec::new();
for case in ast.cases_of(id) {
let Ok(case_node) = ast.case(*case) else {
return Err(MaterialiseError::Unevaluated {
construct: "union case the model listed".to_string(),
at: node.name.clone(),
});
};
if ghost_created_for(applied, case_node.node_id, "TsCase").is_none() {
continue;
}
let mut labels: Option<Vec<ts::Expr>> = Some(Vec::new());
for label in &case_node.labels {
match label {
CaseLabel::Default => {
declared_default = true;
labels = None;
}
CaseLabel::Value(expr) => {
let value = ast.eval_integral(*expr).map_err(|_| {
MaterialiseError::Unevaluated {
construct: "union case label".to_string(),
at: node.name.clone(),
}
})?;
covered.push(value);
if let Some(list) = labels.as_mut() {
list.push(discriminant_expr(ast, disc, value)?);
}
}
}
}
let position = spec.fields.len();
let mut field = member_field(ast, applied, case_node.member, position, &node.name)?;
field.labels = Some(labels);
field.check = None;
spec.fields.push(field);
}
spec.has_default = Some(declared_default);
spec.has_implicit_default =
Some(!declared_default && !exhausts_discriminator(ast, disc, &covered));
Some(spec)
}
TypeKind::Enumeration { enumerators } => {
if ghost_created_for(applied, node.node_id, "TsEnum").is_none() {
return Ok(None);
}
let mut spec = base("enum", GuardShape::TypeOf("string"))?;
spec.bit_bound = Some(ast.effective_bit_bound(id));
for (position, enumerator) in enumerators.iter().enumerate() {
let Ok(enum_node) = ast.enumerator(*enumerator) else {
return Err(MaterialiseError::Unevaluated {
construct: "enumerator the model listed".to_string(),
at: node.name.clone(),
});
};
if ghost_created_for(applied, enum_node.node_id, "TsEnumerator").is_none() {
continue;
}
let value = ast.effective_enumerator_value(*enumerator).ok_or_else(|| {
MaterialiseError::Unevaluated {
construct: "enumerator value".to_string(),
at: enum_node.name.clone(),
}
})?;
spec.fields.push(FieldSpec {
name: enum_node.name.clone(),
id: u32::try_from(position).unwrap_or(u32::MAX),
type_ref: primitive_ref("int32"),
key: false,
optional: false,
must_understand: false,
default: Some(ts::Expr::Literal(Literal::int(value))),
unit: None,
min: None,
max: None,
hashid: None,
labels: None,
key_equality_hazard: false,
base: false,
check: None,
});
}
Some(spec)
}
TypeKind::Bitmask { bits } => {
if ghost_created_for(applied, node.node_id, "TsBitmask").is_none() {
return Ok(None);
}
let bound = ast.effective_bit_bound(id);
let wide = unsigned_holder(bound) == ts::Type::bigint();
let mut spec = base(
"bitmask",
GuardShape::TypeOf(if wide { "bigint" } else { "number" }),
)?;
spec.bit_bound = Some(bound);
for (position, bit) in bits.iter().enumerate() {
let Ok(bit_node) = ast.bit_value(*bit) else {
return Err(MaterialiseError::Unevaluated {
construct: "bitmask bit the model listed".to_string(),
at: node.name.clone(),
});
};
if ghost_created_for(applied, bit_node.node_id, "TsBitConst").is_none() {
continue;
}
let at = ast.effective_bit_position(*bit).ok_or_else(|| {
MaterialiseError::Unevaluated {
construct: "bit position".to_string(),
at: bit_node.name.clone(),
}
})?;
spec.fields.push(FieldSpec {
name: bit_node.name.clone(),
id: u32::try_from(position).unwrap_or(u32::MAX),
type_ref: bitfield_ref(1),
key: false,
optional: false,
must_understand: false,
default: Some(ts::Expr::Literal(Literal::int(i128::from(at)))),
unit: None,
min: None,
max: None,
hashid: None,
labels: None,
key_equality_hazard: false,
base: false,
check: None,
});
}
Some(spec)
}
TypeKind::Bitset { .. } => {
if ghost_created_for(applied, node.node_id, "TsBitset").is_none() {
return Ok(None);
}
let mut spec = base("bitset", GuardShape::Object)?;
let mut total: u32 = 0;
for (position, bitfield) in ast.all_bitfields_of(id).into_iter().enumerate() {
let Ok(field_node) = ast.bitfield(bitfield) else {
return Err(MaterialiseError::Unevaluated {
construct: "bitset field the model listed".to_string(),
at: node.name.clone(),
});
};
total += u32::from(field_node.bits);
let Some(name) = field_node.name.as_ref() else {
continue;
};
let holder = unsigned_holder(field_node.bits);
spec.fields.push(FieldSpec {
name: name.clone(),
id: u32::try_from(position).unwrap_or(u32::MAX),
type_ref: bitfield_ref(field_node.bits),
key: false,
optional: false,
must_understand: false,
default: None,
unit: None,
min: None,
max: None,
hashid: None,
labels: None,
key_equality_hazard: false,
base: false,
check: Some(typeof_is(
&member_access(name),
if holder == ts::Type::bigint() {
"bigint"
} else {
"number"
},
)),
});
}
let bound = match u16::try_from(total) {
Ok(bound) if bound <= ANCHOR_BITS => bound,
_ => {
return Err(MaterialiseError::NoLanguageForm {
construct: format!(
"a bitset of {total} bits, which is wider than the \
{ANCHOR_BITS} an anchor holds"
),
language: "TypeScript",
at: node.name.clone(),
});
}
};
spec.bit_bound = Some(bound);
Some(spec)
}
TypeKind::Alias { aliased } => {
if !emits_alias(ast, applied, id) {
return Ok(None);
}
let Some(target) = aliased.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "typedef target".to_string(),
at: node.name.clone(),
});
};
let mut spec = base(
"alias",
GuardShape::Delegate(Box::new(
guard_check(ast, applied, target, &ts::Expr::Path(Path::single("v")))?
.unwrap_or(ts::Expr::Literal(Literal::Bool(true))),
)),
)?;
let bound = node.facets.xtypes.as_ref().and_then(|x| x.bit_bound);
spec.bit_bound = bound;
spec.fields.push(FieldSpec {
name: "value".to_string(),
id: 0,
type_ref: type_ref_expr(ast, applied, target, &node.name, 0)?,
key: false,
optional: false,
must_understand: false,
default: None,
unit: None,
min: None,
max: None,
hashid: None,
labels: None,
key_equality_hazard: false,
base: false,
check: None,
});
Some(spec)
}
_ => None,
})
}
fn exhausts_discriminator(ast: &Ast, discriminator: Id<TypeNode>, covered: &[i128]) -> bool {
let distinct = |values: &[i128]| {
let mut seen: Vec<i128> = Vec::new();
for value in values {
if !seen.contains(value) {
seen.push(*value);
}
}
seen.len()
};
if matches!(
resolved_kind(ast, discriminator, 0),
Some(PrimitiveKind::Boolean)
) {
return distinct(covered) >= 2;
}
let mut resolved = discriminator;
for _ in 0..MAX_TYPE_DEPTH {
match ast.type_(resolved).map(|node| &node.kind) {
Ok(TypeKind::Alias { aliased }) => match aliased.resolved() {
Some(next) => resolved = next,
None => return false,
},
Ok(TypeKind::Enumeration { enumerators }) => {
let mut values = Vec::new();
for enumerator in enumerators {
match ast.effective_enumerator_value(*enumerator) {
Some(value) => values.push(value),
None => return false,
}
}
return !values.is_empty() && values.iter().all(|value| covered.contains(value));
}
_ => return false,
}
}
false
}
fn member_field(
ast: &Ast,
applied: &Applied,
member: Id<zerodds_ast::MemberNode>,
position: usize,
at: &str,
) -> Result<FieldSpec, MaterialiseError> {
let Ok(node) = ast.member(member) else {
return Err(MaterialiseError::Unevaluated {
construct: "member the model listed".to_string(),
at: at.to_string(),
});
};
let Some(target) = node.type_.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "member type".to_string(),
at: node.name.clone(),
});
};
let xtypes = node.facets.xtypes.as_ref();
let optional = node.facets.is_optional();
let access = member_access(&node.name);
Ok(FieldSpec {
name: node.name.clone(),
id: node
.facets
.effective_member_id()
.unwrap_or_else(|| u32::try_from(position).unwrap_or(u32::MAX)),
type_ref: type_ref_expr(ast, applied, target, &node.name, 0)?,
key: node.facets.is_key(),
optional,
must_understand: node.facets.is_must_understand(),
default: match xtypes.and_then(|x| x.default_value) {
Some(expr) => Some(annotation_value(ast, Some(target), expr, &node.name)?),
None => None,
},
unit: xtypes.and_then(|x| x.unit.clone()),
min: match xtypes.and_then(|x| x.range.as_ref()).and_then(|r| r.min) {
Some(expr) => Some(annotation_value(ast, Some(target), expr, &node.name)?),
None => None,
},
max: match xtypes.and_then(|x| x.range.as_ref()).and_then(|r| r.max) {
Some(expr) => Some(annotation_value(ast, Some(target), expr, &node.name)?),
None => None,
},
hashid: xtypes
.and_then(|x| x.member_id.as_ref())
.and_then(|assignment| match &assignment.origin {
zerodds_ast::MemberIdOrigin::Hashed { source_name } => Some(source_name.clone()),
_ => None,
}),
labels: None,
key_equality_hazard: map_key_hazard(ast, target),
base: false,
check: if optional {
None
} else {
guard_check(ast, applied, target, &access)?
},
})
}
fn parent_field(
ast: &Ast,
applied: &Applied,
base_type: Id<TypeNode>,
at: &str,
) -> Result<FieldSpec, MaterialiseError> {
Ok(FieldSpec {
name: "parent".to_string(),
id: PARENT_ID,
type_ref: type_ref_expr(ast, applied, base_type, at, 0)?,
key: false,
optional: false,
must_understand: false,
default: None,
unit: None,
min: None,
max: None,
hashid: None,
labels: None,
key_equality_hazard: false,
base: true,
check: guard_check(ast, applied, base_type, &member_access("parent"))?,
})
}
fn annotation_value(
ast: &Ast,
declared: Option<Id<TypeNode>>,
expr: Id<zerodds_ast::ConstExprNode>,
at: &str,
) -> Result<ts::Expr, MaterialiseError> {
let value = ast
.eval_const(expr)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "annotation value".to_string(),
at: at.to_string(),
})?;
constant_expr(ast, declared, &value, at)
}
fn map_key_hazard(ast: &Ast, target: Id<TypeNode>) -> bool {
let mut resolved = target;
for _ in 0..MAX_TYPE_DEPTH {
match ast.type_(resolved).map(|node| &node.kind) {
Ok(TypeKind::Alias { aliased }) => match aliased.resolved() {
Some(next) => resolved = next,
None => return false,
},
Ok(TypeKind::Map { key, .. }) => {
let Some(key) = key.resolved() else {
return false;
};
return !compares_by_value(ast, key, 0);
}
_ => return false,
}
}
false
}
fn compares_by_value(ast: &Ast, id: Id<TypeNode>, depth: usize) -> bool {
if depth > MAX_TYPE_DEPTH {
return false;
}
match ast.type_(id).map(|node| &node.kind) {
Ok(TypeKind::Primitive(PrimitiveKind::Float128)) => false,
Ok(TypeKind::Primitive(_) | TypeKind::String { .. } | TypeKind::Fixed { .. }) => true,
Ok(TypeKind::Enumeration { .. } | TypeKind::Bitmask { .. }) => true,
Ok(TypeKind::Alias { aliased }) => aliased
.resolved()
.is_some_and(|target| compares_by_value(ast, target, depth + 1)),
_ => false,
}
}
fn type_ref_expr(
ast: &Ast,
applied: &Applied,
id: Id<TypeNode>,
at: &str,
depth: usize,
) -> Result<ts::Expr, MaterialiseError> {
if depth > MAX_TYPE_DEPTH {
return Err(MaterialiseError::Unevaluated {
construct: format!("type reference, which nests past {MAX_TYPE_DEPTH}"),
at: at.to_string(),
});
}
let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
construct: "type".to_string(),
at: at.to_string(),
})?;
Ok(match &node.kind {
TypeKind::Primitive(kind) => primitive_ref(primitive_ref_name(*kind)),
TypeKind::Fixed { .. } => ts::Expr::Object(vec![
(
ts::PropertyName::ident("kind"),
ts::Expr::Literal(Literal::string("string")),
),
(
ts::PropertyName::ident("wide"),
ts::Expr::Literal(Literal::Bool(false)),
),
]),
TypeKind::String { width, bound } => {
let mut entries = vec![(
ts::PropertyName::ident("kind"),
ts::Expr::Literal(Literal::string("string")),
)];
if let Some(value) = evaluated_bound(ast, *bound, at)? {
entries.push((
ts::PropertyName::ident("bound"),
ts::Expr::Literal(Literal::int(value)),
));
}
entries.push((
ts::PropertyName::ident("wide"),
ts::Expr::Literal(Literal::Bool(matches!(width, CharWidth::WChar))),
));
ts::Expr::Object(entries)
}
TypeKind::Sequence { element, bound } => {
let Some(element) = element.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "sequence element".to_string(),
at: at.to_string(),
});
};
let mut entries = vec![
(
ts::PropertyName::ident("kind"),
ts::Expr::Literal(Literal::string("sequence")),
),
(
ts::PropertyName::ident("element"),
type_ref_expr(ast, applied, element, at, depth + 1)?,
),
];
if let Some(value) = evaluated_bound(ast, *bound, at)? {
entries.push((
ts::PropertyName::ident("bound"),
ts::Expr::Literal(Literal::int(value)),
));
}
ts::Expr::Object(entries)
}
TypeKind::Array { element, dims } => {
let Some(element) = element.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "array element".to_string(),
at: at.to_string(),
});
};
let mut lengths = Vec::new();
for dim in dims {
let value = ast
.eval_integral(*dim)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "array length".to_string(),
at: at.to_string(),
})?;
lengths.push(ts::Expr::Literal(Literal::int(value)));
}
ts::Expr::Object(vec![
(
ts::PropertyName::ident("kind"),
ts::Expr::Literal(Literal::string("array")),
),
(
ts::PropertyName::ident("element"),
type_ref_expr(ast, applied, element, at, depth + 1)?,
),
(ts::PropertyName::ident("lengths"), ts::Expr::Array(lengths)),
])
}
TypeKind::Map { key, value, bound } => {
let (Some(key), Some(value)) = (key.resolved(), value.resolved()) else {
return Err(MaterialiseError::Unevaluated {
construct: "map key or value".to_string(),
at: at.to_string(),
});
};
let mut entries = vec![
(
ts::PropertyName::ident("kind"),
ts::Expr::Literal(Literal::string("map")),
),
(
ts::PropertyName::ident("key"),
type_ref_expr(ast, applied, key, at, depth + 1)?,
),
(
ts::PropertyName::ident("value"),
type_ref_expr(ast, applied, value, at, depth + 1)?,
),
];
if let Some(number) = evaluated_bound(ast, *bound, at)? {
entries.push((
ts::PropertyName::ident("bound"),
ts::Expr::Literal(Literal::int(number)),
));
}
ts::Expr::Object(entries)
}
TypeKind::Alias { aliased } => {
let Some(target) = aliased.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "typedef target".to_string(),
at: at.to_string(),
});
};
if emits_alias(ast, applied, id) {
named_ref(&qualified_idl_name(ast, node.module, &node.name)?)
} else {
type_ref_expr(ast, applied, target, at, depth + 1)?
}
}
TypeKind::Structure { .. }
| TypeKind::Union { .. }
| TypeKind::Bitset { .. }
| TypeKind::Valuetype { .. }
| TypeKind::Enumeration { .. }
| TypeKind::Bitmask { .. }
| TypeKind::Native => named_ref(&qualified_idl_name(ast, node.module, &node.name)?),
TypeKind::ValueBox { boxed } => {
let Some(target) = boxed.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "value box target".to_string(),
at: at.to_string(),
});
};
type_ref_expr(ast, applied, target, at, depth + 1)?
}
TypeKind::Any => ts::Expr::Object(vec![(
ts::PropertyName::ident("kind"),
ts::Expr::Literal(Literal::string("any")),
)]),
TypeKind::ValueBase | TypeKind::ObjectRef { .. } | TypeKind::AnnotationType { .. } => {
return Err(MaterialiseError::NoLanguageForm {
construct: node.kind.as_str().to_string(),
language: "TypeScript",
at: at.to_string(),
});
}
})
}
fn evaluated_bound(
ast: &Ast,
bound: zerodds_ast::Bound,
at: &str,
) -> Result<Option<i128>, MaterialiseError> {
let Some(expr) = bound.expr() else {
return Ok(None);
};
ast.eval_integral(expr)
.map(Some)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "collection bound".to_string(),
at: at.to_string(),
})
}
fn primitive_ref(name: &str) -> ts::Expr {
ts::Expr::Object(vec![
(
ts::PropertyName::ident("kind"),
ts::Expr::Literal(Literal::string("primitive")),
),
(
ts::PropertyName::ident("name"),
ts::Expr::Literal(Literal::string(name)),
),
])
}
fn bitfield_ref(width: u16) -> ts::Expr {
ts::Expr::Object(vec![
(
ts::PropertyName::ident("kind"),
ts::Expr::Literal(Literal::string("bitfield")),
),
(
ts::PropertyName::ident("width"),
ts::Expr::Literal(Literal::int(i128::from(width))),
),
])
}
fn named_ref(qualified: &str) -> ts::Expr {
ts::Expr::Object(vec![
(
ts::PropertyName::ident("kind"),
ts::Expr::Literal(Literal::string("ref")),
),
(
ts::PropertyName::ident("name"),
ts::Expr::Literal(Literal::string(qualified)),
),
])
}
const fn primitive_ref_name(kind: PrimitiveKind) -> &'static str {
match kind {
PrimitiveKind::Boolean => "boolean",
PrimitiveKind::Octet | PrimitiveKind::UInt8 => "octet",
PrimitiveKind::Int8 => "int8",
PrimitiveKind::Int16 => "int16",
PrimitiveKind::UInt16 => "uint16",
PrimitiveKind::Int32 => "int32",
PrimitiveKind::UInt32 => "uint32",
PrimitiveKind::Int64 => "int64",
PrimitiveKind::UInt64 => "uint64",
PrimitiveKind::Float32 => "float",
PrimitiveKind::Float64 => "double",
PrimitiveKind::Float128 => "longDouble",
PrimitiveKind::Char => "char",
PrimitiveKind::WChar => "wchar",
}
}
fn qualified_idl_name(
ast: &Ast,
module: Id<ModuleNode>,
name: &str,
) -> Result<String, MaterialiseError> {
let mut segments: Vec<String> = Vec::new();
for (_, _, idl) in module_chain(ast, module)? {
segments.push(idl);
}
segments.push(name.to_string());
Ok(segments.join("::"))
}
fn guard_name(idl_name: &str) -> String {
format!("is{idl_name}")
}
const fn extensibility_word(kind: zerodds_ast::Extensibility) -> &'static str {
match kind {
zerodds_ast::Extensibility::Final => "final",
zerodds_ast::Extensibility::Appendable => "appendable",
zerodds_ast::Extensibility::Mutable => "mutable",
}
}
fn is_nested(facets: &zerodds_ast::Facets) -> bool {
facets.xtypes.as_ref().is_some_and(|x| x.is_nested)
}
fn topic_of(facets: &zerodds_ast::Facets, type_name: &str) -> Option<String> {
let annotation = facets.xtypes.as_ref()?.topic.as_ref()?;
Some(
annotation
.name
.clone()
.unwrap_or_else(|| type_name.to_string()),
)
}
fn autoid_word(facets: &zerodds_ast::Facets) -> Option<&'static str> {
match facets.xtypes.as_ref()?.autoid? {
zerodds_ast::AutoId::Sequential => Some("sequential"),
zerodds_ast::AutoId::Hash => Some("hash"),
}
}
fn member_access(name: &str) -> ts::Expr {
ts::Expr::Index {
object: Box::new(ts::Expr::Paren(Box::new(ts::Expr::As {
value: Box::new(ts::Expr::Path(Path::single("v"))),
type_: Box::new(ts::Type::generic(
Path::single("Record"),
vec![ts::Type::string(), ts::Type::Keyword(ts::Keyword::Unknown)],
)),
}))),
index: Box::new(ts::Expr::Literal(Literal::string(name))),
}
}
fn typeof_is(access: &ts::Expr, name: &str) -> ts::Expr {
ts::Expr::Binary {
op: "===",
lhs: Box::new(ts::Expr::Unary {
op: "typeof ",
value: Box::new(access.clone()),
}),
rhs: Box::new(ts::Expr::Literal(Literal::string(name))),
}
}
fn guard_check(
ast: &Ast,
applied: &Applied,
target: Id<TypeNode>,
access: &ts::Expr,
) -> Result<Option<ts::Expr>, MaterialiseError> {
guard_check_at(ast, applied, target, access, 0)
}
fn guard_check_at(
ast: &Ast,
applied: &Applied,
target: Id<TypeNode>,
access: &ts::Expr,
depth: usize,
) -> Result<Option<ts::Expr>, MaterialiseError> {
if depth > MAX_TYPE_DEPTH {
return Ok(None);
}
let node = ast
.type_(target)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "type".to_string(),
at: "a member's type".to_string(),
})?;
Ok(match &node.kind {
TypeKind::Primitive(PrimitiveKind::Float128) | TypeKind::Any => Some(object_check(access)),
TypeKind::Primitive(kind) => Some(typeof_is(
access,
match ts_primitive(*kind) {
ts::Keyword::Boolean => "boolean",
ts::Keyword::BigInt => "bigint",
ts::Keyword::String => "string",
_ => "number",
},
)),
TypeKind::String { .. } | TypeKind::Fixed { .. } => Some(typeof_is(access, "string")),
TypeKind::Sequence { .. } | TypeKind::Array { .. } => Some(ts::Expr::Call {
callee: Box::new(ts::Expr::Path(Path::new(&["Array", "isArray"]))),
args: vec![access.clone()],
}),
TypeKind::Map { .. } => Some(ts::Expr::Binary {
op: "instanceof",
lhs: Box::new(access.clone()),
rhs: Box::new(ts::Expr::Path(Path::single("Map"))),
}),
TypeKind::Alias { aliased } => {
let Some(inner) = aliased.resolved() else {
return Ok(None);
};
if emits_alias(ast, applied, target) {
Some(guard_call(ast, target, access)?)
} else {
guard_check_at(ast, applied, inner, access, depth + 1)?
}
}
TypeKind::Structure { .. }
| TypeKind::Valuetype { .. }
| TypeKind::Union { .. }
| TypeKind::Bitset { .. }
| TypeKind::Enumeration { .. }
| TypeKind::Bitmask { .. } => {
if describes(ast, applied, target) {
Some(guard_call(ast, target, access)?)
} else {
None
}
}
_ => None,
})
}
fn guard_call(
ast: &Ast,
target: Id<TypeNode>,
access: &ts::Expr,
) -> Result<ts::Expr, MaterialiseError> {
let node = ast
.type_(target)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "type".to_string(),
at: "a member's type".to_string(),
})?;
let mut segments: Vec<String> = Vec::new();
for (_, name, _) in module_chain(ast, node.module)? {
segments.push(name);
}
segments.push(guard_name(&node.name));
Ok(ts::Expr::Call {
callee: Box::new(ts::Expr::Path(Path::new(
&segments.iter().map(String::as_str).collect::<Vec<&str>>(),
))),
args: vec![access.clone()],
})
}
fn describes(ast: &Ast, applied: &Applied, id: Id<TypeNode>) -> bool {
let Ok(node) = ast.type_(id) else {
return false;
};
let created = match &node.kind {
TypeKind::Structure { .. } => "TsStruct",
TypeKind::Valuetype { .. } => "TsValue",
TypeKind::Union { .. } => "TsUnion",
TypeKind::Enumeration { .. } => "TsEnum",
TypeKind::Bitmask { .. } => "TsBitmask",
TypeKind::Bitset { .. } => "TsBitset",
TypeKind::Alias { .. } => return emits_alias(ast, applied, id),
_ => return false,
};
ghost_created_for(applied, node.node_id, created).is_some()
}
fn object_check(access: &ts::Expr) -> ts::Expr {
ts::Expr::Binary {
op: "&&",
lhs: Box::new(typeof_is(access, "object")),
rhs: Box::new(ts::Expr::Binary {
op: "!==",
lhs: Box::new(access.clone()),
rhs: Box::new(ts::Expr::Literal(Literal::Null)),
}),
}
}
fn push_descriptor(items: &mut Vec<Pending>, module: Id<ModuleNode>, spec: &TypeSpec) {
let guard = ts::Decl::Function(ts::Function {
doc: Doc::lines(&[
&format!(
"Whether a value is a `{}`. DDS-TS 1.0 ยง7.4.1.",
spec.qualified
),
"",
"The presence and the type of every required member. An @optional",
"member is not required, and `@unit`, `@min` and `@max` are not",
"checked: they are documentation, and a guard that enforced them",
"would refuse a sample the wire accepted.",
]),
exported: true,
name: Ident::new(&spec.guard),
generics: Vec::new(),
params: vec![ts::Param {
name: Ident::new("v"),
optional: false,
rest: false,
type_: Some(ts::Type::Keyword(ts::Keyword::Unknown)),
default: None,
}],
returns: Some(ts::Type::Predicate {
param: Ident::new("v"),
type_: Box::new(ts::Type::named(Path::single(&spec.local))),
}),
body: Some(ts::Block {
statements: vec![ts::Statement::Return(Some(guard_expr(spec)))],
}),
});
let descriptor = ts::Decl::Variable(ts::Variable {
doc: Doc::lines(&[
&format!("The side-table of `{}`. DDS-TS 1.0 ยง7.4.1.", spec.qualified),
"",
"A TypeScript interface is erased, so what the IDL annotated it with",
"has nowhere to live in the emitted type. It lives here, as a value,",
"and `registerType` below makes it findable by the qualified name.",
]),
exported: true,
declare: false,
kind: ts::VarKind::Const,
name: Ident::new(&descriptor_name(&spec.local)),
type_: Some(ts::Type::generic(
Path::single("DdsTypeDescriptor"),
vec![ts::Type::named(Path::single(&spec.local))],
)),
value: Some(descriptor_object(spec)),
});
let registration = ts::Decl::Statement(ts::Statement::Expr(ts::Expr::Call {
callee: Box::new(ts::Expr::Path(Path::single("registerType"))),
args: vec![ts::Expr::Path(Path::single(&descriptor_name(&spec.local)))],
}));
for decl in [guard, descriptor, registration] {
items.push(Pending { module, decl });
}
}
fn descriptor_name(local: &str) -> String {
format!("{local}Type")
}
fn guard_expr(spec: &TypeSpec) -> ts::Expr {
match &spec.guard_shape {
GuardShape::TypeOf(name) => typeof_is(&ts::Expr::Path(Path::single("v")), name),
GuardShape::Delegate(check) => (**check).clone(),
GuardShape::Object => {
let mut terms = vec![object_check(&ts::Expr::Path(Path::single("v")))];
for field in &spec.fields {
if let Some(check) = &field.check {
terms.push(check.clone());
}
}
and_all(terms)
}
}
}
fn and_all(terms: Vec<ts::Expr>) -> ts::Expr {
let mut iter = terms.into_iter();
let Some(first) = iter.next() else {
return ts::Expr::Literal(Literal::Bool(true));
};
iter.fold(first, |lhs, rhs| ts::Expr::Binary {
op: "&&",
lhs: Box::new(lhs),
rhs: Box::new(rhs),
})
}
fn descriptor_object(spec: &TypeSpec) -> ts::Expr {
let mut entries = vec![
(
ts::PropertyName::ident("kind"),
ts::Expr::Literal(Literal::string(spec.kind)),
),
(
ts::PropertyName::ident("name"),
ts::Expr::Literal(Literal::string(&spec.qualified)),
),
(
ts::PropertyName::ident("extensibility"),
ts::Expr::Literal(Literal::string(spec.extensibility)),
),
(
ts::PropertyName::ident("nested"),
ts::Expr::Literal(Literal::Bool(spec.nested)),
),
];
if let Some(topic) = &spec.topic {
entries.push((
ts::PropertyName::ident("topic"),
ts::Expr::Literal(Literal::string(topic)),
));
}
if let Some(autoid) = spec.autoid {
entries.push((
ts::PropertyName::ident("autoid"),
ts::Expr::Literal(Literal::string(autoid)),
));
}
if let Some(bound) = spec.bit_bound {
entries.push((
ts::PropertyName::ident("bitBound"),
ts::Expr::Literal(Literal::int(i128::from(bound))),
));
}
if let Some(flag) = spec.has_default {
entries.push((
ts::PropertyName::ident("hasDefault"),
ts::Expr::Literal(Literal::Bool(flag)),
));
}
if let Some(flag) = spec.has_implicit_default {
entries.push((
ts::PropertyName::ident("hasImplicitDefault"),
ts::Expr::Literal(Literal::Bool(flag)),
));
}
entries.push((
ts::PropertyName::ident("fields"),
ts::Expr::Array(spec.fields.iter().map(field_object).collect()),
));
entries.push((
ts::PropertyName::ident("typeGuard"),
ts::Expr::Path(Path::single(&spec.guard)),
));
ts::Expr::Object(entries)
}
fn field_object(field: &FieldSpec) -> ts::Expr {
let mut entries = vec![
(
ts::PropertyName::ident("name"),
ts::Expr::Literal(Literal::string(&field.name)),
),
(
ts::PropertyName::ident("id"),
ts::Expr::Literal(Literal::int(i128::from(field.id))),
),
(ts::PropertyName::ident("type"), field.type_ref.clone()),
(
ts::PropertyName::ident("key"),
ts::Expr::Literal(Literal::Bool(field.key)),
),
(
ts::PropertyName::ident("optional"),
ts::Expr::Literal(Literal::Bool(field.optional)),
),
(
ts::PropertyName::ident("mustUnderstand"),
ts::Expr::Literal(Literal::Bool(field.must_understand)),
),
];
if let Some(value) = &field.default {
entries.push((ts::PropertyName::ident("default"), value.clone()));
}
if let Some(unit) = &field.unit {
entries.push((
ts::PropertyName::ident("unit"),
ts::Expr::Literal(Literal::string(unit)),
));
}
if let Some(value) = &field.min {
entries.push((ts::PropertyName::ident("min"), value.clone()));
}
if let Some(value) = &field.max {
entries.push((ts::PropertyName::ident("max"), value.clone()));
}
if let Some(name) = &field.hashid {
entries.push((
ts::PropertyName::ident("hashid"),
ts::Expr::Literal(Literal::string(name)),
));
}
if let Some(labels) = &field.labels {
entries.push((
ts::PropertyName::ident("labels"),
match labels {
Some(list) => ts::Expr::Array(list.clone()),
None => ts::Expr::Undefined,
},
));
}
if field.key_equality_hazard {
entries.push((
ts::PropertyName::ident("keyEqualityHazard"),
ts::Expr::Literal(Literal::Bool(true)),
));
}
if field.base {
entries.push((
ts::PropertyName::ident("base"),
ts::Expr::Literal(Literal::Bool(true)),
));
}
ts::Expr::Object(entries)
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use super::*;
use crate::rule;
use crate::{ModelGraph, apply};
use std::path::{Path as FsPath, PathBuf};
fn render(idl: &str, path: &str) -> Result<String, String> {
let parsed = zerodds_idl_parse::parse(idl);
if parsed.has_errors() {
return Err(format!("parse: {:?}", parsed.diagnostics()));
}
let lowered = zerodds_idl_lower::lower(&parsed.syntax(), path);
if !lowered.diagnostics.is_empty() {
return Err(format!("lower: {:?}", lowered.diagnostics));
}
let mut ast = lowered.ast;
ast.resolve_all_by_name()
.map_err(|error| format!("resolve: {error}"))?;
let applied = apply(&ModelGraph::of(&ast), &rule::typescript::catalogue())
.map_err(|error| format!("apply: {error}"))?;
let mut decls = prelude();
decls.extend(
ts_items_with_descriptors(&ast, &applied)
.map_err(|error| format!("materialise: {error}"))?,
);
Ok(zerodds_lang_render::typescript::file(&ts::File {
doc: Doc::lines(&[
"Generated by zerodds-lang-rules. Do not edit.",
"",
"The TypeScript shape of one IDL file. The wire rules are not here.",
]),
imports: vec![descriptor_import(DEFAULT_RUNTIME_PACKAGE)],
decls,
}))
}
fn declarations(idl: &str) -> String {
match render(idl, "test.idl") {
Ok(text) => text,
Err(why) => panic!("{why}"),
}
}
fn corpus() -> Vec<(String, PathBuf)> {
let root = FsPath::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/interop-corpus");
let mut out = Vec::new();
for entry in std::fs::read_dir(&root).expect("the corpus directory") {
let entry = entry.expect("a corpus entry");
let idl = entry.path().join("type.idl");
if idl.is_file() {
out.push((entry.file_name().to_string_lossy().into_owned(), idl));
}
}
out.sort();
out
}
#[test]
fn every_corpus_case_renders_or_names_its_failure() {
let cases = corpus();
assert!(
cases.len() > 300,
"the corpus is not where it was: {} cases",
cases.len()
);
let out = std::env::var("ZD_TS_OUT").ok();
let mut failures = Vec::new();
for (name, path) in cases {
let source = std::fs::read_to_string(&path).expect("a corpus source");
match render(&source, &path.display().to_string()) {
Ok(text) => {
if let Some(dir) = &out {
let case = FsPath::new(dir).join(&name);
std::fs::create_dir_all(&case).expect("the output directory");
std::fs::write(case.join("types.ts"), text).expect("the output file");
}
}
Err(why) => failures.push(format!("{name}: {why}")),
}
}
let failures = crate::adapter::without_the_front_end_refusals(failures);
assert!(failures.is_empty(), "{failures:#?}");
}
#[test]
fn a_sixty_four_bit_integer_is_a_bigint_and_everything_narrower_is_a_number() {
let generated = declarations(
"@final struct P {
boolean a; octet b; char c; wchar d;
int8 e; uint8 f; short g; unsigned short h;
long i; unsigned long j; long long k; unsigned long long l;
float m; double n; long double o;
};",
);
for expected in [
"a: boolean;",
"b: number;",
"c: string;",
"d: string;",
"e: number;",
"f: number;",
"g: number;",
"h: number;",
"i: number;",
"j: number;",
"k: bigint;",
"l: bigint;",
"m: number;",
"n: number;",
"o: ZdFloat128;",
] {
assert!(generated.contains(expected), "{expected}\n{generated}");
}
}
#[test]
fn a_bigint_field_starts_from_a_bigint_literal() {
let generated = declarations("@final struct P { long long k; long i; };");
assert!(generated.contains("k: 0n"), "{generated}");
assert!(generated.contains("i: 0"), "{generated}");
}
#[test]
fn a_struct_is_an_interface_and_a_factory_and_never_a_class() {
let generated = declarations(
"@final struct P { long x; }; \
bitset Packed { bitfield<3> low; }; \
enum Colour { RED }; \
@final union U switch (long) { case 1: long a; }; \
exception E { long code; }; \
typedef long Counter;",
);
assert!(generated.contains("export interface P {"), "{generated}");
assert!(
generated.contains("export function newP(): P {"),
"{generated}"
);
assert!(generated.contains("return { x: 0 };"), "{generated}");
assert!(
!without_comments(&generated).contains("class"),
"a class reached the output for an IDL data type:\n{generated}"
);
}
fn without_comments(text: &str) -> String {
let mut out = String::new();
let mut rest = text;
while let Some(open) = rest.find("/*") {
out.push_str(&rest[..open]);
match rest[open..].find("*/") {
Some(close) => rest = &rest[open + close + 2..],
None => return out,
}
}
out.push_str(rest);
out.lines()
.filter(|line| !line.trim_start().starts_with("//"))
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn a_sequence_and_an_array_are_both_typescript_arrays() {
let generated = declarations(
"@final struct P { sequence<long> s; long a[3]; long g[2][3]; sequence<string> t; };",
);
assert!(generated.contains("s: number[];"), "{generated}");
assert!(generated.contains("a: number[];"), "{generated}");
assert!(generated.contains("g: number[][];"), "{generated}");
assert!(generated.contains("t: string[];"), "{generated}");
assert!(generated.contains("s: []"), "{generated}");
assert!(
generated.contains("a: Array.from({ length: 3 }, () => 0)"),
"{generated}"
);
}
#[test]
fn an_array_of_unions_brackets_the_object_its_arrow_returns() {
let generated = declarations(
"union Choice switch (long) { case 1: long whole; default: double fraction; };
@final struct Sample { Choice items[2]; };",
);
assert!(
generated
.contains("Array.from({ length: 2 }, () => ({ discriminator: 0, fraction: 0 }))"),
"{generated}"
);
}
#[test]
fn a_map_member_is_a_readonly_map_over_a_staged_entry_type() {
let generated = declarations("@final struct P { map<long, string> m; };");
assert!(
generated.contains("m: ReadonlyMap<number, string>;"),
"{generated}"
);
assert!(!generated.contains("Record<number"), "{generated}");
assert!(
generated.contains("export interface ZdEntry_long_string {"),
"{generated}"
);
assert!(generated.contains("key: number;"), "{generated}");
assert!(generated.contains("value: string;"), "{generated}");
}
#[test]
fn two_maps_of_one_typescript_shape_and_two_idl_shapes_are_two_entry_types() {
let generated =
declarations("@final struct P { map<octet, long> a; map<unsigned short, long> b; };");
assert!(
generated.contains("interface ZdEntry_octet_long {"),
"{generated}"
);
assert!(
generated.contains("interface ZdEntry_unsigned_short_long {"),
"{generated}"
);
}
#[test]
fn two_maps_of_one_shape_are_one_entry_type() {
let generated = declarations("@final struct P { map<long, long> a; map<long, long> b; };");
assert_eq!(
generated.matches("interface ZdEntry_long_long {").count(),
1
);
}
#[test]
fn an_optional_member_is_a_question_mark_and_not_a_null() {
let generated =
declarations("@mutable struct P { @optional long a; @optional string b; long c; };");
assert!(generated.contains("a?: number | undefined;"), "{generated}");
assert!(generated.contains("b?: string | undefined;"), "{generated}");
assert!(!generated.contains("| null"), "{generated}");
assert!(generated.contains("return { c: 0 };"), "{generated}");
}
#[test]
fn a_union_is_a_discriminated_union_narrowed_on_a_literal() {
let generated = declarations(
"union Choice switch (long) {
case 1: long whole;
case 2: long other;
default: double fraction;
};
@final struct P { Choice c; };",
);
assert!(
generated.contains("export type Choice = { discriminator: 1; whole: number }"),
"{generated}"
);
assert!(
generated.contains("{ discriminator: 2; other: number }"),
"{generated}"
);
assert!(
generated.contains("{ discriminator: Exclude<number, 1 | 2>; fraction: number }"),
"{generated}"
);
assert!(generated.contains("c: Choice;"), "{generated}");
}
#[test]
fn a_union_discriminant_takes_the_discriminators_own_literal_form() {
let wide = declarations(
"union W switch (long long) { case 2: long a; };
@final struct P { W picked; };",
);
assert!(wide.contains("{ discriminator: 2n; a: number }"), "{wide}");
let flag = declarations(
"union B switch (boolean) { case TRUE: long a; };
@final struct P { B picked; };",
);
assert!(
flag.contains("{ discriminator: true; a: number }"),
"{flag}"
);
}
#[test]
fn an_enumeration_is_an_as_const_object_beside_a_string_literal_union() {
let generated = declarations(
"enum Colour { RED, GREEN, BLUE };
@final struct P { Colour c; };",
);
assert!(
generated.contains(
"export const Colour = { RED: \"RED\", GREEN: \"GREEN\", BLUE: \"BLUE\" } as const;"
),
"{generated}"
);
assert!(
generated.contains("export type Colour = (typeof Colour)[keyof typeof Colour];"),
"{generated}"
);
assert!(
generated.contains(
"export const ColourOrdinal: Readonly<Record<Colour, number>> = \
{ RED: 0, GREEN: 1, BLUE: 2 } as const;"
),
"{generated}"
);
assert!(
generated.contains("export const ColourFromOrdinal: ReadonlyMap<number, Colour>"),
"{generated}"
);
assert!(generated.contains("c: Colour;"), "{generated}");
assert!(!generated.contains("export enum "), "{generated}");
}
#[test]
fn an_explicit_enumerator_value_is_written_out() {
let generated = declarations("enum E { @value(-1) LOW, @value(70000) HIGH };");
assert!(generated.contains("LOW: -1"), "{generated}");
assert!(generated.contains("HIGH: 70000"), "{generated}");
}
#[test]
fn a_bitmask_writes_its_bit_as_the_number_and_not_as_a_shift() {
let narrow =
declarations("@bit_bound(32) bitmask Small { @position(0) A, @position(31) B };");
assert!(
narrow.contains("export const Small = { A: 1, B: 2147483648 } as const;"),
"{narrow}"
);
assert!(narrow.contains("export type Small = number;"), "{narrow}");
let wide = declarations("@bit_bound(64) bitmask Big { @position(0) A, @position(40) B };");
assert!(wide.contains("B: 1099511627776n"), "{wide}");
assert!(wide.contains("export type Big = bigint;"), "{wide}");
}
#[test]
fn a_bitset_is_one_field_per_bitfield() {
let generated = declarations(
"bitset Packed { bitfield<3> low; bitfield<5> high; };
@final struct P { Packed value; };",
);
assert!(generated.contains("low: number;"), "{generated}");
assert!(generated.contains("high: number;"), "{generated}");
assert!(generated.contains("value: Packed;"), "{generated}");
assert!(generated.contains("value: newPacked()"), "{generated}");
}
#[test]
fn a_fixed_is_a_string_and_not_a_number() {
let generated = declarations(
"typedef fixed<9,3> Money;
@final struct P { Money amount; };",
);
assert!(
generated.contains("export type Money = string;"),
"{generated}"
);
assert!(generated.contains("amount: Money;"), "{generated}");
}
#[test]
fn a_typedef_is_a_type_alias_and_not_a_comment() {
let generated = declarations(
"typedef sequence<long> Row;
@final struct P { Row r; };",
);
assert!(
generated.contains("export type Row = number[];"),
"{generated}"
);
assert!(generated.contains("r: Row;"), "{generated}");
}
#[test]
fn a_module_is_a_namespace_and_two_modules_do_not_collide() {
let generated = declarations(
"module a { @final struct P { long x; }; };
module b { @final struct P { double y; }; };
@final struct Q { a::P one; b::P two; };",
);
assert!(generated.contains("export namespace a {"), "{generated}");
assert!(generated.contains("export namespace b {"), "{generated}");
assert!(generated.contains("one: a.P;"), "{generated}");
assert!(generated.contains("two: b.P;"), "{generated}");
}
#[test]
fn a_module_reopened_beside_a_nested_one_is_one_namespace() {
let generated = declarations(
"module common {
module util { @final struct Location { double x; }; };
@final struct Robot { ::common::util::Location where_; };
};",
);
assert_eq!(
generated.matches("export namespace common {").count(),
1,
"{generated}"
);
assert!(
generated.contains("where_: common.util.Location;"),
"{generated}"
);
}
#[test]
fn a_binding_that_is_a_reserved_word_is_escaped_and_a_member_is_not() {
let generated = declarations(
"module package {
enum class { template, virtual, friend };
@final struct type { long new; double delete; short operator; };
};",
);
assert!(
generated.contains("export namespace package_ {"),
"{generated}"
);
assert!(generated.contains("export const class_ ="), "{generated}");
assert!(generated.contains("export interface type {"), "{generated}");
assert!(generated.contains("new: number;"), "{generated}");
assert!(generated.contains("delete: number;"), "{generated}");
assert!(generated.contains("operator: number;"), "{generated}");
assert!(generated.contains("IDL `package`"), "{generated}");
}
#[test]
fn a_construct_with_no_typescript_data_form_is_named_and_not_left_out() {
let failure = render("@final struct P { Object o; long tail; };", "test.idl")
.expect_err("an object reference");
assert!(failure.contains("Object"), "{failure}");
assert!(failure.contains("TypeScript"), "{failure}");
}
#[test]
fn an_any_member_takes_the_box_and_not_a_typescript_keyword() {
let generated = declarations("@final struct P { any a; long tail; };");
assert!(generated.contains("a: DdsAny;"), "{generated}");
assert!(!generated.contains("a: any;"), "{generated}");
assert!(!generated.contains("a: unknown;"), "{generated}");
assert!(generated.contains("kind: \"any\""), "{generated}");
}
#[test]
fn a_nested_name_that_would_capture_a_root_reference_is_refused() {
let failure = render(
"module c { @final struct Q { long x; }; };
module a {
module c { @final struct R { long y; }; };
@final struct P { ::c::Q q; };
};",
"test.idl",
)
.expect_err("a nested namespace shadowing a top-level one is refused");
assert!(failure.contains('c'), "{failure}");
}
#[test]
fn a_declaration_named_after_the_prelude_type_is_refused() {
let failure = render("@final struct ZdFloat128 { long x; };", "test.idl")
.expect_err("a type named after the prelude declaration is refused");
assert!(failure.contains("ZdFloat128"), "{failure}");
}
#[test]
fn an_unevaluated_array_length_is_named_and_not_substituted() {
let failure = render("@final struct P { long a[UNKNOWN]; };", "test.idl")
.expect_err("an array whose length does not evaluate is refused");
assert!(!failure.is_empty(), "{failure}");
}
#[test]
fn a_constant_past_the_safe_integer_range_is_refused_rather_than_rounded() {
let ok = declarations("const long long BIG = 9007199254740993;");
assert!(ok.contains("9007199254740993n"), "{ok}");
}
#[test]
fn a_string_constant_reaches_the_output() {
let generated = declarations("const string MODULE_ID = \"spatial/1.5\";");
assert!(
generated.contains("export const MODULE_ID: string = \"spatial/1.5\";"),
"{generated}"
);
}
}