use boltffi_ffi_rules::naming::{
self, snake_to_camel as camel_case, to_upper_camel_case as pascal_case,
};
use heck::ToLowerCamelCase;
use std::collections::HashMap;
use super::emit;
use super::plan::{
CompositeFieldMapping, DirectBufferCompositeMapping, SwiftAsyncConversion, SwiftAsyncResult,
SwiftCallMode, SwiftCallback, SwiftCallbackMethod, SwiftCallbackParam, SwiftClass,
SwiftClosureTrampoline, SwiftClosureTrampolineParam, SwiftConstructor, SwiftConversion,
SwiftCustomType, SwiftEnum, SwiftEnumStyle, SwiftField, SwiftFunction, SwiftMethod,
SwiftModule, SwiftNativeConversion, SwiftNativeMapping, SwiftParam, SwiftRecord, SwiftReturn,
SwiftStream, SwiftStreamMode, SwiftVariant, SwiftVariantPayload,
};
use crate::ir::abi::{
AbiCall, AbiCallbackInvocation, AbiContract, AbiEnum, AbiEnumField, AbiEnumPayload,
AbiEnumVariant, AbiParam, AbiRecord, AbiStream, CallId, CallMode, ErrorTransport, ParamRole,
ReturnShape, StreamItemTransport,
};
use crate::ir::codec::CodecPlan;
use crate::ir::contract::FfiContract;
use crate::ir::definitions::{
CallbackKind, ConstructorDef, DefaultValue, EnumRepr, ParamDef, Receiver, ReturnDef, StreamDef,
StreamMode,
};
use crate::ir::ids::{CallbackId, ClassId, EnumId, FieldName, ParamName, RecordId};
use crate::ir::ops::{
FieldReadOp, OffsetExpr, ReadOp, ReadSeq, SizeExpr, ValueExpr, WireShape, WriteOp, WriteSeq,
remap_root_in_seq,
};
use crate::ir::plan::{
AbiType, CallbackStyle, CompositeLayout, Mutability, ScalarOrigin, SpanContent, Transport,
};
use crate::ir::types::{PrimitiveType, TypeExpr};
use crate::render::{TypeConversion, TypeMappings};
struct AbiIndex {
calls: HashMap<CallId, usize>,
callbacks: HashMap<CallbackId, usize>,
records: HashMap<RecordId, usize>,
enums: HashMap<EnumId, usize>,
}
impl AbiIndex {
fn new(contract: &AbiContract) -> Self {
let calls = contract
.calls
.iter()
.enumerate()
.map(|(index, call)| (call.id.clone(), index))
.collect();
let callbacks = contract
.callbacks
.iter()
.enumerate()
.map(|(index, callback)| (callback.callback_id.clone(), index))
.collect();
let records = contract
.records
.iter()
.enumerate()
.map(|(index, record)| (record.id.clone(), index))
.collect();
let enums = contract
.enums
.iter()
.enumerate()
.map(|(index, enumeration)| (enumeration.id.clone(), index))
.collect();
Self {
calls,
callbacks,
records,
enums,
}
}
fn call<'a>(&self, contract: &'a AbiContract, id: &CallId) -> &'a AbiCall {
let index = self.calls.get(id).expect("abi call should exist");
&contract.calls[*index]
}
fn callback<'a>(
&self,
contract: &'a AbiContract,
id: &CallbackId,
) -> &'a AbiCallbackInvocation {
let index = self.callbacks.get(id).expect("abi callback should exist");
&contract.callbacks[*index]
}
fn record<'a>(&self, contract: &'a AbiContract, id: &RecordId) -> &'a AbiRecord {
let index = self.records.get(id).expect("record should exist");
&contract.records[*index]
}
fn enumeration<'a>(&self, contract: &'a AbiContract, id: &EnumId) -> &'a AbiEnum {
let index = self.enums.get(id).expect("enum should exist");
&contract.enums[*index]
}
}
pub struct SwiftLowerer<'a> {
contract: &'a FfiContract,
abi: &'a AbiContract,
abi_index: AbiIndex,
type_mappings: TypeMappings,
}
impl<'a> SwiftLowerer<'a> {
pub fn new(contract: &'a FfiContract, abi: &'a AbiContract) -> Self {
Self {
contract,
abi,
abi_index: AbiIndex::new(abi),
type_mappings: TypeMappings::new(),
}
}
pub fn with_type_mappings(mut self, mappings: TypeMappings) -> Self {
self.type_mappings = mappings;
self
}
pub fn lower(self) -> SwiftModule {
let custom_types = self.lower_custom_types();
let records = self.lower_records();
let enums = self.lower_enums();
let classes = self.lower_classes();
let callbacks = self.lower_callbacks();
let functions = self.lower_functions();
SwiftModule {
imports: vec!["Foundation".to_string()],
custom_types,
records,
enums,
classes,
callbacks,
functions,
}
}
fn resolve_swift_type(&self, type_expr: &TypeExpr) -> String {
match type_expr {
TypeExpr::Custom(id) => {
if let Some(mapping) = self.type_mappings.get(id.as_str()) {
mapping.native_type.clone()
} else {
pascal_case(id.as_str())
}
}
TypeExpr::Option(inner) => format!("{}?", self.resolve_swift_type(inner)),
TypeExpr::Vec(inner) => format!("[{}]", self.resolve_swift_type(inner)),
TypeExpr::Result { ok, err } => {
format!(
"Result<{}, {}>",
self.resolve_swift_type(ok),
self.resolve_swift_type(err)
)
}
_ => emit::swift_type(type_expr),
}
}
}
impl<'a> SwiftLowerer<'a> {
fn lower_custom_types(&self) -> Vec<SwiftCustomType> {
self.contract
.catalog
.all_custom_types()
.map(|def| {
let alias_name = pascal_case(def.id.as_str());
let target_type = emit::swift_type(&def.repr);
let native_mapping = self
.type_mappings
.get(def.id.as_str())
.map(|mapping| self.build_native_mapping(mapping, &target_type));
SwiftCustomType {
alias_name,
target_type,
native_mapping,
}
})
.collect()
}
fn build_native_mapping(
&self,
mapping: &crate::render::TypeMapping,
_repr_type: &str,
) -> SwiftNativeMapping {
let (decode_expr, encode_expr) = match mapping.conversion {
TypeConversion::UuidString => (
"UUID(uuidString: $0)!".to_string(),
"$0.uuidString".to_string(),
),
TypeConversion::UrlString => (
"URL(string: $0)!".to_string(),
"$0.absoluteString".to_string(),
),
};
SwiftNativeMapping {
native_type: mapping.native_type.clone(),
decode_expr,
encode_expr,
}
}
fn native_conversion_for_type(&self, type_expr: &TypeExpr) -> Option<SwiftNativeConversion> {
match type_expr {
TypeExpr::Custom(id) => self.type_mappings.get(id.as_str()).map(|mapping| {
let (decode_wrapper, encode_wrapper) = match mapping.conversion {
TypeConversion::UuidString => (
"UUID(uuidString: $0)!".to_string(),
"$0.uuidString".to_string(),
),
TypeConversion::UrlString => (
"URL(string: $0)!".to_string(),
"$0.absoluteString".to_string(),
),
};
SwiftNativeConversion {
decode_wrapper,
encode_wrapper,
}
}),
_ => None,
}
}
}
impl<'a> SwiftLowerer<'a> {
fn lower_records(&self) -> Vec<SwiftRecord> {
self.contract
.catalog
.all_records()
.map(|def| {
let abi_record = self.abi_index.record(self.abi, &def.id);
let decode_fields = self.record_decode_fields(abi_record);
let encode_fields = self.record_encode_fields(abi_record);
let fields =
def.fields
.iter()
.map(|field| {
let swift_name = camel_case(field.name.as_str());
let decode =
decode_fields.get(&field.name).cloned().unwrap_or_else(|| {
ReadSeq {
size: SizeExpr::Fixed(0),
ops: vec![],
shape: WireShape::Value,
}
});
let encode =
encode_fields.get(&field.name).cloned().unwrap_or_else(|| {
WriteSeq {
size: SizeExpr::Fixed(0),
ops: vec![],
shape: WireShape::Value,
}
});
let c_offset = if abi_record.is_blittable {
decode_fields
.get(&field.name)
.and_then(|seq| self.record_field_offset(seq))
} else {
None
};
let native_conversion =
self.native_conversion_for_type(&field.type_expr);
SwiftField {
swift_name,
swift_type: self.swift_type(&field.type_expr),
default_expr: field.default.as_ref().map(swift_default_literal),
decode,
encode,
doc: field.doc.clone(),
c_offset,
native_conversion,
}
})
.collect();
SwiftRecord {
class_name: self.swift_name_for_record(&def.id),
fields,
is_blittable: abi_record.is_blittable,
blittable_size: abi_record.size,
doc: def.doc.clone(),
}
})
.collect()
}
}
impl<'a> SwiftLowerer<'a> {
fn lower_enums(&self) -> Vec<SwiftEnum> {
self.contract
.catalog
.all_enums()
.map(|def| {
let abi_enum = self.abi_index.enumeration(self.abi, &def.id);
let variant_docs = def.variant_docs();
let variants = abi_enum
.variants
.iter()
.enumerate()
.map(|(i, variant)| SwiftVariant {
swift_name: emit::escape_swift_keyword(
&variant.name.as_str().to_lower_camel_case(),
),
discriminant: variant.discriminant,
payload: self.lower_variant_payload(variant),
doc: variant_docs.get(i).cloned().flatten(),
})
.collect();
let style = if abi_enum.is_c_style {
SwiftEnumStyle::CStyle
} else {
SwiftEnumStyle::Data
};
SwiftEnum {
name: self.swift_name_for_enum(&def.id),
variants,
style,
c_style_tag_type: match &def.repr {
EnumRepr::CStyle { tag_type, .. } => Some(*tag_type),
_ => None,
},
is_error: def.is_error,
doc: def.doc.clone(),
}
})
.collect()
}
fn lower_variant_payload(&self, variant: &AbiEnumVariant) -> SwiftVariantPayload {
match &variant.payload {
AbiEnumPayload::Unit => SwiftVariantPayload::Unit,
AbiEnumPayload::Tuple(fields) => SwiftVariantPayload::Tuple(
fields
.iter()
.map(|field| {
let lowered = self.lower_enum_field(field);
let encode =
remap_root_in_seq(&lowered.encode, ValueExpr::Var("value".into()));
SwiftField { encode, ..lowered }
})
.collect(),
),
AbiEnumPayload::Struct(fields) => SwiftVariantPayload::Struct(
fields
.iter()
.map(|field| {
let lowered = self.lower_enum_field(field);
if lowered.swift_name.chars().all(|c| c.is_ascii_digit()) {
let encode =
remap_root_in_seq(&lowered.encode, ValueExpr::Var("value".into()));
SwiftField { encode, ..lowered }
} else {
lowered
}
})
.collect(),
),
}
}
fn lower_enum_field(&self, field: &AbiEnumField) -> SwiftField {
let swift_name = camel_case(field.name.as_str());
let encode = field.encode.clone();
let native_conversion = self.native_conversion_for_type(&field.type_expr);
SwiftField {
swift_name,
swift_type: self.swift_type(&field.type_expr),
default_expr: None,
decode: field.decode.clone(),
encode,
doc: None,
c_offset: None,
native_conversion,
}
}
}
impl<'a> SwiftLowerer<'a> {
fn lower_classes(&self) -> Vec<SwiftClass> {
self.contract
.catalog
.all_classes()
.map(|def| {
let class_name = self.swift_name_for_class(&def.id);
let ffi_free = naming::class_ffi_free(def.id.as_str()).to_string();
let constructors = def
.constructors
.iter()
.enumerate()
.map(|(idx, ctor)| {
let call = self.abi_call(&CallId::Constructor {
class_id: def.id.clone(),
index: idx,
});
match ctor {
ConstructorDef::Default {
is_fallible, doc, ..
} => SwiftConstructor::Designated {
ffi_symbol: call.symbol.as_str().to_string(),
params: ctor
.params()
.into_iter()
.map(|p| self.lower_param(p, call))
.collect(),
is_fallible: *is_fallible,
doc: doc.clone(),
},
ConstructorDef::NamedFactory {
name,
is_fallible,
doc,
..
} => SwiftConstructor::Factory {
name: camel_case(name.as_str()),
ffi_symbol: call.symbol.as_str().to_string(),
is_fallible: *is_fallible,
doc: doc.clone(),
},
ConstructorDef::NamedInit {
name,
first_param,
rest_params,
is_fallible,
doc,
..
} => {
let label = camel_case(name.as_str());
let mut first = self.lower_param(first_param, call);
first.label = Some(label.clone());
let rest = rest_params.iter().map(|p| self.lower_param(p, call));
SwiftConstructor::Convenience {
name: label,
ffi_symbol: call.symbol.as_str().to_string(),
params: std::iter::once(first).chain(rest).collect(),
is_fallible: *is_fallible,
doc: doc.clone(),
}
}
}
})
.collect();
let methods =
def.methods
.iter()
.map(|method| {
let call = self.abi_call(&CallId::Method {
class_id: def.id.clone(),
method_id: method.id.clone(),
});
let mode = self.lower_call_mode(call, &method.returns);
let returns = match &call.mode {
CallMode::Async(async_call) => self
.lower_return_def_for_async(&async_call.error, &method.returns),
CallMode::Sync => self.swift_return_from_abi(
&call.returns,
&call.error,
&method.returns,
),
};
SwiftMethod {
name: camel_case(method.id.as_str()),
mode,
params: method
.params
.iter()
.map(|p| self.lower_param(p, call))
.collect(),
returns,
is_static: method.receiver == Receiver::Static,
doc: method.doc.clone(),
}
})
.collect();
let streams = def
.streams
.iter()
.map(|stream_def| {
let abi_stream = self
.abi
.streams
.iter()
.find(|stream| {
stream.class_id == def.id && stream.stream_id == stream_def.id
})
.expect("abi stream");
self.lower_stream(stream_def, abi_stream, &class_name)
})
.collect();
SwiftClass {
name: class_name,
ffi_free,
constructors,
methods,
streams,
doc: def.doc.clone(),
}
})
.collect()
}
fn lower_stream(
&self,
stream_def: &StreamDef,
stream: &AbiStream,
class_name: &str,
) -> SwiftStream {
let StreamItemTransport::WireEncoded { decode_ops } = &stream.item;
let method_name_pascal = pascal_case(stream.stream_id.as_str());
let mode = match stream.mode {
StreamMode::Async => SwiftStreamMode::Async,
StreamMode::Batch => SwiftStreamMode::Batch {
class_name: class_name.to_string(),
method_name_pascal: method_name_pascal.clone(),
},
StreamMode::Callback => SwiftStreamMode::Callback {
class_name: class_name.to_string(),
method_name_pascal: method_name_pascal.clone(),
},
};
SwiftStream {
name: camel_case(stream.stream_id.as_str()),
mode,
item_type: self.swift_type(&stream_def.item_type),
item_decode: self.rebase_read_seq(decode_ops, "pos", "0"),
subscribe: stream.subscribe.to_string(),
poll: stream.poll.to_string(),
pop_batch: stream.pop_batch.to_string(),
wait: stream.wait.to_string(),
unsubscribe: stream.unsubscribe.to_string(),
free: stream.free.to_string(),
free_buf: self.abi.free_buf.to_string(),
atomic_cas: self.abi.atomic_cas.to_string(),
}
}
}
impl<'a> SwiftLowerer<'a> {
fn lower_callbacks(&self) -> Vec<SwiftCallback> {
self.contract
.catalog
.all_callbacks()
.filter(|def| def.kind == CallbackKind::Trait)
.map(|def| {
let protocol_name = pascal_case(def.id.as_str());
let vtable_var = format!("{}VTableInstance", lower_first_char(&protocol_name));
let plan = self.abi_index.callback(self.abi, &def.id);
let methods = def
.methods
.iter()
.map(|method_def| {
let abi_method = plan
.methods
.iter()
.find(|m| m.id == method_def.id)
.expect("callback method");
let returns = self.rebase_return_encode(
self.swift_return_from_abi(
&abi_method.returns,
&abi_method.error,
&method_def.returns,
),
"result",
);
let has_out_param = !abi_method.is_async && !returns.is_void();
let param_map = method_def
.params
.iter()
.map(|param| (param.name.clone(), param))
.collect::<HashMap<_, _>>();
let params = abi_method
.params
.iter()
.filter(|param| matches!(
param.role,
ParamRole::Input {
transport: Transport::Scalar(_) | Transport::Span(SpanContent::Encoded(_)),
..
}
))
.map(|param| {
let def = param_map.get(¶m.name).unwrap_or_else(|| {
unreachable!(
"param def not found: callback={}, method={}, param={}, role={:?}",
plan.callback_id.as_str(),
abi_method.id.as_str(),
param.name.as_str(),
param.role,
)
});
self.lower_callback_param(def, param)
})
.collect();
SwiftCallbackMethod {
swift_name: camel_case(abi_method.id.as_str()),
ffi_name: abi_method.vtable_field.as_str().to_string(),
params,
returns,
is_async: abi_method.is_async,
has_out_param,
doc: method_def.doc.clone(),
}
})
.collect();
SwiftCallback {
protocol_name: protocol_name.clone(),
wrapper_class: format!("{}Wrapper", protocol_name),
vtable_var,
vtable_type: plan.vtable_type.as_str().to_string(),
bridge_name: format!("{}Bridge", protocol_name),
register_fn: plan.register_fn.as_str().to_string(),
create_fn: plan.create_fn.as_str().to_string(),
methods,
doc: def.doc.clone(),
}
})
.collect()
}
fn lower_callback_param(&self, def: &ParamDef, param: &AbiParam) -> SwiftCallbackParam {
let label = camel_case(param.name.as_str());
let (swift_type, ffi_args, decode_prelude) = match ¶m.role {
ParamRole::Input {
transport: Transport::Scalar(_),
..
} => (self.swift_type(&def.type_expr), vec![label.clone()], None),
ParamRole::Input {
transport: Transport::Span(SpanContent::Encoded(_)),
decode_ops: Some(decode_ops),
..
} => {
let len_name = format!("{}Len", label);
let reader_decode = emit::emit_reader_read(decode_ops);
(
self.swift_type(&def.type_expr),
vec![label.clone(), len_name.clone()],
Some(format!(
"let {} = {{ var reader = WireReader(ptr: {}!, len: Int({})); return {} }}()",
label, label, len_name, reader_decode
)),
)
}
_ => unreachable!(
"unsupported ABI param role for Swift callback: {:?}",
param.role
),
};
SwiftCallbackParam {
label: label.clone(),
swift_type,
call_arg: label,
ffi_args,
decode_prelude,
}
}
}
impl<'a> SwiftLowerer<'a> {
fn lower_functions(&self) -> Vec<SwiftFunction> {
self.contract
.functions
.iter()
.map(|def| {
let call = self.abi_call(&CallId::Function(def.id.clone()));
let mode = self.lower_call_mode(call, &def.returns);
let returns = match &call.mode {
CallMode::Async(async_call) => {
self.lower_return_def_for_async(&async_call.error, &def.returns)
}
CallMode::Sync => {
self.swift_return_from_abi(&call.returns, &call.error, &def.returns)
}
};
SwiftFunction {
name: camel_case(def.id.as_str()),
mode,
params: def
.params
.iter()
.map(|p| self.lower_param(p, call))
.collect(),
returns,
doc: def.doc.clone(),
}
})
.collect()
}
}
impl<'a> SwiftLowerer<'a> {
fn lower_param(&self, param: &ParamDef, call: &AbiCall) -> SwiftParam {
let abi_param = self.abi_param_for_semantic(call, ¶m.name);
let swift_name = camel_case(param.name.as_str());
let (swift_type, conversion) = match &abi_param.role {
ParamRole::Input {
transport: Transport::Scalar(origin),
..
} => match origin {
ScalarOrigin::CStyleEnum { enum_id, .. } => {
let swift_enum = self.swift_name_for_enum(enum_id);
(swift_enum, SwiftConversion::CStyleEnumRawValue)
}
ScalarOrigin::Primitive(p) => (
self.abi_to_swift(&AbiType::from(*p)),
SwiftConversion::Direct,
),
},
ParamRole::Input {
transport: Transport::Span(SpanContent::Scalar(origin)),
mutability,
..
} => {
let primitive = origin.primitive();
let element_abi = AbiType::from(primitive);
let element_type = self.abi_to_swift(&element_abi);
if element_abi == AbiType::U8 && *mutability == Mutability::Shared {
("Data".to_string(), SwiftConversion::ToData)
} else if let ScalarOrigin::CStyleEnum { enum_id, .. } = origin {
let swift_enum = self.swift_name_for_enum(enum_id);
(
format!("[{}]", swift_enum),
SwiftConversion::EnumBufferInput {
swift_enum: swift_enum.clone(),
element_type: element_type.clone(),
},
)
} else {
let conversion = match mutability {
Mutability::Mutable => SwiftConversion::MutableBuffer {
element_type: element_type.clone(),
},
Mutability::Shared => SwiftConversion::PrimitiveBuffer {
element_type: element_type.clone(),
},
};
(format!("[{}]", element_type), conversion)
}
}
ParamRole::Input {
transport: Transport::Span(SpanContent::Utf8),
..
} => ("String".to_string(), SwiftConversion::ToString),
ParamRole::Input {
transport: Transport::Span(SpanContent::Encoded(codec)),
encode_ops: Some(encode_ops),
..
} => (
self.swift_type_from_codec(codec),
SwiftConversion::ToWireBuffer {
encode: encode_ops.clone(),
},
),
ParamRole::Input {
transport: Transport::Span(_),
mutability: Mutability::Mutable,
..
} => {
let element_type = self.buffer_element_swift_type(¶m.type_expr);
(
format!("[{}]", element_type),
SwiftConversion::MutableBuffer {
element_type: element_type.clone(),
},
)
}
ParamRole::Input {
transport: Transport::Handle { class_id, nullable },
..
} => {
let class_name = self.swift_name_for_class(class_id);
let swift_type = if *nullable {
format!("{}?", class_name)
} else {
class_name.clone()
};
(
swift_type,
SwiftConversion::PassHandle {
class_name,
nullable: *nullable,
},
)
}
ParamRole::Input {
transport:
Transport::Callback {
callback_id,
nullable,
style,
},
..
} => match style {
CallbackStyle::BoxedDyn => {
let protocol = pascal_case(callback_id.as_str());
let swift_type = if *nullable {
format!("(any {})?", protocol)
} else {
format!("any {}", protocol)
};
(
swift_type,
SwiftConversion::WrapCallback {
protocol,
nullable: *nullable,
},
)
}
CallbackStyle::ImplTrait => {
let closure_plan = self.build_closure_trampoline(callback_id, &swift_name);
let swift_type = format!("@escaping {}", closure_plan.swift_type);
(
swift_type,
SwiftConversion::InlineClosure {
closure: closure_plan,
},
)
}
},
ParamRole::Input {
transport: Transport::Composite(layout),
..
} => {
let c_type = format!("___{}", layout.record_id.as_str());
let fields = self.composite_field_mappings(layout);
(
self.swift_name_for_record(&layout.record_id),
SwiftConversion::ToComposite { c_type, fields },
)
}
_ => unreachable!("unsupported param role for Swift: {:?}", abi_param.role),
};
SwiftParam {
label: None,
name: swift_name,
swift_type,
conversion,
}
}
fn swift_type_from_codec(&self, codec: &CodecPlan) -> String {
match codec {
CodecPlan::Record { id, .. } => self.swift_name_for_record(id),
CodecPlan::Enum { id, .. } => self.swift_name_for_enum(id),
CodecPlan::Vec { element, .. } => format!("[{}]", self.swift_type_from_codec(element)),
CodecPlan::Option(inner) => format!("{}?", self.swift_type_from_codec(inner)),
CodecPlan::Result { ok, err } => format!(
"Result<{}, {}>",
self.swift_type_from_codec(ok),
self.swift_type_from_codec(err)
),
CodecPlan::String => "String".to_string(),
CodecPlan::Bytes => "Data".to_string(),
CodecPlan::Primitive(p) => self.abi_to_swift(&AbiType::from(*p)),
CodecPlan::Void => "Void".to_string(),
CodecPlan::Builtin(id) => pascal_case(id.as_str()),
CodecPlan::Custom { id, .. } => self
.type_mappings
.get(id.as_str())
.map(|m| m.native_type.clone())
.unwrap_or_else(|| pascal_case(id.as_str())),
}
}
fn abi_param_for_semantic<'b>(&self, call: &'b AbiCall, name: &ParamName) -> &'b AbiParam {
call.params
.iter()
.find(|param| param.name.as_str() == name.as_str() && param.transport().is_some())
.expect("ABI param should exist")
}
fn composite_field_mappings(&self, layout: &CompositeLayout) -> Vec<CompositeFieldMapping> {
layout
.fields
.iter()
.map(|field| CompositeFieldMapping {
swift_name: camel_case(field.name.as_str()),
c_name: field.name.as_str().to_string(),
})
.collect()
}
}
impl<'a> SwiftLowerer<'a> {
fn swift_return_from_abi(
&self,
return_shape: &ReturnShape,
error: &ErrorTransport,
returns: &ReturnDef,
) -> SwiftReturn {
let base = match &return_shape.transport {
None => SwiftReturn::Void,
Some(Transport::Scalar(origin)) => match origin {
ScalarOrigin::CStyleEnum { enum_id, .. } => SwiftReturn::CStyleEnumFromRawValue {
swift_type: self.swift_name_for_enum(enum_id),
},
ScalarOrigin::Primitive(_) => SwiftReturn::Direct {
swift_type: self.swift_return_value_type(returns),
},
},
Some(Transport::Composite(layout)) => {
let c_type = format!("___{}", layout.record_id.as_str());
let fields = self.composite_field_mappings(layout);
SwiftReturn::FromComposite {
swift_type: self.swift_return_value_type(returns),
c_type,
fields,
}
}
Some(Transport::Span(SpanContent::Scalar(origin))) => {
let primitive = origin.primitive();
let element_swift_type = self.abi_to_swift(&AbiType::from(primitive));
let enum_mapping = match origin {
ScalarOrigin::CStyleEnum { enum_id, .. } => {
Some(self.swift_name_for_enum(enum_id))
}
ScalarOrigin::Primitive(_) => None,
};
SwiftReturn::FromDirectBuffer {
swift_type: self.swift_return_value_type(returns),
element_swift_type,
composite_mapping: None,
enum_mapping,
}
}
Some(Transport::Span(SpanContent::Composite(layout))) => {
let c_struct = format!("___{}", layout.record_id.as_str());
let swift_record = self.swift_name_for_record(&layout.record_id);
let fields = self.composite_field_mappings(layout);
SwiftReturn::FromDirectBuffer {
swift_type: self.swift_return_value_type(returns),
element_swift_type: c_struct,
composite_mapping: Some(DirectBufferCompositeMapping {
swift_record_type: swift_record,
fields,
}),
enum_mapping: None,
}
}
Some(Transport::Span(_)) => SwiftReturn::FromWireBuffer {
swift_type: self.swift_return_value_type(returns),
decode: return_shape.decode_ops.clone().unwrap_or_else(|| ReadSeq {
size: SizeExpr::Fixed(0),
ops: vec![],
shape: WireShape::Value,
}),
encode: return_shape.encode_ops.clone().unwrap_or_else(|| WriteSeq {
size: SizeExpr::Fixed(0),
ops: vec![],
shape: WireShape::Value,
}),
},
Some(Transport::Handle { class_id, nullable }) => {
let class_name = self.swift_name_for_class(class_id);
SwiftReturn::Handle {
class_name,
nullable: *nullable,
}
}
Some(Transport::Callback {
callback_id,
nullable,
..
}) => {
let protocol = pascal_case(callback_id.as_str());
let swift_type = if *nullable {
format!("(any {})?", protocol)
} else {
format!("any {}", protocol)
};
SwiftReturn::Direct { swift_type }
}
};
match error {
ErrorTransport::None => base,
ErrorTransport::Encoded {
decode_ops,
encode_ops,
} => {
let result_decode = return_shape.decode_ops.clone().unwrap_or_else(|| ReadSeq {
size: SizeExpr::Fixed(0),
ops: vec![],
shape: WireShape::Value,
});
let ok_variant = if self.is_c_style_enum_return(returns) {
SwiftReturn::CStyleEnumFromRawValue {
swift_type: self.swift_return_value_type(returns),
}
} else {
base
};
SwiftReturn::Throws {
ok: Box::new(ok_variant),
err_type: self.swift_error_type(returns),
result_decode,
err_decode: decode_ops.clone(),
err_is_string: self.error_is_string(returns),
err_encode: encode_ops.clone(),
}
}
ErrorTransport::StatusCode => SwiftReturn::Throws {
ok: Box::new(base),
err_type: "FfiError".to_string(),
result_decode: ReadSeq {
size: SizeExpr::Fixed(0),
ops: vec![],
shape: WireShape::Value,
},
err_decode: ReadSeq {
size: SizeExpr::Fixed(0),
ops: vec![],
shape: WireShape::Value,
},
err_is_string: false,
err_encode: None,
},
}
}
}
impl<'a> SwiftLowerer<'a> {
fn abi_call(&self, id: &CallId) -> &AbiCall {
self.abi_index.call(self.abi, id)
}
fn swift_type(&self, ty: &TypeExpr) -> String {
match ty {
TypeExpr::Handle(id) => self.swift_name_for_class(id),
TypeExpr::Callback(id) => format!("any {}", pascal_case(id.as_str())),
TypeExpr::Custom(id) => {
if let Some(mapping) = self.type_mappings.get(id.as_str()) {
mapping.native_type.clone()
} else {
pascal_case(id.as_str())
}
}
TypeExpr::Option(inner) => match inner.as_ref() {
TypeExpr::Handle(id) => format!("{}?", self.swift_name_for_class(id)),
TypeExpr::Callback(id) => format!("(any {})?", pascal_case(id.as_str())),
TypeExpr::Custom(id) => {
if let Some(mapping) = self.type_mappings.get(id.as_str()) {
format!("{}?", mapping.native_type)
} else {
format!("{}?", pascal_case(id.as_str()))
}
}
_ => self.resolve_swift_type(ty),
},
TypeExpr::Vec(inner) => {
if matches!(inner.as_ref(), TypeExpr::Primitive(PrimitiveType::U8)) {
"Data".to_string()
} else {
format!("[{}]", self.swift_type(inner))
}
}
TypeExpr::Result { ok, err } => {
format!("Result<{}, {}>", self.swift_type(ok), self.swift_type(err))
}
_ => emit::swift_type(ty),
}
}
fn swift_return_value_type(&self, returns: &ReturnDef) -> String {
match returns {
ReturnDef::Void => "Void".to_string(),
ReturnDef::Value(ty) => self.swift_type(ty),
ReturnDef::Result { ok, .. } => self.swift_type(ok),
}
}
fn is_c_style_enum_return(&self, returns: &ReturnDef) -> bool {
let enum_id = match returns {
ReturnDef::Value(TypeExpr::Enum(id))
| ReturnDef::Result {
ok: TypeExpr::Enum(id),
..
} => id,
_ => return false,
};
self.contract
.catalog
.resolve_enum(enum_id)
.map(|e| matches!(e.repr, EnumRepr::CStyle { .. }))
.unwrap_or(false)
}
fn swift_error_type(&self, returns: &ReturnDef) -> String {
match returns {
ReturnDef::Result { err, .. } => self.swift_type(err),
_ => "FfiError".to_string(),
}
}
fn error_is_string(&self, returns: &ReturnDef) -> bool {
matches!(returns, ReturnDef::Result { err, .. } if matches!(err, TypeExpr::String))
}
fn record_decode_fields(&self, record: &AbiRecord) -> HashMap<FieldName, ReadSeq> {
record
.decode_ops
.ops
.iter()
.find_map(|op| match op {
ReadOp::Record { fields, .. } => Some(fields),
_ => None,
})
.into_iter()
.flat_map(|fields| {
fields
.iter()
.map(|field| (field.name.clone(), field.seq.clone()))
})
.collect()
}
fn record_encode_fields(&self, record: &AbiRecord) -> HashMap<FieldName, WriteSeq> {
record
.encode_ops
.ops
.iter()
.find_map(|op| match op {
WriteOp::Record { fields, .. } => Some(fields),
_ => None,
})
.into_iter()
.flat_map(|fields| {
fields
.iter()
.map(|field| (field.name.clone(), field.seq.clone()))
})
.collect()
}
fn record_field_offset(&self, seq: &ReadSeq) -> Option<usize> {
seq.ops.first().and_then(|op| match op {
ReadOp::Primitive { offset, .. } => match offset {
OffsetExpr::Fixed(value) => Some(*value),
OffsetExpr::Base => Some(0),
OffsetExpr::BasePlus(add) => Some(*add),
OffsetExpr::Var(_) | OffsetExpr::VarPlus(_, _) => None,
},
_ => None,
})
}
fn rebase_read_seq(&self, seq: &ReadSeq, old_base: &str, new_base: &str) -> ReadSeq {
ReadSeq {
size: seq.size.clone(),
ops: seq
.ops
.iter()
.map(|op| self.rebase_read_op(op, old_base, new_base))
.collect(),
shape: seq.shape,
}
}
fn rebase_read_op(&self, op: &ReadOp, old_base: &str, new_base: &str) -> ReadOp {
match op {
ReadOp::Primitive { primitive, offset } => ReadOp::Primitive {
primitive: *primitive,
offset: self.rebase_offset_expr(offset, old_base, new_base),
},
ReadOp::String { offset } => ReadOp::String {
offset: self.rebase_offset_expr(offset, old_base, new_base),
},
ReadOp::Bytes { offset } => ReadOp::Bytes {
offset: self.rebase_offset_expr(offset, old_base, new_base),
},
ReadOp::Option { tag_offset, some } => ReadOp::Option {
tag_offset: self.rebase_offset_expr(tag_offset, old_base, new_base),
some: Box::new(self.rebase_read_seq(some, old_base, new_base)),
},
ReadOp::Vec {
len_offset,
element_type,
element,
layout,
} => ReadOp::Vec {
len_offset: self.rebase_offset_expr(len_offset, old_base, new_base),
element_type: element_type.clone(),
element: Box::new(self.rebase_read_seq(element, old_base, new_base)),
layout: layout.clone(),
},
ReadOp::Record { id, offset, fields } => ReadOp::Record {
id: id.clone(),
offset: self.rebase_offset_expr(offset, old_base, new_base),
fields: fields
.iter()
.map(|field| {
let seq = self.rebase_read_seq(&field.seq, old_base, new_base);
FieldReadOp {
name: field.name.clone(),
seq,
}
})
.collect(),
},
ReadOp::Enum { id, offset, layout } => ReadOp::Enum {
id: id.clone(),
offset: self.rebase_offset_expr(offset, old_base, new_base),
layout: layout.clone(),
},
ReadOp::Result {
tag_offset,
ok,
err,
} => ReadOp::Result {
tag_offset: self.rebase_offset_expr(tag_offset, old_base, new_base),
ok: Box::new(self.rebase_read_seq(ok, old_base, new_base)),
err: Box::new(self.rebase_read_seq(err, old_base, new_base)),
},
ReadOp::Builtin { id, offset } => ReadOp::Builtin {
id: id.clone(),
offset: self.rebase_offset_expr(offset, old_base, new_base),
},
ReadOp::Custom { id, underlying } => ReadOp::Custom {
id: id.clone(),
underlying: Box::new(self.rebase_read_seq(underlying, old_base, new_base)),
},
}
}
fn rebase_offset_expr(
&self,
offset: &OffsetExpr,
old_base: &str,
new_base: &str,
) -> OffsetExpr {
match offset {
OffsetExpr::Fixed(value) => OffsetExpr::Fixed(*value),
OffsetExpr::Base => OffsetExpr::Base,
OffsetExpr::BasePlus(add) => OffsetExpr::BasePlus(*add),
OffsetExpr::Var(name) => {
if name == old_base {
OffsetExpr::Var(new_base.to_string())
} else {
OffsetExpr::Var(name.clone())
}
}
OffsetExpr::VarPlus(name, add) => {
if name == old_base {
OffsetExpr::VarPlus(new_base.to_string(), *add)
} else {
OffsetExpr::VarPlus(name.clone(), *add)
}
}
}
}
fn rebase_return_encode(&self, returns: SwiftReturn, new_base: &str) -> SwiftReturn {
match returns {
SwiftReturn::FromWireBuffer {
swift_type,
decode,
encode,
} => SwiftReturn::FromWireBuffer {
swift_type,
decode,
encode: remap_root_in_seq(&encode, ValueExpr::Var(new_base.to_string())),
},
SwiftReturn::Throws {
ok,
err_type,
result_decode,
err_decode,
err_is_string,
err_encode,
} => SwiftReturn::Throws {
ok: Box::new(self.rebase_return_encode(*ok, new_base)),
err_type,
result_decode,
err_decode,
err_is_string,
err_encode: err_encode
.map(|seq| remap_root_in_seq(&seq, ValueExpr::Var("error".to_string()))),
},
other => other,
}
}
fn buffer_element_swift_type(&self, ty: &TypeExpr) -> String {
match ty {
TypeExpr::Vec(inner) => self.swift_type(inner),
TypeExpr::Bytes => "UInt8".to_string(),
_ => "UInt8".to_string(),
}
}
fn build_closure_trampoline(
&self,
callback_id: &CallbackId,
param_name: &str,
) -> SwiftClosureTrampoline {
let callback_def = self
.contract
.catalog
.resolve_callback(callback_id)
.expect("closure callback should exist");
let method = &callback_def.methods[0];
let abi_callback = self.abi_index.callback(self.abi, callback_id);
let abi_method = abi_callback
.methods
.iter()
.find(|m| m.id == method.id)
.expect("closure callback method");
let param_types: Vec<String> = method
.params
.iter()
.map(|p| self.swift_type(&p.type_expr))
.collect();
let return_type = match &method.returns {
ReturnDef::Void => "Void".to_string(),
ReturnDef::Value(ty) => self.swift_type(ty),
ReturnDef::Result { ok, .. } => format!("Result<{}, Error>", self.swift_type(ok)),
};
let swift_type = if param_types.is_empty() {
format!("() -> {}", return_type)
} else {
format!("({}) -> {}", param_types.join(", "), return_type)
};
let upper_name = pascal_case(param_name);
let type_alias = format!("{}CallbackFn", upper_name);
let box_class = format!("{}CallbackBox", upper_name);
let abi_params: Vec<&AbiParam> = abi_method
.params
.iter()
.filter(|param| {
matches!(
param.role,
ParamRole::Input {
transport: Transport::Scalar(_) | Transport::Span(SpanContent::Encoded(_)),
..
}
)
})
.collect();
let trampoline_params: Vec<SwiftClosureTrampolineParam> = method
.params
.iter()
.zip(abi_params.iter())
.enumerate()
.map(|(idx, (param_def, abi_param))| {
self.build_closure_trampoline_param(idx, param_def, abi_param)
})
.collect();
SwiftClosureTrampoline {
type_alias,
swift_type,
box_class,
box_var: format!("{}Box", param_name),
ptr_var: format!("{}Ptr", param_name),
trampoline_var: format!("{}Trampoline", param_name),
param_name: param_name.to_string(),
trampoline_params,
}
}
fn build_closure_trampoline_param(
&self,
idx: usize,
param_def: &ParamDef,
abi_param: &AbiParam,
) -> SwiftClosureTrampolineParam {
match &abi_param.role {
ParamRole::Input {
transport: Transport::Span(SpanContent::Encoded(_)),
decode_ops: Some(decode_ops),
..
} => {
let ptr_name = format!("ptr{}", idx);
let len_name = format!("len{}", idx);
let reader_decode = emit::emit_reader_read(decode_ops);
let decode_expr = format!(
"{{ var reader = WireReader(ptr: {}!, len: Int({})); return {} }}()",
ptr_name, len_name, reader_decode
);
SwiftClosureTrampolineParam {
name: format!("{}, {}", ptr_name, len_name),
c_type: "UnsafePointer<UInt8>?, UInt".to_string(),
decode_expr,
}
}
ParamRole::Input {
transport: Transport::Scalar(_),
..
} => {
let arg_name = format!("arg{}", idx);
SwiftClosureTrampolineParam {
name: arg_name.clone(),
c_type: self.abi_to_swift(&abi_param.abi_type),
decode_expr: arg_name,
}
}
_ => unreachable!(
"unsupported closure param role for {}",
param_def.name.as_str()
),
}
}
fn abi_to_swift(&self, abi: &AbiType) -> String {
match abi {
AbiType::Void => "Void".to_string(),
AbiType::Bool => "Bool".to_string(),
AbiType::I8 => "Int8".to_string(),
AbiType::U8 => "UInt8".to_string(),
AbiType::I16 => "Int16".to_string(),
AbiType::U16 => "UInt16".to_string(),
AbiType::I32 => "Int32".to_string(),
AbiType::U32 => "UInt32".to_string(),
AbiType::I64 => "Int64".to_string(),
AbiType::U64 => "UInt64".to_string(),
AbiType::ISize => "Int".to_string(),
AbiType::USize => "UInt".to_string(),
AbiType::F32 => "Float".to_string(),
AbiType::F64 => "Double".to_string(),
AbiType::Pointer(_) | AbiType::InlineCallbackFn(_) | AbiType::Handle(_) => {
"OpaquePointer".to_string()
}
AbiType::CallbackHandle => "BoltFFICallbackHandle".to_string(),
AbiType::Struct(id) => format!("___{}", id.as_str()),
}
}
fn swift_name_for_record(&self, id: &RecordId) -> String {
pascal_case(id.as_str())
}
fn swift_name_for_enum(&self, id: &EnumId) -> String {
pascal_case(id.as_str())
}
fn swift_name_for_class(&self, id: &ClassId) -> String {
pascal_case(id.as_str())
}
fn lower_call_mode(&self, call: &AbiCall, returns: &ReturnDef) -> SwiftCallMode {
match &call.mode {
CallMode::Sync => SwiftCallMode::Sync {
symbol: call.symbol.as_str().to_string(),
},
CallMode::Async(async_call) => SwiftCallMode::Async {
start: call.symbol.as_str().to_string(),
poll: async_call.poll.as_str().to_string(),
complete: async_call.complete.as_str().to_string(),
cancel: async_call.cancel.as_str().to_string(),
free: async_call.free.as_str().to_string(),
result: Box::new(self.lower_async_result(
&async_call.result,
&async_call.error,
returns,
)),
},
}
}
fn lower_async_result(
&self,
result_shape: &ReturnShape,
error: &ErrorTransport,
returns: &ReturnDef,
) -> SwiftAsyncResult {
let returns_is_result = matches!(returns, ReturnDef::Result { .. });
let throws = returns_is_result || matches!(error, ErrorTransport::Encoded { .. });
match &result_shape.transport {
None => SwiftAsyncResult::Void,
Some(Transport::Scalar(origin)) => SwiftAsyncResult::Direct {
swift_type: self.abi_to_swift(&AbiType::from(origin.primitive())),
conversion: SwiftAsyncConversion::None,
},
Some(Transport::Span(SpanContent::Scalar(origin))) => {
let primitive = origin.primitive();
let element_swift_type = self.abi_to_swift(&AbiType::from(primitive));
let enum_mapping = match origin {
ScalarOrigin::CStyleEnum { enum_id, .. } => {
Some(self.swift_name_for_enum(enum_id))
}
ScalarOrigin::Primitive(_) => None,
};
SwiftAsyncResult::DirectBuffer {
swift_type: self.swift_return_value_type(returns),
element_swift_type,
composite_mapping: None,
enum_mapping,
}
}
Some(Transport::Span(SpanContent::Composite(layout))) => {
let c_struct = format!("___{}", layout.record_id.as_str());
let swift_record = self.swift_name_for_record(&layout.record_id);
let fields = self.composite_field_mappings(layout);
SwiftAsyncResult::DirectBuffer {
swift_type: self.swift_return_value_type(returns),
element_swift_type: c_struct,
composite_mapping: Some(DirectBufferCompositeMapping {
swift_record_type: swift_record,
fields,
}),
enum_mapping: None,
}
}
Some(Transport::Span(_)) => {
let decode_ops = result_shape.decode_ops.clone().unwrap_or_else(|| ReadSeq {
size: SizeExpr::Fixed(0),
ops: vec![],
shape: WireShape::Value,
});
self.encoded_async_result(decode_ops, throws, error, returns)
}
Some(Transport::Composite(_)) => {
let decode_ops = result_shape.decode_ops.clone().unwrap_or_else(|| ReadSeq {
size: SizeExpr::Fixed(0),
ops: vec![],
shape: WireShape::Value,
});
self.encoded_async_result(decode_ops, throws, error, returns)
}
Some(Transport::Handle { class_id, nullable }) => SwiftAsyncResult::Direct {
swift_type: if *nullable {
format!("{}?", self.swift_name_for_class(class_id))
} else {
self.swift_name_for_class(class_id)
},
conversion: SwiftAsyncConversion::Handle {
class_name: self.swift_name_for_class(class_id),
nullable: *nullable,
},
},
Some(Transport::Callback {
callback_id,
nullable,
..
}) => SwiftAsyncResult::Direct {
swift_type: if *nullable {
format!("(any {})?", pascal_case(callback_id.as_str()))
} else {
format!("any {}", pascal_case(callback_id.as_str()))
},
conversion: SwiftAsyncConversion::Callback {
protocol: pascal_case(callback_id.as_str()),
nullable: *nullable,
},
},
}
}
fn encoded_async_result(
&self,
decode_ops: ReadSeq,
throws: bool,
error: &ErrorTransport,
returns: &ReturnDef,
) -> SwiftAsyncResult {
let ok_type = if throws {
match returns {
ReturnDef::Result { ok, .. } => Some(self.swift_type(ok)),
_ => None,
}
} else {
None
};
let (swift_type, err_is_string) = match returns {
ReturnDef::Result { ok, err } => (self.swift_type(ok), matches!(err, TypeExpr::String)),
ReturnDef::Value(ty) => (self.swift_type(ty), false),
ReturnDef::Void => ("Void".to_string(), false),
};
let err_decode = match error {
ErrorTransport::Encoded { decode_ops, .. } => decode_ops.clone(),
ErrorTransport::None | ErrorTransport::StatusCode => {
self.error_decode_from_result_read(&decode_ops)
}
};
SwiftAsyncResult::Encoded {
swift_type,
ok_type,
decode: decode_ops,
throws,
err_decode,
err_is_string,
}
}
fn error_decode_from_result_read(&self, decode_ops: &ReadSeq) -> ReadSeq {
match decode_ops.ops.first() {
Some(ReadOp::Result { err, .. }) => err.as_ref().clone(),
_ => ReadSeq {
size: SizeExpr::Fixed(0),
ops: Vec::new(),
shape: WireShape::Value,
},
}
}
fn lower_return_def_for_async(
&self,
error: &ErrorTransport,
returns: &ReturnDef,
) -> SwiftReturn {
match error {
ErrorTransport::None => SwiftReturn::Void,
ErrorTransport::StatusCode => SwiftReturn::Throws {
ok: Box::new(SwiftReturn::Void),
err_type: "FfiError".to_string(),
result_decode: ReadSeq {
size: SizeExpr::Fixed(0),
ops: vec![],
shape: WireShape::Value,
},
err_decode: ReadSeq {
size: SizeExpr::Fixed(0),
ops: vec![],
shape: WireShape::Value,
},
err_is_string: false,
err_encode: None,
},
ErrorTransport::Encoded {
decode_ops,
encode_ops,
} => SwiftReturn::Throws {
ok: Box::new(SwiftReturn::Void),
err_type: self.swift_error_type(returns),
result_decode: decode_ops.clone(),
err_decode: decode_ops.clone(),
err_is_string: self.error_is_string(returns),
err_encode: encode_ops.clone(),
},
}
}
}
fn swift_default_literal(default: &DefaultValue) -> String {
match default {
DefaultValue::Bool(true) => "true".to_string(),
DefaultValue::Bool(false) => "false".to_string(),
DefaultValue::Integer(v) => v.to_string(),
DefaultValue::Float(v) => format!("{}", v),
DefaultValue::String(v) => format!("\"{}\"", v),
DefaultValue::EnumVariant { variant_name, .. } => {
format!(".{}", variant_name.to_lower_camel_case())
}
DefaultValue::Null => "nil".to_string(),
}
}
fn lower_first_char(name: &str) -> String {
name.chars()
.enumerate()
.map(|(index, ch)| {
if index == 0 {
ch.to_ascii_lowercase()
} else {
ch
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::Lowerer as IrLowerer;
use crate::ir::contract::{FfiContract, PackageInfo};
use crate::ir::definitions::{
CStyleVariant, CallbackKind, CallbackMethodDef, CallbackTraitDef, DataVariant, EnumDef,
EnumRepr, FieldDef, ParamDef, ParamPassing, RecordDef, ReturnDef, VariantPayload,
};
use crate::ir::ids::{CallbackId, FieldName, MethodId, ParamName, VariantName};
use crate::ir::types::{PrimitiveType, TypeExpr};
fn empty_contract() -> FfiContract {
FfiContract {
package: PackageInfo {
name: "test".to_string(),
version: None,
},
functions: vec![],
catalog: Default::default(),
}
}
fn lower_contract(contract: &FfiContract) -> SwiftModule {
let abi = IrLowerer::new(contract).to_abi_contract();
SwiftLowerer::new(contract, &abi).lower()
}
#[test]
fn blittable_record_is_detected() {
let mut contract = empty_contract();
contract.catalog.insert_record(RecordDef {
is_repr_c: true,
id: RecordId::new("Point"),
fields: vec![
FieldDef {
name: FieldName::new("x"),
type_expr: TypeExpr::Primitive(PrimitiveType::F64),
doc: None,
default: None,
},
FieldDef {
name: FieldName::new("y"),
type_expr: TypeExpr::Primitive(PrimitiveType::F64),
doc: None,
default: None,
},
],
doc: None,
deprecated: None,
});
let module = lower_contract(&contract);
assert_eq!(module.records.len(), 1);
let record = &module.records[0];
assert!(
record.is_blittable,
"Point should be blittable (primitives only)"
);
assert_eq!(record.blittable_size, Some(16));
}
#[test]
fn non_blittable_record_with_string() {
let mut contract = empty_contract();
contract.catalog.insert_record(RecordDef {
is_repr_c: true,
id: RecordId::new("User"),
fields: vec![
FieldDef {
name: FieldName::new("id"),
type_expr: TypeExpr::Primitive(PrimitiveType::I64),
doc: None,
default: None,
},
FieldDef {
name: FieldName::new("name"),
type_expr: TypeExpr::String,
doc: None,
default: None,
},
],
doc: None,
deprecated: None,
});
let module = lower_contract(&contract);
assert_eq!(module.records.len(), 1);
let record = &module.records[0];
assert!(
!record.is_blittable,
"User should NOT be blittable (has String)"
);
assert_eq!(record.blittable_size, None);
}
#[test]
fn non_blittable_record_with_vec() {
let mut contract = empty_contract();
contract.catalog.insert_record(RecordDef {
is_repr_c: true,
id: RecordId::new("Scores"),
fields: vec![FieldDef {
name: FieldName::new("values"),
type_expr: TypeExpr::Vec(Box::new(TypeExpr::Primitive(PrimitiveType::I32))),
doc: None,
default: None,
}],
doc: None,
deprecated: None,
});
let module = lower_contract(&contract);
assert_eq!(module.records.len(), 1);
let record = &module.records[0];
assert!(
!record.is_blittable,
"Scores should NOT be blittable (has Vec)"
);
}
#[test]
fn field_names_are_camel_case() {
let mut contract = empty_contract();
contract.catalog.insert_record(RecordDef {
is_repr_c: true,
id: RecordId::new("Config"),
fields: vec![
FieldDef {
name: FieldName::new("max_connections"),
type_expr: TypeExpr::Primitive(PrimitiveType::I32),
doc: None,
default: None,
},
FieldDef {
name: FieldName::new("timeout_ms"),
type_expr: TypeExpr::Primitive(PrimitiveType::U64),
doc: None,
default: None,
},
],
doc: None,
deprecated: None,
});
let module = lower_contract(&contract);
let record = &module.records[0];
assert_eq!(record.fields[0].swift_name, "maxConnections");
assert_eq!(record.fields[1].swift_name, "timeoutMs");
}
#[test]
fn primitive_types_map_correctly() {
let mut contract = empty_contract();
contract.catalog.insert_record(RecordDef {
is_repr_c: true,
id: RecordId::new("AllPrimitives"),
fields: vec![
FieldDef {
name: FieldName::new("a"),
type_expr: TypeExpr::Primitive(PrimitiveType::Bool),
doc: None,
default: None,
},
FieldDef {
name: FieldName::new("b"),
type_expr: TypeExpr::Primitive(PrimitiveType::I8),
doc: None,
default: None,
},
FieldDef {
name: FieldName::new("c"),
type_expr: TypeExpr::Primitive(PrimitiveType::U8),
doc: None,
default: None,
},
FieldDef {
name: FieldName::new("d"),
type_expr: TypeExpr::Primitive(PrimitiveType::I16),
doc: None,
default: None,
},
FieldDef {
name: FieldName::new("e"),
type_expr: TypeExpr::Primitive(PrimitiveType::U16),
doc: None,
default: None,
},
FieldDef {
name: FieldName::new("f"),
type_expr: TypeExpr::Primitive(PrimitiveType::I32),
doc: None,
default: None,
},
FieldDef {
name: FieldName::new("g"),
type_expr: TypeExpr::Primitive(PrimitiveType::U32),
doc: None,
default: None,
},
FieldDef {
name: FieldName::new("h"),
type_expr: TypeExpr::Primitive(PrimitiveType::I64),
doc: None,
default: None,
},
FieldDef {
name: FieldName::new("i"),
type_expr: TypeExpr::Primitive(PrimitiveType::U64),
doc: None,
default: None,
},
FieldDef {
name: FieldName::new("j"),
type_expr: TypeExpr::Primitive(PrimitiveType::F32),
doc: None,
default: None,
},
FieldDef {
name: FieldName::new("k"),
type_expr: TypeExpr::Primitive(PrimitiveType::F64),
doc: None,
default: None,
},
],
doc: None,
deprecated: None,
});
let module = lower_contract(&contract);
let record = &module.records[0];
assert_eq!(record.fields[0].swift_type, "Bool");
assert_eq!(record.fields[1].swift_type, "Int8");
assert_eq!(record.fields[2].swift_type, "UInt8");
assert_eq!(record.fields[3].swift_type, "Int16");
assert_eq!(record.fields[4].swift_type, "UInt16");
assert_eq!(record.fields[5].swift_type, "Int32");
assert_eq!(record.fields[6].swift_type, "UInt32");
assert_eq!(record.fields[7].swift_type, "Int64");
assert_eq!(record.fields[8].swift_type, "UInt64");
assert_eq!(record.fields[9].swift_type, "Float");
assert_eq!(record.fields[10].swift_type, "Double");
}
#[test]
fn c_style_enum_is_lowered() {
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: VariantName::new("Active"),
discriminant: 0,
doc: None,
},
CStyleVariant {
name: VariantName::new("Inactive"),
discriminant: 1,
doc: None,
},
CStyleVariant {
name: VariantName::new("Pending"),
discriminant: 2,
doc: None,
},
],
},
is_error: false,
doc: None,
deprecated: None,
});
let module = lower_contract(&contract);
assert_eq!(module.enums.len(), 1);
let e = &module.enums[0];
assert_eq!(e.name, "Status");
assert!(e.is_c_style());
assert_eq!(e.variants.len(), 3);
assert_eq!(e.variants[0].swift_name, "active");
assert_eq!(e.variants[1].swift_name, "inactive");
assert_eq!(e.variants[2].swift_name, "pending");
}
#[test]
fn data_enum_is_lowered() {
let mut contract = empty_contract();
contract.catalog.insert_enum(EnumDef {
id: EnumId::new("Value"),
repr: EnumRepr::Data {
tag_type: PrimitiveType::I32,
variants: vec![
DataVariant {
name: VariantName::new("Int"),
discriminant: 0,
payload: VariantPayload::Tuple(vec![TypeExpr::Primitive(
PrimitiveType::I64,
)]),
doc: None,
},
DataVariant {
name: VariantName::new("Text"),
discriminant: 1,
payload: VariantPayload::Tuple(vec![TypeExpr::String]),
doc: None,
},
],
},
is_error: false,
doc: None,
deprecated: None,
});
let module = lower_contract(&contract);
assert_eq!(module.enums.len(), 1);
let e = &module.enums[0];
assert_eq!(e.name, "Value");
assert!(!e.is_c_style());
assert_eq!(e.variants.len(), 2);
}
#[test]
fn blittable_struct_has_c_offsets() {
let mut contract = empty_contract();
contract.catalog.insert_record(RecordDef {
is_repr_c: true,
id: RecordId::new("Aligned"),
fields: vec![
FieldDef {
name: FieldName::new("a"),
type_expr: TypeExpr::Primitive(PrimitiveType::U8),
doc: None,
default: None,
},
FieldDef {
name: FieldName::new("b"),
type_expr: TypeExpr::Primitive(PrimitiveType::U32),
doc: None,
default: None,
},
FieldDef {
name: FieldName::new("c"),
type_expr: TypeExpr::Primitive(PrimitiveType::U8),
doc: None,
default: None,
},
],
doc: None,
deprecated: None,
});
let module = lower_contract(&contract);
let record = &module.records[0];
assert!(record.is_blittable);
assert_eq!(record.fields[0].c_offset, Some(0));
assert_eq!(record.fields[1].c_offset, Some(4));
assert_eq!(record.fields[2].c_offset, Some(8));
}
#[test]
fn option_type_maps_to_swift_optional() {
let mut contract = empty_contract();
contract.catalog.insert_record(RecordDef {
is_repr_c: true,
id: RecordId::new("MaybeValue"),
fields: vec![FieldDef {
name: FieldName::new("value"),
type_expr: TypeExpr::Option(Box::new(TypeExpr::Primitive(PrimitiveType::I32))),
doc: None,
default: None,
}],
doc: None,
deprecated: None,
});
let module = lower_contract(&contract);
let record = &module.records[0];
assert_eq!(record.fields[0].swift_type, "Int32?");
}
#[test]
fn vec_type_maps_to_swift_array() {
let mut contract = empty_contract();
contract.catalog.insert_record(RecordDef {
is_repr_c: true,
id: RecordId::new("Numbers"),
fields: vec![FieldDef {
name: FieldName::new("items"),
type_expr: TypeExpr::Vec(Box::new(TypeExpr::Primitive(PrimitiveType::I32))),
doc: None,
default: None,
}],
doc: None,
deprecated: None,
});
let module = lower_contract(&contract);
let record = &module.records[0];
assert_eq!(record.fields[0].swift_type, "[Int32]");
}
#[test]
fn nested_record_type_maps_correctly() {
let mut contract = empty_contract();
contract.catalog.insert_record(RecordDef {
is_repr_c: true,
id: RecordId::new("Inner"),
fields: vec![FieldDef {
name: FieldName::new("value"),
type_expr: TypeExpr::Primitive(PrimitiveType::I32),
doc: None,
default: None,
}],
doc: None,
deprecated: None,
});
contract.catalog.insert_record(RecordDef {
is_repr_c: true,
id: RecordId::new("Outer"),
fields: vec![FieldDef {
name: FieldName::new("inner"),
type_expr: TypeExpr::Record(RecordId::new("Inner")),
doc: None,
default: None,
}],
doc: None,
deprecated: None,
});
let module = lower_contract(&contract);
let outer = module
.records
.iter()
.find(|r| r.class_name == "Outer")
.unwrap();
assert_eq!(outer.fields[0].swift_type, "Inner");
}
#[test]
fn callback_with_encoded_param_does_not_panic() {
let mut contract = empty_contract();
contract.catalog.insert_callback(CallbackTraitDef {
id: CallbackId::new("Logger"),
methods: vec![CallbackMethodDef {
id: MethodId::new("log"),
params: vec![ParamDef {
name: ParamName::new("message"),
type_expr: TypeExpr::String,
passing: ParamPassing::Value,
doc: None,
}],
returns: ReturnDef::Void,
is_async: false,
doc: None,
}],
kind: CallbackKind::Trait,
doc: None,
});
let module = lower_contract(&contract);
assert_eq!(module.callbacks.len(), 1);
let cb = &module.callbacks[0];
assert_eq!(cb.protocol_name, "Logger");
assert_eq!(cb.methods.len(), 1);
assert_eq!(cb.methods[0].params.len(), 1);
}
#[test]
fn swift_default_literal_bool() {
assert_eq!(swift_default_literal(&DefaultValue::Bool(true)), "true");
assert_eq!(swift_default_literal(&DefaultValue::Bool(false)), "false");
}
#[test]
fn swift_default_literal_integer() {
assert_eq!(swift_default_literal(&DefaultValue::Integer(42)), "42");
assert_eq!(swift_default_literal(&DefaultValue::Integer(-1)), "-1");
}
#[test]
fn swift_default_literal_float() {
assert_eq!(swift_default_literal(&DefaultValue::Float(2.5)), "2.5");
}
#[test]
fn swift_default_literal_string() {
assert_eq!(
swift_default_literal(&DefaultValue::String("hello".to_string())),
"\"hello\""
);
}
#[test]
fn swift_default_literal_enum_variant() {
assert_eq!(
swift_default_literal(&DefaultValue::EnumVariant {
enum_name: "Direction".to_string(),
variant_name: "North".to_string(),
}),
".north"
);
}
#[test]
fn swift_default_literal_null() {
assert_eq!(swift_default_literal(&DefaultValue::Null), "nil");
}
#[test]
fn record_field_default_expr_propagates() {
let mut contract = empty_contract();
contract.catalog.insert_record(RecordDef {
is_repr_c: true,
id: RecordId::new("Config"),
fields: vec![
FieldDef {
name: FieldName::new("name"),
type_expr: TypeExpr::String,
doc: None,
default: None,
},
FieldDef {
name: FieldName::new("retries"),
type_expr: TypeExpr::Primitive(PrimitiveType::I32),
doc: None,
default: Some(DefaultValue::Integer(3)),
},
FieldDef {
name: FieldName::new("label"),
type_expr: TypeExpr::Option(Box::new(TypeExpr::String)),
doc: None,
default: Some(DefaultValue::Null),
},
],
doc: None,
deprecated: None,
});
let module = lower_contract(&contract);
let record = &module.records[0];
assert!(record.fields[0].default_expr.is_none());
assert_eq!(record.fields[1].default_expr.as_deref(), Some("3"));
assert_eq!(record.fields[2].default_expr.as_deref(), Some("nil"));
}
fn contract_with_blittable_point() -> FfiContract {
let mut contract = empty_contract();
contract.catalog.insert_record(RecordDef {
is_repr_c: true,
id: RecordId::new("Point"),
fields: vec![
FieldDef {
name: FieldName::new("x"),
type_expr: TypeExpr::Primitive(PrimitiveType::F64),
doc: None,
default: None,
},
FieldDef {
name: FieldName::new("y"),
type_expr: TypeExpr::Primitive(PrimitiveType::F64),
doc: None,
default: None,
},
],
doc: None,
deprecated: None,
});
contract
}
#[test]
fn blittable_param_uses_to_composite_conversion() {
use crate::ir::definitions::FunctionDef;
use crate::ir::ids::FunctionId;
let mut contract = contract_with_blittable_point();
contract.functions.push(FunctionDef {
id: FunctionId::new("translate"),
params: vec![ParamDef {
name: ParamName::new("point"),
type_expr: TypeExpr::Record(RecordId::new("Point")),
passing: ParamPassing::Value,
doc: None,
}],
returns: ReturnDef::Void,
is_async: false,
doc: None,
deprecated: None,
});
let module = lower_contract(&contract);
let func = &module.functions[0];
let param = &func.params[0];
assert!(
matches!(param.conversion, SwiftConversion::ToComposite { .. }),
"blittable record param should use ToComposite, got: {:?}",
param.conversion
);
let ffi_arg = param.ffi_arg();
assert!(
ffi_arg.contains("___Point"),
"ffi_arg should construct ___Point, got: {}",
ffi_arg
);
assert!(
ffi_arg.contains("x: point.x") && ffi_arg.contains("y: point.y"),
"ffi_arg should map fields, got: {}",
ffi_arg
);
}
#[test]
fn blittable_return_uses_from_composite() {
use crate::ir::definitions::FunctionDef;
use crate::ir::ids::FunctionId;
let mut contract = contract_with_blittable_point();
contract.functions.push(FunctionDef {
id: FunctionId::new("get_origin"),
params: vec![],
returns: ReturnDef::Value(TypeExpr::Record(RecordId::new("Point"))),
is_async: false,
doc: None,
deprecated: None,
});
let module = lower_contract(&contract);
let func = &module.functions[0];
assert!(
func.returns.is_composite(),
"blittable return should be FromComposite, got: {:?}",
func.returns
);
let convert = func.returns.composite_convert_expr("_raw").unwrap();
assert!(
convert.contains("Point(")
&& convert.contains("x: _raw.x")
&& convert.contains("y: _raw.y"),
"convert expr should construct Point from C fields, got: {}",
convert
);
}
}