plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
35af6cf
— Peter John
2026-07-23T17:55:25+05:30
feat: close closures/match/interpolation known gaps in codegen
- .recall/.capture.json +1 -1
- .recall/history.md +654 -0
- README.md +6 -12
- plum-checker/src/monomorphize.rs +66 -31
- plum-wasm-codegen/src/lib.rs +1180 -215
- plum-wasm-codegen/tests/codegen_tests.rs +425 -17
- plum-wasm-codegen/tests/examples_test.rs +6 -5
- tooling/tree-sitter-plum/src/scanner.c +28 -2
- tooling/tree-sitter-plum/test/corpus/function.txt +29 -0
.recall/.capture.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"505dc4d5-42ca-45dc-8df3-1f5e1b1c5bc6": 1886367, "78fe4b8b-6ba2-4a17-b415-0bf00e4a0f4d": 4168472, "61ed18b5-cab0-453c-9a65-dcb8cfc58361": 5702751, "4126b3b8-6d8e-4ca3-8898-fa4ab65a979c":
|
|
1
|
+
{"505dc4d5-42ca-45dc-8df3-1f5e1b1c5bc6": 1886367, "78fe4b8b-6ba2-4a17-b415-0bf00e4a0f4d": 4168472, "61ed18b5-cab0-453c-9a65-dcb8cfc58361": 5702751, "4126b3b8-6d8e-4ca3-8898-fa4ab65a979c": 5858356}
|
.recall/history.md
CHANGED
|
@@ -7749,3 +7749,657 @@ Now let's fix `else if`, which is a real bug — confirmed it's a single combine
|
|
|
7749
7749
|
If it's still showing as type-color in your actual running Helix session, that's very likely a **stale cache** — Helix compiles and caches the query set once per running instance, and picks up `highlights.scm` changes only on a full restart, not just closing/reopening the buffer or `:reload`. Fully quit Helix and reopen it (not just the file) and it should show correctly now — let me know if it still doesn't after that, since I'd want to dig further rather than assume.
|
|
7750
7750
|
|
|
7751
7751
|
Also fixed along the way: `tooling/tree-sitter-plum/Makefile`'s `make build`/`make corpus_test` called a bare `tree-sitter` binary that isn't installed globally on this machine — updated it to use `npx --yes tree-sitter-cli`, verified both targets now work and produce `grammars/plum.so`.
|
|
7752
|
+
|
|
7753
|
+
**You:** Ok great can you complete and finish the known gaps
|
|
7754
|
+
- `Bash: cd /Users/pyrossh/Code/plum grep -n -i "known gap" -A 40 README.md | head -80`
|
|
7755
|
+
- `Bash: cd /Users/pyrossh/Code/plum grep -n "^## Known gaps" README.md sed -n '/^## Known gaps/,/^## /p' README.md | head -150`
|
|
7756
|
+
- `Bash: cd /Users/pyrossh/Code/plum grep -n -i "known gaps" README.md wc -l README.md`
|
|
7757
|
+
- `Read: /Users/pyrossh/Code/plum/README.md`
|
|
7758
|
+
|
|
7759
|
+
**Claude:** This is a substantial compiler engineering task — let me first get a clear picture of the codegen internals so I can tackle each gap correctly rather than guessing.
|
|
7760
|
+
- `Agent: Survey codegen internals for known-gaps work`
|
|
7761
|
+
|
|
7762
|
+
**Claude:** That research turned up 7 fairly different problems — three are contained (Small/Medium), two are genuinely large and cross-cutting (real type-inference work, and scanner internals with real regression risk), and two are structurally invasive. Here's the shape of it:
|
|
7763
|
+
|
|
7764
|
+
| # | Gap | Difficulty |
|
|
7765
|
+
|---|-----|------------|
|
|
7766
|
+
| 6 | Named function as `fn(...)` value (`each(double)`) | Small–Medium — design already sketched in an old plan doc |
|
|
7767
|
+
| 2 | Multi-subject `match` | Medium |
|
|
7768
|
+
| 3 | Nested constructor patterns in `match` | Medium |
|
|
7769
|
+
| 4 | Multi-line closure body + closing `)` on the same line | Medium–Large — tree-sitter scanner surgery, regression-prone |
|
|
7770
|
+
| 1 | String interpolation codegen | Large — needs a new runtime string-concat/build subsystem that doesn't exist at all |
|
|
7771
|
+
| 5 | Closure-via-assignment `call_indirect` trap at Float/class types | Large — real fix lives in the type checker's non-unifying inference, not codegen |
|
|
7772
|
+
| 7 | Nested closures | Large — the whole closure-discovery pass is single-level; needs to become recursive with multi-level capture-chain analysis |
|
|
7773
|
+
|
|
7774
|
+
Given the size and risk profile here, I'd rather not silently plow through all seven in one unattended pass — a couple of these touch shared inference/scanner code where a mistake could quietly regress things that already work.
|
|
7775
|
+
- `AskUserQuestion`
|
|
7776
|
+
|
|
7777
|
+
**Claude:** Let's start. First, gap #6 — using a plain top-level function as a `fn(...)`-typed value (e.g. `each(double)`).
|
|
7778
|
+
- `Bash: cd /Users/pyrossh/Code/plum sed -n '1,50p' docs/superpowers/plans/2026-07-20-closures.md | head -5 grep -n "useNamedFunctionAsValue\|named function\|table_index`
|
|
7779
|
+
- `Bash: cd /Users/pyrossh/Code/plum grep -n "struct RawClosure\|struct ClosureInfo\|struct ClosureSigKey\|closures:\|closure_table\|elem_segment\|fn record_closure\|fn`
|
|
7780
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7781
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7782
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7783
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7784
|
+
- `Bash: cd /Users/pyrossh/Code/plum grep -n "STRING_BASE\|HEAP_BASE\|bump_global\s*=\|add_data_segment\|memory.min\|Memory {" plum-wasm-codegen/src/lib.rs | head -30`
|
|
7785
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7786
|
+
- `Bash: cd /Users/pyrossh/Code/plum grep -n "struct FuncSig" -A 5 plum-wasm-codegen/src/lib.rs grep -n "fn add_type\|fn add_function\|fn add_table_element\|fn add_globa`
|
|
7787
|
+
|
|
7788
|
+
**Claude:** Now let's implement. First, the discovery-pass changes in `ClosureWalker` to detect a bare reference to a top-level function name.
|
|
7789
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7790
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7791
|
+
|
|
7792
|
+
**Claude:** Need to import `HashSet`. Let's make all the changes now.
|
|
7793
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7794
|
+
|
|
7795
|
+
**Claude:** Now the `ClosureWalker` struct — add `locals` and `named_fn_refs` fields:
|
|
7796
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7797
|
+
|
|
7798
|
+
**Claude:** Now update the discovery-loop setup to seed `locals` and collect `named_fn_refs`:
|
|
7799
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7800
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7801
|
+
|
|
7802
|
+
**Claude:** Now update `walk_stmt`'s `Assign`/`For` arms to track locals, and `walk_expr`'s `Var` arm to detect named function references:
|
|
7803
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7804
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7805
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7806
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7807
|
+
|
|
7808
|
+
**Claude:** Now update `walk_expr`'s catch-all to split out `Var`:
|
|
7809
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7810
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7811
|
+
- `Bash: grep -n "let mut closures: HashMap<usize, ClosureInfo>\|let ctx = CompileCtx\|let mut state = ModuleState\|for f in &fns {$" /Users/pyrossh/Code/plum/plum-wasm-`
|
|
7812
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7813
|
+
- `Bash: grep -n "Instruction::End\|fn compile_fn_body" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs | head -10`
|
|
7814
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7815
|
+
- `Bash: grep -n "fn finish\|functions.push\|fn add_function" -A 15 /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs | head -60`
|
|
7816
|
+
- `Bash: grep -n "Function::new\|CodeSection\|code.function" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs | head -10`
|
|
7817
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7818
|
+
- `Bash: grep -n "Function::new\|encode_leb128_u32\|fn compile_fn_body\|Ok(body)\|-> Result<Vec<u8>, String>" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs | hea`
|
|
7819
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7820
|
+
|
|
7821
|
+
**Claude:** Now let's wire this into `CompileCtx`/`LocalCtx` and the module-building orchestration. First, the struct fields:
|
|
7822
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7823
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7824
|
+
|
|
7825
|
+
**Claude:** Now `LocalCtx`:
|
|
7826
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7827
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7828
|
+
|
|
7829
|
+
**Claude:** Now the two `LocalCtx` construction sites:
|
|
7830
|
+
- `Bash: grep -n "closure_call_types: &ctx.closure_call_types," /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7831
|
+
- `Bash: cd /Users/pyrossh/Code/plum sed -i '' 's/closure_call_types: &ctx.closure_call_types,/closure_call_types: \&ctx.closure_call_types,\n named_fn_values: \&ctx.nam`
|
|
7832
|
+
- `Bash: sed -n '1205,1216p;2298,2310p' /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7833
|
+
|
|
7834
|
+
**Claude:** Good, both aligned correctly. Now the main registration block in `compile_source`, and the `ctx`/`state` construction.
|
|
7835
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7836
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7837
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7838
|
+
|
|
7839
|
+
**Claude:** Now let's update `compile_expr`'s `Var` arm to use this table.
|
|
7840
|
+
- `Bash: grep -n "ast::Expr::Var(name) =>" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7841
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7842
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7843
|
+
- `Bash: sed -n '735,750p;925,940p' /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7844
|
+
- `Bash: grep -n "fn collect_free_vars\|fn fv_collect_refs_expr\|fn fv_collect_refs_stmt" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7845
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7846
|
+
- `Bash: grep -n "^fn fv_collect_refs" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7847
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7848
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7849
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7850
|
+
|
|
7851
|
+
**Claude:** Now update `fv_collect_refs_block` and `fv_collect_refs_expr` signatures and all recursive calls with a sed-based mechanical pass, then fix the `Var` arm.
|
|
7852
|
+
- `Bash: cd /Users/pyrossh/Code/plum python3 - << 'EOF' import re path = "plum-wasm-codegen/src/lib.rs" src = open(path).read() start = src.index("fn fv_collect_refs_blo`
|
|
7853
|
+
|
|
7854
|
+
**Claude:** Now fix the `Var` arm inside `fv_collect_refs_expr` to skip top-level function names:
|
|
7855
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7856
|
+
|
|
7857
|
+
**Claude:** Mechanical threading worked. Now fix the `Var` arm itself:
|
|
7858
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7859
|
+
|
|
7860
|
+
**Claude:** Now update the call site of `collect_free_vars` in `record_closure`:
|
|
7861
|
+
- `Bash: grep -n "collect_free_vars(cl" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7862
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7863
|
+
|
|
7864
|
+
**Claude:** Now let's check for any other `CompileCtx { ... }` construction sites (tests might build one directly) that need the new `named_fn_values` field, then build.
|
|
7865
|
+
- `Bash: grep -rn "CompileCtx {" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/*.rs 2>/dev/null`
|
|
7866
|
+
|
|
7867
|
+
**Claude:** Good, only one construction site. Let's build.
|
|
7868
|
+
- `Bash: cargo build -p plum-wasm-codegen 2>&1 | tail -100`
|
|
7869
|
+
|
|
7870
|
+
**Claude:** Compiles cleanly. Now let's write a real test proving `each(double)` compiles and runs correctly.
|
|
7871
|
+
- `Bash: grep -n "fn run_main\|fn assert_valid\|fn compile_and_run\|^fn " /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs | head -20`
|
|
7872
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
7873
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
7874
|
+
|
|
7875
|
+
**Claude:** Now let's write the test for gap #6, using the exact `each(double)` scenario the README/plan docs anticipated.
|
|
7876
|
+
- `Bash: grep -n "^fn run_main" -A 15 /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
7877
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
7878
|
+
|
|
7879
|
+
**Claude:** Let's insert our new test right after these closure tests.
|
|
7880
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
7881
|
+
- `Bash: grep -n "cb(5)\|cb(0)\|compile_closure_call\|fn check_fn_call\|ast::Expr::FnCall" /Users/pyrossh/Code/plum/plum-checker/src/lib.rs | head -10`
|
|
7882
|
+
- `Read: /Users/pyrossh/Code/plum/plum-checker/src/lib.rs`
|
|
7883
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo test -p plum-wasm-codegen --test codegen_tests named_function -- --nocapture 2>&1 | tail -60`
|
|
7884
|
+
|
|
7885
|
+
**Claude:** All three pass on the first try. Let's run the full test suite to make sure nothing else regressed.
|
|
7886
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo test --workspace 2>&1 | tail -80`
|
|
7887
|
+
|
|
7888
|
+
**Claude:** Gap #6 done, all tests green. Let's update the README before moving on.
|
|
7889
|
+
- `Read: /Users/pyrossh/Code/plum/README.md`
|
|
7890
|
+
- `Edit: /Users/pyrossh/Code/plum/README.md`
|
|
7891
|
+
|
|
7892
|
+
**Claude:** Now let's tackle gap #2: multi-subject `match`. First let's look at the current match compilation machinery in detail.
|
|
7893
|
+
- `Bash: grep -n "fn compile_match\|fn compile_match_arms\|fn compile_variant_eq_arm\|fn compile_variant_constructor_arm\|fn compile_case_body\|fn match_covers_every_enu`
|
|
7894
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7895
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7896
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7897
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7898
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7899
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7900
|
+
|
|
7901
|
+
**Claude:** Let's implement. First, the `Collector`'s `match_scratch` type change and its `Match` walk logic:
|
|
7902
|
+
- `Bash: grep -n "match_scratch: HashMap<usize, PlumType>" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7903
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7904
|
+
|
|
7905
|
+
**Claude:** Now the `walk_stmt`'s `Match` arm:
|
|
7906
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7907
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7908
|
+
|
|
7909
|
+
**Claude:** Now the two duplicated slot-allocation blocks:
|
|
7910
|
+
- `Bash: grep -n "for (ptr, ty) in collector.match_scratch.iter()" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7911
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7912
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7913
|
+
|
|
7914
|
+
**Claude:** Now let's rewrite the core match-compilation functions. Let's view the current state with fresh line numbers:
|
|
7915
|
+
- `Bash: grep -n "^fn match_covers_every_enum_variant\|^fn compile_match\|^fn compile_match_arms\|^fn compile_variant_eq_arm\|^fn compile_variant_constructor_arm\|^fn co`
|
|
7916
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7917
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7918
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7919
|
+
- `Bash: grep -n "compile_match_arms(" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7920
|
+
|
|
7921
|
+
**Claude:** No stale references. Let's build.
|
|
7922
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo build -p plum-wasm-codegen 2>&1 | tail -150`
|
|
7923
|
+
|
|
7924
|
+
**Claude:** Compiles. Now let's fix the `monomorphize.rs` gap flagged in the research (only mangles the first subject/pattern).
|
|
7925
|
+
- `Read: /Users/pyrossh/Code/plum/plum-checker/src/monomorphize.rs`
|
|
7926
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-checker/src/monomorphize.rs`
|
|
7927
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo build -p plum-checker 2>&1 | tail -60`
|
|
7928
|
+
|
|
7929
|
+
**Claude:** Now let's write tests for multi-subject match and run everything.
|
|
7930
|
+
- `Bash: grep -n "match_string_pattern_is_a_clear_error\|nested_constructor_pattern_is_a_clear_error" -A 30 /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_test`
|
|
7931
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
7932
|
+
|
|
7933
|
+
**Claude:** Let's add multi-subject match tests near the closure tests at the end (with `run_main`), covering compile+runtime correctness.
|
|
7934
|
+
- `Bash: grep -n "fn named_function_used_as_a_value_alongside_a_closure_at_the_same_call_type_runs_correctly" -A 20 /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/code`
|
|
7935
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
7936
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo test -p plum-wasm-codegen --test codegen_tests multi_subject -- --nocapture 2>&1 | tail -60`
|
|
7937
|
+
|
|
7938
|
+
**Claude:** This is exactly the case in `libs/std/bool.plum`'s `and`/`or` (a full Bool×Bool cross-product with no wildcard). The existing exhaustiveness heuristic only understood single-subject matches — let's generalize it to a proper cross-product check.
|
|
7939
|
+
- `Bash: grep -n "^fn match_covers_every_enum_variant" -A 40 /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs | head -45`
|
|
7940
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7941
|
+
|
|
7942
|
+
**Claude:** Now update `compile_match` to use this generalized check regardless of subject count:
|
|
7943
|
+
- `Bash: grep -n "match_covers_every_enum_variant\|if let \[(subject_vt, _)\] = all_subjects" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7944
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7945
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7946
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo build -p plum-wasm-codegen 2>&1 | tail -60`
|
|
7947
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo test -p plum-wasm-codegen --test codegen_tests multi_subject -- --nocapture 2>&1 | tail -30`
|
|
7948
|
+
|
|
7949
|
+
**Claude:** All 4 pass. Let's run the full suite.
|
|
7950
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo test --workspace 2>&1 | tail -100`
|
|
7951
|
+
|
|
7952
|
+
**Claude:** All green. Let's update the README and check whether `bool.plum` (which uses this exact `and`/`or` pattern) now actually compiles end-to-end.
|
|
7953
|
+
- `Edit: /Users/pyrossh/Code/plum/README.md`
|
|
7954
|
+
- `Bash: cd /Users/pyrossh/Code/plum cat > /tmp/bool_compile_test.rs << 'EOF' use plum_checker::check_source; use plum_core::AstParser; use plum_wasm_codegen::compile_so`
|
|
7955
|
+
- `Bash: sed -i '' 's#libs/std/bool.plum#/Users/pyrossh/Code/plum/libs/std/bool.plum#' /tmp/bool_check/src/main.rs cd /tmp/bool_check && cargo run 2>&1 | tail -10`
|
|
7956
|
+
- `Bash: rm -rf /tmp/bool_check /tmp/bool_compile_test.rs`
|
|
7957
|
+
|
|
7958
|
+
**Claude:** Confirms our fix directly unblocks `and`/`or` (the remaining error is just `Ok`/`Err` living in a separate file — a cross-module concern, not related to this gap). Now let's tackle gap #3: nested constructor patterns inside `match`.
|
|
7959
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7960
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7961
|
+
|
|
7962
|
+
**Claude:** Given how the recursion works, I can keep this contained — reusing the existing top-level dispatch and adding a new recursive helper just for a constructor pattern's own fields. Let's implement.
|
|
7963
|
+
- `Bash: grep -n "struct Collector\|classcall_scratch: HashMap<usize, u32>,\|next_classcall_slot: u32,\|match_scratch: HashMap" /Users/pyrossh/Code/plum/plum-wasm-codege`
|
|
7964
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7965
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7966
|
+
|
|
7967
|
+
**Claude:** Now let's add the recursive binding/scratch-collection method and rewire the `Match` handling in `walk_stmt`:
|
|
7968
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7969
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7970
|
+
|
|
7971
|
+
**Claude:** Now update the `Match` handling in `walk_stmt` to call this instead of its old flat binding logic:
|
|
7972
|
+
- `Bash: grep -n "ast::Stmt::Match(m) =>" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7973
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7974
|
+
|
|
7975
|
+
**Claude:** This is the one to replace.
|
|
7976
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7977
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7978
|
+
- `Bash: grep -n "next_closure_slot: 0," /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7979
|
+
- `Bash: cd /Users/pyrossh/Code/plum sed -i '' 's/next_closure_slot: 0,/next_closure_slot: 0,\n nested_class_scratch: HashMap::new(),\n next_nested_class_slot: 0,/' plum`
|
|
7980
|
+
|
|
7981
|
+
**Claude:** Now let's add `LocalCtx` fields and wire the slot-allocation logic (both places):
|
|
7982
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7983
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7984
|
+
|
|
7985
|
+
**Claude:** Now update the two slot-allocation blocks (in `compile_fn_body` and `compile_closure_body`):
|
|
7986
|
+
- `Bash: grep -n "let closure_scratch_base = idx;" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7987
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7988
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7989
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7990
|
+
- `Bash: grep -n "closure_scratch_base,\s*$\|closure_scratch: collector.closure_scratch,\s*$" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7991
|
+
- `Bash: cd /Users/pyrossh/Code/plum python3 - << 'EOF' path = "plum-wasm-codegen/src/lib.rs" src = open(path).read() old = " closure_scratch_base,\n closure_scratch: co`
|
|
7992
|
+
|
|
7993
|
+
**Claude:** Now let's build to catch any remaining field-mismatch errors.
|
|
7994
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo build -p plum-wasm-codegen 2>&1 | tail -80`
|
|
7995
|
+
|
|
7996
|
+
**Claude:** Good, plumbing compiles — just unused until we wire it into the constructor-arm field loop. Now let's write `compile_fields`/`compile_field_pattern` and update `compile_variant_constructor_arm`.
|
|
7997
|
+
- `Bash: grep -n "^fn compile_variant_constructor_arm" -A 75 /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs | head -80`
|
|
7998
|
+
- `Bash: grep -n "^fn emit_store\|^fn emit_load" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
7999
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8000
|
+
|
|
8001
|
+
**Claude:** Now let's rewrite `compile_variant_constructor_arm`'s field loop and add the two new recursive helpers.
|
|
8002
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8003
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo build -p plum-wasm-codegen 2>&1 | tail -150`
|
|
8004
|
+
|
|
8005
|
+
**Claude:** Now let's flip the existing "clear error" test into a positive test, and add a couple more covering binding + fallthrough.
|
|
8006
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
8007
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
8008
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo test -p plum-wasm-codegen --test codegen_tests nested_constructor -- --nocapture 2>&1 | tail -60 cargo test -p plum-wasm-codeg`
|
|
8009
|
+
- `Bash: grep -n "fn collect_pattern\|collect_pattern(" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8010
|
+
- `Bash: grep -n "^fn compile_case_body" -A 40 /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs | head -45`
|
|
8011
|
+
- `Bash: grep -n "Stmt::Todo =>" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8012
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8013
|
+
|
|
8014
|
+
**Claude:** Root cause found: the exhaustiveness heuristic clones `m.cases` to append a synthetic wildcard, which reallocates every nested `CasePattern::Class` node at a new address — breaking the pointer-identity-keyed scratch-slot lookup. Let's fix it by threading a boolean flag instead of cloning.
|
|
8015
|
+
- `Bash: grep -n "compile_match_arms_multi(rest, all_subjects, result_vt, body, ctx, state)\|fn compile_match_arms_multi\|fn compile_case_positions\|fn compile_variant_e`
|
|
8016
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8017
|
+
|
|
8018
|
+
**Claude:** Now let's rewrite this whole section with the threaded `exhaustive_fallback: bool` instead of cloning.
|
|
8019
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8020
|
+
|
|
8021
|
+
**Claude:** Now let's view and update `compile_match_arms_multi`'s body and signature:
|
|
8022
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8023
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8024
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8025
|
+
|
|
8026
|
+
**Claude:** Given the volume of mechanical threading needed across these four functions, let's do it with a careful scripted rewrite rather than dozens of manual edits.
|
|
8027
|
+
- `Bash: cd /Users/pyrossh/Code/plum python3 - << 'PYEOF' import re path = "plum-wasm-codegen/src/lib.rs" src = open(path).read() start = src.index("fn compile_case_posi`
|
|
8028
|
+
|
|
8029
|
+
**Claude:** Let's build and see what's left.
|
|
8030
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo build -p plum-wasm-codegen 2>&1 | grep -E "^error" -A 8 | head -200`
|
|
8031
|
+
|
|
8032
|
+
**Claude:** Just one over-eager replacement — `compile_case_body` doesn't need this param. Let's fix it.
|
|
8033
|
+
- `Bash: grep -n "compile_case_body(&case.body, result_vt, exhaustive_fallback, body, ctx, state)" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8034
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8035
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo build -p plum-wasm-codegen 2>&1 | grep -E "^error" -A 8 | head -200`
|
|
8036
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo build -p plum-wasm-codegen 2>&1 | tail -30`
|
|
8037
|
+
- `Bash: grep -n "exhaustive_fallback" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs | wc -l sed -n '1980,2050p' /Users/pyrossh/Code/plum/plum-wasm-codegen/src/l`
|
|
8038
|
+
|
|
8039
|
+
**Claude:** Threading correctly propagated into the nested closure too. Now let's run the tests.
|
|
8040
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo test -p plum-wasm-codegen --test codegen_tests -- --nocapture 2>&1 | tail -100`
|
|
8041
|
+
|
|
8042
|
+
**Claude:** All 62 pass, including the two previously-failing nested-constructor tests. Let's run the full workspace suite.
|
|
8043
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo test --workspace 2>&1 | tail -50`
|
|
8044
|
+
|
|
8045
|
+
**Claude:** Now let's also fix the monomorphize.rs side for gap #3 — recursing into nested constructor-pattern fields for generic-enum mangling.
|
|
8046
|
+
- `Read: /Users/pyrossh/Code/plum/plum-checker/src/monomorphize.rs`
|
|
8047
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-checker/src/monomorphize.rs`
|
|
8048
|
+
- `Bash: grep -n "fn rewrite_stmt\|impl.*Monomorphizer\|enum_variant_mangling:\|enum_variants:" /Users/pyrossh/Code/plum/plum-checker/src/monomorphize.rs | head -20`
|
|
8049
|
+
- `Read: /Users/pyrossh/Code/plum/plum-checker/src/monomorphize.rs`
|
|
8050
|
+
|
|
8051
|
+
**Claude:** Let's add `mangle_pattern` right before `rewrite_stmt`:
|
|
8052
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-checker/src/monomorphize.rs`
|
|
8053
|
+
|
|
8054
|
+
**Claude:** Now let's also update the call site to pass `Option<&BTreeMap<...>>` instead of `Option<BTreeMap<...>>`:
|
|
8055
|
+
- `Bash: grep -n "self.mangle_pattern(pat, subject_ty, variant_mangling" /Users/pyrossh/Code/plum/plum-checker/src/monomorphize.rs`
|
|
8056
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo build -p plum-checker 2>&1 | tail -60`
|
|
8057
|
+
|
|
8058
|
+
**Claude:** Now let's write a test proving nested constructor patterns against a generic-enum specialization work correctly.
|
|
8059
|
+
- `Bash: grep -n "fn generic_enum_specialized_and_matched_runs_correctly\|fn generic_enum_multiple_instantiations_coexist_and_run_correctly" -A 20 /Users/pyrossh/Code/pl`
|
|
8060
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
8061
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo test -p plum-wasm-codegen --test codegen_tests nested_constructor_pattern_against -- --nocapture 2>&1 | tail -40`
|
|
8062
|
+
|
|
8063
|
+
**Claude:** Passes. Let's run the full suite once more to confirm everything's still green.
|
|
8064
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo test --workspace 2>&1 | tail -60`
|
|
8065
|
+
|
|
8066
|
+
**Claude:** Gap #3 done. Let's update the README.
|
|
8067
|
+
- `Read: /Users/pyrossh/Code/plum/README.md`
|
|
8068
|
+
- `Edit: /Users/pyrossh/Code/plum/README.md`
|
|
8069
|
+
|
|
8070
|
+
**Claude:** Now gap #4 — the scanner surgery for multi-line closure bodies followed by a closing bracket on the same line. Let's study the scanner carefully first.
|
|
8071
|
+
- `Bash: wc -l /Users/pyrossh/Code/plum/tooling/tree-sitter-plum/src/scanner.c`
|
|
8072
|
+
- `Read: /Users/pyrossh/Code/plum/tooling/tree-sitter-plum/src/scanner.c`
|
|
8073
|
+
- `Read: /Users/pyrossh/Code/plum/tooling/tree-sitter-plum/src/scanner.c`
|
|
8074
|
+
- `Edit: /Users/pyrossh/Code/plum/tooling/tree-sitter-plum/src/scanner.c`
|
|
8075
|
+
|
|
8076
|
+
**Claude:** Now let's rebuild the parser (native scanner) and regenerate.
|
|
8077
|
+
- `Bash: cd /Users/pyrossh/Code/plum/tooling/tree-sitter-plum make build 2>&1 | tail -40`
|
|
8078
|
+
- `Bash: cd /Users/pyrossh/Code/plum/tooling/tree-sitter-plum make corpus_test 2>&1 | tail -40`
|
|
8079
|
+
|
|
8080
|
+
**Claude:** All 28 existing corpus tests still pass. Now let's directly test the exact target syntax with the tree-sitter CLI parse command.
|
|
8081
|
+
- `Bash: cd /Users/pyrossh/Code/plum/tooling/tree-sitter-plum cat > /tmp/multiline_closure.plum << 'EOF' each(cb: fn(Int) -> Int) -> Int = cb(5) main() -> Int = each(|v|`
|
|
8082
|
+
|
|
8083
|
+
**Claude:** It parses correctly with zero errors. Let's test a multi-statement body too, and the case with a trailing comma/multiple args.
|
|
8084
|
+
- `Bash: cd /Users/pyrossh/Code/plum/tooling/tree-sitter-plum cat > /tmp/multiline_closure2.plum << 'EOF' each(cb: fn(Int) -> Int) -> Int = cb(5) main() -> Int = each(|v`
|
|
8085
|
+
|
|
8086
|
+
**Claude:** Excellent — multi-statement bodies work too. Let's check a couple more edge cases (nested brackets, additional args after the closure) before locking this in.
|
|
8087
|
+
- `Bash: cd /Users/pyrossh/Code/plum/tooling/tree-sitter-plum cat > /tmp/multiline_closure3.plum << 'EOF' each(cb: fn(Int) -> Int, extra: Int) -> Int = cb(5) + extra mai`
|
|
8088
|
+
- `Bash: cat /tmp/out3.txt`
|
|
8089
|
+
|
|
8090
|
+
**Claude:** Let's handle the comma-continuation case too (closure followed by more call arguments on the same line) — same root cause.
|
|
8091
|
+
- `Bash: grep -n "at_closing_bracket_without_newline" /Users/pyrossh/Code/plum/tooling/tree-sitter-plum/src/scanner.c`
|
|
8092
|
+
- `Read: /Users/pyrossh/Code/plum/tooling/tree-sitter-plum/src/scanner.c`
|
|
8093
|
+
- `Edit: /Users/pyrossh/Code/plum/tooling/tree-sitter-plum/src/scanner.c`
|
|
8094
|
+
- `Bash: cd /Users/pyrossh/Code/plum/tooling/tree-sitter-plum make build 2>&1 | tail -20 npx --yes tree-sitter-cli parse /tmp/multiline_closure3.plum > /tmp/out3.txt 2>&`
|
|
8095
|
+
|
|
8096
|
+
**Claude:** That specific case (comma continuation) doesn't have `DEDENT` as a valid symbol at that exact parser state, so my extension didn't help — and it's outside the documented gap anyway (which specifically calls out the closing-bracket case). Let's revert that speculative addition and keep scope to what's proven and documented.
|
|
8097
|
+
- `Edit: /Users/pyrossh/Code/plum/tooling/tree-sitter-plum/src/scanner.c`
|
|
8098
|
+
|
|
8099
|
+
**Claude:** Let's rebuild and re-verify everything, then run the full corpus + Rust test suites.
|
|
8100
|
+
- `Bash: cd /Users/pyrossh/Code/plum/tooling/tree-sitter-plum make build 2>&1 | tail -20 make corpus_test 2>&1 | tail -15`
|
|
8101
|
+
|
|
8102
|
+
**Claude:** Now let's add a corpus test locking in this new behavior, then run the full Rust workspace suite (which will actually compile & execute wasm for this shape).
|
|
8103
|
+
- `Bash: grep -n "function - inline closure literal as a call argument" -A 20 /Users/pyrossh/Code/plum/tooling/tree-sitter-plum/test/corpus/function.txt`
|
|
8104
|
+
- `Read: /Users/pyrossh/Code/plum/tooling/tree-sitter-plum/test/corpus/function.txt`
|
|
8105
|
+
|
|
8106
|
+
**Claude:** Let's add a new corpus case right after this for the multi-line body + same-line closing paren.
|
|
8107
|
+
- `Edit: /Users/pyrossh/Code/plum/tooling/tree-sitter-plum/test/corpus/function.txt`
|
|
8108
|
+
- `Bash: cd /Users/pyrossh/Code/plum/tooling/tree-sitter-plum make corpus_test 2>&1 | tail -20`
|
|
8109
|
+
|
|
8110
|
+
**Claude:** All 29 pass. Now let's run the full Rust workspace test suite (which rebuilds the vendored scanner.c via cargo's build.rs) to confirm nothing regressed.
|
|
8111
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo test --workspace 2>&1 | tail -100`
|
|
8112
|
+
|
|
8113
|
+
**Claude:** Now let's update the existing codegen test that explicitly avoided this shape, and add a real multi-statement version.
|
|
8114
|
+
- `Bash: grep -n "non_capturing_closure_passed_and_called_runs_correctly" -A 20 /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
8115
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
8116
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo test -p plum-wasm-codegen --test codegen_tests multi_line_closure -- --nocapture 2>&1 | tail -30`
|
|
8117
|
+
|
|
8118
|
+
**Claude:** Real end-to-end wasm execution proves it. Let's run the full suite once more and update the README, then also simplify `libs/std/list.plum`'s `join()` now that this actually works.
|
|
8119
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo test --workspace 2>&1 | tail -15`
|
|
8120
|
+
- `Read: /Users/pyrossh/Code/plum/README.md`
|
|
8121
|
+
- `Edit: /Users/pyrossh/Code/plum/README.md`
|
|
8122
|
+
|
|
8123
|
+
**Claude:** Let's tackle gap #1: string interpolation codegen. First, let's understand the existing string representation exactly.
|
|
8124
|
+
- `Bash: grep -n "ast::Expr::String(s) =>" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8125
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8126
|
+
- `Bash: cat /Users/pyrossh/Code/plum/examples/strings.plum`
|
|
8127
|
+
- `Bash: grep -n "wasm-encoder" /Users/pyrossh/Code/plum/plum-wasm-codegen/Cargo.toml grep -rn "MemoryCopy\|memory.copy\|bulk" /Users/pyrossh/Code/plum/plum-wasm-codegen`
|
|
8128
|
+
- `Bash: grep -n "MemoryCopy\|MemoryFill" ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wasm-encoder-0.224.1/src/core/code.rs | head -5`
|
|
8129
|
+
- `Bash: grep -n "Instruction::Loop\|Instruction::Block\|Instruction::BrIf\|Instruction::Br(" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs | head -20`
|
|
8130
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8131
|
+
|
|
8132
|
+
**Claude:** This is a substantial addition — I need to hand-write two runtime helper functions in raw wasm bytecode (string concatenation and int-to-string conversion), since wasm has no built-in string manipulation. Let me implement carefully and test thoroughly.
|
|
8133
|
+
- `Bash: grep -n "pub struct FuncSig\|struct CompileCtx" -A 20 /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs | head -25`
|
|
8134
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8135
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8136
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8137
|
+
|
|
8138
|
+
**Claude:** Now `LocalCtx`:
|
|
8139
|
+
- `Bash: grep -n "named_fn_values: &'a HashMap<String, u32>," /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8140
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8141
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8142
|
+
|
|
8143
|
+
**Claude:** Now let's update both `LocalCtx` construction sites:
|
|
8144
|
+
- `Bash: grep -n "named_fn_values: &ctx.named_fn_values," /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8145
|
+
- `Bash: cd /Users/pyrossh/Code/plum python3 - << 'EOF' path = "plum-wasm-codegen/src/lib.rs" src = open(path).read() old = " named_fn_values: &ctx.named_fn_values,\n" n`
|
|
8146
|
+
|
|
8147
|
+
**Claude:** Now let's write the two helper-function-generator functions and wire them into `compile_source`.
|
|
8148
|
+
- `Bash: grep -n "let ctx = CompileCtx {" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8149
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8150
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8151
|
+
- `Bash: grep -n "^pub fn compile_source" -A 2 /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs awk '/^pub fn compile_source/{f=1} f && /^}/{print NR; exit}' /Users`
|
|
8152
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8153
|
+
|
|
8154
|
+
**Claude:** Now let's insert the two helper-generator functions right after `compile_source`:
|
|
8155
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8156
|
+
|
|
8157
|
+
**Claude:** Now let's wire up the actual `Expr::String` interpolation-lowering logic.
|
|
8158
|
+
- `Bash: grep -n "ast::Expr::String(s) =>" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8159
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8160
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8161
|
+
|
|
8162
|
+
**Claude:** Now let's add `compile_static_string` and `compile_interpolated_string` helper functions right after this. Let's find a good spot (near `emit_load`/`emit_store`):
|
|
8163
|
+
- `Bash: grep -n "^fn emit_load" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8164
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8165
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8166
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo build -p plum-wasm-codegen 2>&1 | tail -150`
|
|
8167
|
+
- `Bash: grep -n "struct Str\|enum StringPart\|pub struct.*Str" /Users/pyrossh/Code/plum/plum-core/src/ast.rs`
|
|
8168
|
+
- `Read: /Users/pyrossh/Code/plum/plum-core/src/ast.rs`
|
|
8169
|
+
- `Bash: grep -n "String(" /Users/pyrossh/Code/plum/plum-core/src/ast.rs | head -5`
|
|
8170
|
+
- `Bash: grep -n "fn compile_interpolated_string" -A 15 /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8171
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8172
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo build -p plum-wasm-codegen 2>&1 | tail -100`
|
|
8173
|
+
|
|
8174
|
+
**Claude:** Now let's update the two existing "clear error" tests that assumed interpolation was unsupported, and write real positive tests that read the string back from wasm memory.
|
|
8175
|
+
- `Bash: grep -n "string_interpolation_is_a_clear_error" -A 10 /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs echo "---" grep -n "strings_example_repo`
|
|
8176
|
+
- `Bash: grep -n "add_export.*[Mm]emory\|ExportKind::Memory" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8177
|
+
- `Bash: grep -n "enum ExportKind\|fn add_memory" -A 8 /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs | head -30`
|
|
8178
|
+
- `Bash: grep -n "ExportKind" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs | head -5`
|
|
8179
|
+
- `Bash: grep -n "module.add_memory" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8180
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8181
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8182
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo build -p plum-wasm-codegen 2>&1 | tail -30`
|
|
8183
|
+
|
|
8184
|
+
**Claude:** Now let's write the test helper and real tests.
|
|
8185
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
8186
|
+
- `Bash: grep -n "^fn run_main" -A 10 /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
8187
|
+
|
|
8188
|
+
**Claude:** Let's add a `run_main_str` helper right after `run_main`.
|
|
8189
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
8190
|
+
|
|
8191
|
+
**Claude:** Now let's replace the "clear error" test with real positive tests.
|
|
8192
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
8193
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
8194
|
+
|
|
8195
|
+
**Claude:** Now let's fix the `examples_test.rs` one and check `examples/strings.plum` content, since it uses interpolation extensively.
|
|
8196
|
+
- `Bash: cat /Users/pyrossh/Code/plum/examples/strings.plum echo "---" sed -n '1,120p' /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/examples_test.rs`
|
|
8197
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/examples_test.rs`
|
|
8198
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo test -p plum-wasm-codegen --test codegen_tests string_interpolation -- --nocapture 2>&1 | tail -60`
|
|
8199
|
+
- `Bash: which wasm2wat wasm-tools 2>&1 cargo tree -p plum-wasm-codegen 2>&1 | grep -i "wasmprinter\|wat "`
|
|
8200
|
+
|
|
8201
|
+
**Claude:** `wasm-tools` is available. Let's dump the failing module and inspect around the byte offset.
|
|
8202
|
+
- `Bash: cd /Users/pyrossh/Code/plum cat > /tmp/dump_wasm.rs << 'EOF' fn main() { let src = "main() -> Str =\n x = 42\n \"{x}\"\n"; let mut parser = tree_sitter::Parser`
|
|
8203
|
+
- `Bash: wasm-tools print /tmp/out.wasm 2>&1 | head -150`
|
|
8204
|
+
- `Bash: wasm-tools print /tmp/out.wasm 2>&1 | sed -n '150,220p'`
|
|
8205
|
+
- `Bash: wasm-tools validate /tmp/out.wasm 2>&1`
|
|
8206
|
+
|
|
8207
|
+
**Claude:** Found the bug — the `string_concat` helper's bump-allocation math is missing a final `I32Add` to combine the saved base pointer with the computed size (the `int_to_string` helper has it correctly, `string_concat` doesn't). Let's fix it.
|
|
8208
|
+
- `Bash: grep -n "register_string_concat_helper" -A 25 /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs | head -30`
|
|
8209
|
+
- `Bash: grep -n "result = bump; bump += 4 + len_a + len_b" -A 10 /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8210
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8211
|
+
|
|
8212
|
+
**Claude:** Now let's rebuild, re-validate, and re-run the tests.
|
|
8213
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo build -p plum-wasm-codegen 2>&1 | tail -10 cd /tmp/dumpwasm && cargo run 2>&1 | tail -5 wasm-tools validate /tmp/out.wasm 2>&1`
|
|
8214
|
+
- `Bash: wasm-tools print /tmp/out.wasm 2>&1 | sed -n '95,145p'`
|
|
8215
|
+
- `Bash: wasm-tools print /tmp/out.wasm 2>&1 | sed -n '140,180p'`
|
|
8216
|
+
- `Bash: wasm-tools print /tmp/out.wasm 2>&1 | sed -n '85,100p'`
|
|
8217
|
+
|
|
8218
|
+
**Claude:** Found it — a local-index/type mismatch: I declared locals as one group of 5 `i32` then one group of 2 `i64`, but my constants interleaved i32/i64 assignments assuming per-variable ordering. Let me fix the constant indices to match the actual declared groups.
|
|
8219
|
+
- `Bash: grep -n "const N: u32 = 0;" -A 10 /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8220
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8221
|
+
|
|
8222
|
+
**Claude:** Let's verify the locals group declaration matches (5 i32, then 2 i64), rebuild, and re-validate.
|
|
8223
|
+
- `Bash: grep -n "body.extend(encode_leb128_u32(2)); // two locals groups" -A 5 /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8224
|
+
|
|
8225
|
+
**Claude:** Good, matches. Let's rebuild and re-validate.
|
|
8226
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo build -p plum-wasm-codegen 2>&1 | tail -10 cd /tmp/dumpwasm && cargo run 2>&1 | tail -5 wasm-tools validate /tmp/out.wasm 2>&1`
|
|
8227
|
+
|
|
8228
|
+
**Claude:** Valid now. Let's clean up the scratch project and run the full test suite.
|
|
8229
|
+
- `Bash: rm -rf /tmp/dumpwasm /tmp/out.wasm /tmp/dump_wasm.rs cd /Users/pyrossh/Code/plum cargo test -p plum-wasm-codegen --test codegen_tests string_interpolation -- --`
|
|
8230
|
+
|
|
8231
|
+
**Claude:** All 7 pass, including negative numbers, zero, multi-interpolation, Str, and Bool. Let's run the complete workspace suite.
|
|
8232
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo test --workspace 2>&1 | tail -100`
|
|
8233
|
+
|
|
8234
|
+
**Claude:** Gap #1 done. Let's update the README.
|
|
8235
|
+
- `Read: /Users/pyrossh/Code/plum/README.md`
|
|
8236
|
+
- `Edit: /Users/pyrossh/Code/plum/README.md`
|
|
8237
|
+
|
|
8238
|
+
**Claude:** Now let's tackle gap #5 — the `call_indirect` trap. Let's look at the exact checker logic for closure inference first.
|
|
8239
|
+
- `Bash: grep -n "ast::Expr::Closure(cl) =>" -A 20 /Users/pyrossh/Code/plum/plum-checker/src/lib.rs`
|
|
8240
|
+
- `Bash: grep -n "fn record_closure" -A 40 /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs | head -45`
|
|
8241
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8242
|
+
- `Bash: grep -n "^fn collect_free_vars" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8243
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8244
|
+
|
|
8245
|
+
**Claude:** Let's insert the new helper functions right before `collect_free_vars`:
|
|
8246
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8247
|
+
|
|
8248
|
+
**Claude:** That got garbled — let me fix the `Not`/`Unary` arm properly.
|
|
8249
|
+
- `Bash: grep -n "ast::Expr::Not(inner) | ast::Expr::Unary" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8250
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8251
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8252
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo build -p plum-wasm-codegen 2>&1 | tail -150`
|
|
8253
|
+
|
|
8254
|
+
**Claude:** I need to check the call-site's own signature re-derivation too, since it independently re-infers the closure's type — let's find that.
|
|
8255
|
+
- `Bash: grep -n "ast::Stmt::Assign(a) =>" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8256
|
+
- `Bash: grep -n "type_env.borrow_mut().insert" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs | head -20`
|
|
8257
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8258
|
+
- `Bash: grep -n "^fn compile_closure_call" -A 45 /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8259
|
+
|
|
8260
|
+
**Claude:** I also need to fix the call-site's own signature re-derivation, which independently re-infers the closure's type from `ctx.type_env` (populated via the same buggy checker inference at the assignment site) — otherwise the two would now disagree. Let's add a side-channel that threads the already-correct `ClosureInfo` through.
|
|
8261
|
+
- `Bash: grep -n "string_concat_func: u32,\s*$" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8262
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8263
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8264
|
+
|
|
8265
|
+
**Claude:** Now let's initialize this field at both `LocalCtx` construction sites:
|
|
8266
|
+
- `Bash: grep -n "type_env: RefCell::new(base_env)," /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8267
|
+
- `Bash: cd /Users/pyrossh/Code/plum python3 - << 'EOF' path = "plum-wasm-codegen/src/lib.rs" src = open(path).read() old = " type_env: RefCell::new(base_env),\n" new =`
|
|
8268
|
+
|
|
8269
|
+
**Claude:** Now let's update the `Assign` handling in `compile_stmt` to populate this map when the value is a closure literal:
|
|
8270
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8271
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8272
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8273
|
+
|
|
8274
|
+
**Claude:** Now let's update `compile_closure_call` to check this map first:
|
|
8275
|
+
- `Bash: grep -n "^fn compile_closure_call" -A 25 /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8276
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8277
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo build -p plum-wasm-codegen 2>&1 | tail -100`
|
|
8278
|
+
|
|
8279
|
+
**Claude:** Compiles cleanly. Now let's write the exact regression test for the documented bug — a closure assigned via `=` and later called at Float and at a class/pointer type.
|
|
8280
|
+
- `Bash: grep -n "fn capturing_closure_snapshots_value_at_creation_time_runs_correctly" -A 20 /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
8281
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
8282
|
+
- `Bash: grep -n "^fn run_main(bytes" -A 10 /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
8283
|
+
|
|
8284
|
+
**Claude:** Let's add an `f64` runner right after `run_main`.
|
|
8285
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
8286
|
+
|
|
8287
|
+
**Claude:** Now let's add the regression tests right after `capturing_closure_snapshots_value_at_creation_time_runs_correctly`.
|
|
8288
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
8289
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
8290
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
8291
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo test -p plum-wasm-codegen --test codegen_tests closure_assigned_then_called -- --nocapture 2>&1 | tail -40`
|
|
8292
|
+
|
|
8293
|
+
**Claude:** Let's extend the resolution to handle the class/pointer case via attribute access (`c.age` implies `c`'s type has an `age` field).
|
|
8294
|
+
- `Bash: grep -n "pub type ClassEnv" /Users/pyrossh/Code/plum/plum-checker/src/lib.rs`
|
|
8295
|
+
- `Bash: grep -n "pub struct CheckCtx" -A 6 /Users/pyrossh/Code/plum/plum-checker/src/lib.rs`
|
|
8296
|
+
|
|
8297
|
+
**Claude:** Now let's update the `Attribute` arm in `scan_expr_for_param_types`:
|
|
8298
|
+
- `Bash: grep -n "ast::Expr::Attribute(a) => {" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8299
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8300
|
+
- `Bash: grep -n "enum AttrKind" -A 6 /Users/pyrossh/Code/plum/plum-core/src/ast.rs`
|
|
8301
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8302
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8303
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo build -p plum-wasm-codegen 2>&1 | tail -60`
|
|
8304
|
+
- `Bash: grep -n "struct Attribute" -A 5 /Users/pyrossh/Code/plum/plum-core/src/ast.rs`
|
|
8305
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8306
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo build -p plum-wasm-codegen 2>&1 | tail -60`
|
|
8307
|
+
|
|
8308
|
+
**Claude:** Compiles. Let's test.
|
|
8309
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo test -p plum-wasm-codegen --test codegen_tests closure_assigned_then_called -- --nocapture 2>&1 | tail -30`
|
|
8310
|
+
- `Bash: cd /Users/pyrossh/Code/plum mkdir -p /tmp/dumpwasm2/src cat > /tmp/dumpwasm2/src/main.rs << 'EOF' fn main() { let src = "\ type Cat = age: Int eachCat(cb: fn(Ca`
|
|
8311
|
+
- `Bash: wasm-tools print /tmp/out2.wasm 2>&1`
|
|
8312
|
+
- `Bash: rm -rf /tmp/dumpwasm2 /tmp/out2.wasm grep -n "impl<'a> Collector" -A 3 /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8313
|
+
- `Bash: sed -n '1490,1560p' /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8314
|
+
- `Bash: grep -n "ast::Expr::Closure" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8315
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8316
|
+
- `Bash: grep -n "let mut collector = Collector {" -A 15 /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs | head -20`
|
|
8317
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8318
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8319
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo build -p plum-wasm-codegen 2>&1 | tail -10 mkdir -p /tmp/dumpwasm3/src cat > /tmp/dumpwasm3/src/main.rs << 'EOF' fn main() { l`
|
|
8320
|
+
|
|
8321
|
+
**Claude:** Found it — a *third* place with the same root problem: the `Collector` (which decides each local's wasm `ValType`) also calls the checker's fragile closure inference, and here it fails outright (returns `Err`, not just a bad `TVar`), silently defaulting `cb`'s local slot to `Int` (i64) instead of the pointer type (i32) every closure actually is. Let's remove my debug print and fix this properly.
|
|
8322
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8323
|
+
|
|
8324
|
+
**Claude:** Now let's fix `Collector::walk_stmt`'s `Assign` arm to not rely on the fragile whole-closure inference for local-slot typing:
|
|
8325
|
+
- `Bash: grep -n "fn walk_stmt(&mut self, stmt: &ast::Stmt) {" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8326
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8327
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8328
|
+
|
|
8329
|
+
**Claude:** Let's apply the same defensive fix to `compile_stmt`'s runtime type-tracking for consistency, then rebuild and test.
|
|
8330
|
+
- `Bash: grep -n "let vty = infer_local_type(value, ctx);" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8331
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8332
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8333
|
+
|
|
8334
|
+
**Claude:** Now let's rebuild and test.
|
|
8335
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo build -p plum-wasm-codegen 2>&1 | tail -20 cargo test -p plum-wasm-codegen --test codegen_tests closure_assigned_then_called -`
|
|
8336
|
+
|
|
8337
|
+
**Claude:** Both pass. Let's clean up scratch files and run the full workspace suite.
|
|
8338
|
+
- `Bash: rm -rf /tmp/dumpwasm3 cd /Users/pyrossh/Code/plum cargo test --workspace 2>&1 | tail -100`
|
|
8339
|
+
|
|
8340
|
+
**Claude:** All green. Gap #5 done — let's update the README.
|
|
8341
|
+
- `Read: /Users/pyrossh/Code/plum/README.md`
|
|
8342
|
+
- `Edit: /Users/pyrossh/Code/plum/README.md`
|
|
8343
|
+
|
|
8344
|
+
**Claude:** Now the final one — gap #7, nested closures. Let's implement it by making `record_closure` recurse into the closure's own body using the same `ClosureWalker` machinery.
|
|
8345
|
+
- `Bash: grep -n "fn record_closure" -A 40 /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs | head -45`
|
|
8346
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8347
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8348
|
+
|
|
8349
|
+
**Claude:** Now let's fix `fv_collect_refs_expr`'s catch-all, which currently skips into nested closures entirely (needed for the outer closure's own capture list to include names its nested closure needs from the outer's scope):
|
|
8350
|
+
- `Bash: grep -n "A nested closure isn't supported by the discovery pre-pass" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8351
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8352
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8353
|
+
|
|
8354
|
+
**Claude:** Let's also clean up the now-inaccurate error message in `compile_closure_literal`.
|
|
8355
|
+
- `Bash: grep -n "nested closures are not supported" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8356
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8357
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8358
|
+
|
|
8359
|
+
**Claude:** Now let's build and test.
|
|
8360
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo build -p plum-wasm-codegen 2>&1 | tail -100`
|
|
8361
|
+
- `Bash: grep -n "param_ptypes,$" /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs | head -5`
|
|
8362
|
+
- `Read: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8363
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/src/lib.rs`
|
|
8364
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo build -p plum-wasm-codegen 2>&1 | tail -60`
|
|
8365
|
+
|
|
8366
|
+
**Claude:** Compiles. Now let's write tests for nested closures.
|
|
8367
|
+
- `Bash: grep -n "fn closure_passed_through_already_generic_higher_order_function_runs_correctly" -A 15 /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
8368
|
+
- `Edit: /Users/pyrossh/Code/plum/plum-wasm-codegen/tests/codegen_tests.rs`
|
|
8369
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo test -p plum-wasm-codegen --test codegen_tests nested_closure -- --nocapture 2>&1 | tail -60`
|
|
8370
|
+
|
|
8371
|
+
**Claude:** Both pass on the first try. Let's run the complete workspace suite one final time.
|
|
8372
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo test --workspace 2>&1 | tail -110`
|
|
8373
|
+
|
|
8374
|
+
**Claude:** All 7 gaps done. Let's do the final README update.
|
|
8375
|
+
- `Read: /Users/pyrossh/Code/plum/README.md`
|
|
8376
|
+
- `Edit: /Users/pyrossh/Code/plum/README.md`
|
|
8377
|
+
- `Edit: /Users/pyrossh/Code/plum/README.md`
|
|
8378
|
+
|
|
8379
|
+
**Claude:** Now the final Known Gaps section:
|
|
8380
|
+
- `Edit: /Users/pyrossh/Code/plum/README.md`
|
|
8381
|
+
|
|
8382
|
+
**Claude:** Let's do one final full verification pass and review the overall diff.
|
|
8383
|
+
- `Bash: cd /Users/pyrossh/Code/plum cargo test --workspace 2>&1 | grep -E "test result:|FAILED|error\[" echo "---" git status --short | head -30`
|
|
8384
|
+
|
|
8385
|
+
**Claude:** All 7 known gaps from the README are now closed, and the entire workspace test suite is green (0 failures across every crate).
|
|
8386
|
+
|
|
8387
|
+
## Summary
|
|
8388
|
+
|
|
8389
|
+
Worked through all 7 gaps in the agreed order (6 → 2 → 3 → 4 → 1 → 5 → 7), verifying each with real `wasmtime` execution before moving on:
|
|
8390
|
+
|
|
8391
|
+
1. **Named function as a `fn(...)`-typed value** (`each(double)`) — compiles as a zero-capture "trampoline" closure: a synthetic wasm function forwarding to the real one, registered once in the funcref table with a compile-time-constant `{table_idx, env_ptr=0}` struct (no runtime allocation needed).
|
|
8392
|
+
|
|
8393
|
+
2. **Multi-subject `match`** (`match a, b`) — reworked the match compiler to evaluate N subjects into N scratch locals and check patterns position-by-position with correct AND/fallthrough semantics. Bonus: generalized the exhaustiveness heuristic to full cross-products, which is what let `libs/std/bool.plum`'s `and`/`or` (`match self, o` over all 4 `Bool×Bool` combinations) compile at all. Also fixed a monomorphize.rs bug that only mangled the first subject's generic-enum variants.
|
|
8394
|
+
|
|
8395
|
+
3. **Nested constructor patterns** (`Some(Some(v))`) — made the constructor-arm field-matching recursive, arbitrary depth, with correct case-to-case fallthrough on a nested mismatch. Also fixed monomorphize.rs to recurse into nested patterns' own field types for generic-enum mangling.
|
|
8396
|
+
|
|
8397
|
+
4. **Multi-line closure body + closing `)` on the same line** — real scanner surgery in `tooling/tree-sitter-plum/src/scanner.c`: taught the external scanner to emit a DEDENT when the parser needs one but no newline preceded (only when gated by the grammar's own `valid_symbols[DEDENT]`, so it can't misfire elsewhere).
|
|
8398
|
+
|
|
8399
|
+
5. **String interpolation codegen** — built two hand-written wasm runtime helpers (`__string_concat`, `__int_to_string`) from scratch, byte-copy loops and all. Caught and fixed two real bugs this way (a missing pointer-arithmetic `add` and a local-index/type mismatch) — both found via `wasm-tools validate`, not guessing. `Str`/`Int`/`Bool` interpolation all work; `Float` gets a clear error (real decimal formatting is its own project).
|
|
8400
|
+
|
|
8401
|
+
6. **`call_indirect` trap for closures assigned then called at Float/class type** — the checker's closure inference gives every param a fresh, never-unified `TVar`, defaulting silently to `Int`. Added a usage-based resolution pass (`resolve_closure_param_types_from_usage`) plus a side-channel (`closure_local_sigs`) so the call site's signature can't drift from what the closure's body was actually compiled with.
|
|
8402
|
+
|
|
8403
|
+
7. **Nested closures** — made the discovery pass recurse into a closure's own body, and fixed free-variable collection to propagate multi-level captures correctly (a name only the inner closure needs still gets captured by the outer, but a name the outer already binds doesn't).
|
|
8404
|
+
|
|
8405
|
+
Also updated the README's closure/match sections to drop language describing now-fixed limitations, and cleaned up now-inaccurate error messages/test names along the way.
|
README.md
CHANGED
|
@@ -262,11 +262,11 @@ main() -> Int =
|
|
|
262
262
|
|
|
263
263
|
A closure literal is `|params| body`: zero or more bare parameter names between pipes, followed by an expression or an indented block body. A parameter's type is inferred from context (e.g. the declared type of the `fn(...)` slot it's passed into) rather than annotated inline.
|
|
264
264
|
|
|
265
|
-
A `fn(...)` / `fn(...) -> T` type annotates a closure-typed parameter or field: `fn(Int) -> Int` is a function from one `Int` to an `Int`; parameter types are positional only (no names). Closures compile to a `{table_index, env_pointer}` pair and are called via `call_indirect` through a function table, so a `fn(...)`-typed value can hold
|
|
265
|
+
A `fn(...)` / `fn(...) -> T` type annotates a closure-typed parameter or field: `fn(Int) -> Int` is a function from one `Int` to an `Int`; parameter types are positional only (no names). Closures compile to a `{table_index, env_pointer}` pair and are called via `call_indirect` through a function table, so a `fn(...)`-typed value can hold a closure literal, a plain top-level named function (`each(double)` — compiled as a zero-capture "trampoline" closure), or a closure nested inside another closure's body.
|
|
266
266
|
|
|
267
267
|
Capture is **snapshot-by-value**: any outer variable a closure body references is copied into the closure's environment at the moment the closure literal is evaluated, not re-read live later. Reassigning the captured variable afterward does not change what the closure sees.
|
|
268
268
|
|
|
269
|
-
Closures parse and compile in two shapes: passed directly as a call argument (`each(|v| v * 3)`
|
|
269
|
+
Closures parse and compile in two shapes: passed directly as a call argument (`each(|v| v * 3)`), or assigned to a local first and called or passed on later (`cb = |v|\n v + offset` followed by `cb(5)` or `each(cb)`). Both support a multi-line indented body.
|
|
270
270
|
|
|
271
271
|
Full example: [`examples/closures.plum`](examples/closures.plum).
|
|
272
272
|
|
|
@@ -327,7 +327,7 @@ match opt
|
|
|
327
327
|
0
|
|
328
328
|
```
|
|
329
329
|
|
|
330
|
-
A case body can be a single inline expression right after `=>`, or an indented block — pick whichever reads better for that arm. Patterns can be: integer/float/string literals, a bare identifier (binds a new local to the subject's value), a bare capitalized tag (`True`, `False`, or any declared `enum` variant with no payload — compared, not bound), a constructor pattern (`Some(v)`, binding its argument), or `_` (wildcard).
|
|
330
|
+
A case body can be a single inline expression right after `=>`, or an indented block — pick whichever reads better for that arm. Patterns can be: integer/float/string literals, a bare identifier (binds a new local to the subject's value), a bare capitalized tag (`True`, `False`, or any declared `enum` variant with no payload — compared, not bound), a constructor pattern (`Some(v)`, binding its argument — sub-patterns can themselves be constructor patterns, e.g. `Wrap(Some(v))`), or `_` (wildcard). `match a, b, ...` against multiple comma-separated subjects is also supported — each case supplies one pattern per subject, and every position must match for that case to apply.
|
|
331
331
|
|
|
332
332
|
Full example: [`examples/match.plum`](examples/match.plum).
|
|
333
333
|
|
|
@@ -338,7 +338,7 @@ greet(name: Str) -> Str =
|
|
|
338
338
|
"Hello, {name}!"
|
|
339
339
|
```
|
|
340
340
|
|
|
341
|
-
`{expr}` inside a string interpolates a single primary expression (a variable, literal, attribute access, etc). Parses
|
|
341
|
+
`{expr}` inside a string interpolates a single primary expression (a variable, literal, attribute access, etc). Parses, type-checks, and compiles for `Str`, `Int`, and `Bool` values; interpolating a `Float` reports a clear error (see below).
|
|
342
342
|
|
|
343
343
|
Full example: [`examples/strings.plum`](examples/strings.plum).
|
|
344
344
|
|
|
@@ -346,11 +346,5 @@ Full example: [`examples/strings.plum`](examples/strings.plum).
|
|
|
346
346
|
|
|
347
347
|
Some things parse and type-check but don't compile to wasm yet — `plum-wasm-codegen` reports a clear error rather than silently producing wrong code:
|
|
348
348
|
|
|
349
|
-
- string interpolation
|
|
350
|
-
-
|
|
351
|
-
- nested constructor patterns inside `match` (`Some(Some(v))`) — a constructor pattern's own sub-patterns must be a bare binding or `_`
|
|
352
|
-
- `libs/std`'s actual `List`/`Map`/`Option`/`Result` still don't compile — they use several other unimplemented features (`Nil`/optional chaining, decorators, colon-arrow return syntax) unrelated to generics, which are themselves now monomorphized and compiled correctly for the currently-documented generic syntax
|
|
353
|
-
- a closure literal used directly as a call argument only parses with a single-line body (`each(|v| v * 3)`); a multi-line indented body immediately followed by a closing `)` on the same line as the body's last statement (e.g. `each(|v|\n v)`) does not yet parse — the external scanner doesn't emit a `DEDENT` at a closing bracket. Assigning the closure to a local first (`cb = |v|\n v + offset` then `cb(...)` or `each(cb)`) does support a multi-line body and sidesteps this
|
|
354
|
-
- a closure created via assignment (not passed directly as a call argument) and later called at a concrete non-`Int` type (`Float` or a class/pointer type) can hit a wasm runtime trap due to a signature-resolution gap in `call_indirect` dispatch; this does not affect closures over `Int`, or closures passed directly as call arguments at any type. Tracked as a separate follow-up task
|
|
355
|
-
- a plain top-level named function (not a `|params| body` closure literal) cannot yet be used directly as a `fn(...)`-typed value (e.g. `each(double)`) — only closure literals are supported at closure-typed call sites today
|
|
356
|
-
- a closure literal nested inside another closure's body is not supported (produces a clear compile error)
|
|
349
|
+
- interpolating a `Float` value in a string (`Str`/`Int`/`Bool` interpolation, and plain non-interpolated literals, all compile) — correct decimal formatting of a float is a substantial separate undertaking (something like Grisu/Ryu), scoped out for now
|
|
350
|
+
- `libs/std`'s actual `List`/`Map` still don't fully compile — mutating a field or attribute (`self.head = ...`) isn't a supported assignment target yet (only a plain local variable is), and there's no cross-file import resolution yet either, so a file that references a type/enum declared in a different `libs/std` file won't type-check standalone
|
plum-checker/src/monomorphize.rs
CHANGED
|
@@ -326,6 +326,55 @@ impl<'a> Monomorphizer<'a> {
|
|
|
326
326
|
Ok(())
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
+
/// Rewrites `pat` (bare-name/constructor pattern) in place: an uppercase variant
|
|
330
|
+
/// name gets mangled to its specialized form (`Some` -> `Some$Int`) if `mangling`
|
|
331
|
+
/// says this position's subject is a specialized generic enum; a plain binding
|
|
332
|
+
/// name is inserted into `case_env` at `ty`. Recurses into a constructor
|
|
333
|
+
/// pattern's own fields (`Some(Some(v))`), looking up *that* field's own
|
|
334
|
+
/// mangling table from `self.enum_variant_mangling` — a nested sub-pattern can
|
|
335
|
+
/// be a specialization independent of its enclosing pattern's.
|
|
336
|
+
fn mangle_pattern(
|
|
337
|
+
&self,
|
|
338
|
+
pat: &mut ast::CasePattern,
|
|
339
|
+
ty: &PlumType,
|
|
340
|
+
mangling: Option<&BTreeMap<String, String>>,
|
|
341
|
+
case_env: &mut TypeEnv,
|
|
342
|
+
) {
|
|
343
|
+
match pat {
|
|
344
|
+
ast::CasePattern::Name(n) => {
|
|
345
|
+
let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
|
|
346
|
+
&& self.enum_variants.contains_key(n.as_str());
|
|
347
|
+
if is_variant {
|
|
348
|
+
if let Some(table) = mangling {
|
|
349
|
+
if let Some(mangled_variant) = table.get(n) {
|
|
350
|
+
*n = mangled_variant.clone();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
} else {
|
|
354
|
+
case_env.insert(n.clone(), TypeScheme::mono(ty.clone()));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
ast::CasePattern::Class { name, fields } => {
|
|
358
|
+
if let Some(table) = mangling {
|
|
359
|
+
if let Some(mangled_variant) = table.get(name) {
|
|
360
|
+
*name = mangled_variant.clone();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if let Some(info) = self.enum_variants.get(name.as_str()) {
|
|
364
|
+
let field_types = info.field_types.clone();
|
|
365
|
+
for (f, fty) in fields.iter_mut().zip(field_types.iter()) {
|
|
366
|
+
let field_mangling = match fty {
|
|
367
|
+
PlumType::TNamed(n) => self.enum_variant_mangling.get(n),
|
|
368
|
+
_ => None,
|
|
369
|
+
};
|
|
370
|
+
self.mangle_pattern(f, fty, field_mangling, case_env);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
_ => {}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
329
378
|
fn rewrite_stmt(&mut self, stmt: &mut ast::Stmt, env: &mut TypeEnv) -> Result<(), String> {
|
|
330
379
|
match stmt {
|
|
331
380
|
ast::Stmt::Assign(a) => {
|
|
@@ -366,42 +415,28 @@ impl<'a> Monomorphizer<'a> {
|
|
|
366
415
|
for s in &mut m.subjects {
|
|
367
416
|
self.rewrite_expr(s, env)?;
|
|
368
417
|
}
|
|
418
|
+
// One (type, variant-mangling table) pair per subject — `match a, b`
|
|
419
|
+
// needs each position's own generic-enum specialization handled
|
|
420
|
+
// independently, not just the first subject's.
|
|
369
|
-
let
|
|
421
|
+
let subject_types: Vec<PlumType> = m.subjects.iter().map(|s| self.infer(s, env)).collect();
|
|
370
|
-
// If
|
|
422
|
+
// If a subject's concrete type is a specialized generic enum, its
|
|
371
|
-
// variant-name mangling table lets us rewrite
|
|
423
|
+
// variant-name mangling table lets us rewrite that position's patterns
|
|
372
424
|
// (`Some`/`None` -> `Some$Int`/`None$Int`) to reference the correct
|
|
373
425
|
// specialization, so the checker/codegen's unmodified, bare-name-keyed
|
|
374
426
|
// `EnumVariants` lookup still resolves each pattern correctly.
|
|
375
|
-
let
|
|
427
|
+
let variant_manglings: Vec<Option<BTreeMap<String, String>>> = subject_types
|
|
428
|
+
.iter()
|
|
429
|
+
.map(|ty| match ty {
|
|
376
|
-
|
|
430
|
+
PlumType::TNamed(n) => self.enum_variant_mangling.get(n).cloned(),
|
|
377
|
-
|
|
431
|
+
_ => None,
|
|
378
|
-
|
|
432
|
+
})
|
|
433
|
+
.collect();
|
|
379
434
|
for case in &mut m.cases {
|
|
380
435
|
let mut case_env = env.clone();
|
|
381
|
-
if let Some(pat) = case.patterns.first_mut() {
|
|
382
|
-
match pat {
|
|
383
|
-
ast::CasePattern::Name(n) => {
|
|
384
|
-
let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
|
|
385
|
-
&& self.enum_variants.contains_key(n.as_str());
|
|
386
|
-
if is_variant {
|
|
387
|
-
|
|
436
|
+
for (pat, (subject_ty, variant_mangling)) in
|
|
388
|
-
if let Some(mangled_variant) = table.get(n) {
|
|
389
|
-
*n = mangled_variant.clone();
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
} else {
|
|
393
|
-
|
|
437
|
+
case.patterns.iter_mut().zip(subject_types.iter().zip(variant_manglings.iter()))
|
|
394
|
-
|
|
438
|
+
{
|
|
395
|
-
}
|
|
396
|
-
ast::CasePattern::Class { name, .. } => {
|
|
397
|
-
|
|
439
|
+
self.mangle_pattern(pat, subject_ty, variant_mangling.as_ref(), &mut case_env);
|
|
398
|
-
if let Some(mangled_variant) = table.get(name) {
|
|
399
|
-
*name = mangled_variant.clone();
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
_ => {}
|
|
404
|
-
}
|
|
405
440
|
}
|
|
406
441
|
self.rewrite_block(&mut case.body, &mut case_env)?;
|
|
407
442
|
}
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
use wasm_encoder::*;
|
|
2
2
|
use std::cell::RefCell;
|
|
3
|
-
use std::collections::HashMap;
|
|
3
|
+
use std::collections::{HashMap, HashSet};
|
|
4
4
|
use plum_core::ast;
|
|
5
5
|
use plum_checker::types::{PlumType, TypeEnv, TypeScheme};
|
|
6
6
|
use plum_checker::{ClassEnv, MethodEnv, EnumVariants, EnumVariantInfo};
|
|
@@ -256,6 +256,19 @@ pub struct CompileCtx<'a> {
|
|
|
256
256
|
pub closure_asts: HashMap<usize, &'a ast::Closure>,
|
|
257
257
|
/// Closure wasm signature -> function-type index, for `call_indirect` at call sites.
|
|
258
258
|
pub closure_call_types: HashMap<ClosureSigKey, u32>,
|
|
259
|
+
/// Top-level function name -> static data offset of its zero-capture "trampoline"
|
|
260
|
+
/// closure struct `{table_idx, env_ptr=0}`, for using a plain named function
|
|
261
|
+
/// wherever a `fn(...)`-typed value is expected (e.g. `each(double)`). Since the
|
|
262
|
+
/// struct has no captures it never changes, so it's baked into a data segment
|
|
263
|
+
/// once at compile time instead of being bump-allocated per reference.
|
|
264
|
+
pub named_fn_values: HashMap<String, u32>,
|
|
265
|
+
/// Shared runtime helper `(a: i32, b: i32) -> i32`: bump-allocates a new
|
|
266
|
+
/// length-prefixed string exactly long enough to hold `a`'s bytes followed by
|
|
267
|
+
/// `b`'s, for lowering string interpolation (`"{expr}"`).
|
|
268
|
+
pub string_concat_func: u32,
|
|
269
|
+
/// Shared runtime helper `(n: i64) -> i32`: bump-allocates a new length-prefixed
|
|
270
|
+
/// string holding `n`'s decimal representation, for interpolating an `Int`.
|
|
271
|
+
pub int_to_string_func: u32,
|
|
259
272
|
}
|
|
260
273
|
|
|
261
274
|
/// Per-module state that accumulates as function bodies are compiled: the running
|
|
@@ -280,10 +293,19 @@ struct LocalCtx<'a> {
|
|
|
280
293
|
closure_scratch_base: u32,
|
|
281
294
|
/// `Closure` expr identity (pointer address) -> offset of its first (of two) slots.
|
|
282
295
|
closure_scratch: HashMap<usize, u32>,
|
|
296
|
+
/// First local index reserved for nested-constructor-pattern scratch temporaries
|
|
297
|
+
/// (`Some(Some(v))`'s inner `Some(v)`); the outermost pattern uses a
|
|
298
|
+
/// `match_scratch` slot instead, so this only covers depth >= 1.
|
|
299
|
+
nested_class_scratch_base: u32,
|
|
300
|
+
/// `CasePattern::Class` identity (pointer address) -> scratch slot offset.
|
|
301
|
+
nested_class_scratch: HashMap<usize, u32>,
|
|
283
302
|
func_ids: &'a HashMap<String, u32>,
|
|
284
303
|
func_sigs: &'a HashMap<String, FuncSig>,
|
|
285
304
|
closures: &'a HashMap<usize, ClosureInfo>,
|
|
286
305
|
closure_call_types: &'a HashMap<ClosureSigKey, u32>,
|
|
306
|
+
named_fn_values: &'a HashMap<String, u32>,
|
|
307
|
+
string_concat_func: u32,
|
|
308
|
+
int_to_string_func: u32,
|
|
287
309
|
classes: &'a ClassEnv,
|
|
288
310
|
methods: &'a MethodEnv,
|
|
289
311
|
enum_variants: &'a EnumVariants,
|
|
@@ -291,6 +313,15 @@ struct LocalCtx<'a> {
|
|
|
291
313
|
/// statements in order, mirroring `plum-checker`'s own env evolution — needed
|
|
292
314
|
/// to resolve `Attribute`/`ClassCall` targets and pick the right load/store width.
|
|
293
315
|
type_env: RefCell<TypeEnv>,
|
|
316
|
+
/// Local name -> the exact wasm `call_indirect` signature of the closure literal
|
|
317
|
+
/// assigned to it (populated when compiling that `Stmt::Assign`, straight from
|
|
318
|
+
/// the already-correct `ClosureInfo` the discovery pass computed). Exists so
|
|
319
|
+
/// `compile_closure_call` doesn't have to re-derive the signature via
|
|
320
|
+
/// `plum_checker::infer_expr` on the closure a second time — which, unlike the
|
|
321
|
+
/// discovery pass, doesn't have `resolve_closure_param_types_from_usage`'s fix
|
|
322
|
+
/// and would fall back to its old TVar-defaults-to-Int behavior, disagreeing
|
|
323
|
+
/// with the (now correct) signature the closure's body was actually compiled with.
|
|
324
|
+
closure_local_sigs: RefCell<HashMap<String, ClosureSigKey>>,
|
|
294
325
|
bump_global: u32,
|
|
295
326
|
}
|
|
296
327
|
|
|
@@ -390,6 +421,7 @@ pub fn compile_source(source: &ast::Source) -> Result<Vec<u8>, String> {
|
|
|
390
421
|
|
|
391
422
|
let mut module = WasmModule::new();
|
|
392
423
|
module.add_memory(2, None);
|
|
424
|
+
module.add_export("memory", ExportKind::Memory, 0);
|
|
393
425
|
let mut bump_init = Vec::new();
|
|
394
426
|
Instruction::I32Const(HEAP_BASE as i32).encode(&mut bump_init);
|
|
395
427
|
let bump_global = module.add_global(ValType::I32, true, &bump_init);
|
|
@@ -447,25 +479,31 @@ pub fn compile_source(source: &ast::Source) -> Result<Vec<u8>, String> {
|
|
|
447
479
|
// element and records the free variables it must capture.
|
|
448
480
|
let fn_decls: HashMap<String, &ast::Fn> = fns.iter().map(|f| (f.name.clone(), *f)).collect();
|
|
449
481
|
let mut raw_closures: Vec<RawClosure> = Vec::new();
|
|
482
|
+
let mut named_fn_refs: HashSet<String> = HashSet::new();
|
|
450
483
|
for f in &fns {
|
|
451
484
|
let mut env = global_env.clone();
|
|
452
485
|
if let Some(recv) = &f.type_param {
|
|
453
486
|
env.insert("self".to_string(), TypeScheme::mono(PlumType::TNamed(recv.clone())));
|
|
454
487
|
}
|
|
488
|
+
let mut locals: HashSet<String> = HashSet::new();
|
|
455
489
|
for p in &f.params {
|
|
456
490
|
env.insert(p.name.clone(), TypeScheme::mono(param_plum_type(&p.ty)));
|
|
491
|
+
locals.insert(p.name.clone());
|
|
457
492
|
}
|
|
458
493
|
let mut walker = ClosureWalker {
|
|
459
494
|
env,
|
|
460
495
|
cctx: check_ctx_of(&classes, &methods, &enum_variants),
|
|
461
496
|
fn_decls: &fn_decls,
|
|
462
497
|
found: Vec::new(),
|
|
498
|
+
locals,
|
|
499
|
+
named_fn_refs: HashSet::new(),
|
|
463
500
|
};
|
|
464
501
|
match &f.body {
|
|
465
502
|
ast::FnBody::Block(block) => walker.walk_block(block),
|
|
466
503
|
ast::FnBody::Expr(e) => walker.walk_expr(e, None),
|
|
467
504
|
}
|
|
468
505
|
raw_closures.extend(walker.found);
|
|
506
|
+
named_fn_refs.extend(walker.named_fn_refs);
|
|
469
507
|
}
|
|
470
508
|
|
|
471
509
|
let mut closures: HashMap<usize, ClosureInfo> = HashMap::new();
|
|
@@ -496,12 +534,64 @@ pub fn compile_source(source: &ast::Source) -> Result<Vec<u8>, String> {
|
|
|
496
534
|
closure_asts.insert(rc.ptr, rc.closure);
|
|
497
535
|
}
|
|
498
536
|
|
|
537
|
+
// Register a zero-capture "trampoline" closure for every top-level function
|
|
538
|
+
// referenced as a bare value (e.g. `each(double)`): a real wasm function with the
|
|
539
|
+
// closure calling convention `(env_ptr, ...real_params) -> ret` that ignores its
|
|
540
|
+
// env pointer and forwards straight to the real function, plus a funcref-table
|
|
541
|
+
// entry for it. Since it never captures anything, its `{table_idx, env_ptr=0}`
|
|
542
|
+
// closure struct is a compile-time constant — baked into a data segment once
|
|
543
|
+
// rather than bump-allocated at every reference site.
|
|
544
|
+
let mut named_fn_values: HashMap<String, u32> = HashMap::new();
|
|
545
|
+
let mut named_fn_data_segments: Vec<(u32, Vec<u8>)> = Vec::new();
|
|
546
|
+
let mut next_static_offset = STRING_BASE;
|
|
547
|
+
for name in &named_fn_refs {
|
|
548
|
+
let sig = func_sigs.get(name).expect("named fn ref must be a registered top-level function");
|
|
549
|
+
let real_func_idx = *func_ids.get(name).expect("named fn ref must be a registered top-level function");
|
|
550
|
+
|
|
551
|
+
let mut sig_params = vec![ValType::I32]; // env pointer
|
|
552
|
+
sig_params.extend(sig.params.iter().copied());
|
|
553
|
+
let sig_key: ClosureSigKey = (sig_params.clone(), sig.ret);
|
|
554
|
+
let type_idx = match closure_call_types.get(&sig_key) {
|
|
555
|
+
Some(t) => *t,
|
|
556
|
+
None => {
|
|
557
|
+
let results: Vec<ValType> = sig.ret.into_iter().collect();
|
|
558
|
+
let t = module.add_type(&sig_params, &results);
|
|
559
|
+
closure_call_types.insert(sig_key, t);
|
|
560
|
+
t
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
let mut tbody = Vec::new();
|
|
565
|
+
tbody.push(0u8); // no locals beyond the params already in the signature
|
|
566
|
+
for i in 0..sig.params.len() {
|
|
567
|
+
Instruction::LocalGet((i + 1) as u32).encode(&mut tbody); // local 0 is env_ptr, ignored
|
|
568
|
+
}
|
|
569
|
+
Instruction::Call(real_func_idx).encode(&mut tbody);
|
|
570
|
+
Instruction::End.encode(&mut tbody);
|
|
571
|
+
let func_idx = module.add_function(type_idx, &tbody);
|
|
572
|
+
let table_idx = module.add_table_element(func_idx);
|
|
573
|
+
|
|
574
|
+
let offset = next_static_offset;
|
|
575
|
+
next_static_offset += 8;
|
|
576
|
+
let mut bytes = Vec::with_capacity(8);
|
|
577
|
+
bytes.extend_from_slice(&(table_idx as i32).to_le_bytes());
|
|
578
|
+
bytes.extend_from_slice(&0i32.to_le_bytes());
|
|
579
|
+
named_fn_data_segments.push((offset, bytes));
|
|
580
|
+
named_fn_values.insert(name.clone(), offset);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Runtime helpers backing string interpolation (`"{expr}"`): always registered
|
|
584
|
+
// (unconditionally, for simplicity) since they're cheap and self-contained.
|
|
585
|
+
let string_concat_func = register_string_concat_helper(&mut module, bump_global);
|
|
586
|
+
let int_to_string_func = register_int_to_string_helper(&mut module, bump_global);
|
|
587
|
+
|
|
499
588
|
let ctx = CompileCtx {
|
|
500
589
|
func_ids, func_sigs, classes, methods, enum_variants, global_env, bump_global,
|
|
501
|
-
closures, closure_asts, closure_call_types,
|
|
590
|
+
closures, closure_asts, closure_call_types, named_fn_values,
|
|
591
|
+
string_concat_func, int_to_string_func,
|
|
502
592
|
};
|
|
503
593
|
|
|
504
|
-
let mut state = ModuleState { next_string_offset:
|
|
594
|
+
let mut state = ModuleState { next_string_offset: next_static_offset, data_segments: named_fn_data_segments };
|
|
505
595
|
|
|
506
596
|
let mut compiled_bodies: Vec<(String, Vec<u8>)> = Vec::new();
|
|
507
597
|
for f in &fns {
|
|
@@ -539,6 +629,259 @@ pub fn compile_source(source: &ast::Source) -> Result<Vec<u8>, String> {
|
|
|
539
629
|
Ok(module.finish())
|
|
540
630
|
}
|
|
541
631
|
|
|
632
|
+
/// Registers `__string_concat(a: i32, b: i32) -> i32`, a hand-written runtime
|
|
633
|
+
/// helper backing string interpolation: bump-allocates a new length-prefixed
|
|
634
|
+
/// string (`[len: u32][bytes]`, matching a literal string's own layout) exactly
|
|
635
|
+
/// long enough for `a`'s bytes followed by `b`'s, and copies them in. There's no
|
|
636
|
+
/// bulk-memory `memory.copy` assumed available, so each byte is copied by an
|
|
637
|
+
/// explicit loop.
|
|
638
|
+
fn register_string_concat_helper(module: &mut WasmModule, bump_global: u32) -> u32 {
|
|
639
|
+
let type_idx = module.add_type(&[ValType::I32, ValType::I32], &[ValType::I32]);
|
|
640
|
+
|
|
641
|
+
// locals: 0=a (param), 1=b (param), 2=len_a, 3=len_b, 4=result, 5=i
|
|
642
|
+
const A: u32 = 0;
|
|
643
|
+
const B: u32 = 1;
|
|
644
|
+
const LEN_A: u32 = 2;
|
|
645
|
+
const LEN_B: u32 = 3;
|
|
646
|
+
const RESULT: u32 = 4;
|
|
647
|
+
const I: u32 = 5;
|
|
648
|
+
|
|
649
|
+
let mut body = Vec::new();
|
|
650
|
+
body.extend(encode_leb128_u32(1)); // one locals group
|
|
651
|
+
body.extend(encode_leb128_u32(4)); // 4 locals (len_a, len_b, result, i)
|
|
652
|
+
ValType::I32.encode(&mut body);
|
|
653
|
+
|
|
654
|
+
// len_a = load(a); len_b = load(b)
|
|
655
|
+
Instruction::LocalGet(A).encode(&mut body);
|
|
656
|
+
Instruction::I32Load(MemArg { offset: 0, align: 2, memory_index: 0 }).encode(&mut body);
|
|
657
|
+
Instruction::LocalSet(LEN_A).encode(&mut body);
|
|
658
|
+
Instruction::LocalGet(B).encode(&mut body);
|
|
659
|
+
Instruction::I32Load(MemArg { offset: 0, align: 2, memory_index: 0 }).encode(&mut body);
|
|
660
|
+
Instruction::LocalSet(LEN_B).encode(&mut body);
|
|
661
|
+
|
|
662
|
+
// result = bump; bump += 4 + len_a + len_b
|
|
663
|
+
Instruction::GlobalGet(bump_global).encode(&mut body);
|
|
664
|
+
Instruction::LocalSet(RESULT).encode(&mut body);
|
|
665
|
+
Instruction::GlobalGet(bump_global).encode(&mut body);
|
|
666
|
+
Instruction::LocalGet(LEN_A).encode(&mut body);
|
|
667
|
+
Instruction::LocalGet(LEN_B).encode(&mut body);
|
|
668
|
+
Instruction::I32Add.encode(&mut body);
|
|
669
|
+
Instruction::I32Const(4).encode(&mut body);
|
|
670
|
+
Instruction::I32Add.encode(&mut body);
|
|
671
|
+
Instruction::I32Add.encode(&mut body);
|
|
672
|
+
Instruction::GlobalSet(bump_global).encode(&mut body);
|
|
673
|
+
|
|
674
|
+
// store combined length at result+0
|
|
675
|
+
Instruction::LocalGet(RESULT).encode(&mut body);
|
|
676
|
+
Instruction::LocalGet(LEN_A).encode(&mut body);
|
|
677
|
+
Instruction::LocalGet(LEN_B).encode(&mut body);
|
|
678
|
+
Instruction::I32Add.encode(&mut body);
|
|
679
|
+
Instruction::I32Store(MemArg { offset: 0, align: 2, memory_index: 0 }).encode(&mut body);
|
|
680
|
+
|
|
681
|
+
// for (i = 0; i < len_a; i++) result[4+i] = a[4+i]
|
|
682
|
+
Instruction::I32Const(0).encode(&mut body);
|
|
683
|
+
Instruction::LocalSet(I).encode(&mut body);
|
|
684
|
+
Instruction::Block(BlockType::Empty).encode(&mut body);
|
|
685
|
+
Instruction::Loop(BlockType::Empty).encode(&mut body);
|
|
686
|
+
Instruction::LocalGet(I).encode(&mut body);
|
|
687
|
+
Instruction::LocalGet(LEN_A).encode(&mut body);
|
|
688
|
+
Instruction::I32GeU.encode(&mut body);
|
|
689
|
+
Instruction::BrIf(1).encode(&mut body);
|
|
690
|
+
// address = result + i
|
|
691
|
+
Instruction::LocalGet(RESULT).encode(&mut body);
|
|
692
|
+
Instruction::LocalGet(I).encode(&mut body);
|
|
693
|
+
Instruction::I32Add.encode(&mut body);
|
|
694
|
+
// value = load8u(a + i, offset 4)
|
|
695
|
+
Instruction::LocalGet(A).encode(&mut body);
|
|
696
|
+
Instruction::LocalGet(I).encode(&mut body);
|
|
697
|
+
Instruction::I32Add.encode(&mut body);
|
|
698
|
+
Instruction::I32Load8U(MemArg { offset: 4, align: 0, memory_index: 0 }).encode(&mut body);
|
|
699
|
+
Instruction::I32Store8(MemArg { offset: 4, align: 0, memory_index: 0 }).encode(&mut body);
|
|
700
|
+
Instruction::LocalGet(I).encode(&mut body);
|
|
701
|
+
Instruction::I32Const(1).encode(&mut body);
|
|
702
|
+
Instruction::I32Add.encode(&mut body);
|
|
703
|
+
Instruction::LocalSet(I).encode(&mut body);
|
|
704
|
+
Instruction::Br(0).encode(&mut body);
|
|
705
|
+
Instruction::End.encode(&mut body);
|
|
706
|
+
Instruction::End.encode(&mut body);
|
|
707
|
+
|
|
708
|
+
// for (i = 0; i < len_b; i++) result[4+len_a+i] = b[4+i]
|
|
709
|
+
Instruction::I32Const(0).encode(&mut body);
|
|
710
|
+
Instruction::LocalSet(I).encode(&mut body);
|
|
711
|
+
Instruction::Block(BlockType::Empty).encode(&mut body);
|
|
712
|
+
Instruction::Loop(BlockType::Empty).encode(&mut body);
|
|
713
|
+
Instruction::LocalGet(I).encode(&mut body);
|
|
714
|
+
Instruction::LocalGet(LEN_B).encode(&mut body);
|
|
715
|
+
Instruction::I32GeU.encode(&mut body);
|
|
716
|
+
Instruction::BrIf(1).encode(&mut body);
|
|
717
|
+
// address = result + len_a + i
|
|
718
|
+
Instruction::LocalGet(RESULT).encode(&mut body);
|
|
719
|
+
Instruction::LocalGet(LEN_A).encode(&mut body);
|
|
720
|
+
Instruction::I32Add.encode(&mut body);
|
|
721
|
+
Instruction::LocalGet(I).encode(&mut body);
|
|
722
|
+
Instruction::I32Add.encode(&mut body);
|
|
723
|
+
// value = load8u(b + i, offset 4)
|
|
724
|
+
Instruction::LocalGet(B).encode(&mut body);
|
|
725
|
+
Instruction::LocalGet(I).encode(&mut body);
|
|
726
|
+
Instruction::I32Add.encode(&mut body);
|
|
727
|
+
Instruction::I32Load8U(MemArg { offset: 4, align: 0, memory_index: 0 }).encode(&mut body);
|
|
728
|
+
Instruction::I32Store8(MemArg { offset: 4, align: 0, memory_index: 0 }).encode(&mut body);
|
|
729
|
+
Instruction::LocalGet(I).encode(&mut body);
|
|
730
|
+
Instruction::I32Const(1).encode(&mut body);
|
|
731
|
+
Instruction::I32Add.encode(&mut body);
|
|
732
|
+
Instruction::LocalSet(I).encode(&mut body);
|
|
733
|
+
Instruction::Br(0).encode(&mut body);
|
|
734
|
+
Instruction::End.encode(&mut body);
|
|
735
|
+
Instruction::End.encode(&mut body);
|
|
736
|
+
|
|
737
|
+
Instruction::LocalGet(RESULT).encode(&mut body);
|
|
738
|
+
Instruction::End.encode(&mut body);
|
|
739
|
+
|
|
740
|
+
module.add_function(type_idx, &body)
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/// Registers `__int_to_string(n: i64) -> i32`, a hand-written runtime helper
|
|
744
|
+
/// backing string interpolation: bump-allocates a new length-prefixed string
|
|
745
|
+
/// holding `n`'s decimal representation (handling a leading `-` for negatives, and
|
|
746
|
+
/// `0` correctly via a do-while digit count that always runs at least once).
|
|
747
|
+
fn register_int_to_string_helper(module: &mut WasmModule, bump_global: u32) -> u32 {
|
|
748
|
+
let type_idx = module.add_type(&[ValType::I64], &[ValType::I32]);
|
|
749
|
+
|
|
750
|
+
// locals: 0=n (param, i64), 1=is_neg (i32), 2=abs_n (i64), 3=count (i32),
|
|
751
|
+
// 4=total_len (i32), 5=result (i32), 6=pos (i32), 7=temp (i64)
|
|
752
|
+
// Locals are declared as one group of 5 `i32`s followed by one group of 2
|
|
753
|
+
// `i64`s (see below), so indices must stay grouped by type in that same order —
|
|
754
|
+
// NOT in whatever order reads best logically.
|
|
755
|
+
const N: u32 = 0;
|
|
756
|
+
const IS_NEG: u32 = 1;
|
|
757
|
+
const COUNT: u32 = 2;
|
|
758
|
+
const TOTAL_LEN: u32 = 3;
|
|
759
|
+
const RESULT: u32 = 4;
|
|
760
|
+
const POS: u32 = 5;
|
|
761
|
+
const ABS_N: u32 = 6;
|
|
762
|
+
const TEMP: u32 = 7;
|
|
763
|
+
|
|
764
|
+
let mut body = Vec::new();
|
|
765
|
+
body.extend(encode_leb128_u32(2)); // two locals groups
|
|
766
|
+
body.extend(encode_leb128_u32(5)); // is_neg, count, total_len, result, pos: i32
|
|
767
|
+
ValType::I32.encode(&mut body);
|
|
768
|
+
body.extend(encode_leb128_u32(2)); // abs_n, temp: i64
|
|
769
|
+
ValType::I64.encode(&mut body);
|
|
770
|
+
|
|
771
|
+
// is_neg = n < 0
|
|
772
|
+
Instruction::LocalGet(N).encode(&mut body);
|
|
773
|
+
Instruction::I64Const(0).encode(&mut body);
|
|
774
|
+
Instruction::I64LtS.encode(&mut body);
|
|
775
|
+
Instruction::LocalSet(IS_NEG).encode(&mut body);
|
|
776
|
+
|
|
777
|
+
// abs_n = is_neg ? (0 - n) : n
|
|
778
|
+
Instruction::LocalGet(IS_NEG).encode(&mut body);
|
|
779
|
+
Instruction::If(BlockType::Result(ValType::I64)).encode(&mut body);
|
|
780
|
+
Instruction::I64Const(0).encode(&mut body);
|
|
781
|
+
Instruction::LocalGet(N).encode(&mut body);
|
|
782
|
+
Instruction::I64Sub.encode(&mut body);
|
|
783
|
+
Instruction::Else.encode(&mut body);
|
|
784
|
+
Instruction::LocalGet(N).encode(&mut body);
|
|
785
|
+
Instruction::End.encode(&mut body);
|
|
786
|
+
Instruction::LocalSet(ABS_N).encode(&mut body);
|
|
787
|
+
|
|
788
|
+
// count digits: do { temp /= 10; count++ } while (temp != 0); temp starts as abs_n
|
|
789
|
+
Instruction::I32Const(0).encode(&mut body);
|
|
790
|
+
Instruction::LocalSet(COUNT).encode(&mut body);
|
|
791
|
+
Instruction::LocalGet(ABS_N).encode(&mut body);
|
|
792
|
+
Instruction::LocalSet(TEMP).encode(&mut body);
|
|
793
|
+
Instruction::Loop(BlockType::Empty).encode(&mut body);
|
|
794
|
+
Instruction::LocalGet(TEMP).encode(&mut body);
|
|
795
|
+
Instruction::I64Const(10).encode(&mut body);
|
|
796
|
+
Instruction::I64DivS.encode(&mut body);
|
|
797
|
+
Instruction::LocalSet(TEMP).encode(&mut body);
|
|
798
|
+
Instruction::LocalGet(COUNT).encode(&mut body);
|
|
799
|
+
Instruction::I32Const(1).encode(&mut body);
|
|
800
|
+
Instruction::I32Add.encode(&mut body);
|
|
801
|
+
Instruction::LocalSet(COUNT).encode(&mut body);
|
|
802
|
+
Instruction::LocalGet(TEMP).encode(&mut body);
|
|
803
|
+
Instruction::I64Const(0).encode(&mut body);
|
|
804
|
+
Instruction::I64Ne.encode(&mut body);
|
|
805
|
+
Instruction::BrIf(0).encode(&mut body);
|
|
806
|
+
Instruction::End.encode(&mut body);
|
|
807
|
+
|
|
808
|
+
// total_len = count + is_neg
|
|
809
|
+
Instruction::LocalGet(COUNT).encode(&mut body);
|
|
810
|
+
Instruction::LocalGet(IS_NEG).encode(&mut body);
|
|
811
|
+
Instruction::I32Add.encode(&mut body);
|
|
812
|
+
Instruction::LocalSet(TOTAL_LEN).encode(&mut body);
|
|
813
|
+
|
|
814
|
+
// result = bump; bump += 4 + total_len
|
|
815
|
+
Instruction::GlobalGet(bump_global).encode(&mut body);
|
|
816
|
+
Instruction::LocalSet(RESULT).encode(&mut body);
|
|
817
|
+
Instruction::GlobalGet(bump_global).encode(&mut body);
|
|
818
|
+
Instruction::LocalGet(TOTAL_LEN).encode(&mut body);
|
|
819
|
+
Instruction::I32Const(4).encode(&mut body);
|
|
820
|
+
Instruction::I32Add.encode(&mut body);
|
|
821
|
+
Instruction::I32Add.encode(&mut body);
|
|
822
|
+
Instruction::GlobalSet(bump_global).encode(&mut body);
|
|
823
|
+
|
|
824
|
+
// store total_len at result+0
|
|
825
|
+
Instruction::LocalGet(RESULT).encode(&mut body);
|
|
826
|
+
Instruction::LocalGet(TOTAL_LEN).encode(&mut body);
|
|
827
|
+
Instruction::I32Store(MemArg { offset: 0, align: 2, memory_index: 0 }).encode(&mut body);
|
|
828
|
+
|
|
829
|
+
// pos = 4 + total_len; temp = abs_n
|
|
830
|
+
Instruction::I32Const(4).encode(&mut body);
|
|
831
|
+
Instruction::LocalGet(TOTAL_LEN).encode(&mut body);
|
|
832
|
+
Instruction::I32Add.encode(&mut body);
|
|
833
|
+
Instruction::LocalSet(POS).encode(&mut body);
|
|
834
|
+
Instruction::LocalGet(ABS_N).encode(&mut body);
|
|
835
|
+
Instruction::LocalSet(TEMP).encode(&mut body);
|
|
836
|
+
|
|
837
|
+
// do { pos--; result[pos] = '0' + temp % 10; temp /= 10 } while (pos > 4 + is_neg)
|
|
838
|
+
Instruction::Loop(BlockType::Empty).encode(&mut body);
|
|
839
|
+
Instruction::LocalGet(POS).encode(&mut body);
|
|
840
|
+
Instruction::I32Const(1).encode(&mut body);
|
|
841
|
+
Instruction::I32Sub.encode(&mut body);
|
|
842
|
+
Instruction::LocalSet(POS).encode(&mut body);
|
|
843
|
+
// address = result + pos
|
|
844
|
+
Instruction::LocalGet(RESULT).encode(&mut body);
|
|
845
|
+
Instruction::LocalGet(POS).encode(&mut body);
|
|
846
|
+
Instruction::I32Add.encode(&mut body);
|
|
847
|
+
// value = '0' + (temp % 10)
|
|
848
|
+
Instruction::LocalGet(TEMP).encode(&mut body);
|
|
849
|
+
Instruction::I64Const(10).encode(&mut body);
|
|
850
|
+
Instruction::I64RemS.encode(&mut body);
|
|
851
|
+
Instruction::I64Const(48).encode(&mut body);
|
|
852
|
+
Instruction::I64Add.encode(&mut body);
|
|
853
|
+
Instruction::I32WrapI64.encode(&mut body);
|
|
854
|
+
Instruction::I32Store8(MemArg { offset: 0, align: 0, memory_index: 0 }).encode(&mut body);
|
|
855
|
+
// temp /= 10
|
|
856
|
+
Instruction::LocalGet(TEMP).encode(&mut body);
|
|
857
|
+
Instruction::I64Const(10).encode(&mut body);
|
|
858
|
+
Instruction::I64DivS.encode(&mut body);
|
|
859
|
+
Instruction::LocalSet(TEMP).encode(&mut body);
|
|
860
|
+
// while (pos > 4 + is_neg)
|
|
861
|
+
Instruction::LocalGet(POS).encode(&mut body);
|
|
862
|
+
Instruction::I32Const(4).encode(&mut body);
|
|
863
|
+
Instruction::LocalGet(IS_NEG).encode(&mut body);
|
|
864
|
+
Instruction::I32Add.encode(&mut body);
|
|
865
|
+
Instruction::I32GtU.encode(&mut body);
|
|
866
|
+
Instruction::BrIf(0).encode(&mut body);
|
|
867
|
+
Instruction::End.encode(&mut body);
|
|
868
|
+
|
|
869
|
+
// if (is_neg) result[4] = '-'
|
|
870
|
+
Instruction::LocalGet(IS_NEG).encode(&mut body);
|
|
871
|
+
Instruction::If(BlockType::Empty).encode(&mut body);
|
|
872
|
+
Instruction::LocalGet(RESULT).encode(&mut body);
|
|
873
|
+
Instruction::I32Const(4).encode(&mut body);
|
|
874
|
+
Instruction::I32Add.encode(&mut body);
|
|
875
|
+
Instruction::I32Const(45).encode(&mut body); // '-'
|
|
876
|
+
Instruction::I32Store8(MemArg { offset: 0, align: 0, memory_index: 0 }).encode(&mut body);
|
|
877
|
+
Instruction::End.encode(&mut body);
|
|
878
|
+
|
|
879
|
+
Instruction::LocalGet(RESULT).encode(&mut body);
|
|
880
|
+
Instruction::End.encode(&mut body);
|
|
881
|
+
|
|
882
|
+
module.add_function(type_idx, &body)
|
|
883
|
+
}
|
|
884
|
+
|
|
542
885
|
/// The `PlumType` of a declared parameter, including `fn(...)`-typed params as `TFun`.
|
|
543
886
|
fn param_plum_type(pt: &ast::ParamType) -> PlumType {
|
|
544
887
|
match pt {
|
|
@@ -573,6 +916,16 @@ struct ClosureWalker<'a, 'c> {
|
|
|
573
916
|
cctx: plum_checker::CheckCtx<'c>,
|
|
574
917
|
fn_decls: &'a HashMap<String, &'a ast::Fn>,
|
|
575
918
|
found: Vec<RawClosure<'a>>,
|
|
919
|
+
/// Local names bound in the function currently being walked (params, assign
|
|
920
|
+
/// targets, for-loop vars) — used to tell a local variable reference apart from a
|
|
921
|
+
/// bare reference to a top-level function name (see `named_fn_refs`).
|
|
922
|
+
locals: HashSet<String>,
|
|
923
|
+
/// Top-level (non-method) function names referenced as a bare value (e.g.
|
|
924
|
+
/// `each(double)`) rather than called directly (`double(x)`, which compiles via
|
|
925
|
+
/// `Expr::FnCall` and never reaches here). Each one needs a zero-capture
|
|
926
|
+
/// "trampoline" closure so it can be used wherever a `fn(...)`-typed value is
|
|
927
|
+
/// expected.
|
|
928
|
+
named_fn_refs: HashSet<String>,
|
|
576
929
|
}
|
|
577
930
|
|
|
578
931
|
impl<'a, 'c> ClosureWalker<'a, 'c> {
|
|
@@ -589,6 +942,7 @@ impl<'a, 'c> ClosureWalker<'a, 'c> {
|
|
|
589
942
|
self.walk_expr(value, None);
|
|
590
943
|
let ty = plum_checker::infer_expr(value, &self.env, &self.cctx).unwrap_or(PlumType::TInt);
|
|
591
944
|
self.env.insert(target.clone(), TypeScheme::mono(ty));
|
|
945
|
+
self.locals.insert(target.clone());
|
|
592
946
|
}
|
|
593
947
|
}
|
|
594
948
|
ast::Stmt::Return(Some(e)) => self.walk_expr(e, None),
|
|
@@ -612,6 +966,7 @@ impl<'a, 'c> ClosureWalker<'a, 'c> {
|
|
|
612
966
|
self.walk_expr(&f.iter, None);
|
|
613
967
|
for v in &f.vars {
|
|
614
968
|
self.env.insert(v.clone(), TypeScheme::mono(PlumType::TInt));
|
|
969
|
+
self.locals.insert(v.clone());
|
|
615
970
|
}
|
|
616
971
|
self.walk_block(&f.body);
|
|
617
972
|
}
|
|
@@ -668,11 +1023,23 @@ impl<'a, 'c> ClosureWalker<'a, 'c> {
|
|
|
668
1023
|
}
|
|
669
1024
|
}
|
|
670
1025
|
}
|
|
1026
|
+
ast::Expr::Var(name) => {
|
|
1027
|
+
// A bare name that isn't a local in this scope but does name a
|
|
1028
|
+
// top-level (non-method) function is a reference to that function as
|
|
1029
|
+
// a value (e.g. `each(double)`), not a direct call — a direct call
|
|
1030
|
+
// compiles via `Expr::FnCall` and never reaches this arm.
|
|
1031
|
+
if !self.locals.contains(name) {
|
|
1032
|
+
if let Some(f) = self.fn_decls.get(name) {
|
|
1033
|
+
if f.type_param.is_none() {
|
|
1034
|
+
self.named_fn_refs.insert(name.clone());
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
671
1039
|
ast::Expr::Int(_)
|
|
672
1040
|
| ast::Expr::Float(_)
|
|
673
1041
|
| ast::Expr::String(_)
|
|
674
1042
|
| ast::Expr::Self_
|
|
675
|
-
| ast::Expr::Var(_)
|
|
676
1043
|
| ast::Expr::TypeName(_) => {}
|
|
677
1044
|
}
|
|
678
1045
|
}
|
|
@@ -686,33 +1053,78 @@ impl<'a, 'c> ClosureWalker<'a, 'c> {
|
|
|
686
1053
|
(param_vts, param_ptypes, ret_vt)
|
|
687
1054
|
}
|
|
688
1055
|
_ => {
|
|
689
|
-
// Not a direct `fn`-typed call argument
|
|
1056
|
+
// Not a direct `fn`-typed call argument (e.g. assigned to a local
|
|
1057
|
+
// first, then called/passed on later): the checker's own closure
|
|
1058
|
+
// inference (`plum_checker::infer_expr` on `Expr::Closure`) gives
|
|
1059
|
+
// each param a fresh `TVar` and never actually unifies it against
|
|
1060
|
+
// how the param is used in the body — `unify` is a no-op for any
|
|
1061
|
+
// `TVar` — so a param that's genuinely Float or a class/pointer
|
|
1062
|
+
// silently comes back as an unresolved `TVar`, which
|
|
1063
|
+
// `plum_type_to_valtype` then defaults to `Int`. If the closure is
|
|
1064
|
+
// later called at its real (non-Int) type, the wasm function actually
|
|
1065
|
+
// compiled for its body (built from this wrong, Int-assumed signature)
|
|
1066
|
+
// won't match the `call_indirect` type the real call site expects —
|
|
1067
|
+
// a mismatch `call_indirect` only traps on at runtime, not at compile
|
|
1068
|
+
// or validation time.
|
|
1069
|
+
//
|
|
1070
|
+
// Fix: resolve each param's type from a direct usage in the body
|
|
1071
|
+
// first (see `resolve_closure_param_types_from_usage`) — e.g. `cb = |v|
|
|
1072
|
+
// x + v` with a captured `Float` `x` resolves `v` to `Float` from that
|
|
1073
|
+
// `Binary` op — then infer the return type against an env where the
|
|
1074
|
+
// params are already bound concretely (bypassing the checker's
|
|
1075
|
+
// Closure-inference arm entirely, since it always re-binds params to
|
|
1076
|
+
// fresh TVars regardless of what's already in the env passed to it).
|
|
690
|
-
|
|
1077
|
+
let resolved_params = resolve_closure_param_types_from_usage(cl, &self.env, &self.cctx);
|
|
691
|
-
|
|
1078
|
+
let param_ptypes: Vec<PlumType> = cl.params.iter()
|
|
1079
|
+
.map(|p| resolved_params.get(p).cloned().unwrap_or(PlumType::TInt))
|
|
1080
|
+
.collect();
|
|
692
|
-
|
|
1081
|
+
let param_vts: Vec<ValType> = param_ptypes.iter().map(plum_type_to_valtype).collect();
|
|
1082
|
+
|
|
693
|
-
|
|
1083
|
+
let mut body_env = self.env.clone();
|
|
694
|
-
PlumType::TUnit => None,
|
|
695
|
-
other => Some(plum_type_to_valtype(&other)),
|
|
696
|
-
};
|
|
697
|
-
(param_vts, ptypes, ret_vt)
|
|
698
|
-
}
|
|
699
|
-
_ => {
|
|
700
|
-
|
|
1084
|
+
for (p, ty) in cl.params.iter().zip(param_ptypes.iter()) {
|
|
701
|
-
|
|
1085
|
+
body_env.insert(p.clone(), TypeScheme::mono(ty.clone()));
|
|
702
|
-
(param_vts, param_ptypes, Some(ValType::I64))
|
|
703
|
-
}
|
|
704
1086
|
}
|
|
1087
|
+
let ret_ty = match cl.body.stmts.last() {
|
|
1088
|
+
Some(ast::Stmt::Expr(e)) => plum_checker::infer_expr(e, &body_env, &self.cctx).ok(),
|
|
1089
|
+
Some(ast::Stmt::Return(Some(e))) => plum_checker::infer_expr(e, &body_env, &self.cctx).ok(),
|
|
1090
|
+
_ => Some(PlumType::TUnit),
|
|
1091
|
+
};
|
|
1092
|
+
let ret_vt = match ret_ty {
|
|
1093
|
+
Some(PlumType::TUnit) => None,
|
|
1094
|
+
Some(PlumType::TVar(_)) | None => Some(ValType::I64), // unresolved: preserve prior Int-default behavior
|
|
1095
|
+
Some(other) => Some(plum_type_to_valtype(&other)),
|
|
1096
|
+
};
|
|
1097
|
+
(param_vts, param_ptypes, ret_vt)
|
|
705
1098
|
}
|
|
706
1099
|
};
|
|
707
|
-
let free_vars = collect_free_vars(cl, &self.env);
|
|
1100
|
+
let free_vars = collect_free_vars(cl, &self.env, self.fn_decls);
|
|
708
1101
|
self.found.push(RawClosure {
|
|
709
1102
|
ptr: cl as *const ast::Closure as usize,
|
|
710
1103
|
closure: cl,
|
|
711
1104
|
param_vts,
|
|
712
|
-
param_ptypes,
|
|
1105
|
+
param_ptypes: param_ptypes.clone(),
|
|
713
1106
|
ret_vt,
|
|
714
1107
|
free_vars,
|
|
715
1108
|
});
|
|
1109
|
+
|
|
1110
|
+
// Recurse into this closure's own body to find any closure literals nested
|
|
1111
|
+
// inside it (`|v| |w| v + w`, or a closure literal used inside a `match`/`if`
|
|
1112
|
+
// within this one's body). Each nested closure gets registered exactly like a
|
|
1113
|
+
// top-level one, seeing this closure's own params as locals in scope — which
|
|
1114
|
+
// is also what makes its free-variable analysis correctly capture a name from
|
|
1115
|
+
// *this* closure's scope (rather than silently missing it): once discovery
|
|
1116
|
+
// finishes, `ctx.closures` holds every closure at every depth before any body
|
|
1117
|
+
// is compiled, so the single fixed "compile each registered closure" pass in
|
|
1118
|
+
// `compile_source` already handles arbitrary nesting with no further changes.
|
|
1119
|
+
let saved_env = self.env.clone();
|
|
1120
|
+
let saved_locals = self.locals.clone();
|
|
1121
|
+
for (p, ty) in cl.params.iter().zip(param_ptypes.iter()) {
|
|
1122
|
+
self.env.insert(p.clone(), TypeScheme::mono(ty.clone()));
|
|
1123
|
+
self.locals.insert(p.clone());
|
|
1124
|
+
}
|
|
1125
|
+
self.walk_block(&cl.body);
|
|
1126
|
+
self.env = saved_env;
|
|
1127
|
+
self.locals = saved_locals;
|
|
716
1128
|
}
|
|
717
1129
|
}
|
|
718
1130
|
|
|
@@ -724,17 +1136,197 @@ fn arg_expr_of(arg: &ast::Arg) -> &ast::Expr {
|
|
|
724
1136
|
}
|
|
725
1137
|
}
|
|
726
1138
|
|
|
1139
|
+
/// Resolves as many of a closure's param types as possible from how they're actually
|
|
1140
|
+
/// used in its body — e.g. `|v| x + v` with a captured `Float` `x` resolves `v` to
|
|
1141
|
+
/// `Float` from that `Binary` op, or `|v| helper(v)` where `helper`'s declared param
|
|
1142
|
+
/// type is concrete resolves `v` to that. A param never used in a way that pins down
|
|
1143
|
+
/// a concrete type simply doesn't appear in the returned map (callers fall back to
|
|
1144
|
+
/// `Int`, matching the prior default). This is intentionally a shallow, best-effort
|
|
1145
|
+
/// scan — not full unification — scoped to fixing the specific `call_indirect`
|
|
1146
|
+
/// signature-mismatch gap this exists for, not replacing the checker's inference.
|
|
1147
|
+
fn resolve_closure_param_types_from_usage(
|
|
1148
|
+
cl: &ast::Closure,
|
|
1149
|
+
env: &TypeEnv,
|
|
1150
|
+
cctx: &plum_checker::CheckCtx,
|
|
1151
|
+
) -> HashMap<String, PlumType> {
|
|
1152
|
+
let params: std::collections::HashSet<String> = cl.params.iter().cloned().collect();
|
|
1153
|
+
let mut resolved: HashMap<String, PlumType> = HashMap::new();
|
|
1154
|
+
for stmt in &cl.body.stmts {
|
|
1155
|
+
scan_stmt_for_param_types(stmt, ¶ms, env, cctx, &mut resolved);
|
|
1156
|
+
}
|
|
1157
|
+
resolved
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
fn scan_stmt_for_param_types(
|
|
1161
|
+
stmt: &ast::Stmt,
|
|
1162
|
+
params: &std::collections::HashSet<String>,
|
|
1163
|
+
env: &TypeEnv,
|
|
1164
|
+
cctx: &plum_checker::CheckCtx,
|
|
1165
|
+
resolved: &mut HashMap<String, PlumType>,
|
|
1166
|
+
) {
|
|
1167
|
+
match stmt {
|
|
1168
|
+
ast::Stmt::Assign(a) => {
|
|
1169
|
+
for v in &a.values {
|
|
1170
|
+
scan_expr_for_param_types(v, params, env, cctx, resolved);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
ast::Stmt::Return(Some(e)) | ast::Stmt::Expr(e) | ast::Stmt::Assert(e) => {
|
|
1174
|
+
scan_expr_for_param_types(e, params, env, cctx, resolved);
|
|
1175
|
+
}
|
|
1176
|
+
ast::Stmt::If(i) => {
|
|
1177
|
+
scan_expr_for_param_types(&i.condition, params, env, cctx, resolved);
|
|
1178
|
+
for s in &i.body.stmts {
|
|
1179
|
+
scan_stmt_for_param_types(s, params, env, cctx, resolved);
|
|
1180
|
+
}
|
|
1181
|
+
for ei in &i.else_ifs {
|
|
1182
|
+
scan_expr_for_param_types(&ei.condition, params, env, cctx, resolved);
|
|
1183
|
+
for s in &ei.body.stmts {
|
|
1184
|
+
scan_stmt_for_param_types(s, params, env, cctx, resolved);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
if let Some(e) = &i.else_ {
|
|
1188
|
+
for s in &e.stmts {
|
|
1189
|
+
scan_stmt_for_param_types(s, params, env, cctx, resolved);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
ast::Stmt::While(w) => {
|
|
1194
|
+
scan_expr_for_param_types(&w.condition, params, env, cctx, resolved);
|
|
1195
|
+
for s in &w.body.stmts {
|
|
1196
|
+
scan_stmt_for_param_types(s, params, env, cctx, resolved);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
ast::Stmt::For(f) => {
|
|
1200
|
+
scan_expr_for_param_types(&f.iter, params, env, cctx, resolved);
|
|
1201
|
+
for s in &f.body.stmts {
|
|
1202
|
+
scan_stmt_for_param_types(s, params, env, cctx, resolved);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
_ => {}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
fn scan_expr_for_param_types(
|
|
1210
|
+
expr: &ast::Expr,
|
|
1211
|
+
params: &std::collections::HashSet<String>,
|
|
1212
|
+
env: &TypeEnv,
|
|
1213
|
+
cctx: &plum_checker::CheckCtx,
|
|
1214
|
+
resolved: &mut HashMap<String, PlumType>,
|
|
1215
|
+
) {
|
|
1216
|
+
match expr {
|
|
1217
|
+
ast::Expr::Binary(b) => {
|
|
1218
|
+
try_resolve_param_from_pair(&b.left, &b.right, params, env, cctx, resolved);
|
|
1219
|
+
scan_expr_for_param_types(&b.left, params, env, cctx, resolved);
|
|
1220
|
+
scan_expr_for_param_types(&b.right, params, env, cctx, resolved);
|
|
1221
|
+
}
|
|
1222
|
+
ast::Expr::Compare(c) => {
|
|
1223
|
+
try_resolve_param_from_pair(&c.left, &c.right, params, env, cctx, resolved);
|
|
1224
|
+
scan_expr_for_param_types(&c.left, params, env, cctx, resolved);
|
|
1225
|
+
scan_expr_for_param_types(&c.right, params, env, cctx, resolved);
|
|
1226
|
+
}
|
|
1227
|
+
ast::Expr::Bool(b) => {
|
|
1228
|
+
scan_expr_for_param_types(&b.left, params, env, cctx, resolved);
|
|
1229
|
+
scan_expr_for_param_types(&b.right, params, env, cctx, resolved);
|
|
1230
|
+
}
|
|
1231
|
+
ast::Expr::Not(inner) => scan_expr_for_param_types(inner, params, env, cctx, resolved),
|
|
1232
|
+
ast::Expr::Unary(u) => scan_expr_for_param_types(&u.operand, params, env, cctx, resolved),
|
|
1233
|
+
ast::Expr::Paren(inner) => scan_expr_for_param_types(inner, params, env, cctx, resolved),
|
|
1234
|
+
ast::Expr::Ternary(t) => {
|
|
1235
|
+
scan_expr_for_param_types(&t.condition, params, env, cctx, resolved);
|
|
1236
|
+
scan_expr_for_param_types(&t.then, params, env, cctx, resolved);
|
|
1237
|
+
scan_expr_for_param_types(&t.else_, params, env, cctx, resolved);
|
|
1238
|
+
}
|
|
1239
|
+
ast::Expr::FnCall(call) => {
|
|
1240
|
+
if let Ok(PlumType::TFun(param_types, _)) = plum_checker::lookup(env, &call.name) {
|
|
1241
|
+
for (arg, expected) in call.args.iter().zip(param_types.iter()) {
|
|
1242
|
+
let arg_expr = arg_expr_of(arg);
|
|
1243
|
+
if let ast::Expr::Var(n) = arg_expr {
|
|
1244
|
+
if params.contains(n) && !resolved.contains_key(n) && !matches!(expected, PlumType::TVar(_)) {
|
|
1245
|
+
resolved.insert(n.clone(), expected.clone());
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
for arg in &call.args {
|
|
1251
|
+
scan_expr_for_param_types(arg_expr_of(arg), params, env, cctx, resolved);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
ast::Expr::ClassCall(call) => {
|
|
1255
|
+
for fa in &call.fields {
|
|
1256
|
+
scan_expr_for_param_types(&fa.value, params, env, cctx, resolved);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
ast::Expr::Attribute(a) => {
|
|
1260
|
+
// `c.field` on a bare, unresolved param implies `c`'s type is whichever
|
|
1261
|
+
// class declares that field name — ambiguous if more than one class has
|
|
1262
|
+
// a field by that name, but resolvable in the common case.
|
|
1263
|
+
if let ast::AttrKind::Field(field_name) = &a.attr {
|
|
1264
|
+
if let ast::Expr::Var(n) = &a.object {
|
|
1265
|
+
if params.contains(n) && !resolved.contains_key(n) {
|
|
1266
|
+
let mut matches = cctx.classes.iter().filter(|(_, fields)| fields.iter().any(|(fname, _)| fname == field_name));
|
|
1267
|
+
if let (Some((class_name, _)), None) = (matches.next(), matches.next()) {
|
|
1268
|
+
resolved.insert(n.clone(), PlumType::TNamed(class_name.clone()));
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
scan_expr_for_param_types(&a.object, params, env, cctx, resolved);
|
|
1274
|
+
if let ast::AttrKind::Method(call) = &a.attr {
|
|
1275
|
+
for arg in &call.args {
|
|
1276
|
+
scan_expr_for_param_types(arg_expr_of(arg), params, env, cctx, resolved);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
_ => {}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/// If either side of a `Binary`/`Compare` operand pair is a bare reference to an
|
|
1285
|
+
/// unresolved param and the *other* side has a concrete (non-`TVar`) inferred type,
|
|
1286
|
+
/// binds the param to that type.
|
|
1287
|
+
fn try_resolve_param_from_pair(
|
|
1288
|
+
left: &ast::Expr,
|
|
1289
|
+
right: &ast::Expr,
|
|
1290
|
+
params: &std::collections::HashSet<String>,
|
|
1291
|
+
env: &TypeEnv,
|
|
1292
|
+
cctx: &plum_checker::CheckCtx,
|
|
1293
|
+
resolved: &mut HashMap<String, PlumType>,
|
|
1294
|
+
) {
|
|
1295
|
+
if let ast::Expr::Var(n) = left {
|
|
1296
|
+
if params.contains(n) && !resolved.contains_key(n) {
|
|
1297
|
+
if let Ok(ty) = plum_checker::infer_expr(right, env, cctx) {
|
|
1298
|
+
if !matches!(ty, PlumType::TVar(_)) {
|
|
1299
|
+
resolved.insert(n.clone(), ty);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
if let ast::Expr::Var(n) = right {
|
|
1305
|
+
if params.contains(n) && !resolved.contains_key(n) {
|
|
1306
|
+
if let Ok(ty) = plum_checker::infer_expr(left, env, cctx) {
|
|
1307
|
+
if !matches!(ty, PlumType::TVar(_)) {
|
|
1308
|
+
resolved.insert(n.clone(), ty);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
727
1315
|
/// Determines a closure's captured free variables: every `Var` referenced in its body
|
|
728
1316
|
/// that is neither one of the closure's own params nor assigned locally inside the body,
|
|
729
1317
|
/// in first-appearance order. Each free variable's type is looked up in the *enclosing*
|
|
730
1318
|
/// scope's type env.
|
|
1319
|
+
fn collect_free_vars(
|
|
1320
|
+
cl: &ast::Closure,
|
|
1321
|
+
env: &TypeEnv,
|
|
1322
|
+
fn_decls: &HashMap<String, &ast::Fn>,
|
|
731
|
-
|
|
1323
|
+
) -> Vec<(String, PlumType)> {
|
|
732
1324
|
let mut bound: std::collections::HashSet<String> = cl.params.iter().cloned().collect();
|
|
733
1325
|
fv_collect_bound_block(&cl.body, &mut bound);
|
|
734
1326
|
|
|
735
1327
|
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
|
|
736
1328
|
let mut free: Vec<(String, PlumType)> = Vec::new();
|
|
737
|
-
fv_collect_refs_block(&cl.body, &bound, &mut seen, &mut free, env);
|
|
1329
|
+
fv_collect_refs_block(&cl.body, &bound, &mut seen, &mut free, env, fn_decls);
|
|
738
1330
|
free
|
|
739
1331
|
}
|
|
740
1332
|
|
|
@@ -793,42 +1385,43 @@ fn fv_collect_refs_block(
|
|
|
793
1385
|
seen: &mut std::collections::HashSet<String>,
|
|
794
1386
|
free: &mut Vec<(String, PlumType)>,
|
|
795
1387
|
env: &TypeEnv,
|
|
1388
|
+
fn_decls: &HashMap<String, &ast::Fn>,
|
|
796
1389
|
) {
|
|
797
1390
|
for s in &block.stmts {
|
|
798
1391
|
match s {
|
|
799
1392
|
ast::Stmt::Assign(a) => {
|
|
800
1393
|
for v in &a.values {
|
|
801
|
-
fv_collect_refs_expr(v, bound, seen, free, env);
|
|
1394
|
+
fv_collect_refs_expr(v, bound, seen, free, env, fn_decls);
|
|
802
1395
|
}
|
|
803
1396
|
}
|
|
804
1397
|
ast::Stmt::Return(Some(e)) | ast::Stmt::Expr(e) | ast::Stmt::Assert(e) => {
|
|
805
|
-
fv_collect_refs_expr(e, bound, seen, free, env);
|
|
1398
|
+
fv_collect_refs_expr(e, bound, seen, free, env, fn_decls);
|
|
806
1399
|
}
|
|
807
1400
|
ast::Stmt::If(i) => {
|
|
808
|
-
fv_collect_refs_expr(&i.condition, bound, seen, free, env);
|
|
1401
|
+
fv_collect_refs_expr(&i.condition, bound, seen, free, env, fn_decls);
|
|
809
|
-
fv_collect_refs_block(&i.body, bound, seen, free, env);
|
|
1402
|
+
fv_collect_refs_block(&i.body, bound, seen, free, env, fn_decls);
|
|
810
1403
|
for ei in &i.else_ifs {
|
|
811
|
-
fv_collect_refs_expr(&ei.condition, bound, seen, free, env);
|
|
1404
|
+
fv_collect_refs_expr(&ei.condition, bound, seen, free, env, fn_decls);
|
|
812
|
-
fv_collect_refs_block(&ei.body, bound, seen, free, env);
|
|
1405
|
+
fv_collect_refs_block(&ei.body, bound, seen, free, env, fn_decls);
|
|
813
1406
|
}
|
|
814
1407
|
if let Some(e) = &i.else_ {
|
|
815
|
-
fv_collect_refs_block(e, bound, seen, free, env);
|
|
1408
|
+
fv_collect_refs_block(e, bound, seen, free, env, fn_decls);
|
|
816
1409
|
}
|
|
817
1410
|
}
|
|
818
1411
|
ast::Stmt::While(w) => {
|
|
819
|
-
fv_collect_refs_expr(&w.condition, bound, seen, free, env);
|
|
1412
|
+
fv_collect_refs_expr(&w.condition, bound, seen, free, env, fn_decls);
|
|
820
|
-
fv_collect_refs_block(&w.body, bound, seen, free, env);
|
|
1413
|
+
fv_collect_refs_block(&w.body, bound, seen, free, env, fn_decls);
|
|
821
1414
|
}
|
|
822
1415
|
ast::Stmt::For(f) => {
|
|
823
|
-
fv_collect_refs_expr(&f.iter, bound, seen, free, env);
|
|
1416
|
+
fv_collect_refs_expr(&f.iter, bound, seen, free, env, fn_decls);
|
|
824
|
-
fv_collect_refs_block(&f.body, bound, seen, free, env);
|
|
1417
|
+
fv_collect_refs_block(&f.body, bound, seen, free, env, fn_decls);
|
|
825
1418
|
}
|
|
826
1419
|
ast::Stmt::Match(m) => {
|
|
827
1420
|
for subj in &m.subjects {
|
|
828
|
-
fv_collect_refs_expr(subj, bound, seen, free, env);
|
|
1421
|
+
fv_collect_refs_expr(subj, bound, seen, free, env, fn_decls);
|
|
829
1422
|
}
|
|
830
1423
|
for case in &m.cases {
|
|
831
|
-
fv_collect_refs_block(&case.body, bound, seen, free, env);
|
|
1424
|
+
fv_collect_refs_block(&case.body, bound, seen, free, env, fn_decls);
|
|
832
1425
|
}
|
|
833
1426
|
}
|
|
834
1427
|
_ => {}
|
|
@@ -842,45 +1435,67 @@ fn fv_collect_refs_expr(
|
|
|
842
1435
|
seen: &mut std::collections::HashSet<String>,
|
|
843
1436
|
free: &mut Vec<(String, PlumType)>,
|
|
844
1437
|
env: &TypeEnv,
|
|
1438
|
+
fn_decls: &HashMap<String, &ast::Fn>,
|
|
845
1439
|
) {
|
|
846
1440
|
match expr {
|
|
847
1441
|
ast::Expr::Var(name) => {
|
|
1442
|
+
// A top-level (non-method) function referenced bare (e.g. `each(double)`)
|
|
1443
|
+
// is not a captured variable — it's compiled as a static trampoline
|
|
1444
|
+
// reference (see `named_fn_values`), not loaded from an enclosing local.
|
|
1445
|
+
let is_named_fn_ref = fn_decls.get(name).is_some_and(|f| f.type_param.is_none());
|
|
848
|
-
if !bound.contains(name) && seen.insert(name.clone()) {
|
|
1446
|
+
if !is_named_fn_ref && !bound.contains(name) && seen.insert(name.clone()) {
|
|
849
1447
|
let ty = plum_checker::lookup(env, name).unwrap_or(PlumType::TInt);
|
|
850
1448
|
free.push((name.clone(), ty));
|
|
851
1449
|
}
|
|
852
1450
|
}
|
|
853
|
-
ast::Expr::Binary(b) => { fv_collect_refs_expr(&b.left, bound, seen, free, env); fv_collect_refs_expr(&b.right, bound, seen, free, env); }
|
|
1451
|
+
ast::Expr::Binary(b) => { fv_collect_refs_expr(&b.left, bound, seen, free, env, fn_decls); fv_collect_refs_expr(&b.right, bound, seen, free, env, fn_decls); }
|
|
854
|
-
ast::Expr::Bool(b) => { fv_collect_refs_expr(&b.left, bound, seen, free, env); fv_collect_refs_expr(&b.right, bound, seen, free, env); }
|
|
1452
|
+
ast::Expr::Bool(b) => { fv_collect_refs_expr(&b.left, bound, seen, free, env, fn_decls); fv_collect_refs_expr(&b.right, bound, seen, free, env, fn_decls); }
|
|
855
|
-
ast::Expr::Compare(c) => { fv_collect_refs_expr(&c.left, bound, seen, free, env); fv_collect_refs_expr(&c.right, bound, seen, free, env); }
|
|
1453
|
+
ast::Expr::Compare(c) => { fv_collect_refs_expr(&c.left, bound, seen, free, env, fn_decls); fv_collect_refs_expr(&c.right, bound, seen, free, env, fn_decls); }
|
|
856
|
-
ast::Expr::Not(inner) => fv_collect_refs_expr(inner, bound, seen, free, env),
|
|
1454
|
+
ast::Expr::Not(inner) => fv_collect_refs_expr(inner, bound, seen, free, env, fn_decls),
|
|
857
|
-
ast::Expr::Unary(u) => fv_collect_refs_expr(&u.operand, bound, seen, free, env),
|
|
1455
|
+
ast::Expr::Unary(u) => fv_collect_refs_expr(&u.operand, bound, seen, free, env, fn_decls),
|
|
858
|
-
ast::Expr::Paren(inner) => fv_collect_refs_expr(inner, bound, seen, free, env),
|
|
1456
|
+
ast::Expr::Paren(inner) => fv_collect_refs_expr(inner, bound, seen, free, env, fn_decls),
|
|
859
1457
|
ast::Expr::Ternary(t) => {
|
|
860
|
-
fv_collect_refs_expr(&t.condition, bound, seen, free, env);
|
|
1458
|
+
fv_collect_refs_expr(&t.condition, bound, seen, free, env, fn_decls);
|
|
861
|
-
fv_collect_refs_expr(&t.then, bound, seen, free, env);
|
|
1459
|
+
fv_collect_refs_expr(&t.then, bound, seen, free, env, fn_decls);
|
|
862
|
-
fv_collect_refs_expr(&t.else_, bound, seen, free, env);
|
|
1460
|
+
fv_collect_refs_expr(&t.else_, bound, seen, free, env, fn_decls);
|
|
863
1461
|
}
|
|
864
1462
|
ast::Expr::FnCall(call) => {
|
|
865
1463
|
for arg in &call.args {
|
|
866
|
-
fv_collect_refs_expr(arg_expr_of(arg), bound, seen, free, env);
|
|
1464
|
+
fv_collect_refs_expr(arg_expr_of(arg), bound, seen, free, env, fn_decls);
|
|
867
1465
|
}
|
|
868
1466
|
}
|
|
869
1467
|
ast::Expr::ClassCall(call) => {
|
|
870
1468
|
for fa in &call.fields {
|
|
871
|
-
fv_collect_refs_expr(&fa.value, bound, seen, free, env);
|
|
1469
|
+
fv_collect_refs_expr(&fa.value, bound, seen, free, env, fn_decls);
|
|
872
1470
|
}
|
|
873
1471
|
}
|
|
874
1472
|
ast::Expr::Attribute(a) => {
|
|
875
|
-
fv_collect_refs_expr(&a.object, bound, seen, free, env);
|
|
1473
|
+
fv_collect_refs_expr(&a.object, bound, seen, free, env, fn_decls);
|
|
876
1474
|
if let ast::AttrKind::Method(call) = &a.attr {
|
|
877
1475
|
for arg in &call.args {
|
|
878
|
-
fv_collect_refs_expr(arg_expr_of(arg), bound, seen, free, env);
|
|
1476
|
+
fv_collect_refs_expr(arg_expr_of(arg), bound, seen, free, env, fn_decls);
|
|
879
1477
|
}
|
|
880
1478
|
}
|
|
881
1479
|
}
|
|
1480
|
+
ast::Expr::Closure(inner) => {
|
|
1481
|
+
// A name the *inner* closure references that isn't bound by the inner
|
|
1482
|
+
// itself (its own params/locals) and isn't bound by *this* (outer)
|
|
1483
|
+
// closure either is a genuine multi-level capture: this outer closure
|
|
1484
|
+
// also needs to capture it from its own enclosing scope, in order to
|
|
1485
|
+
// pass it down when it later constructs the inner closure. A name the
|
|
1486
|
+
// inner references that the outer already binds (e.g. one of the
|
|
1487
|
+
// outer's own params) needs no such propagation — the outer's compiled
|
|
1488
|
+
// body can already reference it as an ordinary local when snapshotting
|
|
882
|
-
|
|
1489
|
+
// the inner closure's env, so it's deliberately excluded here by
|
|
1490
|
+
// unioning `bound` (outer) with the inner's own bound set below, rather
|
|
883
|
-
|
|
1491
|
+
// than passing the inner's bound set alone.
|
|
1492
|
+
let mut inner_bound = bound.clone();
|
|
1493
|
+
for p in &inner.params {
|
|
1494
|
+
inner_bound.insert(p.clone());
|
|
1495
|
+
}
|
|
1496
|
+
fv_collect_bound_block(&inner.body, &mut inner_bound);
|
|
1497
|
+
fv_collect_refs_block(&inner.body, &inner_bound, seen, free, env, fn_decls);
|
|
1498
|
+
}
|
|
884
1499
|
_ => {}
|
|
885
1500
|
}
|
|
886
1501
|
}
|
|
@@ -894,11 +1509,18 @@ struct Collector<'a> {
|
|
|
894
1509
|
named: Vec<(String, PlumType)>,
|
|
895
1510
|
named_set: std::collections::HashSet<String>,
|
|
896
1511
|
classcall_scratch: HashMap<usize, u32>,
|
|
1512
|
+
/// One scratch-local type per subject (usually one, more for `match a, b, ...`).
|
|
897
|
-
match_scratch: HashMap<usize, PlumType>,
|
|
1513
|
+
match_scratch: HashMap<usize, Vec<PlumType>>,
|
|
898
1514
|
next_classcall_slot: u32,
|
|
899
1515
|
/// `Closure` expr identity -> offset of its first (of two) construction scratch slots.
|
|
900
1516
|
closure_scratch: HashMap<usize, u32>,
|
|
901
1517
|
next_closure_slot: u32,
|
|
1518
|
+
/// `CasePattern::Class` identity -> its scratch local, for a constructor pattern
|
|
1519
|
+
/// *nested* inside another constructor pattern's fields (`Some(Some(v))`) — the
|
|
1520
|
+
/// outermost, per-subject pattern already has a scratch local via `match_scratch`
|
|
1521
|
+
/// / `all_subjects`, so this only covers depth >= 1.
|
|
1522
|
+
nested_class_scratch: HashMap<usize, u32>,
|
|
1523
|
+
next_nested_class_slot: u32,
|
|
902
1524
|
}
|
|
903
1525
|
|
|
904
1526
|
impl<'a> Collector<'a> {
|
|
@@ -909,6 +1531,37 @@ impl<'a> Collector<'a> {
|
|
|
909
1531
|
self.env.insert(name.to_string(), TypeScheme::mono(ty));
|
|
910
1532
|
}
|
|
911
1533
|
|
|
1534
|
+
/// Binds every `Name` sub-pattern anywhere inside `pat` (at any nesting depth) to
|
|
1535
|
+
/// its correct field type, and reserves a scratch local for every `Class`
|
|
1536
|
+
/// sub-pattern found *nested* inside another constructor pattern's fields (the
|
|
1537
|
+
/// outermost, per-subject pattern doesn't need one — see `nested_class_scratch`).
|
|
1538
|
+
fn collect_pattern(&mut self, pat: &ast::CasePattern, ty: &PlumType, top_level: bool) {
|
|
1539
|
+
match pat {
|
|
1540
|
+
ast::CasePattern::Name(n) => {
|
|
1541
|
+
let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
|
|
1542
|
+
&& self.cctx.enum_variants.contains_key(n);
|
|
1543
|
+
if !is_variant {
|
|
1544
|
+
self.bind(n, ty.clone());
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
ast::CasePattern::Class { name, fields } => {
|
|
1548
|
+
if !top_level {
|
|
1549
|
+
let key = pat as *const ast::CasePattern as usize;
|
|
1550
|
+
let slot = self.next_nested_class_slot;
|
|
1551
|
+
self.next_nested_class_slot += 1;
|
|
1552
|
+
self.nested_class_scratch.insert(key, slot);
|
|
1553
|
+
}
|
|
1554
|
+
if let Some(info) = self.cctx.enum_variants.get(name) {
|
|
1555
|
+
let field_types = info.field_types.clone();
|
|
1556
|
+
for (f, fty) in fields.iter().zip(field_types.iter()) {
|
|
1557
|
+
self.collect_pattern(f, fty, false);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
_ => {}
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
912
1565
|
fn walk_block(&mut self, block: &ast::Block) {
|
|
913
1566
|
for s in &block.stmts {
|
|
914
1567
|
self.walk_stmt(s);
|
|
@@ -920,7 +1573,22 @@ impl<'a> Collector<'a> {
|
|
|
920
1573
|
ast::Stmt::Assign(a) => {
|
|
921
1574
|
for (target, value) in a.targets.iter().zip(a.values.iter()) {
|
|
922
1575
|
self.walk_expr(value);
|
|
1576
|
+
let ty = if matches!(value, ast::Expr::Closure(_)) {
|
|
1577
|
+
// The checker's own closure inference (`infer_expr` on
|
|
1578
|
+
// `Expr::Closure`) infers the return type by recursively
|
|
1579
|
+
// inferring the body's tail expression with each param bound
|
|
1580
|
+
// to a fresh, unconstrained `TVar` — e.g. a captured/param
|
|
1581
|
+
// attribute access (`c.age`) on a `TVar`-typed object isn't a
|
|
1582
|
+
// known class, so it errors out entirely, and this call site
|
|
1583
|
+
// then silently defaults to `TInt` — the *wrong* wasm local
|
|
1584
|
+
// width for what's actually always an `i32` pointer. All that
|
|
1585
|
+
// actually matters here is the local's wasm width, and every
|
|
1586
|
+
// closure value is an i32 pointer regardless of its
|
|
1587
|
+
// parameter/return types, so skip inference entirely.
|
|
1588
|
+
PlumType::TFun(Vec::new(), Box::new(PlumType::TUnit))
|
|
1589
|
+
} else {
|
|
923
|
-
|
|
1590
|
+
plum_checker::infer_expr(value, &self.env, &self.cctx).unwrap_or(PlumType::TInt)
|
|
1591
|
+
};
|
|
924
1592
|
self.bind(target, ty);
|
|
925
1593
|
}
|
|
926
1594
|
}
|
|
@@ -951,39 +1619,15 @@ impl<'a> Collector<'a> {
|
|
|
951
1619
|
ast::Stmt::Expr(e) => self.walk_expr(e),
|
|
952
1620
|
ast::Stmt::Assert(e) => self.walk_expr(e),
|
|
953
1621
|
ast::Stmt::Match(m) => {
|
|
954
|
-
let
|
|
1622
|
+
let subject_types: Vec<PlumType> = m.subjects.iter().map(|s| {
|
|
955
|
-
self.walk_expr(&m.subjects[0]);
|
|
956
|
-
plum_checker::infer_expr(&m.subjects[0], &self.env, &self.cctx).unwrap_or(PlumType::TInt)
|
|
957
|
-
} else {
|
|
958
|
-
for s in &m.subjects {
|
|
959
|
-
|
|
1623
|
+
self.walk_expr(s);
|
|
960
|
-
|
|
1624
|
+
plum_checker::infer_expr(s, &self.env, &self.cctx).unwrap_or(PlumType::TInt)
|
|
961
|
-
|
|
1625
|
+
}).collect();
|
|
962
|
-
};
|
|
963
|
-
self.match_scratch.insert(m as *const ast::Match as usize,
|
|
1626
|
+
self.match_scratch.insert(m as *const ast::Match as usize, subject_types.clone());
|
|
964
1627
|
for case in &m.cases {
|
|
965
1628
|
let saved = self.env.clone();
|
|
966
|
-
if m.subjects.len() == 1 {
|
|
967
|
-
match case.patterns.first() {
|
|
968
|
-
Some(ast::CasePattern::Name(n)) => {
|
|
969
|
-
let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
|
|
970
|
-
&& self.cctx.enum_variants.contains_key(n);
|
|
971
|
-
if !is_variant {
|
|
972
|
-
self.bind(n, subject_ty.clone());
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
Some(ast::CasePattern::Class { name, fields }) => {
|
|
976
|
-
if let Some(info) = self.cctx.enum_variants.get(name) {
|
|
977
|
-
let field_types = info.field_types.clone();
|
|
978
|
-
|
|
1629
|
+
for (pat, subject_ty) in case.patterns.iter().zip(subject_types.iter()) {
|
|
979
|
-
|
|
1630
|
+
self.collect_pattern(pat, subject_ty, true);
|
|
980
|
-
self.bind(n, fty.clone());
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
_ => {}
|
|
986
|
-
}
|
|
987
1631
|
}
|
|
988
1632
|
self.walk_block(&case.body);
|
|
989
1633
|
self.env = saved;
|
|
@@ -1101,6 +1745,8 @@ fn compile_fn_body(f: &ast::Fn, ctx: &CompileCtx, state: &mut ModuleState) -> Re
|
|
|
1101
1745
|
next_classcall_slot: 0,
|
|
1102
1746
|
closure_scratch: HashMap::new(),
|
|
1103
1747
|
next_closure_slot: 0,
|
|
1748
|
+
nested_class_scratch: HashMap::new(),
|
|
1749
|
+
next_nested_class_slot: 0,
|
|
1104
1750
|
};
|
|
1105
1751
|
if let ast::FnBody::Block(block) = &f.body {
|
|
1106
1752
|
collector.walk_block(block);
|
|
@@ -1109,7 +1755,6 @@ fn compile_fn_body(f: &ast::Fn, ctx: &CompileCtx, state: &mut ModuleState) -> Re
|
|
|
1109
1755
|
// `main() -> Int = each(|v| v)`), which needs construction scratch slots.
|
|
1110
1756
|
collector.walk_expr(e);
|
|
1111
1757
|
}
|
|
1112
|
-
|
|
1113
1758
|
// ---- assign local indices: [self?][params][named...][classcall scratch...][match scratch...] ----
|
|
1114
1759
|
let mut locals: HashMap<String, u32> = HashMap::new();
|
|
1115
1760
|
let mut groups: Vec<ValType> = Vec::new();
|
|
@@ -1139,9 +1784,18 @@ fn compile_fn_body(f: &ast::Fn, ctx: &CompileCtx, state: &mut ModuleState) -> Re
|
|
|
1139
1784
|
|
|
1140
1785
|
let match_scratch_base = idx;
|
|
1141
1786
|
let mut match_scratch_index: HashMap<usize, u32> = HashMap::new();
|
|
1142
|
-
for (ptr,
|
|
1787
|
+
for (ptr, types) in collector.match_scratch.iter() {
|
|
1143
1788
|
match_scratch_index.insert(*ptr, idx - match_scratch_base);
|
|
1789
|
+
for ty in types {
|
|
1144
|
-
|
|
1790
|
+
groups.push(plum_type_to_valtype(ty));
|
|
1791
|
+
idx += 1;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
let nested_class_scratch_base = idx;
|
|
1796
|
+
let nested_class_scratch_count = collector.nested_class_scratch.values().copied().max().map(|m| m + 1).unwrap_or(0);
|
|
1797
|
+
for _ in 0..nested_class_scratch_count {
|
|
1798
|
+
groups.push(ValType::I32);
|
|
1145
1799
|
idx += 1;
|
|
1146
1800
|
}
|
|
1147
1801
|
|
|
@@ -1169,14 +1823,20 @@ fn compile_fn_body(f: &ast::Fn, ctx: &CompileCtx, state: &mut ModuleState) -> Re
|
|
|
1169
1823
|
match_scratch_index,
|
|
1170
1824
|
closure_scratch_base,
|
|
1171
1825
|
closure_scratch: collector.closure_scratch,
|
|
1826
|
+
nested_class_scratch_base,
|
|
1827
|
+
nested_class_scratch: collector.nested_class_scratch,
|
|
1172
1828
|
func_ids: &ctx.func_ids,
|
|
1173
1829
|
func_sigs: &ctx.func_sigs,
|
|
1174
1830
|
closures: &ctx.closures,
|
|
1175
1831
|
closure_call_types: &ctx.closure_call_types,
|
|
1832
|
+
named_fn_values: &ctx.named_fn_values,
|
|
1833
|
+
string_concat_func: ctx.string_concat_func,
|
|
1834
|
+
int_to_string_func: ctx.int_to_string_func,
|
|
1176
1835
|
classes: &ctx.classes,
|
|
1177
1836
|
methods: &ctx.methods,
|
|
1178
1837
|
enum_variants: &ctx.enum_variants,
|
|
1179
1838
|
type_env: RefCell::new(base_env),
|
|
1839
|
+
closure_local_sigs: RefCell::new(HashMap::new()),
|
|
1180
1840
|
bump_global: ctx.bump_global,
|
|
1181
1841
|
};
|
|
1182
1842
|
|
|
@@ -1321,7 +1981,15 @@ fn compile_stmt(stmt: &ast::Stmt, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mu
|
|
|
1321
1981
|
match stmt {
|
|
1322
1982
|
ast::Stmt::Assign(a) => {
|
|
1323
1983
|
for (target, value) in a.targets.iter().zip(a.values.iter()) {
|
|
1984
|
+
// See the matching comment in `Collector::walk_stmt`: the checker's
|
|
1985
|
+
// closure inference is unreliable (can error out entirely depending
|
|
1986
|
+
// on the body), but every closure value is an i32 pointer regardless
|
|
1987
|
+
// of its real signature, so don't bother inferring it at all here.
|
|
1988
|
+
let vty = if matches!(value, ast::Expr::Closure(_)) {
|
|
1989
|
+
PlumType::TFun(Vec::new(), Box::new(PlumType::TUnit))
|
|
1990
|
+
} else {
|
|
1324
|
-
|
|
1991
|
+
infer_local_type(value, ctx)
|
|
1992
|
+
};
|
|
1325
1993
|
compile_expr(value, body, ctx, state)?;
|
|
1326
1994
|
let idx = ctx
|
|
1327
1995
|
.locals
|
|
@@ -1330,6 +1998,17 @@ fn compile_stmt(stmt: &ast::Stmt, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mu
|
|
|
1330
1998
|
.ok_or_else(|| format!("undeclared local '{}'", target))?;
|
|
1331
1999
|
Instruction::LocalSet(idx).encode(body);
|
|
1332
2000
|
ctx.type_env.borrow_mut().insert(target.clone(), TypeScheme::mono(vty));
|
|
2001
|
+
// If this assigns a closure *literal*, remember its exact, already-
|
|
2002
|
+
// correct signature (computed by the discovery pass) so a later call
|
|
2003
|
+
// to it doesn't have to re-derive one — see `closure_local_sigs`.
|
|
2004
|
+
if let ast::Expr::Closure(cl) = value {
|
|
2005
|
+
let key = cl.as_ref() as *const ast::Closure as usize;
|
|
2006
|
+
if let Some(info) = ctx.closures.get(&key) {
|
|
2007
|
+
let mut sig_params = vec![ValType::I32];
|
|
2008
|
+
sig_params.extend(info.param_vts.iter().copied());
|
|
2009
|
+
ctx.closure_local_sigs.borrow_mut().insert(target.clone(), (sig_params, info.ret_vt));
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
1333
2012
|
}
|
|
1334
2013
|
}
|
|
1335
2014
|
ast::Stmt::Return(Some(e)) => {
|
|
@@ -1453,40 +2132,64 @@ fn expr_has_result(expr: &ast::Expr, ctx: &LocalCtx) -> bool {
|
|
|
1453
2132
|
/// so previously-working exhaustive enum matches (e.g. `Some`/`None`, `True`/`False`)
|
|
1454
2133
|
/// keep compiling even without a trailing wildcard, while a genuinely non-exhaustive
|
|
1455
2134
|
/// match (an `Int` match, or an enum match missing a variant) still gets a clear error.
|
|
2135
|
+
/// True if `cases` already covers every combination of enum variants across all
|
|
2136
|
+
/// subject positions (by explicit tag/constructor patterns only — no binding or
|
|
2137
|
+
/// wildcard in any position), i.e. the match is exhaustive at runtime even though
|
|
2138
|
+
/// `compile_match_arms_multi` can't see that from the remaining-cases slice alone.
|
|
2139
|
+
/// For a single subject this is "every variant of its enum is named somewhere";
|
|
2140
|
+
/// for `match a, b, ...` it's the full cross product (e.g. `Bool, Bool` needs all
|
|
2141
|
+
/// 4 combinations named, matching `libs/std/bool.plum`'s `and`/`or`).
|
|
1456
|
-
fn match_covers_every_enum_variant(cases: &[ast::Case],
|
|
2142
|
+
fn match_covers_every_enum_variant(cases: &[ast::Case], subject_vts: &[ValType], ctx: &LocalCtx) -> bool {
|
|
1457
|
-
if
|
|
2143
|
+
if subject_vts.is_empty() || subject_vts.iter().any(|vt| *vt != ValType::I32) {
|
|
1458
2144
|
return false;
|
|
1459
2145
|
}
|
|
2146
|
+
let n = subject_vts.len();
|
|
1460
|
-
let mut
|
|
2147
|
+
let mut enum_names: Vec<Option<String>> = vec![None; n];
|
|
1461
|
-
let mut
|
|
2148
|
+
let mut tuples_seen: std::collections::BTreeSet<Vec<i32>> = std::collections::BTreeSet::new();
|
|
1462
2149
|
for case in cases {
|
|
2150
|
+
if case.patterns.len() != n {
|
|
2151
|
+
return false;
|
|
2152
|
+
}
|
|
2153
|
+
let mut tuple = Vec::with_capacity(n);
|
|
2154
|
+
for (i, pat) in case.patterns.iter().enumerate() {
|
|
1463
|
-
|
|
2155
|
+
let variant_name = match pat {
|
|
1464
|
-
|
|
2156
|
+
ast::CasePattern::Name(nm)
|
|
1465
|
-
|
|
2157
|
+
if nm.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) && ctx.enum_variants.contains_key(nm) =>
|
|
1466
|
-
|
|
2158
|
+
{
|
|
1467
|
-
|
|
2159
|
+
nm.as_str()
|
|
1468
|
-
|
|
2160
|
+
}
|
|
1469
|
-
|
|
2161
|
+
ast::CasePattern::Class { name, .. } => name.as_str(),
|
|
2162
|
+
// A binding or wildcard at any position could match variants we
|
|
2163
|
+
// haven't otherwise named, so we can't prove full coverage this way.
|
|
1470
|
-
|
|
2164
|
+
_ => return false,
|
|
1471
|
-
|
|
2165
|
+
};
|
|
1472
|
-
|
|
2166
|
+
let info = match ctx.enum_variants.get(variant_name) {
|
|
1473
|
-
|
|
2167
|
+
Some(info) => info,
|
|
2168
|
+
None => return false,
|
|
2169
|
+
};
|
|
2170
|
+
match &enum_names[i] {
|
|
2171
|
+
Some(en) if en != &info.enum_name => return false,
|
|
2172
|
+
Some(_) => {}
|
|
2173
|
+
None => enum_names[i] = Some(info.enum_name.clone()),
|
|
2174
|
+
}
|
|
2175
|
+
tuple.push(info.tag);
|
|
2176
|
+
}
|
|
2177
|
+
tuples_seen.insert(tuple);
|
|
2178
|
+
}
|
|
2179
|
+
let mut total_combinations: usize = 1;
|
|
2180
|
+
for en in &enum_names {
|
|
2181
|
+
match en {
|
|
2182
|
+
Some(name) => {
|
|
2183
|
+
let count = ctx.enum_variants.values().filter(|v| &v.enum_name == name).count();
|
|
2184
|
+
total_combinations = match total_combinations.checked_mul(count) {
|
|
2185
|
+
Some(t) => t,
|
|
2186
|
+
None => return false,
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
1474
2189
|
None => return false,
|
|
1475
|
-
};
|
|
1476
|
-
match enum_name {
|
|
1477
|
-
Some(en) if en != info.enum_name => return false,
|
|
1478
|
-
Some(_) => {}
|
|
1479
|
-
None => enum_name = Some(&info.enum_name),
|
|
1480
2190
|
}
|
|
1481
|
-
tags_seen.insert(info.tag);
|
|
1482
|
-
}
|
|
1483
|
-
match enum_name {
|
|
1484
|
-
Some(en) => {
|
|
1485
|
-
let total_variants = ctx.enum_variants.values().filter(|v| v.enum_name == en).count();
|
|
1486
|
-
!tags_seen.is_empty() && tags_seen.len() == total_variants
|
|
1487
|
-
}
|
|
1488
|
-
None => false,
|
|
1489
2191
|
}
|
|
2192
|
+
!tuples_seen.is_empty() && tuples_seen.len() == total_combinations
|
|
1490
2193
|
}
|
|
1491
2194
|
|
|
1492
2195
|
fn compile_match(
|
|
@@ -1496,46 +2199,51 @@ fn compile_match(
|
|
|
1496
2199
|
state: &mut ModuleState,
|
|
1497
2200
|
result_vt: Option<ValType>,
|
|
1498
2201
|
) -> Result<(), String> {
|
|
1499
|
-
if m.subjects.len() != 1 {
|
|
1500
|
-
return Err("codegen: multi-subject match is not yet supported".to_string());
|
|
1501
|
-
}
|
|
1502
|
-
let subject = &m.subjects[0];
|
|
1503
|
-
let subject_ty = infer_local_type(subject, ctx);
|
|
1504
|
-
let subject_vt = plum_type_to_valtype(&subject_ty);
|
|
1505
|
-
|
|
1506
2202
|
let key = m as *const ast::Match as usize;
|
|
1507
|
-
let
|
|
2203
|
+
let base_slot = *ctx
|
|
1508
2204
|
.match_scratch_index
|
|
1509
2205
|
.get(&key)
|
|
1510
2206
|
.ok_or_else(|| "internal codegen error: missing match scratch slot".to_string())?;
|
|
1511
|
-
let scratch_local = ctx.match_scratch_base + slot;
|
|
1512
|
-
|
|
1513
|
-
compile_expr(subject, body, ctx, state)?;
|
|
1514
|
-
Instruction::LocalSet(scratch_local).encode(body);
|
|
1515
|
-
|
|
1516
|
-
// A match in value position whose arms already cover every variant of the
|
|
1517
|
-
// subject's enum type (by explicit tag/constructor patterns, no wildcard) is
|
|
1518
|
-
// exhaustive at runtime even though `compile_match_arms` can't see that from the
|
|
1519
|
-
// remaining-cases slice alone. Give it a synthetic trailing `_ => todo` arm so the
|
|
1520
|
-
// "out of patterns" fallback compiles to an (unreachable, but valid) trap instead
|
|
1521
|
-
// of a spurious non-exhaustive-match error.
|
|
1522
|
-
if result_vt.is_some() && match_covers_every_enum_variant(&m.cases, subject_vt, ctx) {
|
|
1523
|
-
let mut cases = m.cases.clone();
|
|
1524
|
-
cases.push(ast::Case {
|
|
1525
|
-
patterns: vec![ast::CasePattern::Wildcard],
|
|
1526
|
-
body: ast::Block { stmts: vec![ast::Stmt::Todo] },
|
|
1527
|
-
});
|
|
1528
|
-
return compile_match_arms(&cases, subject_vt, scratch_local, result_vt, body, ctx, state);
|
|
1529
|
-
}
|
|
1530
2207
|
|
|
2208
|
+
// Evaluate every subject into its own consecutive scratch local (one per
|
|
2209
|
+
// subject, in `match a, b, ...` order) before checking any pattern.
|
|
2210
|
+
let mut all_subjects: Vec<(ValType, u32)> = Vec::with_capacity(m.subjects.len());
|
|
2211
|
+
for (i, subject) in m.subjects.iter().enumerate() {
|
|
2212
|
+
let subject_ty = infer_local_type(subject, ctx);
|
|
2213
|
+
let subject_vt = plum_type_to_valtype(&subject_ty);
|
|
2214
|
+
let scratch_local = ctx.match_scratch_base + base_slot + i as u32;
|
|
2215
|
+
compile_expr(subject, body, ctx, state)?;
|
|
2216
|
+
Instruction::LocalSet(scratch_local).encode(body);
|
|
2217
|
+
all_subjects.push((subject_vt, scratch_local));
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
// A match in value position whose arms already cover every combination of
|
|
2221
|
+
// enum variants across all subjects (by explicit tag/constructor patterns, no
|
|
2222
|
+
// wildcard) is exhaustive at runtime even though `compile_match_arms_multi` can't
|
|
2223
|
+
// see that from the remaining-cases slice alone. `exhaustive_fallback` tells it to
|
|
2224
|
+
// compile the "ran out of cases" path as an (unreachable, but valid) trap instead
|
|
2225
|
+
// of a spurious non-exhaustive-match error — threaded through as a flag, rather
|
|
2226
|
+
// than appending a synthetic wildcard case by cloning `m.cases`, because cloning
|
|
2227
|
+
// would reallocate every nested `CasePattern::Class` node at a new address and
|
|
2228
|
+
// break `nested_class_scratch`'s pointer-identity-keyed lookup.
|
|
2229
|
+
let subject_vts: Vec<ValType> = all_subjects.iter().map(|(vt, _)| *vt).collect();
|
|
2230
|
+
let exhaustive_fallback = result_vt.is_some() && match_covers_every_enum_variant(&m.cases, &subject_vts, ctx);
|
|
2231
|
+
|
|
1531
|
-
|
|
2232
|
+
compile_match_arms_multi(&m.cases, &all_subjects, result_vt, exhaustive_fallback, body, ctx, state)
|
|
1532
2233
|
}
|
|
1533
2234
|
|
|
2235
|
+
/// Tries each case in turn (in source order); a case that fails to match falls
|
|
2236
|
+
/// through to the next one. `all_subjects` is the full `(valtype, scratch_local)`
|
|
2237
|
+
/// list for every subject of the enclosing `match`, shared unchanged across every
|
|
2238
|
+
/// case (each case's own pattern list is checked position-by-position against it
|
|
2239
|
+
/// via `compile_case_positions`). `exhaustive_fallback` (see `compile_match`) says
|
|
2240
|
+
/// what to do once `cases` runs out: trap (proven exhaustive) or report a
|
|
2241
|
+
/// non-exhaustive-match error.
|
|
1534
|
-
fn
|
|
2242
|
+
fn compile_match_arms_multi(
|
|
1535
2243
|
cases: &[ast::Case],
|
|
1536
|
-
|
|
2244
|
+
all_subjects: &[(ValType, u32)],
|
|
1537
|
-
scratch_local: u32,
|
|
1538
2245
|
result_vt: Option<ValType>,
|
|
2246
|
+
exhaustive_fallback: bool,
|
|
1539
2247
|
body: &mut Vec<u8>,
|
|
1540
2248
|
ctx: &LocalCtx,
|
|
1541
2249
|
state: &mut ModuleState,
|
|
@@ -1543,6 +2251,10 @@ fn compile_match_arms(
|
|
|
1543
2251
|
let (case, rest) = match cases.split_first() {
|
|
1544
2252
|
None => {
|
|
1545
2253
|
return match result_vt {
|
|
2254
|
+
Some(_) if exhaustive_fallback => {
|
|
2255
|
+
Instruction::Unreachable.encode(body);
|
|
2256
|
+
Ok(())
|
|
2257
|
+
}
|
|
1546
2258
|
Some(_) => Err(
|
|
1547
2259
|
"codegen: function has a control-flow path that doesn't produce a return value (non-exhaustive match)".to_string(),
|
|
1548
2260
|
),
|
|
@@ -1551,17 +2263,49 @@ fn compile_match_arms(
|
|
|
1551
2263
|
}
|
|
1552
2264
|
Some(pair) => pair,
|
|
1553
2265
|
};
|
|
2266
|
+
if case.patterns.len() != all_subjects.len() {
|
|
2267
|
+
return Err(format!(
|
|
2268
|
+
"codegen: match case has {} pattern(s), expected {} (one per subject)",
|
|
2269
|
+
case.patterns.len(), all_subjects.len()
|
|
2270
|
+
));
|
|
2271
|
+
}
|
|
2272
|
+
compile_case_positions(case, 0, all_subjects, rest, result_vt, exhaustive_fallback, body, ctx, state)
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
/// Checks `case.patterns[pos]` against `all_subjects[pos]`; on success, recurses to
|
|
2276
|
+
/// `pos + 1` (or, once every position has matched, compiles the case body). On
|
|
2277
|
+
/// failure at any position, falls through to `compile_match_arms_multi(rest, ...)`
|
|
2278
|
+
/// — i.e. the *entire next case*, restarting from its own position 0, not the next
|
|
1554
|
-
|
|
2279
|
+
/// position of this case. This is what gives `match a, b` its "all positions must
|
|
2280
|
+
/// match" (AND) semantics while still trying cases in order.
|
|
2281
|
+
#[allow(clippy::too_many_arguments)]
|
|
2282
|
+
fn compile_case_positions(
|
|
2283
|
+
case: &ast::Case,
|
|
2284
|
+
pos: usize,
|
|
2285
|
+
all_subjects: &[(ValType, u32)],
|
|
2286
|
+
rest: &[ast::Case],
|
|
2287
|
+
result_vt: Option<ValType>,
|
|
2288
|
+
exhaustive_fallback: bool,
|
|
2289
|
+
body: &mut Vec<u8>,
|
|
2290
|
+
ctx: &LocalCtx,
|
|
2291
|
+
state: &mut ModuleState,
|
|
2292
|
+
) -> Result<(), String> {
|
|
2293
|
+
if pos == case.patterns.len() {
|
|
2294
|
+
// Every position matched.
|
|
2295
|
+
return compile_case_body(&case.body, result_vt, body, ctx, state);
|
|
2296
|
+
}
|
|
2297
|
+
let pat = &case.patterns[pos];
|
|
2298
|
+
let (subject_vt, scratch_local) = all_subjects[pos];
|
|
1555
2299
|
match pat {
|
|
1556
2300
|
ast::CasePattern::Wildcard => {
|
|
1557
|
-
//
|
|
2301
|
+
// Always matches this position; move on to the next one.
|
|
1558
|
-
|
|
2302
|
+
compile_case_positions(case, pos + 1, all_subjects, rest, result_vt, exhaustive_fallback, body, ctx, state)
|
|
1559
2303
|
}
|
|
1560
2304
|
ast::CasePattern::Name(n) => {
|
|
1561
2305
|
let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
|
|
1562
2306
|
&& ctx.enum_variants.contains_key(n);
|
|
1563
2307
|
if is_variant {
|
|
1564
|
-
compile_variant_eq_arm(n, subject_vt, scratch_local,
|
|
2308
|
+
compile_variant_eq_arm(n, subject_vt, scratch_local, case, pos, all_subjects, rest, result_vt, exhaustive_fallback, body, ctx, state)
|
|
1565
2309
|
} else {
|
|
1566
2310
|
let idx = ctx
|
|
1567
2311
|
.locals
|
|
@@ -1571,8 +2315,8 @@ fn compile_match_arms(
|
|
|
1571
2315
|
Instruction::LocalGet(scratch_local).encode(body);
|
|
1572
2316
|
Instruction::LocalSet(idx).encode(body);
|
|
1573
2317
|
ctx.type_env.borrow_mut().insert(n.clone(), TypeScheme::mono(plum_type_from_valtype_hint(subject_vt)));
|
|
1574
|
-
compile_case_body(&case.body, result_vt, body, ctx, state)
|
|
1575
|
-
// A binding
|
|
2318
|
+
// A binding always matches this position; move on to the next one.
|
|
2319
|
+
compile_case_positions(case, pos + 1, all_subjects, rest, result_vt, exhaustive_fallback, body, ctx, state)
|
|
1576
2320
|
}
|
|
1577
2321
|
}
|
|
1578
2322
|
ast::CasePattern::Int(n) => {
|
|
@@ -1583,16 +2327,16 @@ fn compile_match_arms(
|
|
|
1583
2327
|
Instruction::I64Const(*n).encode(body);
|
|
1584
2328
|
Instruction::I64Eq.encode(body);
|
|
1585
2329
|
Instruction::If(block_type_for(result_vt)).encode(body);
|
|
1586
|
-
|
|
2330
|
+
compile_case_positions(case, pos + 1, all_subjects, rest, result_vt, exhaustive_fallback, body, ctx, state)?;
|
|
1587
2331
|
Instruction::Else.encode(body);
|
|
1588
|
-
|
|
2332
|
+
compile_match_arms_multi(rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state)?;
|
|
1589
2333
|
Instruction::End.encode(body);
|
|
1590
2334
|
Ok(())
|
|
1591
2335
|
}
|
|
1592
2336
|
ast::CasePattern::String(_) => Err("codegen: string match patterns are not yet supported".to_string()),
|
|
1593
2337
|
ast::CasePattern::Float(_) => Err("codegen: float match patterns are not yet supported".to_string()),
|
|
1594
2338
|
ast::CasePattern::Class { name, fields } => {
|
|
1595
|
-
compile_variant_constructor_arm(name, fields, subject_vt, scratch_local,
|
|
2339
|
+
compile_variant_constructor_arm(name, fields, subject_vt, scratch_local, case, pos, all_subjects, rest, result_vt, exhaustive_fallback, body, ctx, state)
|
|
1596
2340
|
}
|
|
1597
2341
|
}
|
|
1598
2342
|
}
|
|
@@ -1602,9 +2346,12 @@ fn compile_variant_eq_arm(
|
|
|
1602
2346
|
name: &str,
|
|
1603
2347
|
subject_vt: ValType,
|
|
1604
2348
|
scratch_local: u32,
|
|
1605
|
-
result_vt: Option<ValType>,
|
|
1606
2349
|
case: &ast::Case,
|
|
2350
|
+
pos: usize,
|
|
2351
|
+
all_subjects: &[(ValType, u32)],
|
|
1607
2352
|
rest: &[ast::Case],
|
|
2353
|
+
result_vt: Option<ValType>,
|
|
2354
|
+
exhaustive_fallback: bool,
|
|
1608
2355
|
body: &mut Vec<u8>,
|
|
1609
2356
|
ctx: &LocalCtx,
|
|
1610
2357
|
state: &mut ModuleState,
|
|
@@ -1621,9 +2368,9 @@ fn compile_variant_eq_arm(
|
|
|
1621
2368
|
Instruction::I32Const(tag).encode(body);
|
|
1622
2369
|
Instruction::I32Eq.encode(body);
|
|
1623
2370
|
Instruction::If(block_type_for(result_vt)).encode(body);
|
|
1624
|
-
|
|
2371
|
+
compile_case_positions(case, pos + 1, all_subjects, rest, result_vt, exhaustive_fallback, body, ctx, state)?;
|
|
1625
2372
|
Instruction::Else.encode(body);
|
|
1626
|
-
|
|
2373
|
+
compile_match_arms_multi(rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state)?;
|
|
1627
2374
|
Instruction::End.encode(body);
|
|
1628
2375
|
Ok(())
|
|
1629
2376
|
}
|
|
@@ -1634,9 +2381,12 @@ fn compile_variant_constructor_arm(
|
|
|
1634
2381
|
fields: &[ast::CasePattern],
|
|
1635
2382
|
subject_vt: ValType,
|
|
1636
2383
|
scratch_local: u32,
|
|
1637
|
-
result_vt: Option<ValType>,
|
|
1638
2384
|
case: &ast::Case,
|
|
2385
|
+
pos: usize,
|
|
2386
|
+
all_subjects: &[(ValType, u32)],
|
|
1639
2387
|
rest: &[ast::Case],
|
|
2388
|
+
result_vt: Option<ValType>,
|
|
2389
|
+
exhaustive_fallback: bool,
|
|
1640
2390
|
body: &mut Vec<u8>,
|
|
1641
2391
|
ctx: &LocalCtx,
|
|
1642
2392
|
state: &mut ModuleState,
|
|
@@ -1674,34 +2424,151 @@ fn compile_variant_constructor_arm(
|
|
|
1674
2424
|
Instruction::I32Const(0).encode(body);
|
|
1675
2425
|
Instruction::End.encode(body);
|
|
1676
2426
|
Instruction::If(block_type_for(result_vt)).encode(body);
|
|
2427
|
+
compile_field_patterns(
|
|
2428
|
+
fields, &field_types, 0, scratch_local, rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state,
|
|
2429
|
+
&mut |body, state| compile_case_positions(case, pos + 1, all_subjects, rest, result_vt, exhaustive_fallback, body, ctx, state),
|
|
2430
|
+
)?;
|
|
2431
|
+
Instruction::Else.encode(body);
|
|
2432
|
+
compile_match_arms_multi(rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state)?;
|
|
2433
|
+
Instruction::End.encode(body);
|
|
2434
|
+
Ok(())
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
/// Checks `fields[fpos..]` (a constructor pattern's own sub-patterns, e.g. the `v` in
|
|
2438
|
+
/// `Some(v)`, or — recursively — the `Some(v)` in `Wrap(Some(v))`) against the
|
|
1677
|
-
|
|
2439
|
+
/// already-loaded value in `container_local`, one field at a time. Once every field
|
|
2440
|
+
/// has matched, calls `on_match` (typically: proceed to the next top-level subject
|
|
2441
|
+
/// position). A mismatch at any field — at any nesting depth — falls through to
|
|
2442
|
+
/// `compile_match_arms_multi(rest, ...)`, exactly like a top-level pattern mismatch.
|
|
2443
|
+
#[allow(clippy::too_many_arguments)]
|
|
2444
|
+
fn compile_field_patterns(
|
|
2445
|
+
fields: &[ast::CasePattern],
|
|
2446
|
+
field_types: &[PlumType],
|
|
2447
|
+
fpos: usize,
|
|
2448
|
+
container_local: u32,
|
|
2449
|
+
rest: &[ast::Case],
|
|
2450
|
+
all_subjects: &[(ValType, u32)],
|
|
2451
|
+
result_vt: Option<ValType>,
|
|
2452
|
+
exhaustive_fallback: bool,
|
|
2453
|
+
body: &mut Vec<u8>,
|
|
2454
|
+
ctx: &LocalCtx,
|
|
2455
|
+
state: &mut ModuleState,
|
|
2456
|
+
on_match: &mut dyn FnMut(&mut Vec<u8>, &mut ModuleState) -> Result<(), String>,
|
|
2457
|
+
) -> Result<(), String> {
|
|
2458
|
+
if fpos == fields.len() {
|
|
2459
|
+
return on_match(body, state);
|
|
2460
|
+
}
|
|
1678
|
-
|
|
2461
|
+
let pat = &fields[fpos];
|
|
2462
|
+
let field_ty = &field_types[fpos];
|
|
1679
|
-
|
|
2463
|
+
let field_vt = plum_type_to_valtype(field_ty);
|
|
2464
|
+
let offset = ((fpos + 1) as u64) * 8;
|
|
2465
|
+
|
|
2466
|
+
match pat {
|
|
1680
|
-
|
|
2467
|
+
ast::CasePattern::Wildcard => {
|
|
1681
|
-
|
|
2468
|
+
compile_field_patterns(fields, field_types, fpos + 1, container_local, rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state, on_match)
|
|
1682
|
-
}
|
|
2469
|
+
}
|
|
2470
|
+
ast::CasePattern::Name(n) if !(n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) && ctx.enum_variants.contains_key(n)) => {
|
|
2471
|
+
// A plain binding always matches this field; load it straight into its
|
|
1683
|
-
|
|
2472
|
+
// binding local and move on to the next field.
|
|
1684
2473
|
let idx = ctx
|
|
1685
2474
|
.locals
|
|
1686
2475
|
.get(n)
|
|
1687
2476
|
.copied()
|
|
1688
2477
|
.ok_or_else(|| format!("internal codegen error: missing binding local '{}'", n))?;
|
|
1689
|
-
Instruction::LocalGet(
|
|
2478
|
+
Instruction::LocalGet(container_local).encode(body);
|
|
1690
|
-
let offset = ((i + 1) as u64) * 8;
|
|
1691
|
-
|
|
2479
|
+
emit_load(field_vt, offset, body);
|
|
1692
|
-
ValType::I64 => Instruction::I64Load(MemArg { offset, align: 3, memory_index: 0 }),
|
|
1693
|
-
ValType::F64 => Instruction::F64Load(MemArg { offset, align: 3, memory_index: 0 }),
|
|
1694
|
-
_ => Instruction::I32Load(MemArg { offset, align: 2, memory_index: 0 }),
|
|
1695
|
-
}.encode(body);
|
|
1696
2480
|
Instruction::LocalSet(idx).encode(body);
|
|
1697
2481
|
ctx.type_env.borrow_mut().insert(n.to_string(), TypeScheme::mono(field_ty.clone()));
|
|
2482
|
+
compile_field_patterns(fields, field_types, fpos + 1, container_local, rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state, on_match)
|
|
2483
|
+
}
|
|
2484
|
+
ast::CasePattern::Name(n) => {
|
|
2485
|
+
// An uppercase, payload-free variant name used as a field pattern (e.g.
|
|
2486
|
+
// matching a nested `None` rather than binding a name to it).
|
|
2487
|
+
let info = ctx
|
|
2488
|
+
.enum_variants
|
|
2489
|
+
.get(n)
|
|
2490
|
+
.ok_or_else(|| format!("codegen: unknown enum variant '{}'", n))?;
|
|
2491
|
+
if field_vt != ValType::I32 {
|
|
2492
|
+
return Err(format!("codegen: enum tag pattern '{}' against a non-enum field", n));
|
|
2493
|
+
}
|
|
2494
|
+
Instruction::LocalGet(container_local).encode(body);
|
|
2495
|
+
emit_load(field_vt, offset, body);
|
|
2496
|
+
Instruction::I32Const(info.tag).encode(body);
|
|
2497
|
+
Instruction::I32Eq.encode(body);
|
|
2498
|
+
Instruction::If(block_type_for(result_vt)).encode(body);
|
|
2499
|
+
compile_field_patterns(fields, field_types, fpos + 1, container_local, rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state, on_match)?;
|
|
2500
|
+
Instruction::Else.encode(body);
|
|
2501
|
+
compile_match_arms_multi(rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state)?;
|
|
2502
|
+
Instruction::End.encode(body);
|
|
2503
|
+
Ok(())
|
|
2504
|
+
}
|
|
2505
|
+
ast::CasePattern::Int(n) => {
|
|
2506
|
+
if field_vt != ValType::I64 {
|
|
2507
|
+
return Err("codegen: integer match pattern against a non-Int field".to_string());
|
|
2508
|
+
}
|
|
2509
|
+
Instruction::LocalGet(container_local).encode(body);
|
|
2510
|
+
emit_load(field_vt, offset, body);
|
|
2511
|
+
Instruction::I64Const(*n).encode(body);
|
|
2512
|
+
Instruction::I64Eq.encode(body);
|
|
2513
|
+
Instruction::If(block_type_for(result_vt)).encode(body);
|
|
2514
|
+
compile_field_patterns(fields, field_types, fpos + 1, container_local, rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state, on_match)?;
|
|
2515
|
+
Instruction::Else.encode(body);
|
|
2516
|
+
compile_match_arms_multi(rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state)?;
|
|
2517
|
+
Instruction::End.encode(body);
|
|
2518
|
+
Ok(())
|
|
2519
|
+
}
|
|
2520
|
+
ast::CasePattern::String(_) => Err("codegen: string match patterns are not yet supported".to_string()),
|
|
2521
|
+
ast::CasePattern::Float(_) => Err("codegen: float match patterns are not yet supported".to_string()),
|
|
2522
|
+
ast::CasePattern::Class { name, fields: inner_fields } => {
|
|
2523
|
+
let info = ctx
|
|
2524
|
+
.enum_variants
|
|
2525
|
+
.get(name)
|
|
2526
|
+
.ok_or_else(|| format!("codegen: unknown enum variant '{}'", name))?;
|
|
2527
|
+
if field_vt != ValType::I32 {
|
|
2528
|
+
return Err(format!("codegen: constructor pattern '{}' against a non-enum field", name));
|
|
2529
|
+
}
|
|
2530
|
+
if inner_fields.len() != info.field_types.len() {
|
|
2531
|
+
return Err(format!(
|
|
2532
|
+
"codegen: constructor pattern '{}' expects {} field(s), got {}",
|
|
2533
|
+
name, info.field_types.len(), inner_fields.len()
|
|
2534
|
+
));
|
|
2535
|
+
}
|
|
2536
|
+
let key = pat as *const ast::CasePattern as usize;
|
|
2537
|
+
let slot = *ctx
|
|
2538
|
+
.nested_class_scratch
|
|
2539
|
+
.get(&key)
|
|
2540
|
+
.ok_or_else(|| "internal codegen error: missing nested constructor pattern scratch slot".to_string())?;
|
|
2541
|
+
let nested_local = ctx.nested_class_scratch_base + slot;
|
|
2542
|
+
let tag = info.tag;
|
|
2543
|
+
let inner_field_types = info.field_types.clone();
|
|
2544
|
+
|
|
2545
|
+
Instruction::LocalGet(container_local).encode(body);
|
|
2546
|
+
emit_load(field_vt, offset, body);
|
|
2547
|
+
Instruction::LocalSet(nested_local).encode(body);
|
|
2548
|
+
|
|
2549
|
+
// Same heap-pointer guard as the top-level constructor check: a
|
|
2550
|
+
// payload-free sibling variant is a small int tag, not a pointer.
|
|
2551
|
+
Instruction::LocalGet(nested_local).encode(body);
|
|
2552
|
+
Instruction::I32Const(HEAP_BASE as i32).encode(body);
|
|
2553
|
+
Instruction::I32GeU.encode(body);
|
|
2554
|
+
Instruction::If(BlockType::Result(ValType::I32)).encode(body);
|
|
2555
|
+
Instruction::LocalGet(nested_local).encode(body);
|
|
2556
|
+
Instruction::I32Load(MemArg { offset: 0, align: 2, memory_index: 0 }).encode(body);
|
|
2557
|
+
Instruction::I32Const(tag).encode(body);
|
|
2558
|
+
Instruction::I32Eq.encode(body);
|
|
2559
|
+
Instruction::Else.encode(body);
|
|
2560
|
+
Instruction::I32Const(0).encode(body);
|
|
2561
|
+
Instruction::End.encode(body);
|
|
2562
|
+
Instruction::If(block_type_for(result_vt)).encode(body);
|
|
2563
|
+
compile_field_patterns(inner_fields, &inner_field_types, 0, nested_local, rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state, &mut |body, state| {
|
|
2564
|
+
compile_field_patterns(fields, field_types, fpos + 1, container_local, rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state, on_match)
|
|
2565
|
+
})?;
|
|
2566
|
+
Instruction::Else.encode(body);
|
|
2567
|
+
compile_match_arms_multi(rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state)?;
|
|
2568
|
+
Instruction::End.encode(body);
|
|
2569
|
+
Ok(())
|
|
1698
2570
|
}
|
|
1699
2571
|
}
|
|
1700
|
-
compile_case_body(&case.body, result_vt, body, ctx, state)?;
|
|
1701
|
-
Instruction::Else.encode(body);
|
|
1702
|
-
compile_match_arms(rest, subject_vt, scratch_local, result_vt, body, ctx, state)?;
|
|
1703
|
-
Instruction::End.encode(body);
|
|
1704
|
-
Ok(())
|
|
1705
2572
|
}
|
|
1706
2573
|
|
|
1707
2574
|
fn plum_type_from_valtype_hint(vt: ValType) -> PlumType {
|
|
@@ -1721,11 +2588,19 @@ fn compile_expr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mu
|
|
|
1721
2588
|
Instruction::F64Const(*f).encode(body);
|
|
1722
2589
|
}
|
|
1723
2590
|
ast::Expr::Var(name) => {
|
|
2591
|
+
match ctx.locals.get(name.as_str()) {
|
|
2592
|
+
Some(idx) => Instruction::LocalGet(*idx).encode(body),
|
|
2593
|
+
// Not a local: a bare reference to a top-level function used as a
|
|
2594
|
+
// value (e.g. `each(double)`) — push its zero-capture trampoline
|
|
2595
|
+
// closure's (compile-time-constant) struct pointer.
|
|
2596
|
+
None => {
|
|
1724
|
-
|
|
2597
|
+
let offset = *ctx
|
|
1725
|
-
|
|
2598
|
+
.named_fn_values
|
|
1726
|
-
|
|
2599
|
+
.get(name.as_str())
|
|
1727
|
-
|
|
2600
|
+
.ok_or_else(|| format!("undeclared variable '{}'", name))?;
|
|
1728
|
-
|
|
2601
|
+
Instruction::I32Const(offset as i32).encode(body);
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
1729
2604
|
}
|
|
1730
2605
|
ast::Expr::Paren(inner) => {
|
|
1731
2606
|
compile_expr(inner, body, ctx, state)?;
|
|
@@ -1943,23 +2818,20 @@ fn compile_expr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mu
|
|
|
1943
2818
|
}
|
|
1944
2819
|
}
|
|
1945
2820
|
ast::Expr::String(s) => {
|
|
2821
|
+
let has_interp = s.parts.iter().any(|p| matches!(p, ast::StringPart::Interp(_)));
|
|
2822
|
+
if !has_interp {
|
|
2823
|
+
// Fast path: every part is static text, so the whole literal is one
|
|
2824
|
+
// fixed byte blob known at compile time — no runtime work at all.
|
|
1946
|
-
|
|
2825
|
+
let mut text = String::new();
|
|
1947
|
-
|
|
2826
|
+
for part in &s.parts {
|
|
1948
|
-
match part {
|
|
1949
|
-
ast::StringPart::Text(t) => text.push_str(t),
|
|
1950
|
-
ast::StringPart::
|
|
2827
|
+
if let ast::StringPart::Text(t) = part {
|
|
1951
|
-
|
|
2828
|
+
text.push_str(t);
|
|
1952
2829
|
}
|
|
1953
2830
|
}
|
|
2831
|
+
compile_static_string(&text, body, state);
|
|
2832
|
+
} else {
|
|
2833
|
+
compile_interpolated_string(s, body, ctx, state)?;
|
|
1954
2834
|
}
|
|
1955
|
-
let bytes = text.as_bytes();
|
|
1956
|
-
let offset = state.next_string_offset;
|
|
1957
|
-
let mut data = Vec::with_capacity(4 + bytes.len());
|
|
1958
|
-
data.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
|
|
1959
|
-
data.extend_from_slice(bytes);
|
|
1960
|
-
state.next_string_offset += data.len() as u32;
|
|
1961
|
-
state.data_segments.push((offset, data));
|
|
1962
|
-
Instruction::I32Const(offset as i32).encode(body);
|
|
1963
2835
|
}
|
|
1964
2836
|
ast::Expr::Closure(cl) => {
|
|
1965
2837
|
compile_closure_literal(cl, body, ctx, state)?;
|
|
@@ -2047,6 +2919,74 @@ fn emit_load(vt: ValType, offset: u64, body: &mut Vec<u8>) {
|
|
|
2047
2919
|
}.encode(body);
|
|
2048
2920
|
}
|
|
2049
2921
|
|
|
2922
|
+
/// Emits a static (compile-time-known) length-prefixed string as a fresh data
|
|
2923
|
+
/// segment, pushing its address.
|
|
2924
|
+
fn compile_static_string(text: &str, body: &mut Vec<u8>, state: &mut ModuleState) {
|
|
2925
|
+
let bytes = text.as_bytes();
|
|
2926
|
+
let offset = state.next_string_offset;
|
|
2927
|
+
let mut data = Vec::with_capacity(4 + bytes.len());
|
|
2928
|
+
data.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
|
|
2929
|
+
data.extend_from_slice(bytes);
|
|
2930
|
+
state.next_string_offset += data.len() as u32;
|
|
2931
|
+
state.data_segments.push((offset, data));
|
|
2932
|
+
Instruction::I32Const(offset as i32).encode(body);
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
/// Lowers a string literal that contains at least one `{expr}` interpolation.
|
|
2936
|
+
/// Every part becomes a string-pointer-valued expression (static text via
|
|
2937
|
+
/// `compile_static_string`; `Str`/`Int`/`Bool` interpolated values converted at
|
|
2938
|
+
/// runtime), then all parts are left-folded together with the `__string_concat`
|
|
2939
|
+
/// runtime helper.
|
|
2940
|
+
fn compile_interpolated_string(
|
|
2941
|
+
s: &ast::StringExpr,
|
|
2942
|
+
body: &mut Vec<u8>,
|
|
2943
|
+
ctx: &LocalCtx,
|
|
2944
|
+
state: &mut ModuleState,
|
|
2945
|
+
) -> Result<(), String> {
|
|
2946
|
+
let mut first = true;
|
|
2947
|
+
for part in &s.parts {
|
|
2948
|
+
match part {
|
|
2949
|
+
ast::StringPart::Text(t) => {
|
|
2950
|
+
compile_static_string(t, body, state);
|
|
2951
|
+
}
|
|
2952
|
+
ast::StringPart::Interp(expr) => {
|
|
2953
|
+
let ty = infer_local_type(expr, ctx);
|
|
2954
|
+
match ty {
|
|
2955
|
+
PlumType::TStr => {
|
|
2956
|
+
compile_expr(expr, body, ctx, state)?;
|
|
2957
|
+
}
|
|
2958
|
+
PlumType::TInt => {
|
|
2959
|
+
compile_expr(expr, body, ctx, state)?;
|
|
2960
|
+
Instruction::Call(ctx.int_to_string_func).encode(body);
|
|
2961
|
+
}
|
|
2962
|
+
PlumType::TBool => {
|
|
2963
|
+
compile_expr(expr, body, ctx, state)?;
|
|
2964
|
+
Instruction::If(BlockType::Result(ValType::I32)).encode(body);
|
|
2965
|
+
compile_static_string("True", body, state);
|
|
2966
|
+
Instruction::Else.encode(body);
|
|
2967
|
+
compile_static_string("False", body, state);
|
|
2968
|
+
Instruction::End.encode(body);
|
|
2969
|
+
}
|
|
2970
|
+
PlumType::TFloat => {
|
|
2971
|
+
return Err("codegen: interpolating a Float value is not yet supported".to_string());
|
|
2972
|
+
}
|
|
2973
|
+
other => {
|
|
2974
|
+
return Err(format!(
|
|
2975
|
+
"codegen: interpolating a value of type {} is not yet supported",
|
|
2976
|
+
other
|
|
2977
|
+
));
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
if !first {
|
|
2983
|
+
Instruction::Call(ctx.string_concat_func).encode(body);
|
|
2984
|
+
}
|
|
2985
|
+
first = false;
|
|
2986
|
+
}
|
|
2987
|
+
Ok(())
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2050
2990
|
/// Compiles a closure *literal* at its construction site. Snapshots each captured free
|
|
2051
2991
|
/// variable's CURRENT value into a fresh heap env struct, then builds the 2-word closure
|
|
2052
2992
|
/// struct `{table_index @0, env_pointer @8}` and leaves its pointer on the stack.
|
|
@@ -2060,7 +3000,7 @@ fn compile_closure_literal(
|
|
|
2060
3000
|
let info = ctx
|
|
2061
3001
|
.closures
|
|
2062
3002
|
.get(&key)
|
|
2063
|
-
.ok_or_else(|| "codegen: closure literal was not discovered
|
|
3003
|
+
.ok_or_else(|| "internal codegen error: closure literal was not discovered by the discovery pre-pass".to_string())?;
|
|
2064
3004
|
let scratch_off = *ctx
|
|
2065
3005
|
.closure_scratch
|
|
2066
3006
|
.get(&key)
|
|
@@ -2124,20 +3064,28 @@ fn compile_closure_call(
|
|
|
2124
3064
|
.get(&call.name)
|
|
2125
3065
|
.ok_or_else(|| format!("codegen: closure '{}' is not a local", call.name))?;
|
|
2126
3066
|
|
|
3067
|
+
// Prefer the exact signature recorded when this local was assigned a closure
|
|
3068
|
+
// *literal* (see `closure_local_sigs`) — it's already correct. Otherwise (e.g.
|
|
3069
|
+
// `call.name` is a `fn(...)`-typed parameter, whose declared type is reliable on
|
|
3070
|
+
// its own) fall back to re-deriving it from the type env.
|
|
3071
|
+
let sig_key: ClosureSigKey = match ctx.closure_local_sigs.borrow().get(&call.name) {
|
|
3072
|
+
Some(key) => key.clone(),
|
|
3073
|
+
None => {
|
|
2127
|
-
|
|
3074
|
+
let (param_ptypes, ret_ptype) = match infer_local_type(&ast::Expr::Var(call.name.clone()), ctx) {
|
|
2128
|
-
|
|
3075
|
+
PlumType::TFun(p, r) => (p, *r),
|
|
2129
|
-
|
|
3076
|
+
other => return Err(format!("codegen: '{}' is not callable (type {:?})", call.name, other)),
|
|
2130
|
-
|
|
3077
|
+
};
|
|
2131
|
-
|
|
2132
|
-
|
|
3078
|
+
let mut sig_params = vec![ValType::I32]; // env pointer
|
|
2133
|
-
|
|
3079
|
+
for p in ¶m_ptypes {
|
|
2134
|
-
|
|
3080
|
+
sig_params.push(plum_type_to_valtype(p));
|
|
2135
|
-
|
|
3081
|
+
}
|
|
2136
|
-
|
|
3082
|
+
let ret_vt = match ret_ptype {
|
|
2137
|
-
|
|
3083
|
+
PlumType::TUnit => None,
|
|
2138
|
-
|
|
3084
|
+
other => Some(plum_type_to_valtype(&other)),
|
|
3085
|
+
};
|
|
3086
|
+
(sig_params, ret_vt)
|
|
3087
|
+
}
|
|
2139
3088
|
};
|
|
2140
|
-
let sig_key: ClosureSigKey = (sig_params, ret_vt);
|
|
2141
3089
|
let type_index = *ctx
|
|
2142
3090
|
.closure_call_types
|
|
2143
3091
|
.get(&sig_key)
|
|
@@ -2187,6 +3135,8 @@ fn compile_closure_body(
|
|
|
2187
3135
|
next_classcall_slot: 0,
|
|
2188
3136
|
closure_scratch: HashMap::new(),
|
|
2189
3137
|
next_closure_slot: 0,
|
|
3138
|
+
nested_class_scratch: HashMap::new(),
|
|
3139
|
+
next_nested_class_slot: 0,
|
|
2190
3140
|
};
|
|
2191
3141
|
collector.walk_block(&cl.body);
|
|
2192
3142
|
|
|
@@ -2224,9 +3174,18 @@ fn compile_closure_body(
|
|
|
2224
3174
|
|
|
2225
3175
|
let match_scratch_base = idx;
|
|
2226
3176
|
let mut match_scratch_index: HashMap<usize, u32> = HashMap::new();
|
|
2227
|
-
for (ptr,
|
|
3177
|
+
for (ptr, types) in collector.match_scratch.iter() {
|
|
2228
3178
|
match_scratch_index.insert(*ptr, idx - match_scratch_base);
|
|
3179
|
+
for ty in types {
|
|
2229
|
-
|
|
3180
|
+
groups.push(plum_type_to_valtype(ty));
|
|
3181
|
+
idx += 1;
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
let nested_class_scratch_base = idx;
|
|
3186
|
+
let nested_class_scratch_count = collector.nested_class_scratch.values().copied().max().map(|m| m + 1).unwrap_or(0);
|
|
3187
|
+
for _ in 0..nested_class_scratch_count {
|
|
3188
|
+
groups.push(ValType::I32);
|
|
2230
3189
|
idx += 1;
|
|
2231
3190
|
}
|
|
2232
3191
|
|
|
@@ -2262,14 +3221,20 @@ fn compile_closure_body(
|
|
|
2262
3221
|
match_scratch_index,
|
|
2263
3222
|
closure_scratch_base,
|
|
2264
3223
|
closure_scratch: collector.closure_scratch,
|
|
3224
|
+
nested_class_scratch_base,
|
|
3225
|
+
nested_class_scratch: collector.nested_class_scratch,
|
|
2265
3226
|
func_ids: &ctx.func_ids,
|
|
2266
3227
|
func_sigs: &ctx.func_sigs,
|
|
2267
3228
|
closures: &ctx.closures,
|
|
2268
3229
|
closure_call_types: &ctx.closure_call_types,
|
|
3230
|
+
named_fn_values: &ctx.named_fn_values,
|
|
3231
|
+
string_concat_func: ctx.string_concat_func,
|
|
3232
|
+
int_to_string_func: ctx.int_to_string_func,
|
|
2269
3233
|
classes: &ctx.classes,
|
|
2270
3234
|
methods: &ctx.methods,
|
|
2271
3235
|
enum_variants: &ctx.enum_variants,
|
|
2272
3236
|
type_env: RefCell::new(base_env),
|
|
3237
|
+
closure_local_sigs: RefCell::new(HashMap::new()),
|
|
2273
3238
|
bump_global: ctx.bump_global,
|
|
2274
3239
|
};
|
|
2275
3240
|
|
plum-wasm-codegen/tests/codegen_tests.rs
CHANGED
|
@@ -80,11 +80,62 @@ fn empty_string_literal_compiles() {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
#[test]
|
|
83
|
-
fn
|
|
83
|
+
fn string_interpolation_of_an_int_runs_correctly() {
|
|
84
|
-
let src = "main() -> Str =\n x =
|
|
84
|
+
let src = "main() -> Str =\n x = 42\n \"{x}\"\n";
|
|
85
85
|
let source = parse(src);
|
|
86
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
87
|
+
assert_eq!(run_main_str(&bytes), "42");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#[test]
|
|
91
|
+
fn string_interpolation_of_a_negative_int_runs_correctly() {
|
|
92
|
+
let src = "main() -> Str =\n x = -7\n \"{x}\"\n";
|
|
93
|
+
let source = parse(src);
|
|
94
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
95
|
+
assert_eq!(run_main_str(&bytes), "-7");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#[test]
|
|
99
|
+
fn string_interpolation_of_zero_runs_correctly() {
|
|
100
|
+
let src = "main() -> Str =\n x = 0\n \"{x}\"\n";
|
|
101
|
+
let source = parse(src);
|
|
102
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
103
|
+
assert_eq!(run_main_str(&bytes), "0");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#[test]
|
|
107
|
+
fn string_interpolation_with_surrounding_text_and_multiple_interps_runs_correctly() {
|
|
108
|
+
let src = "main() -> Str =\n count = 3\n total = 10\n \"{count} of {total} complete\"\n";
|
|
109
|
+
let source = parse(src);
|
|
110
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
111
|
+
assert_eq!(run_main_str(&bytes), "3 of 10 complete");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#[test]
|
|
115
|
+
fn string_interpolation_of_a_str_runs_correctly() {
|
|
116
|
+
let src = "greet(name: Str) -> Str =\n \"Hello, {name}!\"\n\nmain() -> Str =\n greet(\"World\")\n";
|
|
117
|
+
let source = parse(src);
|
|
118
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
119
|
+
assert_eq!(run_main_str(&bytes), "Hello, World!");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#[test]
|
|
123
|
+
fn string_interpolation_of_a_bool_runs_correctly() {
|
|
124
|
+
let src = "main() -> Str =\n b = True\n \"is {b}\"\n";
|
|
125
|
+
let source = parse(src);
|
|
126
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
127
|
+
assert_eq!(run_main_str(&bytes), "is True");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#[test]
|
|
131
|
+
fn string_interpolation_of_a_float_is_a_clear_error() {
|
|
132
|
+
// Float-to-decimal-string formatting is a substantial separate undertaking
|
|
133
|
+
// (correct rounding needs something like Grisu/Ryu); scoped out for now with
|
|
134
|
+
// an explicit error rather than emitting an incorrect conversion.
|
|
135
|
+
let src = "main() -> Str =\n x = 1.5\n \"{x}\"\n";
|
|
136
|
+
let source = parse(src);
|
|
86
|
-
let err = compile_source(&source).expect_err("interpolation should not silently compile");
|
|
137
|
+
let err = compile_source(&source).expect_err("float interpolation should not silently compile");
|
|
87
|
-
assert!(err.contains("
|
|
138
|
+
assert!(err.contains("Float"), "got: {}", err);
|
|
88
139
|
}
|
|
89
140
|
|
|
90
141
|
#[test]
|
|
@@ -161,7 +212,7 @@ fn match_string_pattern_is_a_clear_error() {
|
|
|
161
212
|
}
|
|
162
213
|
|
|
163
214
|
#[test]
|
|
164
|
-
fn
|
|
215
|
+
fn nested_constructor_pattern_matches_and_binds_runs_correctly() {
|
|
165
216
|
let src = "\
|
|
166
217
|
enum Option =
|
|
167
218
|
| Some(Int)
|
|
@@ -175,19 +226,101 @@ f(n: Nested) -> Int =
|
|
|
175
226
|
match n
|
|
176
227
|
Wrap(Some(v)) =>
|
|
177
228
|
return v
|
|
229
|
+
Wrap(None) =>
|
|
230
|
+
return -1
|
|
178
231
|
Empty =>
|
|
179
232
|
return 0
|
|
180
233
|
|
|
181
234
|
main() -> Int =
|
|
182
|
-
f(
|
|
235
|
+
f(Wrap(Some(5)))
|
|
183
236
|
";
|
|
184
237
|
let source = parse(src);
|
|
185
|
-
let
|
|
238
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
239
|
+
assert_eq!(run_main(&bytes), 5);
|
|
240
|
+
}
|
|
241
|
+
|
|
186
|
-
|
|
242
|
+
#[test]
|
|
243
|
+
fn nested_constructor_pattern_mismatch_falls_through_to_next_case_runs_correctly() {
|
|
187
|
-
|
|
244
|
+
// `Wrap(Some(v))` shouldn't match a runtime `Wrap(None)` — codegen must fall
|
|
245
|
+
// through to the next *top-level* case, not just fail to bind `v`.
|
|
188
|
-
|
|
246
|
+
let src = "\
|
|
247
|
+
enum Option =
|
|
248
|
+
| Some(Int)
|
|
249
|
+
| None
|
|
250
|
+
|
|
251
|
+
enum Nested =
|
|
252
|
+
| Wrap(Option)
|
|
253
|
+
| Empty
|
|
254
|
+
|
|
255
|
+
f(n: Nested) -> Int =
|
|
256
|
+
match n
|
|
257
|
+
Wrap(Some(v)) =>
|
|
189
|
-
|
|
258
|
+
return v
|
|
259
|
+
Wrap(None) =>
|
|
260
|
+
return -1
|
|
261
|
+
Empty =>
|
|
262
|
+
return 0
|
|
263
|
+
|
|
264
|
+
main() -> Int =
|
|
265
|
+
f(Wrap(None))
|
|
190
|
-
|
|
266
|
+
";
|
|
267
|
+
let source = parse(src);
|
|
268
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
269
|
+
assert_eq!(run_main(&bytes), -1);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#[test]
|
|
273
|
+
fn nested_constructor_pattern_against_a_specialized_generic_enum_runs_correctly() {
|
|
274
|
+
// Exercises monomorphize.rs's recursive mangling: the outer `Full(...)` pattern
|
|
275
|
+
// matches against `Box`'s own specialization, but the *inner* `Some(v)`/`None`
|
|
276
|
+
// sub-pattern matches against `Box`'s generic field type (`Option`, itself
|
|
277
|
+
// specialized to `Option$Int`) — each level needs its own mangling table, not
|
|
278
|
+
// just the outermost one.
|
|
279
|
+
let src = "\
|
|
280
|
+
enum Option =
|
|
281
|
+
| Some(a)
|
|
282
|
+
| None
|
|
283
|
+
|
|
284
|
+
enum Box =
|
|
285
|
+
| Full(a)
|
|
286
|
+
| Empty
|
|
287
|
+
|
|
288
|
+
unwrap(b: Box) -> Int =
|
|
289
|
+
match b
|
|
290
|
+
Full(Some(v)) => v
|
|
291
|
+
Full(None) => -1
|
|
292
|
+
Empty => 0
|
|
293
|
+
|
|
294
|
+
main() -> Int =
|
|
295
|
+
unwrap(Full(Some(7)))
|
|
296
|
+
";
|
|
297
|
+
let source = parse(src);
|
|
298
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
299
|
+
assert_eq!(run_main(&bytes), 7);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#[test]
|
|
303
|
+
fn doubly_nested_constructor_pattern_runs_correctly() {
|
|
304
|
+
// `Some(Some(v))` — two levels of nesting, proving the recursion isn't just
|
|
305
|
+
// one level deep.
|
|
306
|
+
let src = "\
|
|
307
|
+
enum Option =
|
|
308
|
+
| Some(Option)
|
|
309
|
+
| None
|
|
310
|
+
|
|
311
|
+
unwrapTwice(o: Option) -> Int =
|
|
312
|
+
match o
|
|
313
|
+
Some(Some(None)) => 1
|
|
314
|
+
Some(None) => 2
|
|
315
|
+
None => 3
|
|
316
|
+
_ => 0
|
|
317
|
+
|
|
318
|
+
main() -> Int =
|
|
319
|
+
unwrapTwice(Some(Some(None)))
|
|
320
|
+
";
|
|
321
|
+
let source = parse(src);
|
|
322
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
323
|
+
assert_eq!(run_main(&bytes), 1);
|
|
191
324
|
}
|
|
192
325
|
|
|
193
326
|
/// Runs `main`'s wasm bytes and returns its i64 result. wasm's own validator
|
|
@@ -204,6 +337,37 @@ fn run_main(bytes: &[u8]) -> i64 {
|
|
|
204
337
|
main.call(&mut store, ()).expect("main should not trap")
|
|
205
338
|
}
|
|
206
339
|
|
|
340
|
+
fn run_main_f64(bytes: &[u8]) -> f64 {
|
|
341
|
+
let engine = wasmtime::Engine::default();
|
|
342
|
+
let module = wasmtime::Module::new(&engine, bytes).expect("module should be loadable");
|
|
343
|
+
let mut store = wasmtime::Store::new(&engine, ());
|
|
344
|
+
let instance = wasmtime::Instance::new(&mut store, &module, &[]).expect("module should instantiate");
|
|
345
|
+
let main = instance
|
|
346
|
+
.get_typed_func::<(), f64>(&mut store, "main")
|
|
347
|
+
.expect("main should have signature () -> f64");
|
|
348
|
+
main.call(&mut store, ()).expect("main should not trap")
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/// Runs a `() -> Str`-returning `main`, reading the length-prefixed
|
|
352
|
+
/// `[len: u32][utf8 bytes]` string back out of the module's exported memory.
|
|
353
|
+
fn run_main_str(bytes: &[u8]) -> String {
|
|
354
|
+
let engine = wasmtime::Engine::default();
|
|
355
|
+
let module = wasmtime::Module::new(&engine, bytes).expect("module should be loadable");
|
|
356
|
+
let mut store = wasmtime::Store::new(&engine, ());
|
|
357
|
+
let instance = wasmtime::Instance::new(&mut store, &module, &[]).expect("module should instantiate");
|
|
358
|
+
let main = instance
|
|
359
|
+
.get_typed_func::<(), i32>(&mut store, "main")
|
|
360
|
+
.expect("main should have signature () -> i32");
|
|
361
|
+
let ptr = main.call(&mut store, ()).expect("main should not trap") as u32 as usize;
|
|
362
|
+
let memory = instance.get_memory(&mut store, "memory").expect("module should export memory");
|
|
363
|
+
let mut len_bytes = [0u8; 4];
|
|
364
|
+
memory.read(&store, ptr, &mut len_bytes).expect("string length read should be in bounds");
|
|
365
|
+
let len = u32::from_le_bytes(len_bytes) as usize;
|
|
366
|
+
let mut str_bytes = vec![0u8; len];
|
|
367
|
+
memory.read(&store, ptr + 4, &mut str_bytes).expect("string bytes read should be in bounds");
|
|
368
|
+
String::from_utf8(str_bytes).expect("string bytes should be valid utf8")
|
|
369
|
+
}
|
|
370
|
+
|
|
207
371
|
#[test]
|
|
208
372
|
fn factorial_runs_correctly() {
|
|
209
373
|
let src = "\
|
|
@@ -904,11 +1068,6 @@ fn wasm_module_with_a_table_element_validates_and_call_indirect_works() {
|
|
|
904
1068
|
|
|
905
1069
|
#[test]
|
|
906
1070
|
fn non_capturing_closure_passed_and_called_runs_correctly() {
|
|
907
|
-
// The brief's plan scopes closures as "passable as an ordinary call argument
|
|
908
|
-
// (`each(|v| ...)`)"; the inline single-expression body is that supported form.
|
|
909
|
-
// (A multi-line indented closure body immediately followed by `)` on the same line
|
|
910
|
-
// needs the external scanner to emit a dedent at a closing bracket, which is a
|
|
911
|
-
// parser/scanner concern outside this codegen task — see task-5-report.md.)
|
|
912
1071
|
let src = "\
|
|
913
1072
|
each(cb: fn(Int) -> Int) -> Int =
|
|
914
1073
|
cb(5)
|
|
@@ -921,6 +1080,25 @@ main() -> Int =
|
|
|
921
1080
|
assert_eq!(run_main(&bytes), 5);
|
|
922
1081
|
}
|
|
923
1082
|
|
|
1083
|
+
#[test]
|
|
1084
|
+
fn multi_line_closure_call_argument_with_closing_paren_on_last_statement_line_runs_correctly() {
|
|
1085
|
+
// Was a documented gap: the external scanner never emitted a dedent for a
|
|
1086
|
+
// multi-line closure body immediately followed by `)` on the same line as the
|
|
1087
|
+
// body's last statement, so this shape didn't parse at all before.
|
|
1088
|
+
let src = "\
|
|
1089
|
+
each(cb: fn(Int) -> Int) -> Int =
|
|
1090
|
+
cb(5)
|
|
1091
|
+
|
|
1092
|
+
main() -> Int =
|
|
1093
|
+
each(|v|
|
|
1094
|
+
x = v + 1
|
|
1095
|
+
x * 2)
|
|
1096
|
+
";
|
|
1097
|
+
let source = parse(src);
|
|
1098
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
1099
|
+
assert_eq!(run_main(&bytes), 12);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
924
1102
|
#[test]
|
|
925
1103
|
fn capturing_closure_snapshots_value_at_creation_time_runs_correctly() {
|
|
926
1104
|
let src = "\
|
|
@@ -945,6 +1123,54 @@ main() -> Int =
|
|
|
945
1123
|
assert_eq!(run_main(&bytes), 10);
|
|
946
1124
|
}
|
|
947
1125
|
|
|
1126
|
+
#[test]
|
|
1127
|
+
fn closure_assigned_then_called_at_float_type_runs_correctly() {
|
|
1128
|
+
// Was a documented gap: a closure created via assignment (not passed directly as
|
|
1129
|
+
// a call argument) and later called at a concrete non-Int type could hit a wasm
|
|
1130
|
+
// runtime trap — the checker's own closure inference gives every param a fresh
|
|
1131
|
+
// TVar and never unifies it against how it's used in the body, so a genuinely
|
|
1132
|
+
// Float param silently defaulted to Int, producing a `call_indirect` signature
|
|
1133
|
+
// mismatch between the compiled closure body and its call site.
|
|
1134
|
+
let src = "\
|
|
1135
|
+
eachF(cb: fn(Float) -> Float) -> Float =
|
|
1136
|
+
cb(0.0)
|
|
1137
|
+
|
|
1138
|
+
useClosure() -> Float =
|
|
1139
|
+
offset = 2.5
|
|
1140
|
+
cb = |v|
|
|
1141
|
+
offset + v
|
|
1142
|
+
eachF(cb)
|
|
1143
|
+
|
|
1144
|
+
main() -> Float =
|
|
1145
|
+
useClosure()
|
|
1146
|
+
";
|
|
1147
|
+
let source = parse(src);
|
|
1148
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
1149
|
+
assert_eq!(run_main_f64(&bytes), 2.5);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
#[test]
|
|
1153
|
+
fn closure_assigned_then_called_at_class_type_runs_correctly() {
|
|
1154
|
+
let src = "\
|
|
1155
|
+
type Cat =
|
|
1156
|
+
age: Int
|
|
1157
|
+
|
|
1158
|
+
eachCat(cb: fn(Cat) -> Int) -> Int =
|
|
1159
|
+
cb(Cat(age: 7))
|
|
1160
|
+
|
|
1161
|
+
useClosure() -> Int =
|
|
1162
|
+
cb = |c|
|
|
1163
|
+
c.age
|
|
1164
|
+
eachCat(cb)
|
|
1165
|
+
|
|
1166
|
+
main() -> Int =
|
|
1167
|
+
useClosure()
|
|
1168
|
+
";
|
|
1169
|
+
let source = parse(src);
|
|
1170
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
1171
|
+
assert_eq!(run_main(&bytes), 7);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
948
1174
|
#[test]
|
|
949
1175
|
fn closure_passed_through_already_generic_higher_order_function_runs_correctly() {
|
|
950
1176
|
let src = "\
|
|
@@ -961,3 +1187,185 @@ main() -> Int =
|
|
|
961
1187
|
let bytes = compile_source(&source).expect("compile failed");
|
|
962
1188
|
assert_eq!(run_main(&bytes), 14);
|
|
963
1189
|
}
|
|
1190
|
+
|
|
1191
|
+
#[test]
|
|
1192
|
+
fn nested_closure_literal_runs_correctly() {
|
|
1193
|
+
// Was a documented gap: a closure literal nested inside another closure's body
|
|
1194
|
+
// produced a clear compile error (the discovery pre-pass never recursed into a
|
|
1195
|
+
// closure's own body to find closures nested inside it). Here `inner` (nested
|
|
1196
|
+
// inside `outer`'s body) needs `offset` — a name from `useNested`'s scope, two
|
|
1197
|
+
// levels up from `inner` itself, and not referenced by `outer` directly — which
|
|
1198
|
+
// exercises the multi-level capture chain: `outer` must itself capture `offset`
|
|
1199
|
+
// purely because `inner` needs it, not because `outer` uses it.
|
|
1200
|
+
let src = "\
|
|
1201
|
+
each(cb: fn(Int) -> Int) -> Int =
|
|
1202
|
+
cb(5)
|
|
1203
|
+
|
|
1204
|
+
useNested() -> Int =
|
|
1205
|
+
offset = 100
|
|
1206
|
+
outer = |v|
|
|
1207
|
+
inner = |w|
|
|
1208
|
+
w + offset
|
|
1209
|
+
inner(v)
|
|
1210
|
+
each(outer)
|
|
1211
|
+
|
|
1212
|
+
main() -> Int =
|
|
1213
|
+
useNested()
|
|
1214
|
+
";
|
|
1215
|
+
let source = parse(src);
|
|
1216
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
1217
|
+
assert_eq!(run_main(&bytes), 105);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
#[test]
|
|
1221
|
+
fn nested_closure_literal_passed_directly_as_call_argument_runs_correctly() {
|
|
1222
|
+
let src = "\
|
|
1223
|
+
each(cb: fn(Int) -> Int) -> Int =
|
|
1224
|
+
cb(5)
|
|
1225
|
+
|
|
1226
|
+
main() -> Int =
|
|
1227
|
+
each(|v|
|
|
1228
|
+
each(|w| w + v))
|
|
1229
|
+
";
|
|
1230
|
+
let source = parse(src);
|
|
1231
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
1232
|
+
assert_eq!(run_main(&bytes), 10);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
#[test]
|
|
1236
|
+
fn named_function_used_as_a_closure_typed_value_runs_correctly() {
|
|
1237
|
+
// A plain top-level named function (not a `|params| body` closure literal) used
|
|
1238
|
+
// wherever a `fn(...)`-typed value is expected — no closure literal involved at
|
|
1239
|
+
// the call site at all.
|
|
1240
|
+
let src = "\
|
|
1241
|
+
double(x: Int) -> Int =
|
|
1242
|
+
x * 2
|
|
1243
|
+
|
|
1244
|
+
each(cb: fn(Int) -> Int) -> Int =
|
|
1245
|
+
cb(21)
|
|
1246
|
+
|
|
1247
|
+
main() -> Int =
|
|
1248
|
+
each(double)
|
|
1249
|
+
";
|
|
1250
|
+
let source = parse(src);
|
|
1251
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
1252
|
+
assert_eq!(run_main(&bytes), 42);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
#[test]
|
|
1256
|
+
fn named_function_used_as_a_value_assigned_then_called_runs_correctly() {
|
|
1257
|
+
let src = "\
|
|
1258
|
+
double(x: Int) -> Int =
|
|
1259
|
+
x * 2
|
|
1260
|
+
|
|
1261
|
+
main() -> Int =
|
|
1262
|
+
f = double
|
|
1263
|
+
f(21)
|
|
1264
|
+
";
|
|
1265
|
+
let source = parse(src);
|
|
1266
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
1267
|
+
assert_eq!(run_main(&bytes), 42);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
#[test]
|
|
1271
|
+
fn named_function_used_as_a_value_alongside_a_closure_at_the_same_call_type_runs_correctly() {
|
|
1272
|
+
// Proves the trampoline shares the same call_indirect type as an ordinary
|
|
1273
|
+
// closure of the same signature (both must resolve to the same wasm function
|
|
1274
|
+
// type, since both flow through the exact same `cb(...)` call site).
|
|
1275
|
+
let src = "\
|
|
1276
|
+
double(x: Int) -> Int =
|
|
1277
|
+
x * 2
|
|
1278
|
+
|
|
1279
|
+
each(cb: fn(Int) -> Int) -> Int =
|
|
1280
|
+
cb(10)
|
|
1281
|
+
|
|
1282
|
+
main() -> Int =
|
|
1283
|
+
each(double) + each(|v| v + 1)
|
|
1284
|
+
";
|
|
1285
|
+
let source = parse(src);
|
|
1286
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
1287
|
+
assert_eq!(run_main(&bytes), 31);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
#[test]
|
|
1291
|
+
fn multi_subject_match_with_enum_tags_runs_correctly() {
|
|
1292
|
+
// Mirrors libs/std/bool.plum's `and`/`or`: `match self, o` against two Bool
|
|
1293
|
+
// subjects, each case naming a tag pattern per position.
|
|
1294
|
+
let src = "\
|
|
1295
|
+
and(a: Bool, b: Bool) -> Bool =
|
|
1296
|
+
match a, b
|
|
1297
|
+
True, True => True
|
|
1298
|
+
True, False => False
|
|
1299
|
+
False, True => False
|
|
1300
|
+
False, False => False
|
|
1301
|
+
|
|
1302
|
+
main() -> Int =
|
|
1303
|
+
x = and(True, True)
|
|
1304
|
+
y = and(True, False)
|
|
1305
|
+
match x, y
|
|
1306
|
+
True, False => 1
|
|
1307
|
+
_, _ => 0
|
|
1308
|
+
";
|
|
1309
|
+
let source = parse(src);
|
|
1310
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
1311
|
+
assert_eq!(run_main(&bytes), 1);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
#[test]
|
|
1315
|
+
fn multi_subject_match_falls_through_to_next_case_when_only_first_position_matches() {
|
|
1316
|
+
// The first case's position-0 pattern (`1`) matches, but position-1 (`1`)
|
|
1317
|
+
// doesn't (b is 2) — codegen must fall through to the *next case* (trying its
|
|
1318
|
+
// own position 0 again), not just "move on" within the first case.
|
|
1319
|
+
let src = "\
|
|
1320
|
+
classify(a: Int, b: Int) -> Int =
|
|
1321
|
+
match a, b
|
|
1322
|
+
1, 1 => 100
|
|
1323
|
+
1, 2 => 200
|
|
1324
|
+
_, _ => 0
|
|
1325
|
+
|
|
1326
|
+
main() -> Int =
|
|
1327
|
+
classify(1, 2)
|
|
1328
|
+
";
|
|
1329
|
+
let source = parse(src);
|
|
1330
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
1331
|
+
assert_eq!(run_main(&bytes), 200);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
#[test]
|
|
1335
|
+
fn multi_subject_match_with_binding_and_wildcard_runs_correctly() {
|
|
1336
|
+
let src = "\
|
|
1337
|
+
combine(a: Int, b: Int) -> Int =
|
|
1338
|
+
match a, b
|
|
1339
|
+
0, y => y
|
|
1340
|
+
x, 0 => x
|
|
1341
|
+
x, y => x + y
|
|
1342
|
+
|
|
1343
|
+
main() -> Int =
|
|
1344
|
+
combine(3, 4)
|
|
1345
|
+
";
|
|
1346
|
+
let source = parse(src);
|
|
1347
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
1348
|
+
assert_eq!(run_main(&bytes), 7);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
#[test]
|
|
1352
|
+
fn multi_subject_match_with_generic_enum_variant_runs_correctly() {
|
|
1353
|
+
// Exercises the monomorphize.rs fix: each subject's own generic-enum
|
|
1354
|
+
// specialization (`Some$Int`) must be mangled independently per position.
|
|
1355
|
+
let src = "\
|
|
1356
|
+
enum Option =
|
|
1357
|
+
| Some(a)
|
|
1358
|
+
| None
|
|
1359
|
+
|
|
1360
|
+
both(a: Option, b: Option) -> Int =
|
|
1361
|
+
match a, b
|
|
1362
|
+
Some(x), Some(y) => x + y
|
|
1363
|
+
_, _ => 0
|
|
1364
|
+
|
|
1365
|
+
main() -> Int =
|
|
1366
|
+
both(Some(3), Some(4))
|
|
1367
|
+
";
|
|
1368
|
+
let source = parse(src);
|
|
1369
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
1370
|
+
assert_eq!(run_main(&bytes), 7);
|
|
1371
|
+
}
|
plum-wasm-codegen/tests/examples_test.rs
CHANGED
|
@@ -100,10 +100,11 @@ fn closures_example_compiles_and_runs_correctly() {
|
|
|
100
100
|
assert_eq!(main.call(&mut store, ()).expect("main should not trap"), 120);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
/// strings.plum
|
|
103
|
+
/// strings.plum exercises string interpolation (plain literals, `Str`/`Int`
|
|
104
|
+
/// interpolation, and escape sequences) — it has no `main` to execute, so (matching
|
|
105
|
+
/// `basics_compiles`/`control_flow_compiles`/etc. above for other main-less examples)
|
|
106
|
+
/// this only checks that it compiles to a valid module.
|
|
104
107
|
#[test]
|
|
105
|
-
fn
|
|
108
|
+
fn strings_example_compiles() {
|
|
106
|
-
|
|
109
|
+
assert_compiles("strings.plum");
|
|
107
|
-
let err = compile_source(&source).expect_err("string interpolation is not yet supported");
|
|
108
|
-
assert!(err.contains("interpolation"), "got: {}", err);
|
|
109
110
|
}
|
tooling/tree-sitter-plum/src/scanner.c
CHANGED
|
@@ -263,7 +263,33 @@ bool tree_sitter_plum_external_scanner_scan(void *payload, TSLexer *lexer, const
|
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
// A closure literal's indented body, used directly as a call argument, can be
|
|
267
|
+
// immediately followed by a closing bracket with no intervening newline at all
|
|
268
|
+
// (`each(|v|\n v)`), since the body's last statement and the bracket that ends
|
|
269
|
+
// the enclosing call are on the same line. The loop above only ever sets
|
|
270
|
+
// `found_end_of_line` by actually consuming a `\n`, so that shape reaches here
|
|
271
|
+
// with `found_end_of_line == false` and no dedent is ever considered — even
|
|
272
|
+
// though the grammar, mid-parse, is already asking for one (`valid_symbols[DEDENT]`
|
|
273
|
+
// is true here exactly when a `body` node needs to close before the bracket can be
|
|
274
|
+
// shifted). Treat that specific situation as equivalent to "found a blank new
|
|
275
|
+
// line" for dedent purposes only: there's no partial line indentation to measure
|
|
276
|
+
// (we're mid-expression, not at a line start), so it behaves as `indent_length == 0`,
|
|
277
|
+
// which is `< current_indent_length` for any real indented block. Never treat it as
|
|
278
|
+
// a NEWLINE — the grammar isn't expecting one at a raw closing bracket.
|
|
279
|
+
//
|
|
280
|
+
// (A comma continuing the argument list on the same line, e.g.
|
|
281
|
+
// `each(|v|\n v, extra)`, is the same underlying shape but isn't handled here:
|
|
282
|
+
// at that exact position the parser's automaton doesn't have `valid_symbols[DEDENT]`
|
|
283
|
+
// true, so there's no signal to act on without guessing. Out of scope for now —
|
|
284
|
+
// put the closure last, or give it its own line, to work around it.)
|
|
285
|
+
bool at_closing_bracket_without_newline =
|
|
286
|
+
!found_end_of_line && within_brackets && valid_symbols[DEDENT] &&
|
|
287
|
+
(lexer->lookahead == ')' || lexer->lookahead == ']' || lexer->lookahead == '}');
|
|
266
|
-
if (
|
|
288
|
+
if (at_closing_bracket_without_newline) {
|
|
289
|
+
indent_length = 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (found_end_of_line || at_closing_bracket_without_newline) {
|
|
267
293
|
if (scanner->indents.size > 0) {
|
|
268
294
|
uint16_t current_indent_length = *array_back(&scanner->indents);
|
|
269
295
|
|
|
@@ -291,7 +317,7 @@ bool tree_sitter_plum_external_scanner_scan(void *payload, TSLexer *lexer, const
|
|
|
291
317
|
}
|
|
292
318
|
}
|
|
293
319
|
|
|
294
|
-
if (valid_symbols[NEWLINE] && !error_recovery_mode) {
|
|
320
|
+
if (found_end_of_line && valid_symbols[NEWLINE] && !error_recovery_mode) {
|
|
295
321
|
lexer->result_symbol = NEWLINE;
|
|
296
322
|
return true;
|
|
297
323
|
}
|
tooling/tree-sitter-plum/test/corpus/function.txt
CHANGED
|
@@ -405,3 +405,32 @@ main() -> Int =
|
|
|
405
405
|
body: (expression
|
|
406
406
|
(primary_expression
|
|
407
407
|
(var_identifier))))))))))))
|
|
408
|
+
|
|
409
|
+
================================================================================
|
|
410
|
+
function - multi-line closure literal call argument, closing paren on same line as body
|
|
411
|
+
================================================================================
|
|
412
|
+
|
|
413
|
+
main() -> Int =
|
|
414
|
+
each(|v|
|
|
415
|
+
v)
|
|
416
|
+
|
|
417
|
+
--------------------------------------------------------------------------------
|
|
418
|
+
|
|
419
|
+
(source
|
|
420
|
+
(fn
|
|
421
|
+
name: (fn_identifier)
|
|
422
|
+
returns: (return_type
|
|
423
|
+
(type_identifier))
|
|
424
|
+
body: (body
|
|
425
|
+
(expression
|
|
426
|
+
(primary_expression
|
|
427
|
+
(fn_call
|
|
428
|
+
function: (var_identifier)
|
|
429
|
+
arguments: (fn_argument_list
|
|
430
|
+
(expression
|
|
431
|
+
(closure
|
|
432
|
+
parameters: (var_identifier)
|
|
433
|
+
body: (body
|
|
434
|
+
(expression
|
|
435
|
+
(primary_expression
|
|
436
|
+
(var_identifier)))))))))))))
|