use std::collections::BTreeSet;
use std::fmt::Write;
use zerodds_idl::ast::{
Annotation, Case, CaseLabel, ConstExpr, ConstrTypeDecl, Declarator, Definition, EnumDef,
ExceptDecl, Export, InterfaceDcl, InterfaceDef, Literal, LiteralKind, Member, ModuleDef,
OpDecl, ParamAttribute, ScopedName, Specification, StateVisibility, StructDcl, StructDef,
SwitchTypeSpec, TypeDecl, TypeSpec, TypedefDecl, UnionDcl, UnionDef, ValueDef, ValueElement,
};
use zerodds_idl::semantics::annotations::PlacementKind;
use crate::CsGenOptions;
use crate::annotations::{is_nested_type, member_attributes, type_attributes};
use crate::bitset::{emit_bitmask, emit_bitset};
use crate::error::CsGenError;
use crate::keywords::escape_identifier;
use crate::type_map::primitive_to_cs;
use crate::verbatim::emit_verbatim_at;
#[derive(Debug, Default, Clone)]
struct Usings {
imports: BTreeSet<&'static str>,
}
impl Usings {
fn add(&mut self, h: &'static str) {
self.imports.insert(h);
}
}
pub(crate) fn emit_source(spec: &Specification, opts: &CsGenOptions) -> Result<String, CsGenError> {
detect_inheritance_cycles(spec)?;
let mut usings = Usings::default();
usings.add("System");
collect_usings(spec, &mut usings);
let mut out = String::new();
write_preamble(&mut out, &usings)?;
let mut ctx = EmitCtx::new(opts);
let outer_prefix: Option<&str> = opts.root_namespace.as_deref().filter(|p| !p.is_empty());
if let Some(prefix) = outer_prefix {
ctx.open_namespace(&mut out, prefix)?;
}
for d in &spec.definitions {
if let Some(anns) = top_level_annotations(d) {
emit_verbatim_at(&mut out, "", anns, PlacementKind::BeginFile)?;
}
}
for d in &spec.definitions {
emit_definition(&mut out, &mut ctx, d)?;
}
for d in &spec.definitions {
if let Some(anns) = top_level_annotations(d) {
emit_verbatim_at(&mut out, "", anns, PlacementKind::EndFile)?;
}
}
if let Some(prefix) = outer_prefix {
ctx.close_namespace(&mut out, prefix)?;
}
Ok(out)
}
fn top_level_annotations(d: &Definition) -> Option<&[Annotation]> {
match d {
Definition::Module(m) => Some(&m.annotations),
Definition::Type(TypeDecl::Constr(c)) => match c {
ConstrTypeDecl::Struct(StructDcl::Def(s)) => Some(&s.annotations),
ConstrTypeDecl::Union(UnionDcl::Def(u)) => Some(&u.annotations),
ConstrTypeDecl::Enum(e) => Some(&e.annotations),
_ => None,
},
Definition::Type(TypeDecl::Typedef(t)) => Some(&t.annotations),
Definition::Const(c) => Some(&c.annotations),
Definition::Except(e) => Some(&e.annotations),
_ => None,
}
}
fn write_preamble(out: &mut String, usings: &Usings) -> Result<(), CsGenError> {
writeln!(out, "// Generated by zerodds idl-csharp. Do not edit.").map_err(fmt_err)?;
writeln!(out, "// Target: C# 12.0 / .NET 8+").map_err(fmt_err)?;
writeln!(out, "#nullable enable").map_err(fmt_err)?;
writeln!(out).map_err(fmt_err)?;
for u in &usings.imports {
writeln!(out, "using {u};").map_err(fmt_err)?;
}
writeln!(out).map_err(fmt_err)?;
Ok(())
}
fn collect_usings(spec: &Specification, u: &mut Usings) {
for d in &spec.definitions {
collect_in_def(d, u);
}
}
fn collect_in_def(d: &Definition, u: &mut Usings) {
match d {
Definition::Module(m) => {
for sub in &m.definitions {
collect_in_def(sub, u);
}
}
Definition::Type(td) => collect_in_typedecl(td, u),
Definition::Const(_) => {}
Definition::Except(e) => {
for m in &e.members {
collect_in_typespec(&m.type_spec, u);
if !member_attributes(&m.annotations).attrs.is_empty() {
u.add("Omg.Types");
}
}
}
Definition::Interface(_)
| Definition::ValueBox(_)
| Definition::ValueForward(_)
| Definition::ValueDef(_)
| Definition::TypeId(_)
| Definition::TypePrefix(_)
| Definition::Import(_)
| Definition::Component(_)
| Definition::Home(_)
| Definition::Event(_)
| Definition::Porttype(_)
| Definition::Connector(_)
| Definition::TemplateModule(_)
| Definition::TemplateModuleInst(_)
| Definition::Annotation(_)
| Definition::VendorExtension(_) => {
}
}
}
fn collect_in_typedecl(td: &TypeDecl, u: &mut Usings) {
match td {
TypeDecl::Constr(c) => match c {
ConstrTypeDecl::Struct(StructDcl::Def(s)) => {
if !is_nested_type(&s.annotations) {
u.add("Omg.Types");
u.add("ZeroDDS.Cdr");
}
if !type_attributes(&s.annotations).attrs.is_empty() {
u.add("Omg.Types");
}
for m in &s.members {
collect_in_typespec(&m.type_spec, u);
if !member_attributes(&m.annotations).attrs.is_empty() {
u.add("Omg.Types");
}
}
}
ConstrTypeDecl::Struct(StructDcl::Forward(_)) => {}
ConstrTypeDecl::Union(UnionDcl::Def(u_def)) => {
if !is_nested_type(&u_def.annotations) {
u.add("Omg.Types");
}
if !type_attributes(&u_def.annotations).attrs.is_empty() {
u.add("Omg.Types");
}
for c in &u_def.cases {
collect_in_typespec(&c.element.type_spec, u);
}
}
ConstrTypeDecl::Union(UnionDcl::Forward(_)) => {}
ConstrTypeDecl::Enum(_) | ConstrTypeDecl::Bitset(_) | ConstrTypeDecl::Bitmask(_) => {}
},
TypeDecl::Typedef(t) => {
collect_in_typespec(&t.type_spec, u);
}
}
}
fn collect_in_typespec(ts: &TypeSpec, u: &mut Usings) {
match ts {
TypeSpec::Primitive(_) => {}
TypeSpec::Scoped(_) => {}
TypeSpec::Sequence(s) => {
u.add("System.Collections.Generic");
u.add("Omg.Types");
collect_in_typespec(&s.elem, u);
}
TypeSpec::String(_) => {
}
TypeSpec::Map(m) => {
u.add("System.Collections.Generic");
collect_in_typespec(&m.key, u);
collect_in_typespec(&m.value, u);
}
TypeSpec::Fixed(_) => {
}
TypeSpec::Any => {
}
}
}
struct EmitCtx<'o> {
opts: &'o CsGenOptions,
indent_level: usize,
module_path: Vec<String>,
}
impl<'o> EmitCtx<'o> {
fn new(opts: &'o CsGenOptions) -> Self {
Self {
opts,
indent_level: 0,
module_path: Vec::new(),
}
}
fn indent(&self) -> String {
" ".repeat(self.indent_level * self.opts.indent_width)
}
fn open_namespace(&mut self, out: &mut String, name: &str) -> Result<(), CsGenError> {
let escaped = escape_identifier(name)?;
writeln!(out, "{}namespace {escaped}", self.indent()).map_err(fmt_err)?;
writeln!(out, "{}{{", self.indent()).map_err(fmt_err)?;
self.indent_level += 1;
Ok(())
}
fn close_namespace(&mut self, out: &mut String, name: &str) -> Result<(), CsGenError> {
self.indent_level = self.indent_level.saturating_sub(1);
writeln!(out, "{}}} // namespace {name}", self.indent()).map_err(fmt_err)?;
Ok(())
}
}
fn emit_definition(
out: &mut String,
ctx: &mut EmitCtx<'_>,
def: &Definition,
) -> Result<(), CsGenError> {
match def {
Definition::Module(m) => emit_module(out, ctx, m),
Definition::Type(td) => emit_type_decl(out, ctx, td),
Definition::Const(c) => emit_const_decl(out, ctx, c),
Definition::Except(e) => emit_exception(out, ctx, e),
Definition::Interface(InterfaceDcl::Def(iface)) => {
emit_interface_stub(out, ctx, iface)
}
Definition::Interface(InterfaceDcl::Forward(_)) => Ok(()),
Definition::ValueDef(v) => emit_value_type(out, ctx, v),
Definition::ValueBox(_) | Definition::ValueForward(_) => Ok(()),
Definition::TypeId(_)
| Definition::TypePrefix(_)
| Definition::Import(_)
| Definition::Component(_)
| Definition::Home(_)
| Definition::Event(_)
| Definition::Porttype(_)
| Definition::Connector(_)
| Definition::TemplateModule(_)
| Definition::TemplateModuleInst(_) => Err(CsGenError::UnsupportedConstruct {
construct: "corba/ccm/template construct".into(),
context: None,
}),
Definition::Annotation(_) => Ok(()), Definition::VendorExtension(v) => Err(CsGenError::UnsupportedConstruct {
construct: format!("vendor-extension:{}", v.production_name),
context: None,
}),
}
}
fn emit_module(out: &mut String, ctx: &mut EmitCtx<'_>, m: &ModuleDef) -> Result<(), CsGenError> {
ctx.open_namespace(out, &m.name.text)?;
ctx.module_path.push(m.name.text.clone());
for d in &m.definitions {
emit_definition(out, ctx, d)?;
}
ctx.module_path.pop();
ctx.close_namespace(out, &m.name.text)?;
Ok(())
}
fn emit_type_decl(
out: &mut String,
ctx: &mut EmitCtx<'_>,
td: &TypeDecl,
) -> Result<(), CsGenError> {
match td {
TypeDecl::Constr(c) => match c {
ConstrTypeDecl::Struct(StructDcl::Def(s)) => emit_struct(out, ctx, s),
ConstrTypeDecl::Struct(StructDcl::Forward(f)) => {
let name = escape_identifier(&f.name.text)?;
writeln!(out, "{}public partial record class {name};", ctx.indent())
.map_err(fmt_err)?;
Ok(())
}
ConstrTypeDecl::Union(UnionDcl::Def(u)) => emit_union(out, ctx, u),
ConstrTypeDecl::Union(UnionDcl::Forward(f)) => {
let name = escape_identifier(&f.name.text)?;
writeln!(out, "{}public partial record class {name};", ctx.indent())
.map_err(fmt_err)?;
Ok(())
}
ConstrTypeDecl::Enum(e) => emit_enum(out, ctx, e),
ConstrTypeDecl::Bitset(b) => {
let ind = ctx.indent();
let inner = " ".repeat((ctx.indent_level + 1) * ctx.opts.indent_width);
emit_bitset(out, &ind, &inner, b)
}
ConstrTypeDecl::Bitmask(b) => {
let ind = ctx.indent();
let inner = " ".repeat((ctx.indent_level + 1) * ctx.opts.indent_width);
emit_bitmask(out, &ind, &inner, b)
}
},
TypeDecl::Typedef(t) => emit_typedef(out, ctx, t),
}
}
fn emit_struct(out: &mut String, ctx: &mut EmitCtx<'_>, s: &StructDef) -> Result<(), CsGenError> {
let _name = escape_identifier(&s.name.text)?;
let _ = _name;
emit_struct_inner(out, ctx, s)?;
if !is_nested_type(&s.annotations) {
let ind = ctx.indent();
let inner = " ".repeat((ctx.indent_level + 1) * ctx.opts.indent_width);
let deeper = " ".repeat((ctx.indent_level + 2) * ctx.opts.indent_width);
let tctx = crate::typesupport::TsEmitContext {
module_path: &ctx.module_path,
indent: &ind,
inner_indent: &inner,
deeper_indent: &deeper,
};
crate::typesupport::emit_typesupport_class(out, &tctx, s)?;
}
emit_verbatim_at(
out,
&ctx.indent(),
&s.annotations,
PlacementKind::AfterDeclaration,
)?;
Ok(())
}
fn emit_struct_inner(
out: &mut String,
ctx: &mut EmitCtx<'_>,
s: &StructDef,
) -> Result<(), CsGenError> {
let name = escape_identifier(&s.name.text)?;
let ind = ctx.indent();
emit_verbatim_at(out, &ind, &s.annotations, PlacementKind::BeforeDeclaration)?;
let type_attrs = type_attributes(&s.annotations);
for a in &type_attrs.attrs {
writeln!(out, "{ind}{a}").map_err(fmt_err)?;
}
let topic_marker = !is_nested_type(&s.annotations);
let mut bases: Vec<String> = Vec::new();
if let Some(base) = &s.base {
bases.push(scoped_to_cs(base)?);
}
if topic_marker {
bases.push(format!("ITopicType<{name}>"));
}
if bases.is_empty() {
writeln!(out, "{ind}public partial record class {name}").map_err(fmt_err)?;
} else {
let joined = bases.join(", ");
writeln!(out, "{ind}public partial record class {name} : {joined}").map_err(fmt_err)?;
}
writeln!(out, "{ind}{{").map_err(fmt_err)?;
let inner = " ".repeat((ctx.indent_level + 1) * ctx.opts.indent_width);
emit_verbatim_at(out, &inner, &s.annotations, PlacementKind::BeginDeclaration)?;
for m in &s.members {
emit_struct_member_property(out, &inner, m)?;
}
emit_verbatim_at(out, &inner, &s.annotations, PlacementKind::EndDeclaration)?;
writeln!(out, "{ind}}}").map_err(fmt_err)?;
writeln!(out).map_err(fmt_err)?;
Ok(())
}
fn emit_struct_member_property(
out: &mut String,
inner: &str,
m: &Member,
) -> Result<(), CsGenError> {
let attrs = member_attributes(&m.annotations);
for decl in &m.declarators {
let cs_ty = type_for_declarator(&m.type_spec, decl)?;
let name = escape_identifier(&decl.name().text)?;
let pascal = pascal_case(&decl.name().text);
let prop_name = if name == pascal { name.clone() } else { pascal };
let storage_ty = if attrs.optional {
format!("{cs_ty}?")
} else {
cs_ty
};
for a in &attrs.attrs {
writeln!(out, "{inner}{a}").map_err(fmt_err)?;
}
writeln!(
out,
"{inner}public {storage_ty} {prop_name} {{ get; init; }} = default!;"
)
.map_err(fmt_err)?;
}
Ok(())
}
fn emit_union(out: &mut String, ctx: &mut EmitCtx<'_>, u: &UnionDef) -> Result<(), CsGenError> {
let name = escape_identifier(&u.name.text)?;
let ind = ctx.indent();
let inner = " ".repeat((ctx.indent_level + 1) * ctx.opts.indent_width);
let disc_ty = switch_type_to_cs(&u.switch_type)?;
emit_verbatim_at(out, &ind, &u.annotations, PlacementKind::BeforeDeclaration)?;
let type_attrs = type_attributes(&u.annotations);
for a in &type_attrs.attrs {
writeln!(out, "{ind}{a}").map_err(fmt_err)?;
}
let topic_marker = !is_nested_type(&u.annotations);
if topic_marker {
writeln!(
out,
"{ind}public partial record class {name} : ITopicType<{name}>"
)
.map_err(fmt_err)?;
} else {
writeln!(out, "{ind}public partial record class {name}").map_err(fmt_err)?;
}
writeln!(out, "{ind}{{").map_err(fmt_err)?;
emit_verbatim_at(out, &inner, &u.annotations, PlacementKind::BeginDeclaration)?;
writeln!(
out,
"{inner}public {disc_ty} Discriminator {{ get; init; }} = default!;"
)
.map_err(fmt_err)?;
writeln!(
out,
"{inner}public object? Value {{ get; init; }} = default;"
)
.map_err(fmt_err)?;
writeln!(out).map_err(fmt_err)?;
let mut has_default = false;
for c in &u.cases {
emit_union_case_comment(out, &inner, c, &mut has_default)?;
}
if !has_default {
writeln!(out, "{inner}// no explicit 'default:' branch").map_err(fmt_err)?;
}
emit_verbatim_at(out, &inner, &u.annotations, PlacementKind::EndDeclaration)?;
writeln!(out, "{ind}}}").map_err(fmt_err)?;
emit_verbatim_at(out, &ind, &u.annotations, PlacementKind::AfterDeclaration)?;
writeln!(out).map_err(fmt_err)?;
Ok(())
}
fn emit_union_case_comment(
out: &mut String,
inner: &str,
c: &Case,
has_default: &mut bool,
) -> Result<(), CsGenError> {
for label in &c.labels {
match label {
CaseLabel::Default => {
*has_default = true;
writeln!(
out,
"{inner}// case default -> {}",
declarator_name(&c.element.declarator)
)
.map_err(fmt_err)?;
}
CaseLabel::Value(expr) => {
let val = const_expr_to_cs(expr);
writeln!(
out,
"{inner}// case {val} -> {}",
declarator_name(&c.element.declarator)
)
.map_err(fmt_err)?;
}
}
}
Ok(())
}
fn declarator_name(d: &Declarator) -> &str {
&d.name().text
}
fn emit_enum(out: &mut String, ctx: &mut EmitCtx<'_>, e: &EnumDef) -> Result<(), CsGenError> {
let name = escape_identifier(&e.name.text)?;
let ind = ctx.indent();
emit_verbatim_at(out, &ind, &e.annotations, PlacementKind::BeforeDeclaration)?;
writeln!(out, "{ind}public enum {name} : int").map_err(fmt_err)?;
writeln!(out, "{ind}{{").map_err(fmt_err)?;
let inner = " ".repeat((ctx.indent_level + 1) * ctx.opts.indent_width);
emit_verbatim_at(out, &inner, &e.annotations, PlacementKind::BeginDeclaration)?;
for en in &e.enumerators {
let en_name = escape_identifier(&en.name.text)?;
writeln!(out, "{inner}{en_name},").map_err(fmt_err)?;
}
emit_verbatim_at(out, &inner, &e.annotations, PlacementKind::EndDeclaration)?;
writeln!(out, "{ind}}}").map_err(fmt_err)?;
emit_verbatim_at(out, &ind, &e.annotations, PlacementKind::AfterDeclaration)?;
writeln!(out).map_err(fmt_err)?;
Ok(())
}
fn emit_interface_stub(
out: &mut String,
ctx: &mut EmitCtx<'_>,
iface: &InterfaceDef,
) -> Result<(), CsGenError> {
let name = escape_identifier(&iface.name.text)?;
let ind = ctx.indent();
let inner = " ".repeat((ctx.indent_level + 1) * ctx.opts.indent_width);
let bases: Vec<String> = iface
.bases
.iter()
.map(scoped_to_cs)
.collect::<Result<Vec<_>, _>>()?;
if bases.is_empty() {
writeln!(out, "{ind}public interface {name}").map_err(fmt_err)?;
} else {
writeln!(out, "{ind}public interface {name} : {}", bases.join(", ")).map_err(fmt_err)?;
}
writeln!(out, "{ind}{{").map_err(fmt_err)?;
for export in &iface.exports {
match export {
zerodds_idl::ast::Export::Op(op) => emit_interface_op_cs(out, &inner, op)?,
zerodds_idl::ast::Export::Attr(attr) => emit_interface_attr_cs(out, &inner, attr)?,
_ => {} }
}
writeln!(out, "{ind}}}").map_err(fmt_err)?;
writeln!(out).map_err(fmt_err)?;
Ok(())
}
fn emit_interface_op_cs(out: &mut String, inner: &str, op: &OpDecl) -> Result<(), CsGenError> {
let name = escape_identifier(&op.name.text)?;
let ret = match &op.return_type {
None => "void".to_string(),
Some(t) => typespec_to_cs(t)?,
};
let params: Vec<String> = op
.params
.iter()
.map(|p| -> Result<String, CsGenError> {
let ty = typespec_to_cs(&p.type_spec)?;
let modifier = match p.attribute {
ParamAttribute::In => "",
ParamAttribute::Out => "out ",
ParamAttribute::InOut => "ref ",
};
let pname = escape_identifier(&p.name.text)?;
Ok(format!("{modifier}{ty} {pname}"))
})
.collect::<Result<_, _>>()?;
writeln!(out, "{inner}{ret} {name}({});", params.join(", ")).map_err(fmt_err)?;
Ok(())
}
fn emit_interface_attr_cs(
out: &mut String,
inner: &str,
attr: &zerodds_idl::ast::AttrDecl,
) -> Result<(), CsGenError> {
let name = escape_identifier(&attr.name.text)?;
let pascal = pascal_case(&attr.name.text);
let prop_name = if name == pascal { name.clone() } else { pascal };
let ty = typespec_to_cs(&attr.type_spec)?;
if attr.readonly {
writeln!(out, "{inner}{ty} {prop_name} {{ get; }}").map_err(fmt_err)?;
} else {
writeln!(out, "{inner}{ty} {prop_name} {{ get; set; }}").map_err(fmt_err)?;
}
Ok(())
}
fn emit_value_type(
out: &mut String,
ctx: &mut EmitCtx<'_>,
v: &ValueDef,
) -> Result<(), CsGenError> {
let name = escape_identifier(&v.name.text)?;
let ind = ctx.indent();
let inner = " ".repeat((ctx.indent_level + 1) * ctx.opts.indent_width);
let mut bases: Vec<String> = Vec::new();
if let Some(inh) = &v.inheritance {
for b in &inh.bases {
bases.push(format!("{}Abstract", scoped_to_cs(b)?));
}
for s in &inh.supports {
bases.push(scoped_to_cs(s)?);
}
}
let abstract_name = format!("{name}Abstract");
if bases.is_empty() {
writeln!(out, "{ind}public abstract class {abstract_name}").map_err(fmt_err)?;
} else {
writeln!(
out,
"{ind}public abstract class {abstract_name} : {}",
bases.join(", ")
)
.map_err(fmt_err)?;
}
writeln!(out, "{ind}{{").map_err(fmt_err)?;
for el in &v.elements {
match el {
ValueElement::State(s) => {
let ty = typespec_to_cs(&s.type_spec)?;
let visibility = match s.visibility {
StateVisibility::Public => "public",
StateVisibility::Private => "protected",
};
for d in &s.declarators {
let pname = pascal_case(&d.name().text);
writeln!(
out,
"{inner}{visibility} abstract {ty} {pname} {{ get; set; }}"
)
.map_err(fmt_err)?;
}
}
ValueElement::Init(i) => {
let params: Vec<String> = i
.params
.iter()
.map(|p| -> Result<String, CsGenError> {
let ty = typespec_to_cs(&p.type_spec)?;
let pname = escape_identifier(&p.name.text)?;
Ok(format!("{ty} {pname}"))
})
.collect::<Result<_, _>>()?;
let fname = escape_identifier(&i.name.text)?;
writeln!(
out,
"{inner}public abstract void {fname}({});",
params.join(", ")
)
.map_err(fmt_err)?;
}
ValueElement::Export(Export::Op(op)) => {
emit_interface_op_cs(out, &inner, op)?;
}
ValueElement::Export(Export::Attr(a)) => {
emit_interface_attr_cs(out, &inner, a)?;
}
_ => {}
}
}
writeln!(out, "{ind}}}").map_err(fmt_err)?;
writeln!(out).map_err(fmt_err)?;
writeln!(
out,
"{ind}public class {name} : {abstract_name} {{ /* user impl */ }}"
)
.map_err(fmt_err)?;
writeln!(out).map_err(fmt_err)?;
Ok(())
}
fn emit_typedef(
out: &mut String,
ctx: &mut EmitCtx<'_>,
t: &TypedefDecl,
) -> Result<(), CsGenError> {
let ind = ctx.indent();
for decl in &t.declarators {
let alias = escape_identifier(&decl.name().text)?;
let target_ty = type_for_declarator(&t.type_spec, decl)?;
writeln!(out, "{ind}// typedef {target_ty} {alias};").map_err(fmt_err)?;
writeln!(
out,
"{ind}public sealed record class {alias}({target_ty} Value);"
)
.map_err(fmt_err)?;
}
writeln!(out).map_err(fmt_err)?;
Ok(())
}
fn emit_const_decl(
out: &mut String,
ctx: &mut EmitCtx<'_>,
c: &zerodds_idl::ast::ConstDecl,
) -> Result<(), CsGenError> {
let name = escape_identifier(&c.name.text)?;
let ind = ctx.indent();
let cs_ty = match &c.type_ {
zerodds_idl::ast::ConstType::Integer(i) => crate::type_map::integer_to_cs(*i).to_string(),
zerodds_idl::ast::ConstType::Floating(f) => crate::type_map::floating_to_cs(*f).to_string(),
zerodds_idl::ast::ConstType::Boolean => "bool".into(),
zerodds_idl::ast::ConstType::Char => "char".into(),
zerodds_idl::ast::ConstType::WideChar => "char".into(),
zerodds_idl::ast::ConstType::Octet => "byte".into(),
zerodds_idl::ast::ConstType::String { wide: false } => "string".into(),
zerodds_idl::ast::ConstType::String { wide: true } => "string".into(),
zerodds_idl::ast::ConstType::Scoped(s) => scoped_to_cs(s)?,
zerodds_idl::ast::ConstType::Fixed => "decimal".into(),
};
let val = const_expr_to_cs(&c.value);
writeln!(out, "{ind}public const {cs_ty} {name} = {val};").map_err(fmt_err)?;
Ok(())
}
fn emit_exception(
out: &mut String,
ctx: &mut EmitCtx<'_>,
e: &ExceptDecl,
) -> Result<(), CsGenError> {
let name = escape_identifier(&e.name.text)?;
let ind = ctx.indent();
let inner = " ".repeat((ctx.indent_level + 1) * ctx.opts.indent_width);
writeln!(out, "{ind}public sealed class {name} : Exception").map_err(fmt_err)?;
writeln!(out, "{ind}{{").map_err(fmt_err)?;
writeln!(out, "{inner}public {name}() : base() {{ }}").map_err(fmt_err)?;
writeln!(
out,
"{inner}public {name}(string message) : base(message) {{ }}"
)
.map_err(fmt_err)?;
for m in &e.members {
for decl in &m.declarators {
let cs_ty = type_for_declarator(&m.type_spec, decl)?;
let prop_name = pascal_case(&decl.name().text);
writeln!(
out,
"{inner}public {cs_ty} {prop_name} {{ get; init; }} = default!;"
)
.map_err(fmt_err)?;
}
}
writeln!(out, "{ind}}}").map_err(fmt_err)?;
writeln!(out).map_err(fmt_err)?;
Ok(())
}
fn type_for_declarator(ts: &TypeSpec, decl: &Declarator) -> Result<String, CsGenError> {
let base = typespec_to_cs(ts)?;
match decl {
Declarator::Simple(_) => Ok(base),
Declarator::Array(arr) => {
let mut out = base;
for _ in &arr.sizes {
out = format!("{out}[]");
}
Ok(out)
}
}
}
fn typespec_to_cs(ts: &TypeSpec) -> Result<String, CsGenError> {
match ts {
TypeSpec::Primitive(p) => Ok(primitive_to_cs(*p).to_string()),
TypeSpec::Scoped(s) => scoped_to_cs(s),
TypeSpec::Sequence(s) => {
let inner = typespec_to_cs(&s.elem)?;
if s.bound.is_some() {
Ok(format!("IBoundedSequence<{inner}>"))
} else {
Ok(format!("ISequence<{inner}>"))
}
}
TypeSpec::String(_) => Ok("string".into()),
TypeSpec::Map(m) => {
let k = typespec_to_cs(&m.key)?;
let v = typespec_to_cs(&m.value)?;
Ok(format!("IDictionary<{k}, {v}>"))
}
TypeSpec::Fixed(_) => {
Ok("decimal".into())
}
TypeSpec::Any => {
Ok("Omg.Types.Any".into())
}
}
}
fn switch_type_to_cs(s: &SwitchTypeSpec) -> Result<String, CsGenError> {
Ok(match s {
SwitchTypeSpec::Integer(i) => crate::type_map::integer_to_cs(*i).to_string(),
SwitchTypeSpec::Char => "char".into(),
SwitchTypeSpec::Boolean => "bool".into(),
SwitchTypeSpec::Octet => "byte".into(),
SwitchTypeSpec::Scoped(s) => scoped_to_cs(s)?,
})
}
fn scoped_to_cs(s: &ScopedName) -> Result<String, CsGenError> {
let mut parts: Vec<String> = Vec::with_capacity(s.parts.len());
for p in &s.parts {
parts.push(escape_identifier(&p.text)?);
}
let joined = parts.join(".");
if s.absolute {
Ok(format!("global::{joined}"))
} else {
Ok(joined)
}
}
fn const_expr_to_cs(e: &ConstExpr) -> String {
match e {
ConstExpr::Literal(l) => literal_to_cs(l),
ConstExpr::Scoped(s) => match scoped_to_cs(s) {
Ok(v) => v,
Err(_) => s
.parts
.iter()
.map(|p| p.text.clone())
.collect::<Vec<_>>()
.join("."),
},
ConstExpr::Unary { op, operand, .. } => {
let prefix = match op {
zerodds_idl::ast::UnaryOp::Plus => "+",
zerodds_idl::ast::UnaryOp::Minus => "-",
zerodds_idl::ast::UnaryOp::BitNot => "~",
};
format!("{prefix}{}", const_expr_to_cs(operand))
}
ConstExpr::Binary { op, lhs, rhs, .. } => {
let opstr = match op {
zerodds_idl::ast::BinaryOp::Or => "|",
zerodds_idl::ast::BinaryOp::Xor => "^",
zerodds_idl::ast::BinaryOp::And => "&",
zerodds_idl::ast::BinaryOp::Shl => "<<",
zerodds_idl::ast::BinaryOp::Shr => ">>",
zerodds_idl::ast::BinaryOp::Add => "+",
zerodds_idl::ast::BinaryOp::Sub => "-",
zerodds_idl::ast::BinaryOp::Mul => "*",
zerodds_idl::ast::BinaryOp::Div => "/",
zerodds_idl::ast::BinaryOp::Mod => "%",
};
format!(
"({} {opstr} {})",
const_expr_to_cs(lhs),
const_expr_to_cs(rhs)
)
}
}
}
fn literal_to_cs(l: &Literal) -> String {
match l.kind {
LiteralKind::Boolean => l.raw.clone(),
LiteralKind::Integer | LiteralKind::Floating => l.raw.clone(),
LiteralKind::Char => l.raw.clone(),
LiteralKind::WideChar => l.raw.clone(),
LiteralKind::String => l.raw.clone(),
LiteralKind::WideString => l.raw.clone(),
LiteralKind::Fixed => l.raw.clone(),
}
}
fn pascal_case(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut upper_next = true;
for c in s.chars() {
if c == '_' {
upper_next = true;
continue;
}
if upper_next {
for u in c.to_uppercase() {
out.push(u);
}
upper_next = false;
} else {
out.push(c);
}
}
if out.is_empty() {
return s.to_string();
}
out
}
fn collect_inheritance_edges(
defs: &[Definition],
parents: &mut std::collections::HashMap<String, String>,
prefix: &str,
) {
for d in defs {
match d {
Definition::Module(m) => {
let new_prefix = if prefix.is_empty() {
m.name.text.clone()
} else {
format!("{prefix}.{}", m.name.text)
};
collect_inheritance_edges(&m.definitions, parents, &new_prefix);
}
Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) => {
let key = if prefix.is_empty() {
s.name.text.clone()
} else {
format!("{prefix}.{}", s.name.text)
};
if let Some(b) = &s.base {
let base_str = b
.parts
.iter()
.map(|p| p.text.clone())
.collect::<Vec<_>>()
.join(".");
parents.insert(key, base_str);
}
}
_ => {}
}
}
}
fn detect_inheritance_cycles(spec: &Specification) -> Result<(), CsGenError> {
use std::collections::HashMap;
let mut parents: HashMap<String, String> = HashMap::new();
collect_inheritance_edges(&spec.definitions, &mut parents, "");
for start in parents.keys() {
let mut current = start.clone();
let mut visited: BTreeSet<String> = BTreeSet::new();
visited.insert(current.clone());
while let Some(p) = parents.get(¤t) {
let resolved = parents
.keys()
.find(|k| *k == p || k.ends_with(&format!(".{p}")))
.cloned()
.unwrap_or_else(|| p.clone());
if visited.contains(&resolved) {
return Err(CsGenError::InheritanceCycle {
type_name: short_name(&resolved),
});
}
visited.insert(resolved.clone());
if resolved == current {
return Err(CsGenError::InheritanceCycle {
type_name: short_name(&resolved),
});
}
current = resolved;
if !parents.contains_key(¤t) {
break;
}
}
}
Ok(())
}
fn short_name(s: &str) -> String {
s.rsplit('.').next().unwrap_or(s).to_string()
}
fn fmt_err(_: core::fmt::Error) -> CsGenError {
CsGenError::Internal("string formatting failed".into())
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic)]
use super::*;
#[test]
fn pascal_case_simple() {
assert_eq!(pascal_case("foo"), "Foo");
}
#[test]
fn pascal_case_snake() {
assert_eq!(pascal_case("sensor_id"), "SensorId");
}
#[test]
fn pascal_case_already_pascal() {
assert_eq!(pascal_case("SensorId"), "SensorId");
}
#[test]
fn pascal_case_empty() {
assert_eq!(pascal_case(""), "");
}
#[test]
fn pascal_case_multi_underscore() {
assert_eq!(pascal_case("a_b_c"), "ABC");
}
#[test]
fn short_name_strips_namespace() {
assert_eq!(short_name("A.B.C"), "C");
assert_eq!(short_name("Foo"), "Foo");
}
}