use crate::adapter::c::ordered_types;
use crate::adapter::constant;
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, ModuleNode, PrimitiveKind, TypeKind, TypeNode};
use zerodds_ast_core::{Id, NodeId};
use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
use zerodds_lang_cst::java;
pub const OUTER: &str = "ZdTypes";
const FLOAT128: &[&str] = &["org", "zerodds", "wire", "Float128"];
const STRING: &[&str] = &["java", "lang", "String"];
const BIG_DECIMAL: &[&str] = &["java", "math", "BigDecimal"];
const COLLECTION: &[&str] = &["java", "util", "Collection"];
pub const ARRAY_LIST: &[&str] = &["java", "util", "ArrayList"];
const ENTRY_PREFIX: &str = "ZdEntry_";
pub const MAP_PREFIX: &str = "ZdMap";
const RESERVED_TOP_LEVEL: &[&str] = &[OUTER, "java", "org"];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum JavaMapping {
Omg,
#[default]
Psm,
}
impl JavaMapping {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Omg => "omg",
Self::Psm => "psm",
}
}
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
match name {
"omg" => Some(Self::Omg),
"psm" => Some(Self::Psm),
_ => None,
}
}
}
std::thread_local! {
static MAPPING: std::cell::Cell<JavaMapping> =
const { std::cell::Cell::new(JavaMapping::Psm) };
}
#[must_use]
pub fn mapping() -> JavaMapping {
MAPPING.with(std::cell::Cell::get)
}
pub fn with_mapping<R>(mapping: JavaMapping, body: impl FnOnce() -> R) -> R {
struct Restore(JavaMapping);
impl Drop for Restore {
fn drop(&mut self) {
MAPPING.with(|cell| cell.set(self.0));
}
}
let _restore = Restore(MAPPING.with(|cell| cell.replace(mapping)));
body()
}
#[must_use]
pub fn getter_name(field: &str) -> String {
format!("get{}", property(field))
}
#[must_use]
pub fn setter_name(field: &str) -> String {
format!("set{}", property(field))
}
#[must_use]
pub fn boolean_getter_name(field: &str) -> String {
format!("is{}", property(field))
}
fn property(field: &str) -> String {
if field == "_d" {
return "Discriminator".to_string();
}
let mut chars = field.chars();
let mut out = String::new();
if let Some(first) = chars.next() {
out.extend(first.to_uppercase());
}
out.push_str(chars.as_str());
if out == "Class" {
out.push('_');
}
out
}
pub const SEQUENCES: &str = "Sequences";
fn is_sequences(receiver: &java::Expr) -> bool {
matches!(receiver, java::Expr::Path(path)
if matches!(path.segments(), [only] if only.as_str() == SEQUENCES))
}
fn sequences_call(name: &str, args: Vec<java::Expr>) -> java::Expr {
java::Expr::MethodCall {
receiver: Some(Box::new(java::Expr::Path(Path::new(&[SEQUENCES])))),
name: Ident::new(name),
args,
}
}
#[must_use]
pub fn sequence_view(value: java::Expr) -> java::Expr {
sequences_call("view", vec![value])
}
#[must_use]
pub fn index(value: java::Expr, at: java::Expr) -> java::Expr {
if let java::Expr::MethodCall {
receiver: Some(receiver),
name,
args,
} = &value
&& name.as_str() == "view"
&& is_sequences(receiver)
&& let [inner] = args.as_slice()
{
return sequences_call("at", vec![inner.clone(), at]);
}
java::Expr::Index {
value: Box::new(value),
index: Box::new(at),
}
}
#[must_use]
pub fn sequences_count(value: java::Expr) -> java::Expr {
sequences_call("count", vec![value])
}
#[must_use]
pub fn sequences_first(value: java::Expr, count: java::Expr) -> java::Expr {
sequences_call("first", vec![value, count])
}
#[must_use]
pub fn sequences_alloc(value: java::Expr, count: java::Expr) -> java::Expr {
sequences_call("alloc", vec![value, count])
}
#[must_use]
pub fn sequence_slot(sequence: java::Expr, at: java::Expr) -> java::Expr {
sequences_call("slot", vec![sequence, at])
}
#[must_use]
pub fn accessor(base: java::Expr, field: &str) -> java::Expr {
java::Expr::MethodCall {
receiver: Some(Box::new(base)),
name: Ident::new(&getter_name(field)),
args: Vec::new(),
}
}
#[must_use]
pub fn assign(place: java::Expr, value: java::Expr) -> java::Statement {
if mapping() == JavaMapping::Psm {
if let java::Expr::MethodCall {
receiver: Some(receiver),
name,
args,
} = &place
{
if args.is_empty() && name.as_str().starts_with("get") {
return java::Statement::Expr(java::Expr::MethodCall {
receiver: Some(receiver.clone()),
name: Ident::new(&format!("set{}", &name.as_str()["get".len()..])),
args: vec![value],
});
}
if name.as_str() == "slot"
&& is_sequences(receiver)
&& let [sequence, at] = args.as_slice()
{
return java::Statement::Expr(sequences_call(
"put",
vec![sequence.clone(), at.clone(), value],
));
}
}
}
java::Statement::Expr(java::Expr::Binary {
op: "=",
lhs: Box::new(place),
rhs: Box::new(value),
})
}
#[must_use]
pub fn assignment(statement: java::Statement) -> java::Statement {
match statement {
java::Statement::Expr(java::Expr::Binary { op: "=", lhs, rhs }) => assign(*lhs, *rhs),
other => other,
}
}
pub fn java_items(ast: &Ast, applied: &Applied) -> Result<Vec<java::TypeDecl>, MaterialiseError> {
let mut structural: Vec<java::Member> = Vec::new();
let mut emitted: Vec<String> = Vec::new();
let mut items: Vec<Pending> = Vec::new();
for id in ordered_types(ast) {
let start = items.len();
declare(ast, applied, id, &mut items, &mut structural, &mut emitted)?;
annotate_declared(ast, id, start, &mut items)?;
weave_verbatim(ast, id, start, &mut items);
}
for (module, member) in annotation_types(ast)? {
items.push(Pending { module, member });
}
for (id, node) in ast.constants() {
if ghost_created_for(applied, node.node_id, "JavaConst").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 type_ = match node.type_.resolved() {
Some(target) => java_member_type(ast, target, &node.name)?,
None => java::Type::Primitive(java::Primitive::Int),
};
let value = match value {
zerodds_ast::ConstValue::Integer(number)
if (128..=255).contains(&number)
&& node
.type_
.resolved()
.is_some_and(|target| is_octet(ast, target, 0)) =>
{
java::Expr::Cast {
type_: Box::new(java::Type::Primitive(java::Primitive::Byte)),
value: Box::new(java::Expr::Literal(Literal::Int(format!("0x{number:02X}")))),
}
}
value => constant_expr(&type_, &value, &node.name)?,
};
items.push(Pending {
module: node.module,
member: java::Member::Field(java::Field {
doc: Doc::none(),
annotations: user_annotations(ast, zerodds_ast::AnyId::Const(id, node.node_id))?,
modifiers: vec![
java::Modifier::Public,
java::Modifier::Static,
java::Modifier::Final,
],
type_,
name: Ident::new(&java::escape_keyword(&node.name)),
value: Some(value),
}),
});
}
for (id, node) in ast.exceptions() {
if ghost_created_for(applied, node.node_id, "JavaError").is_none() {
continue;
}
let _ = id;
let mut members = Vec::new();
for member in &node.members {
let Ok(member_node) = ast.member(*member) else {
return Err(MaterialiseError::Unevaluated {
construct: "exception member the model listed".to_string(),
at: node.name.clone(),
});
};
let Some(target) = member_node.type_.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "exception member type".to_string(),
at: member_node.name.clone(),
});
};
members.push(field(
java_member_type(ast, target, &member_node.name)?,
&member_node.name,
Doc::none(),
));
}
items.push(Pending {
module: node.module,
member: data_class(
&java::escape_keyword(&node.name),
members,
Doc::lines(&[
"An exception, as the structure its members make.",
"",
"A data holder and not a java.lang.Throwable: a Throwable is",
"Serializable, which under -Xlint:all -Werror demands a",
"serialVersionUID, and nothing about the wire form decides what",
"that number should be.",
]),
None,
true,
)?,
});
}
let mut root = Scope::default();
for item in items {
insert(ast, &mut root, item)?;
}
let mut members = structural;
members.extend(scope_members(ast, root, 0)?);
check_names(&members, &[OUTER.to_string()], 0)?;
let (begin_file, end_file) = verbatim::file_slots(ast, verbatim::aliases::JAVA);
let mut decls: Vec<java::TypeDecl> = begin_file
.into_iter()
.map(java::TypeDecl::Verbatim)
.collect();
decls.push(java::TypeDecl::Class(java::Class {
doc: Doc::lines(&[
"The Java shape of one IDL file.",
"",
"Every declaration nests here: Java allows one public top-level type",
"per file, and one file is what a generated probe compiles.",
]),
annotations: Vec::new(),
modifiers: vec![java::Modifier::Public, java::Modifier::Final],
name: Ident::new(OUTER),
generics: Vec::new(),
extends: None,
implements: Vec::new(),
members,
}));
decls.extend(end_file.into_iter().map(java::TypeDecl::Verbatim));
Ok(decls)
}
const GROUP_SUFFIX: &str = "Group";
fn element_type(
ast: &Ast,
member: &zerodds_ast::AnnotationMemberNode,
owner: &str,
) -> Result<java::Type, MaterialiseError> {
let at = format!("@{owner}.{}", member.name);
let Some(target) = member.type_.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "annotation member type".to_string(),
at,
});
};
let type_ = java_member_type(ast, target, &at)?;
let allowed = match &type_ {
java::Type::Primitive(_) => true,
java::Type::Named { path, .. } => {
let segments: Vec<&str> = path.segments().iter().map(Ident::as_str).collect();
segments == STRING
}
_ => false,
};
if !allowed {
return Err(MaterialiseError::NoLanguageForm {
construct: format!(
"an annotation member of the type {}, which a Java annotation element cannot have",
zerodds_lang_render::java::type_(&type_)
),
language: "Java",
at,
});
}
Ok(type_)
}
fn annotation_path(
ast: &Ast,
def: Id<zerodds_ast::AnnotationDefNode>,
group: bool,
) -> Result<Path, MaterialiseError> {
let node = ast
.annotation_def(def)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "annotation declaration".to_string(),
at: OUTER.to_string(),
})?;
let mut segments = vec![OUTER.to_string()];
for (_, name) in module_chain(ast, node.module)? {
segments.push(name);
}
let mut name = java::escape_keyword(&node.name);
if group {
name.push_str(GROUP_SUFFIX);
}
segments.push(name);
let refs: Vec<&str> = segments.iter().map(String::as_str).collect();
Ok(Path::new(&refs))
}
fn annotation_types(ast: &Ast) -> Result<Vec<(Id<ModuleNode>, java::Member)>, MaterialiseError> {
let mut out = Vec::new();
for (_, def) in ast.annotation_defs() {
let in_builtin = ast
.module(def.module)
.is_ok_and(|module| module.name == zerodds_ast::node::annotation::BUILTIN_MODULE_NAME);
if in_builtin {
continue;
}
let name = java::escape_keyword(&def.name);
let mut elements = Vec::new();
for member_id in &def.members {
let member =
ast.annotation_member(*member_id)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "annotation member".to_string(),
at: def.name.clone(),
})?;
let type_ = element_type(ast, member, &def.name)?;
let default = match member.default {
Some(expr) => {
let at = format!("@{}.{}", def.name, member.name);
let value =
ast.eval_const(expr)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "annotation member default".to_string(),
at: at.clone(),
})?;
Some(constant_expr(&type_, &value, &at)?)
}
None => None,
};
elements.push(java::AnnotationElement {
doc: Doc::none(),
type_,
name: Ident::new(&java::escape_keyword(&member.name)),
default,
});
}
out.push((
def.module,
java::Member::Nested(Box::new(java::TypeDecl::AnnotationType(
java::AnnotationType {
doc: Doc::line("A user-defined annotation type (XTypes 1.3 7.5.1.2.6.1)."),
annotations: Vec::new(),
modifiers: vec![java::Modifier::Public],
name: Ident::new(&name),
elements,
},
))),
));
out.push((
def.module,
java::Member::Nested(Box::new(java::TypeDecl::AnnotationType(
java::AnnotationType {
doc: Doc::line(
"Several instances of the annotation on one element (XTypes 1.3 \
7.5.1.2.6.1).",
),
annotations: Vec::new(),
modifiers: vec![java::Modifier::Public],
name: Ident::new(&format!("{name}{GROUP_SUFFIX}")),
elements: vec![java::AnnotationElement {
doc: Doc::none(),
type_: java::Type::Array(Box::new(java::Type::named(Path::single(&name)))),
name: Ident::new("value"),
default: None,
}],
},
))),
));
}
Ok(out)
}
fn is_builtin_annotation(ast: &Ast, def: Id<zerodds_ast::AnnotationDefNode>) -> bool {
ast.annotation_def(def)
.ok()
.and_then(|node| ast.module(node.module).ok())
.is_some_and(|module| module.name == zerodds_ast::node::annotation::BUILTIN_MODULE_NAME)
}
fn applied_annotation(
ast: &Ast,
applied: &zerodds_ast::AppliedAnnotation,
def: Id<zerodds_ast::AnnotationDefNode>,
) -> Result<java::Annotation, MaterialiseError> {
let node = ast
.annotation_def(def)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "annotation declaration".to_string(),
at: applied.name.clone(),
})?;
let mut args = Vec::new();
for argument in &applied.arguments {
let member = argument
.name
.as_deref()
.and_then(|written| {
node.members.iter().find_map(|id| {
ast.annotation_member(*id)
.ok()
.filter(|m| m.name.eq_ignore_ascii_case(written))
})
})
.or_else(|| {
node.members.iter().find_map(|id| {
ast.annotation_member(*id)
.ok()
.filter(|m| m.is_value_member)
})
})
.ok_or_else(|| MaterialiseError::Unevaluated {
construct: "annotation argument naming no member".to_string(),
at: applied.name.clone(),
})?;
let at = format!("@{}.{}", node.name, member.name);
let type_ = element_type(ast, member, &node.name)?;
let value = ast
.eval_const(argument.value)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "annotation argument".to_string(),
at: at.clone(),
})?;
args.push(java::AnnotationArg::Pair {
name: Ident::new(&java::escape_keyword(&member.name)),
value: constant_expr(&type_, &value, &at)?,
});
}
Ok(java::Annotation {
path: annotation_path(ast, def, false)?,
args,
})
}
fn user_annotations(
ast: &Ast,
any: zerodds_ast::AnyId,
) -> Result<Vec<java::Annotation>, MaterialiseError> {
let facets = ast
.facets(any)
.ok_or_else(|| MaterialiseError::Unevaluated {
construct: "the annotations of an element the model listed".to_string(),
at: OUTER.to_string(),
})?;
let mut seen: Vec<Id<zerodds_ast::AnnotationDefNode>> = Vec::new();
let mut out = Vec::new();
for applied in &facets.annotations {
let Some(def) = applied.definition.resolved() else {
continue;
};
if is_builtin_annotation(ast, def) {
continue;
}
if seen.contains(&def) {
continue;
}
seen.push(def);
let same: Vec<&zerodds_ast::AppliedAnnotation> = facets
.annotations
.iter()
.filter(|other| other.definition.resolved() == Some(def))
.collect();
if let [only] = same.as_slice() {
out.push(applied_annotation(ast, only, def)?);
continue;
}
let mut instances = Vec::new();
for each in same {
instances.push(java::Expr::Annotation(applied_annotation(ast, each, def)?));
}
out.push(java::Annotation {
path: annotation_path(ast, def, true)?,
args: vec![java::AnnotationArg::Value(java::Expr::ArrayInit(instances))],
});
}
Ok(out)
}
fn annotate_declared(
ast: &Ast,
id: Id<TypeNode>,
start: usize,
items: &mut [Pending],
) -> Result<(), MaterialiseError> {
let lost = |construct: &str, at: &str| MaterialiseError::Unevaluated {
construct: construct.to_string(),
at: at.to_string(),
};
let node = ast
.type_(id)
.map_err(|_| lost("a type the model listed", OUTER))?;
let type_name = java::escape_keyword(&node.name);
let type_annotations = user_annotations(ast, zerodds_ast::AnyId::Type(id, node.node_id))?;
let mut by_field: Vec<(String, Vec<java::Annotation>)> = Vec::new();
let mut push_field = |name: String, annotations: Vec<java::Annotation>| {
if !annotations.is_empty() {
by_field.push((name, annotations));
}
};
match &node.kind {
TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
for member in ast.all_members_of(id) {
let member_node = ast
.member(member)
.map_err(|_| lost("a member the model listed", &node.name))?;
push_field(
java::escape_keyword(&member_node.name),
user_annotations(ast, zerodds_ast::AnyId::Member(member, member_node.node_id))?,
);
}
}
TypeKind::Union { .. } => {
for case in ast.cases_of(id) {
let case_node = ast
.case(*case)
.map_err(|_| lost("a union case the model listed", &node.name))?;
let member_node = ast
.member(case_node.member)
.map_err(|_| lost("the member of a union case", &node.name))?;
push_field(
java::escape_keyword(&member_node.name),
user_annotations(
ast,
zerodds_ast::AnyId::Member(case_node.member, member_node.node_id),
)?,
);
}
}
TypeKind::Enumeration { enumerators } => {
for enumerator in enumerators {
let enum_node = ast
.enumerator(*enumerator)
.map_err(|_| lost("an enumerator the model listed", &node.name))?;
push_field(
format!("_{}", enum_node.name),
user_annotations(
ast,
zerodds_ast::AnyId::Enumerator(*enumerator, enum_node.node_id),
)?,
);
}
}
TypeKind::Bitmask { bits } => {
for bit in bits {
let bit_node = ast
.bit_value(*bit)
.map_err(|_| lost("a bitmask bit the model listed", &node.name))?;
push_field(
java::escape_keyword(&bit_node.name),
user_annotations(ast, zerodds_ast::AnyId::BitValue(*bit, bit_node.node_id))?,
);
}
}
TypeKind::Bitset { .. } => {
for bitfield in &ast.all_bitfields_of(id) {
let field_node = ast
.bitfield(*bitfield)
.map_err(|_| lost("a bitset field the model listed", &node.name))?;
if let Some(name) = field_node.name.as_ref() {
push_field(
java::escape_keyword(name),
user_annotations(
ast,
zerodds_ast::AnyId::Bitfield(*bitfield, field_node.node_id),
)?,
);
}
}
}
TypeKind::Primitive(_)
| TypeKind::String { .. }
| TypeKind::Fixed { .. }
| TypeKind::Sequence { .. }
| TypeKind::Array { .. }
| TypeKind::Map { .. }
| TypeKind::Alias { .. }
| TypeKind::ValueBox { .. }
| TypeKind::Native
| TypeKind::Any
| TypeKind::ValueBase
| TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. } => {}
}
for pending in items.iter_mut().skip(start) {
let java::Member::Nested(decl) = &mut pending.member else {
continue;
};
let java::TypeDecl::Class(class) = decl.as_mut() else {
continue;
};
if class.name.as_str() != type_name {
continue;
}
class.annotations.extend(type_annotations.iter().cloned());
for member in &mut class.members {
let java::Member::Field(field) = member else {
continue;
};
for (name, annotations) in &by_field {
if field.name.as_str() == name {
field.annotations.extend(annotations.iter().cloned());
}
}
}
}
Ok(())
}
fn weave_verbatim(ast: &Ast, id: Id<TypeNode>, start: usize, items: &mut Vec<Pending>) {
let slots = verbatim::declaration_slots(ast, id, verbatim::aliases::JAVA);
if slots.before.is_empty()
&& slots.begin.is_empty()
&& slots.end.is_empty()
&& slots.after.is_empty()
{
return;
}
let Ok(node) = ast.type_(id) else {
return;
};
let name = java::escape_keyword(&node.name);
let mut declared = items.split_off(start);
let mut begin = slots.begin;
let mut end = slots.end;
let body = declared
.iter_mut()
.find_map(|pending| match &mut pending.member {
java::Member::Nested(decl) => match decl.as_mut() {
java::TypeDecl::Class(class) if class.name.as_str() == name => Some(class),
_ => None,
},
_ => None,
});
if let Some(class) = body {
for text in begin.drain(..).rev() {
class.members.insert(0, java::Member::Verbatim(text));
}
class
.members
.extend(end.drain(..).map(java::Member::Verbatim));
}
for text in slots.before.into_iter().chain(begin) {
items.push(Pending {
module: node.module,
member: java::Member::Verbatim(text),
});
}
items.append(&mut declared);
for text in end.into_iter().chain(slots.after) {
items.push(Pending {
module: node.module,
member: java::Member::Verbatim(text),
});
}
}
struct Pending {
module: Id<ModuleNode>,
member: java::Member,
}
#[derive(Default)]
struct Scope {
members: Vec<java::Member>,
children: Vec<(Id<ModuleNode>, String, Scope)>,
}
const MAX_MODULE_DEPTH: usize = 64;
const MAX_TYPE_DEPTH: usize = 64;
fn insert(ast: &Ast, root: &mut Scope, item: Pending) -> Result<(), MaterialiseError> {
let chain = module_chain(ast, item.module)?;
let mut scope = root;
for (id, name) in chain {
let at = match scope.children.iter().position(|(seen, _, _)| *seen == id) {
Some(at) => at,
None => {
scope.children.push((id, name, Scope::default()));
scope.children.len() - 1
}
};
scope = &mut scope
.children
.get_mut(at)
.ok_or_else(|| MaterialiseError::NoModule {
at: module_handle(item.module),
})?
.2;
}
scope.members.push(item.member);
Ok(())
}
fn scope_members(
ast: &Ast,
scope: Scope,
depth: usize,
) -> Result<Vec<java::Member>, MaterialiseError> {
if depth > MAX_MODULE_DEPTH {
return Err(MaterialiseError::NoModule {
at: format!("the nesting runs past {MAX_MODULE_DEPTH}"),
});
}
let mut out = scope.members;
for (module, name, child) in scope.children {
let mut class = nested_class(
&name,
scope_members(ast, child, depth + 1)?,
Doc::line("An IDL module, as the class its declarations nest in."),
);
let node = ast.module(module).map_err(|_| MaterialiseError::NoModule {
at: module_handle(module),
})?;
let annotations = user_annotations(ast, zerodds_ast::AnyId::Module(module, node.node_id))?;
if let java::Member::Nested(decl) = &mut class
&& let java::TypeDecl::Class(inner) = decl.as_mut()
{
inner.annotations = annotations;
}
out.push(class);
}
Ok(out)
}
fn module_chain(
ast: &Ast,
module: Id<ModuleNode>,
) -> Result<Vec<(Id<ModuleNode>, String)>, MaterialiseError> {
let mut out = 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: format!(
"{} — the parent chain runs past {MAX_MODULE_DEPTH}",
module_handle(module)
),
});
}
let node = ast.module(id).map_err(|_| MaterialiseError::NoModule {
at: module_handle(id),
})?;
if node.parent.is_none() {
break;
}
out.push((id, java::escape_keyword(&node.name)));
current = node.parent;
depth += 1;
}
out.reverse();
Ok(out)
}
fn module_handle(module: Id<ModuleNode>) -> String {
format!("#{}@{}", module.slot(), module.generation())
}
fn check_names(
members: &[java::Member],
enclosing: &[String],
depth: usize,
) -> Result<(), MaterialiseError> {
if depth > MAX_MODULE_DEPTH {
return Err(MaterialiseError::NoModule {
at: format!("the nesting runs past {MAX_MODULE_DEPTH}"),
});
}
let mut seen: Vec<String> = Vec::new();
for member in members {
let java::Member::Nested(decl) = member else {
continue;
};
let name = decl_name(decl).to_string();
if name.is_empty() {
continue;
}
if enclosing.contains(&name) {
return Err(MaterialiseError::NoLanguageForm {
construct: format!(
"a declaration named `{name}` inside a class of that name \
(JLS 8.1: a nested class may not share the simple name of an \
enclosing one)"
),
language: "Java",
at: name,
});
}
if depth == 0 && RESERVED_TOP_LEVEL.contains(&name.as_str()) {
return Err(MaterialiseError::NoLanguageForm {
construct: format!(
"a declaration named `{name}`, which would capture the first \
segment of every qualified reference this file writes \
(JLS 6.5.2)"
),
language: "Java",
at: name,
});
}
if seen.contains(&name) {
return Err(MaterialiseError::NoLanguageForm {
construct: format!("a second declaration named `{name}` in one scope"),
language: "Java",
at: name,
});
}
let mut inner: Vec<String> = enclosing.to_vec();
inner.push(name.clone());
seen.push(name);
if let java::TypeDecl::Class(class) = decl.as_ref() {
check_names(&class.members, &inner, depth + 1)?;
}
}
Ok(())
}
fn decl_name(decl: &java::TypeDecl) -> &str {
match decl {
java::TypeDecl::Class(node) => node.name.as_str(),
java::TypeDecl::Interface(node) => node.name.as_str(),
java::TypeDecl::Enum(node) => node.name.as_str(),
java::TypeDecl::Record(node) => node.name.as_str(),
java::TypeDecl::AnnotationType(node) => node.name.as_str(),
java::TypeDecl::Verbatim(_) => "",
}
}
#[allow(clippy::too_many_lines)]
fn declare(
ast: &Ast,
applied: &Applied,
id: Id<TypeNode>,
items: &mut Vec<Pending>,
structural: &mut Vec<java::Member>,
emitted: &mut Vec<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 { 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::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),
});
};
let name = map_entry_name(ast, id)?;
let at = named(&node.name, node.node_id);
if !emitted.contains(&name) {
emitted.push(name.clone());
structural.push(nested_class(
&name,
vec![
field(java_member_type(ast, key, &at)?, "key", Doc::none()),
field(java_member_type(ast, value, &at)?, "value", Doc::none()),
],
Doc::line(
"One entry of a map. 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. The codec reads and writes the entries \
of a map in this form, in the order of the map.",
),
));
}
if map_key_supported(ast, key) {
let class = map_class_name(ast, id)?;
if !emitted.contains(&class) {
emitted.push(class.clone());
structural.push(map_class(ast, id, key, value, &class, &name, &at)?);
}
}
}
TypeKind::Alias { aliased } => {
if node.name.is_empty() {
return Ok(());
}
if ghost_created_for(applied, node.node_id, "JavaAlias").is_none() {
return Ok(());
}
let Some(target) = aliased.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "typedef target".to_string(),
at: node.name.clone(),
});
};
let spelling =
zerodds_lang_render::java::type_(&java_member_type(ast, target, &node.name)?);
items.push(Pending {
module: node.module,
member: java::Member::Comment(Doc::line(&format!(
"typedef {} {}; — Java has no alias, so every use takes {spelling}.",
spelling, node.name
))),
});
}
TypeKind::Enumeration { enumerators } => {
if ghost_created_for(applied, node.node_id, "JavaEnum").is_none() {
return Ok(());
}
let mut listed: Vec<(String, i128)> = Vec::new();
for enumerator in enumerators {
let Ok(enum_node) = ast.enumerator(*enumerator) else {
return Err(MaterialiseError::Unevaluated {
construct: "enumerator the model listed".to_string(),
at: node.name.clone(),
});
};
if ghost_created_for(applied, enum_node.node_id, "JavaEnumerator").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 value = in_range(
value,
i128::from(i32::MIN),
i128::from(u32::MAX),
"int",
&enum_node.name,
)?;
let literal = match i32::try_from(value) {
Ok(fits) => i128::from(fits),
Err(_) => value - (i128::from(u32::MAX) + 1),
};
listed.push((java::escape_keyword(&enum_node.name), literal));
}
let class_name = java::escape_keyword(&node.name);
items.push(Pending {
module: node.module,
member: nested_class(
&class_name,
vec![java::Member::Verbatim(enumeration_class_body(
&class_name,
&listed,
))],
Doc::line(
"An enumeration, as CORBA 3.3 part 3 §1.5.9 maps it: one instance \
per enumerator, the int constants `_NAME`, `value()` and \
`from_int`.",
),
),
});
}
TypeKind::Bitmask { bits } => {
if ghost_created_for(applied, node.node_id, "JavaBitmask").is_none() {
return Ok(());
}
let bound = ast.effective_bit_bound(id);
let holder = unsigned_holder(bound);
let wide = holder == java::Type::Primitive(java::Primitive::Long);
let mut members = Vec::new();
let mut flags: Vec<(String, java::Expr)> = Vec::new();
for bit in bits {
let Ok(bit_node) = ast.bit_value(*bit) else {
return Err(MaterialiseError::Unevaluated {
construct: "bitmask bit the model listed".to_string(),
at: node.name.clone(),
});
};
if ghost_created_for(applied, bit_node.node_id, "JavaBitConst").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 flag_value = java::Expr::Binary {
op: "<<",
lhs: Box::new(java::Expr::Literal(Literal::Int(
if wide { "1L" } else { "1" }.to_string(),
))),
rhs: Box::new(java::Expr::Literal(Literal::int(i128::from(position)))),
};
flags.push((bit_node.name.clone(), flag_value.clone()));
members.push(constant(holder.clone(), &bit_node.name, flag_value));
}
items.push(Pending {
module: node.module,
member: type_safe_enumeration(
&format!("{}Bits", java::escape_keyword(&node.name)),
holder.clone(),
flags,
Doc::line(
"The enumeration of the flags of a bitmask (XTypes 1.3 \
7.5.1.2.2): {@code _<label>} is the flag's value, \
{@code 1 << position}.",
),
),
});
items.push(Pending {
module: node.module,
member: nested_class(
&java::escape_keyword(&node.name),
members,
Doc::line(
"A bitmask, as the constants its bits name. A member of this \
type takes the unsigned holder Table 33 of XTypes 1.3 \
7.4.1.1.3.2 gives for its bound, mapped through the unsigned \
rule this file states.",
),
),
});
}
TypeKind::Bitset { .. } => {
if ghost_created_for(applied, node.node_id, "JavaBitset").is_none() {
return Ok(());
}
let mut members = Vec::new();
for bitfield in &ast.all_bitfields_of(id) {
let Ok(field_node) = ast.bitfield(*bitfield) else {
return Err(MaterialiseError::Unevaluated {
construct: "bitset field the model listed".to_string(),
at: node.name.clone(),
});
};
let Some(field_name) = field_node.name.as_ref() else {
continue;
};
members.push(field(
unsigned_holder(field_node.bits),
field_name,
Doc::none(),
));
}
items.push(Pending {
module: node.module,
member: data_class(
&java::escape_keyword(&node.name),
members,
Doc::line(
"A bitset. The fields are separate integers here; the packing \
into the anchor is the codec's.",
),
None,
true,
)?,
});
}
TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
let created = if matches!(node.kind, TypeKind::Valuetype { .. }) {
"JavaValue"
} else {
"JavaStruct"
};
if ghost_created_for(applied, node.node_id, created).is_none() {
return Ok(());
}
let base = match &node.kind {
TypeKind::Structure {
base: Some(base), ..
} => base.resolved(),
_ => None,
};
let own: Vec<_> = ast.members_of(id).to_vec();
let mut members = Vec::new();
let mut spelled = Vec::new();
for member in ast.all_members_of(id) {
let Ok(member_node) = ast.member(member) else {
return Err(MaterialiseError::Unevaluated {
construct: "member the model listed".to_string(),
at: node.name.clone(),
});
};
let Some(target) = member_node.type_.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "member type".to_string(),
at: member_node.name.clone(),
});
};
let type_ = java_member_type(ast, target, &member_node.name)?;
spelled.push((
java::escape_keyword(&member_node.name),
member_node.name.clone(),
));
if base.is_some() && !own.contains(&member) {
continue;
}
if member_node.facets.is_optional() {
members.push(field(
boxed(type_),
&member_node.name,
Doc::line(
"An @optional member: null when absent. In a @mutable type \
an absent member is not written at all; outside one its \
absence is written — a zero is_present byte under XCDR2 \
(rule 20) or a parameter header declaring a length of zero \
under XCDR1 (7.4.1.2).",
),
));
} else if super::annotated_external(member_node) {
members.push(field(
boxed(type_),
&member_node.name,
Doc::line(
"An @external member: an object reference, so a primitive \
is held as its java.lang box (XTypes 1.3 7.5.1.2.3.4).",
),
));
} else {
members.push(field(type_, &member_node.name, Doc::none()));
}
}
no_spelling_collision(
spelled
.iter()
.map(|(field, declared)| (field.as_str(), declared.as_str())),
"Java",
&node.name,
)?;
let extends = match base {
Some(base) => Some(java::Type::named(qualified(ast, base)?)),
None => None,
};
items.push(Pending {
module: node.module,
member: data_class(
&java::escape_keyword(&node.name),
members,
Doc::none(),
extends,
!has_derived(ast, id),
)?,
});
}
TypeKind::Union { discriminator, .. } => {
if ghost_created_for(applied, node.node_id, "JavaUnion").is_none() {
return Ok(());
}
let Some(disc) = discriminator.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "union discriminator".to_string(),
at: node.name.clone(),
});
};
let mut members = vec![java::Member::Field(java::Field {
doc: Doc::line("The discriminator. Which case field is live is what this says."),
annotations: Vec::new(),
modifiers: vec![java::Modifier::Public],
type_: java_member_type(ast, disc, &node.name)?,
name: Ident::new("_d"),
value: None,
})];
let mut accessed: Vec<UnionAccess> = Vec::new();
for case in ast.cases_of(id) {
let Ok(case_node) = ast.case(*case) else {
return Err(MaterialiseError::Unevaluated {
construct: "union case the model listed".to_string(),
at: node.name.clone(),
});
};
if ghost_created_for(applied, case_node.node_id, "JavaCase").is_none() {
continue;
}
let Ok(member) = ast.member(case_node.member) else {
return Err(MaterialiseError::Unevaluated {
construct: "member behind a union case the model listed".to_string(),
at: node.name.clone(),
});
};
let Some(target) = member.type_.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "union case type".to_string(),
at: member.name.clone(),
});
};
let branch = java_member_type(ast, target, &member.name)?;
let case_type = if super::annotated_external(member) {
boxed(branch)
} else {
branch
};
let mut labels = Vec::new();
for candidate in &case_node.labels {
if let zerodds_ast::CaseLabel::Value(expr) = candidate {
let value = ast.eval_integral(*expr).map_err(|_| {
MaterialiseError::Unevaluated {
construct: "union case label".to_string(),
at: node.name.clone(),
}
})?;
labels.push(java_label_text(ast, disc, value));
}
}
accessed.push(UnionAccess {
name: java::escape_keyword(&member.name),
type_: zerodds_lang_render::java::type_(&case_type),
labels,
});
members.push(field(case_type, &member.name, Doc::none()));
}
if mapping() != JavaMapping::Psm {
let disc_type =
zerodds_lang_render::java::type_(&java_member_type(ast, disc, &node.name)?);
members.push(java::Member::Verbatim(union_accessors(
&disc_type, &accessed,
)));
}
items.push(Pending {
module: node.module,
member: data_class(
&java::escape_keyword(&node.name),
members,
Doc::line("A union: the discriminator, and one field per case."),
None,
true,
)?,
});
}
TypeKind::Native => {
if ghost_created_for(applied, node.node_id, "JavaNative").is_none() {
return Ok(());
}
let spelling = zerodds_lang_render::java::type_(&native_type(ast, id));
items.push(Pending {
module: node.module,
member: java::Member::Comment(Doc::line(&format!(
"native {}; — every use takes {spelling}.",
node.name
))),
});
}
TypeKind::ValueBox { .. } => {}
TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. }
| TypeKind::Any
| TypeKind::ValueBase => {}
}
Ok(())
}
pub fn java_member_type(
ast: &Ast,
target: Id<TypeNode>,
at: &str,
) -> Result<java::Type, MaterialiseError> {
type_at(ast, target, at, 0)
}
fn is_octet(ast: &Ast, id: Id<TypeNode>, depth: usize) -> bool {
if depth > MAX_TYPE_DEPTH {
return false;
}
match ast.type_(id).map(|node| &node.kind) {
Ok(TypeKind::Primitive(PrimitiveKind::Octet)) => true,
Ok(TypeKind::Alias { aliased }) => aliased
.resolved()
.is_some_and(|target| is_octet(ast, target, depth + 1)),
_ => false,
}
}
fn type_at(
ast: &Ast,
id: Id<TypeNode>,
at: &str,
depth: usize,
) -> Result<java::Type, MaterialiseError> {
if depth > MAX_TYPE_DEPTH {
return Err(MaterialiseError::Unevaluated {
construct: format!("type, whose Java spelling nests past {MAX_TYPE_DEPTH}"),
at: at.to_string(),
});
}
let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
construct: "type".to_string(),
at: at.to_string(),
})?;
let refused = match &node.kind {
TypeKind::Any => Some("any"),
TypeKind::ValueBase => Some("ValueBase"),
TypeKind::ObjectRef { .. } => Some("Object"),
TypeKind::AnnotationType { .. } => Some("annotation type"),
_ => None,
};
if let Some(construct) = refused {
return Err(MaterialiseError::NoLanguageForm {
construct: construct.to_string(),
language: "Java",
at: at.to_string(),
});
}
Ok(match &node.kind {
TypeKind::Primitive(PrimitiveKind::Float128) => java::Type::named(Path::new(FLOAT128)),
TypeKind::Primitive(kind) => java::Type::Primitive(java_primitive(*kind)),
TypeKind::String { .. } => java::Type::named(Path::new(STRING)),
TypeKind::Fixed { .. } => java::Type::named(Path::new(BIG_DECIMAL)),
TypeKind::Sequence { element, bound } => {
let Some(element) = element.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "sequence element".to_string(),
at: at.to_string(),
});
};
let element = type_at(ast, element, at, depth + 1)?;
if mapping() == JavaMapping::Psm && matches!(bound, zerodds_ast::Bound::Unbounded) {
java::Type::generic(Path::new(COLLECTION), vec![boxed(element)])
} else {
java::Type::Array(Box::new(element))
}
}
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 out = type_at(ast, element, at, depth + 1)?;
for _ in dims {
out = java::Type::Array(Box::new(out));
}
out
}
TypeKind::Map { key, .. }
if key
.resolved()
.is_some_and(|key| map_key_supported(ast, key)) =>
{
java::Type::named(Path::new(&[OUTER, &map_class_name(ast, id)?]))
}
TypeKind::Map { .. } => java::Type::Array(Box::new(java::Type::named(Path::new(&[
OUTER,
&map_entry_name(ast, id)?,
])))),
TypeKind::Alias { aliased } => {
let Some(target) = aliased.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "typedef target".to_string(),
at: at.to_string(),
});
};
type_at(ast, target, at, depth + 1)?
}
TypeKind::Enumeration { .. } => java::Type::named(qualified(ast, id)?),
TypeKind::Bitmask { .. } => unsigned_holder(ast.effective_bit_bound(id)),
TypeKind::Structure { .. }
| TypeKind::Union { .. }
| TypeKind::Bitset { .. }
| TypeKind::Valuetype { .. } => java::Type::named(qualified(ast, id)?),
TypeKind::Native => native_type(ast, id),
TypeKind::ValueBox { boxed: inner } => {
let Some(target) = inner.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "value box target".to_string(),
at: at.to_string(),
});
};
boxed(type_at(ast, target, at, depth + 1)?)
}
TypeKind::Any
| TypeKind::ValueBase
| TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. } => {
return Err(MaterialiseError::NoLanguageForm {
construct: node.kind.as_str().to_string(),
language: "Java",
at: at.to_string(),
});
}
})
}
#[must_use]
pub const fn java_primitive(kind: PrimitiveKind) -> java::Primitive {
match kind {
PrimitiveKind::Boolean => java::Primitive::Boolean,
PrimitiveKind::Char | PrimitiveKind::WChar => java::Primitive::Char,
PrimitiveKind::Int8 => java::Primitive::Byte,
PrimitiveKind::Octet => java::Primitive::Byte,
PrimitiveKind::UInt8 => java::Primitive::Int,
PrimitiveKind::Int16 => java::Primitive::Short,
PrimitiveKind::UInt16 => java::Primitive::Int,
PrimitiveKind::Int32 => java::Primitive::Int,
PrimitiveKind::UInt32 => java::Primitive::Long,
PrimitiveKind::Int64 => java::Primitive::Long,
PrimitiveKind::UInt64 => java::Primitive::Long,
PrimitiveKind::Float32 => java::Primitive::Float,
PrimitiveKind::Float64 | PrimitiveKind::Float128 => java::Primitive::Double,
}
}
#[must_use]
pub fn unsigned_holder(bits: u16) -> java::Type {
if bits <= 16 {
java::Type::Primitive(java::Primitive::Int)
} else {
java::Type::Primitive(java::Primitive::Long)
}
}
fn boxed(type_: java::Type) -> java::Type {
match type_ {
java::Type::Primitive(primitive) => {
java::Type::named(Path::new(&["java", "lang", primitive.boxed()]))
}
other => other,
}
}
fn native_type(ast: &Ast, id: Id<TypeNode>) -> java::Type {
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());
match mapping {
Some(spelling) => java::Type::named(Path::new(&spelling.split('.').collect::<Vec<&str>>())),
None => java::Type::named(Path::new(&["java", "lang", "Object"])),
}
}
fn qualified(ast: &Ast, id: Id<TypeNode>) -> Result<Path, MaterialiseError> {
let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
construct: "type".to_string(),
at: OUTER.to_string(),
})?;
let mut segments = vec![OUTER.to_string()];
for (_, name) in module_chain(ast, node.module)? {
segments.push(name);
}
segments.push(java::escape_keyword(&node.name));
Ok(Path::new(
&segments.iter().map(String::as_str).collect::<Vec<&str>>(),
))
}
pub fn map_entry_name(ast: &Ast, id: Id<TypeNode>) -> Result<String, MaterialiseError> {
let Ok(node) = ast.type_(id) else {
return Err(MaterialiseError::Unevaluated {
construct: "map".to_string(),
at: OUTER.to_string(),
});
};
let TypeKind::Map { key, value, .. } = &node.kind else {
return Err(MaterialiseError::Unevaluated {
construct: "map".to_string(),
at: named(&node.name, node.node_id),
});
};
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),
});
};
Ok(format!(
"{ENTRY_PREFIX}{}_{}",
type_tag(ast, key, 0)?,
type_tag(ast, value, 0)?
))
}
#[must_use]
pub fn entry_class_of_map_class(class: &str) -> Option<String> {
let rest = class.strip_prefix(MAP_PREFIX)?;
let rest = rest.strip_prefix('_').map_or(rest, |tail| {
let after = tail.strip_prefix('b').and_then(|bound| {
let end = bound.find('_')?;
let (digits, remainder) = bound.split_at(end);
let is_bound =
digits == "x" || (!digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit()));
(is_bound && remainder[1..].contains('_')).then(|| &remainder[1..])
});
after.unwrap_or(tail)
});
rest.contains('_').then(|| format!("{ENTRY_PREFIX}{rest}"))
}
pub fn map_class_name(ast: &Ast, id: Id<TypeNode>) -> Result<String, MaterialiseError> {
let Ok(node) = ast.type_(id) else {
return Err(MaterialiseError::Unevaluated {
construct: "map".to_string(),
at: OUTER.to_string(),
});
};
let TypeKind::Map { key, value, bound } = &node.kind else {
return Err(MaterialiseError::Unevaluated {
construct: "map".to_string(),
at: named(&node.name, node.node_id),
});
};
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),
});
};
Ok(format!(
"{MAP_PREFIX}{}_{}_{}",
bound_tag(ast, bound),
type_tag(ast, key, 0)?,
type_tag(ast, value, 0)?
))
}
#[must_use]
pub fn map_key_supported(ast: &Ast, key: Id<TypeNode>) -> bool {
let mut current = key;
for _ in 0..MAX_TYPE_DEPTH {
let Ok(node) = ast.type_(current) else {
return false;
};
match &node.kind {
TypeKind::Primitive(PrimitiveKind::Float128) => return false,
TypeKind::Primitive(_)
| TypeKind::String { .. }
| TypeKind::Enumeration { .. }
| TypeKind::Bitmask { .. }
| TypeKind::Structure { .. }
| TypeKind::Union { .. }
| TypeKind::Bitset { .. } => return true,
TypeKind::Alias { aliased } => match aliased.resolved() {
Some(next) => current = next,
None => return false,
},
_ => return false,
}
}
false
}
fn map_class(
ast: &Ast,
id: Id<TypeNode>,
key: Id<TypeNode>,
value: Id<TypeNode>,
class: &str,
entry: &str,
at: &str,
) -> Result<java::Member, MaterialiseError> {
let bound = match ast.type_(id).map(|node| &node.kind) {
Ok(TypeKind::Map { bound, .. }) => match bound.expr() {
None => 0,
Some(expr) => ast
.eval_integral(expr)
.ok()
.and_then(|limit| u32::try_from(limit).ok())
.ok_or_else(|| MaterialiseError::Unevaluated {
construct: "map bound".to_string(),
at: at.to_string(),
})?,
},
_ => 0,
};
let key_type = zerodds_lang_render::java::type_(&boxed(java_member_type(ast, key, at)?));
let value_type = zerodds_lang_render::java::type_(&boxed(java_member_type(ast, value, at)?));
let entry = format!("{OUTER}.{entry}");
let text = format!(
" private static final long serialVersionUID = 1L;
/** `map<K, V, N>`: N, or 0 for a map with no bound. */
private static final int ZD_BOUND = {bound};
/** DDS 1.4 2.2.2.1.1, ReturnCode_t. */
public static final int RETCODE_OK = 0;
/** DDS 1.4 2.2.2.1.1, ReturnCode_t. */
public static final int RETCODE_ERROR = 1;
/** DDS 1.4 2.2.2.1.1, ReturnCode_t. */
public static final int RETCODE_BAD_PARAMETER = 3;
/** DDS 1.4 2.2.2.1.1, ReturnCode_t. */
public static final int RETCODE_PRECONDITION_NOT_MET = 4;
private int zdMaximum;
/** Table 47 initialize: the size and the capacity are 0. */
public {class}() {{
super();
}}
/**
* Table 47 get_max_size: how many entries the map may contain. A map
* the codec filled holds the entries it read, so it is never below
* the size.
*/
public int getMaxSize() {{
return Math.max(zdMaximum, size());
}}
/**
* Table 47 set_max_size: BAD_PARAMETER if the new maximum is below
* the size or above the bound.
*/
public int setMaxSize(int maxSize) {{
if (maxSize < size() || (ZD_BOUND != 0 && maxSize > ZD_BOUND)) {{
return RETCODE_BAD_PARAMETER;
}}
zdMaximum = maxSize;
return RETCODE_OK;
}}
/**
* Table 47 insert (for {{@code replace}} false) and insert_or_assign (for
* true): an existing key is BAD_PARAMETER or has its element replaced, a
* new one past the capacity is PRECONDITION_NOT_MET. A null key or
* element is BAD_PARAMETER.
*/
public int insert({key_type} key, {value_type} element, boolean replace) {{
if (key == null || element == null) {{
return RETCODE_BAD_PARAMETER;
}}
if (containsKey(key)) {{
if (!replace) {{
return RETCODE_BAD_PARAMETER;
}}
put(key, element);
return RETCODE_OK;
}}
if (size() >= getMaxSize()) {{
return RETCODE_PRECONDITION_NOT_MET;
}}
put(key, element);
return RETCODE_OK;
}}
/**
* Table 47 copy: overwrite this map with {{@code other}}. Too large for
* the capacity, {{@code autogrow}} decides: the capacity grows, or the
* answer is PRECONDITION_NOT_MET and this map stays as it was.
*/
public int copyFrom({class} other, boolean autogrow) {{
if (other == null) {{
return RETCODE_BAD_PARAMETER;
}}
if (other == this) {{
return RETCODE_OK;
}}
if (other.size() > getMaxSize()) {{
if (!autogrow) {{
return RETCODE_PRECONDITION_NOT_MET;
}}
int grown = setMaxSize(other.size());
if (grown != RETCODE_OK) {{
return grown;
}}
}}
clear();
putAll(other);
return RETCODE_OK;
}}
/** The entries, in the order of the map: what the codec writes. */
public {entry}[] entries() {{
{entry}[] out = new {entry}[size()];
int i = 0;
for (java.util.Map.Entry<{key_type}, {value_type}> e : entrySet()) {{
out[i] = new {entry}();
out[i].key = e.getKey();
out[i].value = e.getValue();
i = i + 1;
}}
return out;
}}
/**
* The map of entries, in their order: what the codec reads. A key
* twice is no map and is refused.
*/
public static {class} of({entry}[] entries) {{
{class} map = new {class}();
for ({entry} e : entries) {{
if (map.containsKey(e.key)) {{
throw new java.lang.IllegalArgumentException(\"the map has the key \" + e.key + \" twice\");
}}
map.put(e.key, e.value);
}}
map.zdMaximum = map.size();
return map;
}}
/** No entries: where a reader starts. */
public static {entry}[] noEntries() {{
return new {entry}[0];
}}
"
);
Ok(java::Member::Nested(Box::new(java::TypeDecl::Class(
java::Class {
doc: Doc::lines(&[
"A map (XTypes 1.3 7.5.1.3.5): a java.util.Map in the order the entries",
"were put, with the operations of Table 47 that Java does not have.",
]),
annotations: Vec::new(),
modifiers: vec![
java::Modifier::Public,
java::Modifier::Static,
java::Modifier::Final,
],
name: Ident::new(class),
generics: Vec::new(),
extends: Some(java::Type::generic(
Path::new(&["java", "util", "LinkedHashMap"]),
vec![
boxed(java_member_type(ast, key, at)?),
boxed(java_member_type(ast, value, at)?),
],
)),
implements: Vec::new(),
members: vec![java::Member::Verbatim(text)],
},
))))
}
pub fn declared_path(ast: &Ast, id: Id<TypeNode>) -> Result<String, MaterialiseError> {
let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
construct: "type".to_string(),
at: OUTER.to_string(),
})?;
let mut segments = vec!["ZdTypes".to_string()];
for (_, name) in module_chain(ast, node.module)? {
segments.push(name);
}
segments.push(java::escape_keyword(&node.name));
Ok(segments.join("."))
}
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(),
},
}
}
pub fn type_tag(ast: &Ast, id: Id<TypeNode>, depth: usize) -> Result<String, MaterialiseError> {
if depth > MAX_TYPE_DEPTH {
return Err(MaterialiseError::Unevaluated {
construct: format!("type, whose name nests past {MAX_TYPE_DEPTH}"),
at: OUTER.to_string(),
});
}
let Ok(node) = ast.type_(id) else {
return Ok("unknown".to_string());
};
Ok(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_unknown{}", 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_unknown".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 key = match key.resolved() {
Some(key) => type_tag(ast, key, depth + 1)?,
None => "unknown".to_string(),
};
let value = match value.resolved() {
Some(value) => type_tag(ast, value, depth + 1)?,
None => "unknown".to_string(),
};
format!("map{}_{key}_{value}", bound_tag(ast, bound))
}
TypeKind::Alias { aliased } => match aliased.resolved() {
Some(target) => type_tag(ast, target, depth + 1)?,
None => "unknown".to_string(),
},
TypeKind::ValueBox { boxed: inner } => match inner.resolved() {
Some(target) => format!("box_{}", type_tag(ast, target, depth + 1)?),
None => "box_unknown".to_string(),
},
TypeKind::Structure { .. }
| TypeKind::Union { .. }
| TypeKind::Enumeration { .. }
| TypeKind::Bitmask { .. }
| TypeKind::Bitset { .. }
| TypeKind::Valuetype { .. }
| TypeKind::Native => {
let mut segments = Vec::new();
for (_, name) in module_chain(ast, node.module)? {
segments.push(name);
}
segments.push(java::escape_keyword(&node.name));
segments.join("_")
}
TypeKind::Any => "any".to_string(),
TypeKind::ValueBase => "valuebase".to_string(),
TypeKind::ObjectRef { .. } => "objectref".to_string(),
TypeKind::AnnotationType { .. } => "annotation".to_string(),
})
}
fn nested_class(name: &str, members: Vec<java::Member>, doc: Doc) -> java::Member {
nested_class_extending(name, members, doc, None, true)
}
fn nested_class_extending(
name: &str,
members: Vec<java::Member>,
doc: Doc,
extends: Option<java::Type>,
final_: bool,
) -> java::Member {
let mut modifiers = vec![java::Modifier::Public, java::Modifier::Static];
if final_ {
modifiers.push(java::Modifier::Final);
}
java::Member::Nested(Box::new(java::TypeDecl::Class(java::Class {
doc,
annotations: Vec::new(),
modifiers,
name: Ident::new(name),
generics: Vec::new(),
extends,
implements: Vec::new(),
members,
})))
}
pub(crate) fn has_derived(ast: &Ast, id: Id<TypeNode>) -> bool {
ast.types().any(|(_, node)| {
matches!(
&node.kind,
TypeKind::Structure { base: Some(base), .. } if base.resolved() == Some(id)
)
})
}
fn data_class(
name: &str,
members: Vec<java::Member>,
doc: Doc,
extends: Option<java::Type>,
final_: bool,
) -> Result<java::Member, MaterialiseError> {
let value_methods = equals_and_hash_code(name, &members, extends.is_some());
let mut members = if mapping() == JavaMapping::Psm {
encapsulate(name, members)?
} else {
members
};
members.push(java::Member::Verbatim(value_methods));
Ok(nested_class_extending(name, members, doc, extends, final_))
}
fn equals_and_hash_code(name: &str, members: &[java::Member], derived: bool) -> String {
let fields: Vec<&str> = members
.iter()
.filter_map(|member| match member {
java::Member::Field(data) if !data.modifiers.contains(&java::Modifier::Static) => {
Some(data.name.as_str())
}
_ => None,
})
.collect();
let mut equal = String::new();
if derived {
equal.push_str("super.equals(other)");
}
for field in &fields {
if !equal.is_empty() {
equal.push_str("\n && ");
}
equal.push_str(&format!(
"java.util.Objects.deepEquals({field}, that.{field})"
));
}
if equal.is_empty() {
equal.push_str("true");
}
let mut hashed: Vec<String> = Vec::new();
if derived {
hashed.push("super.hashCode()".to_string());
}
hashed.extend(fields.iter().map(|field| (*field).to_string()));
format!(
" /** Equal when every member is equal; an array by its elements. */
@Override
public boolean equals(Object other) {{
if (this == other) {{
return true;
}}
if (other == null || getClass() != other.getClass()) {{
return false;
}}
{name} that = ({name}) other;
return {equal};
}}
/** Consistent with {{@link #equals(Object)}}. */
@Override
public int hashCode() {{
return java.util.Arrays.deepHashCode(new Object[] {{ {} }});
}}
",
hashed.join(", ")
)
}
fn encapsulate(
class_name: &str,
members: Vec<java::Member>,
) -> Result<Vec<java::Member>, MaterialiseError> {
let mut fields = Vec::new();
let mut accessors = Vec::new();
let mut others = Vec::new();
let mut names: Vec<(String, String)> = Vec::new();
for member in members {
let java::Member::Field(mut data) = member else {
others.push(member);
continue;
};
if data.modifiers.contains(&java::Modifier::Static) {
fields.push(java::Member::Field(data));
continue;
}
let field_name = data.name.as_str().to_string();
let type_ = data.type_.clone();
data.modifiers = vec![java::Modifier::Private];
fields.push(java::Member::Field(data));
let this = |name: &str| java::Expr::Field {
value: Box::new(java::Expr::Path(Path::new(&["this"]))),
name: Ident::new(name),
};
let getter = |name: String| {
java::Member::Method(java::Method {
doc: Doc::none(),
annotations: Vec::new(),
modifiers: vec![java::Modifier::Public],
generics: Vec::new(),
returns: type_.clone(),
name: Ident::new(&name),
params: Vec::new(),
throws: Vec::new(),
body: Some(java::Block {
statements: vec![java::Statement::Return(Some(this(&field_name)))],
}),
})
};
names.push((getter_name(&field_name), field_name.clone()));
accessors.push(getter(getter_name(&field_name)));
if type_ == java::Type::Primitive(java::Primitive::Boolean) {
names.push((boolean_getter_name(&field_name), field_name.clone()));
accessors.push(getter(boolean_getter_name(&field_name)));
}
names.push((setter_name(&field_name), field_name.clone()));
accessors.push(java::Member::Method(java::Method {
doc: Doc::none(),
annotations: Vec::new(),
modifiers: vec![java::Modifier::Public],
generics: Vec::new(),
returns: java::Type::Void,
name: Ident::new(&setter_name(&field_name)),
params: vec![java::Param {
annotations: Vec::new(),
final_: false,
type_: type_.clone(),
name: Ident::new("v"),
varargs: false,
}],
throws: Vec::new(),
body: Some(java::Block {
statements: vec![java::Statement::Expr(java::Expr::Binary {
op: "=",
lhs: Box::new(this(&field_name)),
rhs: Box::new(java::Expr::Path(Path::new(&["v"]))),
})],
}),
}));
}
no_spelling_collision(
names
.iter()
.map(|(spelled, declared)| (spelled.as_str(), declared.as_str())),
"Java accessor",
class_name,
)?;
let mut out = others;
out.extend(fields);
out.push(java::Member::Constructor(java::Constructor {
doc: Doc::none(),
annotations: Vec::new(),
modifiers: vec![java::Modifier::Public],
name: Ident::new(class_name),
params: Vec::new(),
throws: Vec::new(),
body: java::Block {
statements: Vec::new(),
},
}));
out.extend(accessors);
Ok(out)
}
fn field(type_: java::Type, name: &str, doc: Doc) -> java::Member {
java::Member::Field(java::Field {
doc,
annotations: Vec::new(),
modifiers: vec![java::Modifier::Public],
type_,
name: Ident::new(&java::escape_keyword(name)),
value: None,
})
}
fn type_safe_enumeration(
class: &str,
value_type: java::Type,
literals: Vec<(String, java::Expr)>,
doc: Doc,
) -> java::Member {
let own = || java::Type::named(Path::single(class));
let path = |name: &str| java::Expr::Path(Path::single(name));
let mut members = Vec::new();
for (label, value) in &literals {
members.push(constant(
value_type.clone(),
&format!("_{label}"),
value.clone(),
));
}
for (label, _) in &literals {
members.push(constant(
own(),
label,
java::Expr::New {
type_: Box::new(own()),
args: vec![path(&format!("_{label}"))],
},
));
}
members.push(java::Member::Field(java::Field {
doc: Doc::none(),
annotations: Vec::new(),
modifiers: vec![java::Modifier::Private, java::Modifier::Final],
type_: value_type.clone(),
name: Ident::new("__value"),
value: None,
}));
members.push(java::Member::Constructor(java::Constructor {
doc: Doc::none(),
annotations: Vec::new(),
modifiers: vec![java::Modifier::Private],
name: Ident::new(class),
params: vec![java::Param {
annotations: Vec::new(),
final_: false,
type_: value_type.clone(),
name: Ident::new("v"),
varargs: false,
}],
throws: Vec::new(),
body: java::Block {
statements: vec![java::Statement::Expr(java::Expr::Binary {
op: "=",
lhs: Box::new(java::Expr::Field {
value: Box::new(java::Expr::This),
name: Ident::new("__value"),
}),
rhs: Box::new(path("v")),
})],
},
}));
members.push(java::Member::Method(java::Method {
doc: Doc::line("The value the IDL gives the literal."),
annotations: Vec::new(),
modifiers: vec![java::Modifier::Public],
generics: Vec::new(),
returns: value_type.clone(),
name: Ident::new("value"),
params: Vec::new(),
throws: Vec::new(),
body: Some(java::Block {
statements: vec![java::Statement::Return(Some(path("__value")))],
}),
}));
let mut lookup: Vec<java::Statement> = Vec::new();
for (label, _) in &literals {
lookup.push(java::Statement::If {
condition: java::Expr::Binary {
op: "==",
lhs: Box::new(path("v")),
rhs: Box::new(path(&format!("_{label}"))),
},
then: java::Block {
statements: vec![java::Statement::Return(Some(path(&java::escape_keyword(
label,
))))],
},
otherwise: None,
});
}
lookup.push(java::Statement::Throw(java::Expr::New {
type_: Box::new(java::Type::named(Path::new(&[
"java",
"lang",
"IllegalArgumentException",
]))),
args: vec![java::Expr::Binary {
op: "+",
lhs: Box::new(java::Expr::Literal(Literal::Str(format!(
"not a value of {class}: "
)))),
rhs: Box::new(path("v")),
}],
}));
members.push(java::Member::Method(java::Method {
doc: Doc::line("The literal that has the value `v`."),
annotations: Vec::new(),
modifiers: vec![java::Modifier::Public, java::Modifier::Static],
generics: Vec::new(),
returns: own(),
name: Ident::new("from_int"),
params: vec![java::Param {
annotations: Vec::new(),
final_: false,
type_: value_type,
name: Ident::new("v"),
varargs: false,
}],
throws: Vec::new(),
body: Some(java::Block { statements: lookup }),
}));
nested_class(class, members, doc)
}
fn constant(type_: java::Type, name: &str, value: java::Expr) -> java::Member {
java::Member::Field(java::Field {
doc: Doc::none(),
annotations: Vec::new(),
modifiers: vec![
java::Modifier::Public,
java::Modifier::Static,
java::Modifier::Final,
],
type_,
name: Ident::new(&java::escape_keyword(name)),
value: Some(value),
})
}
fn enumeration_class_body(class: &str, enumerators: &[(String, i128)]) -> String {
let mut text = format!(
" private final int zdValue;\n\n private {class}(int value) {{\n zdValue = value;\n }}\n\n"
);
for (name, value) in enumerators {
text.push_str(&format!(
" /** The value of `{name}`. */\n public static final int _{name} = {value};\n /** The instance of `{name}`. */\n public static final {class} {name} = new {class}(_{name});\n\n"
));
}
text.push_str(" /** The value of this enumerator. */\n public int value() {\n return zdValue;\n }\n\n");
text.push_str(&format!(
" /** The enumerator with this value (the first one, where two share it). */\n public static {class} from_int(int value) {{\n"
));
for (name, _) in enumerators {
text.push_str(&format!(
" if (value == _{name}) {{\n return {name};\n }}\n"
));
}
text.push_str(&format!(
" throw new IllegalArgumentException(\"no enumerator of {class} has the value \" + value);\n }}\n"
));
text
}
struct UnionAccess {
name: String,
type_: String,
labels: Vec<String>,
}
fn java_label_text(ast: &Ast, discriminator: Id<TypeNode>, value: i128) -> String {
let mut current = discriminator;
for _ in 0..MAX_TYPE_DEPTH {
match ast.type_(current).map(|node| &node.kind) {
Ok(TypeKind::Alias { aliased }) => match aliased.resolved() {
Some(next) => current = next,
None => break,
},
Ok(TypeKind::Enumeration { .. }) => {
let owner = declared_path(ast, current)
.unwrap_or_else(|_| format!("UnresolvedEnum{}", current.slot()));
let narrowed = i32::try_from(value).unwrap_or_else(|_| {
u32::try_from(value & 0xFFFF_FFFF)
.map_or(0, |bits| i32::from_ne_bytes(bits.to_ne_bytes()))
});
return format!("{owner}.from_int({narrowed})");
}
Ok(TypeKind::Primitive(PrimitiveKind::Boolean)) => {
return if value == 0 { "false" } else { "true" }.to_string();
}
Ok(TypeKind::Primitive(kind)) => {
return match java_primitive(*kind) {
java::Primitive::Byte => format!("(byte) {value}"),
java::Primitive::Short => format!("(short) {value}"),
java::Primitive::Char => format!("(char) {value}"),
java::Primitive::Long => {
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let pattern = if value > i128::from(i64::MAX) {
value as u64 as i64
} else {
value as i64
};
format!("{pattern}L")
}
_ => format!("{value}"),
};
}
_ => break,
}
}
format!("{value}")
}
fn union_accessors(discriminator: &str, cases: &[UnionAccess]) -> String {
let all: Vec<&str> = cases
.iter()
.flat_map(|case| case.labels.iter().map(String::as_str))
.collect();
let any_label = |labels: &[&str]| -> String {
if labels.is_empty() {
"false".to_string()
} else {
labels
.iter()
.map(|label| format!("_d == {label}"))
.collect::<Vec<_>>()
.join(" || ")
}
};
let mut text = format!(
" /** The discriminator. */\n public {discriminator} discriminator() {{\n return _d;\n }}\n\n"
);
for case in cases {
let own: Vec<&str> = case.labels.iter().map(String::as_str).collect();
let selected = if own.is_empty() {
format!("!({})", any_label(&all))
} else {
any_label(&own)
};
let (name, ty) = (&case.name, &case.type_);
text.push_str(&format!(
" /** The value of `{name}`; refused when the discriminator selects another case. */\n public {ty} {name}() {{\n if (!({selected})) {{\n throw new IllegalStateException(\"the discriminator does not select `{name}`\");\n }}\n return {name};\n }}\n\n"
));
if let [only] = own.as_slice() {
text.push_str(&format!(
" /** Sets `{name}` and selects it. */\n public void {name}({ty} value) {{\n this.{name} = value;\n this._d = {only};\n }}\n\n"
));
}
text.push_str(&format!(
" /** Sets `{name}` under the discriminator `disc`, which has to select it. */\n public void {name}({discriminator} disc, {ty} value) {{\n {discriminator} previous = _d;\n _d = disc;\n if (!({selected})) {{\n _d = previous;\n throw new IllegalArgumentException(\"the discriminator does not select `{name}`\");\n }}\n this.{name} = value;\n }}\n\n"
));
}
text
}
fn constant_expr(
type_: &java::Type,
value: &zerodds_ast::ConstValue,
at: &str,
) -> Result<java::Expr, MaterialiseError> {
use zerodds_ast::ConstValue;
let refuse = |construct: String| MaterialiseError::NoLanguageForm {
construct,
language: "Java",
at: at.to_string(),
};
match value {
ConstValue::Integer(number) => literal_of(type_, *number, at),
ConstValue::Boolean(flag) => Ok(java::Expr::Literal(Literal::Bool(*flag))),
ConstValue::Char(character) => Ok(java::Expr::Literal(Literal::Char(*character))),
ConstValue::WChar(point) => match constant::wchar_char(*point)
.filter(|character| u32::from(*character) <= u32::from(u16::MAX))
{
Some(character) => Ok(java::Expr::Literal(Literal::Char(character))),
None => Err(refuse(format!(
"the code point U+{point:04X}, which a Java `char` does not hold"
))),
},
ConstValue::Float { mantissa, exponent } => {
if !matches!(
type_,
java::Type::Primitive(java::Primitive::Float | java::Primitive::Double)
) {
return Err(refuse(String::from(
"a `long double` constant, which has no Java literal",
)));
}
let mut text = constant::float_text(mantissa, exponent.as_deref());
if matches!(type_, java::Type::Primitive(java::Primitive::Float)) {
text.push('f');
}
Ok(java::Expr::Literal(Literal::Float(text)))
}
ConstValue::Fixed { digits, scale } => Ok(java::Expr::New {
type_: Box::new(java::Type::named(Path::new(BIG_DECIMAL))),
args: vec![java::Expr::Literal(Literal::Str(constant::fixed_decimal(
digits, *scale,
)))],
}),
ConstValue::String(text) => Ok(java::Expr::Literal(Literal::Str(text.clone()))),
ConstValue::WString(points) => match constant::wstring_utf8(points) {
Some(text) => Ok(java::Expr::Literal(Literal::Str(text))),
None => Err(refuse(String::from(
"a `wstring` constant holding a lone surrogate",
))),
},
_ => Err(refuse(String::from(
"a constant form this binding has no literal for",
))),
}
}
fn literal_of(type_: &java::Type, value: i128, at: &str) -> Result<java::Expr, MaterialiseError> {
let java::Type::Primitive(primitive) = type_ else {
return Err(MaterialiseError::Unevaluated {
construct: "constant's value, which is not an integer".to_string(),
at: at.to_string(),
});
};
Ok(match primitive {
java::Primitive::Boolean => java::Expr::Literal(Literal::Bool(value != 0)),
java::Primitive::Char => cast(
*primitive,
in_range(value, 0, i128::from(u16::MAX), "char", at)?,
),
java::Primitive::Byte => cast(*primitive, in_range(value, -128, 127, "byte", at)?),
java::Primitive::Short => cast(
*primitive,
in_range(
value,
i128::from(i16::MIN),
i128::from(i16::MAX),
"short",
at,
)?,
),
java::Primitive::Int => java::Expr::Literal(Literal::int(in_range(
value,
i128::from(i32::MIN),
i128::from(i32::MAX),
"int",
at,
)?)),
java::Primitive::Long => {
let value = in_range(
value,
i128::from(i64::MIN),
i128::from(i64::MAX),
"long",
at,
)?;
java::Expr::Literal(Literal::Int(format!("{value}L")))
}
java::Primitive::Float | java::Primitive::Double => {
java::Expr::Literal(Literal::int(value))
}
})
}
fn cast(primitive: java::Primitive, value: i128) -> java::Expr {
java::Expr::Cast {
type_: Box::new(java::Type::Primitive(primitive)),
value: Box::new(java::Expr::Literal(Literal::int(value))),
}
}
fn in_range(
value: i128,
low: i128,
high: i128,
type_: &str,
at: &str,
) -> Result<i128, MaterialiseError> {
if value < low || value > high {
return Err(MaterialiseError::NoLanguageForm {
construct: format!("the value {value}, which a Java `{type_}` does not hold"),
language: "Java",
at: at.to_string(),
});
}
Ok(value)
}
fn named(name: &str, node: NodeId) -> String {
if name.is_empty() {
format!("{node:?}")
} else {
name.to_string()
}
}
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))
})
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use super::*;
use crate::rule;
use crate::{ModelGraph, apply};
use std::path::{Path as FsPath, PathBuf};
#[test]
fn the_entry_class_is_found_from_the_map_class() {
assert_eq!(
entry_class_of_map_class("ZdMap_long_string").as_deref(),
Some("ZdEntry_long_string")
);
assert_eq!(
entry_class_of_map_class("ZdMap_b8_long_string").as_deref(),
Some("ZdEntry_long_string")
);
assert_eq!(
entry_class_of_map_class("ZdMap_bx_unsigned_short_string").as_deref(),
Some("ZdEntry_unsigned_short_string")
);
assert_eq!(entry_class_of_map_class("Other_long_string"), None);
for idl in [
"@final struct P { map<long, string> m; };",
"@final struct P { map<long, string, 8> m; };",
] {
let ast = {
let parsed = zerodds_idl_parse::parse(idl);
let lowered = zerodds_idl_lower::lower(&parsed.syntax(), "t.idl");
let mut ast = lowered.ast;
ast.resolve_all_by_name().expect("resolve");
ast
};
let map = ast
.types()
.find(|(_, node)| matches!(node.kind, TypeKind::Map { .. }))
.map(|(id, _)| id)
.expect("a map type");
assert_eq!(
entry_class_of_map_class(&map_class_name(&ast, map).expect("class")),
Some(map_entry_name(&ast, map).expect("entry")),
"{idl}"
);
}
}
fn render(idl: &str, path: &str) -> Result<String, String> {
with_mapping(JavaMapping::Omg, || render_in_force(idl, path))
}
fn render_in_force(idl: &str, path: &str) -> Result<String, String> {
let parsed = zerodds_idl_parse::parse(idl);
if parsed.has_errors() {
return Err(format!("parse: {:?}", parsed.diagnostics()));
}
let lowered = zerodds_idl_lower::lower(&parsed.syntax(), path);
if !lowered.diagnostics.is_empty() {
return Err(format!("lower: {:?}", lowered.diagnostics));
}
let mut ast = lowered.ast;
ast.resolve_all_by_name()
.map_err(|error| format!("resolve: {error}"))?;
let applied = apply(&ModelGraph::of(&ast), &rule::java::catalogue())
.map_err(|error| format!("apply: {error}"))?;
let types = java_items(&ast, &applied).map_err(|error| format!("materialise: {error}"))?;
Ok(zerodds_lang_render::java::file(&java::File {
doc: Doc::lines(&[
"Generated by zerodds-lang-rules. Do not edit.",
"",
"The Java shape of one IDL file. The wire rules are not here — they",
"are in runtime/java, which implements them from DDS-XTypes 1.3.",
]),
package: None,
imports: Vec::new(),
types,
}))
}
fn declarations(idl: &str) -> String {
match render(idl, "test.idl") {
Ok(text) => text,
Err(why) => panic!("{why}"),
}
}
fn corpus() -> Vec<(String, PathBuf)> {
let root = FsPath::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/interop-corpus");
let mut out = Vec::new();
for entry in std::fs::read_dir(&root).expect("the corpus directory") {
let entry = entry.expect("a corpus entry");
let idl = entry.path().join("type.idl");
if idl.is_file() {
out.push((entry.file_name().to_string_lossy().into_owned(), idl));
}
}
out.sort();
out
}
#[test]
fn every_corpus_case_renders_or_names_its_failure() {
let cases = corpus();
assert!(
cases.len() > 300,
"the corpus is not where it was: {} cases",
cases.len()
);
let out = std::env::var("ZD_JAVA_OUT").ok();
let mut failures = Vec::new();
for (name, path) in cases {
let source = std::fs::read_to_string(&path).expect("a corpus source");
match render(&source, &path.display().to_string()) {
Ok(text) => {
if let Some(dir) = &out {
let case = FsPath::new(dir).join(&name);
std::fs::create_dir_all(&case).expect("the output directory");
std::fs::write(case.join(format!("{OUTER}.java")), text)
.expect("the output file");
}
}
Err(why) => failures.push(format!("{name}: {why}")),
}
}
let failures = crate::adapter::without_the_front_end_refusals(failures);
assert!(failures.is_empty(), "{failures:#?}");
}
#[test]
fn every_idl_primitive_reaches_the_java_type_that_holds_it() {
let generated = declarations(
"@final struct P {
boolean a; octet b; char c; wchar d;
int8 e; uint8 f; short g; unsigned short h;
long i; unsigned long j; long long k; unsigned long long l;
float m; double n; long double o;
};",
);
for expected in [
"public boolean a;",
"public byte b;",
"public char c;",
"public char d;",
"public byte e;",
"public int f;",
"public short g;",
"public int h;",
"public int i;",
"public long j;",
"public long k;",
"public long l;",
"public float m;",
"public double n;",
"public org.zerodds.wire.Float128 o;",
] {
assert!(generated.contains(expected), "{expected}\n{generated}");
}
}
#[test]
fn a_sequence_and_an_array_are_both_java_arrays() {
let generated = declarations(
"@final struct P { sequence<long> s; long a[3]; long g[2][3]; sequence<string> t; };",
);
assert!(generated.contains("public int[] s;"), "{generated}");
assert!(generated.contains("public int[] a;"), "{generated}");
assert!(generated.contains("public int[][] g;"), "{generated}");
assert!(
generated.contains("public java.lang.String[] t;"),
"{generated}"
);
}
#[test]
fn a_map_is_a_java_util_map_over_a_generated_entry_class() {
let generated = declarations("@final struct P { map<long, string> m; };");
assert!(
generated.contains("class ZdEntry_long_string {"),
"{generated}"
);
assert!(generated.contains("public int key;"), "{generated}");
assert!(
generated.contains("public java.lang.String value;"),
"{generated}"
);
assert!(
generated.contains(
"class ZdMap_long_string extends java.util.LinkedHashMap<java.lang.Integer, java.lang.String> {"
),
"{generated}"
);
assert!(
generated.contains("public ZdTypes.ZdMap_long_string m;"),
"{generated}"
);
for operation in [
"public int getMaxSize()",
"public int setMaxSize(int maxSize)",
"public int insert(java.lang.Integer key, java.lang.String element, boolean replace)",
"public int copyFrom(ZdMap_long_string other, boolean autogrow)",
] {
assert!(generated.contains(operation), "{operation}:\n{generated}");
}
}
#[test]
fn a_map_keyed_by_a_structure_is_a_java_util_map_over_a_class_with_equals() {
let generated = declarations("struct K { long a; }; @final struct P { map<K, long> m; };");
assert!(
generated.contains("class ZdMap_K_long extends"),
"{generated}"
);
assert!(
generated.contains("public boolean equals(Object other)")
&& generated.contains("public int hashCode()"),
"{generated}"
);
assert!(!generated.contains("ZdEntry_K_long[] m;"), "{generated}");
}
#[test]
fn a_map_keyed_by_a_sequence_keeps_its_entries() {
let generated = declarations("@final struct P { map<sequence<long>, long> m; };");
assert!(generated.contains("[] m;"), "{generated}");
assert!(!generated.contains("ZdMap_"), "{generated}");
}
#[test]
fn a_bounded_map_has_a_class_of_its_own_with_the_bound() {
let generated =
declarations("@final struct P { map<long, long, 3> a; map<long, long> b; };");
assert!(
generated.contains("class ZdMap_b3_long_long extends"),
"{generated}"
);
assert!(
generated.contains("class ZdMap_long_long extends"),
"{generated}"
);
assert!(
generated.contains("private static final int ZD_BOUND = 3;"),
"{generated}"
);
assert_eq!(generated.matches("class ZdEntry_long_long {").count(), 1);
}
#[test]
fn two_maps_of_one_shape_are_one_entry_class() {
let generated = declarations("@final struct P { map<long, long> a; map<long, long> b; };");
assert_eq!(generated.matches("class ZdEntry_long_long {").count(), 1);
}
#[test]
fn two_maps_of_one_java_shape_and_two_idl_shapes_are_two_entry_classes() {
let generated =
declarations("@final struct P { map<uint8, long> a; map<unsigned short, long> b; };");
assert!(
generated.contains("class ZdEntry_uint8_long {"),
"{generated}"
);
assert!(
generated.contains("class ZdEntry_unsigned_short_long {"),
"{generated}"
);
}
#[test]
fn an_optional_member_is_boxed_and_a_primitive_one_too() {
let generated = declarations(
"@mutable struct P {
@optional long a; @optional string b; @optional long long c;
};",
);
assert!(
generated.contains("public java.lang.Integer a;"),
"{generated}"
);
assert!(
generated.contains("public java.lang.String b;"),
"{generated}"
);
assert!(
generated.contains("public java.lang.Long c;"),
"{generated}"
);
}
#[test]
fn a_user_annotation_is_an_interface_with_its_defaults_and_a_group() {
let generated = declarations(
"@annotation Note { string text default \"none\"; long level; };\n\
@final struct P { long a; };",
);
assert!(
generated.contains("public @interface Note {"),
"{generated}"
);
assert!(
generated.contains("java.lang.String text() default \"none\";"),
"{generated}"
);
assert!(
generated.contains("int level();"),
"no default: {generated}"
);
assert!(
generated.contains("public @interface NoteGroup {"),
"{generated}"
);
assert!(generated.contains("Note[] value();"), "{generated}");
}
#[test]
fn an_applied_annotation_sits_on_the_java_element_and_several_go_through_the_group() {
let generated = declarations(
"@annotation Note { string text default \"none\"; long level; };\n\
@Note(level = 3) @final struct P { @Note(text = \"a\", level = 1) \
@Note(level = 2) long a; long b; };",
);
assert!(
generated.contains("@ZdTypes.Note(level = 3)"),
"{generated}"
);
assert!(
generated.contains(
"@ZdTypes.NoteGroup({@ZdTypes.Note(text = \"a\", level = 1), \
@ZdTypes.Note(level = 2)})"
),
"{generated}"
);
}
#[test]
fn an_annotation_on_a_module_and_on_a_constant_reaches_the_java_element() {
let generated = declarations(
"@annotation Note { long level; };\n\
@Note(level = 5) module M { @Note(level = 6) const long LIMIT = 3; \
@final struct P { long a; }; };",
);
assert!(
generated.contains("@ZdTypes.Note(level = 5)"),
"{generated}"
);
assert!(
generated.contains("@ZdTypes.Note(level = 6)"),
"{generated}"
);
}
#[test]
fn an_external_member_is_an_object_reference_and_a_primitive_one_is_boxed() {
let generated = declarations(
"@final struct P { @external short a; @external long b; @external string c; \
@external sequence<long> d; @external boolean e; };",
);
assert!(
generated.contains("public java.lang.Short a;"),
"{generated}"
);
assert!(
generated.contains("public java.lang.Integer b;"),
"{generated}"
);
assert!(
generated.contains("public java.lang.String c;"),
"{generated}"
);
assert!(generated.contains("public int[] d;"), "{generated}");
assert!(
generated.contains("public java.lang.Boolean e;"),
"{generated}"
);
}
#[test]
fn a_union_is_a_class_with_a_discriminator_and_one_field_per_case() {
let generated = declarations(
"union Choice switch (long) {
case 1: long whole;
case 2: long other;
default: double fraction;
};
@final struct P { Choice c; };",
);
assert!(generated.contains("public int _d;"), "{generated}");
assert!(generated.contains("public int whole;"), "{generated}");
assert!(generated.contains("public int other;"), "{generated}");
assert!(generated.contains("public double fraction;"), "{generated}");
assert!(
generated.contains("public ZdTypes.Choice c;"),
"{generated}"
);
}
#[test]
fn an_enumeration_is_a_class_of_instances_and_a_member_of_it_is_one() {
let generated = declarations(
"enum Colour { RED, GREEN, BLUE };
@final struct P { Colour c; };",
);
assert!(
generated.contains("public static final int _RED = 0;"),
"{generated}"
);
assert!(
generated.contains("public static final Colour RED = new Colour(_RED);"),
"{generated}"
);
assert!(
generated.contains("public static final int _BLUE = 2;"),
"{generated}"
);
assert!(generated.contains("public int value()"), "{generated}");
assert!(
generated.contains("public static Colour from_int(int value)"),
"{generated}"
);
assert!(
generated.contains("public ZdTypes.Colour c;"),
"{generated}"
);
}
#[test]
fn a_construct_with_no_java_data_form_is_named_and_not_left_out() {
for (idl, construct) in [
("@final struct P { any a; long tail; };", "any"),
("@final struct P { Object o; long tail; };", "Object"),
] {
let failure = render(idl, "test.idl").expect_err(construct);
assert!(failure.contains(construct), "{failure}");
assert!(failure.contains("Java"), "{failure}");
}
}
#[test]
fn an_explicit_enumerator_value_is_written_out() {
let generated = declarations("enum E { @value(-1) LOW, @value(70000) HIGH };");
assert!(
generated.contains("public static final int _LOW = -1;"),
"{generated}"
);
assert!(
generated.contains("public static final int _HIGH = 70000;"),
"{generated}"
);
}
#[test]
fn a_bitmask_takes_the_holder_its_bound_calls_for() {
let narrow =
declarations("@bit_bound(8) bitmask Small { @position(0) A, @position(7) B };");
assert!(
narrow.contains("public static final int B = 1 << 7;"),
"{narrow}"
);
let wide = declarations("@bit_bound(64) bitmask Big { @position(0) A, @position(40) B };");
assert!(
wide.contains("public static final long B = 1L << 40;"),
"{wide}"
);
}
#[test]
fn a_bitset_is_a_class_of_one_field_per_bitfield() {
let generated = declarations(
"bitset Packed { bitfield<3> low; bitfield<5> high; };
@final struct P { Packed value; };",
);
assert!(generated.contains("public int low;"), "{generated}");
assert!(generated.contains("public int high;"), "{generated}");
assert!(
generated.contains("public ZdTypes.Packed value;"),
"{generated}"
);
}
#[test]
fn a_fixed_is_a_bigdecimal() {
let generated = declarations(
"typedef fixed<9,3> Money;
@final struct P { Money amount; };",
);
assert!(
generated.contains("public java.math.BigDecimal amount;"),
"{generated}"
);
}
#[test]
fn a_typedef_declares_nothing_and_says_so() {
let generated = declarations(
"typedef sequence<long> Row;
@final struct P { Row r; };",
);
assert!(generated.contains("public int[] r;"), "{generated}");
assert!(generated.contains("typedef int[] Row;"), "{generated}");
}
#[test]
fn a_module_is_a_nested_class_and_two_modules_do_not_collide() {
let generated = declarations(
"module a { @final struct P { long x; }; };
module b { @final struct P { double y; }; };
@final struct Q { a::P one; b::P two; };",
);
assert!(generated.contains("class a {"), "{generated}");
assert!(generated.contains("class b {"), "{generated}");
assert!(generated.contains("public ZdTypes.a.P one;"), "{generated}");
assert!(generated.contains("public ZdTypes.b.P two;"), "{generated}");
}
#[test]
fn a_module_reopened_beside_a_nested_one_is_one_class() {
let generated = declarations(
"module common {
module util { @final struct Location { double x; }; };
@final struct Robot { ::common::util::Location where_; };
};",
);
assert_eq!(
generated.matches("class common {").count(),
1,
"{generated}"
);
assert!(
generated.contains("public ZdTypes.common.util.Location where_;"),
"{generated}"
);
}
#[test]
fn an_identifier_that_is_a_java_keyword_is_escaped() {
let generated = declarations(
"module package {
enum class { template, virtual, friend };
@final struct type { long new; };
};",
);
assert!(generated.contains("class package_ {"), "{generated}");
assert!(generated.contains("class class_ {"), "{generated}");
assert!(generated.contains("class type {"), "{generated}");
assert!(generated.contains("public int new_;"), "{generated}");
assert!(
generated.contains("public static final int _template = 0;"),
"{generated}"
);
}
#[test]
fn a_name_that_would_capture_the_qualified_references_is_refused() {
let failure = render("module java { @final struct P { long x; }; };", "test.idl")
.expect_err("a module named java is refused");
assert!(failure.contains("java"), "{failure}");
}
#[test]
fn a_declaration_named_after_the_outer_class_is_refused() {
let failure = render("@final struct ZdTypes { long x; };", "test.idl")
.expect_err("a type named after the outer class is refused");
assert!(failure.contains("ZdTypes"), "{failure}");
}
#[test]
fn an_unevaluated_array_length_is_named_and_not_substituted() {
let failure = render("@final struct P { long a[UNKNOWN]; };", "test.idl")
.expect_err("an array whose length does not evaluate is refused");
assert!(!failure.is_empty(), "{failure}");
}
}