diff --git a/crates/moonbit/src/ffi.rs b/crates/moonbit/src/ffi.rs index 63c9daa41..72811d55e 100644 --- a/crates/moonbit/src/ffi.rs +++ b/crates/moonbit/src/ffi.rs @@ -1,9 +1,3 @@ -pub(crate) const EXTEND16: &str = r#" -///| -extern "wasm" fn mbt_ffi_extend16(value : Int) -> Int = - #|(func (param i32) (result i32) local.get 0 i32.extend16_s) -"#; - pub(crate) const EXTEND8: &str = r#" ///| extern "wasm" fn mbt_ffi_extend8(value : Int) -> Int = @@ -126,6 +120,13 @@ extern "wasm" fn mbt_ffi_ptr2bytes(ptr : Int, len : Int) -> FixedArray[Byte] = #| local.get 0) "#; +pub(crate) const BOOL_ARRAY2PTR: &str = r#" +///| +#owned(array) +extern "wasm" fn mbt_ffi_bool_array2ptr(array : FixedArray[Bool]) -> Int = + #|(func (param i32) (result i32) local.get 0) +"#; + pub(crate) const UINT_ARRAY2PTR: &str = r#" ///| #owned(array) @@ -133,6 +134,13 @@ extern "wasm" fn mbt_ffi_uint_array2ptr(array : FixedArray[UInt]) -> Int = #|(func (param i32) (result i32) local.get 0) "#; +pub(crate) const UINT16_ARRAY2PTR: &str = r#" +///| +#owned(array) +extern "wasm" fn mbt_ffi_uint16_array2ptr(array : FixedArray[UInt16]) -> Int = + #|(func (param i32) (result i32) local.get 0) +"#; + pub(crate) const UINT64_ARRAY2PTR: &str = r#" ///| #owned(array) @@ -147,6 +155,13 @@ extern "wasm" fn mbt_ffi_int_array2ptr(array : FixedArray[Int]) -> Int = #|(func (param i32) (result i32) local.get 0) "#; +pub(crate) const INT16_ARRAY2PTR: &str = r#" +///| +#owned(array) +extern "wasm" fn mbt_ffi_int16_array2ptr(array : FixedArray[Int16]) -> Int = + #|(func (param i32) (result i32) local.get 0) +"#; + pub(crate) const INT64_ARRAY2PTR: &str = r#" ///| #owned(array) @@ -168,6 +183,15 @@ extern "wasm" fn mbt_ffi_double_array2ptr(array : FixedArray[Double]) -> Int = #|(func (param i32) (result i32) local.get 0) "#; +pub(crate) const PTR2BOOL_ARRAY: &str = r#" +///| +extern "wasm" fn mbt_ffi_ptr2bool_array(ptr : Int, len : Int) -> FixedArray[Bool] = + #|(func (param i32) (param i32) (result i32) + #| local.get 0 + #| local.get 1 call $moonbit.init_array8 + #| local.get 0) +"#; + pub(crate) const PTR2UINT_ARRAY: &str = r#" ///| extern "wasm" fn mbt_ffi_ptr2uint_array(ptr : Int, len : Int) -> FixedArray[UInt] = @@ -177,6 +201,15 @@ extern "wasm" fn mbt_ffi_ptr2uint_array(ptr : Int, len : Int) -> FixedArray[UInt #| local.get 0) "#; +pub(crate) const PTR2UINT16_ARRAY: &str = r#" +///| +extern "wasm" fn mbt_ffi_ptr2uint16_array(ptr : Int, len : Int) -> FixedArray[UInt16] = + #|(func (param i32) (param i32) (result i32) + #| local.get 0 + #| local.get 1 call $moonbit.init_array16 + #| local.get 0) +"#; + pub(crate) const PTR2INT_ARRAY: &str = r#" ///| extern "wasm" fn mbt_ffi_ptr2int_array(ptr : Int, len : Int) -> FixedArray[Int] = @@ -186,6 +219,15 @@ extern "wasm" fn mbt_ffi_ptr2int_array(ptr : Int, len : Int) -> FixedArray[Int] #| local.get 0) "#; +pub(crate) const PTR2INT16_ARRAY: &str = r#" +///| +extern "wasm" fn mbt_ffi_ptr2int16_array(ptr : Int, len : Int) -> FixedArray[Int16] = + #|(func (param i32) (param i32) (result i32) + #| local.get 0 + #| local.get 1 call $moonbit.init_array16 + #| local.get 0) +"#; + pub(crate) const PTR2FLOAT_ARRAY: &str = r#" ///| extern "wasm" fn mbt_ffi_ptr2float_array(ptr : Int, len : Int) -> FixedArray[Float] = diff --git a/crates/moonbit/src/lib.rs b/crates/moonbit/src/lib.rs index 78d1a0dbf..13165841e 100644 --- a/crates/moonbit/src/lib.rs +++ b/crates/moonbit/src/lib.rs @@ -2,7 +2,7 @@ use anyhow::Result; use core::panic; use heck::{ToShoutySnakeCase, ToUpperCamelCase}; use std::{ - collections::{HashMap, HashSet}, + collections::{BTreeMap, HashMap, HashSet}, fmt::Write, mem, ops::Deref, @@ -11,7 +11,7 @@ use wit_bindgen_core::{ AsyncFilterSet, Direction, Files, InterfaceGenerator as CoreInterfaceGenerator, Ns, Source, WorldGenerator, abi::{self, AbiVariant, Bindgen, Bitcast, Instruction, LiftLower, WasmType}, - uwrite, uwriteln, + dealias, uwrite, uwriteln, wit_parser::{ Alignment, ArchitectureSize, Docs, Enum, Flags, FlagsRepr, Function, Int, InterfaceId, LiftLowerAbi, LiveTypes, ManglingAndAbi, Param, Record, Resolve, ResourceIntrinsic, @@ -29,10 +29,88 @@ mod async_support; mod ffi; mod pkg; +pub(crate) fn is_list_canonical(resolve: &Resolve, element: &Type) -> bool { + match element { + Type::Bool + | Type::U8 + | Type::U16 + | Type::U32 + | Type::U64 + | Type::S16 + | Type::S32 + | Type::S64 + | Type::F32 + | Type::F64 => true, + Type::Id(id) => match &resolve.types[dealias(resolve, *id)].kind { + TypeDefKind::Type(element) => is_list_canonical(resolve, element), + _ => false, + }, + _ => false, + } +} + +fn collect_direct_canonical_lists( + resolve: &Resolve, + ty: &Type, + expression: String, + wasm_param: usize, + expressions: &mut HashSet, + wasm_params: &mut BTreeMap, +) { + let Type::Id(id) = ty else { + return; + }; + match &resolve.types[*id].kind { + TypeDefKind::Type(ty) => collect_direct_canonical_lists( + resolve, + ty, + expression, + wasm_param, + expressions, + wasm_params, + ), + TypeDefKind::List(element) => { + if is_list_canonical(resolve, element) { + expressions.insert(expression); + wasm_params.insert(wasm_param, *element); + } + } + TypeDefKind::Record(record) => { + let mut field_wasm_param = wasm_param; + for field in &record.fields { + collect_direct_canonical_lists( + resolve, + &field.ty, + format!("({expression}).{}", field.name.to_moonbit_ident()), + field_wasm_param, + expressions, + wasm_params, + ); + field_wasm_param += abi::flat_types(resolve, &field.ty, None).unwrap().len(); + } + } + TypeDefKind::Tuple(tuple) => { + let mut field_wasm_param = wasm_param; + for (index, field) in tuple.types.iter().enumerate() { + collect_direct_canonical_lists( + resolve, + field, + format!("({expression}).{index}"), + field_wasm_param, + expressions, + wasm_params, + ); + field_wasm_param += abi::flat_types(resolve, field, None).unwrap().len(); + } + } + _ => {} + } +} + // Assumptions: -// - Data: u8 -> Byte, s8 | s16 | s32 -> Int, u16 | u32 -> UInt, s64 -> Int64, u64 -> UInt64, f32 | f64 -> Double, address -> Int +// - Data: u8 -> Byte, s8 | s32 -> Int, s16 -> Int16, u16 -> UInt16, u32 -> UInt, s64 -> Int64, u64 -> UInt64, f32 -> Float, f64 -> Double, address -> Int // - Encoding: UTF16 -// - Lift/Lower list: T == Int/UInt/Int64/UInt64/Float/Double -> FixedArray[T], T == Byte -> Bytes, T == Char -> String +// - Lift/Lower list: representation-compatible elements -> FixedArray[T], otherwise Array[T] // Organization: // - one package per interface (export and import are treated as different interfaces) // - ffi utils are under `./ffi`, and the project entrance (package as link target) is under `./gen` @@ -607,6 +685,22 @@ impl InterfaceGenerator<'_> { let endpoint_plan = self.import_async_function_plan(self.interface, func); let wasm_sig = self.resolve.wasm_signature(variant, func); let mbt_sig = self.world_gen.pkg_resolver.mbt_sig(self.name, func, false); + let mut direct_canonical_lists = HashSet::new(); + let mut direct_canonical_wasm_params = BTreeMap::new(); + if !async_plan.is_async() && !wasm_sig.indirect_params { + let mut wasm_param = 0; + for Param { name, ty, .. } in &func.params { + collect_direct_canonical_lists( + self.resolve, + ty, + name.to_moonbit_ident(), + wasm_param, + &mut direct_canonical_lists, + &mut direct_canonical_wasm_params, + ); + wasm_param += abi::flat_types(self.resolve, ty, None).unwrap().len(); + } + } let (src, needs_cleanup_list, endpoint_state) = if async_plan.is_async() { let body = self.generate_async_import_body(&endpoint_plan, func, &mbt_sig, &wasm_sig); (body.src, body.needs_cleanup_list, body.state) @@ -618,6 +712,13 @@ impl InterfaceGenerator<'_> { .map(|Param { name, .. }| name.to_moonbit_ident()) .collect(), ) + .with_direct_canonical_list_params( + direct_canonical_lists, + direct_canonical_wasm_params + .keys() + .map(|param| param + 1) + .collect(), + ) .with_async_state(endpoint_plan.state()); if endpoint_plan.has_endpoints() { bindgen = bindgen.with_sync_import_commit( @@ -658,14 +759,35 @@ impl InterfaceGenerator<'_> { .params .iter() .enumerate() - .map(|(i, param)| format!("p{i} : {}", wasm_type(*param))) + .map(|(i, param)| { + if let Some(element) = direct_canonical_wasm_params.get(&i) { + let element = self.world_gen.pkg_resolver.type_name(self.name, element); + format!("p{i} : FixedArray[{element}]") + } else if i > 0 && direct_canonical_wasm_params.contains_key(&(i - 1)) { + format!("p{i}? : Int = p{}.length()", i - 1) + } else { + format!("p{i} : {}", wasm_type(*param)) + } + }) .collect::>() .join(", "); + let direct_borrows = direct_canonical_wasm_params + .keys() + .map(|i| format!("p{i}")) + .collect::>(); + let direct_borrow_attributes = if direct_borrows.is_empty() { + String::new() + } else { + format!( + "#unsafe_skip_stub_check\n#borrow({})\n", + direct_borrows.join(", ") + ) + }; let ffi_import_name = format!("wasmImport{}", func.name.to_upper_camel_case()); uwriteln!( self.ffi, r#" - fn {ffi_import_name}({params}) {result_type} = "{import_module}" "{import_name}" + {direct_borrow_attributes}fn {ffi_import_name}({params}) {result_type} = "{import_module}" "{import_name}" "# ); @@ -1415,6 +1537,8 @@ struct FunctionBindgen<'a, 'b> { sync_endpoint_drop: bool, commit_endpoints: bool, sync_import_argument_types: Option>, + direct_canonical_list_params: HashSet, + direct_canonical_list_length_params: HashSet, async_state: AsyncFunctionState, } @@ -1445,10 +1569,22 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> { sync_endpoint_drop: false, commit_endpoints: false, sync_import_argument_types: None, + direct_canonical_list_params: HashSet::new(), + direct_canonical_list_length_params: HashSet::new(), async_state: AsyncFunctionState::default(), } } + fn with_direct_canonical_list_params( + mut self, + params: HashSet, + length_params: HashSet, + ) -> Self { + self.direct_canonical_list_params = params; + self.direct_canonical_list_length_params = length_params; + self + } + fn lower_variant( &mut self, cases: &[(&str, Option)], @@ -1640,7 +1776,7 @@ impl Bindgen for FunctionBindgen<'_, '_> { fn emit( &mut self, - _resolve: &Resolve, + resolve: &Resolve, inst: &Instruction<'_>, operands: &mut Vec, results: &mut Vec, @@ -1683,9 +1819,7 @@ impl Bindgen for FunctionBindgen<'_, '_> { Instruction::I32FromChar => results.push(format!("({}).to_int()", operands[0])), Instruction::I32FromU8 => results.push(format!("({}).to_int()", operands[0])), - Instruction::I32FromU16 => { - results.push(format!("({}).reinterpret_as_int()", operands[0])) - } + Instruction::I32FromU16 => results.push(format!("({}).to_int()", operands[0])), Instruction::U8FromI32 => results.push(format!("({}).to_byte()", operands[0])), Instruction::I32FromS8 => { @@ -1693,15 +1827,9 @@ impl Bindgen for FunctionBindgen<'_, '_> { results.push(format!("mbt_ffi_extend8({})", operands[0])) } Instruction::S8FromI32 => results.push(format!("({} - 0x100)", operands[0])), - Instruction::S16FromI32 => results.push(format!("({} - 0x10000)", operands[0])), - Instruction::I32FromS16 => { - self.use_ffi(ffi::EXTEND16); - results.push(format!("mbt_ffi_extend16({})", operands[0])) - } - Instruction::U16FromI32 => results.push(format!( - "({}.land(0xFFFF).reinterpret_as_uint())", - operands[0] - )), + Instruction::S16FromI32 => results.push(format!("Int16::from_int({})", operands[0])), + Instruction::I32FromS16 => results.push(format!("({}).to_int()", operands[0])), + Instruction::U16FromI32 => results.push(format!("({}).to_uint16()", operands[0])), Instruction::U32FromI32 => { results.push(format!("({}).reinterpret_as_uint()", operands[0])) } @@ -2006,128 +2134,172 @@ impl Bindgen for FunctionBindgen<'_, '_> { operands[0] )), - Instruction::ListCanonLower { element, realloc } => match element { - Type::U8 => { - let op = &operands[0]; - let ptr = self.locals.tmp("ptr"); - self.use_ffi(ffi::BYTES2PTR); - uwriteln!( - self.src, - " + Instruction::ListCanonLower { element, realloc } => { + let element: &Type = element; + let element = match element { + Type::Id(id) => match &resolve.types[dealias(resolve, *id)].kind { + TypeDefKind::Type(element) => element, + _ => unreachable!("unsupported list element type"), + }, + _ => element, + }; + match element { + Type::U8 => { + let op = &operands[0]; + if realloc.is_none() && self.direct_canonical_list_params.contains(op) { + results.push(op.clone()); + results.push(format!("{op}.length()")); + return; + } + let ptr = self.locals.tmp("ptr"); + self.use_ffi(ffi::BYTES2PTR); + uwriteln!( + self.src, + " let {ptr} = mbt_ffi_bytes2ptr({op}) ", - ); - results.push(ptr.clone()); - results.push(format!("{op}.length()")); - if realloc.is_none() { - self.cleanup.push(Cleanup { address: ptr }); - } - } - Type::U32 | Type::U64 | Type::S32 | Type::S64 | Type::F32 | Type::F64 => { - let op = &operands[0]; - let ptr = self.locals.tmp("ptr"); - let ty = match element { - Type::U32 => { - self.use_ffi(ffi::UINT_ARRAY2PTR); - "uint" - } - Type::U64 => { - self.use_ffi(ffi::UINT64_ARRAY2PTR); - "uint64" - } - Type::S32 => { - self.use_ffi(ffi::INT_ARRAY2PTR); - "int" + ); + results.push(ptr.clone()); + results.push(format!("{op}.length()")); + if realloc.is_none() { + self.cleanup.push(Cleanup { address: ptr }); } - Type::S64 => { - self.use_ffi(ffi::INT64_ARRAY2PTR); - "int64" - } - Type::F32 => { - self.use_ffi(ffi::FLOAT_ARRAY2PTR); - "float" - } - Type::F64 => { - self.use_ffi(ffi::DOUBLE_ARRAY2PTR); - "double" + } + Type::Bool + | Type::U16 + | Type::U32 + | Type::U64 + | Type::S16 + | Type::S32 + | Type::S64 + | Type::F32 + | Type::F64 => { + let op = &operands[0]; + if realloc.is_none() && self.direct_canonical_list_params.contains(op) { + results.push(op.clone()); + results.push(format!("{op}.length()")); + return; } - _ => unreachable!(), - }; - - uwriteln!( - self.src, - " + let ptr = self.locals.tmp("ptr"); + let (owned_ffi, ty) = match element { + Type::Bool => (ffi::BOOL_ARRAY2PTR, "bool"), + Type::U16 => (ffi::UINT16_ARRAY2PTR, "uint16"), + Type::U32 => (ffi::UINT_ARRAY2PTR, "uint"), + Type::U64 => (ffi::UINT64_ARRAY2PTR, "uint64"), + Type::S16 => (ffi::INT16_ARRAY2PTR, "int16"), + Type::S32 => (ffi::INT_ARRAY2PTR, "int"), + Type::S64 => (ffi::INT64_ARRAY2PTR, "int64"), + Type::F32 => (ffi::FLOAT_ARRAY2PTR, "float"), + Type::F64 => (ffi::DOUBLE_ARRAY2PTR, "double"), + _ => unreachable!(), + }; + self.use_ffi(owned_ffi); + + uwriteln!( + self.src, + " let {ptr} = mbt_ffi_{ty}_array2ptr({op}) ", - ); - results.push(ptr.clone()); - results.push(format!("{op}.length()")); - if realloc.is_none() { - self.cleanup.push(Cleanup { address: ptr }); + ); + results.push(ptr.clone()); + results.push(format!("{op}.length()")); + if realloc.is_none() { + self.cleanup.push(Cleanup { address: ptr }); + } } + _ => unreachable!("unsupported list element type"), } - _ => unreachable!("unsupported list element type"), - }, + } - Instruction::ListCanonLift { element, .. } => match element { - Type::U8 => { - let result = self.locals.tmp("result"); - let address = &operands[0]; - let length = &operands[1]; - self.use_ffi(ffi::PTR2BYTES); - uwrite!( - self.src, - " + Instruction::ListCanonLift { element, .. } => { + let element: &Type = element; + let element = match element { + Type::Id(id) => match &resolve.types[dealias(resolve, *id)].kind { + TypeDefKind::Type(element) => element, + _ => unreachable!("unsupported list element type"), + }, + _ => element, + }; + match element { + Type::U8 => { + let result = self.locals.tmp("result"); + let address = &operands[0]; + let length = &operands[1]; + self.use_ffi(ffi::PTR2BYTES); + uwrite!( + self.src, + " let {result} = mbt_ffi_ptr2bytes({address}, {length}) ", - ); + ); - results.push(result); - } - Type::U32 | Type::U64 | Type::S32 | Type::S64 | Type::F32 | Type::F64 => { - let ty = match element { - Type::U32 => { - self.use_ffi(ffi::PTR2UINT_ARRAY); - "uint" - } - Type::U64 => { - self.use_ffi(ffi::PTR2UINT64_ARRAY); - "uint64" - } - Type::S32 => { - self.use_ffi(ffi::PTR2INT_ARRAY); - "int" - } - Type::S64 => { - self.use_ffi(ffi::PTR2INT64_ARRAY); - "int64" - } - Type::F32 => { - self.use_ffi(ffi::PTR2FLOAT_ARRAY); - "float" - } - Type::F64 => { - self.use_ffi(ffi::PTR2DOUBLE_ARRAY); - "double" - } - _ => unreachable!(), - }; + results.push(result); + } + Type::Bool + | Type::U16 + | Type::U32 + | Type::U64 + | Type::S16 + | Type::S32 + | Type::S64 + | Type::F32 + | Type::F64 => { + let ty = match element { + Type::Bool => { + self.use_ffi(ffi::PTR2BOOL_ARRAY); + "bool" + } + Type::U16 => { + self.use_ffi(ffi::PTR2UINT16_ARRAY); + "uint16" + } + Type::U32 => { + self.use_ffi(ffi::PTR2UINT_ARRAY); + "uint" + } + Type::U64 => { + self.use_ffi(ffi::PTR2UINT64_ARRAY); + "uint64" + } + Type::S16 => { + self.use_ffi(ffi::PTR2INT16_ARRAY); + "int16" + } + Type::S32 => { + self.use_ffi(ffi::PTR2INT_ARRAY); + "int" + } + Type::S64 => { + self.use_ffi(ffi::PTR2INT64_ARRAY); + "int64" + } + Type::F32 => { + self.use_ffi(ffi::PTR2FLOAT_ARRAY); + "float" + } + Type::F64 => { + self.use_ffi(ffi::PTR2DOUBLE_ARRAY); + "double" + } + _ => unreachable!(), + }; - let result = self.locals.tmp("result"); - let address = &operands[0]; - let length = &operands[1]; + let result = self.locals.tmp("result"); + let address = &operands[0]; + let length = &operands[1]; - uwrite!( - self.src, - " + uwrite!( + self.src, + " let {result} = mbt_ffi_ptr2{ty}_array({address}, {length}) ", - ); + ); - results.push(result); + results.push(result); + } + _ => unreachable!("unsupported list element type"), } - _ => unreachable!("unsupported list element type"), - }, + } Instruction::StringLower { realloc } => { let op = &operands[0]; @@ -2281,7 +2453,15 @@ impl Bindgen for FunctionBindgen<'_, '_> { } else { operands.clone() }; - let arguments = call_operands.join(", "); + let arguments = call_operands + .iter() + .enumerate() + .filter_map(|(i, operand)| { + (!self.direct_canonical_list_length_params.contains(&i)) + .then_some(operand.as_str()) + }) + .collect::>() + .join(", "); // TODO: handle this to support async functions uwriteln!(self.src, "{assignment} wasmImport{func_name}({arguments});"); self.commit_sync_import_arguments(sig, &call_operands); @@ -2894,11 +3074,8 @@ impl Bindgen for FunctionBindgen<'_, '_> { &self.interface_gen.world_gen.sizes } - fn is_list_canonical(&self, _resolve: &Resolve, element: &Type) -> bool { - matches!( - element, - Type::U8 | Type::U32 | Type::U64 | Type::S32 | Type::S64 | Type::F32 | Type::F64 - ) + fn is_list_canonical(&self, resolve: &Resolve, element: &Type) -> bool { + crate::is_list_canonical(resolve, element) } } @@ -3515,6 +3692,138 @@ mod tests { assert!(!sink.contains("FixedArray::makei")); } + #[test] + fn sixteen_bit_integers_use_width_preserving_moonbit_types() { + let files = generate( + r#" + package a:b; + + interface api { + roundtrip: func(unsigned: u16, signed: s16) -> tuple; + } + + world client { import api; } + "#, + "client", + ); + let top = file(&files, "interface/a/b/api/top.mbt"); + + assert!( + top.contains("pub fn roundtrip(unsigned : UInt16, signed : Int16) -> (UInt16, Int16)"), + "u16 and s16 must preserve their public element widths: {top}" + ); + } + + #[test] + fn imported_canonical_lists_borrow_backing_storage_until_the_call_returns() { + let files = generate( + r#" + package a:b; + + interface api { + type unsigned-short = u16; + type signed-short = s16; + record envelope { + words: list, + } + send: func( + bytes: list, + unsigned-shorts: list, + signed-shorts: list, + words: list, + envelope: envelope, + booleans: list, + ); + } + + world client { import api; } + "#, + "client", + ); + let top = file(&files, "interface/a/b/api/top.mbt"); + let ffi = file(&files, "interface/a/b/api/ffi.mbt"); + + assert!( + top.contains( + "wasmImportSend(bytes, unsigned_shorts, signed_shorts, words, (envelope).words, booleans" + ), + "direct canonical lists must let the FFI supply their lengths: {top}" + ); + assert!( + !top.contains("mbt_ffi_borrowed_array2ptr(bytes)") + && !top.contains("mbt_ffi_borrowed_array2ptr(unsigned_shorts)") + && !top.contains("mbt_ffi_borrowed_array2ptr(signed_shorts)") + && !top.contains("mbt_ffi_borrowed_array2ptr(words)"), + "direct list parameters must not round-trip through Int: {top}" + ); + assert!( + !top.contains("bytes.length()") + && !top.contains("unsigned_shorts.length()") + && !top.contains("signed_shorts.length()") + && !top.contains("words.length()") + && !top.contains("(envelope).words.length()") + && !top.contains("booleans.length()"), + "direct canonical list lengths must be supplied by FFI defaults: {top}" + ); + assert!(!top.contains("mbt_ffi_borrowed_array2ptr"), "{top}"); + assert!( + ffi.contains( + "#unsafe_skip_stub_check\n#borrow(p0, p2, p4, p6, p8, p10)\nfn wasmImportSend(\ + p0 : FixedArray[Byte], p1? : Int = p0.length(), \ + p2 : FixedArray[UInt16], p3? : Int = p2.length(), \ + p4 : FixedArray[Int16], p5? : Int = p4.length(), \ + p6 : FixedArray[UInt], p7? : Int = p6.length(), \ + p8 : FixedArray[UInt], p9? : Int = p8.length(), \ + p10 : FixedArray[Bool], p11? : Int = p10.length())" + ), + "the imported FFI must derive borrowed array lengths by default: {ffi}" + ); + assert!( + !ffi.contains("mbt_ffi_borrowed_array2ptr"), + "direct list parameters must not need an array-to-pointer helper: {ffi}" + ); + assert!(!ffi.contains("mbt_ffi_borrowed_bytes2ptr"), "{ffi}"); + assert!(!ffi.contains("mbt_ffi_borrowed_uint_array2ptr"), "{ffi}"); + assert!( + !top.contains("mbt_ffi_free(ptr)\n"), + "borrowed canonical backing storage must not be freed after the call: {top}" + ); + assert!( + !top.contains("mbt_ffi_malloc((booleans).length() * 1)") + && !top.contains("for index = 0; index < (booleans).length();"), + "boolean arrays have canonical byte storage and must be borrowed directly: {top}" + ); + } + + #[test] + fn imported_noncanonical_lists_retain_generic_lowering() { + let files = generate( + r#" + package a:b; + + interface api { + send: func(strings: list); + } + + world client { import api; } + "#, + "client", + ); + let top = file(&files, "interface/a/b/api/top.mbt"); + let ffi = file(&files, "interface/a/b/api/ffi.mbt"); + + assert!( + top.contains("pub fn send(strings : Array[String]) -> Unit") + && top.contains("mbt_ffi_malloc((strings).length() * 8)") + && top.contains("for index = 0; index < (strings).length();"), + "non-canonical lists must retain generic lowering: {top}" + ); + assert!( + ffi.contains("fn wasmImportSend(p0 : Int, p1 : Int)") && !ffi.contains("#borrow"), + "non-canonical list parameters must retain the raw canonical ABI: {ffi}" + ); + } + #[test] fn async_export_background_group_name_is_deconflicted() { let files = generate( diff --git a/crates/moonbit/src/pkg.rs b/crates/moonbit/src/pkg.rs index b98343f23..68149ac06 100644 --- a/crates/moonbit/src/pkg.rs +++ b/crates/moonbit/src/pkg.rs @@ -176,8 +176,10 @@ impl PkgResolver { match ty { Type::Bool => "Bool".into(), Type::U8 => "Byte".into(), - Type::S32 | Type::S8 | Type::S16 => "Int".into(), - Type::U16 | Type::U32 => "UInt".into(), + Type::S8 | Type::S32 => "Int".into(), + Type::S16 => "Int16".into(), + Type::U16 => "UInt16".into(), + Type::U32 => "UInt".into(), Type::Char => "Char".into(), Type::U64 => "UInt64".into(), Type::S64 => "Int64".into(), @@ -189,18 +191,13 @@ impl PkgResolver { let ty = self.resolve.types[dealias(&self.resolve, *id)].clone(); match ty.kind { TypeDefKind::Type(ty) => self.type_name(this, &ty), - TypeDefKind::List(ty) => match ty { - Type::U8 - | Type::U32 - | Type::U64 - | Type::S32 - | Type::S64 - | Type::F32 - | Type::F64 => { + TypeDefKind::List(ty) => { + if crate::is_list_canonical(&self.resolve, &ty) { format!("FixedArray[{}]", self.type_name(this, &ty)) + } else { + format!("Array[{}]", self.type_name(this, &ty)) } - _ => format!("Array[{}]", self.type_name(this, &ty)), - }, + } TypeDefKind::FixedLengthList(ty, _size) => { format!("FixedArray[{}]", self.type_name(this, &ty)) } diff --git a/tests/runtime/flavorful/test.mbt b/tests/runtime/flavorful/test.mbt index ba178359a..9e36357a5 100644 --- a/tests/runtime/flavorful/test.mbt +++ b/tests/runtime/flavorful/test.mbt @@ -127,10 +127,10 @@ pub fn list_typedefs( ///| pub fn list_of_variants( - _a : Array[Bool], + _a : FixedArray[Bool], _b : Array[Result[Unit, Unit]], _c : Array[MyErrno], -) -> (Array[Bool], Array[Result[Unit, Unit]], Array[MyErrno]) { +) -> (FixedArray[Bool], Array[Result[Unit, Unit]], Array[MyErrno]) { try { assert_eq(_a, [true, false]) assert_eq(_b, [Ok(()), Err(())]) diff --git a/tests/runtime/list-in-variant/runner.mbt b/tests/runtime/list-in-variant/runner.mbt index 24228b707..80e4af18d 100644 --- a/tests/runtime/list-in-variant/runner.mbt +++ b/tests/runtime/list-in-variant/runner.mbt @@ -33,4 +33,25 @@ pub fn run() -> Unit { // top-level-list (contrast) let r7 = @to-test.top_level_list(["x", "y", "z"]) guard r7 == "x,y,z" + + // Canonical primitive lists borrow their MoonBit backing arrays through the + // call, including when nested under another parameter. + let bytes : FixedArray[Byte] = [1, 2, 3, 4] + let unsigned_shorts : FixedArray[UInt16] = [100, 200] + let signed_shorts : FixedArray[Int16] = [-10, 20] + let words : FixedArray[UInt] = [10, 20, 30] + let nested = @to-test.CanonicalLists::{ + bytes: [5, 6, 7], + unsigned_shorts: [300, 400], + signed_shorts: [-30, 40], + booleans: [false, true, false], + } + let r8 = @to-test.canonical_list_params( + bytes, + unsigned_shorts, + signed_shorts, + words, + nested, + ) + guard r8 == 172U } diff --git a/tests/runtime/list-in-variant/test.rs b/tests/runtime/list-in-variant/test.rs index e94b51dd6..8d0f24a79 100644 --- a/tests/runtime/list-in-variant/test.rs +++ b/tests/runtime/list-in-variant/test.rs @@ -44,4 +44,22 @@ impl exports::test::list_in_variant::to_test::Guest for Component { fn top_level_list(items: Vec) -> String { items.join(",") } + + fn canonical_list_params( + bytes: Vec, + unsigned_shorts: Vec, + signed_shorts: Vec, + words: Vec, + nested: CanonicalLists, + ) -> u32 { + assert_eq!(bytes, [1, 2, 3, 4]); + assert_eq!(unsigned_shorts, [100, 200]); + assert_eq!(signed_shorts, [-10, 20]); + assert_eq!(words, [10, 20, 30]); + assert_eq!(nested.bytes, [5, 6, 7]); + assert_eq!(nested.unsigned_shorts, [300, 400]); + assert_eq!(nested.signed_shorts, [-30, 40]); + assert_eq!(nested.booleans, [false, true, false]); + 172 + } } diff --git a/tests/runtime/list-in-variant/test.wit b/tests/runtime/list-in-variant/test.wit index da156e5d1..e3159cafe 100644 --- a/tests/runtime/list-in-variant/test.wit +++ b/tests/runtime/list-in-variant/test.wit @@ -18,6 +18,20 @@ interface to-test { list-in-option-with-return: func(data: option>) -> summary; top-level-list: func(items: list) -> string; + + record canonical-lists { + bytes: list, + unsigned-shorts: list, + signed-shorts: list, + booleans: list, + } + canonical-list-params: func( + bytes: list, + unsigned-shorts: list, + signed-shorts: list, + words: list, + nested: canonical-lists, + ) -> u32; } world test { diff --git a/tests/runtime/lists/test.mbt b/tests/runtime/lists/test.mbt index a4c19f926..2e1deacb0 100644 --- a/tests/runtime/lists/test.mbt +++ b/tests/runtime/lists/test.mbt @@ -84,9 +84,9 @@ pub fn list_minmax8( ///| pub fn list_minmax16( - a : Array[UInt], - b : Array[Int] -) -> (Array[UInt], Array[Int]) { + a : FixedArray[UInt16], + b : FixedArray[Int16] +) -> (FixedArray[UInt16], FixedArray[Int16]) { (a, b) } diff --git a/tests/runtime/moonbit/http-background-hook/runner.mbt b/tests/runtime/moonbit/http-background-hook/runner.mbt index 7fe4bf046..6e22689cc 100644 --- a/tests/runtime/moonbit/http-background-hook/runner.mbt +++ b/tests/runtime/moonbit/http-background-hook/runner.mbt @@ -26,7 +26,7 @@ async fn run_hook(payload : Bytes, expected_error : String?) -> Unit { Ok(response) => response Err(_) => panic() } - guard response.get_status_code() == 202U else { panic() } + guard response.get_status_code() == 202 else { panic() } let (response_body, response_trailers) = response.consume_body( @async-core.Future::ready(Ok(())), diff --git a/tests/runtime/moonbit/http-background-hook/test.mbt b/tests/runtime/moonbit/http-background-hook/test.mbt index 8e490d36b..654012bef 100644 --- a/tests/runtime/moonbit/http-background-hook/test.mbt +++ b/tests/runtime/moonbit/http-background-hook/test.mbt @@ -56,7 +56,7 @@ pub async fn handle( Some(response_body), response_trailers, ) - guard response.set_status_code(202U) is Ok(_) else { + guard response.set_status_code(202) is Ok(_) else { let error = @types.ErrorCode::InternalError( Some("failed to set hook response status"), ) diff --git a/tests/runtime/numbers/test.mbt b/tests/runtime/numbers/test.mbt index bd1679c7b..da798716d 100644 --- a/tests/runtime/numbers/test.mbt +++ b/tests/runtime/numbers/test.mbt @@ -12,12 +12,12 @@ pub fn roundtrip_s8(a : Int) -> Int { } ///| -pub fn roundtrip_u16(a : UInt) -> UInt { +pub fn roundtrip_u16(a : UInt16) -> UInt16 { a } ///| -pub fn roundtrip_s16(a : Int) -> Int { +pub fn roundtrip_s16(a : Int16) -> Int16 { a }