From b0ac0ed87f5231c71aba817ff4f8c8cc69e309a6 Mon Sep 17 00:00:00 2001 From: zihang Date: Mon, 27 Jul 2026 17:04:58 +0800 Subject: [PATCH 1/4] perf(moonbit): borrow canonical list arguments --- crates/moonbit/src/ffi.rs | 38 +- crates/moonbit/src/lib.rs | 524 +++++++++++++----- crates/moonbit/src/pkg.rs | 23 +- tests/runtime/list-in-variant/runner.mbt | 21 + tests/runtime/list-in-variant/test.rs | 18 + tests/runtime/list-in-variant/test.wit | 14 + tests/runtime/lists/test.mbt | 6 +- .../moonbit/http-background-hook/runner.mbt | 2 +- .../moonbit/http-background-hook/test.mbt | 2 +- tests/runtime/numbers/test.mbt | 4 +- 10 files changed, 498 insertions(+), 154 deletions(-) diff --git a/crates/moonbit/src/ffi.rs b/crates/moonbit/src/ffi.rs index 63c9daa41..900258bf8 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 = @@ -133,6 +127,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 +148,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) @@ -177,6 +185,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 +203,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..8af70e58a 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, @@ -29,10 +29,116 @@ mod async_support; mod ffi; mod pkg; +#[derive(Clone, Copy)] +pub(crate) enum CanonicalListElement { + U8, + U16, + U32, + U64, + S16, + S32, + S64, + F32, + F64, +} + +impl CanonicalListElement { + pub(crate) fn fixed_array_type(self) -> &'static str { + match self { + Self::U8 => "FixedArray[Byte]", + Self::U16 => "FixedArray[UInt16]", + Self::U32 => "FixedArray[UInt]", + Self::U64 => "FixedArray[UInt64]", + Self::S16 => "FixedArray[Int16]", + Self::S32 => "FixedArray[Int]", + Self::S64 => "FixedArray[Int64]", + Self::F32 => "FixedArray[Float]", + Self::F64 => "FixedArray[Double]", + } + } +} + +pub(crate) fn canonical_list_element(resolve: &Resolve, ty: &Type) -> Option { + match ty { + Type::Id(id) => match &resolve.types[*id].kind { + TypeDefKind::Type(ty) => canonical_list_element(resolve, ty), + _ => None, + }, + Type::U8 => Some(CanonicalListElement::U8), + Type::U16 => Some(CanonicalListElement::U16), + Type::U32 => Some(CanonicalListElement::U32), + Type::U64 => Some(CanonicalListElement::U64), + Type::S16 => Some(CanonicalListElement::S16), + Type::S32 => Some(CanonicalListElement::S32), + Type::S64 => Some(CanonicalListElement::S64), + Type::F32 => Some(CanonicalListElement::F32), + Type::F64 => Some(CanonicalListElement::F64), + _ => None, + } +} + +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 let Some(element) = canonical_list_element(resolve, element) { + expressions.insert(expression); + wasm_params.insert(wasm_param, element.fixed_array_type()); + } + } + 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: canonically represented numeric 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 +713,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 +740,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 +787,34 @@ impl InterfaceGenerator<'_> { .params .iter() .enumerate() - .map(|(i, param)| format!("p{i} : {}", wasm_type(*param))) + .map(|(i, param)| { + if let Some(array_type) = direct_canonical_wasm_params.get(&i) { + format!("p{i} : {array_type}") + } 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 +1564,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 +1596,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 +1803,7 @@ impl Bindgen for FunctionBindgen<'_, '_> { fn emit( &mut self, - _resolve: &Resolve, + resolve: &Resolve, inst: &Instruction<'_>, operands: &mut Vec, results: &mut Vec, @@ -1683,9 +1846,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 +1854,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 +2161,135 @@ 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 = canonical_list_element(resolve, element).unwrap(); + match element { + CanonicalListElement::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" - } - Type::S64 => { - self.use_ffi(ffi::INT64_ARRAY2PTR); - "int64" - } - Type::F32 => { - self.use_ffi(ffi::FLOAT_ARRAY2PTR); - "float" + ); + results.push(ptr.clone()); + results.push(format!("{op}.length()")); + if realloc.is_none() { + self.cleanup.push(Cleanup { address: ptr }); } - Type::F64 => { - self.use_ffi(ffi::DOUBLE_ARRAY2PTR); - "double" + } + element => { + 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 { + CanonicalListElement::U16 => (ffi::UINT16_ARRAY2PTR, "uint16"), + CanonicalListElement::U32 => (ffi::UINT_ARRAY2PTR, "uint"), + CanonicalListElement::U64 => (ffi::UINT64_ARRAY2PTR, "uint64"), + CanonicalListElement::S16 => (ffi::INT16_ARRAY2PTR, "int16"), + CanonicalListElement::S32 => (ffi::INT_ARRAY2PTR, "int"), + CanonicalListElement::S64 => (ffi::INT64_ARRAY2PTR, "int64"), + CanonicalListElement::F32 => (ffi::FLOAT_ARRAY2PTR, "float"), + CanonicalListElement::F64 => (ffi::DOUBLE_ARRAY2PTR, "double"), + CanonicalListElement::U8 => 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"), - }, + } - 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 = canonical_list_element(resolve, element).unwrap(); + match element { + CanonicalListElement::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); + } + element => { + let ty = match element { + CanonicalListElement::U16 => { + self.use_ffi(ffi::PTR2UINT16_ARRAY); + "uint16" + } + CanonicalListElement::U32 => { + self.use_ffi(ffi::PTR2UINT_ARRAY); + "uint" + } + CanonicalListElement::U64 => { + self.use_ffi(ffi::PTR2UINT64_ARRAY); + "uint64" + } + CanonicalListElement::S16 => { + self.use_ffi(ffi::PTR2INT16_ARRAY); + "int16" + } + CanonicalListElement::S32 => { + self.use_ffi(ffi::PTR2INT_ARRAY); + "int" + } + CanonicalListElement::S64 => { + self.use_ffi(ffi::PTR2INT64_ARRAY); + "int64" + } + CanonicalListElement::F32 => { + self.use_ffi(ffi::PTR2FLOAT_ARRAY); + "float" + } + CanonicalListElement::F64 => { + self.use_ffi(ffi::PTR2DOUBLE_ARRAY); + "double" + } + CanonicalListElement::U8 => 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"), - }, + } Instruction::StringLower { realloc } => { let op = &operands[0]; @@ -2281,7 +2443,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 +3064,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 { + canonical_list_element(resolve, element).is_some() } } @@ -3515,6 +3682,107 @@ 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," + ), + "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()"), + "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)\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()," + ), + "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();"), + "non-canonical lists must retain generic lowering: {top}" + ); + } + #[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..747b7cb28 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 => { - format!("FixedArray[{}]", self.type_name(this, &ty)) + TypeDefKind::List(ty) => { + if let Some(element) = crate::canonical_list_element(&self.resolve, &ty) { + element.fixed_array_type().to_string() + } 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/list-in-variant/runner.mbt b/tests/runtime/list-in-variant/runner.mbt index 24228b707..5b12aa4a0 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], + words: [40, 50], + } + 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..cfba42394 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.words, [40, 50]); + 172 + } } diff --git a/tests/runtime/list-in-variant/test.wit b/tests/runtime/list-in-variant/test.wit index da156e5d1..8a1b987a7 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, + words: 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 } From 5bb09f4e3ce90ccbbd3cfadac4a27f01ca8f83eb Mon Sep 17 00:00:00 2001 From: zihang Date: Tue, 28 Jul 2026 18:24:27 +0800 Subject: [PATCH 2/4] perf(moonbit): treat boolean lists as canonical arrays --- crates/moonbit/src/ffi.rs | 16 +++++++ crates/moonbit/src/lib.rs | 53 ++++++++++++++++++++---- tests/runtime/flavorful/test.mbt | 4 +- tests/runtime/list-in-variant/runner.mbt | 2 +- tests/runtime/list-in-variant/test.rs | 2 +- tests/runtime/list-in-variant/test.wit | 2 +- 6 files changed, 67 insertions(+), 12 deletions(-) diff --git a/crates/moonbit/src/ffi.rs b/crates/moonbit/src/ffi.rs index 900258bf8..72811d55e 100644 --- a/crates/moonbit/src/ffi.rs +++ b/crates/moonbit/src/ffi.rs @@ -120,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) @@ -176,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] = diff --git a/crates/moonbit/src/lib.rs b/crates/moonbit/src/lib.rs index 8af70e58a..3c3decfb6 100644 --- a/crates/moonbit/src/lib.rs +++ b/crates/moonbit/src/lib.rs @@ -31,6 +31,7 @@ mod pkg; #[derive(Clone, Copy)] pub(crate) enum CanonicalListElement { + Bool, U8, U16, U32, @@ -45,6 +46,7 @@ pub(crate) enum CanonicalListElement { impl CanonicalListElement { pub(crate) fn fixed_array_type(self) -> &'static str { match self { + Self::Bool => "FixedArray[Bool]", Self::U8 => "FixedArray[Byte]", Self::U16 => "FixedArray[UInt16]", Self::U32 => "FixedArray[UInt]", @@ -64,6 +66,7 @@ pub(crate) fn canonical_list_element(resolve: &Resolve, ty: &Type) -> Option canonical_list_element(resolve, ty), _ => None, }, + Type::Bool => Some(CanonicalListElement::Bool), Type::U8 => Some(CanonicalListElement::U8), Type::U16 => Some(CanonicalListElement::U16), Type::U32 => Some(CanonicalListElement::U32), @@ -138,7 +141,7 @@ fn collect_direct_canonical_lists( // Assumptions: // - 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: canonically represented numeric elements -> FixedArray[T], otherwise Array[T] +// - 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` @@ -2194,6 +2197,7 @@ impl Bindgen for FunctionBindgen<'_, '_> { } let ptr = self.locals.tmp("ptr"); let (owned_ffi, ty) = match element { + CanonicalListElement::Bool => (ffi::BOOL_ARRAY2PTR, "bool"), CanonicalListElement::U16 => (ffi::UINT16_ARRAY2PTR, "uint16"), CanonicalListElement::U32 => (ffi::UINT_ARRAY2PTR, "uint"), CanonicalListElement::U64 => (ffi::UINT64_ARRAY2PTR, "uint64"), @@ -2240,6 +2244,10 @@ impl Bindgen for FunctionBindgen<'_, '_> { } element => { let ty = match element { + CanonicalListElement::Bool => { + self.use_ffi(ffi::PTR2BOOL_ARRAY); + "bool" + } CanonicalListElement::U16 => { self.use_ffi(ffi::PTR2UINT16_ARRAY); "uint16" @@ -3735,7 +3743,7 @@ mod tests { assert!( top.contains( - "wasmImportSend(bytes, unsigned_shorts, signed_shorts, words, (envelope).words," + "wasmImportSend(bytes, unsigned_shorts, signed_shorts, words, (envelope).words, booleans" ), "direct canonical lists must let the FFI supply their lengths: {top}" ); @@ -3751,18 +3759,20 @@ mod tests { && !top.contains("unsigned_shorts.length()") && !top.contains("signed_shorts.length()") && !top.contains("words.length()") - && !top.contains("(envelope).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)\nfn wasmImportSend(\ + "#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()," + 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}" ); @@ -3777,10 +3787,39 @@ mod tests { "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();"), + !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] 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 5b12aa4a0..80e4af18d 100644 --- a/tests/runtime/list-in-variant/runner.mbt +++ b/tests/runtime/list-in-variant/runner.mbt @@ -44,7 +44,7 @@ pub fn run() -> Unit { bytes: [5, 6, 7], unsigned_shorts: [300, 400], signed_shorts: [-30, 40], - words: [40, 50], + booleans: [false, true, false], } let r8 = @to-test.canonical_list_params( bytes, diff --git a/tests/runtime/list-in-variant/test.rs b/tests/runtime/list-in-variant/test.rs index cfba42394..8d0f24a79 100644 --- a/tests/runtime/list-in-variant/test.rs +++ b/tests/runtime/list-in-variant/test.rs @@ -59,7 +59,7 @@ impl exports::test::list_in_variant::to_test::Guest for Component { assert_eq!(nested.bytes, [5, 6, 7]); assert_eq!(nested.unsigned_shorts, [300, 400]); assert_eq!(nested.signed_shorts, [-30, 40]); - assert_eq!(nested.words, [40, 50]); + 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 8a1b987a7..e3159cafe 100644 --- a/tests/runtime/list-in-variant/test.wit +++ b/tests/runtime/list-in-variant/test.wit @@ -23,7 +23,7 @@ interface to-test { bytes: list, unsigned-shorts: list, signed-shorts: list, - words: list, + booleans: list, } canonical-list-params: func( bytes: list, From baba2f045494379db90bd7b6dabf10d14428cc23 Mon Sep 17 00:00:00 2001 From: zihang Date: Wed, 29 Jul 2026 11:28:43 +0800 Subject: [PATCH 3/4] refactor(moonbit): reuse WIT types for canonical lists --- crates/moonbit/src/lib.rs | 162 +++++++++++++++++++------------------- crates/moonbit/src/pkg.rs | 4 +- 2 files changed, 84 insertions(+), 82 deletions(-) diff --git a/crates/moonbit/src/lib.rs b/crates/moonbit/src/lib.rs index 3c3decfb6..13165841e 100644 --- a/crates/moonbit/src/lib.rs +++ b/crates/moonbit/src/lib.rs @@ -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,54 +29,23 @@ mod async_support; mod ffi; mod pkg; -#[derive(Clone, Copy)] -pub(crate) enum CanonicalListElement { - Bool, - U8, - U16, - U32, - U64, - S16, - S32, - S64, - F32, - F64, -} - -impl CanonicalListElement { - pub(crate) fn fixed_array_type(self) -> &'static str { - match self { - Self::Bool => "FixedArray[Bool]", - Self::U8 => "FixedArray[Byte]", - Self::U16 => "FixedArray[UInt16]", - Self::U32 => "FixedArray[UInt]", - Self::U64 => "FixedArray[UInt64]", - Self::S16 => "FixedArray[Int16]", - Self::S32 => "FixedArray[Int]", - Self::S64 => "FixedArray[Int64]", - Self::F32 => "FixedArray[Float]", - Self::F64 => "FixedArray[Double]", - } - } -} - -pub(crate) fn canonical_list_element(resolve: &Resolve, ty: &Type) -> Option { - match ty { - Type::Id(id) => match &resolve.types[*id].kind { - TypeDefKind::Type(ty) => canonical_list_element(resolve, ty), - _ => None, +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, }, - Type::Bool => Some(CanonicalListElement::Bool), - Type::U8 => Some(CanonicalListElement::U8), - Type::U16 => Some(CanonicalListElement::U16), - Type::U32 => Some(CanonicalListElement::U32), - Type::U64 => Some(CanonicalListElement::U64), - Type::S16 => Some(CanonicalListElement::S16), - Type::S32 => Some(CanonicalListElement::S32), - Type::S64 => Some(CanonicalListElement::S64), - Type::F32 => Some(CanonicalListElement::F32), - Type::F64 => Some(CanonicalListElement::F64), - _ => None, + _ => false, } } @@ -86,7 +55,7 @@ fn collect_direct_canonical_lists( expression: String, wasm_param: usize, expressions: &mut HashSet, - wasm_params: &mut BTreeMap, + wasm_params: &mut BTreeMap, ) { let Type::Id(id) = ty else { return; @@ -101,9 +70,9 @@ fn collect_direct_canonical_lists( wasm_params, ), TypeDefKind::List(element) => { - if let Some(element) = canonical_list_element(resolve, element) { + if is_list_canonical(resolve, element) { expressions.insert(expression); - wasm_params.insert(wasm_param, element.fixed_array_type()); + wasm_params.insert(wasm_param, *element); } } TypeDefKind::Record(record) => { @@ -791,8 +760,9 @@ impl InterfaceGenerator<'_> { .iter() .enumerate() .map(|(i, param)| { - if let Some(array_type) = direct_canonical_wasm_params.get(&i) { - format!("p{i} : {array_type}") + 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 { @@ -2165,9 +2135,16 @@ impl Bindgen for FunctionBindgen<'_, '_> { )), Instruction::ListCanonLower { element, realloc } => { - let element = canonical_list_element(resolve, element).unwrap(); + 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 { - CanonicalListElement::U8 => { + Type::U8 => { let op = &operands[0]; if realloc.is_none() && self.direct_canonical_list_params.contains(op) { results.push(op.clone()); @@ -2188,7 +2165,15 @@ impl Bindgen for FunctionBindgen<'_, '_> { self.cleanup.push(Cleanup { address: ptr }); } } - element => { + 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()); @@ -2197,16 +2182,16 @@ impl Bindgen for FunctionBindgen<'_, '_> { } let ptr = self.locals.tmp("ptr"); let (owned_ffi, ty) = match element { - CanonicalListElement::Bool => (ffi::BOOL_ARRAY2PTR, "bool"), - CanonicalListElement::U16 => (ffi::UINT16_ARRAY2PTR, "uint16"), - CanonicalListElement::U32 => (ffi::UINT_ARRAY2PTR, "uint"), - CanonicalListElement::U64 => (ffi::UINT64_ARRAY2PTR, "uint64"), - CanonicalListElement::S16 => (ffi::INT16_ARRAY2PTR, "int16"), - CanonicalListElement::S32 => (ffi::INT_ARRAY2PTR, "int"), - CanonicalListElement::S64 => (ffi::INT64_ARRAY2PTR, "int64"), - CanonicalListElement::F32 => (ffi::FLOAT_ARRAY2PTR, "float"), - CanonicalListElement::F64 => (ffi::DOUBLE_ARRAY2PTR, "double"), - CanonicalListElement::U8 => unreachable!(), + 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); @@ -2222,13 +2207,21 @@ impl Bindgen for FunctionBindgen<'_, '_> { self.cleanup.push(Cleanup { address: ptr }); } } + _ => unreachable!("unsupported list element type"), } } Instruction::ListCanonLift { element, .. } => { - let element = canonical_list_element(resolve, element).unwrap(); + 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 { - CanonicalListElement::U8 => { + Type::U8 => { let result = self.locals.tmp("result"); let address = &operands[0]; let length = &operands[1]; @@ -2242,45 +2235,53 @@ impl Bindgen for FunctionBindgen<'_, '_> { results.push(result); } - element => { + Type::Bool + | Type::U16 + | Type::U32 + | Type::U64 + | Type::S16 + | Type::S32 + | Type::S64 + | Type::F32 + | Type::F64 => { let ty = match element { - CanonicalListElement::Bool => { + Type::Bool => { self.use_ffi(ffi::PTR2BOOL_ARRAY); "bool" } - CanonicalListElement::U16 => { + Type::U16 => { self.use_ffi(ffi::PTR2UINT16_ARRAY); "uint16" } - CanonicalListElement::U32 => { + Type::U32 => { self.use_ffi(ffi::PTR2UINT_ARRAY); "uint" } - CanonicalListElement::U64 => { + Type::U64 => { self.use_ffi(ffi::PTR2UINT64_ARRAY); "uint64" } - CanonicalListElement::S16 => { + Type::S16 => { self.use_ffi(ffi::PTR2INT16_ARRAY); "int16" } - CanonicalListElement::S32 => { + Type::S32 => { self.use_ffi(ffi::PTR2INT_ARRAY); "int" } - CanonicalListElement::S64 => { + Type::S64 => { self.use_ffi(ffi::PTR2INT64_ARRAY); "int64" } - CanonicalListElement::F32 => { + Type::F32 => { self.use_ffi(ffi::PTR2FLOAT_ARRAY); "float" } - CanonicalListElement::F64 => { + Type::F64 => { self.use_ffi(ffi::PTR2DOUBLE_ARRAY); "double" } - CanonicalListElement::U8 => unreachable!(), + _ => unreachable!(), }; let result = self.locals.tmp("result"); @@ -2296,6 +2297,7 @@ impl Bindgen for FunctionBindgen<'_, '_> { results.push(result); } + _ => unreachable!("unsupported list element type"), } } @@ -3073,7 +3075,7 @@ impl Bindgen for FunctionBindgen<'_, '_> { } fn is_list_canonical(&self, resolve: &Resolve, element: &Type) -> bool { - canonical_list_element(resolve, element).is_some() + crate::is_list_canonical(resolve, element) } } diff --git a/crates/moonbit/src/pkg.rs b/crates/moonbit/src/pkg.rs index 747b7cb28..68149ac06 100644 --- a/crates/moonbit/src/pkg.rs +++ b/crates/moonbit/src/pkg.rs @@ -192,8 +192,8 @@ impl PkgResolver { match ty.kind { TypeDefKind::Type(ty) => self.type_name(this, &ty), TypeDefKind::List(ty) => { - if let Some(element) = crate::canonical_list_element(&self.resolve, &ty) { - element.fixed_array_type().to_string() + if crate::is_list_canonical(&self.resolve, &ty) { + format!("FixedArray[{}]", self.type_name(this, &ty)) } else { format!("Array[{}]", self.type_name(this, &ty)) } From dedc483b3f4d5cc96e234514ef7fb3fd0637bf7b Mon Sep 17 00:00:00 2001 From: zihang Date: Wed, 29 Jul 2026 14:42:37 +0800 Subject: [PATCH 4/4] refactor(moonbit): derive list borrows from lowering --- crates/moonbit/src/lib.rs | 266 ++++++++++++++++++-------------------- 1 file changed, 127 insertions(+), 139 deletions(-) diff --git a/crates/moonbit/src/lib.rs b/crates/moonbit/src/lib.rs index 13165841e..4b39179f3 100644 --- a/crates/moonbit/src/lib.rs +++ b/crates/moonbit/src/lib.rs @@ -49,64 +49,6 @@ pub(crate) fn is_list_canonical(resolve: &Resolve, element: &Type) -> bool { } } -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 | s32 -> Int, s16 -> Int16, u16 -> UInt16, u32 -> UInt, s64 -> Int64, u64 -> UInt64, f32 -> Float, f64 -> Double, address -> Int // - Encoding: UTF16 @@ -685,25 +627,16 @@ 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 (src, needs_cleanup_list, endpoint_state, direct_canonical_wasm_params) = 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) + ( + body.src, + body.needs_cleanup_list, + body.state, + BTreeMap::new(), + ) } else { let mut bindgen = FunctionBindgen::new( self, @@ -712,13 +645,7 @@ 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_direct_list_borrows(!wasm_sig.indirect_params) .with_async_state(endpoint_plan.state()); if endpoint_plan.has_endpoints() { bindgen = bindgen.with_sync_import_commit( @@ -734,7 +661,12 @@ impl InterfaceGenerator<'_> { &mut bindgen, false, ); - (bindgen.src, bindgen.needs_cleanup_list, bindgen.async_state) + ( + bindgen.src, + bindgen.needs_cleanup_list, + bindgen.async_state, + bindgen.direct_canonical_wasm_params, + ) }; let cleanup_list = if needs_cleanup_list { @@ -763,8 +695,6 @@ impl InterfaceGenerator<'_> { 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)) } @@ -1537,8 +1467,9 @@ 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, + direct_list_borrows: bool, + direct_list_borrow_candidates: HashMap, + direct_canonical_wasm_params: BTreeMap, async_state: AsyncFunctionState, } @@ -1569,19 +1500,15 @@ 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(), + direct_list_borrows: false, + direct_list_borrow_candidates: HashMap::new(), + direct_canonical_wasm_params: BTreeMap::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; + fn with_direct_list_borrows(mut self, enabled: bool) -> Self { + self.direct_list_borrows = enabled; self } @@ -2135,22 +2062,29 @@ impl Bindgen for FunctionBindgen<'_, '_> { )), Instruction::ListCanonLower { element, realloc } => { - let element: &Type = element; - let element = match element { + let original_element: &Type = element; + let element = match original_element { Type::Id(id) => match &resolve.types[dealias(resolve, *id)].kind { TypeDefKind::Type(element) => element, _ => unreachable!("unsupported list element type"), }, - _ => element, + _ => original_element, }; + let op = &operands[0]; + // A canonical list can be passed as a typed borrow only when its + // pointer and length flow directly to the Wasm call. Lists lowered + // inside blocks are merged into variants or written into enclosing + // list storage, so those still need their integer pointer. + if self.direct_list_borrows && realloc.is_none() && self.block_storage.is_empty() { + let length = format!("{op}.length()"); + self.direct_list_borrow_candidates + .insert(op.clone(), (length.clone(), *original_element)); + results.push(op.clone()); + results.push(length); + return; + } 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!( @@ -2174,12 +2108,6 @@ impl Bindgen for FunctionBindgen<'_, '_> { | 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; - } let ptr = self.locals.tmp("ptr"); let (owned_ffi, ty) = match element { Type::Bool => (ffi::BOOL_ARRAY2PTR, "bool"), @@ -2441,6 +2369,13 @@ impl Bindgen for FunctionBindgen<'_, '_> { }; let func_name = name.to_upper_camel_case(); + for (index, operand) in operands.iter().enumerate() { + if let Some((length, element)) = self.direct_list_borrow_candidates.get(operand) + { + assert_eq!(operands.get(index + 1), Some(length)); + self.direct_canonical_wasm_params.insert(index, *element); + } + } let call_operands = if self.sync_import_argument_types.is_some() { operands .iter() @@ -2453,15 +2388,7 @@ impl Bindgen for FunctionBindgen<'_, '_> { } else { operands.clone() }; - 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(", "); + let arguments = call_operands.join(", "); // TODO: handle this to support async functions uwriteln!(self.src, "{assignment} wasmImport{func_name}({arguments});"); self.commit_sync_import_arguments(sig, &call_operands); @@ -3745,9 +3672,11 @@ mod tests { assert!( top.contains( - "wasmImportSend(bytes, unsigned_shorts, signed_shorts, words, (envelope).words, booleans" + "wasmImportSend(bytes, bytes.length(), unsigned_shorts, unsigned_shorts.length(), \ + signed_shorts, signed_shorts.length(), words, words.length(), \ + (envelope).words, (envelope).words.length(), booleans, booleans.length())" ), - "direct canonical lists must let the FFI supply their lengths: {top}" + "direct canonical lists must retain their canonical pointer/length operands: {top}" ); assert!( !top.contains("mbt_ffi_borrowed_array2ptr(bytes)") @@ -3756,27 +3685,18 @@ mod tests { && !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())" + p0 : FixedArray[Byte], p1 : Int, \ + p2 : FixedArray[UInt16], p3 : Int, \ + p4 : FixedArray[Int16], p5 : Int, \ + p6 : FixedArray[UInt], p7 : Int, \ + p8 : FixedArray[UInt], p9 : Int, \ + p10 : FixedArray[Bool], p11 : Int)" ), - "the imported FFI must derive borrowed array lengths by default: {ffi}" + "the imported FFI must borrow arrays while retaining explicit lengths: {ffi}" ); assert!( !ffi.contains("mbt_ffi_borrowed_array2ptr"), @@ -3824,6 +3744,74 @@ mod tests { ); } + #[test] + fn imported_conditional_canonical_lists_retain_pointer_lowering() { + let files = generate( + r#" + package a:b; + + interface api { + variant optional-bytes { + none, + some(list), + } + send: func(bytes: optional-bytes); + } + + 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("mbt_ffi_bytes2ptr"), + "conditional list pointers must be lowered before merging variant cases: {top}" + ); + assert!( + !ffi.contains("#borrow"), + "a conditionally occupied pointer parameter cannot be a typed borrow: {ffi}" + ); + } + + #[test] + fn imported_indirect_canonical_lists_retain_pointer_lowering() { + let files = generate( + r#" + package a:b; + + interface api { + send: func( + a: list, + b: list, + c: list, + d: list, + e: list, + f: list, + g: list, + h: list, + i: 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("mbt_ffi_bytes2ptr(a)"), + "lists stored in an indirect argument area still need integer pointers: {top}" + ); + assert!( + ffi.contains("fn wasmImportSend(p0 : Int)") && !ffi.contains("#borrow"), + "an indirect import receives only its argument-area pointer: {ffi}" + ); + } + #[test] fn async_export_background_group_name_is_deconflicted() { let files = generate(