use crate::adapter::cst::{MaterialiseError, no_spelling_collision};
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::kotlin;
const LANGUAGE: &str = "Kotlin";
const INVENTED_PREFIX: &str = "ZdEntry_";
pub const DISCRIMINATOR: &str = "_d";
const MAX_MODULE_DEPTH: usize = 64;
const MAX_TYPE_DEPTH: usize = 64;
const MAX_INTERFACE_DEPTH: usize = 64;
pub fn kotlin_items(ast: &Ast, applied: &Applied) -> Result<Vec<kotlin::Decl>, MaterialiseError> {
let mut out = Vec::new();
let mut emitted: Vec<(String, String)> = Vec::new();
for (id, _) in ast.types() {
let start = out.len();
declare(ast, applied, id, &mut out, &mut emitted)?;
verbatim::weave(
verbatim::declaration_slots(ast, id, verbatim::aliases::KOTLIN),
&kotlin_type_name(ast, id),
start,
&mut out,
);
}
for (_, node) in ast.constants() {
if ghost_created_for(applied, node.node_id, "KotlinConst").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) => kotlin_value_type(ast, target, &node.name)?,
None => kotlin::Type::Primitive(kotlin::Primitive::Int),
};
let name = declared_name(ast, &node.name, node.module);
remember(&mut emitted, &name, &node.name)?;
out.push(kotlin::Decl::Const(kotlin::ConstDecl {
doc: idl_note(&name, &node.name),
is_const: is_const_eligible(&type_),
name: Ident::new(&name),
type_: type_.clone(),
value: kotlin_constant(&value, &type_, &node.name)?,
}));
}
for (_, node) in ast.exceptions() {
if ghost_created_for(applied, node.node_id, "KotlinError").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((property_name(&member_node.name), member_node.name.clone()));
}
}
no_spelling_collision(as_pairs(&pairs), LANGUAGE, &name)?;
let mut properties = Vec::new();
for member in &node.members {
let Ok(member_node) = ast.member(*member) else {
return Err(MaterialiseError::NoNode {
kind: "member",
at: crate::adapter::cst::handle_of(*member),
});
};
let Some(target) = member_node.type_.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "exception member type".to_string(),
at: member_node.name.clone(),
});
};
properties.push(member_property(ast, member_node, target)?);
}
out.push(kotlin::Decl::Class(kotlin::Class {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: with_idl_note(
Doc::line("An exception, as the class its members make."),
&name,
&node.name,
),
is_data: !properties.is_empty(),
name: Ident::new(&name),
properties,
}));
}
for (id, node) in ast.interfaces() {
if ghost_created_for(applied, node.node_id, "KotlinInterface").is_none() {
continue;
}
let name = declared_name(ast, &node.name, node.module);
remember(&mut emitted, &name, &node.name)?;
let mut properties = Vec::new();
let mut signatures = Vec::new();
for operation in &operations_with_bases(ast, id, &node.name)? {
let Ok(op) = ast.operation(*operation) else {
return Err(MaterialiseError::NoNode {
kind: "operation",
at: crate::adapter::cst::handle_of(*operation),
});
};
signatures.push((property_name(&op.name), op.name.clone()));
let mut params = Vec::new();
for param in &op.params {
let Ok(param_node) = ast.param(*param) else {
return Err(MaterialiseError::NoNode {
kind: "parameter",
at: crate::adapter::cst::handle_of(*param),
});
};
let Some(target) = param_node.type_.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "parameter type".to_string(),
at: param_node.name.clone(),
});
};
params.push(kotlin_member_type(ast, target, ¶m_node.name)?);
}
let returns = match op
.return_type
.as_ref()
.and_then(zerodds_ast_core::Ref::resolved)
{
Some(target) => kotlin_member_type(ast, target, &op.name)?,
None => kotlin::Type::Unit,
};
properties.push(kotlin::Property {
doc: idl_note(&property_name(&op.name), &op.name),
mutable: false,
name: Ident::new(&property_name(&op.name)),
type_: kotlin::Type::Function {
params,
returns: Box::new(returns),
},
default: None,
});
}
no_spelling_collision(as_pairs(&signatures), LANGUAGE, &name)?;
out.push(kotlin::Decl::Interface(kotlin::Class {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: with_idl_note(
Doc::line(
"An interface, as the class of function-typed properties its operations make.",
),
&name,
&node.name,
),
is_data: false,
name: Ident::new(&name),
properties,
}));
}
Ok(verbatim::weave_file(ast, verbatim::aliases::KOTLIN, out))
}
impl verbatim::Carrier for kotlin::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::Class(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 kotlin_file(ast: &Ast, applied: &Applied) -> Result<kotlin::File, MaterialiseError> {
let decls = kotlin_items(ast, applied)?;
Ok(kotlin::File {
doc: Doc::lines(&[
"Generated by ZeroDDS from an IDL source. Do not edit.",
"",
"The IDL spelling of every renamed name is in the comment above it.",
"Nothing is renamed for style — a rename here is always the keyword",
"escape. A member's ID under @autoid(HASH) is the MD5 of its IDL name",
"(DDS-XTypes 1.3 7.3.1.2.1.1) and the escape does not invert, which is",
"why the comment is there.",
]),
package: None,
imports: Vec::new(),
decls,
})
}
#[allow(clippy::too_many_lines)]
fn declare(
ast: &Ast,
applied: &Applied,
id: Id<TypeNode>,
out: &mut Vec<kotlin::Decl>,
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::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: kotlin_type_name(ast, id),
});
};
let name = kotlin_map_entry_name(ast, id);
if emitted.iter().any(|(spelled, _)| *spelled == name) {
return Ok(());
}
emitted.push((name.clone(), name.clone()));
out.push(kotlin::Decl::Class(kotlin::Class {
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.",
]),
is_data: true,
name: Ident::new(&name),
properties: vec![
kotlin::Property {
doc: Doc::none(),
mutable: true,
name: Ident::new("key"),
type_: kotlin_member_type(ast, key, &name)?,
default: Some(zero_of(ast, key)?),
},
kotlin::Property {
doc: Doc::none(),
mutable: true,
name: Ident::new("value"),
type_: kotlin_member_type(ast, value, &name)?,
default: Some(zero_of(ast, value)?),
},
],
}));
}
TypeKind::Alias { aliased } => {
if node.name.is_empty() {
return Ok(());
}
if ghost_created_for(applied, node.node_id, "KotlinAlias").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 = kotlin_type_name(ast, id);
remember(emitted, &name, &node.name)?;
out.push(kotlin::Decl::Alias(kotlin::AliasDecl {
doc: idl_note(&name, &node.name),
name: Ident::new(&name),
target: kotlin_member_type(ast, target, &node.name)?,
}));
}
TypeKind::Enumeration { enumerators } => {
if ghost_created_for(applied, node.node_id, "KotlinEnum").is_none() {
return Ok(());
}
let name = kotlin_type_name(ast, id);
remember(emitted, &name, &node.name)?;
let mut pairs = Vec::new();
for enumerator in enumerators {
if let Ok(enum_node) = ast.enumerator(*enumerator) {
pairs.push((property_name(&enum_node.name), enum_node.name.clone()));
}
}
no_spelling_collision(as_pairs(&pairs), LANGUAGE, &name)?;
let mut members = Vec::new();
for enumerator in enumerators {
let Ok(enum_node) = ast.enumerator(*enumerator) else {
return Err(MaterialiseError::NoNode {
kind: "enumerator",
at: crate::adapter::cst::handle_of(*enumerator),
});
};
if ghost_created_for(applied, enum_node.node_id, "KotlinConstant").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 = property_name(&enum_node.name);
members.push(kotlin::ConstDecl {
doc: idl_note(&spelled, &enum_node.name),
is_const: true,
name: Ident::new(&spelled),
type_: kotlin::Type::Primitive(kotlin::Primitive::Int),
value: kotlin::Expr::int(value),
});
}
out.push(kotlin::Decl::Object(kotlin::Object {
doc: with_idl_note(
Doc::lines(&[
"An enumeration's constants. Not `enum class`: its ordinal is the",
"constant's position and DDS-XTypes 1.3 7.4.3.5.3 admits an",
"@value out of order or with a gap, which an ordinal cannot hold",
"without renumbering the wire.",
]),
&name,
&node.name,
),
name: Ident::new(&name),
members,
}));
}
TypeKind::Bitmask { bits } => {
if ghost_created_for(applied, node.node_id, "KotlinBitmask").is_none() {
return Ok(());
}
let bound = ast.effective_bit_bound(id);
let prefix = module_prefix(ast, id);
for bit in bits {
let Ok(bit_node) = ast.bit_value(*bit) else {
return Err(MaterialiseError::NoNode {
kind: "bit",
at: crate::adapter::cst::handle_of(*bit),
});
};
if ghost_created_for(applied, bit_node.node_id, "KotlinBitConst").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 = kotlin::escape_identifier(&format!("{prefix}{}", bit_node.name));
remember(emitted, &constant, &bit_node.name)?;
let holder = unsigned_holder(bound);
let one = if holder == kotlin::Primitive::Long {
kotlin::Expr::Literal(Literal::Int("1L".to_string()))
} else {
kotlin::Expr::int(1)
};
out.push(kotlin::Decl::Const(kotlin::ConstDecl {
doc: idl_note(&constant, &bit_node.name),
is_const: true,
name: Ident::new(&constant),
type_: kotlin::Type::Primitive(holder),
value: kotlin::Expr::Binary {
op: "shl",
lhs: Box::new(one),
rhs: Box::new(kotlin::Expr::int(i128::from(position))),
},
}));
}
}
TypeKind::Bitset { .. } => {
if ghost_created_for(applied, node.node_id, "KotlinBitset").is_none() {
return Ok(());
}
let name = kotlin_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 {
return Err(MaterialiseError::NoNode {
kind: "bitfield",
at: crate::adapter::cst::handle_of(*field),
});
};
let Some(field_ident) = field_node.name.as_ref() else {
continue;
};
pairs.push((property_name(field_ident), field_ident.clone()));
}
no_spelling_collision(as_pairs(&pairs), LANGUAGE, &name)?;
let mut properties = Vec::new();
let mut index = 0;
for field in &ast.all_bitfields_of(id) {
let Ok(field_node) = ast.bitfield(*field) else {
return Err(MaterialiseError::NoNode {
kind: "bitfield",
at: crate::adapter::cst::handle_of(*field),
});
};
if field_node.name.is_none() {
continue;
}
properties.push(kotlin::Property {
doc: idl_note(&pairs[index].0, &pairs[index].1),
mutable: true,
name: Ident::new(&pairs[index].0),
type_: kotlin::Type::Primitive(unsigned_holder(field_node.bits)),
default: Some(kotlin::Expr::int(0)),
});
index += 1;
}
out.push(kotlin::Decl::Class(kotlin::Class {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: with_idl_note(
Doc::lines(&[
"A bitset. The fields are separate properties here and not a",
"single packed word, because the order on the wire is the",
"declaration's and the packing into the anchor is the codec's.",
]),
&name,
&node.name,
),
is_data: !properties.is_empty(),
name: Ident::new(&name),
properties,
}));
}
TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
let created = if matches!(node.kind, TypeKind::Valuetype { .. }) {
"KotlinValue"
} else {
"KotlinStruct"
};
if ghost_created_for(applied, node.node_id, created).is_none() {
return Ok(());
}
let name = kotlin_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((property_name("parent"), "parent".to_string()));
}
for member in &members {
if let Ok(member_node) = ast.member(*member) {
pairs.push((property_name(&member_node.name), member_node.name.clone()));
}
}
no_spelling_collision(as_pairs(&pairs), LANGUAGE, &name)?;
let mut properties = Vec::new();
if let Some(base) = base {
properties.push(kotlin::Property {
doc: Doc::line(
"The base structure, the first member (XTypes 1.3 7.5.1.4.1.3).",
),
mutable: true,
name: Ident::new(&property_name("parent")),
type_: kotlin_member_type(ast, base, "parent")?,
default: Some(zero_of(ast, base)?),
});
}
for member in &members {
let Ok(member_node) = ast.member(*member) else {
return Err(MaterialiseError::NoNode {
kind: "member",
at: crate::adapter::cst::handle_of(*member),
});
};
let Some(target) = member_node.type_.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "member type".to_string(),
at: member_node.name.clone(),
});
};
properties.push(member_property(ast, member_node, target)?);
}
out.push(kotlin::Decl::Class(kotlin::Class {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: idl_note(&name, &node.name),
is_data: !properties.is_empty(),
name: Ident::new(&name),
properties,
}));
}
TypeKind::Union { discriminator, .. } => {
if ghost_created_for(applied, node.node_id, "KotlinUnion").is_none() {
return Ok(());
}
let name = kotlin_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((property_name(&member.name), member.name.clone()));
}
}
}
no_spelling_collision(as_pairs(&pairs), LANGUAGE, &name)?;
let mut properties = vec![kotlin::Property {
doc: Doc::line("Which branch carries the value."),
mutable: true,
name: Ident::new(DISCRIMINATOR),
type_: kotlin_member_type(ast, disc, &name)?,
default: Some(zero_of(ast, disc)?),
}];
for case in cases {
let Ok(case_node) = ast.case(*case) else {
return Err(MaterialiseError::NoNode {
kind: "case",
at: crate::adapter::cst::handle_of(*case),
});
};
if ghost_created_for(applied, case_node.node_id, "KotlinCase").is_none() {
continue;
}
let Ok(member) = ast.member(case_node.member) else {
return Err(MaterialiseError::NoNode {
kind: "member",
at: crate::adapter::cst::handle_of(case_node.member),
});
};
let Some(target) = member.type_.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "union case type".to_string(),
at: member.name.clone(),
});
};
properties.push(member_property(ast, member, target)?);
}
out.push(kotlin::Decl::Class(kotlin::Class {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: with_idl_note(
Doc::lines(&[
"A union, as a data class carrying the discriminator beside every",
"branch. Only the branch the discriminator selects is written",
"(DDS-XTypes 1.3 7.4.1.1.5.2); the codec's business, not the",
"type's.",
]),
&name,
&node.name,
),
is_data: true,
name: Ident::new(&name),
properties,
}));
}
TypeKind::Native => {
if ghost_created_for(applied, node.node_id, "KotlinNative").is_none() {
return Ok(());
}
let name = kotlin_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(kotlin::Decl::Alias(kotlin::AliasDecl {
doc: with_idl_note(
Doc::line("A native type. `Any` unless a facet named its mapping."),
&name,
&node.name,
),
name: Ident::new(&name),
target: match mapping {
Some(spelling) => kotlin::Type::named(&spelling),
None => kotlin::Type::named("Any"),
},
}));
}
TypeKind::ValueBox { boxed } => {
let Some(target) = boxed.resolved() else {
return Ok(());
};
let name = kotlin_type_name(ast, id);
if emitted.iter().any(|(spelled, _)| *spelled == name) {
return Ok(());
}
remember(emitted, &name, &node.name)?;
out.push(kotlin::Decl::Alias(kotlin::AliasDecl {
doc: with_idl_note(
Doc::line("A boxed value: nullable, `null` when absent."),
&name,
&node.name,
),
name: Ident::new(&name),
target: kotlin::Type::nullable(kotlin_member_type(ast, target, &name)?),
}));
}
TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. }
| TypeKind::Any
| TypeKind::ValueBase => {}
}
Ok(())
}
fn operations_with_bases(
ast: &Ast,
id: Id<zerodds_ast::InterfaceNode>,
at: &str,
) -> Result<Vec<Id<zerodds_ast::OperationNode>>, MaterialiseError> {
let mut seen = Vec::new();
let mut out = Vec::new();
collect_operations(ast, id, at, &mut seen, &mut out)?;
Ok(out)
}
fn collect_operations(
ast: &Ast,
id: Id<zerodds_ast::InterfaceNode>,
at: &str,
seen: &mut Vec<Id<zerodds_ast::InterfaceNode>>,
out: &mut Vec<Id<zerodds_ast::OperationNode>>,
) -> Result<(), MaterialiseError> {
if seen.contains(&id) || seen.len() >= MAX_INTERFACE_DEPTH {
return Ok(());
}
seen.push(id);
let Ok(node) = ast.interface(id) else {
return Err(MaterialiseError::NoNode {
kind: "interface",
at: crate::adapter::cst::handle_of(id),
});
};
for base in &node.bases {
let Some(base_id) = base.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "base interface".to_string(),
at: at.to_string(),
});
};
collect_operations(ast, base_id, at, seen, out)?;
}
out.extend(ast.operations_of(id).iter().copied());
Ok(())
}
pub fn kotlin_member_type(
ast: &Ast,
target: Id<TypeNode>,
at: &str,
) -> Result<kotlin::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(),
});
}
kotlin_value_type(ast, target, at)
}
pub fn kotlin_value_type(
ast: &Ast,
id: Id<TypeNode>,
at: &str,
) -> Result<kotlin::Type, MaterialiseError> {
value_type_at(ast, id, at, 0)
}
fn value_type_at(
ast: &Ast,
id: Id<TypeNode>,
at: &str,
depth: usize,
) -> Result<kotlin::Type, MaterialiseError> {
if depth > MAX_TYPE_DEPTH {
return Err(MaterialiseError::Unevaluated {
construct: format!("type, whose Kotlin spelling nests past {MAX_TYPE_DEPTH}"),
at: at.to_string(),
});
}
let Ok(node) = ast.type_(id) else {
return Err(MaterialiseError::Unevaluated {
construct: "type behind a member".to_string(),
at: at.to_string(),
});
};
Ok(match &node.kind {
TypeKind::Primitive(kind) => kotlin_primitive_type(*kind),
TypeKind::String { .. } | TypeKind::Fixed { .. } => kotlin::Type::named("String"),
TypeKind::Sequence { element, .. } => {
let Some(element) = element.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "sequence element".to_string(),
at: node.name.clone(),
});
};
kotlin::Type::array(value_type_at(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: node.name.clone(),
});
};
let mut type_ = value_type_at(ast, element, at, depth + 1)?;
for _ in dims.iter() {
type_ = kotlin::Type::array(type_);
}
type_
}
TypeKind::Map { .. } => {
kotlin::Type::array(kotlin::Type::named(&kotlin_map_entry_name(ast, id)))
}
TypeKind::Bitmask { .. } => {
kotlin::Type::Primitive(unsigned_holder(ast.effective_bit_bound(id)))
}
TypeKind::Alias { aliased } if node.name.is_empty() => {
let Some(target) = aliased.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "inline typedef target".to_string(),
at: at.to_string(),
});
};
value_type_at(ast, target, at, depth + 1)?
}
TypeKind::Enumeration { .. } => kotlin::Type::Primitive(kotlin::Primitive::Int),
_ => kotlin::Type::named(&kotlin_type_name(ast, id)),
})
}
#[must_use]
pub fn kotlin_primitive_type(kind: PrimitiveKind) -> kotlin::Type {
use kotlin::{Primitive, Type};
match kind {
PrimitiveKind::Boolean => Type::Primitive(Primitive::Boolean),
PrimitiveKind::Char => Type::Primitive(Primitive::Char),
PrimitiveKind::Int8 => Type::Primitive(Primitive::Byte),
PrimitiveKind::Int16 => Type::Primitive(Primitive::Short),
PrimitiveKind::Octet
| PrimitiveKind::UInt8
| PrimitiveKind::WChar
| PrimitiveKind::UInt16 => Type::Primitive(Primitive::Int),
PrimitiveKind::Int32 => Type::Primitive(Primitive::Int),
PrimitiveKind::UInt32 | PrimitiveKind::Int64 | PrimitiveKind::UInt64 => {
Type::Primitive(Primitive::Long)
}
PrimitiveKind::Float32 => Type::Primitive(Primitive::Float),
PrimitiveKind::Float64 => Type::Primitive(Primitive::Double),
PrimitiveKind::Float128 => Type::named("Float128"),
}
}
#[must_use]
pub const fn unsigned_holder(bits: u16) -> kotlin::Primitive {
if bits <= 16 {
kotlin::Primitive::Int
} else {
kotlin::Primitive::Long
}
}
#[must_use]
pub fn kotlin_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 kotlin_map_entry_name(ast: &Ast, id: Id<TypeNode>) -> String {
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, 0));
let value = value
.resolved()
.map_or_else(|| "unknown".to_string(), |v| type_tag(ast, v, 0));
format!("{INVENTED_PREFIX}{key}_{value}")
}
fn bound_tag(ast: &Ast, bound: &zerodds_ast::Bound) -> String {
match bound.expr() {
None => String::new(),
Some(expr) => match ast.eval_integral(expr) {
Ok(limit) => format!("_b{limit}"),
Err(_) => "_bx".to_string(),
},
}
}
#[must_use]
pub fn type_tag(ast: &Ast, id: Id<TypeNode>, depth: usize) -> String {
if depth > MAX_TYPE_DEPTH {
return "unresolved".to_string();
}
let node = match ast.type_(id) {
Ok(node) => node,
Err(_) => return "unresolved".to_string(),
};
match &node.kind {
TypeKind::Primitive(kind) => kind.as_str().replace(' ', "_"),
TypeKind::String {
width: CharWidth::Char,
bound,
} => format!("string{}", bound_tag(ast, bound)),
TypeKind::String {
width: CharWidth::WChar,
bound,
} => format!("wstring{}", bound_tag(ast, bound)),
TypeKind::Fixed { digits, scale } => format!("fixed_{digits}_{scale}"),
TypeKind::Sequence { element, bound } => match element.resolved() {
Some(element) => format!(
"seq{}_{}",
bound_tag(ast, bound),
type_tag(ast, element, depth + 1)
),
None => format!("seq_unresolved{}", bound_tag(ast, bound)),
},
TypeKind::Array { element, dims } => {
let mut name = match element.resolved() {
Some(element) => format!("arr_{}", type_tag(ast, element, depth + 1)),
None => "arr_unresolved".to_string(),
};
for dim in dims {
match ast.eval_integral(*dim) {
Ok(length) => name.push_str(&format!("_{length}")),
Err(_) => name.push_str("_x"),
}
}
name
}
TypeKind::Map { key, value, bound } => {
let half = |half: Option<Id<TypeNode>>| match half {
Some(target) => type_tag(ast, target, depth + 1),
None => "unresolved".to_string(),
};
format!(
"map{}_{}_{}",
bound_tag(ast, bound),
half(key.resolved()),
half(value.resolved())
)
}
TypeKind::Alias { aliased } => match aliased.resolved() {
Some(target) => type_tag(ast, target, depth + 1),
None => "unresolved".to_string(),
},
TypeKind::ValueBox { boxed } => match boxed.resolved() {
Some(target) => format!("box_{}", type_tag(ast, target, depth + 1)),
None => "box_unresolved".to_string(),
},
TypeKind::Structure { .. }
| TypeKind::Union { .. }
| TypeKind::Enumeration { .. }
| TypeKind::Bitmask { .. }
| TypeKind::Bitset { .. }
| TypeKind::Valuetype { .. }
| TypeKind::Native => kotlin_type_name(ast, id),
TypeKind::Any => "any".to_string(),
TypeKind::ValueBase => "valuebase".to_string(),
TypeKind::ObjectRef { .. } => "objectref".to_string(),
TypeKind::AnnotationType { .. } => "annotation".to_string(),
}
}
fn module_prefix(ast: &Ast, id: Id<TypeNode>) -> String {
let Ok(any) = ast.any_of_type(id) else {
return String::new();
};
let Some(scoped) = ast.scoped_name_of(any) else {
return String::new();
};
let segments = scoped.segments();
if segments.len() <= 1 {
return String::new();
}
let mut prefix = segments[..segments.len() - 1].join("_");
prefix.push('_');
prefix
}
fn declared_name(ast: &Ast, name: &str, module: Id<zerodds_ast::ModuleNode>) -> String {
let mut segments: Vec<String> = Vec::new();
let mut current = Some(module);
let mut depth = 0;
while let Some(id) = current {
if depth > MAX_MODULE_DEPTH {
break;
}
depth += 1;
let Ok(node) = ast.module(id) else { break };
if node.name.is_empty() {
break;
}
segments.push(node.name.clone());
current = node.parent;
}
segments.reverse();
segments.push(name.to_string());
kotlin::escape_identifier(&segments.join("_"))
}
fn flatten(segments: &[String]) -> String {
kotlin::escape_identifier(&segments.join("_"))
}
#[must_use]
pub fn property_name(name: &str) -> String {
kotlin::escape_identifier(name)
}
fn member_property(
ast: &Ast,
member: &zerodds_ast::MemberNode,
target: Id<TypeNode>,
) -> Result<kotlin::Property, MaterialiseError> {
let spelled = property_name(&member.name);
let mut doc = idl_note(&spelled, &member.name);
let mut type_ = kotlin_member_type(ast, target, &member.name)?;
if is_external(member) {
doc.push("Stored by reference — @external, DDS-XTypes 1.3 Table 21. Every");
doc.push("JVM object field already is one, so nothing further is needed.");
}
let default = if member.facets.is_optional() {
type_ = kotlin::Type::nullable(type_);
doc.push("Optional. `null` when the member is absent — DDS-XTypes 1.3");
doc.push("7.2.2.4.4.4.7.");
kotlin::Expr::Literal(Literal::Null)
} else {
zero_of(ast, target)?
};
Ok(kotlin::Property {
doc,
mutable: true,
name: Ident::new(&spelled),
type_,
default: Some(default),
})
}
fn is_external(member: &zerodds_ast::MemberNode) -> bool {
member
.facets
.xtypes
.as_ref()
.is_some_and(|xtypes| xtypes.is_external)
}
fn zero_of(ast: &Ast, id: Id<TypeNode>) -> Result<kotlin::Expr, MaterialiseError> {
zero_of_depth(ast, id, 0)
}
fn zero_of_depth(
ast: &Ast,
id: Id<TypeNode>,
depth: usize,
) -> Result<kotlin::Expr, MaterialiseError> {
if depth > MAX_TYPE_DEPTH {
return Ok(kotlin::Expr::int(0));
}
let Ok(node) = ast.type_(id) else {
return Err(MaterialiseError::Unevaluated {
construct: "the type a resting value is built for".to_string(),
at: crate::adapter::cst::handle_of(id),
});
};
Ok(match &node.kind {
TypeKind::Primitive(PrimitiveKind::Boolean) => kotlin::Expr::Literal(Literal::Bool(false)),
TypeKind::Primitive(PrimitiveKind::Char) => kotlin::Expr::Literal(Literal::Char('\u{0}')),
TypeKind::Primitive(PrimitiveKind::Float32) => {
kotlin::Expr::Literal(Literal::Float("0.0f".to_string()))
}
TypeKind::Primitive(PrimitiveKind::Float64) => {
kotlin::Expr::Literal(Literal::Float("0.0".to_string()))
}
TypeKind::Primitive(PrimitiveKind::Float128) => {
kotlin::Expr::method_call(kotlin::Expr::name("Float128"), "zero", Vec::new())
}
TypeKind::Primitive(
PrimitiveKind::UInt32 | PrimitiveKind::Int64 | PrimitiveKind::UInt64,
) => kotlin::Expr::Literal(Literal::Int("0L".to_string())),
TypeKind::Primitive(_) | TypeKind::Enumeration { .. } => kotlin::Expr::int(0),
TypeKind::String { .. } | TypeKind::Fixed { .. } => {
kotlin::Expr::Literal(Literal::Str(String::new()))
}
TypeKind::Sequence { element, .. } => {
let Some(element) = element.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "sequence element behind a resting value".to_string(),
at: named_at(node, id),
});
};
kotlin::Expr::ArrayOf {
element: Box::new(kotlin_value_type(ast, element, "sequence default")?),
elements: Vec::new(),
}
}
TypeKind::Array { element, dims } => {
let Some(element) = element.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "array element behind a resting value".to_string(),
at: named_at(node, id),
});
};
let mut expr = zero_of_depth(ast, element, depth + 1)?;
let mut type_ = kotlin_value_type(ast, element, "array default")?;
for dim in dims.iter().rev() {
let length =
ast.eval_integral(*dim)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "array length".to_string(),
at: named_at(node, id),
})?;
let count = usize::try_from(length).map_err(|_| MaterialiseError::Unevaluated {
construct: "array length this column cannot spell".to_string(),
at: named_at(node, id),
})?;
expr = kotlin::Expr::ArrayOf {
element: Box::new(type_.clone()),
elements: vec![expr; count],
};
type_ = kotlin::Type::array(type_);
}
expr
}
TypeKind::Map { .. } => kotlin::Expr::ArrayOf {
element: Box::new(kotlin::Type::named(&kotlin_map_entry_name(ast, id))),
elements: Vec::new(),
},
TypeKind::Alias { aliased } => match aliased.resolved() {
Some(target) => zero_of_depth(ast, target, depth + 1)?,
None => kotlin::Expr::int(0),
},
TypeKind::ValueBox { .. } => kotlin::Expr::Literal(Literal::Null),
TypeKind::Bitmask { .. } => kotlin::Expr::int(0),
_ => kotlin::Expr::New {
type_: Box::new(kotlin::Type::named(&kotlin_type_name(ast, id))),
args: Vec::new(),
},
})
}
fn named_at(node: &TypeNode, id: Id<TypeNode>) -> String {
if node.name.is_empty() {
return crate::adapter::cst::handle_of(id);
}
node.name.clone()
}
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 is_const_eligible(type_: &kotlin::Type) -> bool {
match type_ {
kotlin::Type::Primitive(_) => true,
kotlin::Type::Named { path, args } => {
args.is_empty() && path.segments().len() == 1 && path.segments()[0].as_str() == "String"
}
_ => false,
}
}
fn kotlin_constant(
value: &zerodds_ast::ConstValue,
type_: &kotlin::Type,
at: &str,
) -> Result<kotlin::Expr, MaterialiseError> {
use zerodds_ast::ConstValue;
let refuse = |construct: &str| MaterialiseError::NoLanguageForm {
construct: construct.to_string(),
language: LANGUAGE,
at: at.to_string(),
};
let is_wide = matches!(type_, kotlin::Type::Primitive(kotlin::Primitive::Long));
let is_float32 = matches!(type_, kotlin::Type::Primitive(kotlin::Primitive::Float));
let is_float128 = *type_ == kotlin::Type::named("Float128");
Ok(match value {
ConstValue::Boolean(flag) => kotlin::Expr::Literal(Literal::Bool(*flag)),
ConstValue::Integer(number) => {
if is_wide {
let text = if *number == i128::from(i64::MIN) {
zerodds_lang_render::kotlin::long_text(i64::MIN)
} else {
format!("{number}L")
};
kotlin::Expr::Literal(Literal::Int(text))
} else {
kotlin::Expr::Literal(Literal::int(*number))
}
}
ConstValue::Char(character) => kotlin::Expr::Literal(Literal::Char(*character)),
ConstValue::WChar(point) => kotlin::Expr::int(i128::from(*point)),
ConstValue::Float { mantissa, exponent } => {
let text = crate::adapter::constant::float_text(mantissa, exponent.as_deref());
if is_float128 {
kotlin::Expr::method_call(
kotlin::Expr::name("Float128"),
"parse",
vec![kotlin::Expr::Literal(Literal::Str(text))],
)
} else if is_float32 {
kotlin::Expr::Literal(Literal::Float(format!("{text}f")))
} else {
kotlin::Expr::Literal(Literal::Float(text))
}
}
ConstValue::Fixed { digits, scale } => kotlin::Expr::Literal(Literal::Str(
crate::adapter::constant::fixed_decimal(digits, *scale),
)),
ConstValue::String(text) => kotlin::Expr::Literal(Literal::Str(text.clone())),
ConstValue::WString(points) => kotlin::Expr::Literal(Literal::Str(
crate::adapter::constant::wstring_utf8(points)
.ok_or_else(|| refuse("a `wstring` constant holding a lone surrogate"))?,
)),
_ => return Err(refuse("a constant form this binding has no literal for")),
})
}