use std::collections::HashSet;
use std::fmt;
use crate::ir::codec::EnumLayout;
use crate::ir::definitions::{EnumDef, EnumRepr};
use crate::ir::ops::ReadOp;
use crate::ir::types::{PrimitiveType, TypeExpr};
use boltffi_ffi_rules::naming::{LibraryName, Name};
use super::NamingConvention;
#[derive(Debug, Clone)]
pub struct CSharpModule {
pub namespace: String,
pub class_name: String,
pub lib_name: Name<LibraryName>,
pub prefix: String,
pub records: Vec<CSharpRecord>,
pub enums: Vec<CSharpEnum>,
pub functions: Vec<CSharpFunction>,
}
impl CSharpModule {
pub fn has_functions(&self) -> bool {
!self.functions.is_empty()
}
pub fn needs_system_text(&self) -> bool {
self.functions
.iter()
.any(|f| f.params.iter().any(|p| p.csharp_type.contains_string()))
|| self.records.iter().any(CSharpRecord::has_string_fields)
}
pub fn has_wire_params(&self) -> bool {
self.functions.iter().any(|f| !f.wire_writers.is_empty())
}
pub fn has_ffi_buf_returns(&self) -> bool {
self.functions
.iter()
.any(|f| f.return_kind.native_returns_ffi_buf())
}
pub fn needs_ffi_buf(&self) -> bool {
self.has_ffi_buf_returns() || !self.records.is_empty() || !self.enums.is_empty()
}
pub fn needs_wire_reader(&self) -> bool {
self.has_ffi_buf_returns() || !self.records.is_empty() || !self.enums.is_empty()
}
pub fn needs_wire_writer(&self) -> bool {
self.has_wire_params() || !self.records.is_empty() || !self.enums.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CSharpType {
Void,
Bool,
SByte,
Byte,
Short,
UShort,
Int,
UInt,
Long,
ULong,
NInt,
NUInt,
Float,
Double,
String,
Record(String),
CStyleEnum(String),
DataEnum(String),
Array(Box<CSharpType>),
Nullable(Box<CSharpType>),
}
impl CSharpType {
pub fn is_void(&self) -> bool {
matches!(self, Self::Void)
}
pub fn is_bool(&self) -> bool {
matches!(self, Self::Bool)
}
pub fn is_string(&self) -> bool {
matches!(self, Self::String)
}
pub fn contains_string(&self) -> bool {
match self {
Self::String => true,
Self::Array(inner) | Self::Nullable(inner) => inner.contains_string(),
_ => false,
}
}
pub fn is_record(&self) -> bool {
matches!(self, Self::Record(_))
}
pub fn is_c_style_enum(&self) -> bool {
matches!(self, Self::CStyleEnum(_))
}
pub fn is_data_enum(&self) -> bool {
matches!(self, Self::DataEnum(_))
}
pub fn is_array(&self) -> bool {
matches!(self, Self::Array(_))
}
pub fn array_element(&self) -> Option<&CSharpType> {
match self {
Self::Array(inner) => Some(inner),
_ => None,
}
}
pub fn qualify_if_shadowed(
self,
shadowed: &std::collections::HashSet<String>,
namespace: &str,
) -> Self {
match self {
Self::Record(n) if shadowed.contains(&n) => {
Self::Record(format!("global::{}.{}", namespace, n))
}
Self::CStyleEnum(n) if shadowed.contains(&n) => {
Self::CStyleEnum(format!("global::{}.{}", namespace, n))
}
Self::DataEnum(n) if shadowed.contains(&n) => {
Self::DataEnum(format!("global::{}.{}", namespace, n))
}
Self::Array(inner) => {
Self::Array(Box::new((*inner).qualify_if_shadowed(shadowed, namespace)))
}
Self::Nullable(inner) => {
Self::Nullable(Box::new((*inner).qualify_if_shadowed(shadowed, namespace)))
}
other => other,
}
}
pub fn qualify_if_shadowed_opt(self, scope: Option<&ShadowScope<'_>>) -> Self {
match scope {
Some(s) => self.qualify_if_shadowed(s.shadowed, s.namespace),
None => self,
}
}
pub fn enum_backing_for(tag_type: PrimitiveType) -> Option<CSharpType> {
match tag_type {
PrimitiveType::I8 => Some(CSharpType::SByte),
PrimitiveType::U8 => Some(CSharpType::Byte),
PrimitiveType::I16 => Some(CSharpType::Short),
PrimitiveType::U16 => Some(CSharpType::UShort),
PrimitiveType::I32 => Some(CSharpType::Int),
PrimitiveType::U32 => Some(CSharpType::UInt),
PrimitiveType::I64 => Some(CSharpType::Long),
PrimitiveType::U64 => Some(CSharpType::ULong),
PrimitiveType::Bool
| PrimitiveType::ISize
| PrimitiveType::USize
| PrimitiveType::F32
| PrimitiveType::F64 => None,
}
}
pub fn for_enum(enum_def: &EnumDef) -> CSharpType {
let class_name = NamingConvention::class_name(enum_def.id.as_str());
match &enum_def.repr {
EnumRepr::CStyle { .. } => CSharpType::CStyleEnum(class_name),
EnumRepr::Data { .. } => CSharpType::DataEnum(class_name),
}
}
pub fn from_read_op(op: &ReadOp) -> Self {
match op {
ReadOp::Primitive { primitive, .. } => Self::from(*primitive),
ReadOp::String { .. } => Self::String,
ReadOp::Bytes { .. } => Self::Array(Box::new(Self::Byte)),
ReadOp::Option { some, .. } => {
let inner = Self::from_read_op(some.ops.first().expect("option inner read op"));
Self::Nullable(Box::new(inner))
}
ReadOp::Vec { element_type, .. } => {
Self::Array(Box::new(Self::from_type_expr(element_type)))
}
ReadOp::Record { id, .. } => Self::Record(NamingConvention::class_name(id.as_str())),
ReadOp::Enum { id, layout, .. } => {
let class_name = NamingConvention::class_name(id.as_str());
match layout {
EnumLayout::CStyle { .. } => Self::CStyleEnum(class_name),
EnumLayout::Data { .. } | EnumLayout::Recursive => Self::DataEnum(class_name),
}
}
ReadOp::Custom { underlying, .. } => {
Self::from_read_op(underlying.ops.first().expect("custom underlying read op"))
}
ReadOp::Result { .. } | ReadOp::Builtin { .. } => {
todo!("CSharpType::from_read_op: {:?}", op)
}
}
}
pub fn from_type_expr(expr: &TypeExpr) -> Self {
match expr {
TypeExpr::Void => Self::Void,
TypeExpr::Primitive(p) => Self::from(*p),
TypeExpr::String => Self::String,
TypeExpr::Bytes => Self::Array(Box::new(Self::Byte)),
TypeExpr::Vec(inner) => Self::Array(Box::new(Self::from_type_expr(inner))),
TypeExpr::Option(inner) => Self::Nullable(Box::new(Self::from_type_expr(inner))),
TypeExpr::Record(id) => Self::Record(NamingConvention::class_name(id.as_str())),
TypeExpr::Enum(id) => Self::DataEnum(NamingConvention::class_name(id.as_str())),
TypeExpr::Result { .. }
| TypeExpr::Callback(_)
| TypeExpr::Custom(_)
| TypeExpr::Builtin(_)
| TypeExpr::Handle(_) => todo!("CSharpType::from_type_expr: {:?}", expr),
}
}
pub fn is_blittable_leaf(&self) -> bool {
match self {
Self::SByte
| Self::Byte
| Self::Short
| Self::UShort
| Self::Int
| Self::UInt
| Self::Long
| Self::ULong
| Self::NInt
| Self::NUInt
| Self::Float
| Self::Double
| Self::CStyleEnum(_) => true,
Self::Void
| Self::Bool
| Self::String
| Self::Record(_)
| Self::DataEnum(_)
| Self::Array(_)
| Self::Nullable(_) => false,
}
}
}
impl From<PrimitiveType> for CSharpType {
fn from(primitive: PrimitiveType) -> Self {
match primitive {
PrimitiveType::Bool => CSharpType::Bool,
PrimitiveType::I8 => CSharpType::SByte,
PrimitiveType::U8 => CSharpType::Byte,
PrimitiveType::I16 => CSharpType::Short,
PrimitiveType::U16 => CSharpType::UShort,
PrimitiveType::I32 => CSharpType::Int,
PrimitiveType::U32 => CSharpType::UInt,
PrimitiveType::I64 => CSharpType::Long,
PrimitiveType::U64 => CSharpType::ULong,
PrimitiveType::ISize => CSharpType::NInt,
PrimitiveType::USize => CSharpType::NUInt,
PrimitiveType::F32 => CSharpType::Float,
PrimitiveType::F64 => CSharpType::Double,
}
}
}
impl fmt::Display for CSharpType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Void => f.write_str("void"),
Self::Bool => f.write_str("bool"),
Self::SByte => f.write_str("sbyte"),
Self::Byte => f.write_str("byte"),
Self::Short => f.write_str("short"),
Self::UShort => f.write_str("ushort"),
Self::Int => f.write_str("int"),
Self::UInt => f.write_str("uint"),
Self::Long => f.write_str("long"),
Self::ULong => f.write_str("ulong"),
Self::NInt => f.write_str("nint"),
Self::NUInt => f.write_str("nuint"),
Self::Float => f.write_str("float"),
Self::Double => f.write_str("double"),
Self::String => f.write_str("string"),
Self::Record(name) | Self::CStyleEnum(name) | Self::DataEnum(name) => f.write_str(name),
Self::Array(inner) => write!(f, "{inner}[]"),
Self::Nullable(inner) => write!(f, "{inner}?"),
}
}
}
pub struct ShadowScope<'a> {
pub shadowed: &'a HashSet<String>,
pub namespace: &'a str,
}
#[derive(Debug, Clone)]
pub struct CSharpRecord {
pub class_name: String,
pub fields: Vec<CSharpRecordField>,
pub is_blittable: bool,
}
impl CSharpRecord {
pub fn is_empty(&self) -> bool {
self.fields.is_empty()
}
pub fn needs_wire_helpers(&self) -> bool {
!self.is_blittable
}
pub fn has_string_fields(&self) -> bool {
self.fields.iter().any(|f| f.csharp_type.contains_string())
}
}
#[derive(Debug, Clone)]
pub struct CSharpRecordField {
pub name: String,
pub csharp_type: CSharpType,
pub wire_decode_expr: String,
pub wire_size_expr: String,
pub wire_encode_expr: String,
}
#[derive(Debug, Clone)]
pub struct CSharpEnum {
pub class_name: String,
pub kind: CSharpEnumKind,
pub c_style_tag_type: Option<PrimitiveType>,
pub variants: Vec<CSharpEnumVariant>,
pub methods: Vec<CSharpMethod>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CSharpEnumKind {
CStyle,
Data,
}
#[derive(Debug, Clone)]
pub struct CSharpEnumVariant {
pub name: String,
pub tag: i32,
pub wire_tag: i32,
pub fields: Vec<CSharpRecordField>,
}
impl CSharpEnum {
pub fn is_c_style(&self) -> bool {
self.kind == CSharpEnumKind::CStyle
}
pub fn is_data(&self) -> bool {
self.kind == CSharpEnumKind::Data
}
pub fn has_methods(&self) -> bool {
!self.methods.is_empty()
}
fn c_style_tag_type(&self) -> PrimitiveType {
self.c_style_tag_type
.expect("c-style enum helpers only apply to C-style enums")
}
pub fn c_style_backing_type(&self) -> &'static str {
match self.c_style_tag_type() {
PrimitiveType::I8 => "sbyte",
PrimitiveType::U8 => "byte",
PrimitiveType::I16 => "short",
PrimitiveType::U16 => "ushort",
PrimitiveType::I32 => "int",
PrimitiveType::U32 => "uint",
PrimitiveType::I64 => "long",
PrimitiveType::U64 => "ulong",
PrimitiveType::Bool
| PrimitiveType::ISize
| PrimitiveType::USize
| PrimitiveType::F32
| PrimitiveType::F64 => panic!("unsupported C# enum backing type"),
}
}
pub fn has_string_fields(&self) -> bool {
self.variants
.iter()
.flat_map(|v| v.fields.iter())
.any(|f| f.csharp_type.contains_string())
}
}
impl CSharpEnumVariant {
pub fn is_unit(&self) -> bool {
self.fields.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct CSharpMethod {
pub name: String,
pub native_method_name: String,
pub ffi_name: String,
pub receiver: CSharpReceiver,
pub params: Vec<CSharpParam>,
pub return_type: CSharpType,
pub return_kind: CSharpReturnKind,
pub wire_writers: Vec<CSharpWireWriter>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CSharpReceiver {
Static,
InstanceExtension,
InstanceNative,
}
impl CSharpReceiver {
pub fn is_static(&self) -> bool {
matches!(self, Self::Static)
}
pub fn is_instance_extension(&self) -> bool {
matches!(self, Self::InstanceExtension)
}
pub fn is_instance_native(&self) -> bool {
matches!(self, Self::InstanceNative)
}
}
impl CSharpMethod {
pub fn is_void(&self) -> bool {
matches!(self.return_kind, CSharpReturnKind::Void)
}
pub fn wrapper_param_list(&self) -> String {
self.params
.iter()
.map(CSharpParam::wrapper_declaration)
.collect::<Vec<_>>()
.join(", ")
}
pub fn native_call_args(&self) -> String {
self.params
.iter()
.map(CSharpParam::native_call_arg)
.collect::<Vec<_>>()
.join(", ")
}
pub fn native_return_type(&self) -> String {
if self.return_kind.native_returns_ffi_buf() {
"FfiBuf".to_string()
} else {
self.return_type.to_string()
}
}
pub fn pinned_fixed_args(&self) -> Vec<String> {
pinned_fixed_args(&self.params)
}
pub fn has_pinned_params(&self) -> bool {
!self.pinned_fixed_args().is_empty()
}
pub fn native_param_list(&self, owner_class_name: &str, owner_is_blittable: bool) -> String {
let explicit: Vec<String> = self
.params
.iter()
.map(CSharpParam::native_declaration)
.collect();
let self_decl: Option<String> = match self.receiver {
CSharpReceiver::Static => None,
CSharpReceiver::InstanceExtension => Some(format!("{} self", owner_class_name)),
CSharpReceiver::InstanceNative if owner_is_blittable => {
Some(format!("{} self", owner_class_name))
}
CSharpReceiver::InstanceNative => Some("byte[] self, UIntPtr selfLen".to_string()),
};
match self_decl {
Some(d) => std::iter::once(d)
.chain(explicit)
.collect::<Vec<_>>()
.join(", "),
None => explicit.join(", "),
}
}
pub fn full_native_call_args(&self) -> String {
let explicit = self.native_call_args();
let self_prefix: &str = match self.receiver {
CSharpReceiver::Static => "",
CSharpReceiver::InstanceExtension => "self",
CSharpReceiver::InstanceNative => "_selfBytes, (UIntPtr)_selfBytes.Length",
};
match (self_prefix.is_empty(), explicit.is_empty()) {
(true, _) => explicit,
(false, true) => self_prefix.to_string(),
(false, false) => format!("{self_prefix}, {explicit}"),
}
}
}
#[derive(Debug, Clone)]
pub struct CSharpFunction {
pub name: String,
pub params: Vec<CSharpParam>,
pub return_type: CSharpType,
pub return_kind: CSharpReturnKind,
pub ffi_name: String,
pub wire_writers: Vec<CSharpWireWriter>,
}
impl CSharpFunction {
pub fn is_void(&self) -> bool {
matches!(self.return_kind, CSharpReturnKind::Void)
}
pub fn wrapper_param_list(&self) -> String {
self.params
.iter()
.map(CSharpParam::wrapper_declaration)
.collect::<Vec<_>>()
.join(", ")
}
pub fn native_param_list(&self) -> String {
self.params
.iter()
.map(CSharpParam::native_declaration)
.collect::<Vec<_>>()
.join(", ")
}
pub fn native_call_args(&self) -> String {
self.params
.iter()
.map(CSharpParam::native_call_arg)
.collect::<Vec<_>>()
.join(", ")
}
pub fn native_return_type(&self) -> String {
if self.return_kind.native_returns_ffi_buf() {
"FfiBuf".to_string()
} else {
self.return_type.to_string()
}
}
pub fn pinned_fixed_args(&self) -> Vec<String> {
pinned_fixed_args(&self.params)
}
pub fn has_pinned_params(&self) -> bool {
!self.pinned_fixed_args().is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CSharpReturnKind {
Void,
Direct,
WireDecodeString,
WireDecodeObject { class_name: String },
WireDecodeArray { reader_call: String },
WireDecodeOption { decode_expr: String },
}
impl CSharpReturnKind {
pub fn is_void(&self) -> bool {
matches!(self, Self::Void)
}
pub fn is_direct(&self) -> bool {
matches!(self, Self::Direct)
}
pub fn is_wire_decode_string(&self) -> bool {
matches!(self, Self::WireDecodeString)
}
pub fn is_wire_decode_object(&self) -> bool {
matches!(self, Self::WireDecodeObject { .. })
}
pub fn is_wire_decode_array(&self) -> bool {
matches!(self, Self::WireDecodeArray { .. })
}
pub fn is_wire_decode_option(&self) -> bool {
matches!(self, Self::WireDecodeOption { .. })
}
pub fn native_returns_ffi_buf(&self) -> bool {
matches!(
self,
Self::WireDecodeString
| Self::WireDecodeObject { .. }
| Self::WireDecodeArray { .. }
| Self::WireDecodeOption { .. }
)
}
pub fn decode_class_name(&self) -> Option<&str> {
match self {
Self::WireDecodeObject { class_name } => Some(class_name),
_ => None,
}
}
pub fn wire_decode_return(&self, buf_var: &str) -> Option<String> {
match self {
Self::WireDecodeString => {
Some(format!("return new WireReader({}).ReadString();", buf_var))
}
Self::WireDecodeObject { class_name } => Some(format!(
"return {}.Decode(new WireReader({}));",
class_name, buf_var
)),
Self::WireDecodeArray { reader_call } => Some(format!(
"return new WireReader({}).{};",
buf_var, reader_call
)),
Self::WireDecodeOption { decode_expr } => Some(format!(
"var reader = new WireReader({}); return {};",
buf_var, decode_expr
)),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct CSharpParam {
pub name: String,
pub csharp_type: CSharpType,
pub kind: CSharpParamKind,
}
impl CSharpParam {
pub fn wrapper_declaration(&self) -> String {
format!("{} {}", self.csharp_type, self.name)
}
pub fn native_declaration(&self) -> String {
match &self.kind {
CSharpParamKind::Utf8Bytes | CSharpParamKind::WireEncoded { .. } => {
format!("byte[] {name}, UIntPtr {name}Len", name = self.name)
}
CSharpParamKind::Direct if self.csharp_type.is_bool() => {
format!("[MarshalAs(UnmanagedType.I1)] bool {}", self.name)
}
CSharpParamKind::Direct => {
format!("{} {}", self.csharp_type, self.name)
}
CSharpParamKind::DirectArray => {
let element = self
.csharp_type
.array_element()
.expect("DirectArray param must carry an Array type");
let decl = format!("{element}[] {name}, UIntPtr {name}Len", name = self.name);
if matches!(element, CSharpType::Bool) {
format!(
"[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U1)] {decl}"
)
} else {
decl
}
}
CSharpParamKind::PinnedArray { .. } => {
format!("IntPtr {name}, UIntPtr {name}Len", name = self.name)
}
}
}
pub fn native_call_arg(&self) -> String {
match &self.kind {
CSharpParamKind::Direct => self.name.clone(),
CSharpParamKind::Utf8Bytes => {
let buf = format!("_{}Bytes", self.name);
format!("{buf}, (UIntPtr){buf}.Length")
}
CSharpParamKind::WireEncoded { binding_name } => {
format!("{binding_name}, (UIntPtr){binding_name}.Length")
}
CSharpParamKind::DirectArray => {
format!("{name}, (UIntPtr){name}.Length", name = self.name)
}
CSharpParamKind::PinnedArray { element_type } => {
let ptr_name = self
.pinned_ptr_name()
.expect("PinnedArray params must have a pointer local");
format!(
"(IntPtr){ptr_name}, (UIntPtr)({name}.Length * Unsafe.SizeOf<{element_type}>())",
ptr_name = ptr_name,
name = self.name,
)
}
}
}
pub fn setup_statement(&self) -> Option<String> {
match &self.kind {
CSharpParamKind::Utf8Bytes => Some(format!(
"byte[] _{name}Bytes = Encoding.UTF8.GetBytes({name});",
name = self.name
)),
_ => None,
}
}
pub fn pinned_fixed_arg(&self) -> Option<String> {
match &self.kind {
CSharpParamKind::PinnedArray { element_type } => Some(format!(
"{element_type}* {ptr_name} = {name}",
ptr_name = self
.pinned_ptr_name()
.expect("PinnedArray params must have a pointer local"),
name = self.name,
)),
_ => None,
}
}
fn pinned_ptr_name(&self) -> Option<String> {
match self.kind {
CSharpParamKind::PinnedArray { .. } => {
let base_name = self.name.strip_prefix('@').unwrap_or(&self.name);
Some(format!("_{base_name}Ptr"))
}
_ => None,
}
}
}
fn pinned_fixed_args(params: &[CSharpParam]) -> Vec<String> {
params
.iter()
.filter_map(CSharpParam::pinned_fixed_arg)
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CSharpParamKind {
Direct,
Utf8Bytes,
WireEncoded { binding_name: String },
DirectArray,
PinnedArray { element_type: String },
}
#[derive(Debug, Clone)]
pub struct CSharpWireWriter {
pub binding_name: String,
pub bytes_binding_name: String,
pub param_name: String,
pub size_expr: String,
pub encode_expr: String,
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
fn function_with_return(
return_type: CSharpType,
return_kind: CSharpReturnKind,
) -> CSharpFunction {
CSharpFunction {
name: "Test".to_string(),
params: vec![],
return_type,
return_kind,
ffi_name: "boltffi_test".to_string(),
wire_writers: vec![],
}
}
fn param(name: &str, csharp_type: CSharpType, kind: CSharpParamKind) -> CSharpParam {
CSharpParam {
name: name.to_string(),
csharp_type,
kind,
}
}
#[rstest]
#[case::void(CSharpType::Void, CSharpReturnKind::Void, true)]
#[case::int(CSharpType::Int, CSharpReturnKind::Direct, false)]
#[case::bool(CSharpType::Bool, CSharpReturnKind::Direct, false)]
#[case::double(CSharpType::Double, CSharpReturnKind::Direct, false)]
fn is_void(
#[case] return_type: CSharpType,
#[case] return_kind: CSharpReturnKind,
#[case] expected: bool,
) {
assert_eq!(
function_with_return(return_type, return_kind).is_void(),
expected
);
}
#[test]
fn record_type_display_uses_class_name() {
let ty = CSharpType::Record("Point".to_string());
assert_eq!(ty.to_string(), "Point");
assert!(ty.is_record());
}
#[test]
fn c_style_enum_type_display_uses_class_name() {
let ty = CSharpType::CStyleEnum("Status".to_string());
assert_eq!(ty.to_string(), "Status");
assert!(ty.is_c_style_enum());
assert!(!ty.is_data_enum());
}
#[test]
fn data_enum_type_display_uses_class_name() {
let ty = CSharpType::DataEnum("Shape".to_string());
assert_eq!(ty.to_string(), "Shape");
assert!(ty.is_data_enum());
assert!(!ty.is_c_style_enum());
}
#[test]
fn variant_with_empty_fields_is_unit() {
let variant = CSharpEnumVariant {
name: "Active".to_string(),
tag: 0,
wire_tag: 0,
fields: vec![],
};
assert!(variant.is_unit());
}
#[test]
fn variant_with_payload_is_not_unit() {
let variant = CSharpEnumVariant {
name: "Circle".to_string(),
tag: 0,
wire_tag: 0,
fields: vec![CSharpRecordField {
name: "Radius".to_string(),
csharp_type: CSharpType::Double,
wire_decode_expr: "reader.ReadF64()".to_string(),
wire_size_expr: "8".to_string(),
wire_encode_expr: "wire.WriteF64(this.Radius)".to_string(),
}],
};
assert!(!variant.is_unit());
}
#[test]
fn c_style_kind_is_c_style_and_not_data() {
let enumeration = CSharpEnum {
class_name: "Status".to_string(),
kind: CSharpEnumKind::CStyle,
c_style_tag_type: Some(PrimitiveType::I32),
variants: vec![],
methods: vec![],
};
assert!(enumeration.is_c_style());
assert!(!enumeration.is_data());
}
#[test]
fn data_kind_is_data_and_not_c_style() {
let enumeration = CSharpEnum {
class_name: "Shape".to_string(),
kind: CSharpEnumKind::Data,
c_style_tag_type: None,
variants: vec![],
methods: vec![],
};
assert!(enumeration.is_data());
assert!(!enumeration.is_c_style());
}
#[test]
fn c_style_backing_type_maps_primitive_to_csharp_keyword() {
let enumeration = CSharpEnum {
class_name: "LogLevel".to_string(),
kind: CSharpEnumKind::CStyle,
c_style_tag_type: Some(PrimitiveType::U8),
variants: vec![],
methods: vec![],
};
assert_eq!(enumeration.c_style_backing_type(), "byte");
}
#[rstest]
#[case::int(CSharpType::Int, true)]
#[case::double(CSharpType::Double, true)]
#[case::cstyle_enum(CSharpType::CStyleEnum("Status".to_string()), true)]
#[case::bool(CSharpType::Bool, false)]
#[case::string(CSharpType::String, false)]
#[case::record(CSharpType::Record("Point".to_string()), false)]
#[case::data_enum(CSharpType::DataEnum("Shape".to_string()), false)]
#[case::nullable_int(CSharpType::Nullable(Box::new(CSharpType::Int)), false)]
#[case::nullable_string(CSharpType::Nullable(Box::new(CSharpType::String)), false)]
fn is_blittable_leaf_matches_marshaling_story(#[case] ty: CSharpType, #[case] expected: bool) {
assert_eq!(ty.is_blittable_leaf(), expected);
}
#[test]
fn nullable_type_display_appends_question_mark() {
assert_eq!(
CSharpType::Nullable(Box::new(CSharpType::Int)).to_string(),
"int?"
);
assert_eq!(
CSharpType::Nullable(Box::new(CSharpType::String)).to_string(),
"string?"
);
assert_eq!(
CSharpType::Nullable(Box::new(CSharpType::Record("Point".to_string()))).to_string(),
"Point?"
);
}
#[test]
fn contains_string_sees_through_nullable() {
assert!(CSharpType::Nullable(Box::new(CSharpType::String)).contains_string());
assert!(
CSharpType::Array(Box::new(CSharpType::Nullable(Box::new(CSharpType::String))))
.contains_string()
);
assert!(!CSharpType::Nullable(Box::new(CSharpType::Int)).contains_string());
}
#[test]
fn wrapper_declaration_puts_type_before_name() {
let p = param("value", CSharpType::Int, CSharpParamKind::Direct);
assert_eq!(p.wrapper_declaration(), "int value");
}
#[test]
fn wrapper_declaration_uses_record_class_name() {
let p = param(
"point",
CSharpType::Record("Point".to_string()),
CSharpParamKind::Direct,
);
assert_eq!(p.wrapper_declaration(), "Point point");
}
#[test]
fn native_declaration_direct_primitive_matches_wrapper() {
let p = param("value", CSharpType::Int, CSharpParamKind::Direct);
assert_eq!(p.native_declaration(), "int value");
}
#[test]
fn native_declaration_bool_gets_marshal_attribute() {
let p = param("flag", CSharpType::Bool, CSharpParamKind::Direct);
assert_eq!(
p.native_declaration(),
"[MarshalAs(UnmanagedType.I1)] bool flag"
);
}
#[test]
fn native_declaration_blittable_record_passes_by_value() {
let p = param(
"point",
CSharpType::Record("Point".to_string()),
CSharpParamKind::Direct,
);
assert_eq!(p.native_declaration(), "Point point");
}
#[test]
fn native_declaration_string_splits_into_bytes_and_length() {
let p = param("v", CSharpType::String, CSharpParamKind::Utf8Bytes);
assert_eq!(p.native_declaration(), "byte[] v, UIntPtr vLen");
}
#[test]
fn native_declaration_wire_encoded_record_splits_into_bytes_and_length() {
let p = param(
"person",
CSharpType::Record("Person".to_string()),
CSharpParamKind::WireEncoded {
binding_name: "_personBytes".to_string(),
},
);
assert_eq!(p.native_declaration(), "byte[] person, UIntPtr personLen");
}
#[test]
fn native_call_arg_direct_passes_name() {
let p = param("value", CSharpType::Int, CSharpParamKind::Direct);
assert_eq!(p.native_call_arg(), "value");
}
#[test]
fn native_call_arg_utf8_bytes_passes_buffer_and_length() {
let p = param("v", CSharpType::String, CSharpParamKind::Utf8Bytes);
assert_eq!(p.native_call_arg(), "_vBytes, (UIntPtr)_vBytes.Length");
}
#[test]
fn native_call_arg_wire_encoded_uses_binding_name() {
let p = param(
"person",
CSharpType::Record("Person".to_string()),
CSharpParamKind::WireEncoded {
binding_name: "_personBytes".to_string(),
},
);
assert_eq!(
p.native_call_arg(),
"_personBytes, (UIntPtr)_personBytes.Length"
);
}
#[rstest]
#[case::direct(CSharpParamKind::Direct, None)]
#[case::wire_encoded(
CSharpParamKind::WireEncoded { binding_name: "_personBytes".to_string() },
None,
)]
fn setup_statement_non_string_has_none(
#[case] kind: CSharpParamKind,
#[case] expected: Option<&str>,
) {
let p = param("x", CSharpType::Int, kind);
assert_eq!(p.setup_statement().as_deref(), expected);
}
#[test]
fn setup_statement_utf8_bytes_encodes_string() {
let p = param("v", CSharpType::String, CSharpParamKind::Utf8Bytes);
assert_eq!(
p.setup_statement().as_deref(),
Some("byte[] _vBytes = Encoding.UTF8.GetBytes(v);"),
);
}
fn function_with_params(
params: Vec<CSharpParam>,
return_type: CSharpType,
return_kind: CSharpReturnKind,
) -> CSharpFunction {
CSharpFunction {
name: "Test".to_string(),
params,
return_type,
return_kind,
ffi_name: "boltffi_test".to_string(),
wire_writers: vec![],
}
}
#[test]
fn wrapper_param_list_joins_with_comma_space() {
let f = function_with_params(
vec![
param("a", CSharpType::Int, CSharpParamKind::Direct),
param("b", CSharpType::String, CSharpParamKind::Utf8Bytes),
],
CSharpType::Void,
CSharpReturnKind::Void,
);
assert_eq!(f.wrapper_param_list(), "int a, string b");
}
#[test]
fn wrapper_param_list_empty_for_no_params() {
let f = function_with_params(vec![], CSharpType::Void, CSharpReturnKind::Void);
assert_eq!(f.wrapper_param_list(), "");
}
#[test]
fn native_param_list_expands_each_slot_by_kind() {
let f = function_with_params(
vec![
param("flag", CSharpType::Bool, CSharpParamKind::Direct),
param("v", CSharpType::String, CSharpParamKind::Utf8Bytes),
param("count", CSharpType::UInt, CSharpParamKind::Direct),
param(
"person",
CSharpType::Record("Person".to_string()),
CSharpParamKind::WireEncoded {
binding_name: "_personBytes".to_string(),
},
),
],
CSharpType::Void,
CSharpReturnKind::Void,
);
assert_eq!(
f.native_param_list(),
"[MarshalAs(UnmanagedType.I1)] bool flag, byte[] v, UIntPtr vLen, uint count, byte[] person, UIntPtr personLen",
);
}
#[test]
fn native_call_args_mirror_param_shapes() {
let f = function_with_params(
vec![
param("v", CSharpType::String, CSharpParamKind::Utf8Bytes),
param("count", CSharpType::UInt, CSharpParamKind::Direct),
],
CSharpType::Void,
CSharpReturnKind::Void,
);
assert_eq!(
f.native_call_args(),
"_vBytes, (UIntPtr)_vBytes.Length, count",
);
}
#[rstest]
#[case::void(CSharpType::Void, CSharpReturnKind::Void, "void")]
#[case::primitive(CSharpType::Int, CSharpReturnKind::Direct, "int")]
#[case::blittable_record(
CSharpType::Record("Point".to_string()),
CSharpReturnKind::Direct,
"Point",
)]
#[case::string(CSharpType::String, CSharpReturnKind::WireDecodeString, "FfiBuf")]
#[case::wire_record(
CSharpType::Record("Person".to_string()),
CSharpReturnKind::WireDecodeObject { class_name: "Person".to_string() },
"FfiBuf",
)]
#[case::option_primitive(
CSharpType::Nullable(Box::new(CSharpType::Int)),
CSharpReturnKind::WireDecodeOption {
decode_expr: "reader.ReadU8() == 0 ? (int?)null : reader.ReadI32()".to_string(),
},
"FfiBuf",
)]
fn native_return_type_reflects_ffi_buf_paths(
#[case] return_type: CSharpType,
#[case] return_kind: CSharpReturnKind,
#[case] expected: &str,
) {
assert_eq!(
function_with_return(return_type, return_kind).native_return_type(),
expected
);
}
#[test]
fn wire_decode_return_for_string_uses_read_string() {
let kind = CSharpReturnKind::WireDecodeString;
assert_eq!(
kind.wire_decode_return("_buf").as_deref(),
Some("return new WireReader(_buf).ReadString();"),
);
}
#[test]
fn wire_decode_return_for_object_calls_decode() {
let kind = CSharpReturnKind::WireDecodeObject {
class_name: "Person".to_string(),
};
assert_eq!(
kind.wire_decode_return("_buf").as_deref(),
Some("return Person.Decode(new WireReader(_buf));"),
);
}
#[test]
fn wire_decode_return_for_option_binds_reader_local() {
let kind = CSharpReturnKind::WireDecodeOption {
decode_expr: "reader.ReadU8() == 0 ? (int?)null : reader.ReadI32()".to_string(),
};
assert_eq!(
kind.wire_decode_return("_buf").as_deref(),
Some(
"var reader = new WireReader(_buf); return reader.ReadU8() == 0 ? (int?)null : reader.ReadI32();"
),
);
}
#[rstest]
#[case::void(CSharpReturnKind::Void)]
#[case::direct(CSharpReturnKind::Direct)]
fn wire_decode_return_none_for_non_wire_kinds(#[case] kind: CSharpReturnKind) {
assert_eq!(kind.wire_decode_return("_buf"), None);
}
#[test]
fn decode_class_name_some_only_for_wire_decode_object() {
assert_eq!(
CSharpReturnKind::WireDecodeObject {
class_name: "Point".to_string()
}
.decode_class_name(),
Some("Point"),
);
assert_eq!(CSharpReturnKind::WireDecodeString.decode_class_name(), None);
assert_eq!(CSharpReturnKind::Void.decode_class_name(), None);
assert_eq!(CSharpReturnKind::Direct.decode_class_name(), None);
}
mod from_read_op {
use super::*;
use crate::ir::codec::{EnumLayout, VecLayout};
use crate::ir::ids::{EnumId, RecordId};
use crate::ir::ops::{OffsetExpr, ReadOp, ReadSeq, SizeExpr, WireShape};
use boltffi_ffi_rules::transport::EnumTagStrategy;
fn seq(op: ReadOp) -> ReadSeq {
ReadSeq {
size: SizeExpr::Fixed(0),
ops: vec![op],
shape: WireShape::Value,
}
}
fn prim(p: PrimitiveType) -> ReadOp {
ReadOp::Primitive {
primitive: p,
offset: OffsetExpr::Base,
}
}
fn cstyle_layout() -> EnumLayout {
EnumLayout::CStyle {
tag_type: PrimitiveType::I32,
tag_strategy: EnumTagStrategy::Discriminant,
is_error: false,
}
}
fn data_layout() -> EnumLayout {
EnumLayout::Data {
tag_type: PrimitiveType::I32,
tag_strategy: EnumTagStrategy::Discriminant,
variants: vec![],
}
}
#[test]
fn primitive_maps_to_backing_type() {
assert_eq!(
CSharpType::from_read_op(&prim(PrimitiveType::I32)),
CSharpType::Int
);
assert_eq!(
CSharpType::from_read_op(&prim(PrimitiveType::F64)),
CSharpType::Double
);
}
#[test]
fn string_maps_to_string() {
let op = ReadOp::String {
offset: OffsetExpr::Base,
};
assert_eq!(CSharpType::from_read_op(&op), CSharpType::String);
}
#[test]
fn record_maps_to_record_with_class_name() {
let op = ReadOp::Record {
id: RecordId::new("point"),
offset: OffsetExpr::Base,
fields: vec![],
};
assert_eq!(
CSharpType::from_read_op(&op),
CSharpType::Record("Point".to_string())
);
}
#[test]
fn enum_cstyle_layout_maps_to_cstyle_enum() {
let op = ReadOp::Enum {
id: EnumId::new("status"),
offset: OffsetExpr::Base,
layout: cstyle_layout(),
};
assert_eq!(
CSharpType::from_read_op(&op),
CSharpType::CStyleEnum("Status".to_string())
);
}
#[test]
fn enum_data_layout_maps_to_data_enum() {
let op = ReadOp::Enum {
id: EnumId::new("shape"),
offset: OffsetExpr::Base,
layout: data_layout(),
};
assert_eq!(
CSharpType::from_read_op(&op),
CSharpType::DataEnum("Shape".to_string())
);
}
#[test]
fn option_wraps_inner_in_nullable() {
let op = ReadOp::Option {
tag_offset: OffsetExpr::Base,
some: Box::new(seq(prim(PrimitiveType::I32))),
};
assert_eq!(
CSharpType::from_read_op(&op),
CSharpType::Nullable(Box::new(CSharpType::Int))
);
}
#[test]
fn vec_wraps_element_type_in_array() {
let op = ReadOp::Vec {
len_offset: OffsetExpr::Base,
element_type: TypeExpr::Record(RecordId::new("point")),
element: Box::new(seq(ReadOp::Record {
id: RecordId::new("point"),
offset: OffsetExpr::Base,
fields: vec![],
})),
layout: VecLayout::Encoded,
};
assert_eq!(
CSharpType::from_read_op(&op),
CSharpType::Array(Box::new(CSharpType::Record("Point".to_string())))
);
}
#[test]
fn option_of_vec_of_record_nests_correctly() {
let inner_vec = ReadOp::Vec {
len_offset: OffsetExpr::Base,
element_type: TypeExpr::Record(RecordId::new("point")),
element: Box::new(seq(ReadOp::Record {
id: RecordId::new("point"),
offset: OffsetExpr::Base,
fields: vec![],
})),
layout: VecLayout::Encoded,
};
let option_op = ReadOp::Option {
tag_offset: OffsetExpr::Base,
some: Box::new(seq(inner_vec)),
};
assert_eq!(
CSharpType::from_read_op(&option_op),
CSharpType::Nullable(Box::new(CSharpType::Array(Box::new(CSharpType::Record(
"Point".to_string()
)))))
);
}
#[test]
fn qualify_if_shadowed_reaches_through_nested_builder_output() {
let option_op = ReadOp::Option {
tag_offset: OffsetExpr::Base,
some: Box::new(seq(ReadOp::Vec {
len_offset: OffsetExpr::Base,
element_type: TypeExpr::Record(RecordId::new("point")),
element: Box::new(seq(ReadOp::Record {
id: RecordId::new("point"),
offset: OffsetExpr::Base,
fields: vec![],
})),
layout: VecLayout::Encoded,
})),
};
let ty = CSharpType::from_read_op(&option_op);
let shadowed: std::collections::HashSet<String> =
std::iter::once("Point".to_string()).collect();
let qualified = ty.qualify_if_shadowed(&shadowed, "Demo");
assert_eq!(qualified.to_string(), "global::Demo.Point[]?");
}
}
mod from_type_expr {
use super::*;
use crate::ir::ids::{EnumId, RecordId};
#[test]
fn primitive_maps_to_backing_type() {
assert_eq!(
CSharpType::from_type_expr(&TypeExpr::Primitive(PrimitiveType::I32)),
CSharpType::Int
);
}
#[test]
fn string_maps_to_string() {
assert_eq!(
CSharpType::from_type_expr(&TypeExpr::String),
CSharpType::String
);
}
#[test]
fn record_maps_to_record_with_class_name() {
assert_eq!(
CSharpType::from_type_expr(&TypeExpr::Record(RecordId::new("point"))),
CSharpType::Record("Point".to_string())
);
}
#[test]
fn enum_maps_to_data_enum_by_convention() {
assert_eq!(
CSharpType::from_type_expr(&TypeExpr::Enum(EnumId::new("status"))),
CSharpType::DataEnum("Status".to_string())
);
}
#[test]
fn vec_wraps_element_in_array() {
let expr = TypeExpr::Vec(Box::new(TypeExpr::Primitive(PrimitiveType::F64)));
assert_eq!(
CSharpType::from_type_expr(&expr),
CSharpType::Array(Box::new(CSharpType::Double))
);
}
#[test]
fn option_wraps_inner_in_nullable() {
let expr = TypeExpr::Option(Box::new(TypeExpr::String));
assert_eq!(
CSharpType::from_type_expr(&expr),
CSharpType::Nullable(Box::new(CSharpType::String))
);
}
#[test]
fn option_of_vec_of_record_nests_correctly() {
let expr = TypeExpr::Option(Box::new(TypeExpr::Vec(Box::new(TypeExpr::Record(
RecordId::new("point"),
)))));
assert_eq!(
CSharpType::from_type_expr(&expr),
CSharpType::Nullable(Box::new(CSharpType::Array(Box::new(CSharpType::Record(
"Point".to_string()
)))))
);
}
}
mod for_enum {
use super::*;
use crate::ir::definitions::{CStyleVariant, DataVariant, VariantPayload};
use crate::ir::ids::EnumId;
fn enum_def(id: &str, repr: EnumRepr) -> EnumDef {
EnumDef {
id: EnumId::new(id),
repr,
is_error: false,
constructors: vec![],
methods: vec![],
doc: None,
deprecated: None,
}
}
#[test]
fn c_style_repr_maps_to_c_style_enum_type() {
let def = enum_def(
"Status",
EnumRepr::CStyle {
tag_type: PrimitiveType::I32,
variants: vec![CStyleVariant {
name: "Active".into(),
discriminant: 0,
doc: None,
}],
},
);
assert_eq!(
CSharpType::for_enum(&def),
CSharpType::CStyleEnum("Status".to_string())
);
}
#[test]
fn data_repr_maps_to_data_enum_type() {
let def = enum_def(
"Shape",
EnumRepr::Data {
tag_type: PrimitiveType::I32,
variants: vec![DataVariant {
name: "Point".into(),
discriminant: 0,
payload: VariantPayload::Unit,
doc: None,
}],
},
);
assert_eq!(
CSharpType::for_enum(&def),
CSharpType::DataEnum("Shape".to_string())
);
}
#[test]
fn class_name_round_trips_through_naming_convention() {
let def = enum_def(
"log_level",
EnumRepr::CStyle {
tag_type: PrimitiveType::I32,
variants: vec![],
},
);
assert_eq!(
CSharpType::for_enum(&def),
CSharpType::CStyleEnum("LogLevel".to_string())
);
}
}
mod enum_backing_for {
use super::*;
#[test]
fn maps_u8_to_byte() {
assert_eq!(
CSharpType::enum_backing_for(PrimitiveType::U8),
Some(CSharpType::Byte)
);
}
#[test]
fn rejects_usize() {
assert_eq!(CSharpType::enum_backing_for(PrimitiveType::USize), None);
}
}
}