use crate::adapter::constant;
use crate::adapter::cst::{MaterialiseError, no_spelling_collision};
use crate::adapter::interface;
use crate::apply::Applied;
use crate::verbatim;
use seesaw_tgg::ident::GhostId;
use zerodds_ast::{Ast, PrimitiveKind, TypeKind, TypeNode};
use zerodds_ast_core::{Id, NodeId};
use zerodds_lang_cst::common::{Doc, Ident, Literal};
use zerodds_lang_cst::julia;
const LANGUAGE: &str = "Julia";
pub const DISCRIMINATOR: &str = "_d";
const INVENTED_PREFIX: &str = "ZdMapEntry_";
const FORWARD_PREFIX: &str = "ZdFwd_";
const MAX_DEPTH: usize = 64;
#[allow(clippy::too_many_lines)]
pub fn julia_items(ast: &Ast, applied: &Applied) -> Result<Vec<julia::Decl>, MaterialiseError> {
let mut out = Vec::new();
let mut emitted: Vec<(String, String)> = Vec::new();
let mut ctx = Ctx::over(ordered_types(ast));
for index in 0..ctx.order.len() {
let Some(id) = ctx.order.get(index).copied() else {
continue;
};
ctx.at = Some(index);
let start = out.len();
declare(ast, applied, id, &mut out, &mut emitted, &mut ctx)?;
verbatim::weave(
verbatim::declaration_slots(ast, id, verbatim::aliases::JULIA),
&julia_type_name(ast, id),
start,
&mut out,
);
}
ctx.at = None;
for (_, node) in ast.constants() {
if ghost_created_for(applied, node.node_id, "JuliaConst").is_none() {
continue;
}
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) => value_type(ast, target, &mut ctx)?,
None => julia::Type::Primitive(julia::Primitive::Int32),
};
let name = declared_name(ast, &node.name, node.module);
remember(&mut emitted, &name, &node.name)?;
out.push(julia::Decl::Const(julia::ConstDecl {
doc: idl_note(&name, &node.name),
name: Ident::new(&name),
value: julia_constant(&value, &type_, &node.name)?,
}));
}
for (_, node) in ast.exceptions() {
if ghost_created_for(applied, node.node_id, "JuliaError").is_none() {
continue;
}
let name = declared_name(ast, &node.name, node.module);
remember(&mut emitted, &name, &node.name)?;
let mut pairs = Vec::new();
for member in &node.members {
if let Ok(member_node) = ast.member(*member) {
pairs.push((
julia::escape_field(&member_node.name),
member_node.name.clone(),
));
}
}
no_spelling_collision(as_pairs(&pairs), LANGUAGE, &name)?;
let mut fields = Vec::new();
for member in &node.members {
let Ok(member_node) = ast.member(*member) else {
continue;
};
let Some(target) = member_node.type_.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "exception member type".to_string(),
at: member_node.name.clone(),
});
};
let spelled = julia::escape_field(&member_node.name);
fields.push(julia::Field {
doc: idl_note(&spelled, &member_node.name),
name: Ident::new(&spelled),
type_: member_type(ast, target, &member_node.name, &mut ctx)?,
});
}
out.push(julia::Decl::Struct(julia::StructDecl {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: with_idl_note(
Doc::line("An exception, as the structure its members make."),
&name,
&node.name,
),
name: Ident::new(&name),
supertype: None,
fields,
}));
}
let mut functions: Vec<String> = Vec::new();
for (id, node) in ast.interfaces() {
if ghost_created_for(applied, node.node_id, "JuliaInterface").is_none() {
continue;
}
let name = declared_name(ast, &node.name, node.module);
remember(&mut emitted, &name, &node.name)?;
out.push(julia::Decl::Abstract(julia::AbstractDecl {
doc: with_idl_note(
Doc::lines(&[
"An interface. Julia has no interface construct, so this is an",
"abstract type and a set of functions declared before anything",
"implements them, one per operation and one (or, `readonly`",
"aside, two) per attribute's implied get_/set_ pair; the contract",
"is the comment above each one, including a `raises`/",
"`getraises`/`setraises` clause where the operation has one. The",
"transport is the RPC arc's business; the shape is the IDL's.",
]),
&name,
&node.name,
),
name: Ident::new(&name),
}));
let mut stubs = Vec::new();
let operations = interface::all_operations_flattened(ast, id)
.map_err(|error| interface::interface_error_to_materialise(error, &node.name))?;
for op in &operations {
let spelled = julia::escape_name(&op.name);
let mut signature = spelled.clone();
signature.push('(');
let mut params = Vec::new();
for param in &op.params {
let type_ = member_type(ast, param.type_, ¶m.name, &mut ctx)?;
params.push(format!(
"{}::{}",
julia::escape_name(¶m.name),
zerodds_lang_render::julia::type_(&type_)
));
}
signature.push_str(¶ms.join(", "));
signature.push(')');
let returns = match op.returns {
Some(target) => {
let type_ = member_type(ast, target, &op.name, &mut ctx)?;
zerodds_lang_render::julia::type_(&type_)
}
None => julia::Primitive::Nothing.as_str().to_string(),
};
signature.push_str("::");
signature.push_str(&returns);
let mut doc_line = signature.clone();
if let Some(raises) = op.raises_line() {
doc_line.push_str(" -- ");
doc_line.push_str(&raises);
}
stubs.push((spelled, op.name.clone(), doc_line));
}
let signatures: Vec<(String, String)> = stubs
.iter()
.map(|(spelled, declared, _)| (spelled.clone(), declared.clone()))
.collect();
no_spelling_collision(as_pairs(&signatures), LANGUAGE, &name)?;
for (spelled, declared, signature) in stubs {
if let Some((_, first)) = emitted.iter().find(|(other, _)| *other == spelled) {
return Err(MaterialiseError::SpellingCollision {
first: first.clone(),
second: declared,
spelled,
language: LANGUAGE,
at: "the generated file".to_string(),
});
}
if functions.contains(&spelled) {
continue;
}
functions.push(spelled.clone());
let mut doc = Doc::line(&signature);
if spelled != declared {
doc.push(&format!("IDL: `{declared}`."));
}
out.push(julia::Decl::Function(julia::FunctionDecl {
doc,
name: Ident::new(&spelled),
}));
}
}
resolve_forwards(&mut out, &ctx.forward)?;
Ok(verbatim::weave_file(ast, verbatim::aliases::JULIA, out))
}
impl verbatim::Carrier for julia::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 {
let Self::Struct(node) = self else {
return false;
};
if node.name.as_str() != name {
return false;
}
node.body_text.begin.append(begin);
node.body_text.end.append(end);
true
}
}
pub fn julia_file(ast: &Ast, applied: &Applied) -> Result<julia::File, MaterialiseError> {
Ok(julia::File {
doc: Doc::lines(&[
"Generated by ZeroDDS from an IDL source. Do not edit.",
"",
"Declarations are in dependency order, not source order: a struct's",
"field types are evaluated when the struct is defined, so a name",
"declared later in the file is an UndefVarError.",
"",
"The IDL spelling of every renamed declaration and every renamed",
"member is in the comment above it. Julia has no field annotation a",
"program can read back, and a member's ID under @autoid(HASH) is the",
"MD5 of the IDL name (DDS-XTypes 1.3 7.3.1.2.1.1), so the codec is",
"told that name rather than asked to recover it from the type.",
]),
decls: julia_items(ast, applied)?,
})
}
#[must_use]
pub fn ordered_types(ast: &Ast) -> Vec<Id<TypeNode>> {
let mut order = Vec::new();
let mut seen = Vec::new();
for (id, _) in ast.types() {
visit(ast, id, &mut order, &mut seen, 0);
}
order
}
fn visit(
ast: &Ast,
id: Id<TypeNode>,
order: &mut Vec<Id<TypeNode>>,
seen: &mut Vec<Id<TypeNode>>,
depth: usize,
) {
if depth > MAX_DEPTH || seen.contains(&id) {
return;
}
seen.push(id);
for dependency in dependencies(ast, id) {
visit(ast, dependency, order, seen, depth + 1);
}
order.push(id);
}
fn dependencies(ast: &Ast, id: Id<TypeNode>) -> Vec<Id<TypeNode>> {
let mut out = Vec::new();
let Ok(node) = ast.type_(id) else {
return out;
};
match &node.kind {
TypeKind::Sequence { element, .. } | TypeKind::Array { element, .. } => {
out.extend(element.resolved());
}
TypeKind::Map { key, value, .. } => {
out.extend(key.resolved());
out.extend(value.resolved());
}
TypeKind::Alias { aliased } => out.extend(aliased.resolved()),
TypeKind::ValueBox { boxed } => out.extend(boxed.resolved()),
TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
out.extend(super::c::struct_base(ast, id));
for member in ast.all_members_of(id) {
if let Ok(member_node) = ast.member(member) {
out.extend(member_node.type_.resolved());
}
}
}
TypeKind::Union { discriminator, .. } => {
out.extend(discriminator.resolved());
for case in ast.cases_of(id) {
if let Ok(case_node) = ast.case(*case) {
if let Ok(member) = ast.member(case_node.member) {
out.extend(member.type_.resolved());
}
}
}
}
TypeKind::Primitive(_)
| TypeKind::String { .. }
| TypeKind::Fixed { .. }
| TypeKind::Enumeration { .. }
| TypeKind::Bitmask { .. }
| TypeKind::Bitset { .. }
| TypeKind::Native
| TypeKind::Any
| TypeKind::ValueBase
| TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. } => {}
}
out
}
struct Ctx {
order: Vec<Id<TypeNode>>,
at: Option<usize>,
forward: Vec<String>,
depth: usize,
}
impl Ctx {
fn over(order: Vec<Id<TypeNode>>) -> Self {
Self {
order,
at: None,
forward: Vec::new(),
depth: 0,
}
}
fn unordered() -> Self {
Self::over(Vec::new())
}
fn is_forward(&self, id: Id<TypeNode>) -> bool {
let Some(at) = self.at else { return false };
self.order
.iter()
.position(|other| *other == id)
.is_some_and(|pos| pos > at)
}
fn record_forward(&mut self, name: &str) {
if !self.forward.iter().any(|other| other == name) {
self.forward.push(name.to_string());
}
}
}
fn resolve_forwards(
out: &mut Vec<julia::Decl>,
forward: &[String],
) -> Result<(), MaterialiseError> {
if forward.is_empty() {
return Ok(());
}
let mut heads = Vec::new();
for name in forward {
let abstract_name = format!("{FORWARD_PREFIX}{name}");
let mut found = false;
for decl in out.iter_mut() {
if let julia::Decl::Struct(structure) = decl {
if structure.name.as_str() == name {
structure.supertype = Some(Ident::new(&abstract_name));
found = true;
break;
}
}
}
if !found {
return Err(MaterialiseError::NoLanguageForm {
construct: "a type graph cycle closing through a declaration that is not a \
struct, which Julia cannot forward-declare"
.to_string(),
language: LANGUAGE,
at: name.clone(),
});
}
heads.push(julia::Decl::Abstract(julia::AbstractDecl {
doc: Doc::lines(&[
"Stands in for the declaration below that a member above has to name.",
"A struct's field types are evaluated when the struct is defined, so a",
"cycle through two declarations has no order; an abstract type has no",
"fields and therefore no forward reference of its own.",
]),
name: Ident::new(&abstract_name),
}));
}
heads.append(out);
*out = heads;
Ok(())
}
#[allow(clippy::too_many_lines)]
fn declare(
ast: &Ast,
applied: &Applied,
id: Id<TypeNode>,
out: &mut Vec<julia::Decl>,
emitted: &mut Vec<(String, String)>,
ctx: &mut Ctx,
) -> Result<(), MaterialiseError> {
let Ok(node) = ast.type_(id) else {
return Ok(());
};
match &node.kind {
TypeKind::Primitive(_)
| TypeKind::String { .. }
| TypeKind::Fixed { .. }
| TypeKind::Sequence { .. }
| TypeKind::Array { .. } => {}
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: julia_type_name(ast, id),
});
};
let name = julia_map_entry_name(ast, id);
if emitted.iter().any(|(spelled, _)| *spelled == name) {
return Ok(());
}
emitted.push((name.clone(), name.clone()));
out.push(julia::Decl::Struct(julia::StructDecl {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: Doc::line(
"One entry of a map. DDS-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.",
),
name: Ident::new(&name),
supertype: None,
fields: vec![
julia::Field {
doc: Doc::none(),
name: Ident::new("key"),
type_: member_type(ast, key, &name, ctx)?,
},
julia::Field {
doc: Doc::none(),
name: Ident::new("value"),
type_: member_type(ast, value, &name, ctx)?,
},
],
}));
}
TypeKind::Alias { aliased } => {
if node.name.is_empty() {
return Ok(());
}
if ghost_created_for(applied, node.node_id, "JuliaAlias").is_none() {
return Ok(());
}
let Some(target) = aliased.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "typedef target".to_string(),
at: node.name.clone(),
});
};
let name = julia_type_name(ast, id);
remember(emitted, &name, &node.name)?;
out.push(julia::Decl::Const(julia::ConstDecl {
doc: idl_note(&name, &node.name),
name: Ident::new(&name),
value: julia::Expr::Type(Box::new(member_type(ast, target, &node.name, ctx)?)),
}));
}
TypeKind::Enumeration { enumerators } => {
if ghost_created_for(applied, node.node_id, "JuliaEnum").is_none() {
return Ok(());
}
let name = julia_type_name(ast, id);
remember(emitted, &name, &node.name)?;
let prefix = module_prefix(ast, id);
let mut members = Vec::new();
let mut values: Vec<(i128, String)> = Vec::new();
for enumerator in enumerators {
let Ok(enum_node) = ast.enumerator(*enumerator) else {
continue;
};
if ghost_created_for(applied, enum_node.node_id, "JuliaEnumerator").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(),
}
})?;
if let Some((_, first)) = values.iter().find(|(other, _)| *other == value) {
return Err(MaterialiseError::NoLanguageForm {
construct: format!(
"two enumerators sharing the value {value}, `{first}` and \
`{}`, which @enum refuses at load time",
enum_node.name
),
language: LANGUAGE,
at: node.name.clone(),
});
}
values.push((value, enum_node.name.clone()));
let constant = format!("{prefix}{}", julia::escape_name(&enum_node.name));
remember(emitted, &constant, &enum_node.name)?;
members.push(julia::Enumerator {
doc: idl_note(&constant, &enum_node.name),
name: Ident::new(&constant),
value: julia::Expr::int(value),
});
}
if members.is_empty() {
return Err(MaterialiseError::NoLanguageForm {
construct: "an enumeration with no enumerators, which @enum refuses at \
load time"
.to_string(),
language: LANGUAGE,
at: node.name.clone(),
});
}
out.push(julia::Decl::Enum(julia::EnumDecl {
doc: with_idl_note(
Doc::lines(&[
"An enumeration. The holder is signed because DDS-XTypes 1.3",
"7.4.3.5.3 makes it so; the width on the wire is the",
"@bit_bound's and is applied by the runtime. Every value is",
"written out because @value admits gaps and @enum would",
"otherwise number from zero.",
]),
&name,
&node.name,
),
name: Ident::new(&name),
base: julia::Primitive::Int32,
members,
}));
}
TypeKind::Bitmask { bits } => {
if ghost_created_for(applied, node.node_id, "JuliaBitmask").is_none() {
return Ok(());
}
let name = julia_type_name(ast, id);
remember(emitted, &name, &node.name)?;
let bound = ast.effective_bit_bound(id);
out.push(julia::Decl::Const(julia::ConstDecl {
doc: with_idl_note(
Doc::lines(&[
"A bitmask, as the unsigned integer Table 33 of DDS-XTypes 1.3",
"7.4.1.1.3.2 gives for its bound. Not an @enum: an @enum value",
"is one member and a bitmask holds several at once.",
]),
&name,
&node.name,
),
name: Ident::new(&name),
value: julia::Expr::Type(Box::new(julia::Type::Primitive(julia_holder(bound)))),
}));
let prefix = module_prefix(ast, id);
for bit in bits {
let Ok(bit_node) = ast.bit_value(*bit) else {
continue;
};
if ghost_created_for(applied, bit_node.node_id, "JuliaBitConst").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(),
}
})?;
let constant = format!("{prefix}{}", julia::escape_name(&bit_node.name));
remember(emitted, &constant, &bit_node.name)?;
out.push(julia::Decl::Const(julia::ConstDecl {
doc: idl_note(&constant, &bit_node.name),
name: Ident::new(&constant),
value: julia::Expr::Binary {
op: "<<",
lhs: Box::new(julia::Expr::convert(
julia::Type::named(&name),
julia::Expr::int(1),
)),
rhs: Box::new(julia::Expr::int(i128::from(position))),
},
}));
}
}
TypeKind::Bitset { .. } => {
if ghost_created_for(applied, node.node_id, "JuliaBitset").is_none() {
return Ok(());
}
let name = julia_type_name(ast, id);
remember(emitted, &name, &node.name)?;
let mut pairs = Vec::new();
for field in &ast.all_bitfields_of(id) {
let Ok(field_node) = ast.bitfield(*field) else {
continue;
};
let Some(field_ident) = field_node.name.as_ref() else {
continue;
};
pairs.push((julia::escape_field(field_ident), field_ident.clone()));
}
no_spelling_collision(as_pairs(&pairs), LANGUAGE, &name)?;
let mut fields = Vec::new();
let mut index = 0;
for field in &ast.all_bitfields_of(id) {
let Ok(field_node) = ast.bitfield(*field) else {
continue;
};
let Some(field_ident) = field_node.name.as_ref() else {
continue;
};
fields.push(julia::Field {
doc: idl_note(&pairs[index].0, field_ident),
name: Ident::new(&pairs[index].0),
type_: julia::Type::Primitive(julia_holder(field_node.bits)),
});
index += 1;
}
out.push(julia::Decl::Struct(julia::StructDecl {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: with_idl_note(
Doc::line(
"A bitset. The fields are separate integers here; the packing into \
the anchor is the codec's, because the order on the wire is the \
declaration's.",
),
&name,
&node.name,
),
name: Ident::new(&name),
supertype: None,
fields,
}));
}
TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
let created = if matches!(node.kind, TypeKind::Valuetype { .. }) {
"JuliaValue"
} else {
"JuliaStruct"
};
if ghost_created_for(applied, node.node_id, created).is_none() {
return Ok(());
}
let name = julia_type_name(ast, id);
remember(emitted, &name, &node.name)?;
let members = ast.members_of(id).to_vec();
let base = super::c::struct_base(ast, id);
let mut pairs = Vec::new();
if base.is_some() {
pairs.push((julia::escape_field("parent"), "parent".to_string()));
}
for member in &members {
if let Ok(member_node) = ast.member(*member) {
pairs.push((
julia::escape_field(&member_node.name),
member_node.name.clone(),
));
}
}
no_spelling_collision(as_pairs(&pairs), LANGUAGE, &name)?;
let mut fields = Vec::new();
if let Some(base) = base {
fields.push(julia::Field {
doc: Doc::line(
"The base structure, the first member (XTypes 1.3 7.5.1.4.1.3).",
),
name: Ident::new(&julia::escape_field("parent")),
type_: member_type(ast, base, "parent", ctx)?,
});
}
for member in &members {
let Ok(member_node) = ast.member(*member) else {
continue;
};
let Some(target) = member_node.type_.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "member type".to_string(),
at: member_node.name.clone(),
});
};
let spelled = julia::escape_field(&member_node.name);
let mut type_ = member_type(ast, target, &member_node.name, ctx)?;
let mut doc = idl_note(&spelled, &member_node.name);
if member_node.facets.is_optional() {
type_ = julia::Type::optional(type_);
doc.push(
"Optional. `nothing` when the member is absent — DDS-XTypes 1.3 \
7.2.2.4.4.4.7. In a @mutable type an absent member is not written \
at all; outside one its absence is written.",
);
}
fields.push(julia::Field {
doc,
name: Ident::new(&spelled),
type_,
});
}
out.push(julia::Decl::Struct(julia::StructDecl {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: with_idl_note(
Doc::line(
"Immutable, which is Julia's default `struct`: a decoded sample is \
a value and not a handle a second reference can change underneath \
the first.",
),
&name,
&node.name,
),
name: Ident::new(&name),
supertype: None,
fields,
}));
}
TypeKind::Union { discriminator, .. } => {
if ghost_created_for(applied, node.node_id, "JuliaUnion").is_none() {
return Ok(());
}
let name = julia_type_name(ast, id);
remember(emitted, &name, &node.name)?;
let Some(disc) = discriminator.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "union discriminator".to_string(),
at: name,
});
};
let cases = ast.cases_of(id);
let mut pairs = vec![(DISCRIMINATOR.to_string(), "<discriminator>".to_string())];
for case in cases {
if let Ok(case_node) = ast.case(*case) {
if let Ok(member) = ast.member(case_node.member) {
pairs.push((julia::escape_field(&member.name), member.name.clone()));
}
}
}
no_spelling_collision(as_pairs(&pairs), LANGUAGE, &name)?;
let mut fields = vec![julia::Field {
doc: Doc::line("Which branch carries the value, if any does."),
name: Ident::new(DISCRIMINATOR),
type_: member_type(ast, disc, &name, ctx)?,
}];
for case in cases {
let Ok(case_node) = ast.case(*case) else {
continue;
};
if ghost_created_for(applied, case_node.node_id, "JuliaCase").is_none() {
continue;
}
let Ok(member) = ast.member(case_node.member) else {
continue;
};
let Some(target) = member.type_.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "union case type".to_string(),
at: member.name.clone(),
});
};
let spelled = julia::escape_field(&member.name);
fields.push(julia::Field {
doc: idl_note(&spelled, &member.name),
name: Ident::new(&spelled),
type_: julia::Type::optional(member_type(ast, target, &member.name, ctx)?),
});
}
out.push(julia::Decl::Struct(julia::StructDecl {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: with_idl_note(
Doc::lines(&[
"A union. Not a Julia `Union{…}`: an IDL discriminator may take a",
"value that selects no branch (IDL 4.2 7.4.1.4.4.1), and a",
"`Union{A, B}` value is an A or a B with nowhere to record that.",
"`_d` says which branch carries the value; only that branch is",
"written to the wire (DDS-XTypes 1.3 7.4.1.1.5.2).",
]),
&name,
&node.name,
),
name: Ident::new(&name),
supertype: None,
fields,
}));
}
TypeKind::Native => {
if ghost_created_for(applied, node.node_id, "JuliaNative").is_none() {
return Ok(());
}
let name = julia_type_name(ast, id);
remember(emitted, &name, &node.name)?;
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());
out.push(julia::Decl::Const(julia::ConstDecl {
doc: with_idl_note(
Doc::line("A native type. Opaque unless a facet named its mapping."),
&name,
&node.name,
),
name: Ident::new(&name),
value: julia::Expr::Type(Box::new(match mapping {
Some(spelling) => julia::Type::named(&spelling),
None => julia::Type::Primitive(julia::Primitive::Any),
})),
}));
}
TypeKind::ValueBox { boxed } => {
let Some(target) = boxed.resolved() else {
return Ok(());
};
let name = julia_type_name(ast, id);
if emitted.iter().any(|(spelled, _)| *spelled == name) {
return Ok(());
}
remember(emitted, &name, &node.name)?;
out.push(julia::Decl::Const(julia::ConstDecl {
doc: with_idl_note(
Doc::line("A boxed value: the type or `nothing` when absent."),
&name,
&node.name,
),
name: Ident::new(&name),
value: julia::Expr::Type(Box::new(julia::Type::optional(member_type(
ast, target, &name, ctx,
)?))),
}));
}
TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. }
| TypeKind::Any
| TypeKind::ValueBase => {}
}
Ok(())
}
pub fn julia_member_type(
ast: &Ast,
target: Id<TypeNode>,
at: &str,
) -> Result<julia::Type, MaterialiseError> {
member_type(ast, target, at, &mut Ctx::unordered())
}
fn member_type(
ast: &Ast,
target: Id<TypeNode>,
at: &str,
ctx: &mut Ctx,
) -> Result<julia::Type, MaterialiseError> {
let construct = match ast.type_(target).map(|node| &node.kind) {
Ok(TypeKind::Any) => Some("any"),
Ok(TypeKind::ValueBase) => Some("ValueBase"),
Ok(TypeKind::ObjectRef { .. }) => Some("Object"),
_ => None,
};
if let Some(construct) = construct {
return Err(MaterialiseError::NoLanguageForm {
construct: construct.to_string(),
language: LANGUAGE,
at: at.to_string(),
});
}
value_type(ast, target, ctx)
}
pub fn julia_value_type(ast: &Ast, id: Id<TypeNode>) -> Result<julia::Type, MaterialiseError> {
value_type(ast, id, &mut Ctx::unordered())
}
fn value_type(ast: &Ast, id: Id<TypeNode>, ctx: &mut Ctx) -> Result<julia::Type, MaterialiseError> {
if ctx.depth > MAX_DEPTH {
return Err(MaterialiseError::Unevaluated {
construct: "a type expression deeper than the planner follows".to_string(),
at: ast
.type_(id)
.map_or_else(|_| String::new(), |node| node.name.clone()),
});
}
ctx.depth += 1;
let result = value_type_inner(ast, id, ctx);
ctx.depth -= 1;
result
}
fn value_type_inner(
ast: &Ast,
id: Id<TypeNode>,
ctx: &mut Ctx,
) -> Result<julia::Type, MaterialiseError> {
let Ok(node) = ast.type_(id) else {
return Ok(julia::Type::Primitive(julia::Primitive::Any));
};
Ok(match &node.kind {
TypeKind::Primitive(kind) => julia_primitive_type(*kind),
TypeKind::String { .. } | TypeKind::Fixed { .. } => {
julia::Type::Primitive(julia::Primitive::String)
}
TypeKind::Sequence { element, .. } => {
let Some(element) = element.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "sequence element".to_string(),
at: node.name.clone(),
});
};
julia::Type::vector(value_type(ast, element, ctx)?)
}
TypeKind::Array { element, dims } => {
let Some(element) = element.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "array element".to_string(),
at: node.name.clone(),
});
};
let mut type_ = value_type(ast, element, ctx)?;
for dim in dims.iter().rev() {
let length =
ast.eval_integral(*dim)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "array length".to_string(),
at: node.name.clone(),
})?;
type_ = julia::Type::ntuple(length, type_);
}
type_
}
TypeKind::Map { .. } => {
let entry = julia_map_entry_name(ast, id);
julia::Type::vector(named_or_forward(ctx, id, &entry))
}
TypeKind::Alias { aliased } if node.name.is_empty() => match aliased.resolved() {
Some(target) => value_type(ast, target, ctx)?,
None => julia::Type::Primitive(julia::Primitive::Any),
},
TypeKind::Alias { aliased } if ctx.is_forward(id) => match aliased.resolved() {
Some(target) => value_type(ast, target, ctx)?,
None => julia::Type::Primitive(julia::Primitive::Any),
},
TypeKind::ValueBox { boxed } if ctx.is_forward(id) => match boxed.resolved() {
Some(target) => julia::Type::optional(value_type(ast, target, ctx)?),
None => julia::Type::Primitive(julia::Primitive::Any),
},
_ => {
let name = julia_type_name(ast, id);
named_or_forward(ctx, id, &name)
}
})
}
fn named_or_forward(ctx: &mut Ctx, id: Id<TypeNode>, name: &str) -> julia::Type {
if ctx.is_forward(id) {
ctx.record_forward(name);
return julia::Type::named(&format!("{FORWARD_PREFIX}{name}"));
}
julia::Type::named(name)
}
#[must_use]
pub fn julia_primitive_type(kind: PrimitiveKind) -> julia::Type {
let primitive = match kind {
PrimitiveKind::Boolean => julia::Primitive::Bool,
PrimitiveKind::Octet | PrimitiveKind::UInt8 | PrimitiveKind::Char => {
julia::Primitive::UInt8
}
PrimitiveKind::Int8 => julia::Primitive::Int8,
PrimitiveKind::WChar | PrimitiveKind::UInt16 => julia::Primitive::UInt16,
PrimitiveKind::Int16 => julia::Primitive::Int16,
PrimitiveKind::Int32 => julia::Primitive::Int32,
PrimitiveKind::UInt32 => julia::Primitive::UInt32,
PrimitiveKind::Int64 => julia::Primitive::Int64,
PrimitiveKind::UInt64 => julia::Primitive::UInt64,
PrimitiveKind::Float32 => julia::Primitive::Float32,
PrimitiveKind::Float64 => julia::Primitive::Float64,
PrimitiveKind::Float128 => julia::Primitive::String,
};
julia::Type::Primitive(primitive)
}
#[must_use]
pub const fn julia_holder(bits: u16) -> julia::Primitive {
match bits {
0..=8 => julia::Primitive::UInt8,
9..=16 => julia::Primitive::UInt16,
17..=32 => julia::Primitive::UInt32,
_ => julia::Primitive::UInt64,
}
}
#[must_use]
pub fn julia_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(),
}
}
#[must_use]
pub fn julia_map_entry_name(ast: &Ast, id: Id<TypeNode>) -> String {
map_entry_name_at(ast, id, 0)
}
fn map_entry_name_at(ast: &Ast, id: Id<TypeNode>, depth: usize) -> String {
if depth > MAX_DEPTH {
return format!("{INVENTED_PREFIX}deep_{}", id.slot());
}
let Ok(node) = ast.type_(id) else {
return format!("{INVENTED_PREFIX}unknown");
};
let TypeKind::Map { key, value, .. } = &node.kind else {
return format!("{INVENTED_PREFIX}unknown");
};
let key = key
.resolved()
.map_or_else(|| "unknown".to_string(), |k| type_tag(ast, k, depth + 1));
let value = value
.resolved()
.map_or_else(|| "unknown".to_string(), |v| type_tag(ast, v, depth + 1));
format!("{INVENTED_PREFIX}{key}_{value}")
}
fn type_tag(ast: &Ast, id: Id<TypeNode>, depth: usize) -> String {
if depth > MAX_DEPTH {
return format!("deep_{}", id.slot());
}
match ast.type_(id).map(|node| node.kind.clone()) {
Ok(TypeKind::Primitive(kind)) => kind.as_str().replace(' ', "_"),
Ok(TypeKind::String { width, .. }) => match width {
zerodds_ast::CharWidth::Char => "string".to_string(),
zerodds_ast::CharWidth::WChar => "wstring".to_string(),
},
Ok(TypeKind::Sequence { element, .. }) => match element.resolved() {
Some(element) => format!("seq_of_{}", type_tag(ast, element, depth + 1)),
None => "seq_of_unknown".to_string(),
},
Ok(TypeKind::Array { element, dims }) => match element.resolved() {
Some(element) => {
let mut name = format!("arr_of_{}", type_tag(ast, element, depth + 1));
for dim in &dims {
match ast.eval_integral(*dim) {
Ok(length) => name.push_str(&format!("_{length}")),
Err(_) => name.push_str("_x"),
}
}
name
}
None => "arr_of_unknown".to_string(),
},
Ok(TypeKind::Map { .. }) => map_entry_name_at(ast, id, depth + 1),
_ => julia_type_name(ast, id),
}
}
#[must_use]
pub fn julia_module_prefix(ast: &Ast, id: Id<TypeNode>) -> String {
module_prefix(ast, id)
}
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 = flatten(&segments[..segments.len() - 1]);
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_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());
let borrowed: Vec<&str> = segments.iter().map(String::as_str).collect();
flatten_str(&borrowed)
}
fn flatten(segments: &[String]) -> String {
let borrowed: Vec<&str> = segments.iter().map(String::as_str).collect();
flatten_str(&borrowed)
}
fn flatten_str(segments: &[&str]) -> String {
segments
.iter()
.map(|segment| julia::escape_name(segment))
.collect::<Vec<String>>()
.join("_")
}
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 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))
})
}
fn julia_constant(
value: &zerodds_ast::ConstValue,
type_: &julia::Type,
at: &str,
) -> Result<julia::Expr, MaterialiseError> {
use zerodds_ast::ConstValue;
let refuse = |construct: &str| MaterialiseError::NoLanguageForm {
construct: construct.to_string(),
language: LANGUAGE,
at: at.to_string(),
};
let already_exact = matches!(
type_,
julia::Type::Primitive(julia::Primitive::String | julia::Primitive::Bool)
);
let is_string = matches!(type_, julia::Type::Primitive(julia::Primitive::String));
let convert = |expr: julia::Expr| {
if already_exact {
expr
} else {
julia::Expr::convert(type_.clone(), expr)
}
};
Ok(match value {
ConstValue::Boolean(flag) => julia::Expr::Literal(Literal::Bool(*flag)),
ConstValue::Integer(number) => convert(julia::Expr::Literal(Literal::int(*number))),
ConstValue::Char(character) => convert(julia::Expr::Literal(Literal::int(i128::from(
u32::from(*character),
)))),
ConstValue::WChar(point) => convert(julia::Expr::Literal(Literal::int(i128::from(*point)))),
ConstValue::Float { mantissa, exponent } => {
if is_string {
return Err(refuse(
"a `long double` constant, which has no Julia literal",
));
}
convert(julia::Expr::Literal(Literal::Float(constant::float_text(
mantissa,
exponent.as_deref(),
))))
}
ConstValue::Fixed { digits, scale } => {
julia::Expr::Literal(Literal::Str(constant::fixed_decimal(digits, *scale)))
}
ConstValue::String(text) => julia::Expr::Literal(Literal::Str(text.clone())),
ConstValue::WString(points) => julia::Expr::Literal(Literal::Str(
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")),
})
}