use crate::adapter::cst::{MaterialiseError, no_spelling_collision};
use crate::verbatim;
use zerodds_ast::{Ast, TypeKind, TypeNode};
use zerodds_ast_core::Id;
use zerodds_lang_cst::common::{Doc, Ident, Literal};
use zerodds_lang_cst::node::{self, Expr, Stat};
const LANGUAGE: &str = "JavaScript";
pub const MAP_ENTRY: &str = "_zd_map_entry";
const MAX_MODULE_DEPTH: usize = 64;
pub fn node_items(ast: &Ast) -> Result<Vec<Stat>, MaterialiseError> {
let mut out = Vec::new();
let mut emitted: Vec<(String, String)> = Vec::new();
for (id, _) in ast.types() {
let start = out.len();
declare(ast, id, &mut out, &mut emitted)?;
verbatim::weave(
verbatim::declaration_slots(ast, id, verbatim::aliases::NODE),
"",
start,
&mut out,
);
}
for (_, node) in ast.constants() {
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 is_wide = declared.is_some_and(|target| is_wide_integer(ast, target));
let name = declared_name(ast, &node.name, node.module);
remember(&mut emitted, &name, &node.name)?;
out.push(Stat::Declare {
doc: idl_note(&name, &node.name),
kind: node::VarKind::Const,
name: Ident::new(&name),
value: Some(node_constant(&value, is_wide, &node.name)?),
});
}
Ok(verbatim::weave_file(ast, verbatim::aliases::NODE, out))
}
impl verbatim::Carrier for Stat {
fn verbatim(text: String) -> Self {
Self::Verbatim(text)
}
fn take_into_body(&mut self, _: &str, _: &mut Vec<String>, _: &mut Vec<String>) -> bool {
false
}
}
pub fn node_file(ast: &Ast) -> Result<node::File, MaterialiseError> {
let body = node_items(ast)?;
Ok(node::File {
doc: Doc::lines(&[
"Generated by ZeroDDS from an IDL source. Do not edit.",
"",
"The IDL spelling of every renamed name is in the comment above it.",
"Nothing is renamed for style: a rename here is always an escape, of a",
"JavaScript reserved word used as a top-level declaration. A member's",
"ID under @autoid(HASH) is the MD5 of its IDL name (DDS-XTypes 1.3",
"7.3.1.2.1.1) and the escape does not invert, which is why the",
"comment is there.",
"",
"A struct, a union, a bitset, a valuetype, an exception, an",
"interface, a native declaration and a typedef declare nothing at",
"all here: every one of them is a plain object literal wherever the",
"codec below or its Table 9 defaulter builds one.",
]),
body,
})
}
fn declare(
ast: &Ast,
id: Id<TypeNode>,
out: &mut Vec<Stat>,
emitted: &mut Vec<(String, String)>,
) -> Result<(), MaterialiseError> {
let Ok(type_node) = ast.type_(id) else {
return Ok(());
};
match &type_node.kind {
TypeKind::Enumeration { enumerators } => {
let name = node_type_name(ast, id);
remember(emitted, &name, &type_node.name)?;
let mut pairs = Vec::new();
for enumerator in enumerators {
if let Ok(enum_node) = ast.enumerator(*enumerator) {
pairs.push((field_name(&enum_node.name), enum_node.name.clone()));
}
}
no_spelling_collision(as_pairs(&pairs), LANGUAGE, &name)?;
let mut fields = Vec::new();
for enumerator in enumerators {
let Ok(enum_node) = ast.enumerator(*enumerator) else {
return Err(MaterialiseError::NoNode {
kind: "enumerator",
at: crate::adapter::cst::handle_of(*enumerator),
});
};
let value = ast.effective_enumerator_value(*enumerator).ok_or_else(|| {
MaterialiseError::Unevaluated {
construct: "enumerator value".to_string(),
at: enum_node.name.clone(),
}
})?;
fields.push((Ident::new(&field_name(&enum_node.name)), Expr::int(value)));
}
out.push(Stat::Declare {
doc: with_idl_note(
Doc::lines(&[
"An enumeration, as a frozen object of its enumerator names.",
"`Object.freeze` is not an encoding decision — nothing here",
"reads a value off the wire this way — it only refuses an",
"accidental write to a shared constant.",
]),
&name,
&type_node.name,
),
kind: node::VarKind::Const,
name: Ident::new(&name),
value: Some(Expr::Call {
callee: Box::new(Expr::member(Expr::name("Object"), "freeze")),
args: vec![Expr::Object(fields)],
}),
});
}
TypeKind::Bitmask { bits } => {
let bound = ast.effective_bit_bound(id);
let wide = bound > 32;
let prefix = module_prefix(ast, id);
for bit in bits {
let Ok(bit_node) = ast.bit_value(*bit) else {
return Err(MaterialiseError::NoNode {
kind: "bit",
at: crate::adapter::cst::handle_of(*bit),
});
};
let position = ast.effective_bit_position(*bit).ok_or_else(|| {
MaterialiseError::Unevaluated {
construct: "bit position".to_string(),
at: bit_node.name.clone(),
}
})?;
let constant = node::escape_decl(&format!("{prefix}{}", bit_node.name));
remember(emitted, &constant, &bit_node.name)?;
let value = if wide {
Expr::Binary {
op: "<<",
lhs: Box::new(Expr::Literal(Literal::Int("1n".to_string()))),
rhs: Box::new(Expr::Literal(Literal::Int(format!("{position}n")))),
}
} else {
Expr::Binary {
op: "<<",
lhs: Box::new(Expr::int(1)),
rhs: Box::new(Expr::int(i128::from(position))),
}
};
out.push(Stat::Declare {
doc: idl_note(&constant, &bit_node.name),
kind: node::VarKind::Const,
name: Ident::new(&constant),
value: Some(value),
});
}
}
TypeKind::Primitive(_)
| TypeKind::String { .. }
| TypeKind::Fixed { .. }
| TypeKind::Sequence { .. }
| TypeKind::Array { .. }
| TypeKind::Map { .. }
| TypeKind::Alias { .. }
| TypeKind::Bitset { .. }
| TypeKind::Structure { .. }
| TypeKind::Union { .. }
| TypeKind::Native
| TypeKind::ValueBox { .. }
| TypeKind::Valuetype { .. }
| TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. }
| TypeKind::Any
| TypeKind::ValueBase => {}
}
Ok(())
}
fn is_wide_integer(ast: &Ast, id: Id<TypeNode>) -> bool {
matches!(
ast.type_(id).map(|node| node.kind.clone()),
Ok(TypeKind::Primitive(
zerodds_ast::PrimitiveKind::Int64 | zerodds_ast::PrimitiveKind::UInt64
))
)
}
#[must_use]
pub fn node_type_name(ast: &Ast, id: Id<TypeNode>) -> String {
let Ok(any) = ast.any_of_type(id) else {
return "ZdUnnamed".to_string();
};
match ast.scoped_name_of(any) {
Some(scoped) => flatten(scoped.segments()),
None => "ZdUnnamed".to_string(),
}
}
fn module_prefix(ast: &Ast, id: Id<TypeNode>) -> String {
let Ok(any) = ast.any_of_type(id) else {
return String::new();
};
let Some(scoped) = ast.scoped_name_of(any) else {
return String::new();
};
let segments = scoped.segments();
if segments.len() <= 1 {
return String::new();
}
let mut prefix = segments[..segments.len() - 1].join("_");
prefix.push('_');
prefix
}
fn declared_name(ast: &Ast, name: &str, module: Id<zerodds_ast::ModuleNode>) -> String {
let mut segments: Vec<String> = Vec::new();
let mut current = Some(module);
let mut depth = 0;
while let Some(id) = current {
if depth > MAX_MODULE_DEPTH {
break;
}
depth += 1;
let Ok(node) = ast.module(id) else { break };
if node.name.is_empty() {
break;
}
segments.push(node.name.clone());
current = node.parent;
}
segments.reverse();
segments.push(name.to_string());
node::escape_decl(&segments.join("_"))
}
fn flatten(segments: &[String]) -> String {
node::escape_decl(&segments.join("_"))
}
#[must_use]
pub fn field_name(name: &str) -> String {
node::escape_member(name)
}
fn idl_note(spelled: &str, declared: &str) -> Doc {
if spelled == declared {
return Doc::none();
}
Doc::line(&format!("IDL: `{declared}`."))
}
fn with_idl_note(mut doc: Doc, spelled: &str, declared: &str) -> Doc {
if spelled != declared {
doc.push(&format!("IDL: `{declared}`."));
}
doc
}
fn remember(
emitted: &mut Vec<(String, String)>,
spelled: &str,
declared: &str,
) -> Result<(), MaterialiseError> {
if let Some((_, first)) = emitted.iter().find(|(other, _)| other == spelled) {
return Err(MaterialiseError::SpellingCollision {
first: first.clone(),
second: declared.to_string(),
spelled: spelled.to_string(),
language: LANGUAGE,
at: "the generated file".to_string(),
});
}
emitted.push((spelled.to_string(), declared.to_string()));
Ok(())
}
fn as_pairs(pairs: &[(String, String)]) -> Vec<(&str, &str)> {
pairs
.iter()
.map(|(spelled, declared)| (spelled.as_str(), declared.as_str()))
.collect()
}
fn node_constant(
value: &zerodds_ast::ConstValue,
is_wide: bool,
at: &str,
) -> Result<Expr, MaterialiseError> {
use zerodds_ast::ConstValue;
let refuse = |construct: &str| MaterialiseError::NoLanguageForm {
construct: construct.to_string(),
language: LANGUAGE,
at: at.to_string(),
};
Ok(match value {
ConstValue::Boolean(flag) => Expr::bool(*flag),
ConstValue::Integer(number) => {
if is_wide {
Expr::Literal(Literal::Int(format!("{number}n")))
} else {
Expr::int(*number)
}
}
ConstValue::Char(character) => Expr::Literal(Literal::Char(*character)),
ConstValue::WChar(point) => {
Expr::Literal(Literal::Char(char::from_u32(*point).ok_or_else(|| {
refuse("a `wchar` constant that is not a Unicode scalar value")
})?))
}
ConstValue::Float { mantissa, exponent } => Expr::Literal(Literal::Float(
crate::adapter::constant::float_text(mantissa, exponent.as_deref()),
)),
ConstValue::Fixed { digits, scale } => {
Expr::string(&crate::adapter::constant::fixed_decimal(digits, *scale))
}
ConstValue::String(text) => Expr::string(text),
ConstValue::WString(points) => Expr::string(
&crate::adapter::constant::wstring_utf8(points)
.ok_or_else(|| refuse("a `wstring` constant holding a lone surrogate"))?,
),
_ => return Err(refuse("a constant form this binding has no literal for")),
})
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use super::*;
#[test]
fn a_member_name_never_moves() {
for name in ["class", "new", "constructor", "x"] {
assert_eq!(field_name(name), name);
}
}
#[test]
fn a_map_entry_has_no_declared_name() {
assert_eq!(MAP_ENTRY, "_zd_map_entry");
}
}