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, CharWidth, PrimitiveKind, TypeKind, TypeNode};
use zerodds_ast_core::{Id, NodeId};
use zerodds_lang_cst::common::{Doc, Ident, Literal};
use zerodds_lang_cst::ocaml;
const LANGUAGE: &str = "OCaml";
const INVENTED_PREFIX: &str = "zd_map_entry_";
pub const DISCRIMINATOR: &str = "_d";
const MAX_MODULE_DEPTH: usize = 64;
const MAX_ALIAS_DEPTH: usize = 64;
pub fn ocaml_items(ast: &Ast, applied: &Applied) -> Result<Vec<ocaml::Decl>, MaterialiseError> {
let mut out = Vec::new();
let mut names = Names::default();
let declared = declared_types(ast, applied);
for component in type_groups(ast, &declared) {
let start = out.len();
let mut items = Vec::new();
let mut trailing = Vec::new();
for index in &component {
let Some(id) = declared.get(*index).copied() else {
continue;
};
let built = build_type(ast, applied, id, &mut names)?;
items.push(built.decl);
trailing.extend(built.bindings);
}
if items.is_empty() {
continue;
}
let recursive = items.len() > 1 || self_referential(ast, &declared, &component);
if recursive
&& !items.iter().any(|item| {
matches!(
item.body,
ocaml::TypeBody::Record(_) | ocaml::TypeBody::Variant(_)
)
})
{
let spelled: Vec<&str> = items.iter().map(|item| item.name.as_str()).collect();
return Err(MaterialiseError::NoLanguageForm {
construct: format!(
"a cycle through type abbreviations alone ({}) โ OCaml expands an \
abbreviation and reports the cycle rather than accepting it",
spelled.join(", ")
),
language: LANGUAGE,
at: spelled
.first()
.map_or_else(String::new, |name| (*name).to_string()),
});
}
out.push(ocaml::Decl::Types(ocaml::TypeGroup {
doc: if recursive {
Doc::lines(&[
"A recursive group. OCaml resolves a type name against what precedes",
"it and against its own group, so a cycle in the IDL type graph is one",
"declaration joined by `and` and cannot be written any other way.",
])
} else {
Doc::none()
},
items,
}));
out.extend(trailing.into_iter().map(ocaml::Decl::Let));
let ids: Vec<Id<TypeNode>> = component
.iter()
.filter_map(|index| declared.get(*index).copied())
.collect();
weave_group(ast, &ids, start, &mut out);
}
for (_, node) in ast.exceptions() {
if ghost_created_for(applied, node.node_id, "OCamlError").is_none() {
continue;
}
let name = declared_name(ast, &node.name, node.module);
remember(&mut names.types, &name, &node.name)?;
let mut pairs = Vec::new();
for member in &node.members {
if let Ok(member_node) = ast.member(*member) {
pairs.push((
ocaml::lower_ident(&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(),
});
};
fields.push(ocaml::Field {
doc: member_doc(ast, target, &member_node.name, false),
name: Ident::new(&ocaml::lower_ident(&member_node.name)),
type_: ocaml_member_type(ast, target, &member_node.name)?,
});
}
out.push(ocaml::Decl::Types(ocaml::TypeGroup::one(ocaml::TypeDecl {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: with_idl_note(
Doc::line("An exception, as the record its members make."),
&name,
&node.name,
),
name: Ident::new(&name),
body: record_or_unit(fields),
})));
}
for (_, node) in ast.constants() {
if ghost_created_for(applied, node.node_id, "OCamlConst").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_type = node.type_.resolved();
let name = declared_name(ast, &node.name, node.module);
remember(&mut names.values, &name, &node.name)?;
let annotation = match declared_type {
Some(target) => Some(ocaml_value_type(ast, target)?),
None => None,
};
out.push(ocaml::Decl::Let(ocaml::LetDecl {
doc: idl_note(&name, &node.name),
name: Ident::new(&name),
type_: annotation,
body: ocaml_constant(ast, &value, declared_type, &node.name)?,
}));
}
for (id, node) in ast.interfaces() {
if ghost_created_for(applied, node.node_id, "OCamlInterface").is_none() {
continue;
}
let name = declared_name(ast, &node.name, node.module);
remember(&mut names.types, &name, &node.name)?;
let mut methods = Vec::new();
let mut signatures = 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((ocaml::lower_ident(&op.name), op.name.clone()));
let mut params = Vec::new();
for param in &op.params {
params.push(ocaml_member_type(ast, param.type_, ¶m.name)?);
}
let result = match op.returns {
Some(returns) => ocaml_member_type(ast, returns, &op.name)?,
None => ocaml::Type::Primitive(ocaml::Primitive::Unit),
};
let doc = op
.raises_line()
.map_or_else(Doc::none, |line| Doc::line(&line));
methods.push(ocaml::Method {
doc,
name: Ident::new(&ocaml::lower_ident(&op.name)),
params,
result,
});
}
no_spelling_collision(as_pairs(&signatures), LANGUAGE, &name)?;
out.push(ocaml::Decl::ClassType(ocaml::ClassTypeDecl {
doc: with_idl_note(
Doc::lines(&[
"An interface, as the class type its operations and attributes",
"make โ an attribute a get_/set_ pair of methods, `readonly` only",
"the first. A method type has no place for a direction, so an",
"`out` or `inout` parameter is an argument here like any other;",
"the direction is in the model and the RPC arc reads it there.",
"`raises`/`getraises`/`setraises` are documented on the method",
"they belong to.",
"",
"A derived interface carries its bases' operations and attributes",
"too: there is no `inherit` clause here, so IDL 4.2 7.4.3's",
"inherited members are written into the class type rather than",
"reached through one.",
]),
&name,
&node.name,
),
name: Ident::new(&name),
methods,
}));
}
Ok(verbatim::weave_file(ast, verbatim::aliases::OCAML, out))
}
fn weave_group(ast: &Ast, ids: &[Id<TypeNode>], start: usize, out: &mut Vec<ocaml::Decl>) {
let mut merged = verbatim::Slots::default();
for id in ids {
let mut slots = verbatim::declaration_slots(ast, *id, verbatim::aliases::OCAML);
if slots.is_empty() {
continue;
}
let name = ocaml_type_name(ast, *id);
if let Some(ocaml::Decl::Types(group)) = out.get_mut(start) {
if let Some(record) = group.items.iter_mut().find(|item| {
item.name.as_str() == name && matches!(item.body, ocaml::TypeBody::Record(_))
}) {
record.body_text.begin.append(&mut slots.begin);
record.body_text.end.append(&mut slots.end);
}
}
merged.before.append(&mut slots.before);
merged.begin.append(&mut slots.begin);
merged.end.append(&mut slots.end);
merged.after.append(&mut slots.after);
}
verbatim::weave_with(merged, start, out, ocaml::Decl::Verbatim, |_, _, _| {});
}
impl verbatim::Carrier for ocaml::Decl {
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 ocaml_file(ast: &Ast, applied: &Applied) -> Result<ocaml::File, MaterialiseError> {
Ok(ocaml::File {
doc: Doc::lines(&[
"Generated by ZeroDDS from an IDL source. Do not edit.",
"",
"OCaml has no attribute a program reads back at run time, so the IDL",
"spelling of every renamed declaration and every renamed member is in",
"the comment beside it. 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 case",
"fold nor the flattening of a scoped name inverts.",
]),
decls: ocaml_items(ast, applied)?,
})
}
#[derive(Default)]
struct Names {
types: Vec<(String, String)>,
values: Vec<(String, String)>,
}
struct Built {
decl: ocaml::TypeDecl,
bindings: Vec<ocaml::LetDecl>,
}
fn declared_types(ast: &Ast, applied: &Applied) -> Vec<Id<TypeNode>> {
let mut out = Vec::new();
let mut map_names: Vec<String> = Vec::new();
for (id, node) in ast.types() {
let created = match &node.kind {
TypeKind::Map { .. } => {
let name = ocaml_map_entry_name(ast, id);
if map_names.contains(&name) {
continue;
}
map_names.push(name);
out.push(id);
continue;
}
TypeKind::Alias { .. } if node.name.is_empty() => continue,
TypeKind::Alias { .. } => "OCamlAlias",
TypeKind::Enumeration { .. } => "OCamlVariant",
TypeKind::Bitmask { .. } => "OCamlBitmask",
TypeKind::Bitset { .. } => "OCamlBitset",
TypeKind::Structure { .. } => "OCamlRecord",
TypeKind::Valuetype { .. } => "OCamlValue",
TypeKind::Union { .. } => "OCamlUnion",
TypeKind::Native => "OCamlNative",
TypeKind::ValueBox { boxed } => {
if boxed.resolved().is_some() {
out.push(id);
}
continue;
}
TypeKind::Primitive(_)
| TypeKind::String { .. }
| TypeKind::Fixed { .. }
| TypeKind::Sequence { .. }
| TypeKind::Array { .. }
| TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. }
| TypeKind::Any
| TypeKind::ValueBase => continue,
};
if ghost_created_for(applied, node.node_id, created).is_some() {
out.push(id);
}
}
out
}
#[must_use]
fn type_groups(ast: &Ast, declared: &[Id<TypeNode>]) -> Vec<Vec<usize>> {
let count = declared.len();
let edges: Vec<Vec<usize>> = (0..count)
.map(|index| {
let mut targets = Vec::new();
if let Some(id) = declared.get(index) {
for reference in body_references(ast, *id) {
if let Some(other) = index_of(ast, declared, reference) {
if !targets.contains(&other) {
targets.push(other);
}
}
}
}
targets
})
.collect();
const UNVISITED: usize = usize::MAX;
let mut order = vec![UNVISITED; count];
let mut low = vec![0_usize; count];
let mut on_stack = vec![false; count];
let mut stack: Vec<usize> = Vec::new();
let mut next = 0_usize;
let mut out: Vec<Vec<usize>> = Vec::new();
for root in 0..count {
if order.get(root).copied() != Some(UNVISITED) {
continue;
}
let mut calls: Vec<(usize, usize)> = Vec::new();
set(&mut order, root, next);
set(&mut low, root, next);
next += 1;
stack.push(root);
set(&mut on_stack, root, true);
calls.push((root, 0));
while let Some((node, cursor)) = calls.last().copied() {
let child = edges.get(node).and_then(|list| list.get(cursor)).copied();
if let Some(child) = child {
if let Some(last) = calls.last_mut() {
last.1 = cursor + 1;
}
if order.get(child).copied() == Some(UNVISITED) {
set(&mut order, child, next);
set(&mut low, child, next);
next += 1;
stack.push(child);
set(&mut on_stack, child, true);
calls.push((child, 0));
} else if on_stack.get(child).copied() == Some(true) {
let candidate = order.get(child).copied().unwrap_or(UNVISITED);
let current = low.get(node).copied().unwrap_or(UNVISITED);
set(&mut low, node, current.min(candidate));
}
continue;
}
calls.pop();
if low.get(node).copied() == order.get(node).copied() {
let mut component = Vec::new();
while let Some(top) = stack.pop() {
set(&mut on_stack, top, false);
component.push(top);
if top == node {
break;
}
}
component.sort_unstable();
out.push(component);
}
if let Some((parent, _)) = calls.last().copied() {
let child_low = low.get(node).copied().unwrap_or(UNVISITED);
let parent_low = low.get(parent).copied().unwrap_or(UNVISITED);
set(&mut low, parent, parent_low.min(child_low));
}
}
}
out
}
fn set<T>(slots: &mut [T], index: usize, value: T) {
if let Some(slot) = slots.get_mut(index) {
*slot = value;
}
}
fn self_referential(ast: &Ast, declared: &[Id<TypeNode>], component: &[usize]) -> bool {
let [only] = component else {
return false;
};
let Some(id) = declared.get(*only).copied() else {
return false;
};
body_references(ast, id)
.into_iter()
.any(|reference| index_of(ast, declared, reference) == Some(*only))
}
fn body_references(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::Structure { .. } | TypeKind::Valuetype { .. } => {
if let Some(base) = super::c::struct_base(ast, id) {
out.extend(value_references(ast, base, 0));
}
for member in ast.all_members_of(id) {
if let Ok(member_node) = ast.member(member) {
if let Some(target) = member_node.type_.resolved() {
out.extend(value_references(ast, target, 0));
}
}
}
}
TypeKind::Union { discriminator, .. } => {
if let Some(target) = discriminator.resolved() {
out.extend(value_references(ast, target, 0));
}
for case in ast.cases_of(id) {
if let Ok(case_node) = ast.case(*case) {
if let Ok(member) = ast.member(case_node.member) {
if let Some(target) = member.type_.resolved() {
out.extend(value_references(ast, target, 0));
}
}
}
}
}
TypeKind::Alias { aliased } => {
if let Some(target) = aliased.resolved() {
out.extend(value_references(ast, target, 0));
}
}
TypeKind::ValueBox { boxed } => {
if let Some(target) = boxed.resolved() {
out.extend(value_references(ast, target, 0));
}
}
TypeKind::Map { key, value, .. } => {
if let Some(target) = key.resolved() {
out.extend(value_references(ast, target, 0));
}
if let Some(target) = value.resolved() {
out.extend(value_references(ast, target, 0));
}
}
TypeKind::Primitive(_)
| TypeKind::String { .. }
| TypeKind::Fixed { .. }
| TypeKind::Sequence { .. }
| TypeKind::Array { .. }
| TypeKind::Enumeration { .. }
| TypeKind::Bitmask { .. }
| TypeKind::Bitset { .. }
| TypeKind::Native
| TypeKind::Any
| TypeKind::ValueBase
| TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. } => {}
}
out
}
fn value_references(ast: &Ast, id: Id<TypeNode>, depth: usize) -> Vec<Id<TypeNode>> {
if depth > MAX_ALIAS_DEPTH {
return Vec::new();
}
let Ok(node) = ast.type_(id) else {
return Vec::new();
};
match &node.kind {
TypeKind::Sequence { element, .. } | TypeKind::Array { element, .. } => element
.resolved()
.map(|target| value_references(ast, target, depth + 1))
.unwrap_or_default(),
TypeKind::Alias { aliased } if node.name.is_empty() => aliased
.resolved()
.map(|target| value_references(ast, target, depth + 1))
.unwrap_or_default(),
TypeKind::Primitive(_)
| TypeKind::String { .. }
| TypeKind::Fixed { .. }
| TypeKind::Any
| TypeKind::ValueBase
| TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. } => Vec::new(),
_ => vec![id],
}
}
fn index_of(ast: &Ast, declared: &[Id<TypeNode>], id: Id<TypeNode>) -> Option<usize> {
if let Some(index) = declared.iter().position(|other| *other == id) {
return Some(index);
}
if matches!(
ast.type_(id).map(|node| &node.kind),
Ok(TypeKind::Map { .. })
) {
let name = ocaml_map_entry_name(ast, id);
return declared
.iter()
.position(|other| ocaml_map_entry_name(ast, *other) == name);
}
None
}
#[allow(clippy::too_many_lines)]
fn build_type(
ast: &Ast,
applied: &Applied,
id: Id<TypeNode>,
names: &mut Names,
) -> Result<Built, MaterialiseError> {
let name = ocaml_type_name(ast, id);
let Ok(node) = ast.type_(id) else {
remember(&mut names.types, &name, &name)?;
return Ok(Built {
decl: ocaml::TypeDecl {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: Doc::none(),
name: Ident::new(&name),
body: ocaml::TypeBody::Alias(ocaml::Type::Primitive(ocaml::Primitive::Unit)),
},
bindings: Vec::new(),
});
};
match &node.kind {
TypeKind::Map { key, value, .. } => {
let name = ocaml_map_entry_name(ast, id);
remember(&mut names.types, &name, &name)?;
let (Some(key), Some(value)) = (key.resolved(), value.resolved()) else {
return Err(MaterialiseError::Unevaluated {
construct: "map key or value".to_string(),
at: name,
});
};
Ok(Built {
decl: ocaml::TypeDecl {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: Doc::lines(&[
"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),
body: ocaml::TypeBody::Record(vec![
ocaml::Field {
doc: Doc::none(),
name: Ident::new("key"),
type_: ocaml_member_type(ast, key, &name)?,
},
ocaml::Field {
doc: Doc::none(),
name: Ident::new("value"),
type_: ocaml_member_type(ast, value, &name)?,
},
]),
},
bindings: Vec::new(),
})
}
TypeKind::Alias { aliased } => {
remember(&mut names.types, &name, &node.name)?;
let Some(target) = aliased.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "typedef target".to_string(),
at: node.name.clone(),
});
};
Ok(Built {
decl: ocaml::TypeDecl {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: array_note(ast, target, idl_note(&name, &node.name)),
name: Ident::new(&name),
body: ocaml::TypeBody::Alias(ocaml_member_type(ast, target, &node.name)?),
},
bindings: Vec::new(),
})
}
TypeKind::Enumeration { enumerators } => {
remember(&mut names.types, &name, &node.name)?;
let prefix = module_prefix(ast, id);
let mut constructors = Vec::new();
let mut values = Vec::new();
let mut pairs = Vec::new();
for enumerator in enumerators {
let Ok(enum_node) = ast.enumerator(*enumerator) else {
continue;
};
if ghost_created_for(applied, enum_node.node_id, "OCamlConstructor").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 spelled = ocaml::upper_ident(&format!("{prefix}{}", enum_node.name));
pairs.push((spelled.clone(), enum_node.name.clone()));
constructors.push(ocaml::Constructor {
doc: idl_note(&spelled, &enum_node.name),
name: Ident::new(&spelled),
args: Vec::new(),
});
values.push((spelled, value));
}
no_spelling_collision(as_pairs(&pairs), LANGUAGE, &name)?;
if constructors.is_empty() {
return Ok(Built {
decl: ocaml::TypeDecl {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: with_idl_note(
Doc::line(
"An enumeration with no enumerator, so no value: abstract is \
the OCaml type with none.",
),
&name,
&node.name,
),
name: Ident::new(&name),
body: ocaml::TypeBody::Abstract,
},
bindings: Vec::new(),
});
}
let bindings = enum_conversions(&name, &values);
for binding in &bindings {
remember(&mut names.values, binding.name.as_str(), &node.name)?;
}
Ok(Built {
decl: ocaml::TypeDecl {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: with_idl_note(
Doc::lines(&[
"An enumeration. A variant constructor carries no number, so the",
"numbering DDS-XTypes 1.3 7.4.3.5.3 puts on the wire is in the two",
"conversions below and nowhere else.",
]),
&name,
&node.name,
),
name: Ident::new(&name),
body: ocaml::TypeBody::Variant(constructors),
},
bindings,
})
}
TypeKind::Bitmask { bits } => {
remember(&mut names.types, &name, &node.name)?;
let bound = ast.effective_bit_bound(id);
let holder = ocaml_bitmask_holder(bound);
let prefix = module_prefix(ast, id);
let mut bindings = 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, "OCamlBitLet").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 spelled = ocaml::lower_ident(&format!("{prefix}{}", bit_node.name));
remember(&mut names.values, &spelled, &bit_node.name)?;
let mask =
one_shifted(position, holder).ok_or_else(|| MaterialiseError::Unevaluated {
construct: format!(
"bit position {position}, which does not fit the @bit_bound({bound}) \
holder"
),
at: bit_node.name.clone(),
})?;
bindings.push(ocaml::LetDecl {
doc: with_idl_note(
Doc::line(&format!("Bit {position}.")),
&spelled,
&bit_node.name,
),
name: Ident::new(&spelled),
type_: Some(ocaml::Type::named(&name)),
body: ocaml::Expr::Literal(holder_literal(mask, holder)),
});
}
Ok(Built {
decl: ocaml::TypeDecl {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: with_idl_note(
Doc::lines(&[
"A bitmask. OCaml has no unsigned integer, so Table 33 of",
"DDS-XTypes 1.3 7.4.1.1.3.2 gives the width and the sign is lost:",
"a bound of 32 with bit 31 set reads back negative.",
]),
&name,
&node.name,
),
name: Ident::new(&name),
body: ocaml::TypeBody::Alias(ocaml::Type::Primitive(holder)),
},
bindings,
})
}
TypeKind::Bitset { .. } => {
remember(&mut names.types, &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((ocaml::lower_ident(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;
};
let spelled = pairs
.get(index)
.map_or_else(String::new, |pair| pair.0.clone());
index += 1;
fields.push(ocaml::Field {
doc: idl_note(&spelled, field_ident),
name: Ident::new(&spelled),
type_: ocaml::Type::Primitive(ocaml_holder(field_node.bits)),
});
}
Ok(Built {
decl: ocaml::TypeDecl {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: with_idl_note(
Doc::lines(&[
"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),
body: record_or_unit(fields),
},
bindings: Vec::new(),
})
}
TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
remember(&mut names.types, &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((ocaml::lower_ident("parent"), "parent".to_string()));
}
for member in &members {
if let Ok(member_node) = ast.member(*member) {
pairs.push((
ocaml::lower_ident(&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(ocaml::Field {
doc: Doc::line(
"The base structure, the first member (XTypes 1.3 7.5.1.4.1.3).",
),
name: Ident::new(&ocaml::lower_ident("parent")),
type_: ocaml_member_type(ast, base, "parent")?,
});
}
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 optional = member_node.facets.is_optional();
let mut type_ = ocaml_member_type(ast, target, &member_node.name)?;
if optional {
type_ = ocaml::Type::option(type_);
}
fields.push(ocaml::Field {
doc: member_doc(ast, target, &member_node.name, optional),
name: Ident::new(&ocaml::lower_ident(&member_node.name)),
type_,
});
}
Ok(Built {
decl: ocaml::TypeDecl {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: idl_note(&name, &node.name),
name: Ident::new(&name),
body: record_or_unit(fields),
},
bindings: Vec::new(),
})
}
TypeKind::Union { discriminator, .. } => {
remember(&mut names.types, &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((ocaml::lower_ident(&member.name), member.name.clone()));
}
}
}
no_spelling_collision(as_pairs(&pairs), LANGUAGE, &name)?;
let mut fields = vec![ocaml::Field {
doc: Doc::line("Which branch carries the value."),
name: Ident::new(DISCRIMINATOR),
type_: ocaml_member_type(ast, disc, &name)?,
}];
for case in cases {
let Ok(case_node) = ast.case(*case) else {
continue;
};
if ghost_created_for(applied, case_node.node_id, "OCamlCase").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(),
});
};
fields.push(ocaml::Field {
doc: with_idl_note(
Doc::line("`None` unless the discriminator selects this branch."),
&ocaml::lower_ident(&member.name),
&member.name,
),
name: Ident::new(&ocaml::lower_ident(&member.name)),
type_: ocaml::Type::option(ocaml_member_type(ast, target, &member.name)?),
});
}
Ok(Built {
decl: ocaml::TypeDecl {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: with_idl_note(
Doc::lines(&[
"A union, as a record and not a variant. An IDL discriminator may",
"hold a value that selects no branch at all (IDL 4.2 7.4.1.4.4.1),",
"and a variant has no constructor for that state; only the selected",
"branch is written to the wire (DDS-XTypes 1.3 7.4.1.1.5.2).",
]),
&name,
&node.name,
),
name: Ident::new(&name),
body: ocaml::TypeBody::Record(fields),
},
bindings: Vec::new(),
})
}
TypeKind::Native => {
remember(&mut names.types, &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());
Ok(Built {
decl: ocaml::TypeDecl {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: with_idl_note(
Doc::lines(&[
"A native type. Opaque unless a facet named its mapping: `Obj.t`",
"carries a value of any shape and nothing may be read out of it",
"except by code that knows what was put in.",
]),
&name,
&node.name,
),
name: Ident::new(&name),
body: ocaml::TypeBody::Alias(match mapping {
Some(spelling) => ocaml::Type::named(&spelling),
None => ocaml::Type::qualified("Obj", "t"),
}),
},
bindings: Vec::new(),
})
}
TypeKind::ValueBox { boxed } => {
remember(&mut names.types, &name, &node.name)?;
let Some(target) = boxed.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "boxed value target".to_string(),
at: node.name.clone(),
});
};
Ok(Built {
decl: ocaml::TypeDecl {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: with_idl_note(
Doc::line("A boxed value: `None` when absent."),
&name,
&node.name,
),
name: Ident::new(&name),
body: ocaml::TypeBody::Alias(ocaml::Type::option(ocaml_member_type(
ast, target, &name,
)?)),
},
bindings: Vec::new(),
})
}
_ => {
remember(&mut names.types, &name, &node.name)?;
Ok(Built {
decl: ocaml::TypeDecl {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: Doc::line("A construct with no OCaml declaration of its own."),
name: Ident::new(&name),
body: ocaml::TypeBody::Alias(ocaml::Type::Primitive(ocaml::Primitive::Unit)),
},
bindings: Vec::new(),
})
}
}
}
fn record_or_unit(fields: Vec<ocaml::Field>) -> ocaml::TypeBody {
if fields.is_empty() {
return ocaml::TypeBody::Alias(ocaml::Type::Primitive(ocaml::Primitive::Unit));
}
ocaml::TypeBody::Record(fields)
}
fn enum_conversions(name: &str, values: &[(String, i128)]) -> Vec<ocaml::LetDecl> {
let forward = values
.iter()
.map(|(constructor, value)| ocaml::Arm {
pattern: ocaml::Pattern::Construct(Ident::new(constructor)),
body: ocaml::Expr::Literal(ocaml::int32_literal(*value)),
})
.collect();
let mut backward: Vec<ocaml::Arm> = values
.iter()
.map(|(constructor, value)| ocaml::Arm {
pattern: ocaml::Pattern::Literal(ocaml::int32_literal(*value)),
body: ocaml::Expr::construct(constructor),
})
.collect();
backward.push(ocaml::Arm {
pattern: ocaml::Pattern::Bind(Ident::new("value")),
body: ocaml::Expr::apply(
ocaml::Expr::name("invalid_arg"),
vec![ocaml::Expr::Paren(Box::new(ocaml::Expr::apply(
ocaml::Expr::name("Printf.sprintf"),
vec![
ocaml::Expr::Literal(Literal::string(&format!("{name}_of_int32: %ld"))),
ocaml::Expr::name("value"),
],
)))],
),
});
vec![
ocaml::LetDecl {
doc: Doc::line(&format!(
"The wire value of a `{name}`, which the variant itself does not carry."
)),
name: Ident::new(&ocaml::lower_ident(&format!("int32_of_{name}"))),
type_: None,
body: ocaml::Expr::Function(forward),
},
ocaml::LetDecl {
doc: Doc::lines(&[
&format!("The `{name}` a wire value names."),
"Raises `Invalid_argument` for a value that names no enumerator: a peer",
"sending one disagrees with this IDL about the type, and returning `None`",
"would be a second kind of absence beside `@optional`'s.",
]),
name: Ident::new(&ocaml::lower_ident(&format!("{name}_of_int32"))),
type_: None,
body: ocaml::Expr::Function(backward),
},
]
}
pub fn ocaml_member_type(
ast: &Ast,
target: Id<TypeNode>,
at: &str,
) -> Result<ocaml::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(),
});
}
ocaml_value_type(ast, target)
}
pub fn ocaml_value_type(ast: &Ast, id: Id<TypeNode>) -> Result<ocaml::Type, MaterialiseError> {
value_type_at(ast, id, 0)
}
fn value_type_at(
ast: &Ast,
id: Id<TypeNode>,
depth: usize,
) -> Result<ocaml::Type, MaterialiseError> {
if depth > MAX_ALIAS_DEPTH {
return Err(MaterialiseError::Unevaluated {
construct: format!("type, whose OCaml spelling nests past {MAX_ALIAS_DEPTH}"),
at: ocaml_type_name(ast, id),
});
}
let Ok(node) = ast.type_(id) else {
return Ok(ocaml::Type::Primitive(ocaml::Primitive::Unit));
};
Ok(match &node.kind {
TypeKind::Primitive(kind) => ocaml_primitive_type(*kind),
TypeKind::String { .. } | TypeKind::Fixed { .. } => {
ocaml::Type::Primitive(ocaml::Primitive::String)
}
TypeKind::Sequence { element, .. } => {
let Some(element) = element.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "sequence element".to_string(),
at: node.name.clone(),
});
};
ocaml::Type::array(value_type_at(ast, element, depth + 1)?)
}
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_at(ast, element, depth + 1)?;
for _ in dims {
type_ = ocaml::Type::array(type_);
}
type_
}
TypeKind::Map { .. } => {
ocaml::Type::array(ocaml::Type::named(&ocaml_map_entry_name(ast, id)))
}
TypeKind::Alias { aliased } if node.name.is_empty() => match aliased.resolved() {
Some(target) => value_type_at(ast, target, depth + 1)?,
None => ocaml::Type::Primitive(ocaml::Primitive::Unit),
},
_ => ocaml::Type::named(&ocaml_type_name(ast, id)),
})
}
#[must_use]
pub fn ocaml_primitive_type(kind: PrimitiveKind) -> ocaml::Type {
let primitive = match kind {
PrimitiveKind::Boolean => ocaml::Primitive::Bool,
PrimitiveKind::Octet
| PrimitiveKind::UInt8
| PrimitiveKind::Char
| PrimitiveKind::Int8
| PrimitiveKind::Int16
| PrimitiveKind::UInt16
| PrimitiveKind::WChar => ocaml::Primitive::Int,
PrimitiveKind::Int32 => ocaml::Primitive::Int32,
PrimitiveKind::UInt32 | PrimitiveKind::Int64 => ocaml::Primitive::Int64,
PrimitiveKind::UInt64 | PrimitiveKind::Float128 => ocaml::Primitive::String,
PrimitiveKind::Float32 | PrimitiveKind::Float64 => ocaml::Primitive::Float,
};
ocaml::Type::Primitive(primitive)
}
#[must_use]
pub const fn ocaml_holder(bits: u16) -> ocaml::Primitive {
match bits {
0..=30 => ocaml::Primitive::Int,
31..=32 => ocaml::Primitive::Int32,
_ => ocaml::Primitive::Int64,
}
}
#[must_use]
pub const fn ocaml_bitmask_holder(bound: u16) -> ocaml::Primitive {
if bound <= 32 {
ocaml::Primitive::Int32
} else {
ocaml::Primitive::Int64
}
}
fn one_shifted(position: u16, holder: ocaml::Primitive) -> Option<i128> {
let width = match holder {
ocaml::Primitive::Int64 => 64_u32,
_ => 32_u32,
};
if u32::from(position) >= width {
return None;
}
let raw = 1_i128.checked_shl(u32::from(position))?;
Some(match holder {
ocaml::Primitive::Int64 => i128::from(raw as i64),
_ => i128::from(raw as i32),
})
}
fn holder_literal(value: i128, holder: ocaml::Primitive) -> Literal {
match holder {
ocaml::Primitive::Int64 => ocaml::int64_literal(value),
ocaml::Primitive::Int32 => ocaml::int32_literal(value),
_ => Literal::int(value),
}
}
#[must_use]
pub fn ocaml_type_name(ast: &Ast, id: Id<TypeNode>) -> String {
let Ok(any) = ast.any_of_type(id) else {
return "zd_unnamed".to_string();
};
match ast.scoped_name_of(any) {
Some(scoped) => flatten(scoped.segments()),
None => "zd_unnamed".to_string(),
}
}
#[must_use]
pub fn ocaml_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_ALIAS_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_ALIAS_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: CharWidth::Char,
..
}) => "string".to_string(),
Ok(TypeKind::String {
width: 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),
_ => ocaml_type_name(ast, id),
}
}
#[must_use]
pub fn ocaml_enumerator_name(ast: &Ast, id: Id<TypeNode>, enumerator: &str) -> String {
ocaml::upper_ident(&format!("{}{enumerator}", 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 Some(head) = segments.get(..segments.len() - 1) else {
return String::new();
};
let mut prefix = flatten(head);
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());
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| ocaml::lower_ident(segment))
.collect::<Vec<String>>()
.join("_")
}
fn member_doc(ast: &Ast, target: Id<TypeNode>, declared: &str, optional: bool) -> Doc {
let mut doc = idl_note(&ocaml::lower_ident(declared), declared);
if optional {
doc.push(
"Optional: `None` 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.",
);
}
array_note(ast, target, doc)
}
fn array_note(ast: &Ast, target: Id<TypeNode>, mut doc: Doc) -> Doc {
if let Some(dims) = array_dims(ast, target, 0) {
doc.push(&format!(
"Fixed size in the IDL: {dims}. OCaml has no fixed-size array type, so the bound is \
not in the type and the codec is what enforces it."
));
}
doc
}
fn array_dims(ast: &Ast, id: Id<TypeNode>, depth: usize) -> Option<String> {
if depth > MAX_ALIAS_DEPTH {
return None;
}
let node = ast.type_(id).ok()?;
match &node.kind {
TypeKind::Array { dims, .. } => {
let mut text = String::new();
for dim in dims {
match ast.eval_integral(*dim) {
Ok(length) => text.push_str(&format!("[{length}]")),
Err(_) => text.push_str("[?]"),
}
}
Some(text)
}
TypeKind::Alias { aliased } if node.name.is_empty() => {
array_dims(ast, aliased.resolved()?, depth + 1)
}
_ => None,
}
}
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 structure".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 ocaml_constant(
ast: &Ast,
value: &zerodds_ast::ConstValue,
declared: Option<Id<TypeNode>>,
at: &str,
) -> Result<ocaml::Expr, MaterialiseError> {
use zerodds_ast::ConstValue;
let refuse = |construct: String| MaterialiseError::NoLanguageForm {
construct,
language: LANGUAGE,
at: at.to_string(),
};
if let Some(target) = declared {
if let Some(enumeration) = enumeration_behind(ast, target, 0) {
let ConstValue::Integer(number) = value else {
return Err(refuse(
"a constant of an enumeration type whose value is not a number".to_string(),
));
};
return enumerator_named(ast, enumeration, *number)
.map(|spelled| ocaml::Expr::construct(&spelled))
.ok_or_else(|| {
refuse(format!(
"a constant of an enumeration type whose value {number} names no \
enumerator, and an OCaml variant has no other spelling"
))
});
}
}
let holder = declared
.and_then(|target| literal_holder(ast, target, 0))
.unwrap_or_else(|| holder_for(value));
Ok(match value {
ConstValue::Boolean(flag) => ocaml::Expr::Literal(Literal::Bool(*flag)),
ConstValue::Char(character) => integer_expr(i128::from(u32::from(*character)), holder),
ConstValue::WChar(point) => integer_expr(i128::from(*point), holder),
ConstValue::Integer(number) => integer_expr(*number, holder),
ConstValue::Float { mantissa, exponent } => {
let text = constant::float_text(mantissa, exponent.as_deref());
if matches!(holder, ocaml::Primitive::String) {
ocaml::Expr::Literal(Literal::Str(text))
} else {
ocaml::Expr::Literal(Literal::Float(text))
}
}
ConstValue::Fixed { digits, scale } => {
ocaml::Expr::Literal(Literal::Str(constant::fixed_decimal(digits, *scale)))
}
ConstValue::String(text) => ocaml::Expr::Literal(Literal::Str(text.clone())),
ConstValue::WString(points) => {
ocaml::Expr::Literal(Literal::Str(constant::wstring_utf8(points).ok_or_else(
|| refuse("a `wstring` constant holding a lone surrogate".to_string()),
)?))
}
_ => {
return Err(refuse(
"a constant form this binding has no literal for".to_string(),
));
}
})
}
fn integer_expr(number: i128, holder: ocaml::Primitive) -> ocaml::Expr {
ocaml::Expr::Literal(match holder {
ocaml::Primitive::Int32 => ocaml::int32_literal(number),
ocaml::Primitive::Int64 => ocaml::int64_literal(number),
ocaml::Primitive::String => Literal::Str(format!("{number}")),
ocaml::Primitive::Float => Literal::Float(format!("{number}.0")),
_ => Literal::int(number),
})
}
fn holder_for(value: &zerodds_ast::ConstValue) -> ocaml::Primitive {
use zerodds_ast::ConstValue;
match value {
ConstValue::Boolean(_) => ocaml::Primitive::Bool,
ConstValue::Float { .. } => ocaml::Primitive::Float,
ConstValue::String(_) | ConstValue::WString(_) | ConstValue::Fixed { .. } => {
ocaml::Primitive::String
}
_ => ocaml::Primitive::Int32,
}
}
fn literal_holder(ast: &Ast, id: Id<TypeNode>, depth: usize) -> Option<ocaml::Primitive> {
if depth > MAX_ALIAS_DEPTH {
return None;
}
let node = ast.type_(id).ok()?;
match &node.kind {
TypeKind::Primitive(kind) => match ocaml_primitive_type(*kind) {
ocaml::Type::Primitive(primitive) => Some(primitive),
_ => None,
},
TypeKind::String { .. } | TypeKind::Fixed { .. } => Some(ocaml::Primitive::String),
TypeKind::Bitmask { .. } => Some(ocaml_bitmask_holder(ast.effective_bit_bound(id))),
TypeKind::Alias { aliased } => literal_holder(ast, aliased.resolved()?, depth + 1),
_ => None,
}
}
fn enumeration_behind(ast: &Ast, id: Id<TypeNode>, depth: usize) -> Option<Id<TypeNode>> {
if depth > MAX_ALIAS_DEPTH {
return None;
}
let node = ast.type_(id).ok()?;
match &node.kind {
TypeKind::Enumeration { .. } => Some(id),
TypeKind::Alias { aliased } => enumeration_behind(ast, aliased.resolved()?, depth + 1),
_ => None,
}
}
fn enumerator_named(ast: &Ast, enumeration: Id<TypeNode>, value: i128) -> Option<String> {
let node = ast.type_(enumeration).ok()?;
let TypeKind::Enumeration { enumerators } = &node.kind else {
return None;
};
let prefix = module_prefix(ast, enumeration);
for enumerator in enumerators {
if ast.effective_enumerator_value(*enumerator) != Some(value) {
continue;
}
if let Ok(enum_node) = ast.enumerator(*enumerator) {
return Some(ocaml::upper_ident(&format!("{prefix}{}", enum_node.name)));
}
}
None
}