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, Bound, 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::elixir::{self, Basic, Expr, Type};
const LANGUAGE: &str = "Elixir";
const MAX_MODULE_DEPTH: usize = 64;
const MAX_TYPE_DEPTH: usize = 64;
pub const CONSTANTS_MODULE: &str = "ZdConstants";
pub const DISCRIMINATOR: &str = "_d";
pub fn elixir_modules(
ast: &Ast,
applied: &Applied,
) -> Result<Vec<elixir::TopLevel>, MaterialiseError> {
let mut out = Vec::new();
let mut emitted: Vec<(String, String)> = Vec::new();
let mut text: Vec<(usize, Vec<String>)> = Vec::new();
for (id, _) in ast.types() {
let start = out.len();
declare(ast, applied, id, &mut out, &mut emitted)?;
let slots = verbatim::declaration_slots(ast, id, verbatim::aliases::ELIXIR);
if slots.is_empty() {
continue;
}
let path = type_module_path(ast, id);
let mut begin = slots.begin;
let mut end = slots.end;
if let Some(module) = out
.get_mut(start..)
.unwrap_or_default()
.iter_mut()
.find(|module| module.name == path)
{
let mut body: Vec<elixir::Item> = begin.drain(..).map(elixir::Item::Verbatim).collect();
body.append(&mut module.body);
body.extend(end.drain(..).map(elixir::Item::Verbatim));
module.body = body;
}
let mut before = slots.before;
before.append(&mut begin);
let mut after = end;
after.extend(slots.after);
text.push((start, before));
text.push((out.len(), after));
}
declare_constants(ast, applied, &mut out, &mut emitted)?;
declare_exceptions(ast, applied, &mut out, &mut emitted)?;
declare_interfaces(ast, applied, &mut out, &mut emitted)?;
let (begin_file, end_file) = verbatim::file_slots(ast, verbatim::aliases::ELIXIR);
let mut top: Vec<elixir::TopLevel> = begin_file
.into_iter()
.map(elixir::TopLevel::Verbatim)
.collect();
let mut text = text.into_iter().peekable();
for (index, module) in out.into_iter().enumerate() {
while let Some((_, lines)) = text.next_if(|(at, _)| *at <= index) {
top.extend(lines.into_iter().map(elixir::TopLevel::Verbatim));
}
top.push(elixir::TopLevel::Module(module));
}
for (_, lines) in text {
top.extend(lines.into_iter().map(elixir::TopLevel::Verbatim));
}
top.extend(end_file.into_iter().map(elixir::TopLevel::Verbatim));
Ok(top)
}
pub fn elixir_file(ast: &Ast, applied: &Applied) -> Result<elixir::File, MaterialiseError> {
Ok(elixir::File {
doc: Doc::lines(&[
"Generated by ZeroDDS from an IDL source. Do not edit.",
"",
"Elixir enforces no width, no signedness and no bound, so every one",
"of those is written into a module attribute beside the type: read",
"them back with Module.__zd_members__/0 and Module.__zd_idl__/0.",
"The IDL spelling of every declaration and every member is there",
"too, because a member's ID under @autoid(HASH) is the MD5 of the",
"IDL name (DDS-XTypes 1.3 7.3.1.2.1.1) and neither the module",
"segment's capital nor the atom's lower case inverts.",
]),
modules: elixir_modules(ast, applied)?,
})
}
#[allow(clippy::too_many_lines)]
fn declare(
ast: &Ast,
applied: &Applied,
id: Id<TypeNode>,
out: &mut Vec<elixir::Module>,
emitted: &mut Vec<(String, String)>,
) -> Result<(), MaterialiseError> {
let Ok(node) = ast.type_(id) else {
return Ok(());
};
match &node.kind {
TypeKind::Primitive(_)
| TypeKind::String { .. }
| TypeKind::Fixed { .. }
| TypeKind::Sequence { .. }
| TypeKind::Map { .. } => {}
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::Alias { aliased } => {
if node.name.is_empty() {
return Ok(());
}
if ghost_created_for(applied, node.node_id, "ElixirAlias").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 = type_module_path(ast, id);
remember(emitted, &name, &node.name)?;
let mut body = vec![
idl_attribute(ast, id, "alias", &node.name),
reader("__zd_idl__", "zd_idl"),
elixir::Item::Attribute(elixir::Attribute {
doc: Doc::none(),
name: Ident::new("zd_target"),
value: Expr::Keyword(type_facts(ast, target, &node.name, 0)?),
}),
reader("__zd_target__", "zd_target"),
];
body.push(type_alias(
"t",
elixir_member_type(ast, target, &node.name)?,
Doc::none(),
));
out.push(elixir::Module {
doc: Doc::lines(&[
"A typedef, as the type alias it is.",
"",
"The module declares no struct: IDL 4.2 7.4.1.4.3 says an alias",
"\"does not define a new type\", so t() expands to the target and",
"a value of the target is a value of this.",
]),
name,
body,
});
}
TypeKind::Enumeration { enumerators } => {
if ghost_created_for(applied, node.node_id, "ElixirEnum").is_none() {
return Ok(());
}
let name = type_module_path(ast, id);
remember(emitted, &name, &node.name)?;
let bits = ast.effective_bit_bound(id);
let mut listed: Vec<(String, 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, "ElixirEnumerator").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(),
}
})?;
listed.push((
elixir::atom_name(&enum_node.name),
value,
enum_node.name.clone(),
));
}
no_spelling_collision(
listed
.iter()
.map(|(spelled, _, declared)| (spelled.as_str(), declared.as_str())),
LANGUAGE,
&node.name,
)?;
out.push(elixir::Module {
doc: Doc::lines(&[
"An enumeration, as the atoms it names.",
"",
"Every value is written out rather than counted: @value admits",
"gaps, and a numbering this file inferred would renumber the",
"wire (DDS-XTypes 1.3 7.4.3.5.3 gives the holder; the width is",
"the @bit_bound's and is in @zd_idl).",
"",
"A member of this type is annotated integer() and not t(): an",
"@appendable writer may send an enumerator this IDL does not",
"list, and there is no atom for it. from_value/1 answers",
"{:error, {:unknown_value, n}} for that value rather than",
"raising.",
]),
name,
body: enum_body(ast, id, &node.name, bits, &listed),
});
}
TypeKind::Bitmask { bits } => {
if ghost_created_for(applied, node.node_id, "ElixirBitmask").is_none() {
return Ok(());
}
let name = type_module_path(ast, id);
remember(emitted, &name, &node.name)?;
let bound = ast.effective_bit_bound(id);
let mut listed: Vec<(String, u16, i128, String)> = Vec::new();
for bit in bits {
let Ok(bit_node) = ast.bit_value(*bit) else {
continue;
};
if ghost_created_for(applied, bit_node.node_id, "ElixirBit").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 value = bit_value(position).ok_or_else(|| MaterialiseError::Unevaluated {
construct: "bit position past the widest holder".to_string(),
at: bit_node.name.clone(),
})?;
listed.push((
elixir::atom_name(&bit_node.name),
position,
value,
bit_node.name.clone(),
));
}
no_spelling_collision(
listed
.iter()
.map(|(spelled, _, _, declared)| (spelled.as_str(), declared.as_str())),
LANGUAGE,
&node.name,
)?;
out.push(elixir::Module {
doc: Doc::lines(&[
"A bitmask, as one function per bit.",
"",
"The holder is the unsigned integer Table 33 of DDS-XTypes 1.3",
"7.4.1.1.3.2 gives for the @bit_bound, which is in @zd_idl;",
"Elixir's integer() has no width of its own, so the width is",
"stated there and nowhere else.",
"",
"Each function returns the bit's value and not its position:",
"Bitwise.bsl/2 is a macro and would make every generated module",
"require Bitwise for a number this file already knows.",
]),
name,
body: bitmask_body(ast, id, &node.name, bound, &listed),
});
}
TypeKind::Bitset { .. } => {
if ghost_created_for(applied, node.node_id, "ElixirBitset").is_none() {
return Ok(());
}
let name = type_module_path(ast, id);
remember(emitted, &name, &node.name)?;
let mut listed: Vec<(String, u16, String)> = 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;
};
listed.push((
elixir::atom_name(field_ident),
field_node.bits,
field_ident.clone(),
));
}
no_spelling_collision(
listed
.iter()
.map(|(spelled, _, declared)| (spelled.as_str(), declared.as_str())),
LANGUAGE,
&node.name,
)?;
out.push(elixir::Module {
doc: Doc::lines(&[
"A bitset, as a struct of one field per bitfield.",
"",
"The fields are separate integers here and the packing into the",
"anchor is the codec's, because the order on the wire is the",
"declaration's. The width of each is in @zd_bitfields: a",
"bitfield<3> is three bits, and Elixir has no type that says so.",
]),
name,
body: bitset_body(ast, id, &node.name, &listed),
});
}
TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
let created = if matches!(node.kind, TypeKind::Valuetype { .. }) {
"ElixirValue"
} else {
"ElixirStruct"
};
if ghost_created_for(applied, node.node_id, created).is_none() {
return Ok(());
}
let name = type_module_path(ast, id);
remember(emitted, &name, &node.name)?;
let mut fields = Vec::new();
if let Some(base) = super::c::struct_base(ast, id) {
fields.push(member_field(ast, base, "parent", false, false, Vec::new())?);
}
for member in ast.members_of(id) {
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 optional = member_node.facets.is_optional();
fields.push(member_field(
ast,
target,
&member_node.name,
optional,
optional,
Vec::new(),
)?);
}
check_fields(&fields, &node.name)?;
let kind = if matches!(node.kind, TypeKind::Valuetype { .. }) {
"valuetype"
} else {
"struct"
};
out.push(elixir::Module {
doc: aggregate_doc(),
name,
body: aggregate_body(ast, id, &node.name, kind, &fields, Vec::new()),
});
}
TypeKind::Union { discriminator, .. } => {
if ghost_created_for(applied, node.node_id, "ElixirUnion").is_none() {
return Ok(());
}
let name = type_module_path(ast, id);
remember(emitted, &name, &node.name)?;
let Some(disc) = discriminator.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "union discriminator".to_string(),
at: node.name.clone(),
});
};
let mut fields = vec![Member {
atom: DISCRIMINATOR.to_string(),
idl: "<discriminator>".to_string(),
type_: elixir_member_type(ast, disc, &node.name)?,
default: default_for(ast, disc, 0)?.unwrap_or_else(Expr::nil),
enforced: false,
listed: false,
facts: Vec::new(),
}];
for case in ast.cases_of(id) {
let Ok(case_node) = ast.case(*case) else {
continue;
};
if ghost_created_for(applied, case_node.node_id, "ElixirCase").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 labels = case_labels(ast, &case_node.labels, &member.name)?;
fields.push(member_field(
ast,
target,
&member.name,
true,
member.facets.is_optional(),
labels,
)?);
}
check_fields(&fields, &node.name)?;
let discriminator_facts = type_facts(ast, disc, &node.name, 0)?;
out.push(elixir::Module {
doc: Doc::lines(&[
"A union, as a struct with a discriminator and one nullable",
"field per branch.",
"",
"Not a tagged tuple. An IDL union's discriminator may take a",
"value no branch selects (IDL 4.2 7.4.1.4.4.1), and",
"DDS-XTypes 1.3 7.4.1.1.5.2 writes the discriminator and then",
"only the selected branch — so a sample that selects nothing is",
"well formed and carries no branch. A tagged tuple has no tag",
"for that; here it is the value whose branch fields are all nil.",
"",
"Which values select which branch is in @zd_members, under",
"labels:, with :default for the default case.",
]),
name,
body: aggregate_body(ast, id, &node.name, "union", &fields, discriminator_facts),
});
}
TypeKind::Native => {
if ghost_created_for(applied, node.node_id, "ElixirNative").is_none() {
return Ok(());
}
let name = type_module_path(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());
let type_ = mapping
.as_deref()
.map_or_else(|| Type::Basic(Basic::Any), named_type);
out.push(elixir::Module {
doc: Doc::lines(&[
"A native type. Opaque unless a facet named its mapping.",
"",
"any() here is the declaration's own opacity and not a member",
"this column could not spell: a member that reaches an `any`,",
"a ValueBase or an object reference is refused by name.",
]),
name,
body: vec![
idl_attribute(ast, id, "native", &node.name),
reader("__zd_idl__", "zd_idl"),
type_alias("t", type_, Doc::none()),
],
});
}
TypeKind::ValueBox { boxed } => {
let Some(target) = boxed.resolved() else {
return Ok(());
};
let name = type_module_path(ast, id);
if emitted.iter().any(|(spelled, _)| *spelled == dotted(&name)) {
return Ok(());
}
remember(emitted, &name, &node.name)?;
out.push(elixir::Module {
doc: Doc::line("A boxed value: the boxed type, or nil when absent."),
name,
body: vec![
idl_attribute(ast, id, "valuebox", &node.name),
reader("__zd_idl__", "zd_idl"),
type_alias(
"t",
elixir_member_type(ast, target, &node.name)?.or_nil(),
Doc::none(),
),
],
});
}
TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. }
| TypeKind::Any
| TypeKind::ValueBase => {}
}
Ok(())
}
struct Member {
atom: String,
idl: String,
type_: Type,
default: Expr,
enforced: bool,
listed: bool,
facts: Vec<(Ident, Expr)>,
}
fn member_field(
ast: &Ast,
target: Id<TypeNode>,
name: &str,
nullable: bool,
optional: bool,
extra: Vec<(Ident, Expr)>,
) -> Result<Member, MaterialiseError> {
let type_ = elixir_member_type(ast, target, name)?;
let mut facts = vec![kw("idl", Expr::string(name))];
facts.extend(type_facts(ast, target, name, 0)?);
facts.extend(extra);
if optional {
facts.push(kw("optional", Expr::Literal(Literal::Bool(true))));
}
let natural = default_for(ast, target, 0)?;
Ok(Member {
atom: elixir::atom_name(name),
idl: name.to_string(),
type_: if nullable { type_.or_nil() } else { type_ },
default: if nullable {
Expr::nil()
} else {
natural.clone().unwrap_or_else(Expr::nil)
},
enforced: !nullable && natural.is_none(),
listed: true,
facts,
})
}
fn check_fields(fields: &[Member], at: &str) -> Result<(), MaterialiseError> {
no_spelling_collision(
fields
.iter()
.map(|field| (field.atom.as_str(), field.idl.as_str())),
LANGUAGE,
at,
)
}
fn aggregate_doc() -> Doc {
Doc::lines(&[
"An aggregate, as a struct and the type of one.",
"",
"Elixir states no width, no signedness and no bound, so every one of",
"those is in @zd_members, in declaration order, with the IDL spelling",
"of each member beside it. Read it back with __zd_members__/0.",
"",
"A field with no default is in @enforce_keys: its type has no value",
"that could stand as one, so %__MODULE__{} is a compile error rather",
"than a sample with a hole in it.",
])
}
fn aggregate_body(
ast: &Ast,
id: Id<TypeNode>,
declared: &str,
kind: &str,
fields: &[Member],
discriminator: Vec<(Ident, Expr)>,
) -> Vec<elixir::Item> {
let mut body = vec![
idl_attribute(ast, id, kind, declared),
reader("__zd_idl__", "zd_idl"),
];
if !discriminator.is_empty() {
body.push(elixir::Item::Attribute(elixir::Attribute {
doc: Doc::line("The discriminator's own type, which is not a member."),
name: Ident::new("zd_discriminator"),
value: Expr::Keyword(discriminator),
}));
body.push(reader("__zd_discriminator__", "zd_discriminator"));
}
body.extend(members_body(fields));
body
}
fn enum_body(
ast: &Ast,
id: Id<TypeNode>,
declared: &str,
bits: u16,
listed: &[(String, i128, String)],
) -> Vec<elixir::Item> {
let mut body = vec![
idl_attribute_with(
ast,
id,
"enum",
declared,
vec![kw("bits", Expr::int(i128::from(bits)))],
),
reader("__zd_idl__", "zd_idl"),
elixir::Item::Attribute(elixir::Attribute {
doc: Doc::line("Every enumerator: the atom, its value and its IDL spelling."),
name: Ident::new("zd_enumerators"),
value: Expr::List(
listed
.iter()
.map(|(atom, value, idl)| {
Expr::Tuple(vec![
Expr::Atom(atom.clone()),
Expr::int(*value),
Expr::string(idl),
])
})
.collect(),
),
}),
reader("__zd_enumerators__", "zd_enumerators"),
type_alias(
"t",
match listed.len() {
0 => Type::Basic(Basic::None),
_ => Type::Union(
listed
.iter()
.map(|(atom, _, _)| Type::Atom(atom.clone()))
.collect(),
),
},
Doc::none(),
),
];
for (atom, value, _) in listed {
body.push(elixir::Item::Function(elixir::Function {
doc: Doc::none(),
name: Ident::new(atom),
params: Vec::new(),
body: Expr::int(*value),
}));
}
if listed.is_empty() {
return body;
}
body.push(elixir::Item::Spec(elixir::Signature {
doc: Doc::line("The wire value of an enumerator this IDL names."),
name: Ident::new("value"),
params: vec![elixir::Param {
name: None,
type_: Type::Local(Ident::new("t")),
}],
result: Type::Basic(Basic::Integer),
}));
for (atom, value, _) in listed {
body.push(elixir::Item::Function(elixir::Function {
doc: Doc::none(),
name: Ident::new("value"),
params: vec![Expr::Atom(atom.clone())],
body: Expr::int(*value),
}));
}
body.push(elixir::Item::Spec(elixir::Signature {
doc: Doc::lines(&[
"The enumerator a value names, or the value that names none.",
"",
"An @appendable writer may send an enumerator this IDL does not",
"list, so this answers rather than raising.",
]),
name: Ident::new("from_value"),
params: vec![elixir::Param {
name: None,
type_: Type::Basic(Basic::Integer),
}],
result: Type::Union(vec![
Type::Tuple(vec![
Type::Atom("ok".to_string()),
Type::Local(Ident::new("t")),
]),
Type::Tuple(vec![
Type::Atom("error".to_string()),
Type::Tuple(vec![
Type::Atom("unknown_value".to_string()),
Type::Basic(Basic::Integer),
]),
]),
]),
}));
let mut seen: Vec<i128> = Vec::new();
for (atom, value, _) in listed {
if seen.contains(value) {
continue;
}
seen.push(*value);
body.push(elixir::Item::Function(elixir::Function {
doc: Doc::none(),
name: Ident::new("from_value"),
params: vec![Expr::int(*value)],
body: Expr::Tuple(vec![Expr::Atom("ok".to_string()), Expr::Atom(atom.clone())]),
}));
}
body.push(elixir::Item::Function(elixir::Function {
doc: Doc::none(),
name: Ident::new("from_value"),
params: vec![Expr::Var(Ident::new("other"))],
body: Expr::Tuple(vec![
Expr::Atom("error".to_string()),
Expr::Tuple(vec![
Expr::Atom("unknown_value".to_string()),
Expr::Var(Ident::new("other")),
]),
]),
}));
body
}
fn bitmask_body(
ast: &Ast,
id: Id<TypeNode>,
declared: &str,
bound: u16,
listed: &[(String, u16, i128, String)],
) -> Vec<elixir::Item> {
let mut body = vec![
idl_attribute_with(
ast,
id,
"bitmask",
declared,
vec![kw("bits", Expr::int(i128::from(bound)))],
),
reader("__zd_idl__", "zd_idl"),
elixir::Item::Attribute(elixir::Attribute {
doc: Doc::line("Every bit: the atom, its position, its value and its IDL spelling."),
name: Ident::new("zd_bits"),
value: Expr::List(
listed
.iter()
.map(|(atom, position, value, idl)| {
Expr::Tuple(vec![
Expr::Atom(atom.clone()),
Expr::int(i128::from(*position)),
Expr::int(*value),
Expr::string(idl),
])
})
.collect(),
),
}),
reader("__zd_bits__", "zd_bits"),
type_alias(
"t",
Type::Basic(Basic::Integer),
Doc::line("The holder is unsigned and @bit_bound wide; both are in @zd_idl."),
),
];
for (atom, _, value, _) in listed {
body.push(elixir::Item::Function(elixir::Function {
doc: Doc::none(),
name: Ident::new(atom),
params: Vec::new(),
body: Expr::int(*value),
}));
}
body
}
fn bit_value(position: u16) -> Option<i128> {
1_i128.checked_shl(u32::from(position))
}
fn bitset_body(
ast: &Ast,
id: Id<TypeNode>,
declared: &str,
listed: &[(String, u16, String)],
) -> Vec<elixir::Item> {
vec![
idl_attribute(ast, id, "bitset", declared),
reader("__zd_idl__", "zd_idl"),
elixir::Item::Attribute(elixir::Attribute {
doc: Doc::line("Every bitfield: the atom, its width in bits and its IDL spelling."),
name: Ident::new("zd_bitfields"),
value: Expr::List(
listed
.iter()
.map(|(atom, bits, idl)| {
Expr::Tuple(vec![
Expr::Atom(atom.clone()),
Expr::int(i128::from(*bits)),
Expr::string(idl),
])
})
.collect(),
),
}),
reader("__zd_bitfields__", "zd_bitfields"),
elixir::Item::Defstruct(
listed
.iter()
.map(|(atom, _, _)| elixir::Field {
name: Ident::new(atom),
default: Expr::int(0),
})
.collect(),
),
type_alias(
"t",
Type::Struct(
listed
.iter()
.map(|(atom, _, _)| (Ident::new(atom), Type::Basic(Basic::Integer)))
.collect(),
),
Doc::none(),
),
]
}
struct ConstantGroup {
module: Id<ModuleNode>,
items: Vec<elixir::Item>,
listed: Vec<(Ident, Expr)>,
names: Vec<(String, String)>,
}
fn declare_constants(
ast: &Ast,
applied: &Applied,
out: &mut Vec<elixir::Module>,
emitted: &mut Vec<(String, String)>,
) -> Result<(), MaterialiseError> {
let mut groups: Vec<ConstantGroup> = Vec::new();
for (_, node) in ast.constants() {
if ghost_created_for(applied, node.node_id, "ElixirConst").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 atom = elixir::attribute_name(&node.name);
let declared = node.type_.resolved();
let mut facts = vec![kw("idl", Expr::string(&node.name))];
match declared {
Some(target) => facts.extend(type_facts(ast, target, &node.name, 0)?),
None => facts.extend(primitive_facts(PrimitiveKind::Int32)),
}
let literal = elixir_constant(ast, &value, declared, &node.name)?;
let at = match groups.iter().position(|group| group.module == node.module) {
Some(at) => at,
None => {
groups.push(ConstantGroup {
module: node.module,
items: Vec::new(),
listed: Vec::new(),
names: Vec::new(),
});
groups.len() - 1
}
};
let Some(group) = groups.get_mut(at) else {
continue;
};
group.items.push(elixir::Item::Attribute(elixir::Attribute {
doc: idl_note(&atom, &node.name),
name: Ident::new(&atom),
value: literal,
}));
group.items.push(elixir::Item::Function(elixir::Function {
doc: Doc::none(),
name: Ident::new(&atom),
params: Vec::new(),
body: Expr::Attr(Ident::new(&atom)),
}));
group.listed.push((Ident::new(&atom), Expr::Keyword(facts)));
group.names.push((atom, node.name.clone()));
}
for group in groups {
no_spelling_collision(
group
.names
.iter()
.map(|(spelled, declared)| (spelled.as_str(), declared.as_str())),
LANGUAGE,
CONSTANTS_MODULE,
)?;
let name = module_path(ast, group.module)?.join(CONSTANTS_MODULE);
remember(emitted, &name, CONSTANTS_MODULE)?;
let mut body = vec![
elixir::Item::Attribute(elixir::Attribute {
doc: Doc::line("Every constant: the function's name and what it holds."),
name: Ident::new("zd_constants"),
value: Expr::Keyword(group.listed),
}),
reader("__zd_constants__", "zd_constants"),
];
body.extend(group.items);
out.push(elixir::Module {
doc: Doc::lines(&[
"The constants one IDL scope declares.",
"",
"Each is a module attribute and the zero-arity function that reads",
"it. The attribute alone would not do: it is a compile-time binding",
"and is gone from the compiled module, so nothing outside this file",
"could reach it.",
"",
"The module's name is this column's own — Elixir has no file-scope",
"binding, so a constant needs a module to live in — and it goes",
"through the same collision check as a declared one.",
]),
name,
body,
});
}
Ok(())
}
fn declare_exceptions(
ast: &Ast,
applied: &Applied,
out: &mut Vec<elixir::Module>,
emitted: &mut Vec<(String, String)>,
) -> Result<(), MaterialiseError> {
for (_, node) in ast.exceptions() {
if ghost_created_for(applied, node.node_id, "ElixirError").is_none() {
continue;
}
let mut name = module_path(ast, node.module)?;
name = name.join(&elixir::module_segment(&node.name));
remember(emitted, &name, &node.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 optional = member_node.facets.is_optional();
fields.push(member_field(
ast,
target,
&member_node.name,
optional,
optional,
Vec::new(),
)?);
}
check_fields(&fields, &node.name)?;
let mut body = vec![
declared_idl_attribute(ast, node.module, "exception", &node.name)?,
reader("__zd_idl__", "zd_idl"),
];
body.extend(members_body(&fields));
out.push(elixir::Module {
doc: Doc::lines(&[
"An exception, as the structure its members make.",
"",
"A struct and not a defexception: Elixir's defexception requires",
"either a :message field or a message/1 callback, and an IDL",
"exception has neither — its members are whatever the IDL declared.",
"Raising is a run-time decision the RPC arc makes; the shape is the",
"IDL's.",
]),
name,
body,
});
}
Ok(())
}
fn declare_interfaces(
ast: &Ast,
applied: &Applied,
out: &mut Vec<elixir::Module>,
emitted: &mut Vec<(String, String)>,
) -> Result<(), MaterialiseError> {
for (id, node) in ast.interfaces() {
if ghost_created_for(applied, node.node_id, "ElixirBehaviour").is_none() {
continue;
}
let mut name = module_path(ast, node.module)?;
name = name.join(&elixir::module_segment(&node.name));
remember(emitted, &name, &node.name)?;
let mut body = vec![
declared_idl_attribute(ast, node.module, "interface", &node.name)?,
reader("__zd_idl__", "zd_idl"),
];
let mut signatures: Vec<(String, String)> = 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 {
signatures.push((elixir::atom_name(&op.name), op.name.clone()));
let mut params = Vec::new();
for param in &op.params {
params.push(elixir::Param {
name: Some(Ident::new(&elixir::atom_name(¶m.name))),
type_: elixir_member_type(ast, param.type_, ¶m.name)?,
});
}
let result = match op.returns {
Some(returns) => elixir_member_type(ast, returns, &op.name)?,
None => Type::Atom("ok".to_string()),
};
let mut doc = idl_note(&elixir::atom_name(&op.name), &op.name);
if let Some(line) = op.raises_line() {
doc.push(&line);
}
body.push(elixir::Item::Callback(elixir::Signature {
doc,
name: Ident::new(&elixir::atom_name(&op.name)),
params,
result,
}));
}
no_spelling_collision(
signatures
.iter()
.map(|(spelled, declared)| (spelled.as_str(), declared.as_str())),
LANGUAGE,
&node.name,
)?;
out.push(elixir::Module {
doc: Doc::lines(&[
"An interface, as the behaviour its operations and attributes",
"declare — an attribute a get_/set_ pair of callbacks, `readonly`",
"only the first.",
"",
"A module of @callback declarations is what an Elixir behaviour is;",
"an implementing module writes @behaviour and defines them. The",
"transport is the RPC arc's business; the shape is the IDL's.",
"`raises`/`getraises`/`setraises` are documented on the callback",
"they belong to.",
"",
"A derived interface carries its bases' operations and attributes",
"too: a behaviour does not extend another, so IDL 4.2 7.4.3's",
"inherited members are declared here rather than reached through",
"one.",
]),
name,
body,
});
}
Ok(())
}
fn members_body(fields: &[Member]) -> Vec<elixir::Item> {
let mut body = vec![
elixir::Item::Attribute(elixir::Attribute {
doc: Doc::line("Every member, in declaration order, with what Elixir cannot state."),
name: Ident::new("zd_members"),
value: Expr::List(
fields
.iter()
.filter(|field| field.listed)
.map(|field| {
Expr::Tuple(vec![
Expr::Atom(field.atom.clone()),
Expr::Keyword(field.facts.clone()),
])
})
.collect(),
),
}),
reader("__zd_members__", "zd_members"),
];
let enforced: Vec<Expr> = fields
.iter()
.filter(|field| field.enforced)
.map(|field| Expr::Atom(field.atom.clone()))
.collect();
if !enforced.is_empty() {
body.push(elixir::Item::Attribute(elixir::Attribute {
doc: Doc::none(),
name: Ident::new("enforce_keys"),
value: Expr::List(enforced),
}));
}
body.push(elixir::Item::Defstruct(
fields
.iter()
.map(|field| elixir::Field {
name: Ident::new(&field.atom),
default: field.default.clone(),
})
.collect(),
));
body.push(type_alias(
"t",
Type::Struct(
fields
.iter()
.map(|field| (Ident::new(&field.atom), field.type_.clone()))
.collect(),
),
Doc::none(),
));
body
}
fn case_labels(
ast: &Ast,
labels: &[CaseLabel],
at: &str,
) -> Result<Vec<(Ident, Expr)>, MaterialiseError> {
let mut out = Vec::new();
for label in labels {
match label {
CaseLabel::Default => out.push(Expr::Atom("default".to_string())),
CaseLabel::Value(expr) => {
let value =
ast.eval_integral(*expr)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "union case label".to_string(),
at: at.to_string(),
})?;
out.push(Expr::int(value));
}
}
}
Ok(vec![kw("labels", Expr::List(out))])
}
pub fn elixir_member_type(
ast: &Ast,
target: Id<TypeNode>,
at: &str,
) -> Result<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"),
Ok(TypeKind::AnnotationType { .. }) => Some("an annotation type"),
_ => None,
};
if let Some(construct) = construct {
return Err(MaterialiseError::NoLanguageForm {
construct: construct.to_string(),
language: LANGUAGE,
at: at.to_string(),
});
}
elixir_value_type(ast, target, 0)
}
pub fn elixir_value_type(
ast: &Ast,
id: Id<TypeNode>,
depth: usize,
) -> Result<Type, MaterialiseError> {
if depth > MAX_TYPE_DEPTH {
return Err(MaterialiseError::Unevaluated {
construct: format!("a type nested past {MAX_TYPE_DEPTH} levels"),
at: "the member's type".to_string(),
});
}
let Ok(node) = ast.type_(id) else {
return Ok(Type::Basic(Basic::Any));
};
Ok(match &node.kind {
TypeKind::Primitive(kind) => elixir_primitive_type(*kind),
TypeKind::String { .. } | TypeKind::Fixed { .. } => Type::Basic(Basic::Binary),
TypeKind::Sequence { element, .. } => {
let Some(element) = element.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "sequence element".to_string(),
at: named(&node.name, node.node_id),
});
};
Type::list(elixir_value_type(ast, element, depth + 1)?)
}
TypeKind::Array { element, dims } => {
let Some(element) = element.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "array element".to_string(),
at: named(&node.name, node.node_id),
});
};
let mut type_ = elixir_value_type(ast, element, depth + 1)?;
for dim in dims.iter().rev() {
let length =
ast.eval_integral(*dim)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "array length".to_string(),
at: named(&node.name, node.node_id),
})?;
let count = usize::try_from(length).map_err(|_| MaterialiseError::Unevaluated {
construct: "array length as a tuple arity".to_string(),
at: named(&node.name, node.node_id),
})?;
type_ = Type::Tuple(alloc_repeat(&type_, count));
}
type_
}
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),
});
};
Type::list(Type::Tuple(vec![
elixir_value_type(ast, key, depth + 1)?,
elixir_value_type(ast, value, depth + 1)?,
]))
}
TypeKind::Alias { aliased } if node.name.is_empty() => match aliased.resolved() {
Some(target) => elixir_value_type(ast, target, depth + 1)?,
None => Type::Basic(Basic::Any),
},
TypeKind::Enumeration { .. } | TypeKind::Bitmask { .. } => Type::Basic(Basic::Integer),
_ => Type::Remote {
module: type_module_path(ast, id),
name: Ident::new("t"),
},
})
}
#[must_use]
pub fn elixir_primitive_type(kind: PrimitiveKind) -> Type {
Type::Basic(match kind {
PrimitiveKind::Boolean => Basic::Boolean,
PrimitiveKind::Octet
| PrimitiveKind::UInt8
| PrimitiveKind::Int8
| PrimitiveKind::Char
| PrimitiveKind::WChar
| PrimitiveKind::Int16
| PrimitiveKind::UInt16
| PrimitiveKind::Int32
| PrimitiveKind::UInt32
| PrimitiveKind::Int64
| PrimitiveKind::UInt64 => Basic::Integer,
PrimitiveKind::Float32 | PrimitiveKind::Float64 => Basic::Float,
PrimitiveKind::Float128 => Basic::Binary,
})
}
fn named_type(spelling: &str) -> Type {
match spelling.rsplit_once('.') {
Some((module, name)) => Type::Remote {
module: Path::new(&module.split('.').collect::<Vec<&str>>()),
name: Ident::new(name),
},
None => Type::Local(Ident::new(spelling)),
}
}
fn alloc_repeat(type_: &Type, count: usize) -> Vec<Type> {
let mut out = Vec::with_capacity(count);
for _ in 0..count {
out.push(type_.clone());
}
out
}
#[allow(clippy::too_many_lines)]
pub fn type_facts(
ast: &Ast,
id: Id<TypeNode>,
at: &str,
depth: usize,
) -> Result<Vec<(Ident, Expr)>, MaterialiseError> {
if depth > MAX_TYPE_DEPTH {
return Err(MaterialiseError::Unevaluated {
construct: format!("a type nested past {MAX_TYPE_DEPTH} levels"),
at: at.to_string(),
});
}
let Ok(node) = ast.type_(id) else {
return Err(MaterialiseError::Unevaluated {
construct: "a member's type the model does not answer for".to_string(),
at: at.to_string(),
});
};
Ok(match &node.kind {
TypeKind::Primitive(kind) => primitive_facts(*kind),
TypeKind::String { width, bound } => vec![
kw(
"kind",
Expr::Atom(
match width {
CharWidth::Char => "string",
CharWidth::WChar => "wstring",
}
.to_string(),
),
),
kw("bits", Expr::int(char_bits(*width))),
kw("bound", Expr::int(bound_of(ast, *bound, at)?)),
],
TypeKind::Fixed { digits, scale } => vec![
kw("kind", Expr::Atom("fixed".to_string())),
kw("digits", Expr::int(i128::from(*digits))),
kw("scale", Expr::int(i128::from(*scale))),
kw("holder", Expr::Atom("decimal_text".to_string())),
],
TypeKind::Sequence { element, bound } => {
let Some(element) = element.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "sequence element".to_string(),
at: at.to_string(),
});
};
vec![
kw("kind", Expr::Atom("sequence".to_string())),
kw("bound", Expr::int(bound_of(ast, *bound, at)?)),
kw(
"element",
Expr::Keyword(type_facts(ast, 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 lengths = Vec::new();
for dim in dims {
lengths.push(Expr::int(ast.eval_integral(*dim).map_err(|_| {
MaterialiseError::Unevaluated {
construct: "array length".to_string(),
at: at.to_string(),
}
})?));
}
vec![
kw("kind", Expr::Atom("array".to_string())),
kw("dims", Expr::List(lengths)),
kw(
"element",
Expr::Keyword(type_facts(ast, element, at, depth + 1)?),
),
]
}
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(),
});
};
vec![
kw("kind", Expr::Atom("map".to_string())),
kw("bound", Expr::int(bound_of(ast, *bound, at)?)),
kw("key", Expr::Keyword(type_facts(ast, key, at, depth + 1)?)),
kw(
"value",
Expr::Keyword(type_facts(ast, value, at, depth + 1)?),
),
]
}
TypeKind::Enumeration { .. } => vec![
kw("kind", Expr::Atom("enum".to_string())),
kw("module", Expr::Module(type_module_path(ast, id))),
kw("bits", Expr::int(i128::from(ast.effective_bit_bound(id)))),
],
TypeKind::Bitmask { .. } => vec![
kw("kind", Expr::Atom("bitmask".to_string())),
kw("module", Expr::Module(type_module_path(ast, id))),
kw("bits", Expr::int(i128::from(ast.effective_bit_bound(id)))),
kw("signed", Expr::Literal(Literal::Bool(false))),
],
TypeKind::Alias { aliased } => {
let Some(target) = aliased.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "typedef target".to_string(),
at: at.to_string(),
});
};
if node.name.is_empty() {
return type_facts(ast, target, at, depth + 1);
}
vec![
kw("kind", Expr::Atom("alias".to_string())),
kw("module", Expr::Module(type_module_path(ast, id))),
kw(
"target",
Expr::Keyword(type_facts(ast, target, at, depth + 1)?),
),
]
}
TypeKind::Structure { .. } => named_facts(ast, id, "struct"),
TypeKind::Union { .. } => named_facts(ast, id, "union"),
TypeKind::Valuetype { .. } => named_facts(ast, id, "valuetype"),
TypeKind::Bitset { .. } => named_facts(ast, id, "bitset"),
TypeKind::Native => named_facts(ast, id, "native"),
TypeKind::ValueBox { .. } => named_facts(ast, id, "valuebox"),
TypeKind::Any
| TypeKind::ValueBase
| TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. } => {
return Err(MaterialiseError::NoLanguageForm {
construct: "a construct with no Elixir data form".to_string(),
language: LANGUAGE,
at: at.to_string(),
});
}
})
}
fn named_facts(ast: &Ast, id: Id<TypeNode>, kind: &str) -> Vec<(Ident, Expr)> {
vec![
kw("kind", Expr::Atom(kind.to_string())),
kw("module", Expr::Module(type_module_path(ast, id))),
]
}
fn primitive_facts(kind: PrimitiveKind) -> Vec<(Ident, Expr)> {
let signed = |name: &str, bits: i128, signed: bool| {
vec![
kw("kind", Expr::Atom(name.to_string())),
kw("bits", Expr::int(bits)),
kw("signed", Expr::Literal(Literal::Bool(signed))),
]
};
match kind {
PrimitiveKind::Boolean => vec![
kw("kind", Expr::Atom("boolean".to_string())),
kw("bits", Expr::int(8)),
],
PrimitiveKind::Octet => signed("octet", 8, false),
PrimitiveKind::UInt8 => signed("uint8", 8, false),
PrimitiveKind::Int8 => signed("int8", 8, true),
PrimitiveKind::Char => vec![
kw("kind", Expr::Atom("char".to_string())),
kw("bits", Expr::int(8)),
],
PrimitiveKind::WChar => vec![
kw("kind", Expr::Atom("wchar".to_string())),
kw("bits", Expr::int(16)),
],
PrimitiveKind::Int16 => signed("int16", 16, true),
PrimitiveKind::UInt16 => signed("uint16", 16, false),
PrimitiveKind::Int32 => signed("int32", 32, true),
PrimitiveKind::UInt32 => signed("uint32", 32, false),
PrimitiveKind::Int64 => signed("int64", 64, true),
PrimitiveKind::UInt64 => signed("uint64", 64, false),
PrimitiveKind::Float32 => vec![
kw("kind", Expr::Atom("float32".to_string())),
kw("bits", Expr::int(32)),
],
PrimitiveKind::Float64 => vec![
kw("kind", Expr::Atom("float64".to_string())),
kw("bits", Expr::int(64)),
],
PrimitiveKind::Float128 => vec![
kw("kind", Expr::Atom("float128".to_string())),
kw("bits", Expr::int(128)),
kw("holder", Expr::Atom("decimal_text".to_string())),
],
}
}
const fn char_bits(width: CharWidth) -> i128 {
match width {
CharWidth::Char => 8,
CharWidth::WChar => 16,
}
}
fn bound_of(ast: &Ast, bound: Bound, at: &str) -> Result<i128, MaterialiseError> {
match bound.expr() {
None => Ok(0),
Some(expr) => ast
.eval_integral(expr)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "bound".to_string(),
at: at.to_string(),
}),
}
}
fn default_for(
ast: &Ast,
id: Id<TypeNode>,
depth: usize,
) -> Result<Option<Expr>, MaterialiseError> {
if depth > MAX_TYPE_DEPTH {
return Ok(None);
}
let Ok(node) = ast.type_(id) else {
return Ok(None);
};
Ok(match &node.kind {
TypeKind::Primitive(kind) => Some(match kind {
PrimitiveKind::Boolean => Expr::Literal(Literal::Bool(false)),
PrimitiveKind::Float32 | PrimitiveKind::Float64 => {
Expr::Literal(Literal::Float("0.0".to_string()))
}
PrimitiveKind::Float128 => Expr::string("0"),
_ => Expr::int(0),
}),
TypeKind::String { .. } | TypeKind::Fixed { .. } => Some(Expr::string("")),
TypeKind::Sequence { .. } | TypeKind::Map { .. } => Some(Expr::List(Vec::new())),
TypeKind::Array { element, dims } => {
let Some(element) = element.resolved() else {
return Ok(None);
};
let Some(inner) = default_for(ast, element, depth + 1)? else {
return Ok(None);
};
let mut value = inner;
for dim in dims.iter().rev() {
let length =
ast.eval_integral(*dim)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "array length".to_string(),
at: named(&node.name, node.node_id),
})?;
let count = usize::try_from(length).map_err(|_| MaterialiseError::Unevaluated {
construct: "array length as a tuple arity".to_string(),
at: named(&node.name, node.node_id),
})?;
let mut items = Vec::with_capacity(count);
for _ in 0..count {
items.push(value.clone());
}
value = Expr::Tuple(items);
}
Some(value)
}
TypeKind::Enumeration { .. } | TypeKind::Bitmask { .. } => Some(Expr::int(0)),
TypeKind::Alias { aliased } => match aliased.resolved() {
Some(target) => default_for(ast, target, depth + 1)?,
None => None,
},
_ => None,
})
}
#[must_use]
pub fn type_module_path(ast: &Ast, id: Id<TypeNode>) -> Path {
let Ok(any) = ast.any_of_type(id) else {
return Path::single("ZdUnnamed");
};
let Some(scoped) = ast.scoped_name_of(any) else {
return Path::single("ZdUnnamed");
};
let segments: Vec<String> = scoped
.segments()
.iter()
.map(|segment| elixir::module_segment(segment))
.collect();
if segments.is_empty() {
return Path::single("ZdUnnamed");
}
Path::new(&segments.iter().map(String::as_str).collect::<Vec<&str>>())
}
fn module_path(ast: &Ast, module: Id<ModuleNode>) -> Result<Path, MaterialiseError> {
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 {
return Err(MaterialiseError::NoModule {
at: module_handle(module),
});
}
depth += 1;
let node = ast.module(id).map_err(|_| MaterialiseError::NoModule {
at: module_handle(id),
})?;
if node.parent.is_none() || node.name.is_empty() {
break;
}
segments.push(elixir::module_segment(&node.name));
current = node.parent;
}
segments.reverse();
Ok(Path::new(
&segments.iter().map(String::as_str).collect::<Vec<&str>>(),
))
}
fn module_handle(module: Id<ModuleNode>) -> String {
format!("#{}@{}", module.slot(), module.generation())
}
fn dotted(path: &Path) -> String {
path.segments()
.iter()
.map(Ident::as_str)
.collect::<Vec<&str>>()
.join(".")
}
fn remember(
emitted: &mut Vec<(String, String)>,
name: &Path,
declared: &str,
) -> Result<(), MaterialiseError> {
let spelled = dotted(name);
if let Some((_, first)) = emitted.iter().find(|(other, _)| *other == spelled) {
return Err(MaterialiseError::SpellingCollision {
first: first.clone(),
second: declared.to_string(),
spelled,
language: LANGUAGE,
at: "the generated file".to_string(),
});
}
emitted.push((spelled, declared.to_string()));
Ok(())
}
fn named(name: &str, node: NodeId) -> String {
if name.is_empty() {
return format!("an unnamed type (#{node:?})");
}
name.to_string()
}
fn kw(key: &str, value: Expr) -> (Ident, Expr) {
(Ident::new(key), value)
}
fn idl_attribute(ast: &Ast, id: Id<TypeNode>, kind: &str, declared: &str) -> elixir::Item {
idl_attribute_with(ast, id, kind, declared, Vec::new())
}
fn idl_attribute_with(
ast: &Ast,
id: Id<TypeNode>,
kind: &str,
declared: &str,
extra: Vec<(Ident, Expr)>,
) -> elixir::Item {
let mut facts = vec![
kw("name", Expr::string(declared)),
kw("scoped", Expr::string(&scoped_idl_name(ast, id, declared))),
kw("kind", Expr::Atom(kind.to_string())),
];
facts.extend(extra);
elixir::Item::Attribute(elixir::Attribute {
doc: Doc::line(
"The declaration's own IDL spelling, which the module name does not invert.",
),
name: Ident::new("zd_idl"),
value: Expr::Keyword(facts),
})
}
fn declared_idl_attribute(
ast: &Ast,
module: Id<ModuleNode>,
kind: &str,
declared: &str,
) -> Result<elixir::Item, MaterialiseError> {
let mut scoped = String::from("::");
for segment in idl_module_chain(ast, module)? {
scoped.push_str(&segment);
scoped.push_str("::");
}
scoped.push_str(declared);
Ok(elixir::Item::Attribute(elixir::Attribute {
doc: Doc::line(
"The declaration's own IDL spelling, which the module name does not invert.",
),
name: Ident::new("zd_idl"),
value: Expr::Keyword(vec![
kw("name", Expr::string(declared)),
kw("scoped", Expr::string(&scoped)),
kw("kind", Expr::Atom(kind.to_string())),
]),
}))
}
fn idl_module_chain(ast: &Ast, module: Id<ModuleNode>) -> Result<Vec<String>, MaterialiseError> {
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 {
return Err(MaterialiseError::NoModule {
at: module_handle(module),
});
}
depth += 1;
let node = ast.module(id).map_err(|_| MaterialiseError::NoModule {
at: module_handle(id),
})?;
if node.parent.is_none() || node.name.is_empty() {
break;
}
segments.push(node.name.clone());
current = node.parent;
}
segments.reverse();
Ok(segments)
}
fn scoped_idl_name(ast: &Ast, id: Id<TypeNode>, declared: &str) -> String {
let Ok(any) = ast.any_of_type(id) else {
return declared.to_string();
};
let Some(scoped) = ast.scoped_name_of(any) else {
return declared.to_string();
};
let mut out = String::from("::");
out.push_str(&scoped.segments().join("::"));
out
}
fn reader(function: &str, attribute: &str) -> elixir::Item {
elixir::Item::Function(elixir::Function {
doc: Doc::none(),
name: Ident::new(function),
params: Vec::new(),
body: Expr::Attr(Ident::new(attribute)),
})
}
fn type_alias(name: &str, type_: Type, doc: Doc) -> elixir::Item {
elixir::Item::TypeAlias(elixir::TypeAlias {
doc,
name: Ident::new(name),
type_,
})
}
fn idl_note(spelled: &str, declared: &str) -> Doc {
if spelled == declared {
return Doc::none();
}
Doc::line(&format!("IDL: `{declared}`."))
}
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 elixir_constant(
ast: &Ast,
value: &zerodds_ast::ConstValue,
declared: Option<Id<TypeNode>>,
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(),
};
let is_binary = declared.is_some_and(|id| {
matches!(
elixir_value_type(ast, id, 0),
Ok(Type::Basic(Basic::Binary))
)
});
Ok(match value {
ConstValue::Boolean(flag) => Expr::Literal(Literal::Bool(*flag)),
ConstValue::Integer(number) => Expr::int(*number),
ConstValue::Char(character) => Expr::int(i128::from(u32::from(*character))),
ConstValue::WChar(point) => Expr::int(i128::from(*point)),
ConstValue::Float { mantissa, exponent } => {
if is_binary {
return Err(refuse(
"a `long double` constant, which has no Elixir literal",
));
}
Expr::Literal(Literal::Float(constant::float_text(
mantissa,
exponent.as_deref(),
)))
}
ConstValue::Fixed { digits, scale } => {
Expr::string(&constant::fixed_decimal(digits, *scale))
}
ConstValue::String(text) => Expr::string(text),
ConstValue::WString(points) => Expr::string(
&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")),
})
}