use std::collections::HashSet;
use crate::ir::FfiContract;
use crate::ir::definitions::{EnumDef, EnumRepr, VariantPayload};
use crate::ir::ids::{EnumId, RecordId};
use crate::ir::types::TypeExpr;
use super::super::ast::CSharpEnumUnderlyingType;
use super::lowerer::CSharpLowerer;
impl<'a> CSharpLowerer<'a> {
pub(super) fn compute_supported_sets(
ffi: &FfiContract,
) -> (HashSet<RecordId>, HashSet<EnumId>) {
let mut enums: HashSet<EnumId> = ffi
.catalog
.all_enums()
.filter(|e| match &e.repr {
EnumRepr::CStyle { tag_type, .. } => {
CSharpEnumUnderlyingType::for_primitive(*tag_type).is_some()
}
EnumRepr::Data { .. } => false,
})
.map(|e| e.id.clone())
.collect();
let mut records: HashSet<RecordId> = HashSet::new();
loop {
let record_additions: Vec<RecordId> = ffi
.catalog
.all_records()
.filter(|r| !records.contains(&r.id))
.filter(|r| {
r.fields
.iter()
.all(|f| is_field_type_supported(ffi, &f.type_expr, &records, &enums))
})
.map(|r| r.id.clone())
.collect();
let enum_additions: Vec<EnumId> = ffi
.catalog
.all_enums()
.filter(|e| matches!(e.repr, EnumRepr::Data { .. }))
.filter(|e| !enums.contains(&e.id))
.filter(|e| enum_variant_fields_supported(ffi, e, &records, &enums))
.map(|e| e.id.clone())
.collect();
if record_additions.is_empty() && enum_additions.is_empty() {
break;
}
records.extend(record_additions);
enums.extend(enum_additions);
}
(records, enums)
}
}
fn enum_variant_fields_supported(
ffi: &FfiContract,
enum_def: &EnumDef,
records: &HashSet<RecordId>,
enums: &HashSet<EnumId>,
) -> bool {
let EnumRepr::Data { variants, .. } = &enum_def.repr else {
return true;
};
variants.iter().all(|v| match &v.payload {
VariantPayload::Unit => true,
VariantPayload::Tuple(types) => types
.iter()
.all(|t| is_field_type_supported(ffi, t, records, enums)),
VariantPayload::Struct(fields) => fields
.iter()
.all(|f| is_field_type_supported(ffi, &f.type_expr, records, enums)),
})
}
fn is_field_type_supported(
ffi: &FfiContract,
ty: &TypeExpr,
records: &HashSet<RecordId>,
enums: &HashSet<EnumId>,
) -> bool {
match ty {
TypeExpr::Primitive(_) | TypeExpr::String | TypeExpr::Void => true,
TypeExpr::Record(id) => records.contains(id),
TypeExpr::Enum(id) => enums.contains(id),
TypeExpr::Custom(id) => ffi
.catalog
.resolve_custom(id)
.is_some_and(|custom| is_field_type_supported(ffi, &custom.repr, records, enums)),
TypeExpr::Vec(inner) => is_field_type_supported(ffi, inner, records, enums),
TypeExpr::Option(inner) => {
!matches!(inner.as_ref(), TypeExpr::Option(_))
&& is_field_type_supported(ffi, inner, records, enums)
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::super::test_support::{data_enum, record_with_one_field, struct_variant};
use super::*;
use crate::ir::Lowerer as IrLowerer;
use crate::ir::contract::PackageInfo;
use crate::ir::definitions::{
CStyleVariant, CustomTypeDef, FunctionDef, ParamDef, ParamPassing, ReturnDef,
};
use crate::ir::ids::{ConverterPath, CustomTypeId, FunctionId, ParamName, QualifiedName};
use crate::ir::types::PrimitiveType;
use boltffi_ffi_rules::callable::ExecutionKind;
use super::super::super::CSharpOptions;
fn datetime_custom_type() -> CustomTypeDef {
CustomTypeDef {
id: CustomTypeId::new("UtcDateTime"),
rust_type: QualifiedName::new("chrono::DateTime<Utc>"),
repr: TypeExpr::Primitive(PrimitiveType::I64),
converters: ConverterPath {
into_ffi: QualifiedName::new("test_into_ffi"),
try_from_ffi: QualifiedName::new("test_try_from_ffi"),
},
doc: None,
}
}
#[test]
fn record_referencing_data_enum_is_admitted_jointly() {
let mut contract = FfiContract {
package: PackageInfo {
name: "demo_lib".to_string(),
version: None,
},
functions: vec![],
catalog: Default::default(),
};
contract.catalog.insert_enum(data_enum(
"shape",
vec![struct_variant(
"Circle",
0,
vec![("radius", TypeExpr::Primitive(PrimitiveType::F64))],
)],
));
contract.catalog.insert_record(record_with_one_field(
"holder",
"shape",
TypeExpr::Enum(EnumId::new("shape")),
));
let (records, enums) = CSharpLowerer::compute_supported_sets(&contract);
assert!(
enums.contains(&EnumId::new("shape")),
"expecting the data enum to be admitted first so the record can reference it",
);
assert!(
records.contains(&RecordId::new("holder")),
"expecting the record with a data-enum field to be admitted once the enum joins the set",
);
}
#[test]
fn data_enum_referencing_another_data_enum_is_admitted() {
let mut contract = FfiContract {
package: PackageInfo {
name: "demo_lib".to_string(),
version: None,
},
functions: vec![],
catalog: Default::default(),
};
contract.catalog.insert_enum(data_enum(
"inner",
vec![struct_variant(
"Value",
0,
vec![("n", TypeExpr::Primitive(PrimitiveType::I32))],
)],
));
contract.catalog.insert_enum(data_enum(
"outer",
vec![struct_variant(
"Wrap",
0,
vec![("inner", TypeExpr::Enum(EnumId::new("inner")))],
)],
));
let (_records, enums) = CSharpLowerer::compute_supported_sets(&contract);
assert!(
enums.contains(&EnumId::new("inner")),
"expecting the leaf data enum to be admitted",
);
assert!(
enums.contains(&EnumId::new("outer")),
"expecting the data enum referencing another data enum to join on a later fixed-point iteration",
);
}
#[test]
fn c_style_enum_with_usize_repr_is_not_admitted() {
let mut contract = FfiContract {
package: PackageInfo {
name: "demo_lib".to_string(),
version: None,
},
functions: vec![],
catalog: Default::default(),
};
contract.catalog.insert_enum(EnumDef {
id: EnumId::new("platform_status"),
repr: EnumRepr::CStyle {
tag_type: PrimitiveType::USize,
variants: vec![CStyleVariant {
name: "Ready".into(),
discriminant: 0,
doc: None,
}],
},
is_error: false,
constructors: vec![],
methods: vec![],
doc: None,
deprecated: None,
});
let (_records, enums) = CSharpLowerer::compute_supported_sets(&contract);
assert!(
!enums.contains(&EnumId::new("platform_status")),
"expecting repr(usize) C-style enums to stay unsupported until the backend has a legal C# projection",
);
}
#[test]
fn nested_option_shapes_are_rejected() {
let mut contract = FfiContract {
package: PackageInfo {
name: "demo_lib".to_string(),
version: None,
},
functions: vec![],
catalog: Default::default(),
};
let nested_option = TypeExpr::Option(Box::new(TypeExpr::Option(Box::new(
TypeExpr::Primitive(PrimitiveType::I32),
))));
contract.catalog.insert_record(record_with_one_field(
"holder",
"value",
nested_option.clone(),
));
contract.functions.push(FunctionDef {
id: FunctionId::new("echo_nested_option"),
params: vec![ParamDef {
name: ParamName::new("value"),
type_expr: nested_option.clone(),
passing: ParamPassing::Value,
doc: None,
}],
returns: ReturnDef::Value(nested_option.clone()),
execution_kind: ExecutionKind::Sync,
doc: None,
deprecated: None,
});
let abi = IrLowerer::new(&contract).to_abi_contract();
let options = CSharpOptions::default();
let lowerer = CSharpLowerer::new(&contract, &abi, &options);
let (records, _enums) = CSharpLowerer::compute_supported_sets(&contract);
assert!(
!records.contains(&RecordId::new("holder")),
"expecting a record with Option<Option<i32>> field to stay unsupported because it would render as int??",
);
assert!(
!lowerer.is_supported_type(&nested_option),
"expecting Option<Option<i32>> to fail the C# support gate before lowering",
);
assert!(
lowerer.lower_function(&contract.functions[0]).is_none(),
"expecting a function with nested Option param/return to be dropped rather than emitting int??",
);
}
#[test]
fn record_with_custom_field_is_admitted() {
let mut contract = FfiContract {
package: PackageInfo {
name: "demo_lib".to_string(),
version: None,
},
functions: vec![],
catalog: Default::default(),
};
contract.catalog.insert_custom(datetime_custom_type());
contract.catalog.insert_record(record_with_one_field(
"event",
"timestamp",
TypeExpr::Custom(CustomTypeId::new("UtcDateTime")),
));
let (records, _enums) = CSharpLowerer::compute_supported_sets(&contract);
assert!(
records.contains(&RecordId::new("event")),
"expecting a record with a Custom<i64> field to be admitted",
);
}
#[test]
fn blittable_vec_element_resolves_through_custom() {
let mut contract = FfiContract {
package: PackageInfo {
name: "demo_lib".to_string(),
version: None,
},
functions: vec![],
catalog: Default::default(),
};
contract.catalog.insert_custom(datetime_custom_type());
let abi = IrLowerer::new(&contract).to_abi_contract();
let options = CSharpOptions::default();
let lowerer = CSharpLowerer::new(&contract, &abi, &options);
let custom = TypeExpr::Custom(CustomTypeId::new("UtcDateTime"));
assert!(
lowerer.is_blittable_vec_element(&custom),
"expecting Vec<Custom<i64>> to qualify for the pinned-array fast path",
);
assert!(
lowerer.is_supported_type(&custom),
"expecting bare Custom<i64> to admit as a param/return type",
);
assert!(
lowerer.is_supported_vec_element(&custom),
"expecting Custom<i64> to admit as a Vec element",
);
}
}