Skip to content

Commit dedc483

Browse files
refactor(moonbit): derive list borrows from lowering
1 parent baba2f0 commit dedc483

1 file changed

Lines changed: 127 additions & 139 deletions

File tree

crates/moonbit/src/lib.rs

Lines changed: 127 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -49,64 +49,6 @@ pub(crate) fn is_list_canonical(resolve: &Resolve, element: &Type) -> bool {
4949
}
5050
}
5151

52-
fn collect_direct_canonical_lists(
53-
resolve: &Resolve,
54-
ty: &Type,
55-
expression: String,
56-
wasm_param: usize,
57-
expressions: &mut HashSet<String>,
58-
wasm_params: &mut BTreeMap<usize, Type>,
59-
) {
60-
let Type::Id(id) = ty else {
61-
return;
62-
};
63-
match &resolve.types[*id].kind {
64-
TypeDefKind::Type(ty) => collect_direct_canonical_lists(
65-
resolve,
66-
ty,
67-
expression,
68-
wasm_param,
69-
expressions,
70-
wasm_params,
71-
),
72-
TypeDefKind::List(element) => {
73-
if is_list_canonical(resolve, element) {
74-
expressions.insert(expression);
75-
wasm_params.insert(wasm_param, *element);
76-
}
77-
}
78-
TypeDefKind::Record(record) => {
79-
let mut field_wasm_param = wasm_param;
80-
for field in &record.fields {
81-
collect_direct_canonical_lists(
82-
resolve,
83-
&field.ty,
84-
format!("({expression}).{}", field.name.to_moonbit_ident()),
85-
field_wasm_param,
86-
expressions,
87-
wasm_params,
88-
);
89-
field_wasm_param += abi::flat_types(resolve, &field.ty, None).unwrap().len();
90-
}
91-
}
92-
TypeDefKind::Tuple(tuple) => {
93-
let mut field_wasm_param = wasm_param;
94-
for (index, field) in tuple.types.iter().enumerate() {
95-
collect_direct_canonical_lists(
96-
resolve,
97-
field,
98-
format!("({expression}).{index}"),
99-
field_wasm_param,
100-
expressions,
101-
wasm_params,
102-
);
103-
field_wasm_param += abi::flat_types(resolve, field, None).unwrap().len();
104-
}
105-
}
106-
_ => {}
107-
}
108-
}
109-
11052
// Assumptions:
11153
// - Data: u8 -> Byte, s8 | s32 -> Int, s16 -> Int16, u16 -> UInt16, u32 -> UInt, s64 -> Int64, u64 -> UInt64, f32 -> Float, f64 -> Double, address -> Int
11254
// - Encoding: UTF16
@@ -685,25 +627,16 @@ impl InterfaceGenerator<'_> {
685627
let endpoint_plan = self.import_async_function_plan(self.interface, func);
686628
let wasm_sig = self.resolve.wasm_signature(variant, func);
687629
let mbt_sig = self.world_gen.pkg_resolver.mbt_sig(self.name, func, false);
688-
let mut direct_canonical_lists = HashSet::new();
689-
let mut direct_canonical_wasm_params = BTreeMap::new();
690-
if !async_plan.is_async() && !wasm_sig.indirect_params {
691-
let mut wasm_param = 0;
692-
for Param { name, ty, .. } in &func.params {
693-
collect_direct_canonical_lists(
694-
self.resolve,
695-
ty,
696-
name.to_moonbit_ident(),
697-
wasm_param,
698-
&mut direct_canonical_lists,
699-
&mut direct_canonical_wasm_params,
700-
);
701-
wasm_param += abi::flat_types(self.resolve, ty, None).unwrap().len();
702-
}
703-
}
704-
let (src, needs_cleanup_list, endpoint_state) = if async_plan.is_async() {
630+
let (src, needs_cleanup_list, endpoint_state, direct_canonical_wasm_params) = if async_plan
631+
.is_async()
632+
{
705633
let body = self.generate_async_import_body(&endpoint_plan, func, &mbt_sig, &wasm_sig);
706-
(body.src, body.needs_cleanup_list, body.state)
634+
(
635+
body.src,
636+
body.needs_cleanup_list,
637+
body.state,
638+
BTreeMap::new(),
639+
)
707640
} else {
708641
let mut bindgen = FunctionBindgen::new(
709642
self,
@@ -712,13 +645,7 @@ impl InterfaceGenerator<'_> {
712645
.map(|Param { name, .. }| name.to_moonbit_ident())
713646
.collect(),
714647
)
715-
.with_direct_canonical_list_params(
716-
direct_canonical_lists,
717-
direct_canonical_wasm_params
718-
.keys()
719-
.map(|param| param + 1)
720-
.collect(),
721-
)
648+
.with_direct_list_borrows(!wasm_sig.indirect_params)
722649
.with_async_state(endpoint_plan.state());
723650
if endpoint_plan.has_endpoints() {
724651
bindgen = bindgen.with_sync_import_commit(
@@ -734,7 +661,12 @@ impl InterfaceGenerator<'_> {
734661
&mut bindgen,
735662
false,
736663
);
737-
(bindgen.src, bindgen.needs_cleanup_list, bindgen.async_state)
664+
(
665+
bindgen.src,
666+
bindgen.needs_cleanup_list,
667+
bindgen.async_state,
668+
bindgen.direct_canonical_wasm_params,
669+
)
738670
};
739671

740672
let cleanup_list = if needs_cleanup_list {
@@ -763,8 +695,6 @@ impl InterfaceGenerator<'_> {
763695
if let Some(element) = direct_canonical_wasm_params.get(&i) {
764696
let element = self.world_gen.pkg_resolver.type_name(self.name, element);
765697
format!("p{i} : FixedArray[{element}]")
766-
} else if i > 0 && direct_canonical_wasm_params.contains_key(&(i - 1)) {
767-
format!("p{i}? : Int = p{}.length()", i - 1)
768698
} else {
769699
format!("p{i} : {}", wasm_type(*param))
770700
}
@@ -1537,8 +1467,9 @@ struct FunctionBindgen<'a, 'b> {
15371467
sync_endpoint_drop: bool,
15381468
commit_endpoints: bool,
15391469
sync_import_argument_types: Option<Vec<Type>>,
1540-
direct_canonical_list_params: HashSet<String>,
1541-
direct_canonical_list_length_params: HashSet<usize>,
1470+
direct_list_borrows: bool,
1471+
direct_list_borrow_candidates: HashMap<String, (String, Type)>,
1472+
direct_canonical_wasm_params: BTreeMap<usize, Type>,
15421473
async_state: AsyncFunctionState,
15431474
}
15441475

@@ -1569,19 +1500,15 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> {
15691500
sync_endpoint_drop: false,
15701501
commit_endpoints: false,
15711502
sync_import_argument_types: None,
1572-
direct_canonical_list_params: HashSet::new(),
1573-
direct_canonical_list_length_params: HashSet::new(),
1503+
direct_list_borrows: false,
1504+
direct_list_borrow_candidates: HashMap::new(),
1505+
direct_canonical_wasm_params: BTreeMap::new(),
15741506
async_state: AsyncFunctionState::default(),
15751507
}
15761508
}
15771509

1578-
fn with_direct_canonical_list_params(
1579-
mut self,
1580-
params: HashSet<String>,
1581-
length_params: HashSet<usize>,
1582-
) -> Self {
1583-
self.direct_canonical_list_params = params;
1584-
self.direct_canonical_list_length_params = length_params;
1510+
fn with_direct_list_borrows(mut self, enabled: bool) -> Self {
1511+
self.direct_list_borrows = enabled;
15851512
self
15861513
}
15871514

@@ -2135,22 +2062,29 @@ impl Bindgen for FunctionBindgen<'_, '_> {
21352062
)),
21362063

21372064
Instruction::ListCanonLower { element, realloc } => {
2138-
let element: &Type = element;
2139-
let element = match element {
2065+
let original_element: &Type = element;
2066+
let element = match original_element {
21402067
Type::Id(id) => match &resolve.types[dealias(resolve, *id)].kind {
21412068
TypeDefKind::Type(element) => element,
21422069
_ => unreachable!("unsupported list element type"),
21432070
},
2144-
_ => element,
2071+
_ => original_element,
21452072
};
2073+
let op = &operands[0];
2074+
// A canonical list can be passed as a typed borrow only when its
2075+
// pointer and length flow directly to the Wasm call. Lists lowered
2076+
// inside blocks are merged into variants or written into enclosing
2077+
// list storage, so those still need their integer pointer.
2078+
if self.direct_list_borrows && realloc.is_none() && self.block_storage.is_empty() {
2079+
let length = format!("{op}.length()");
2080+
self.direct_list_borrow_candidates
2081+
.insert(op.clone(), (length.clone(), *original_element));
2082+
results.push(op.clone());
2083+
results.push(length);
2084+
return;
2085+
}
21462086
match element {
21472087
Type::U8 => {
2148-
let op = &operands[0];
2149-
if realloc.is_none() && self.direct_canonical_list_params.contains(op) {
2150-
results.push(op.clone());
2151-
results.push(format!("{op}.length()"));
2152-
return;
2153-
}
21542088
let ptr = self.locals.tmp("ptr");
21552089
self.use_ffi(ffi::BYTES2PTR);
21562090
uwriteln!(
@@ -2174,12 +2108,6 @@ impl Bindgen for FunctionBindgen<'_, '_> {
21742108
| Type::S64
21752109
| Type::F32
21762110
| Type::F64 => {
2177-
let op = &operands[0];
2178-
if realloc.is_none() && self.direct_canonical_list_params.contains(op) {
2179-
results.push(op.clone());
2180-
results.push(format!("{op}.length()"));
2181-
return;
2182-
}
21832111
let ptr = self.locals.tmp("ptr");
21842112
let (owned_ffi, ty) = match element {
21852113
Type::Bool => (ffi::BOOL_ARRAY2PTR, "bool"),
@@ -2441,6 +2369,13 @@ impl Bindgen for FunctionBindgen<'_, '_> {
24412369
};
24422370

24432371
let func_name = name.to_upper_camel_case();
2372+
for (index, operand) in operands.iter().enumerate() {
2373+
if let Some((length, element)) = self.direct_list_borrow_candidates.get(operand)
2374+
{
2375+
assert_eq!(operands.get(index + 1), Some(length));
2376+
self.direct_canonical_wasm_params.insert(index, *element);
2377+
}
2378+
}
24442379
let call_operands = if self.sync_import_argument_types.is_some() {
24452380
operands
24462381
.iter()
@@ -2453,15 +2388,7 @@ impl Bindgen for FunctionBindgen<'_, '_> {
24532388
} else {
24542389
operands.clone()
24552390
};
2456-
let arguments = call_operands
2457-
.iter()
2458-
.enumerate()
2459-
.filter_map(|(i, operand)| {
2460-
(!self.direct_canonical_list_length_params.contains(&i))
2461-
.then_some(operand.as_str())
2462-
})
2463-
.collect::<Vec<_>>()
2464-
.join(", ");
2391+
let arguments = call_operands.join(", ");
24652392
// TODO: handle this to support async functions
24662393
uwriteln!(self.src, "{assignment} wasmImport{func_name}({arguments});");
24672394
self.commit_sync_import_arguments(sig, &call_operands);
@@ -3745,9 +3672,11 @@ mod tests {
37453672

37463673
assert!(
37473674
top.contains(
3748-
"wasmImportSend(bytes, unsigned_shorts, signed_shorts, words, (envelope).words, booleans"
3675+
"wasmImportSend(bytes, bytes.length(), unsigned_shorts, unsigned_shorts.length(), \
3676+
signed_shorts, signed_shorts.length(), words, words.length(), \
3677+
(envelope).words, (envelope).words.length(), booleans, booleans.length())"
37493678
),
3750-
"direct canonical lists must let the FFI supply their lengths: {top}"
3679+
"direct canonical lists must retain their canonical pointer/length operands: {top}"
37513680
);
37523681
assert!(
37533682
!top.contains("mbt_ffi_borrowed_array2ptr(bytes)")
@@ -3756,27 +3685,18 @@ mod tests {
37563685
&& !top.contains("mbt_ffi_borrowed_array2ptr(words)"),
37573686
"direct list parameters must not round-trip through Int: {top}"
37583687
);
3759-
assert!(
3760-
!top.contains("bytes.length()")
3761-
&& !top.contains("unsigned_shorts.length()")
3762-
&& !top.contains("signed_shorts.length()")
3763-
&& !top.contains("words.length()")
3764-
&& !top.contains("(envelope).words.length()")
3765-
&& !top.contains("booleans.length()"),
3766-
"direct canonical list lengths must be supplied by FFI defaults: {top}"
3767-
);
37683688
assert!(!top.contains("mbt_ffi_borrowed_array2ptr"), "{top}");
37693689
assert!(
37703690
ffi.contains(
37713691
"#unsafe_skip_stub_check\n#borrow(p0, p2, p4, p6, p8, p10)\nfn wasmImportSend(\
3772-
p0 : FixedArray[Byte], p1? : Int = p0.length(), \
3773-
p2 : FixedArray[UInt16], p3? : Int = p2.length(), \
3774-
p4 : FixedArray[Int16], p5? : Int = p4.length(), \
3775-
p6 : FixedArray[UInt], p7? : Int = p6.length(), \
3776-
p8 : FixedArray[UInt], p9? : Int = p8.length(), \
3777-
p10 : FixedArray[Bool], p11? : Int = p10.length())"
3692+
p0 : FixedArray[Byte], p1 : Int, \
3693+
p2 : FixedArray[UInt16], p3 : Int, \
3694+
p4 : FixedArray[Int16], p5 : Int, \
3695+
p6 : FixedArray[UInt], p7 : Int, \
3696+
p8 : FixedArray[UInt], p9 : Int, \
3697+
p10 : FixedArray[Bool], p11 : Int)"
37783698
),
3779-
"the imported FFI must derive borrowed array lengths by default: {ffi}"
3699+
"the imported FFI must borrow arrays while retaining explicit lengths: {ffi}"
37803700
);
37813701
assert!(
37823702
!ffi.contains("mbt_ffi_borrowed_array2ptr"),
@@ -3824,6 +3744,74 @@ mod tests {
38243744
);
38253745
}
38263746

3747+
#[test]
3748+
fn imported_conditional_canonical_lists_retain_pointer_lowering() {
3749+
let files = generate(
3750+
r#"
3751+
package a:b;
3752+
3753+
interface api {
3754+
variant optional-bytes {
3755+
none,
3756+
some(list<u8>),
3757+
}
3758+
send: func(bytes: optional-bytes);
3759+
}
3760+
3761+
world client { import api; }
3762+
"#,
3763+
"client",
3764+
);
3765+
let top = file(&files, "interface/a/b/api/top.mbt");
3766+
let ffi = file(&files, "interface/a/b/api/ffi.mbt");
3767+
3768+
assert!(
3769+
top.contains("mbt_ffi_bytes2ptr"),
3770+
"conditional list pointers must be lowered before merging variant cases: {top}"
3771+
);
3772+
assert!(
3773+
!ffi.contains("#borrow"),
3774+
"a conditionally occupied pointer parameter cannot be a typed borrow: {ffi}"
3775+
);
3776+
}
3777+
3778+
#[test]
3779+
fn imported_indirect_canonical_lists_retain_pointer_lowering() {
3780+
let files = generate(
3781+
r#"
3782+
package a:b;
3783+
3784+
interface api {
3785+
send: func(
3786+
a: list<u8>,
3787+
b: list<u8>,
3788+
c: list<u8>,
3789+
d: list<u8>,
3790+
e: list<u8>,
3791+
f: list<u8>,
3792+
g: list<u8>,
3793+
h: list<u8>,
3794+
i: list<u8>,
3795+
);
3796+
}
3797+
3798+
world client { import api; }
3799+
"#,
3800+
"client",
3801+
);
3802+
let top = file(&files, "interface/a/b/api/top.mbt");
3803+
let ffi = file(&files, "interface/a/b/api/ffi.mbt");
3804+
3805+
assert!(
3806+
top.contains("mbt_ffi_bytes2ptr(a)"),
3807+
"lists stored in an indirect argument area still need integer pointers: {top}"
3808+
);
3809+
assert!(
3810+
ffi.contains("fn wasmImportSend(p0 : Int)") && !ffi.contains("#borrow"),
3811+
"an indirect import receives only its argument-area pointer: {ffi}"
3812+
);
3813+
}
3814+
38273815
#[test]
38283816
fn async_export_background_group_name_is_deconflicted() {
38293817
let files = generate(

0 commit comments

Comments
 (0)