use crate::adapter::c::ordered_types;
use crate::adapter::constant;
use crate::adapter::cst::MaterialiseError;
use crate::adapter::interface;
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::csharp;
pub const SUPPORT_NAMESPACE: &[&str] = &["ZeroDds", "Idl"];
const IDL_NAME_ATTRIBUTE: &str = "IdlName";
const FLOAT128: &str = "Float128";
const ENTRY_PREFIX: &str = "ZdEntry_";
const CONSTANTS_CLASS: &str = "ZdConstants";
const RESERVED_TOP_LEVEL: &[&str] = &["ZeroDds", "System", "global"];
const MAX_MODULE_DEPTH: usize = 64;
const MAX_TYPE_DEPTH: usize = 64;
const MAX_RENAME_ATTEMPTS: usize = 64;
pub fn csharp_file(ast: &Ast, applied: &Applied) -> Result<csharp::File, MaterialiseError> {
let mut root = Scope::default();
let mut structural: Vec<csharp::Item> = Vec::new();
let mut emitted: Vec<String> = Vec::new();
for id in ordered_types(ast) {
let start = match ast.type_(id) {
Ok(node) => root.at(ast, node.module)?.items.len(),
Err(_) => 0,
};
declare(ast, applied, id, &mut root, &mut structural, &mut emitted)?;
weave_verbatim(ast, id, start, &mut root)?;
}
constants(ast, applied, &mut root)?;
exceptions(ast, applied, &mut root)?;
interfaces(ast, applied, &mut root)?;
let (begin_file, end_file) = verbatim::file_slots(ast, verbatim::aliases::CSHARP);
let mut items: Vec<csharp::Item> = begin_file.into_iter().map(csharp::Item::Verbatim).collect();
items.push(csharp::Item::Namespace(support_namespace()));
items.extend(structural);
let (usings, scope_items) = scope_items(root, 0)?;
items.extend(scope_items);
items.extend(end_file.into_iter().map(csharp::Item::Verbatim));
check_names(&items, 0)?;
Ok(csharp::File {
doc: Doc::none(),
usings,
items,
})
}
fn weave_verbatim(
ast: &Ast,
id: Id<TypeNode>,
start: usize,
root: &mut Scope,
) -> Result<(), MaterialiseError> {
let slots = verbatim::declaration_slots(ast, id, verbatim::aliases::CSHARP);
if slots.before.is_empty()
&& slots.begin.is_empty()
&& slots.end.is_empty()
&& slots.after.is_empty()
{
return Ok(());
}
let Ok(node) = ast.type_(id) else {
return Ok(());
};
let name = csharp::escape_keyword(&node.name);
let scope = root.at(ast, node.module)?;
let mut declared = scope.items.split_off(start);
let mut begin = slots.begin;
let mut end = slots.end;
let body = declared.iter_mut().find_map(|item| match item {
csharp::Item::Type(csharp::TypeDecl::Class(class)) if class.name.as_str() == name => {
Some(&mut class.members)
}
csharp::Item::Type(csharp::TypeDecl::Struct(node)) if node.name.as_str() == name => {
Some(&mut node.members)
}
_ => None,
});
if let Some(members) = body {
for text in begin.drain(..).rev() {
members.insert(0, csharp::Member::Verbatim(text));
}
members.extend(end.drain(..).map(csharp::Member::Verbatim));
}
scope.items.extend(
slots
.before
.into_iter()
.chain(begin)
.map(csharp::Item::Verbatim),
);
scope.items.append(&mut declared);
scope.items.extend(
end.into_iter()
.chain(slots.after)
.map(csharp::Item::Verbatim),
);
Ok(())
}
fn support_namespace() -> csharp::Namespace {
csharp::Namespace {
doc: Doc::lines(&[
"What the generated declarations below are written against.",
"It is emitted into the file rather than referenced from an assembly:",
"one generated file is one compiler argument, and a column that only",
"built against a package nobody has written would be a claim.",
]),
name: Path::new(SUPPORT_NAMESPACE),
usings: Vec::new(),
items: vec![
csharp::Item::Type(csharp::TypeDecl::Class(idl_name_attribute())),
csharp::Item::Type(csharp::TypeDecl::Struct(float128_struct())),
],
}
}
fn idl_name_attribute() -> csharp::Class {
let name_property = csharp::Member::Property(csharp::Property {
doc: Doc::line("The identifier as the IDL source spelled it."),
attributes: Vec::new(),
modifiers: vec![csharp::Modifier::Public],
type_: csharp::Type::Predefined(csharp::Predefined::String),
name: Ident::new("Name"),
accessors: vec![csharp::Accessor {
kind: csharp::AccessorKind::Get,
modifiers: Vec::new(),
body: None,
expression: None,
}],
expression: None,
initialiser: None,
});
let constructor = csharp::Member::Constructor(csharp::Constructor {
doc: Doc::none(),
attributes: Vec::new(),
modifiers: vec![csharp::Modifier::Public],
name: Ident::new(&idl_name_class()),
params: vec![csharp::Param {
attributes: Vec::new(),
modifier: None,
type_: csharp::Type::Predefined(csharp::Predefined::String),
name: Ident::new("name"),
default: None,
}],
initialiser: None,
body: csharp::Block {
statements: vec![csharp::Statement::Expr(csharp::Expr::Assign {
target: Box::new(csharp::Expr::Path(Path::single("Name"))),
value: Box::new(csharp::Expr::Path(Path::single("name"))),
})],
},
});
csharp::Class {
doc: Doc::lines(&[
"The IDL spelling of the member it annotates.",
"",
"Load-bearing, not documentation. A member's ID under @autoid(HASH)",
"and under @hashid is the first four octets of the MD5 of its IDL",
"name (XTypes 1.3 7.3.1.2.1.1), so a field the mapping renamed to",
"PascalCase cannot compute its own ID without this.",
]),
attributes: Vec::new(),
modifiers: vec![csharp::Modifier::Public, csharp::Modifier::Sealed],
name: Ident::new(&idl_name_class()),
generics: Vec::new(),
bases: vec![csharp::Type::named(Path::rooted(&["System", "Attribute"]))],
constraints: Vec::new(),
members: vec![constructor, name_property],
}
}
fn idl_name_class() -> String {
format!("{IDL_NAME_ATTRIBUTE}Attribute")
}
fn float128_struct() -> csharp::Struct {
let field = |name: &str, doc: Doc| {
csharp::Member::Field(csharp::Field {
doc,
attributes: Vec::new(),
modifiers: vec![csharp::Modifier::Public, csharp::Modifier::Readonly],
type_: csharp::Type::Predefined(csharp::Predefined::ULong),
name: Ident::new(name),
value: None,
})
};
let constructor = csharp::Member::Constructor(csharp::Constructor {
doc: Doc::none(),
attributes: Vec::new(),
modifiers: vec![csharp::Modifier::Public],
name: Ident::new(FLOAT128),
params: vec![
csharp::Param {
attributes: Vec::new(),
modifier: None,
type_: csharp::Type::Predefined(csharp::Predefined::ULong),
name: Ident::new("low"),
default: None,
},
csharp::Param {
attributes: Vec::new(),
modifier: None,
type_: csharp::Type::Predefined(csharp::Predefined::ULong),
name: Ident::new("high"),
default: None,
},
],
initialiser: None,
body: csharp::Block {
statements: vec![
csharp::Statement::Expr(csharp::Expr::Assign {
target: Box::new(csharp::Expr::Path(Path::single("Low"))),
value: Box::new(csharp::Expr::Path(Path::single("low"))),
}),
csharp::Statement::Expr(csharp::Expr::Assign {
target: Box::new(csharp::Expr::Path(Path::single("High"))),
value: Box::new(csharp::Expr::Path(Path::single("high"))),
}),
],
},
});
csharp::Struct {
doc: Doc::lines(&[
"IEEE 754 binary128 - the sixteen octets CDR gives a `long double`.",
"",
".NET has no 128-bit floating type. `decimal` is not one: it is a",
"base-ten type with 28 to 29 significant digits and a scale, and a",
"member typed against it would put a different number on the wire",
"than the IDL wrote. What is held here is the pattern - one sign",
"bit, a fifteen-bit exponent biased by 16383 and a 112-bit stored",
"fraction (CORBA 3.3 part 2 9.3.1.3, XTypes 1.3 Table 31) - split",
"into its low and high halves.",
"",
"A `struct` where every other generated aggregate is a `class`,",
"because this is a primitive: fixed at sixteen octets, never absent",
"and never recursive. Arithmetic is not here; the counterparts in",
"crates/float128 and runtime/java hold the same pattern.",
]),
attributes: Vec::new(),
modifiers: vec![csharp::Modifier::Public, csharp::Modifier::Readonly],
name: Ident::new(FLOAT128),
generics: Vec::new(),
bases: Vec::new(),
constraints: Vec::new(),
members: vec![
field("Low", Doc::line("The low 64 bits of the pattern.")),
field(
"High",
Doc::line("The high 64 bits: sign, exponent, top fraction."),
),
constructor,
],
}
}
#[derive(Default)]
struct Scope {
usings: Vec<csharp::Using>,
items: Vec<csharp::Item>,
constants: Vec<csharp::Member>,
children: Vec<(Id<ModuleNode>, String, Scope)>,
}
impl Scope {
fn at(&mut self, ast: &Ast, module: Id<ModuleNode>) -> Result<&mut Self, MaterialiseError> {
let chain = module_chain(ast, module)?;
let mut scope = self;
for (id, name) in chain {
let at = match scope.children.iter().position(|(seen, _, _)| *seen == id) {
Some(at) => at,
None => {
scope.children.push((id, name, Self::default()));
scope.children.len() - 1
}
};
scope = &mut scope
.children
.get_mut(at)
.ok_or_else(|| MaterialiseError::NoModule {
at: module_handle(module),
})?
.2;
}
Ok(scope)
}
}
fn scope_items(
scope: Scope,
depth: usize,
) -> Result<(Vec<csharp::Using>, Vec<csharp::Item>), MaterialiseError> {
if depth > MAX_MODULE_DEPTH {
return Err(MaterialiseError::NoModule {
at: format!("the nesting runs past {MAX_MODULE_DEPTH}"),
});
}
let mut items = scope.items;
if !scope.constants.is_empty() {
items.push(csharp::Item::Type(csharp::TypeDecl::Class(csharp::Class {
doc: Doc::line(
"The constants this scope declares. C# has no namespace-level \
field, so a constant needs a type to sit in; the names inside \
are the IDL's own.",
),
attributes: Vec::new(),
modifiers: vec![csharp::Modifier::Public, csharp::Modifier::Static],
name: Ident::new(CONSTANTS_CLASS),
generics: Vec::new(),
bases: Vec::new(),
constraints: Vec::new(),
members: scope.constants,
})));
}
for (_, name, child) in scope.children {
let (usings, nested) = scope_items(child, depth + 1)?;
items.push(csharp::Item::Namespace(csharp::Namespace {
doc: Doc::line("An IDL module, as the namespace its declarations sit in."),
name: Path::single(&name),
usings,
items: nested,
}));
}
Ok((scope.usings, items))
}
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, csharp::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(items: &[csharp::Item], 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 item in items {
let name = match item {
csharp::Item::Namespace(node) => node
.name
.last()
.map(|last| last.as_str().to_string())
.unwrap_or_default(),
csharp::Item::Type(node) => node.name().as_str().to_string(),
csharp::Item::Comment(_) | csharp::Item::Verbatim(_) => continue,
};
if depth == 0 && RESERVED_TOP_LEVEL.contains(&name.as_str()) {
return Err(MaterialiseError::NoLanguageForm {
construct: format!(
"a declaration named `{name}` at the global scope, which would \
capture the first segment of every `global::` reference this \
file writes"
),
language: "C#",
at: name,
});
}
if seen.contains(&name) {
return Err(MaterialiseError::NoLanguageForm {
construct: format!("a second declaration named `{name}` in one scope"),
language: "C#",
at: name,
});
}
seen.push(name);
if let csharp::Item::Namespace(node) = item {
check_names(&node.items, depth + 1)?;
}
}
Ok(())
}
#[allow(clippy::too_many_lines)]
fn declare(
ast: &Ast,
applied: &Applied,
id: Id<TypeNode>,
root: &mut Scope,
structural: &mut Vec<csharp::Item>,
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)?;
if emitted.contains(&name) {
return Ok(());
}
emitted.push(name.clone());
let at = named(&node.name, node.node_id);
let members = fields_of(
&name,
vec![
PendingField {
idl_name: "key".to_string(),
type_: csharp_member_type(ast, key, &at)?,
doc: Doc::none(),
},
PendingField {
idl_name: "value".to_string(),
type_: csharp_member_type(ast, value, &at)?,
doc: Doc::none(),
},
],
)?;
structural.push(csharp::Item::Type(csharp::TypeDecl::Class(data_class(
&name,
members,
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. An array of these and not a \
Dictionary<K,V>: the entry order is the sample's, and it is \
what the MapEntryOrder axis measures.",
),
))));
}
TypeKind::Alias { aliased } => {
if node.name.is_empty() {
return Ok(());
}
if ghost_created_for(applied, node.node_id, "CSharpAlias").is_none() {
return Ok(());
}
let Some(target) = aliased.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "typedef target".to_string(),
at: node.name.clone(),
});
};
let type_ = csharp_member_type(ast, target, &node.name)?;
root.at(ast, node.module)?.usings.push(csharp::Using {
doc: Doc::none(),
alias: Some(Ident::new(&csharp::escape_keyword(&node.name))),
static_: false,
global: false,
target: type_,
});
}
TypeKind::Native => {
if ghost_created_for(applied, node.node_id, "CSharpNative").is_none() {
return Ok(());
}
if node.name.is_empty() {
return Ok(());
}
root.at(ast, node.module)?.usings.push(csharp::Using {
doc: Doc::line("An IDL `native` type: opaque, mapped by its facet."),
alias: Some(Ident::new(&csharp::escape_keyword(&node.name))),
static_: false,
global: false,
target: native_type(ast, id),
});
}
TypeKind::Enumeration { enumerators } => {
if ghost_created_for(applied, node.node_id, "CSharpEnum").is_none() {
return Ok(());
}
let mut members = Vec::new();
let mut taken = vec![csharp::escape_keyword(&node.name)];
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, "CSharpEnumMember").is_none() {
return Err(MaterialiseError::Unevaluated {
construct: "enumerator, which no rule created a member for".to_string(),
at: enum_node.name.clone(),
});
}
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 = Literal::int(match i32::try_from(value) {
Ok(fits) => i128::from(fits),
Err(_) => value - (i128::from(u32::MAX) + 1),
});
let name = unique(&csharp::escape_keyword(&enum_node.name), &mut taken)?;
members.push(csharp::EnumMember {
doc: Doc::none(),
attributes: vec![idl_name(&enum_node.name)],
name: Ident::new(&name),
value: Some(csharp::Expr::Literal(literal)),
});
}
root.at(ast, node.module)?
.items
.push(csharp::Item::Type(csharp::TypeDecl::Enum(csharp::Enum {
doc: Doc::line(
"An enumeration. A real C# enum and not a set of int \
constants: a member's identity here is the integer it is \
given, so @value(-1) holds, and every value of the \
underlying type is a value of this type, so an \
@appendable writer that added an enumerator sends \
something a field of this type can carry.",
),
attributes: vec![idl_name(&node.name)],
modifiers: vec![csharp::Modifier::Public],
name: Ident::new(&csharp::escape_keyword(&node.name)),
underlying: Some(csharp::Predefined::Int),
members,
})));
}
TypeKind::Bitmask { bits } => {
if ghost_created_for(applied, node.node_id, "CSharpBitmask").is_none() {
return Ok(());
}
let bound = ast.effective_bit_bound(id);
let holder = unsigned_holder(bound);
let mut members = Vec::new();
let mut taken = vec![csharp::escape_keyword(&node.name)];
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, "CSharpBitMember").is_none() {
return Err(MaterialiseError::Unevaluated {
construct: "bitmask bit, which no rule created a member for".to_string(),
at: bit_node.name.clone(),
});
}
let position = ast.effective_bit_position(*bit).ok_or_else(|| {
MaterialiseError::Unevaluated {
construct: "bit position".to_string(),
at: bit_node.name.clone(),
}
})?;
let value = 1_i128.checked_shl(u32::from(position)).ok_or_else(|| {
MaterialiseError::NoLanguageForm {
construct: format!("a bit at position {position}"),
language: "C#",
at: bit_node.name.clone(),
}
})?;
let name = unique(&csharp::escape_keyword(&bit_node.name), &mut taken)?;
members.push(csharp::EnumMember {
doc: Doc::line(&format!("Bit {position}.")),
attributes: vec![idl_name(&bit_node.name)],
name: Ident::new(&name),
value: Some(csharp::Expr::Literal(Literal::int(value))),
});
}
root.at(ast, node.module)?
.items
.push(csharp::Item::Type(csharp::TypeDecl::Enum(csharp::Enum {
doc: Doc::line(
"A bitmask, as the flags enum its bits name. The underlying \
type is what Table 33 of XTypes 1.3 7.4.1.1.3.2 gives for \
its bound; [Flags] changes how ToString prints a value and \
nothing about the octets.",
),
attributes: vec![
csharp::Attribute {
target: None,
path: Path::rooted(&["System", "Flags"]),
args: Vec::new(),
},
idl_name(&node.name),
],
modifiers: vec![csharp::Modifier::Public],
name: Ident::new(&csharp::escape_keyword(&node.name)),
underlying: Some(holder),
members,
})));
}
TypeKind::Bitset { .. } => {
if ghost_created_for(applied, node.node_id, "CSharpBitset").is_none() {
return Ok(());
}
let name = csharp::escape_keyword(&node.name);
let mut pending = 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;
};
pending.push(PendingField {
idl_name: field_name.clone(),
type_: csharp::Type::Predefined(unsigned_holder(field_node.bits)),
doc: Doc::none(),
});
}
let members = fields_of(&name, pending)?;
root.at(ast, node.module)?
.items
.push(csharp::Item::Type(csharp::TypeDecl::Class(data_class(
&name,
members,
Doc::line(
"A bitset. The fields are separate integers here; the \
packing into the anchor is the codec's.",
),
))));
}
TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
let created = if matches!(node.kind, TypeKind::Valuetype { .. }) {
"CSharpValue"
} else {
"CSharpClass"
};
if ghost_created_for(applied, node.node_id, created).is_none() {
return Ok(());
}
let name = csharp::escape_keyword(&node.name);
let mut pending = Vec::new();
for member in ast.members_of(id).iter().copied() {
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_ = csharp_member_type(ast, target, &member_node.name)?;
if member_node.facets.is_optional() {
pending.push(PendingField {
idl_name: member_node.name.clone(),
type_: optional(ast, target, type_)?,
doc: 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 {
pending.push(PendingField {
idl_name: member_node.name.clone(),
type_,
doc: Doc::none(),
});
}
}
let members = fields_of(&name, pending)?;
let mut class = data_class(&name, members, Doc::none());
if let Some(base) = super::c::struct_base(ast, id) {
class.bases.push(csharp_member_type(ast, base, &node.name)?);
}
if super::java::has_derived(ast, id) {
class
.modifiers
.retain(|modifier| *modifier != csharp::Modifier::Sealed);
}
root.at(ast, node.module)?
.items
.push(csharp::Item::Type(csharp::TypeDecl::Class(class)));
}
TypeKind::Union { discriminator, .. } => {
if ghost_created_for(applied, node.node_id, "CSharpUnion").is_none() {
return Ok(());
}
let Some(disc) = discriminator.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "union discriminator".to_string(),
at: node.name.clone(),
});
};
let name = csharp::escape_keyword(&node.name);
let mut pending = vec![PendingField {
idl_name: "_d".to_string(),
type_: csharp_member_type(ast, disc, &node.name)?,
doc: Doc::line(
"The discriminator. Which case field is live is what this says. \
`_d` is the CORBA accessor's name and not an IDL member, so \
the IdlName below carries it rather than a name from the \
source.",
),
}];
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, "CSharpCase").is_none() {
return Err(MaterialiseError::Unevaluated {
construct: "union case, which no rule created a field for".to_string(),
at: node.name.clone(),
});
}
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(),
});
};
pending.push(PendingField {
idl_name: member.name.clone(),
type_: csharp_member_type(ast, target, &member.name)?,
doc: Doc::none(),
});
}
let members = fields_of(&name, pending)?;
root.at(ast, node.module)?
.items
.push(csharp::Item::Type(csharp::TypeDecl::Class(data_class(
&name,
members,
Doc::line("A union: the discriminator, and one field per case."),
))));
}
TypeKind::ValueBox { .. } => {}
TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. }
| TypeKind::Any
| TypeKind::ValueBase => {}
}
Ok(())
}
fn constants(ast: &Ast, applied: &Applied, root: &mut Scope) -> Result<(), MaterialiseError> {
for (id, node) in ast.constants() {
if ghost_created_for(applied, node.node_id, "CSharpConst").is_none() {
continue;
}
let _ = id;
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) => csharp_member_type(ast, target, &node.name)?,
None => csharp::Type::Predefined(csharp::Predefined::Int),
};
let value = constant_expr(&type_, &value, &node.name)?;
let name = csharp::escape_keyword(&node.name);
root.at(ast, node.module)?
.constants
.push(csharp::Member::Field(csharp::Field {
doc: Doc::none(),
attributes: vec![idl_name(&node.name)],
modifiers: vec![csharp::Modifier::Public, csharp::Modifier::Const],
type_,
name: Ident::new(&name),
value: Some(value),
}));
}
Ok(())
}
fn exceptions(ast: &Ast, applied: &Applied, root: &mut Scope) -> Result<(), MaterialiseError> {
for (id, node) in ast.exceptions() {
if ghost_created_for(applied, node.node_id, "CSharpError").is_none() {
continue;
}
let _ = id;
let name = csharp::escape_keyword(&node.name);
let mut pending = 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(),
});
};
pending.push(PendingField {
idl_name: member_node.name.clone(),
type_: csharp_member_type(ast, target, &member_node.name)?,
doc: Doc::none(),
});
}
let members = fields_of(&name, pending)?;
root.at(ast, node.module)?
.items
.push(csharp::Item::Type(csharp::TypeDecl::Class(data_class(
&name,
members,
Doc::lines(&[
"An exception, as the structure its members make.",
"",
"A data holder and not a System.Exception: an IDL member named",
"`message` or `data` folds onto one of System.Exception's own",
"members, and hiding an inherited member (CS0108) is not a",
"decision the wire form makes.",
]),
))));
}
Ok(())
}
fn csharp_param(
ast: &Ast,
param: &interface::Parameter,
) -> Result<csharp::Param, MaterialiseError> {
Ok(csharp::Param {
attributes: Vec::new(),
modifier: match param.direction {
zerodds_ast::ParamDirection::In => None,
zerodds_ast::ParamDirection::Out => Some(csharp::ParamModifier::Out),
zerodds_ast::ParamDirection::InOut => Some(csharp::ParamModifier::Ref),
},
type_: csharp_member_type(ast, param.type_, ¶m.name)?,
name: Ident::new(&csharp::escape_keyword(¶m.name)),
default: None,
})
}
fn interfaces(ast: &Ast, applied: &Applied, root: &mut Scope) -> Result<(), MaterialiseError> {
for (id, node) in ast.interfaces() {
if ghost_created_for(applied, node.node_id, "CSharpInterface").is_none() {
continue;
}
let name = csharp::escape_keyword(&node.name);
let mut members = Vec::new();
let mut taken = vec![name.clone()];
let operations = interface::all_operations_flattened(ast, id)
.map_err(|error| interface::interface_error_to_materialise(error, &node.name))?;
for op in operations
.iter()
.filter(|op| op.origin == interface::Origin::Declared)
{
let mut params = Vec::new();
for param in &op.params {
params.push(csharp_param(ast, param)?);
}
let returns = match op.returns {
None => csharp::Type::Predefined(csharp::Predefined::Void),
Some(target) => csharp_member_type(ast, target, &op.name)?,
};
let method = unique(&csharp::escape_keyword(&capitalise(&op.name)), &mut taken)?;
let doc = op
.raises_line()
.map_or_else(Doc::none, |line| Doc::line(&line));
members.push(csharp::Member::Method(csharp::Method {
doc,
attributes: vec![idl_name(&op.name)],
modifiers: Vec::new(),
returns,
name: Ident::new(&method),
generics: Vec::new(),
params,
constraints: Vec::new(),
body: None,
expression: None,
}));
}
let attributes = interface::all_attributes_flattened(ast, id)
.map_err(|error| interface::interface_error_to_materialise(error, &node.name))?;
for attribute in &attributes {
let type_ = csharp_member_type(ast, attribute.type_, &attribute.name)?;
let property = unique(
&csharp::escape_keyword(&capitalise(&attribute.name)),
&mut taken,
)?;
let mut doc = Doc::none();
if let Some(line) = attribute.get_raises_line() {
doc.push(&format!("Getter: {line}"));
}
if let Some(line) = attribute.set_raises_line() {
doc.push(&format!("Setter: {line}"));
}
let mut accessors = vec![csharp::Accessor {
kind: csharp::AccessorKind::Get,
modifiers: Vec::new(),
body: None,
expression: None,
}];
if !attribute.readonly {
accessors.push(csharp::Accessor {
kind: csharp::AccessorKind::Set,
modifiers: Vec::new(),
body: None,
expression: None,
});
}
members.push(csharp::Member::Property(csharp::Property {
doc,
attributes: vec![idl_name(&attribute.name)],
modifiers: Vec::new(),
type_,
name: Ident::new(&property),
accessors,
expression: None,
initialiser: None,
}));
}
root.at(ast, node.module)?
.items
.push(csharp::Item::Type(csharp::TypeDecl::Interface(
csharp::Interface {
doc: Doc::lines(&[
"An interface, as the operation signatures and attribute",
"properties it has.",
"",
"The shape and not the transport: how a call is carried is the RPC",
"arc's, and the Rust column's `interface-to-trait` stops in the same",
"place. `raises`/`getraises`/`setraises` have no C# clause - the",
"language has no checked exception - and are documented on each",
"member instead; `oneway` is a transport property, so it is not",
"written here either.",
"",
"A derived interface carries its bases' operations and attributes",
"too: this declaration has no base list, so IDL 4.2 7.4.3's",
"inherited members are written into it rather than reached through",
"one.",
]),
attributes: vec![idl_name(&node.name)],
modifiers: vec![csharp::Modifier::Public],
name: Ident::new(&name),
generics: Vec::new(),
bases: Vec::new(),
constraints: Vec::new(),
members,
},
)));
}
Ok(())
}
struct PendingField {
idl_name: String,
type_: csharp::Type,
doc: Doc,
}
fn fields_of(
type_name: &str,
pending: Vec<PendingField>,
) -> Result<Vec<csharp::Member>, MaterialiseError> {
let mut taken = vec![type_name.to_string()];
let mut out = Vec::new();
for field in pending {
let folded = field_name(&field.idl_name);
let name = unique(&folded, &mut taken)?;
out.push(csharp::Member::Field(csharp::Field {
doc: field.doc,
attributes: vec![idl_name(&field.idl_name)],
modifiers: vec![csharp::Modifier::Public],
type_: field.type_,
name: Ident::new(&name),
value: None,
}));
}
Ok(out)
}
fn unique(name: &str, taken: &mut Vec<String>) -> Result<String, MaterialiseError> {
let mut candidate = name.to_string();
for _ in 0..MAX_RENAME_ATTEMPTS {
if !taken.contains(&candidate) {
taken.push(candidate.clone());
return Ok(candidate);
}
candidate.push('_');
}
Err(MaterialiseError::NoLanguageForm {
construct: format!(
"the name `{name}`, which will not come apart from the others in its \
scope within {MAX_RENAME_ATTEMPTS} attempts"
),
language: "C#",
at: name.to_string(),
})
}
fn idl_name(name: &str) -> csharp::Attribute {
let mut segments: Vec<&str> = SUPPORT_NAMESPACE.to_vec();
segments.push(IDL_NAME_ATTRIBUTE);
csharp::Attribute {
target: None,
path: Path::rooted(&segments),
args: vec![csharp::AttributeArg::Positional(csharp::Expr::Literal(
Literal::string(name),
))],
}
}
#[must_use]
pub fn field_name(idl: &str) -> String {
csharp::escape_keyword(&capitalise(idl))
}
pub fn declared_path(ast: &Ast, id: Id<TypeNode>) -> Result<String, MaterialiseError> {
let path = qualified(ast, id)?;
let mut out = String::from("global::");
let mut first = true;
for segment in path.segments() {
if !first {
out.push('.');
}
out.push_str(segment.as_str());
first = false;
}
Ok(out)
}
pub fn drop_support_namespace(file: &mut csharp::File) -> bool {
let before = file.items.len();
file.items.retain(|item| match item {
csharp::Item::Namespace(node) => {
node.name.segments().len() != SUPPORT_NAMESPACE.len()
|| !node
.name
.segments()
.iter()
.zip(SUPPORT_NAMESPACE)
.all(|(written, wanted)| written.as_str() == *wanted)
}
_ => true,
});
file.items.len() != before
}
fn capitalise(name: &str) -> String {
let mut chars = name.chars();
chars.next().map_or_else(String::new, |first| {
first.to_uppercase().collect::<String>() + chars.as_str()
})
}
fn data_class(name: &str, members: Vec<csharp::Member>, doc: Doc) -> csharp::Class {
csharp::Class {
doc,
attributes: vec![idl_name(name)],
modifiers: vec![csharp::Modifier::Public, csharp::Modifier::Sealed],
name: Ident::new(name),
generics: Vec::new(),
bases: Vec::new(),
constraints: Vec::new(),
members,
}
}
pub fn csharp_member_type(
ast: &Ast,
target: Id<TypeNode>,
at: &str,
) -> Result<csharp::Type, MaterialiseError> {
type_at(ast, target, at, 0)
}
fn type_at(
ast: &Ast,
id: Id<TypeNode>,
at: &str,
depth: usize,
) -> Result<csharp::Type, MaterialiseError> {
if depth > MAX_TYPE_DEPTH {
return Err(MaterialiseError::Unevaluated {
construct: format!("type, whose C# 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: "C#",
at: at.to_string(),
});
}
Ok(match &node.kind {
TypeKind::Primitive(PrimitiveKind::Float128) => {
let mut segments: Vec<&str> = SUPPORT_NAMESPACE.to_vec();
segments.push(FLOAT128);
csharp::Type::named(Path::rooted(&segments))
}
TypeKind::Primitive(kind) => csharp::Type::Predefined(csharp_primitive(*kind)),
TypeKind::String { .. } | TypeKind::Fixed { .. } => {
csharp::Type::Predefined(csharp::Predefined::String)
}
TypeKind::Sequence { element, .. } => {
let Some(element) = element.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "sequence element".to_string(),
at: at.to_string(),
});
};
csharp::Type::array(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: at.to_string(),
});
};
let mut out = type_at(ast, element, at, depth + 1)?;
for _ in dims {
out = csharp::Type::array(out);
}
out
}
TypeKind::Map { .. } => {
csharp::Type::array(csharp::Type::named(Path::rooted(&[&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::Structure { .. }
| TypeKind::Union { .. }
| TypeKind::Enumeration { .. }
| TypeKind::Bitmask { .. }
| TypeKind::Bitset { .. }
| TypeKind::Valuetype { .. } => csharp::Type::named(qualified(ast, id)?),
TypeKind::Native => native_type(ast, id),
TypeKind::ValueBox { boxed } => {
let Some(target) = boxed.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "value box target".to_string(),
at: at.to_string(),
});
};
let inner = type_at(ast, target, at, depth + 1)?;
optional(ast, target, inner)?
}
TypeKind::Any
| TypeKind::ValueBase
| TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. } => {
return Err(MaterialiseError::NoLanguageForm {
construct: node.kind.as_str().to_string(),
language: "C#",
at: at.to_string(),
});
}
})
}
#[must_use]
pub const fn csharp_primitive(kind: PrimitiveKind) -> csharp::Predefined {
match kind {
PrimitiveKind::Boolean => csharp::Predefined::Bool,
PrimitiveKind::Char | PrimitiveKind::WChar => csharp::Predefined::Char,
PrimitiveKind::Int8 => csharp::Predefined::SByte,
PrimitiveKind::Octet | PrimitiveKind::UInt8 => csharp::Predefined::Byte,
PrimitiveKind::Int16 => csharp::Predefined::Short,
PrimitiveKind::UInt16 => csharp::Predefined::UShort,
PrimitiveKind::Int32 => csharp::Predefined::Int,
PrimitiveKind::UInt32 => csharp::Predefined::UInt,
PrimitiveKind::Int64 => csharp::Predefined::Long,
PrimitiveKind::UInt64 => csharp::Predefined::ULong,
PrimitiveKind::Float32 => csharp::Predefined::Float,
PrimitiveKind::Float64 | PrimitiveKind::Float128 => csharp::Predefined::Double,
}
}
#[must_use]
pub const fn unsigned_holder(bits: u16) -> csharp::Predefined {
match bits {
0..=8 => csharp::Predefined::Byte,
9..=16 => csharp::Predefined::UShort,
17..=32 => csharp::Predefined::UInt,
_ => csharp::Predefined::ULong,
}
}
fn optional(
ast: &Ast,
target: Id<TypeNode>,
type_: csharp::Type,
) -> Result<csharp::Type, MaterialiseError> {
if is_value_type(ast, target, 0)? {
return Ok(csharp::Type::Nullable(Box::new(type_)));
}
Ok(type_)
}
#[must_use]
pub fn is_nullable_wrapped(ast: &Ast, target: Id<TypeNode>) -> bool {
is_value_type(ast, target, 0).unwrap_or(false)
}
fn is_value_type(ast: &Ast, id: Id<TypeNode>, depth: usize) -> Result<bool, MaterialiseError> {
if depth > MAX_TYPE_DEPTH {
return Err(MaterialiseError::Unevaluated {
construct: format!("type, whose chain nests past {MAX_TYPE_DEPTH}"),
at: "an @optional member".to_string(),
});
}
let Ok(node) = ast.type_(id) else {
return Ok(false);
};
Ok(match &node.kind {
TypeKind::Primitive(_) => true,
TypeKind::Enumeration { .. } | TypeKind::Bitmask { .. } => true,
TypeKind::Alias { aliased } => match aliased.resolved() {
Some(target) => is_value_type(ast, target, depth + 1)?,
None => false,
},
TypeKind::String { .. }
| TypeKind::Fixed { .. }
| TypeKind::Sequence { .. }
| TypeKind::Array { .. }
| TypeKind::Map { .. }
| TypeKind::Structure { .. }
| TypeKind::Union { .. }
| TypeKind::Bitset { .. }
| TypeKind::Valuetype { .. }
| TypeKind::ValueBox { .. }
| TypeKind::Native
| TypeKind::Any
| TypeKind::ValueBase
| TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. } => false,
})
}
fn native_type(ast: &Ast, id: Id<TypeNode>) -> csharp::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) => {
let segments: Vec<&str> = spelling.split("::").flat_map(|s| s.split('.')).collect();
csharp::Type::named(Path::new(&segments))
}
None => csharp::Type::Predefined(csharp::Predefined::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: "a member".to_string(),
})?;
let mut segments = Vec::new();
for (_, name) in module_chain(ast, node.module)? {
segments.push(name);
}
segments.push(csharp::escape_keyword(&node.name));
Ok(Path::rooted(
&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: ENTRY_PREFIX.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)?
))
}
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: ENTRY_PREFIX.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 } => match boxed.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(csharp::escape_keyword(&node.name));
segments.join("_").replace('@', "")
}
TypeKind::Any => "any".to_string(),
TypeKind::ValueBase => "valuebase".to_string(),
TypeKind::ObjectRef { .. } => "objectref".to_string(),
TypeKind::AnnotationType { .. } => "annotation".to_string(),
})
}
fn constant_expr(
type_: &csharp::Type,
value: &zerodds_ast::ConstValue,
at: &str,
) -> Result<csharp::Expr, MaterialiseError> {
use zerodds_ast::ConstValue;
let refuse = |construct: String| MaterialiseError::NoLanguageForm {
construct,
language: "C#",
at: at.to_string(),
};
match value {
ConstValue::Integer(number) => literal_of(type_, *number, at),
ConstValue::Boolean(flag) => Ok(csharp::Expr::Literal(Literal::Bool(*flag))),
ConstValue::Char(character) => Ok(csharp::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(csharp::Expr::Literal(Literal::Char(character))),
None => Err(refuse(format!(
"the code point U+{point:04X}, which a C# `char` does not hold"
))),
},
ConstValue::Float { mantissa, exponent } => {
let csharp::Type::Predefined(
predefined @ (csharp::Predefined::Float | csharp::Predefined::Double),
) = type_
else {
return Err(refuse(String::from(
"a `long double` constant, which has no C# literal",
)));
};
let mut text = constant::float_text(mantissa, exponent.as_deref());
if matches!(predefined, csharp::Predefined::Float) {
text.push('f');
}
Ok(csharp::Expr::Literal(Literal::Float(text)))
}
ConstValue::Fixed { digits, scale } => Ok(csharp::Expr::Literal(Literal::Str(
constant::fixed_decimal(digits, *scale),
))),
ConstValue::String(text) => Ok(csharp::Expr::Literal(Literal::Str(text.clone()))),
ConstValue::WString(points) => match constant::wstring_utf8(points) {
Some(text) => Ok(csharp::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_: &csharp::Type,
value: i128,
at: &str,
) -> Result<csharp::Expr, MaterialiseError> {
let (predefined, enum_path) = match type_ {
csharp::Type::Predefined(predefined) => (*predefined, None),
csharp::Type::Named { path, .. } => (csharp::Predefined::Int, Some(path.clone())),
_ => {
return Err(MaterialiseError::Unevaluated {
construct: "constant's value, which is not an integer".to_string(),
at: at.to_string(),
});
}
};
let literal = match predefined {
csharp::Predefined::Bool => {
return Ok(csharp::Expr::Literal(Literal::Bool(value != 0)));
}
csharp::Predefined::Char => {
let point = in_range(value, 0, i128::from(u16::MAX), "char", at)?;
return Ok(csharp::Expr::Cast {
type_: Box::new(csharp::Type::Predefined(csharp::Predefined::Char)),
value: Box::new(csharp::Expr::Literal(Literal::int(point))),
});
}
csharp::Predefined::SByte => in_range(value, -128, 127, "sbyte", at)?,
csharp::Predefined::Byte => in_range(value, 0, 255, "byte", at)?,
csharp::Predefined::Short => in_range(
value,
i128::from(i16::MIN),
i128::from(i16::MAX),
"short",
at,
)?,
csharp::Predefined::UShort => in_range(value, 0, i128::from(u16::MAX), "ushort", at)?,
csharp::Predefined::Int => {
in_range(value, i128::from(i32::MIN), i128::from(i32::MAX), "int", at)?
}
csharp::Predefined::UInt => in_range(value, 0, i128::from(u32::MAX), "uint", at)?,
csharp::Predefined::Long => in_range(
value,
i128::from(i64::MIN),
i128::from(i64::MAX),
"long",
at,
)?,
csharp::Predefined::ULong => in_range(value, 0, i128::from(u64::MAX), "ulong", at)?,
csharp::Predefined::Float | csharp::Predefined::Double => value,
_ => {
return Err(MaterialiseError::Unevaluated {
construct: "constant's value, which is not an integer".to_string(),
at: at.to_string(),
});
}
};
let expr = csharp::Expr::Literal(Literal::int(literal));
match enum_path {
None => Ok(expr),
Some(path) => Ok(csharp::Expr::Cast {
type_: Box::new(csharp::Type::named(path)),
value: Box::new(expr),
}),
}
}
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 C# `{type_}` does not hold"),
language: "C#",
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};
fn render(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::csharp::catalogue())
.map_err(|error| format!("apply: {error}"))?;
let mut file =
csharp_file(&ast, &applied).map_err(|error| format!("materialise: {error}"))?;
file.doc = Doc::lines(&[
"Generated by zerodds-lang-rules. Do not edit.",
"",
"The C# shape of one IDL file. The wire rules are not here - they",
"come from DDS-XTypes 1.3 and belong to the codec.",
]);
Ok(zerodds_lang_render::csharp::file(&file))
}
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_CSHARP_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("ZdTypes.cs"), text).expect("the output file");
}
}
Err(why) => failures.push(format!("{name}: {why}")),
}
}
match render(UNREACHABLE_BY_THE_CORPUS, "unreachable-constructs.idl") {
Ok(text) => {
if let Some(dir) = &out {
let case = FsPath::new(dir).join("_unreachable-constructs");
std::fs::create_dir_all(&case).expect("the output directory");
std::fs::write(case.join("ZdTypes.cs"), text).expect("the output file");
}
}
Err(why) => failures.push(format!("_unreachable-constructs: {why}")),
}
let failures = crate::adapter::without_the_front_end_refusals(failures);
assert!(failures.is_empty(), "{failures:#?}");
}
const UNREACHABLE_BY_THE_CORPUS: &str = "
module reach {
const long MAX = 10;
const string NAME = \"zerodds\";
const double RATIO = 1.5;
typedef sequence<long> Row;
native Handle;
exception Overrun { long limit; string message; };
valuetype Node { public long id; public string label; };
interface Robot {
long move(in long distance, out double travelled, inout long budget);
void halt();
};
@final struct Sample { Row r; Handle h; Node n; long tail; };
};
";
#[test]
fn every_idl_primitive_reaches_the_csharp_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 bool A;",
"public byte B;",
"public char C;",
"public char D;",
"public sbyte E;",
"public byte F;",
"public short G;",
"public ushort H;",
"public int I;",
"public uint J;",
"public long K;",
"public ulong L;",
"public float M;",
"public double N;",
"public global::ZeroDds.Idl.Float128 O;",
] {
assert!(generated.contains(expected), "{expected}\n{generated}");
}
}
#[test]
fn every_field_carries_the_idl_name_it_was_folded_from() {
let generated = declarations("@final struct P { long venue; long my_field; };");
assert!(
generated.contains("[global::ZeroDds.Idl.IdlName(\"venue\")]"),
"{generated}"
);
assert!(generated.contains("public int Venue;"), "{generated}");
assert!(generated.contains("public int My_field;"), "{generated}");
}
#[test]
fn two_members_that_differ_only_after_the_fold_stay_two_members() {
let generated = declarations("@final struct P { long my_field; long myField; };");
assert!(generated.contains("public int My_field;"), "{generated}");
assert!(generated.contains("public int MyField;"), "{generated}");
}
#[test]
fn a_member_that_would_collide_with_its_type_cannot_come_from_idl() {
let refused = render("@final struct Point { long point; };", "test.idl");
assert!(
refused.is_err(),
"§7.2.3 stops this before C# sees it: {refused:?}"
);
let generated = declarations("@final struct MyField { long my_field; };");
assert!(
generated.contains("public sealed class MyField"),
"{generated}"
);
assert!(generated.contains("public int My_field;"), "{generated}");
}
#[test]
fn a_sequence_and_an_array_are_both_csharp_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 string[] T;"), "{generated}");
}
#[test]
fn a_map_is_an_array_of_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 string Value;"), "{generated}");
assert!(
generated.contains("public global::ZdEntry_long_string[] M;"),
"{generated}"
);
assert!(
!generated.contains("public global::System.Collections"),
"{generated}"
);
}
#[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_whose_csharp_spelling_coincides_are_still_two_entry_classes() {
let generated = declarations("@final struct P { map<char, long> a; map<wchar, long> b; };");
assert!(generated.contains("class ZdEntry_char_long"), "{generated}");
assert!(
generated.contains("class ZdEntry_wchar_long"),
"{generated}"
);
}
#[test]
fn an_optional_value_type_is_nullable_and_a_reference_one_is_not() {
let generated = declarations(
"@mutable struct P {
@optional long a; @optional string b; @optional long long c;
};",
);
assert!(generated.contains("public int? A;"), "{generated}");
assert!(generated.contains("public string B;"), "{generated}");
assert!(!generated.contains("string?"), "{generated}");
assert!(generated.contains("public long? C;"), "{generated}");
}
#[test]
fn an_optional_enum_is_nullable_because_an_enum_is_a_value_type() {
let generated = declarations(
"enum Colour { RED, GREEN };
@mutable struct P { @optional Colour c; };",
);
assert!(
generated.contains("public global::Colour? C;"),
"{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 global::Choice C;"),
"{generated}"
);
}
#[test]
fn an_enumeration_is_a_real_enum_and_a_member_of_it_is_that_type() {
let generated = declarations(
"enum Colour { RED, GREEN, BLUE };
@final struct P { Colour c; };",
);
assert!(
generated.contains("public enum Colour : int"),
"{generated}"
);
assert!(generated.contains("RED = 0,"), "{generated}");
assert!(generated.contains("BLUE = 2,"), "{generated}");
assert!(
generated.contains("public global::Colour C;"),
"{generated}"
);
}
#[test]
fn an_explicit_enumerator_value_is_written_out() {
let generated = declarations("enum E { @value(-1) LOW, @value(70000) HIGH };");
assert!(generated.contains("LOW = -1,"), "{generated}");
assert!(generated.contains("HIGH = 70000,"), "{generated}");
}
#[test]
fn a_bitmask_is_a_flags_enum_over_the_holder_its_bound_calls_for() {
let narrow =
declarations("@bit_bound(8) bitmask Small { @position(0) A, @position(7) B };");
assert!(narrow.contains("public enum Small : byte"), "{narrow}");
assert!(narrow.contains("[global::System.Flags]"), "{narrow}");
assert!(narrow.contains("B = 128,"), "{narrow}");
let wide = declarations("@bit_bound(64) bitmask Big { @position(0) A, @position(40) B };");
assert!(wide.contains("public enum Big : ulong"), "{wide}");
assert!(wide.contains("B = 1099511627776,"), "{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 byte Low;"), "{generated}");
assert!(generated.contains("public byte High;"), "{generated}");
assert!(
generated.contains("public global::Packed Value;"),
"{generated}"
);
}
#[test]
fn a_fixed_is_a_string_and_not_a_decimal() {
let generated = declarations(
"typedef fixed<9,3> Money;
@final struct P { Money amount; };",
);
assert!(generated.contains("public string Amount;"), "{generated}");
assert!(!generated.contains("public decimal"), "{generated}");
}
#[test]
fn a_typedef_is_a_using_alias_and_every_use_takes_the_resolved_type() {
let generated = declarations(
"typedef sequence<long> Row;
@final struct P { Row r; };",
);
assert!(generated.contains("using Row = int[];"), "{generated}");
assert!(generated.contains("public int[] R;"), "{generated}");
}
#[test]
fn a_module_is_a_namespace_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("namespace a"), "{generated}");
assert!(generated.contains("namespace b"), "{generated}");
assert!(generated.contains("public global::a.P One;"), "{generated}");
assert!(generated.contains("public global::b.P Two;"), "{generated}");
}
#[test]
fn a_module_reopened_beside_a_nested_one_is_one_namespace_block() {
let generated = declarations(
"module common {
module util { @final struct Location { double x; }; };
@final struct Robot { ::common::util::Location where_; };
};",
);
assert_eq!(
generated.matches("namespace common\n").count(),
1,
"{generated}"
);
assert!(
generated.contains("public global::common.util.Location Where_;"),
"{generated}"
);
}
#[test]
fn an_identifier_that_is_a_csharp_keyword_is_escaped_with_an_at_sign() {
let generated = declarations(
"module namespace {
enum class { template, virtual, friend };
@final struct type { long new; };
};",
);
assert!(generated.contains("namespace @namespace"), "{generated}");
assert!(generated.contains("enum @class"), "{generated}");
assert!(generated.contains("class type"), "{generated}");
assert!(generated.contains("public int New;"), "{generated}");
assert!(
generated.contains("[global::ZeroDds.Idl.IdlName(\"new\")]"),
"{generated}"
);
assert!(generated.contains("@virtual = 1,"), "{generated}");
assert!(generated.contains("template = 0,"), "{generated}");
}
#[test]
fn a_constant_becomes_a_const_field_of_a_holder_class() {
let generated = declarations(
"const long MAX = 10;
const string NAME = \"zerodds\";
const unsigned long WIDE = 4294967295;",
);
assert!(generated.contains("class ZdConstants"), "{generated}");
assert!(
generated.contains("public const int MAX = 10;"),
"{generated}"
);
assert!(
generated.contains("public const string NAME = \"zerodds\";"),
"{generated}"
);
assert!(
generated.contains("public const uint WIDE = 4294967295;"),
"{generated}"
);
}
#[test]
fn a_construct_with_no_csharp_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("C#"), "{failure}");
}
}
#[test]
fn a_name_that_would_capture_the_rooted_references_is_refused() {
for module in ["System", "ZeroDds"] {
let idl = format!("module {module} {{ @final struct P {{ long x; }}; }};");
let failure = render(&idl, "test.idl").expect_err(module);
assert!(failure.contains(module), "{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}");
}
#[test]
fn an_interface_is_an_interface_of_signatures_with_the_idl_passing_modes() {
let generated = declarations(
"interface Robot {
long move(in long distance, out double travelled, inout long budget);
void halt();
};",
);
assert!(generated.contains("public interface Robot"), "{generated}");
assert!(
generated.contains("int Move(int distance, out double travelled, ref int budget);"),
"{generated}"
);
assert!(generated.contains("void Halt();"), "{generated}");
assert!(
generated.contains("[global::ZeroDds.Idl.IdlName(\"move\")]"),
"{generated}"
);
}
#[test]
fn an_exception_is_a_data_class_and_not_a_system_exception() {
let generated = declarations("exception Overrun { long limit; string message; };");
assert!(
generated.contains("public sealed class Overrun"),
"{generated}"
);
assert!(
!generated.contains(": global::System.Exception"),
"{generated}"
);
assert!(generated.contains("public int Limit;"), "{generated}");
assert!(generated.contains("public string Message;"), "{generated}");
}
#[test]
fn a_valuetype_is_a_class_of_its_state_members() {
let generated = declarations(
"valuetype Node { public long id; public string label; };
@final struct P { Node n; };",
);
assert!(
generated.contains("public sealed class Node"),
"{generated}"
);
assert!(generated.contains("public int Id;"), "{generated}");
assert!(generated.contains("public string Label;"), "{generated}");
assert!(generated.contains("public global::Node N;"), "{generated}");
}
#[test]
fn a_native_type_takes_an_alias_and_object_where_no_facet_names_one() {
let generated = declarations(
"native Handle;
@final struct P { Handle h; };",
);
assert!(generated.contains("using Handle = object;"), "{generated}");
assert!(generated.contains("public object H;"), "{generated}");
}
#[test]
fn every_generated_file_carries_the_support_namespace() {
let generated = declarations("@final struct P { long x; };");
assert!(generated.contains("namespace ZeroDds.Idl"), "{generated}");
assert!(
generated.contains("class IdlNameAttribute : global::System.Attribute"),
"{generated}"
);
assert!(
generated.contains("public readonly struct Float128"),
"{generated}"
);
}
}