use askama::Template as _;
use crate::ir::codec::{EnumLayout, VecLayout};
use crate::ir::ops::{ReadOp, ReadSeq, SizeExpr, ValueExpr, WriteOp, WriteSeq};
use crate::ir::types::{PrimitiveType, TypeExpr};
use crate::ir::{AbiContract, FfiContract};
use super::{
CSharpOptions, NamingConvention,
lower::CSharpLowerer,
plan::{CSharpEnumKind, CSharpRecord, CSharpType, ShadowScope},
templates::{
EnumCStyleTemplate, EnumDataTemplate, FunctionsTemplate, NativeTemplate, PreambleTemplate,
RecordTemplate,
},
};
#[derive(Debug, Clone)]
pub struct CSharpFile {
pub file_name: String,
pub source: String,
}
#[derive(Debug, Clone)]
pub struct CSharpOutput {
pub files: Vec<CSharpFile>,
pub class_name: String,
pub namespace: String,
}
impl CSharpOutput {
#[cfg(test)]
pub fn combined_source(&self) -> String {
self.files
.iter()
.map(|f| f.source.as_str())
.collect::<Vec<_>>()
.join("\n")
}
}
pub struct CSharpEmitter;
impl CSharpEmitter {
pub fn emit(ffi: &FfiContract, abi: &AbiContract, options: &CSharpOptions) -> CSharpOutput {
let lowerer = CSharpLowerer::new(ffi, abi, options);
let module = lowerer.lower();
let mut files: Vec<CSharpFile> = module
.records
.iter()
.map(|record| CSharpFile {
file_name: format!("{}.cs", record.class_name),
source: RecordTemplate {
record,
namespace: &module.namespace,
}
.render()
.unwrap_or_else(|err| panic!("record {} render failed: {err}", record.class_name)),
})
.collect();
files.extend(module.enums.iter().map(|enumeration| {
CSharpFile {
file_name: format!("{}.cs", enumeration.class_name),
source: match enumeration.kind {
CSharpEnumKind::CStyle => EnumCStyleTemplate {
enumeration,
namespace: &module.namespace,
}
.render()
.unwrap_or_else(|err| {
panic!(
"c-style enum {} render failed: {err}",
enumeration.class_name
)
}),
CSharpEnumKind::Data => EnumDataTemplate {
enumeration,
namespace: &module.namespace,
}
.render()
.unwrap_or_else(|err| {
panic!("data enum {} render failed: {err}", enumeration.class_name)
}),
},
}
}));
let mut main_source = String::new();
main_source.push_str(&PreambleTemplate { module: &module }.render().unwrap());
main_source.push('\n');
main_source.push_str(&FunctionsTemplate { module: &module }.render().unwrap());
main_source.push_str(&NativeTemplate { module: &module }.render().unwrap());
main_source.push('\n');
files.push(CSharpFile {
file_name: format!("{}.cs", module.class_name),
source: main_source,
});
CSharpOutput {
files,
class_name: module.class_name,
namespace: module.namespace,
}
}
}
pub fn render_value(expr: &ValueExpr) -> String {
match expr {
ValueExpr::Instance => "this".to_string(),
ValueExpr::Var(name) => name.clone(),
ValueExpr::Named(name) => NamingConvention::field_name(name),
ValueExpr::Field(parent, field) => {
let parent_str = render_value(parent);
let field_str = NamingConvention::property_name(field.as_str());
format!("{}.{}", parent_str, field_str)
}
}
}
pub fn primitive_read_method(primitive: PrimitiveType) -> &'static str {
match primitive {
PrimitiveType::Bool => "ReadBool",
PrimitiveType::I8 => "ReadI8",
PrimitiveType::U8 => "ReadU8",
PrimitiveType::I16 => "ReadI16",
PrimitiveType::U16 => "ReadU16",
PrimitiveType::I32 => "ReadI32",
PrimitiveType::U32 => "ReadU32",
PrimitiveType::I64 => "ReadI64",
PrimitiveType::U64 => "ReadU64",
PrimitiveType::ISize => "ReadNInt",
PrimitiveType::USize => "ReadNUInt",
PrimitiveType::F32 => "ReadF32",
PrimitiveType::F64 => "ReadF64",
}
}
pub fn primitive_write_method(primitive: PrimitiveType) -> &'static str {
match primitive {
PrimitiveType::Bool => "WriteBool",
PrimitiveType::I8 => "WriteI8",
PrimitiveType::U8 => "WriteU8",
PrimitiveType::I16 => "WriteI16",
PrimitiveType::U16 => "WriteU16",
PrimitiveType::I32 => "WriteI32",
PrimitiveType::U32 => "WriteU32",
PrimitiveType::I64 => "WriteI64",
PrimitiveType::U64 => "WriteU64",
PrimitiveType::ISize => "WriteNInt",
PrimitiveType::USize => "WriteNUInt",
PrimitiveType::F32 => "WriteF32",
PrimitiveType::F64 => "WriteF64",
}
}
pub fn primitive_vec_reader_call(primitive: PrimitiveType) -> String {
match primitive {
PrimitiveType::Bool => "ReadBoolArray()".to_string(),
PrimitiveType::ISize => "ReadNIntArray()".to_string(),
PrimitiveType::USize => "ReadNUIntArray()".to_string(),
other => format!("ReadBlittableArray<{}>()", CSharpType::from(other)),
}
}
fn primitive_vec_nested_reader_call(primitive: PrimitiveType) -> String {
match primitive {
PrimitiveType::Bool => "ReadLengthPrefixedBoolArray()".to_string(),
PrimitiveType::ISize => "ReadLengthPrefixedNIntArray()".to_string(),
PrimitiveType::USize => "ReadLengthPrefixedNUIntArray()".to_string(),
other => format!(
"ReadLengthPrefixedBlittableArray<{}>()",
CSharpType::from(other)
),
}
}
pub fn primitive_vec_writer_call(primitive: PrimitiveType, value: &str) -> String {
match primitive {
PrimitiveType::Bool => format!("WriteBoolArray({value})"),
PrimitiveType::ISize => format!("WriteNIntArray({value})"),
PrimitiveType::USize => format!("WriteNUIntArray({value})"),
_ => format!("WriteBlittableArray({value})"),
}
}
#[derive(Default)]
pub struct CSharpEmitContext {
write_loop_index: usize,
read_closure_index: usize,
size_loop_index: usize,
write_option_bind_index: usize,
size_option_bind_index: usize,
}
impl CSharpEmitContext {
fn next_write_loop_var(&mut self) -> String {
let i = self.write_loop_index;
self.write_loop_index += 1;
format!("item{}", i)
}
fn next_read_closure_var(&mut self) -> String {
let i = self.read_closure_index;
self.read_closure_index += 1;
format!("r{}", i)
}
fn next_size_loop_var(&mut self) -> String {
let i = self.size_loop_index;
self.size_loop_index += 1;
format!("sizeItem{}", i)
}
fn next_write_option_bind_var(&mut self) -> String {
let i = self.write_option_bind_index;
self.write_option_bind_index += 1;
format!("opt{}", i)
}
fn next_size_option_bind_var(&mut self) -> String {
let i = self.size_option_bind_index;
self.size_option_bind_index += 1;
format!("sizeOpt{}", i)
}
}
fn replace_identifier_occurrences(expression: &str, identifier: &str, replacement: &str) -> String {
if identifier.is_empty() {
return expression.to_string();
}
let mut result = String::with_capacity(expression.len());
let mut cursor = 0;
while let Some(rel) = expression[cursor..].find(identifier) {
let start = cursor + rel;
let end = start + identifier.len();
let prev = expression[..start].chars().next_back();
let next = expression[end..].chars().next();
let prev_is_id = prev.map(is_identifier_char).unwrap_or(false);
let next_is_id = next.map(is_identifier_char).unwrap_or(false);
if prev_is_id || next_is_id {
result.push_str(&expression[cursor..end]);
} else {
result.push_str(&expression[cursor..start]);
result.push_str(replacement);
}
cursor = end;
}
result.push_str(&expression[cursor..]);
result
}
fn is_identifier_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_'
}
pub fn vec_return_reader_call(inner: &TypeExpr, element_seq: &ReadSeq) -> String {
match inner {
TypeExpr::Primitive(p) => primitive_vec_reader_call(*p),
_ => {
let mut ctx = CSharpEmitContext::default();
let inner_reader = emit_reader_read_with_context(element_seq, None, &mut ctx);
let closure_var = ctx.next_read_closure_var();
let remapped = replace_identifier_occurrences(&inner_reader, "reader", &closure_var);
let element_ty = CSharpType::from_type_expr(inner);
format!("ReadEncodedArray<{element_ty}>({closure_var} => {remapped})")
}
}
}
pub fn emit_reader_read(seq: &ReadSeq, scope: Option<&ShadowScope>) -> String {
let mut ctx = CSharpEmitContext::default();
emit_reader_read_with_context(seq, scope, &mut ctx)
}
pub fn emit_reader_read_shared(
seq: &ReadSeq,
scope: Option<&ShadowScope>,
ctx: &mut CSharpEmitContext,
) -> String {
emit_reader_read_with_context(seq, scope, ctx)
}
fn emit_reader_read_with_context(
seq: &ReadSeq,
scope: Option<&ShadowScope>,
ctx: &mut CSharpEmitContext,
) -> String {
let op = seq.ops.first().expect("read ops");
match op {
ReadOp::Primitive { primitive, .. } => {
format!("reader.{}()", primitive_read_method(*primitive))
}
ReadOp::String { .. } => "reader.ReadString()".to_string(),
ReadOp::Bytes { .. } => "reader.ReadBytes()".to_string(),
ReadOp::Record { .. } => {
let ty = CSharpType::from_read_op(op).qualify_if_shadowed_opt(scope);
format!("{ty}.Decode(reader)")
}
ReadOp::Enum {
id,
layout: EnumLayout::CStyle { .. },
..
} => {
format!(
"{}Wire.Decode(reader)",
NamingConvention::class_name(id.as_str())
)
}
ReadOp::Enum {
layout: EnumLayout::Data { .. },
..
} => {
let ty = CSharpType::from_read_op(op).qualify_if_shadowed_opt(scope);
format!("{ty}.Decode(reader)")
}
ReadOp::Option { some, .. } => {
let inner = emit_reader_read_with_context(some, scope, ctx);
let inner_ty =
CSharpType::from_read_op(some.ops.first().expect("option inner read op"))
.qualify_if_shadowed_opt(scope);
format!("reader.ReadU8() == 0 ? ({inner_ty}?)null : {inner}")
}
ReadOp::Vec {
element_type: TypeExpr::Primitive(p),
layout: VecLayout::Blittable { .. },
..
} => {
format!("reader.{}", primitive_vec_nested_reader_call(*p))
}
ReadOp::Vec {
element_type,
layout: VecLayout::Blittable { .. },
..
} => {
let element_ty =
CSharpType::from_type_expr(element_type).qualify_if_shadowed_opt(scope);
format!("reader.ReadLengthPrefixedBlittableArray<{element_ty}>()")
}
ReadOp::Vec {
element_type,
element,
layout: VecLayout::Encoded,
..
} => {
let inner_reader = emit_reader_read_with_context(element, scope, ctx);
let closure_var = ctx.next_read_closure_var();
let remapped = replace_identifier_occurrences(&inner_reader, "reader", &closure_var);
let element_ty =
CSharpType::from_type_expr(element_type).qualify_if_shadowed_opt(scope);
format!("reader.ReadEncodedArray<{element_ty}>({closure_var} => {remapped})")
}
other => todo!(
"C# backend has not yet implemented read support for {:?}",
other
),
}
}
pub fn emit_write_expr(seq: &WriteSeq, writer_name: &str) -> String {
let mut ctx = CSharpEmitContext::default();
emit_write_expr_with_context(seq, writer_name, &mut ctx)
}
pub fn emit_write_expr_shared(
seq: &WriteSeq,
writer_name: &str,
ctx: &mut CSharpEmitContext,
) -> String {
emit_write_expr_with_context(seq, writer_name, ctx)
}
fn emit_write_expr_with_context(
seq: &WriteSeq,
writer_name: &str,
ctx: &mut CSharpEmitContext,
) -> String {
let op = seq.ops.first().expect("write ops");
match op {
WriteOp::Primitive { primitive, value } => {
format!(
"{}.{}({})",
writer_name,
primitive_write_method(*primitive),
render_value(value)
)
}
WriteOp::String { value } => {
format!("{}.WriteString({})", writer_name, render_value(value))
}
WriteOp::Bytes { value } => {
format!("{}.WriteBytes({})", writer_name, render_value(value))
}
WriteOp::Record { value, .. } => {
format!("{}.WireEncodeTo({})", render_value(value), writer_name)
}
WriteOp::Enum {
value,
layout: EnumLayout::CStyle { .. } | EnumLayout::Data { .. },
..
} => format!("{}.WireEncodeTo({})", render_value(value), writer_name),
WriteOp::Option { value, some } => {
let option_expr = render_value(value);
let inner = emit_write_expr_with_context(some, writer_name, ctx);
let bind = ctx.next_write_option_bind_var();
let remapped_inner = replace_identifier_occurrences(&inner, "v", &bind);
format!(
"if ({option_expr} is {{ }} {bind}) {{ {writer}.WriteU8((byte)1); {remapped_inner}; }} else {{ {writer}.WriteU8((byte)0); }}",
option_expr = option_expr,
bind = bind,
writer = writer_name,
remapped_inner = remapped_inner,
)
}
WriteOp::Vec {
value,
element_type: TypeExpr::Primitive(p),
layout: VecLayout::Blittable { .. },
..
} => format!(
"{}.{}",
writer_name,
primitive_vec_writer_call(*p, &render_value(value))
),
WriteOp::Vec {
value,
layout: VecLayout::Blittable { .. },
..
} => {
format!(
"{}.WriteBlittableArray({})",
writer_name,
render_value(value),
)
}
WriteOp::Vec {
value,
element_type,
element,
layout: VecLayout::Encoded,
} => {
let inner_write = emit_write_expr_with_context(element, writer_name, ctx);
let loop_var = ctx.next_write_loop_var();
let remapped = replace_identifier_occurrences(&inner_write, "item", &loop_var);
let iter_ty = CSharpType::from_type_expr(element_type);
format!(
"{}.WriteI32({}.Length); foreach ({} {} in {}) {{ {}; }}",
writer_name,
render_value(value),
iter_ty,
loop_var,
render_value(value),
remapped,
)
}
other => todo!(
"C# backend has not yet implemented write support for {:?}",
other
),
}
}
pub fn emit_size_expr(size: &SizeExpr) -> String {
let mut ctx = CSharpEmitContext::default();
emit_size_expr_with_context(size, &mut ctx)
}
pub fn emit_size_expr_shared(size: &SizeExpr, ctx: &mut CSharpEmitContext) -> String {
emit_size_expr_with_context(size, ctx)
}
fn emit_size_expr_with_context(size: &SizeExpr, ctx: &mut CSharpEmitContext) -> String {
match size {
SizeExpr::Fixed(value) => value.to_string(),
SizeExpr::StringLen(value) => {
format!("Encoding.UTF8.GetByteCount({})", render_value(value))
}
SizeExpr::BytesLen(value) => format!("{}.Length", render_value(value)),
SizeExpr::WireSize { value, .. } => {
format!("{}.WireEncodedSize()", render_value(value))
}
SizeExpr::Sum(parts) => {
let rendered = parts
.iter()
.map(|p| emit_size_expr_with_context(p, ctx))
.collect::<Vec<_>>()
.join(" + ");
format!("({})", rendered)
}
SizeExpr::OptionSize { value, inner } => {
let option_expr = render_value(value);
let inner_expr = emit_size_expr_with_context(inner, ctx);
let bind = ctx.next_size_option_bind_var();
let remapped_inner = replace_identifier_occurrences(&inner_expr, "v", &bind);
format!(
"(1 + ({option_expr} is {{ }} {bind} ? {remapped_inner} : 0))",
option_expr = option_expr,
bind = bind,
remapped_inner = remapped_inner,
)
}
SizeExpr::VecSize {
value,
layout: VecLayout::Blittable { element_size },
..
} => format!("(4 + {}.Length * {})", render_value(value), element_size),
SizeExpr::VecSize {
value,
inner,
layout: VecLayout::Encoded,
} => {
let inner_expr = emit_size_expr_with_context(inner, ctx);
let loop_var = ctx.next_size_loop_var();
let remapped = replace_identifier_occurrences(&inner_expr, "item", &loop_var);
format!(
"WireWriter.EncodedArraySize({}, {} => {})",
render_value(value),
loop_var,
remapped,
)
}
other => todo!(
"C# backend has not yet implemented size expression support for {:?}",
other
),
}
}
#[allow(dead_code)]
const _: fn(&CSharpRecord) = |_| {};
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::Lowerer as IrLowerer;
use crate::ir::codec::EnumLayout;
use crate::ir::contract::{FfiContract, PackageInfo};
use crate::ir::definitions::{
CStyleVariant, DataVariant, EnumDef, EnumRepr, FieldDef, FunctionDef, MethodDef, ParamDef,
ParamPassing, Receiver, RecordDef, ReturnDef, VariantPayload,
};
use crate::ir::ids::{EnumId, FieldName, FunctionId, MethodId, ParamName, RecordId};
use crate::ir::ops::{OffsetExpr, SizeExpr, ValueExpr, WireShape};
use crate::ir::types::{PrimitiveType, TypeExpr};
use boltffi_ffi_rules::callable::ExecutionKind;
use boltffi_ffi_rules::transport::EnumTagStrategy;
use std::collections::HashSet;
fn empty_contract() -> FfiContract {
FfiContract {
package: PackageInfo {
name: "demo_lib".to_string(),
version: None,
},
functions: vec![],
catalog: Default::default(),
}
}
fn primitive_function(
name: &str,
params: Vec<(&str, PrimitiveType)>,
returns: ReturnDef,
) -> FunctionDef {
FunctionDef {
id: FunctionId::new(name),
params: params
.into_iter()
.map(|(param_name, prim)| ParamDef {
name: ParamName::new(param_name),
type_expr: TypeExpr::Primitive(prim),
passing: ParamPassing::Value,
doc: None,
})
.collect(),
returns,
execution_kind: ExecutionKind::Sync,
doc: None,
deprecated: None,
}
}
fn emit_contract(contract: &FfiContract) -> CSharpOutput {
let abi = IrLowerer::new(contract).to_abi_contract();
CSharpEmitter::emit(contract, &abi, &CSharpOptions::default())
}
fn assert_source_contains(source: &str, snippet: &str, expecting: &str) {
assert!(
source.contains(snippet),
"expecting {expecting}\n missing snippet: {snippet:?}"
);
}
fn assert_source_lacks(source: &str, snippet: &str, expecting: &str) {
assert!(
!source.contains(snippet),
"expecting {expecting}\n unexpected snippet: {snippet:?}"
);
}
#[test]
fn emit_primitive_function_generates_wrapper_and_native() {
let mut contract = empty_contract();
contract.functions.push(primitive_function(
"echo_i32",
vec![("value", PrimitiveType::I32)],
ReturnDef::Value(TypeExpr::Primitive(PrimitiveType::I32)),
));
let src = emit_contract(&contract).combined_source();
assert!(src.contains("public static int EchoI32(int value)"));
assert!(src.contains("return NativeMethods.EchoI32(value);"));
assert!(src.contains(r#"[DllImport(LibName, EntryPoint = "boltffi_echo_i32")]"#));
assert!(src.contains("internal static extern int EchoI32(int value);"));
}
#[test]
fn emit_void_function_omits_return_keyword() {
let mut contract = empty_contract();
contract
.functions
.push(primitive_function("noop", vec![], ReturnDef::Void));
let src = emit_contract(&contract).combined_source();
assert!(src.contains("public static void Noop()"));
assert!(src.contains("NativeMethods.Noop();"));
assert!(!src.contains("return NativeMethods.Noop()"));
}
#[test]
fn emit_unsigned_types_use_csharp_unsigned_keywords() {
let mut contract = empty_contract();
contract.functions.push(primitive_function(
"unsigned_echo",
vec![
("a", PrimitiveType::U8),
("b", PrimitiveType::U16),
("c", PrimitiveType::U32),
("d", PrimitiveType::U64),
],
ReturnDef::Value(TypeExpr::Primitive(PrimitiveType::U32)),
));
let src = emit_contract(&contract).combined_source();
assert!(src.contains("uint UnsignedEcho(byte a, ushort b, uint c, ulong d)"));
}
#[test]
fn emit_namespace_and_class_use_pascal_case() {
let contract = empty_contract();
let output = emit_contract(&contract);
assert_eq!(output.namespace, "DemoLib");
assert_eq!(output.class_name, "DemoLib");
assert!(output.combined_source().contains("namespace DemoLib"));
}
#[test]
fn emit_escapes_csharp_keywords_in_param_names() {
let mut contract = empty_contract();
contract.functions.push(primitive_function(
"test_keywords",
vec![("int", PrimitiveType::I32), ("value", PrimitiveType::I32)],
ReturnDef::Void,
));
let src = emit_contract(&contract).combined_source();
assert!(src.contains("@int"));
}
fn function_with_types(
name: &str,
params: Vec<(&str, TypeExpr)>,
returns: ReturnDef,
) -> FunctionDef {
FunctionDef {
id: FunctionId::new(name),
params: params
.into_iter()
.map(|(param_name, type_expr)| ParamDef {
name: ParamName::new(param_name),
type_expr,
passing: ParamPassing::Value,
doc: None,
})
.collect(),
returns,
execution_kind: ExecutionKind::Sync,
doc: None,
deprecated: None,
}
}
#[test]
fn emit_bool_function_uses_i1_marshalling_for_native_signature() {
let mut contract = empty_contract();
contract.functions.push(primitive_function(
"flip",
vec![("value", PrimitiveType::Bool)],
ReturnDef::Value(TypeExpr::Primitive(PrimitiveType::Bool)),
));
let src = emit_contract(&contract).combined_source();
assert!(src.contains("public static bool Flip(bool value)"));
assert!(src.contains("[return: MarshalAs(UnmanagedType.I1)]"));
assert!(src.contains(
"internal static extern bool Flip([MarshalAs(UnmanagedType.I1)] bool value);"
));
}
#[test]
fn emit_string_param_marshals_as_byte_array_and_length() {
let mut contract = empty_contract();
contract.functions.push(function_with_types(
"string_length",
vec![("v", TypeExpr::String)],
ReturnDef::Value(TypeExpr::Primitive(PrimitiveType::U32)),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"public static uint StringLength(string v)",
"the public wrapper to expose the string param as a plain C# `string`",
);
assert_source_contains(
&src,
"byte[] _vBytes = Encoding.UTF8.GetBytes(v);",
"UTF-8 encoding of the string into a managed byte[] before the P/Invoke call",
);
assert_source_contains(
&src,
"NativeMethods.StringLength(_vBytes, (UIntPtr)_vBytes.Length)",
"the native call to receive the encoded byte[] and its length as two separate arguments",
);
assert_source_contains(
&src,
"internal static extern uint StringLength(byte[] v, UIntPtr vLen);",
"the P/Invoke declaration to split a string param into (byte[], UIntPtr) matching the C ABI",
);
}
#[test]
fn emit_string_return_decodes_ffibuf_and_frees() {
let mut contract = empty_contract();
contract.functions.push(function_with_types(
"echo_string",
vec![("v", TypeExpr::String)],
ReturnDef::Value(TypeExpr::String),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"public static string EchoString(string v)",
"the public wrapper to hide the FfiBuf and expose a normal `string` return",
);
assert_source_contains(
&src,
"FfiBuf _buf = NativeMethods.EchoString(",
"the native return captured in an `FfiBuf _buf` local so it can be decoded and freed",
);
assert_source_contains(
&src,
"new WireReader(_buf).ReadString()",
"WireReader stateful decode of the FfiBuf-carried string, shared with the record decode path",
);
assert_source_contains(
&src,
"NativeMethods.FreeBuf(_buf);",
"a FreeBuf call in a finally block so the Rust allocator reclaims the buffer even if decoding throws",
);
assert_source_contains(
&src,
"internal static extern FfiBuf EchoString(byte[] v, UIntPtr vLen);",
"the P/Invoke signature to return FfiBuf rather than a bare string",
);
}
#[test]
fn emit_string_helpers_only_appear_when_strings_are_used() {
let mut primitive_only = empty_contract();
primitive_only.functions.push(primitive_function(
"add",
vec![("a", PrimitiveType::I32), ("b", PrimitiveType::I32)],
ReturnDef::Value(TypeExpr::Primitive(PrimitiveType::I32)),
));
let primitive_src = emit_contract(&primitive_only).combined_source();
assert_source_lacks(
&primitive_src,
"FfiBuf",
"no FfiBuf struct or references in primitive-only output",
);
assert_source_lacks(
&primitive_src,
"WireReader",
"no WireReader helper class in primitive-only output",
);
assert_source_lacks(
&primitive_src,
"FreeBuf",
"no FreeBuf DllImport in primitive-only output",
);
assert_source_lacks(
&primitive_src,
"using System.Text;",
"no System.Text using directive when Encoding.UTF8 is never referenced",
);
let mut with_string = empty_contract();
with_string.functions.push(function_with_types(
"echo",
vec![("v", TypeExpr::String)],
ReturnDef::Value(TypeExpr::String),
));
let string_src = emit_contract(&with_string).combined_source();
assert_source_contains(
&string_src,
"internal struct FfiBuf",
"the FfiBuf struct when strings are used (mirrors the Rust FfiBuf_u8 layout)",
);
assert_source_contains(
&string_src,
"WireReader",
"a WireReader helper when strings are used",
);
assert_source_contains(
&string_src,
r#"[DllImport(LibName, EntryPoint = "boltffi_free_buf")]"#,
"a DllImport binding to boltffi_free_buf when strings are used",
);
assert_source_contains(
&string_src,
"using System.Text;",
"the System.Text using directive so Encoding.UTF8 resolves in the wrapper and WireReader",
);
}
#[test]
fn emit_record_only_string_fields_import_system_text_in_main_file() {
let mut contract = empty_contract();
contract.catalog.insert_record(record_with_fields(
"person",
false,
vec![
("name", TypeExpr::String),
("age", TypeExpr::Primitive(PrimitiveType::U32)),
],
));
contract.functions.push(function_with_types(
"echo_person",
vec![("p", TypeExpr::Record(RecordId::new("person")))],
ReturnDef::Value(TypeExpr::Record(RecordId::new("person"))),
));
let output = emit_contract(&contract);
let main_source = output
.files
.iter()
.find(|f| f.file_name == "DemoLib.cs")
.expect("DemoLib.cs")
.source
.as_str();
assert_source_contains(
main_source,
"using System.Text;",
"the main file needs System.Text when record string fields make WireWriter use Encoding.UTF8.GetBytes/GetByteCount",
);
assert_source_contains(
main_source,
"Marshal.PtrToStringUTF8",
"WireReader string decode still lives in the main file for record-only string usage",
);
}
#[test]
fn emit_vec_string_param_imports_system_text_in_main_file() {
let mut contract = empty_contract();
contract.functions.push(function_with_types(
"vec_string_lengths",
vec![("v", TypeExpr::Vec(Box::new(TypeExpr::String)))],
ReturnDef::Value(TypeExpr::Vec(Box::new(TypeExpr::Primitive(
PrimitiveType::U32,
)))),
));
let output = emit_contract(&contract);
let main_source = output
.files
.iter()
.find(|f| f.file_name == "DemoLib.cs")
.expect("DemoLib.cs")
.source
.as_str();
assert_source_contains(
main_source,
"using System.Text;",
"the main file needs System.Text when Vec<String> params make WireWriter size/write code call Encoding.UTF8",
);
assert_source_contains(
main_source,
"WireWriter.EncodedArraySize(v, sizeItem0 => (4 + Encoding.UTF8.GetByteCount(sizeItem0)))",
"the encoded Vec<String> param path uses Encoding.UTF8 inside the shared main-file helpers",
);
}
#[test]
fn emit_wire_reader_require_uses_overflow_safe_guard() {
let mut contract = empty_contract();
contract.functions.push(function_with_types(
"echo",
vec![("v", TypeExpr::String)],
ReturnDef::Value(TypeExpr::String),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"if (n < 0 || n > _length - _pos) throw new InvalidOperationException(\"corrupt wire: truncated \" + kind);",
"WireReader.Require must compare against remaining bytes instead of overflowing `_pos + n`",
);
}
#[test]
fn emit_size_expr_for_string_len_renders_payload_only() {
let size = SizeExpr::Sum(vec![
SizeExpr::Fixed(4),
SizeExpr::StringLen(ValueExpr::Named(
ParamName::new("name").as_str().to_string(),
)),
]);
assert_eq!(
emit_size_expr(&size),
"(4 + Encoding.UTF8.GetByteCount(name))"
);
}
#[test]
fn emit_reader_read_c_style_enum_calls_wire_helper() {
let seq = ReadSeq {
size: SizeExpr::Fixed(4),
ops: vec![ReadOp::Enum {
id: EnumId::new("status"),
offset: OffsetExpr::Fixed(0),
layout: EnumLayout::CStyle {
tag_type: PrimitiveType::I32,
tag_strategy: EnumTagStrategy::OrdinalIndex,
is_error: false,
},
}],
shape: WireShape::Value,
};
assert_eq!(emit_reader_read(&seq, None), "StatusWire.Decode(reader)");
}
#[test]
fn emit_reader_read_qualifies_record_when_shadowed_by_sibling_variant() {
let seq = ReadSeq {
size: SizeExpr::Fixed(16),
ops: vec![ReadOp::Record {
id: RecordId::new("point"),
offset: OffsetExpr::Fixed(0),
fields: vec![],
}],
shape: WireShape::Value,
};
let shadowed: HashSet<String> = ["Point".to_string()].into_iter().collect();
let scope = ShadowScope {
shadowed: &shadowed,
namespace: "Demo",
};
assert_eq!(
emit_reader_read(&seq, Some(&scope)),
"global::Demo.Point.Decode(reader)"
);
}
#[test]
fn emit_reader_read_leaves_record_unqualified_when_not_shadowed() {
let seq = ReadSeq {
size: SizeExpr::Fixed(16),
ops: vec![ReadOp::Record {
id: RecordId::new("point"),
offset: OffsetExpr::Fixed(0),
fields: vec![],
}],
shape: WireShape::Value,
};
let shadowed: HashSet<String> = ["Circle".to_string()].into_iter().collect();
let scope = ShadowScope {
shadowed: &shadowed,
namespace: "Demo",
};
assert_eq!(emit_reader_read(&seq, Some(&scope)), "Point.Decode(reader)");
}
#[test]
fn emit_write_expr_c_style_enum_field_matches_record_call_shape() {
let value = ValueExpr::Field(Box::new(ValueExpr::Instance), FieldName::new("status"));
let seq = WriteSeq {
size: SizeExpr::Fixed(4),
ops: vec![WriteOp::Enum {
id: EnumId::new("status"),
value,
layout: EnumLayout::CStyle {
tag_type: PrimitiveType::I32,
tag_strategy: EnumTagStrategy::OrdinalIndex,
is_error: false,
},
}],
shape: WireShape::Value,
};
assert_eq!(
emit_write_expr(&seq, "wire"),
"this.Status.WireEncodeTo(wire)"
);
}
fn record_with_fields(id: &str, is_repr_c: bool, fields: Vec<(&str, TypeExpr)>) -> RecordDef {
RecordDef {
id: RecordId::new(id),
is_repr_c,
is_error: false,
fields: fields
.into_iter()
.map(|(name, type_expr)| FieldDef {
name: FieldName::new(name),
type_expr,
doc: None,
default: None,
})
.collect(),
constructors: vec![],
methods: vec![],
doc: None,
deprecated: None,
}
}
#[test]
fn emit_blittable_record_gets_struct_layout_attribute() {
let mut contract = empty_contract();
contract.catalog.insert_record(record_with_fields(
"point",
true,
vec![
("x", TypeExpr::Primitive(PrimitiveType::F64)),
("y", TypeExpr::Primitive(PrimitiveType::F64)),
],
));
contract.functions.push(function_with_types(
"echo_point",
vec![("p", TypeExpr::Record(RecordId::new("point")))],
ReturnDef::Value(TypeExpr::Record(RecordId::new("point"))),
));
let output = emit_contract(&contract);
let src = output.combined_source();
assert_source_contains(
&src,
"[StructLayout(LayoutKind.Sequential)]",
"Sequential layout attribute so Rust's #[repr(C)] layout matches the C# struct",
);
assert_source_contains(
&src,
"public readonly record struct Point(",
"readonly record struct declaration: value type with generated equality",
);
}
#[test]
fn emit_blittable_record_passes_by_value_across_p_invoke() {
let mut contract = empty_contract();
contract.catalog.insert_record(record_with_fields(
"point",
true,
vec![
("x", TypeExpr::Primitive(PrimitiveType::F64)),
("y", TypeExpr::Primitive(PrimitiveType::F64)),
],
));
contract.functions.push(function_with_types(
"echo_point",
vec![("p", TypeExpr::Record(RecordId::new("point")))],
ReturnDef::Value(TypeExpr::Record(RecordId::new("point"))),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"public static Point EchoPoint(Point p)",
"wrapper exposes the blittable record directly",
);
assert_source_contains(
&src,
"return NativeMethods.EchoPoint(p);",
"single-line delegating body, no WireWriter, no FfiBuf",
);
assert_source_contains(
&src,
"internal static extern Point EchoPoint(Point p);",
"DllImport takes and returns the struct directly",
);
assert_source_lacks(
&src,
"WireWriter(p.WireEncodedSize())",
"no WireWriter setup for a blittable param (that would defeat the zero-copy win)",
);
}
#[test]
fn emit_blittable_record_vec_params_use_nested_fixed_blocks() {
let mut contract = empty_contract();
contract.catalog.insert_record(record_with_fields(
"point",
true,
vec![
("x", TypeExpr::Primitive(PrimitiveType::F64)),
("y", TypeExpr::Primitive(PrimitiveType::F64)),
],
));
contract.catalog.insert_record(record_with_fields(
"color",
true,
vec![
("r", TypeExpr::Primitive(PrimitiveType::U8)),
("g", TypeExpr::Primitive(PrimitiveType::U8)),
("b", TypeExpr::Primitive(PrimitiveType::U8)),
("a", TypeExpr::Primitive(PrimitiveType::U8)),
],
));
contract.functions.push(function_with_types(
"score_batches",
vec![
(
"points",
TypeExpr::Vec(Box::new(TypeExpr::Record(RecordId::new("point")))),
),
(
"colors",
TypeExpr::Vec(Box::new(TypeExpr::Record(RecordId::new("color")))),
),
],
ReturnDef::Value(TypeExpr::Primitive(PrimitiveType::I32)),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"fixed (Point* _pointsPtr = points)",
"the first pinned record vec param to get its own fixed statement",
);
assert_source_contains(
&src,
"fixed (Color* _colorsPtr = colors)",
"the second pinned record vec param to get a nested fixed statement instead of a comma-joined declaration",
);
assert_source_lacks(
&src,
"fixed (Point* _pointsPtr = points, Color* _colorsPtr = colors)",
"C# does not accept comma-joined fixed declarations across pinned params",
);
assert_source_contains(
&src,
"return NativeMethods.ScoreBatches((IntPtr)_pointsPtr, (UIntPtr)(points.Length * Unsafe.SizeOf<Point>()), (IntPtr)_colorsPtr, (UIntPtr)(colors.Length * Unsafe.SizeOf<Color>()));",
"the native call to use both pointer locals and byte lengths from the nested fixed blocks",
);
assert_source_contains(
&src,
"internal static extern int ScoreBatches(IntPtr points, UIntPtr pointsLen, IntPtr colors, UIntPtr colorsLen);",
"the DllImport signature to expose both pinned arrays as raw pointers plus byte lengths",
);
}
#[test]
fn emit_non_blittable_record_omits_struct_layout_attribute() {
let mut contract = empty_contract();
contract.catalog.insert_record(record_with_fields(
"person",
false,
vec![
("name", TypeExpr::String),
("age", TypeExpr::Primitive(PrimitiveType::U32)),
],
));
let output = emit_contract(&contract);
let person_source = output
.files
.iter()
.find(|f| f.file_name == "Person.cs")
.expect("Person.cs")
.source
.as_str();
assert!(
!person_source.contains("[StructLayout"),
"non-blittable record should not carry Sequential layout, but got:\n{person_source}"
);
assert!(
person_source.contains("public readonly record struct Person("),
"still a record struct just without the layout attribute"
);
}
#[test]
fn emit_non_blittable_record_param_uses_wire_writer_and_byte_array() {
let mut contract = empty_contract();
contract.catalog.insert_record(record_with_fields(
"person",
false,
vec![
("name", TypeExpr::String),
("age", TypeExpr::Primitive(PrimitiveType::U32)),
],
));
contract.functions.push(function_with_types(
"echo_person",
vec![("p", TypeExpr::Record(RecordId::new("person")))],
ReturnDef::Value(TypeExpr::Record(RecordId::new("person"))),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"using var _wire_p = new WireWriter(p.WireEncodedSize());",
"WireWriter rented with the record's exact encoded size, disposed at scope end",
);
assert_source_contains(
&src,
"p.WireEncodeTo(_wire_p);",
"record encodes itself into the WireWriter via its generated method",
);
assert_source_contains(
&src,
"byte[] _pBytes = _wire_p.ToArray();",
"bytes materialized before the native call",
);
assert_source_contains(
&src,
"FfiBuf _buf = NativeMethods.EchoPerson(_pBytes, (UIntPtr)_pBytes.Length);",
"native call hands the (byte[], UIntPtr) pair",
);
assert_source_contains(
&src,
"return Person.Decode(new WireReader(_buf));",
"return decodes the FfiBuf via the record's Decode method",
);
assert_source_contains(
&src,
"NativeMethods.FreeBuf(_buf);",
"FreeBuf in finally so Rust reclaims the buffer even on decode failure",
);
assert_source_contains(
&src,
"internal static extern FfiBuf EchoPerson(byte[] p, UIntPtr pLen);",
"DllImport signature splits the record into (byte[], UIntPtr) and returns FfiBuf",
);
}
#[test]
fn emit_nested_record_encode_decode_delegates_to_inner_record() {
let mut contract = empty_contract();
contract.catalog.insert_record(record_with_fields(
"inner",
false,
vec![("label", TypeExpr::String)],
));
contract.catalog.insert_record(record_with_fields(
"outer",
false,
vec![("inner", TypeExpr::Record(RecordId::new("inner")))],
));
let output = emit_contract(&contract);
let outer = output
.files
.iter()
.find(|f| f.file_name == "Outer.cs")
.expect("Outer.cs")
.source
.as_str();
assert!(
outer.contains("Inner.Decode(reader)"),
"nested field decode walks into the inner record's Decode, but Outer.cs was:\n{outer}"
);
assert!(
outer.contains("this.Inner.WireEncodeTo(wire);"),
"nested field encode walks into the inner record's WireEncodeTo, but Outer.cs was:\n{outer}"
);
}
#[test]
fn emit_record_imports_system_text_only_when_string_fields_present() {
let mut contract = empty_contract();
contract.catalog.insert_record(record_with_fields(
"point",
true,
vec![
("x", TypeExpr::Primitive(PrimitiveType::F64)),
("y", TypeExpr::Primitive(PrimitiveType::F64)),
],
));
contract.catalog.insert_record(record_with_fields(
"person",
false,
vec![
("name", TypeExpr::String),
("age", TypeExpr::Primitive(PrimitiveType::U32)),
],
));
let output = emit_contract(&contract);
let point = output
.files
.iter()
.find(|f| f.file_name == "Point.cs")
.unwrap()
.source
.as_str();
let person = output
.files
.iter()
.find(|f| f.file_name == "Person.cs")
.unwrap()
.source
.as_str();
assert!(
!point.contains("using System.Text;"),
"Point.cs (blittable, no strings) should not import System.Text"
);
assert!(
person.contains("using System.Text;"),
"Person.cs (has string field) needs System.Text for Encoding.UTF8.GetByteCount"
);
assert!(
point.contains("using System.Runtime.InteropServices;"),
"Point.cs uses StructLayout so it imports InteropServices"
);
assert!(
!person.contains("using System.Runtime.InteropServices;"),
"Person.cs has no StructLayout so it should not import InteropServices"
);
}
#[test]
fn emit_mixed_string_and_primitive_params_only_encodes_strings() {
let mut contract = empty_contract();
contract.functions.push(function_with_types(
"repeat_string",
vec![
("v", TypeExpr::String),
("count", TypeExpr::Primitive(PrimitiveType::U32)),
],
ReturnDef::Value(TypeExpr::String),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"byte[] _vBytes = Encoding.UTF8.GetBytes(v);",
"UTF-8 encoding only for the string param `v`",
);
assert_source_lacks(
&src,
"Encoding.UTF8.GetBytes(count)",
"no UTF-8 encoding for the primitive `count` param",
);
assert_source_contains(
&src,
"NativeMethods.RepeatString(_vBytes, (UIntPtr)_vBytes.Length, count)",
"the native call to expand only the string into (bytes, length) and pass the primitive through unchanged",
);
assert_source_contains(
&src,
"internal static extern FfiBuf RepeatString(byte[] v, UIntPtr vLen, uint count);",
"the P/Invoke signature to split only the string into byte[]+UIntPtr, keeping the primitive uint direct",
);
}
fn c_style_enum_with_tag_type(
id: &str,
tag_type: PrimitiveType,
variants: Vec<&str>,
) -> EnumDef {
EnumDef {
id: EnumId::new(id),
repr: EnumRepr::CStyle {
tag_type,
variants: variants
.into_iter()
.enumerate()
.map(|(i, name)| CStyleVariant {
name: name.into(),
discriminant: i as i128,
doc: None,
})
.collect(),
},
is_error: false,
constructors: vec![],
methods: vec![],
doc: None,
deprecated: None,
}
}
fn c_style_enum(id: &str, variants: Vec<&str>) -> EnumDef {
c_style_enum_with_tag_type(id, PrimitiveType::I32, variants)
}
fn emit_files_for(contract: &FfiContract) -> Vec<(String, String)> {
let output = emit_contract(contract);
output
.files
.into_iter()
.map(|f| (f.file_name, f.source))
.collect()
}
#[test]
fn emit_repr_c_record_with_c_style_enum_field_stays_blittable() {
let mut contract = empty_contract();
contract
.catalog
.insert_enum(c_style_enum("status", vec!["Active", "Inactive"]));
contract.catalog.insert_record(record_with_fields(
"flag",
true,
vec![
("status", TypeExpr::Enum(EnumId::new("status"))),
("count", TypeExpr::Primitive(PrimitiveType::U32)),
],
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"[StructLayout(LayoutKind.Sequential)]",
"the zero-copy struct-layout attribute when only primitive + C-style enum fields are present",
);
assert_source_contains(
&src,
"public readonly record struct Flag(",
"the C# record-struct declaration",
);
assert_source_contains(
&src,
"Status Status,",
"the enum field rendered with its C# enum type name, not the backing int",
);
}
fn data_enum_single_variant(id: &str, variant_name: &str, field: (&str, TypeExpr)) -> EnumDef {
EnumDef {
id: EnumId::new(id),
repr: EnumRepr::Data {
tag_type: PrimitiveType::I32,
variants: vec![DataVariant {
name: variant_name.into(),
discriminant: 0,
payload: VariantPayload::Struct(vec![FieldDef {
name: field.0.into(),
type_expr: field.1,
doc: None,
default: None,
}]),
doc: None,
}],
},
is_error: false,
constructors: vec![],
methods: vec![],
doc: None,
deprecated: None,
}
}
#[test]
fn emit_function_with_data_enum_param_and_return_goes_through_wire() {
let mut contract = empty_contract();
contract.catalog.insert_enum(data_enum_single_variant(
"shape",
"Circle",
("radius", TypeExpr::Primitive(PrimitiveType::F64)),
));
contract.functions.push(function_with_types(
"echo_shape",
vec![("s", TypeExpr::Enum(EnumId::new("shape")))],
ReturnDef::Value(TypeExpr::Enum(EnumId::new("shape"))),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"public static Shape EchoShape(Shape s)",
"the public wrapper signature to name the Shape data enum on both sides",
);
assert_source_contains(
&src,
"internal static extern FfiBuf EchoShape(byte[] s, UIntPtr sLen);",
"the DllImport signature to split the data enum param into (byte[], UIntPtr) and return an FfiBuf",
);
assert_source_contains(
&src,
"using var _wire_s = new WireWriter(s.WireEncodedSize());",
"the wrapper body to allocate a WireWriter sized to the input value before the native call",
);
assert_source_contains(
&src,
"s.WireEncodeTo(_wire_s);",
"the wrapper body to drive the data enum's own WireEncodeTo: same call shape as records",
);
assert_source_contains(
&src,
"return Shape.Decode(new WireReader(_buf));",
"the wrapper body to decode the returned FfiBuf through the data enum's static Decode",
);
}
#[test]
fn emit_c_style_enum_instance_method_renders_as_extension_with_prefixed_native_name() {
let mut enum_def = c_style_enum("direction", vec!["North", "South"]);
enum_def.methods.push(MethodDef {
id: MethodId::new("opposite"),
receiver: Receiver::RefSelf,
params: vec![],
returns: ReturnDef::Value(TypeExpr::Enum(EnumId::new("direction"))),
execution_kind: ExecutionKind::Sync,
doc: None,
deprecated: None,
});
let mut contract = empty_contract();
contract.catalog.insert_enum(enum_def);
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"public static Direction Opposite(this Direction self)",
"an instance method on a C-style enum to render as a C# extension method",
);
assert_source_contains(
&src,
"return NativeMethods.DirectionOpposite(self);",
"the extension-method body to call the prefixed native entry with `self` passed directly",
);
assert_source_contains(
&src,
"internal static extern Direction DirectionOpposite(Direction self);",
"the DllImport to declare the prefixed native name, return the enum type directly, and take the enum-typed self param",
);
}
#[test]
fn emit_enum_method_with_blittable_record_vec_param_uses_fixed_block() {
let mut contract = empty_contract();
contract.catalog.insert_record(record_with_fields(
"point",
true,
vec![
("x", TypeExpr::Primitive(PrimitiveType::F64)),
("y", TypeExpr::Primitive(PrimitiveType::F64)),
],
));
let mut enum_def = c_style_enum("direction", vec!["North", "South"]);
enum_def.methods.push(MethodDef {
id: MethodId::new("from_points"),
receiver: Receiver::Static,
params: vec![ParamDef {
name: ParamName::new("points"),
type_expr: TypeExpr::Vec(Box::new(TypeExpr::Record(RecordId::new("point")))),
passing: ParamPassing::Value,
doc: None,
}],
returns: ReturnDef::Value(TypeExpr::Enum(EnumId::new("direction"))),
execution_kind: ExecutionKind::Sync,
doc: None,
deprecated: None,
});
contract.catalog.insert_enum(enum_def);
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"public static Direction FromPoints(Point[] points)",
"the enum companion method to expose the blittable record vec as Point[]",
);
assert_source_contains(
&src,
"fixed (Point* _pointsPtr = points)",
"the method body to pin the managed Point[] before the native call",
);
assert_source_contains(
&src,
"return NativeMethods.DirectionFromPoints((IntPtr)_pointsPtr, (UIntPtr)(points.Length * Unsafe.SizeOf<Point>()));",
"the native call to use the pointer local introduced by the fixed block",
);
assert_source_contains(
&src,
"internal static extern Direction DirectionFromPoints(IntPtr points, UIntPtr pointsLen);",
"the DllImport signature to take a raw pointer and byte length for the pinned array param",
);
}
#[test]
fn emit_function_with_c_style_enum_param_and_return_marshals_direct() {
let mut contract = empty_contract();
contract
.catalog
.insert_enum(c_style_enum("status", vec!["Active", "Inactive"]));
contract.functions.push(function_with_types(
"echo_status",
vec![("s", TypeExpr::Enum(EnumId::new("status")))],
ReturnDef::Value(TypeExpr::Enum(EnumId::new("status"))),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"public static Status EchoStatus(Status s)",
"the public wrapper signature to name the Status enum on both sides, not the backing int",
);
assert_source_contains(
&src,
"internal static extern Status EchoStatus(Status s);",
"the DllImport signature to declare the enum type directly so the CLR marshals it transparently as its backing int",
);
assert_source_contains(
&src,
"return NativeMethods.EchoStatus(s);",
"the wrapper body to pass the enum through unchanged, no cast required",
);
assert_source_lacks(
&src,
"(int)s",
"no explicit int cast since the CLR handles enum marshaling",
);
}
#[test]
fn emit_c_style_enum_produces_per_enum_file() {
let mut contract = empty_contract();
contract.catalog.insert_enum(c_style_enum(
"status",
vec!["Active", "Inactive", "Pending"],
));
let files = emit_files_for(&contract);
let status_cs = files
.iter()
.find(|(name, _)| name == "Status.cs")
.expect("expecting Status.cs to be generated for the status enum");
assert_source_contains(
&status_cs.1,
"public enum Status : int",
"the native C# enum declaration with explicit int backing type",
);
assert_source_contains(
&status_cs.1,
"Active = 0",
"variant tags as ordinal indices matching EnumTagStrategy::OrdinalIndex",
);
assert_source_contains(
&status_cs.1,
"internal static class StatusWire",
"the paired static helper class with Decode and the WireEncodeTo extension",
);
}
#[test]
fn emit_u8_c_style_enum_declares_byte_backing_but_uses_4_byte_i32_wire_codec() {
let mut contract = empty_contract();
contract.catalog.insert_enum(c_style_enum_with_tag_type(
"log_level",
PrimitiveType::U8,
vec!["Trace", "Debug", "Info", "Warn", "Error"],
));
let files = emit_files_for(&contract);
let log_level_cs = files
.iter()
.find(|(name, _)| name == "LogLevel.cs")
.expect("expecting LogLevel.cs to be generated for the log_level enum");
assert_source_contains(
&log_level_cs.1,
"public enum LogLevel : byte",
"the native C# enum declaration with the repr(u8) backing type preserved",
);
assert_source_contains(
&log_level_cs.1,
"internal const int WireEncodedSize = 4;",
"the wire codec to use the cross-backend 4-byte i32 ordinal format",
);
assert_source_contains(
&log_level_cs.1,
"reader.ReadI32() switch",
"the decode helper to read a 4-byte i32 ordinal and switch on it",
);
assert_source_contains(
&log_level_cs.1,
"wire.WriteI32(value switch",
"the encode helper to write a 4-byte i32 after mapping the variant to its ordinal",
);
}
#[test]
fn emit_vec_string_round_trips_through_encoded_array_helpers() {
let mut contract = empty_contract();
contract.functions.push(function_with_types(
"echo_vec_string",
vec![("v", TypeExpr::Vec(Box::new(TypeExpr::String)))],
ReturnDef::Value(TypeExpr::Vec(Box::new(TypeExpr::String))),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"public static string[] EchoVecString(string[] v)",
"the public wrapper exposes Vec<String> on both sides as string[]",
);
assert_source_contains(
&src,
"internal static extern FfiBuf EchoVecString(byte[] v, UIntPtr vLen);",
"the DllImport carries the wire-encoded buffer, not a raw string[]",
);
assert_source_contains(
&src,
"WireWriter.EncodedArraySize(v, sizeItem0 => (4 + Encoding.UTF8.GetByteCount(sizeItem0)))",
"the WireWriter size hint uses EncodedArraySize with a per-element UTF-8 byte-count lambda",
);
assert_source_contains(
&src,
"_wire_v.WriteI32(v.Length); foreach (string item0 in v) { _wire_v.WriteString(item0); }",
"the encode body writes the 4-byte count then loops WriteString over each element",
);
assert_source_contains(
&src,
"return new WireReader(_buf).ReadEncodedArray<string>(r0 => r0.ReadString());",
"the return decodes through ReadEncodedArray with a ReadString closure per element",
);
}
#[test]
fn emit_vec_vec_i32_nests_blittable_inside_encoded() {
let mut contract = empty_contract();
contract.functions.push(function_with_types(
"echo_vec_vec_i32",
vec![(
"v",
TypeExpr::Vec(Box::new(TypeExpr::Vec(Box::new(TypeExpr::Primitive(
PrimitiveType::I32,
))))),
)],
ReturnDef::Value(TypeExpr::Vec(Box::new(TypeExpr::Vec(Box::new(
TypeExpr::Primitive(PrimitiveType::I32),
))))),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"public static int[][] EchoVecVecI32(int[][] v)",
"the public wrapper exposes Vec<Vec<i32>> as a jagged int[][]",
);
assert_source_contains(
&src,
"_wire_v.WriteI32(v.Length); foreach (int[] item0 in v) { _wire_v.WriteBlittableArray(item0); }",
"the outer write emits the count then loops WriteBlittableArray (which writes its own length prefix) over each inner array",
);
assert_source_contains(
&src,
"return new WireReader(_buf).ReadEncodedArray<int[]>(r0 => r0.ReadLengthPrefixedBlittableArray<int>());",
"the return decodes through ReadEncodedArray wrapping a nested ReadLengthPrefixedBlittableArray",
);
}
#[test]
fn emit_vec_vec_string_doubles_the_encoded_array_path() {
let mut contract = empty_contract();
contract.functions.push(function_with_types(
"echo_vec_vec_string",
vec![(
"v",
TypeExpr::Vec(Box::new(TypeExpr::Vec(Box::new(TypeExpr::String)))),
)],
ReturnDef::Value(TypeExpr::Vec(Box::new(TypeExpr::Vec(Box::new(
TypeExpr::String,
))))),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"public static string[][] EchoVecVecString(string[][] v)",
"the public wrapper exposes Vec<Vec<String>> as a jagged string[][]",
);
assert_source_contains(
&src,
"return new WireReader(_buf).ReadEncodedArray<string[]>(r1 => r1.ReadEncodedArray<string>(r0 => r0.ReadString()));",
"the return decodes through two nested ReadEncodedArray closures with distinct receiver names",
);
assert_source_contains(
&src,
"_wire_v.WriteI32(v.Length); foreach (string[] item1 in v) { _wire_v.WriteI32(item1.Length); foreach (string item0 in item1) { _wire_v.WriteString(item0); }; }",
"the encode body nests two foreach loops with distinct loop variables",
);
}
#[test]
fn emit_data_enum_variant_nested_vec_string_prefixes_only_outer_field_access() {
let mut contract = empty_contract();
contract.catalog.insert_enum(data_enum_single_variant(
"filter",
"ByGroups",
(
"groups",
TypeExpr::Vec(Box::new(TypeExpr::Vec(Box::new(TypeExpr::String)))),
),
));
contract.functions.push(function_with_types(
"echo_filter",
vec![("f", TypeExpr::Enum(EnumId::new("filter")))],
ReturnDef::Value(TypeExpr::Enum(EnumId::new("filter"))),
));
let files = emit_files_for(&contract);
let enum_src = files
.iter()
.find(|(name, _)| name == "Filter.cs")
.expect("Filter.cs")
.1
.as_str();
assert_source_contains(
enum_src,
"ByGroups _v => WireWriter.EncodedArraySize(_v.Groups, sizeItem1 => WireWriter.EncodedArraySize(sizeItem1, sizeItem0 => (4 + Encoding.UTF8.GetByteCount(sizeItem0))))",
"the size expression to prefix only the outer field access and keep distinct nested lambda variables",
);
assert_source_contains(
enum_src,
"wire.WriteI32(_v.Groups.Length); foreach (string[] item1 in _v.Groups) { wire.WriteI32(item1.Length); foreach (string item0 in item1) { wire.WriteString(item0); }; }",
"the encode body to prefix only the outer field access and keep the nested foreach bindings untouched",
);
assert_source_lacks(
enum_src,
"_v.item1",
"the outer `_v` prefix must not leak into the nested foreach binding",
);
assert_source_lacks(
enum_src,
"_v.item0",
"the outer `_v` prefix must not leak into the innermost foreach binding",
);
assert_source_lacks(
enum_src,
"_v.sizeItem1",
"the outer `_v` prefix must not leak into the nested size lambda binding",
);
assert_source_lacks(
enum_src,
"_v.sizeItem0",
"the outer `_v` prefix must not leak into the innermost size lambda binding",
);
}
#[test]
fn emit_flatten_vec_vec_i32_keeps_top_level_return_on_blittable_fast_path() {
let mut contract = empty_contract();
contract.functions.push(function_with_types(
"flatten_vec_vec_i32",
vec![(
"v",
TypeExpr::Vec(Box::new(TypeExpr::Vec(Box::new(TypeExpr::Primitive(
PrimitiveType::I32,
))))),
)],
ReturnDef::Value(TypeExpr::Vec(Box::new(TypeExpr::Primitive(
PrimitiveType::I32,
)))),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"return new WireReader(_buf).ReadBlittableArray<int>();",
"the top-level Vec<i32> return stays on the no-prefix fast path, count taken from FfiBuf.len",
);
}
#[test]
fn emit_vec_c_style_enum_round_trips_through_encoded_array_helpers() {
let mut contract = empty_contract();
contract.catalog.insert_enum(c_style_enum(
"status",
vec!["Active", "Inactive", "Pending"],
));
contract.functions.push(function_with_types(
"echo_vec_status",
vec![(
"values",
TypeExpr::Vec(Box::new(TypeExpr::Enum(EnumId::new("status")))),
)],
ReturnDef::Value(TypeExpr::Vec(Box::new(TypeExpr::Enum(EnumId::new(
"status",
))))),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"public static Status[] EchoVecStatus(Status[] values)",
"the public wrapper exposes Vec<Status> on both sides as Status[]",
);
assert_source_contains(
&src,
"internal static extern FfiBuf EchoVecStatus(byte[] values, UIntPtr valuesLen);",
"the DllImport carries the wire-encoded buffer, matching the macro's WireEncoded classification for Vec<Scalar>",
);
assert_source_contains(
&src,
"_wire_values.WriteI32(values.Length); foreach (Status item0 in values) { item0.WireEncodeTo(_wire_values); }",
"the encode body writes the 4-byte count then loops WireEncodeTo over each enum value",
);
assert_source_contains(
&src,
"return new WireReader(_buf).ReadEncodedArray<Status>(r0 => StatusWire.Decode(r0));",
"the return decodes through ReadEncodedArray with the StatusWire.Decode helper per element",
);
}
#[test]
fn emit_vec_data_enum_round_trips_through_encoded_array_helpers() {
let mut contract = empty_contract();
contract.catalog.insert_enum(data_enum_single_variant(
"shape",
"Circle",
("radius", TypeExpr::Primitive(PrimitiveType::F64)),
));
contract.functions.push(function_with_types(
"echo_vec_shape",
vec![(
"values",
TypeExpr::Vec(Box::new(TypeExpr::Enum(EnumId::new("shape")))),
)],
ReturnDef::Value(TypeExpr::Vec(Box::new(TypeExpr::Enum(EnumId::new(
"shape",
))))),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"public static Shape[] EchoVecShape(Shape[] values)",
"the public wrapper exposes Vec<Shape> on both sides as Shape[]",
);
assert_source_contains(
&src,
"internal static extern FfiBuf EchoVecShape(byte[] values, UIntPtr valuesLen);",
"the DllImport takes a wire-encoded buffer and returns an FfiBuf",
);
assert_source_contains(
&src,
"_wire_values.WriteI32(values.Length); foreach (Shape item0 in values) { item0.WireEncodeTo(_wire_values); }",
"the encode body writes the count and loops the data enum's WireEncodeTo over each element",
);
assert_source_contains(
&src,
"return new WireReader(_buf).ReadEncodedArray<Shape>(r0 => Shape.Decode(r0));",
"the return decodes through ReadEncodedArray with Shape.Decode per element",
);
}
#[test]
fn emit_vec_non_blittable_record_round_trips_through_encoded_array_helpers() {
let mut contract = empty_contract();
contract.catalog.insert_record(record_with_fields(
"person",
false,
vec![
("name", TypeExpr::String),
("age", TypeExpr::Primitive(PrimitiveType::U32)),
],
));
contract.functions.push(function_with_types(
"echo_vec_person",
vec![(
"people",
TypeExpr::Vec(Box::new(TypeExpr::Record(RecordId::new("person")))),
)],
ReturnDef::Value(TypeExpr::Vec(Box::new(TypeExpr::Record(RecordId::new(
"person",
))))),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"public static Person[] EchoVecPerson(Person[] people)",
"the public wrapper exposes Vec<Person> on both sides as Person[]",
);
assert_source_contains(
&src,
"internal static extern FfiBuf EchoVecPerson(byte[] people, UIntPtr peopleLen);",
"the DllImport takes a wire-encoded buffer and returns an FfiBuf",
);
assert_source_contains(
&src,
"_wire_people.WriteI32(people.Length); foreach (Person item0 in people) { item0.WireEncodeTo(_wire_people); }",
"the encode body writes the count and loops the record's WireEncodeTo over each element",
);
assert_source_contains(
&src,
"return new WireReader(_buf).ReadEncodedArray<Person>(r0 => Person.Decode(r0));",
"the return decodes through ReadEncodedArray with Person.Decode per element",
);
assert_source_lacks(
&src,
"fixed (Person*",
"non-blittable record vecs should not go through the pinned fast path",
);
}
#[test]
fn emit_record_with_blittable_vec_field_uses_length_prefixed_blittable_codec() {
let mut contract = empty_contract();
contract.catalog.insert_record(record_with_fields(
"point",
true,
vec![
("x", TypeExpr::Primitive(PrimitiveType::F64)),
("y", TypeExpr::Primitive(PrimitiveType::F64)),
],
));
contract.catalog.insert_record(record_with_fields(
"polygon",
false,
vec![(
"points",
TypeExpr::Vec(Box::new(TypeExpr::Record(RecordId::new("point")))),
)],
));
contract.functions.push(function_with_types(
"echo_polygon",
vec![("p", TypeExpr::Record(RecordId::new("polygon")))],
ReturnDef::Value(TypeExpr::Record(RecordId::new("polygon"))),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"wire.WriteBlittableArray(this.Points)",
"the record's encode body writes the Vec<Point> via WriteBlittableArray, \
which emits the 4-byte count and the raw element bytes",
);
assert_source_contains(
&src,
"reader.ReadLengthPrefixedBlittableArray<Point>()",
"the record's decode reads the Vec<Point> back through the length-prefixed blittable helper",
);
assert_source_contains(
&src,
"(4 + this.Points.Length * 16)",
"the size expression accounts for the 4-byte length prefix and the element stride \
(two f64s → 16 bytes per Point)",
);
}
#[test]
fn emit_option_primitive_round_trip_uses_tagged_wire_encoding() {
let mut contract = empty_contract();
contract.functions.push(function_with_types(
"echo_optional_i32",
vec![(
"v",
TypeExpr::Option(Box::new(TypeExpr::Primitive(PrimitiveType::I32))),
)],
ReturnDef::Value(TypeExpr::Option(Box::new(TypeExpr::Primitive(
PrimitiveType::I32,
)))),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"public static int? EchoOptionalI32(int? v)",
"the public wrapper exposes the Option<i32> param and return as int? on both sides",
);
assert_source_contains(
&src,
"using var _wire_v = new WireWriter((1 + (v is { } sizeOpt0 ? 4 : 0)));",
"the WireWriter is rented with 1 byte for the tag plus the inner size when present, \
using the non-null pattern binding under a size-specific prefix so it doesn't \
collide with the write-side `opt0` in the same method scope",
);
assert_source_contains(
&src,
"if (v is { } opt0) { _wire_v.WriteU8((byte)1); _wire_v.WriteI32(opt0); } \
else { _wire_v.WriteU8((byte)0); }",
"the encode body uses the non-null pattern binding to name the unwrapped value \
once for both the tag write and the primitive write",
);
assert_source_contains(
&src,
"internal static extern FfiBuf EchoOptionalI32(byte[] v, UIntPtr vLen);",
"the DllImport takes the option as a wire-encoded byte[] + length pair and \
returns an FfiBuf carrying the tagged response",
);
assert_source_contains(
&src,
"var reader = new WireReader(_buf); \
return reader.ReadU8() == 0 ? (int?)null : reader.ReadI32();",
"the return body binds a reader local, reads the 1-byte tag, and casts null on \
the missing branch so the conditional resolves to int? rather than bare null",
);
assert_source_contains(
&src,
"NativeMethods.FreeBuf(_buf);",
"the FfiBuf is freed in a finally block, same as every other wire-decoded return",
);
}
#[test]
fn emit_function_returning_option_primitive_keeps_direct_param_but_wires_return() {
let mut contract = empty_contract();
contract.functions.push(function_with_types(
"find_even",
vec![("value", TypeExpr::Primitive(PrimitiveType::I32))],
ReturnDef::Value(TypeExpr::Option(Box::new(TypeExpr::Primitive(
PrimitiveType::I32,
)))),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"public static int? FindEven(int value)",
"the wrapper exposes the i32 param directly and the Option<i32> return as int?",
);
assert_source_contains(
&src,
"internal static extern FfiBuf FindEven(int value);",
"the DllImport keeps the i32 param direct and returns an FfiBuf for the tagged option",
);
assert_source_contains(
&src,
"var reader = new WireReader(_buf); \
return reader.ReadU8() == 0 ? (int?)null : reader.ReadI32();",
"the return body reads the option tag and either returns null or the decoded i32",
);
assert_source_lacks(
&src,
"using var _wire_value",
"a direct-param i32 should not get a WireWriter setup, even when the return is Option",
);
}
#[test]
fn emit_every_generated_file_opts_in_to_nullable_annotations() {
let mut contract = empty_contract();
contract.catalog.insert_record(record_with_fields(
"point",
true,
vec![
("x", TypeExpr::Primitive(PrimitiveType::F64)),
("y", TypeExpr::Primitive(PrimitiveType::F64)),
],
));
contract.catalog.insert_enum(EnumDef {
id: EnumId::new("status"),
repr: EnumRepr::CStyle {
tag_type: PrimitiveType::I32,
variants: vec![CStyleVariant {
name: "Active".into(),
discriminant: 0,
doc: None,
}],
},
is_error: false,
constructors: vec![],
methods: vec![],
doc: None,
deprecated: None,
});
contract.catalog.insert_enum(EnumDef {
id: EnumId::new("shape"),
repr: EnumRepr::Data {
tag_type: PrimitiveType::I32,
variants: vec![DataVariant {
name: "Circle".into(),
discriminant: 0,
payload: VariantPayload::Struct(vec![FieldDef {
name: FieldName::new("radius"),
type_expr: TypeExpr::Primitive(PrimitiveType::F64),
doc: None,
default: None,
}]),
doc: None,
}],
},
is_error: false,
constructors: vec![],
methods: vec![],
doc: None,
deprecated: None,
});
contract.functions.push(primitive_function(
"add",
vec![("a", PrimitiveType::I32), ("b", PrimitiveType::I32)],
ReturnDef::Value(TypeExpr::Primitive(PrimitiveType::I32)),
));
let output = emit_contract(&contract);
for file in &output.files {
assert!(
file.source.contains("#nullable enable"),
"expecting #nullable enable in {} but not found:\n{}",
file.file_name,
file.source,
);
}
}
#[test]
fn emit_option_string_renders_utf8_sized_wire_payload() {
let mut contract = empty_contract();
contract.functions.push(function_with_types(
"echo_optional_string",
vec![("v", TypeExpr::Option(Box::new(TypeExpr::String)))],
ReturnDef::Value(TypeExpr::Option(Box::new(TypeExpr::String))),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"public static string? EchoOptionalString(string? v)",
"Option<String> renders as string? on both sides under #nullable enable",
);
assert_source_contains(
&src,
"using var _wire_v = new WireWriter((1 + (v is { } sizeOpt0 ? (4 + Encoding.UTF8.GetByteCount(sizeOpt0)) : 0)));",
"the size sums the 1-byte tag with the 4-byte length prefix and the payload's UTF-8 byte count",
);
assert_source_contains(
&src,
"if (v is { } opt0) { _wire_v.WriteU8((byte)1); _wire_v.WriteString(opt0); } else { _wire_v.WriteU8((byte)0); }",
"the encode dispatches to WriteString on the unwrapped value",
);
assert_source_contains(
&src,
"var reader = new WireReader(_buf); return reader.ReadU8() == 0 ? (string?)null : reader.ReadString();",
"the decode casts the null branch to string? so the conditional resolves to the nullable reference type",
);
}
#[test]
fn emit_option_blittable_record_writes_and_decodes_through_record_helpers() {
let mut contract = empty_contract();
contract.catalog.insert_record(record_with_fields(
"point",
true,
vec![
("x", TypeExpr::Primitive(PrimitiveType::F64)),
("y", TypeExpr::Primitive(PrimitiveType::F64)),
],
));
contract.functions.push(function_with_types(
"echo_optional_point",
vec![(
"v",
TypeExpr::Option(Box::new(TypeExpr::Record(RecordId::new("point")))),
)],
ReturnDef::Value(TypeExpr::Option(Box::new(TypeExpr::Record(RecordId::new(
"point",
))))),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"public static Point? EchoOptionalPoint(Point? v)",
"Option<Point> renders as Point?: value-type inner desugars to Nullable<Point>",
);
assert_source_contains(
&src,
"using var _wire_v = new WireWriter((1 + (v is { } sizeOpt0 ? 16 : 0)));",
"Point is two f64 fields so the payload contributes a fixed 16 bytes after the 1-byte tag",
);
assert_source_contains(
&src,
"if (v is { } opt0) { _wire_v.WriteU8((byte)1); opt0.WireEncodeTo(_wire_v); } else { _wire_v.WriteU8((byte)0); }",
"encode dispatches to the record's own WireEncodeTo on the unwrapped value",
);
assert_source_contains(
&src,
"var reader = new WireReader(_buf); return reader.ReadU8() == 0 ? (Point?)null : Point.Decode(reader);",
"decode casts the null branch to Point? and otherwise reconstructs through Point.Decode",
);
}
#[test]
fn emit_option_c_style_enum_goes_through_wire_helpers() {
let mut contract = empty_contract();
contract.catalog.insert_enum(EnumDef {
id: EnumId::new("status"),
repr: EnumRepr::CStyle {
tag_type: PrimitiveType::I32,
variants: vec![
CStyleVariant {
name: "Active".into(),
discriminant: 0,
doc: None,
},
CStyleVariant {
name: "Inactive".into(),
discriminant: 1,
doc: None,
},
],
},
is_error: false,
constructors: vec![],
methods: vec![],
doc: None,
deprecated: None,
});
contract.functions.push(function_with_types(
"echo_optional_status",
vec![(
"v",
TypeExpr::Option(Box::new(TypeExpr::Enum(EnumId::new("status")))),
)],
ReturnDef::Value(TypeExpr::Option(Box::new(TypeExpr::Enum(EnumId::new(
"status",
))))),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"public static Status? EchoOptionalStatus(Status? v)",
"Option<Status> renders as Status?: C# enums are value types, so the nullable is Nullable<Status>",
);
assert_source_contains(
&src,
"if (v is { } opt0) { _wire_v.WriteU8((byte)1); opt0.WireEncodeTo(_wire_v); } else { _wire_v.WriteU8((byte)0); }",
"encode dispatches to the StatusWire extension method on the unwrapped enum value",
);
assert_source_contains(
&src,
"var reader = new WireReader(_buf); return reader.ReadU8() == 0 ? (Status?)null : StatusWire.Decode(reader);",
"decode calls StatusWire.Decode on the Some branch, null-casts on the None branch",
);
}
#[test]
fn emit_option_data_enum_decodes_through_enum_static_decode() {
let mut contract = empty_contract();
contract.catalog.insert_enum(EnumDef {
id: EnumId::new("shape"),
repr: EnumRepr::Data {
tag_type: PrimitiveType::I32,
variants: vec![
DataVariant {
name: "Circle".into(),
discriminant: 0,
payload: VariantPayload::Struct(vec![FieldDef {
name: FieldName::new("radius"),
type_expr: TypeExpr::Primitive(PrimitiveType::F64),
doc: None,
default: None,
}]),
doc: None,
},
DataVariant {
name: "Square".into(),
discriminant: 1,
payload: VariantPayload::Unit,
doc: None,
},
],
},
is_error: false,
constructors: vec![],
methods: vec![],
doc: None,
deprecated: None,
});
contract.functions.push(function_with_types(
"find_shape",
vec![("id", TypeExpr::Primitive(PrimitiveType::I32))],
ReturnDef::Value(TypeExpr::Option(Box::new(TypeExpr::Enum(EnumId::new(
"shape",
))))),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"public static Shape? FindShape(int id)",
"Option<Shape> renders as Shape?: Shape is an abstract record, so `?` means nullable reference",
);
assert_source_contains(
&src,
"var reader = new WireReader(_buf); return reader.ReadU8() == 0 ? (Shape?)null : Shape.Decode(reader);",
"decode reads the present tag, then either null-casts or dispatches to the enum's Decode",
);
}
#[test]
fn emit_record_with_two_option_fields_uses_distinct_pattern_bindings() {
let mut contract = empty_contract();
contract.catalog.insert_record(record_with_fields(
"user_profile",
false,
vec![
("name", TypeExpr::String),
("email", TypeExpr::Option(Box::new(TypeExpr::String))),
(
"score",
TypeExpr::Option(Box::new(TypeExpr::Primitive(PrimitiveType::F64))),
),
],
));
contract.functions.push(function_with_types(
"echo_user_profile",
vec![("profile", TypeExpr::Record(RecordId::new("user_profile")))],
ReturnDef::Value(TypeExpr::Record(RecordId::new("user_profile"))),
));
let output = emit_contract(&contract);
let record_source = output
.files
.iter()
.find(|f| f.file_name == "UserProfile.cs")
.expect("UserProfile.cs")
.source
.as_str();
assert_source_contains(
record_source,
"(1 + (this.Email is { } sizeOpt0 ? (4 + Encoding.UTF8.GetByteCount(sizeOpt0)) : 0)) +",
"the first Option field's size contribution uses sizeOpt0",
);
assert_source_contains(
record_source,
"(1 + (this.Score is { } sizeOpt1 ? 8 : 0))",
"the second Option field's size contribution advances to sizeOpt1, \
confirming the shared emit context is threaded across sibling fields",
);
assert_source_contains(
record_source,
"if (this.Email is { } opt0) { wire.WriteU8((byte)1); wire.WriteString(opt0); } else { wire.WriteU8((byte)0); };",
"the first Option field's encode uses opt0",
);
assert_source_contains(
record_source,
"if (this.Score is { } opt1) { wire.WriteU8((byte)1); wire.WriteF64(opt1); } else { wire.WriteU8((byte)0); };",
"the second Option field's encode advances to opt1",
);
assert_source_contains(
record_source,
"reader.ReadU8() == 0 ? (string?)null : reader.ReadString()",
"the string? field decodes through ReadString with the (string?)null cast on the None branch",
);
assert_source_contains(
record_source,
"reader.ReadU8() == 0 ? (double?)null : reader.ReadF64()",
"the double? field decodes through ReadF64 with the (double?)null cast on the None branch",
);
}
#[test]
fn emit_option_vec_of_primitive_wraps_blittable_vec_in_option_tag() {
let mut contract = empty_contract();
contract.functions.push(function_with_types(
"echo_optional_vec",
vec![(
"v",
TypeExpr::Option(Box::new(TypeExpr::Vec(Box::new(TypeExpr::Primitive(
PrimitiveType::I32,
))))),
)],
ReturnDef::Value(TypeExpr::Option(Box::new(TypeExpr::Vec(Box::new(
TypeExpr::Primitive(PrimitiveType::I32),
))))),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"public static int[]? EchoOptionalVec(int[]? v)",
"Option<Vec<i32>> renders as int[]?: the `?` nullability applies to the whole array",
);
assert_source_contains(
&src,
"using var _wire_v = new WireWriter((1 + (v is { } sizeOpt0 ? (4 + sizeOpt0.Length * 4) : 0)));",
"size sums the 1-byte Option tag with the 4-byte length prefix and the raw element bytes",
);
assert_source_contains(
&src,
"if (v is { } opt0) { _wire_v.WriteU8((byte)1); _wire_v.WriteBlittableArray(opt0); } else { _wire_v.WriteU8((byte)0); }",
"encode dispatches to WriteBlittableArray on the unwrapped vec; the fast path is preserved \
inside the Option wrapping",
);
assert_source_contains(
&src,
"var reader = new WireReader(_buf); return reader.ReadU8() == 0 ? (int[]?)null : reader.ReadLengthPrefixedBlittableArray<int>();",
"decode null-casts to int[]? and otherwise reads through the length-prefixed blittable helper",
);
}
#[test]
fn emit_vec_of_option_composes_per_element_tag_into_encoded_array() {
let mut contract = empty_contract();
contract.functions.push(function_with_types(
"echo_vec_optional_i32",
vec![(
"v",
TypeExpr::Vec(Box::new(TypeExpr::Option(Box::new(TypeExpr::Primitive(
PrimitiveType::I32,
))))),
)],
ReturnDef::Value(TypeExpr::Vec(Box::new(TypeExpr::Option(Box::new(
TypeExpr::Primitive(PrimitiveType::I32),
))))),
));
let src = emit_contract(&contract).combined_source();
assert_source_contains(
&src,
"public static int?[] EchoVecOptionalI32(int?[] v)",
"Vec<Option<i32>> renders as int?[]: array of Nullable<int>, not a nullable array",
);
assert_source_contains(
&src,
"_wire_v.WriteI32(v.Length); foreach (int? item0 in v) { if (item0 is { } opt0) { _wire_v.WriteU8((byte)1); _wire_v.WriteI32(opt0); } else { _wire_v.WriteU8((byte)0); }; }",
"encode writes the i32 length then loops each element through its own tag + payload",
);
assert_source_contains(
&src,
"return new WireReader(_buf).ReadEncodedArray<int?>(r0 => r0.ReadU8() == 0 ? (int?)null : r0.ReadI32());",
"decode walks ReadEncodedArray with a per-element closure that reads the Option tag first, \
null-casting on the None branch and reading the i32 payload on the Some branch",
);
}
}