Skip to content

Commit 0f8e6c5

Browse files
claudehyperpolymath
authored andcommitted
fix(codegen): support nested patterns inside tuple patterns (WASM)
The core-Wasm backend rejected any tuple sub-pattern that wasn't a plain variable or wildcard (UnsupportedFeature "Only variable and wildcard patterns supported in tuple patterns") — which stdlib/option.affine and result.affine hit. gen_pattern now recurses per tuple element: each element is loaded into a temp local and matched against its sub-pattern, with the per-element test bools ANDed together. Every gen_pattern result is one-bool-net with net-zero binds, so the combination is stack-safe; binds register via the threaded ctx (the same mechanism constructor-argument patterns use). Verified under node: `match (a,b) { (0,y)=>y, (x,0)=>x+100, (x,y)=>x+y }` selects the right arm and binds correctly — (0,5)->5, (7,0)->107, (3,4)->7. option.affine / result.affine now get PAST this gap (they next hit a separate `panic`-builtin gap in the Wasm backend, tracked in #607). dune test 461 green; adds a Wasm nested-tuple regression test. Part of #607. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lz7pRcec2Z3tVtaAhvB3M8
1 parent a3e0661 commit 0f8e6c5

2 files changed

Lines changed: 69 additions & 29 deletions

File tree

lib/codegen.ml

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2281,37 +2281,41 @@ and gen_pattern (ctx : context) (scrutinee_local : int) (pat : pattern)
22812281
Ok (ctx_final, full_code, [])
22822282

22832283
| PatTuple sub_patterns ->
2284-
(* Tuple pattern: (a, b, c) *)
2285-
(* scrutinee is a pointer to [elem0: i32][elem1: i32][elem2: i32]... *)
2286-
2287-
(* Bind each element to sub-pattern *)
2288-
let rec bind_elements ctx_acc offset patterns =
2284+
(* Tuple pattern: (p0, p1, ...). scrutinee is a pointer to
2285+
[elem0: i32][elem1: i32]... Each element is loaded into a temp local
2286+
and matched RECURSIVELY against its sub-pattern, so nested patterns
2287+
(literals, constructors, tuples) work — not just var/wildcard.
2288+
2289+
Stack discipline: every [gen_pattern] result leaves exactly one bool
2290+
(net) and its binds are net-zero (see the PatVar/PatLit/PatCon cases),
2291+
so each element's test bool ANDs cleanly with the accumulator. The
2292+
per-element load is itself net-zero (LocalGet +1, I32Load 0, LocalSet
2293+
-1), so it never disturbs the accumulated bool. Binds register in the
2294+
threaded ctx via [alloc_local] (same mechanism PatCon args use), so the
2295+
returned binding list stays [] and the arm body resolves names via ctx. *)
2296+
let rec match_elements ctx_acc idx patterns ~first =
22892297
match patterns with
2290-
| [] -> Ok (ctx_acc, [])
2298+
| [] ->
2299+
(* Empty tuple, or no elements left: an empty match is vacuously true
2300+
only when it is the whole pattern (first); otherwise the caller has
2301+
already left the accumulated bool on the stack. *)
2302+
if first then Ok (ctx_acc, [I32Const 1l]) else Ok (ctx_acc, [])
22912303
| pat :: rest ->
2292-
begin match pat with
2293-
| PatVar id ->
2294-
(* Allocate local for this element *)
2295-
let (ctx', elem_idx) = alloc_local ctx_acc id.name in
2296-
(* Load element from tuple *)
2297-
let load_code = [
2298-
LocalGet scrutinee_local;
2299-
I32Load (2, offset);
2300-
LocalSet elem_idx;
2301-
] in
2302-
let* (ctx_final, rest_code) = bind_elements ctx' (offset + 4) rest in
2303-
Ok (ctx_final, load_code @ rest_code)
2304-
| PatWildcard _ ->
2305-
(* Skip this element *)
2306-
bind_elements ctx_acc (offset + 4) rest
2307-
| _ ->
2308-
Error (UnsupportedFeature "Only variable and wildcard patterns supported in tuple patterns")
2309-
end
2304+
let (ctx1, elem_tmp) =
2305+
alloc_local ctx_acc (Printf.sprintf "__tuple_elem_%d" idx) in
2306+
let load_elem = [
2307+
LocalGet scrutinee_local;
2308+
I32Load (2, idx * 4);
2309+
LocalSet elem_tmp;
2310+
] in
2311+
let* (ctx2, sub_test, _sub_binds) = gen_pattern ctx1 elem_tmp pat in
2312+
(* For every element after the first, AND its bool with the running
2313+
accumulator already on the stack. *)
2314+
let combine = if first then [] else [I32And] in
2315+
let* (ctx3, rest_code) = match_elements ctx2 (idx + 1) rest ~first:false in
2316+
Ok (ctx3, load_elem @ sub_test @ combine @ rest_code)
23102317
in
2311-
2312-
let* (ctx_final, binding_code) = bind_elements ctx 0 sub_patterns in
2313-
(* Tuple patterns always match (no tag to check) *)
2314-
let match_code = binding_code @ [I32Const 1l] in
2318+
let* (ctx_final, match_code) = match_elements ctx 0 sub_patterns ~first:true in
23152319
Ok (ctx_final, match_code, [])
23162320

23172321
| PatRecord (field_pats, _has_wildcard) ->

test/test_stdlib_aot.ml

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,8 +305,44 @@ let dup_ctor_tests =
305305
Alcotest.test_case "declared Option does not duplicate preamble ctor (JS)"
306306
`Quick test_js_no_duplicate_option_ctor ]
307307

308+
(* ---- WASM: nested patterns inside a tuple pattern --------------------------
309+
310+
The core-Wasm backend previously rejected any tuple sub-pattern that wasn't
311+
a plain variable or wildcard (`UnsupportedFeature "Only variable and
312+
wildcard patterns supported in tuple patterns"` — what stdlib/option.affine
313+
hit). gen_pattern now recurses per element, so literals/constructors/nested
314+
tuples work. This asserts such a program reaches a Wasm module; runtime
315+
correctness (correct arm selection + binding) is verified under node in the
316+
PR's manual check. *)
317+
let nested_tuple_src = {|
318+
module nested_tuple;
319+
pub fn classify(a: Int, b: Int) -> Int {
320+
let t = (a, b);
321+
match t {
322+
(0, y) => y,
323+
(x, 0) => x + 100,
324+
(x, y) => x + y,
325+
}
326+
}
327+
|}
328+
329+
let test_nested_tuple_patterns_wasm () =
330+
match Parse_driver.parse_string ~file:"<nested_tuple>" nested_tuple_src with
331+
| exception e ->
332+
Alcotest.failf "nested-tuple parse raised: %s" (Printexc.to_string e)
333+
| prog ->
334+
(match pipeline_to_wasm prog with
335+
| Ok _ -> ()
336+
| Error m ->
337+
Alcotest.failf "nested tuple patterns must codegen to Wasm: %s" m)
338+
339+
let tuple_pattern_tests =
340+
[ Alcotest.test_case "nested (literal/var) tuple patterns -> Wasm" `Quick
341+
test_nested_tuple_patterns_wasm ]
342+
308343
let tests =
309344
[ ("STAGE-A AOT smoke (#136)", aot_smoke_tests);
310345
("STAGE-A multi-module integration (#137)", integration_tests);
311346
("cross-module constructor linking, Wasm (#138)", xmod_constructor_tests);
312-
("Deno-ESM / JS no duplicate Option/Result constructor", dup_ctor_tests) ]
347+
("Deno-ESM / JS no duplicate Option/Result constructor", dup_ctor_tests);
348+
("Wasm nested tuple patterns", tuple_pattern_tests) ]

0 commit comments

Comments
 (0)