plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
651acd8
— Peter John
2026-11-03T19:17:08+05:30
Closure specialisation; allocation-free actions; direct scene traversal
- README.md +13 -0
- docs/plans/2026-11-03-zero-cost-closures-scene-perf.benchmark.md +22 -0
- docs/plans/2026-11-03-zero-cost-closures-scene-perf.md +121 -0
- docs/plans/2026-11-03-zero-cost-closures-scene-perf.progress.md +31 -0
- docs/plans/2026-11-03-zero-cost-closures-scene-perf.report.json +246 -0
- docs/plans/2026-11-03-zero-cost-closures-scene-perf.report.md +43 -0
- docs/plans/2026-11-03-zero-cost-closures-scene-perf.review.md +90 -0
- plum-checker/src/lib.rs +71 -0
- plum-core/src/closure_escape.rs +256 -0
- plum-core/src/closure_spec.rs +1373 -0
- plum-core/src/lib.rs +2 -0
- plum-core/tests/closure_escape_test.rs +134 -0
- plum-core/tests/closure_spec_test.rs +408 -0
- plum-std/game/Actions.plum +254 -84
- plum-std/game/PureScene.plum +5 -3
- plum-std/game/SceneTree.plum +99 -38
README.md
CHANGED
|
@@ -962,6 +962,19 @@ fun main() -> Int =
|
|
|
962
962
|
|
|
963
963
|
Calling the result of a call directly (`adder(1)(2)`) is not supported yet.
|
|
964
964
|
|
|
965
|
+
### Automatic specialisation
|
|
966
|
+
|
|
967
|
+
A closure literal or a function name passed directly to a `fn` parameter that the callee only calls (never stores, returns, captures or passes on) is specialised automatically: the compiler emits a copy of the callee with the closure body inlined, so there is no closure allocation and no indirect call. Captured variables become extra parameters of the copy. Escaping closures behave as before, and a callee gets at most 8 specialised copies before further call sites fall back to ordinary closure calls.
|
|
968
|
+
|
|
969
|
+
```plum
|
|
970
|
+
fun twice(f: fn(Int) -> Int, x: Int) -> Int =
|
|
971
|
+
f(f(x))
|
|
972
|
+
|
|
973
|
+
test "specialised"
|
|
974
|
+
k := 3
|
|
975
|
+
assert twice(|v| v + k, 1) == 7
|
|
976
|
+
```
|
|
977
|
+
|
|
965
978
|
### Tail calls
|
|
966
979
|
|
|
967
980
|
A call in tail position (the last thing a function does: the value of its body, of an `if`/`match` arm in tail position, or of a `return`) compiles to a real tail call, for direct calls, methods and closure calls, so tail-recursive loops run in constant stack and a 200000-step recursion is fine:
|
docs/plans/2026-11-03-zero-cost-closures-scene-perf.benchmark.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Benchmark: zero-cost closures and pure scene graph
|
|
2
|
+
|
|
3
|
+
## Scene
|
|
4
|
+
|
|
5
|
+
Headed Chromium WebGPU, M3 Pro, `--uncapped --measure 10`, same session (ms per frame).
|
|
6
|
+
|
|
7
|
+
| Variant | avg | p95 | update | draw |
|
|
8
|
+
|---|---|---|---|---|
|
|
9
|
+
| Mutable Scene | 1.830 / 1.843 | 2.0 / 2.1 | 0.048 | 1.31 |
|
|
10
|
+
| Pure scene, after this plan | 1.777 / 1.778 | 2.0 | 0.126 | 1.24 |
|
|
11
|
+
| Probe without action stepping | 1.717 | - | 0.068 | - |
|
|
12
|
+
| Pure scene, before this plan (earlier session) | 1.895 | 2.3 | 0.320 | - |
|
|
13
|
+
|
|
14
|
+
Bar (avg within +10%, p95 within +15% of mutable): MET (avg -3%, p95 equal).
|
|
15
|
+
|
|
16
|
+
What changed: in-place allocation-free Actions on own records; traversal matches Group directly instead of calling `kids()` per node.
|
|
17
|
+
|
|
18
|
+
## Closures
|
|
19
|
+
|
|
20
|
+
Specialisation works: no `call_ref` in specialised copies, captures become params, recursion stays in the copy. But the 1M List map/filter/fold timing barely moved (~632 -> ~625 ms), because the cost was never the closure calls. Wasmtime's copying GC copies the whole live set on each heap growth (sample: `CopyingHeap::forward`/`scan_field`). With a presized GC heap: build 1M 4.8 ms, map 8.0 ms. Slice for-chain 19 ms. In the browser V8's GC applies, so games are unaffected.
|
|
21
|
+
|
|
22
|
+
Open decision: host GC config (`gc_heap_initial_size`).
|
docs/plans/2026-11-03-zero-cost-closures-scene-perf.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Zero-cost closures and pure-scene-graph speed
|
|
2
|
+
|
|
3
|
+
**Review plan:** docs/plans/2026-11-03-zero-cost-closures-scene-perf.review.md
|
|
4
|
+
**Goal:** automatic specialisation of closures passed directly to non-escaping fn params (callee copy with the closure body inlined); profile and speed up the pure scene graph's update path (allocation-free actions, direct traversal); benchmark.
|
|
5
|
+
**Architecture:** an AST pass in plum-core (like trmc) run in plum-checker typedSource on a re-checked clone: find direct closure / fn-name args to non-escaping fn-typed params, emit a specialised copy of the callee per (callee, closure) with calls to the param replaced by the closure body (captures become extra params), cap 8 copies per callee. Scene work is in std/game Plum.
|
|
6
|
+
**Tech stack:** Rust (plum-core, plum-checker), Plum std/game, plum-game web-test benchmarks.
|
|
7
|
+
**Spec:** docs/plans/2026-11-03-zero-cost-closures-scene-perf.review.md
|
|
8
|
+
**Detail:** contracts
|
|
9
|
+
**Target-min:** 50
|
|
10
|
+
|
|
11
|
+
## Global constraints
|
|
12
|
+
- Work only in /Users/pyrossh/Code/plum-pure-scene-values (branch pure-scene-values, base e04834f). Never touch /Users/pyrossh/Code/plum.
|
|
13
|
+
- Plum: `touch plum-cli/src/main.rs && cargo build --quiet -p plum-cli && ./target/debug/plum test <files>`; sweep `./target/debug/plum test plum-std plum-examples` (629 green). Rust: `cargo test --manifest-path <crate>/Cargo.toml`. Known failing: plum-checker generic_funstar_wired.
|
|
14
|
+
- User decisions: specialisation is automatic (no `inline` keyword); no for-chains on List; Array unchanged; cap 8 specialised copies per callee, then fall back to call_ref.
|
|
15
|
+
- Escape rule (conservative): a fn-typed param escapes if the callee stores it (field, list, local that escapes), returns it, captures it in another closure, or passes it to a param that escapes. Unknown => escapes.
|
|
16
|
+
- Never run `plum format` on real files. Headed benchmarks are run by the coordinator only.
|
|
17
|
+
|
|
18
|
+
```mermaid
|
|
19
|
+
flowchart LR
|
|
20
|
+
E["escape analysis"] --> S["specialise + inline"] --> W["typedSource wiring"]
|
|
21
|
+
P["scene profile probes"] --> A["in-place Actions"] --> T["direct traversal"] --> B["benchmark + docs"]
|
|
22
|
+
S --> B
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
File structure: each task's Owns. Spec coverage: What you will get (T1-T6), What will be built (T1-T6; blocks-on-List dropped by user decision), How it fits together (diagram), Decisions you can change (constraints), Risks (R1 T1, R2 T2, R3 T3, R4 T4), Out of scope (not built), How we will know it works (T1-T6), Rough time and cost (schedule).
|
|
26
|
+
|
|
27
|
+
## Task graph
|
|
28
|
+
```mermaid
|
|
29
|
+
flowchart LR
|
|
30
|
+
T1[escape analysis]
|
|
31
|
+
T2[specialise closures]
|
|
32
|
+
T3[scene profile probes]
|
|
33
|
+
T4[in-place actions]
|
|
34
|
+
T5[direct traversal]
|
|
35
|
+
T6[benchmark and docs]
|
|
36
|
+
%% taskgraph
|
|
37
|
+
T1 --> T2
|
|
38
|
+
T4 --> T5
|
|
39
|
+
T2 --> T6
|
|
40
|
+
T5 --> T6
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Closures
|
|
44
|
+
|
|
45
|
+
### T1 escape analysis for fn-typed params · Complexity 4 · Est. context 45k
|
|
46
|
+
**Owns:** plum-core/src/closure_escape.rs, plum-core/src/lib.rs, plum-core/tests/closure_escape_test.rs
|
|
47
|
+
**Components:** 1
|
|
48
|
+
**LOC owned:** 160
|
|
49
|
+
**Tests:** fast
|
|
50
|
+
**Model:** sonnet
|
|
51
|
+
**Consumes:** none
|
|
52
|
+
**Produces:** closure_escape::nonEscapingFnParams(src: &ast::Source) -> HashMap<String, Vec<usize>>
|
|
53
|
+
e.g. `nonEscapingFnParams` on std List -> `{"List.map": [0], "List.filter": [0], "List.fold": [1], "mapCells": [1]}` (indices of fn-typed params, self excluded)
|
|
54
|
+
e.g. a fn storing its param `cb` in a record field -> not in the map
|
|
55
|
+
**Review Focus:** state R1: transitive (passing f to another fn's param that escapes => escapes; recursion with the same f is fine); methods keyed "Type.m"; generics; conservative on anything unknown.
|
|
56
|
+
**Test code:** closure_escape_test.rs: `direct_call_param_non_escaping`, `recursive_same_param_non_escaping` (mapCells), `stored_in_field_escapes`, `returned_escapes`, `captured_by_closure_escapes`, `passed_to_escaping_param_escapes`, `passed_to_non_escaping_param_ok`, `method_keys`, `std_list_combinators_non_escaping` (load std List).
|
|
57
|
+
|
|
58
|
+
### T2 specialise closure arguments · Complexity 6 · Est. context 65k
|
|
59
|
+
**Owns:** plum-core/src/closure_spec.rs, plum-core/tests/closure_spec_test.rs, plum-checker/src/lib.rs
|
|
60
|
+
**Components:** 1
|
|
61
|
+
**LOC owned:** 190
|
|
62
|
+
**Tests:** fast
|
|
63
|
+
**Model:** sonnet
|
|
64
|
+
**Consumes:** closure_escape::nonEscapingFnParams(src: &ast::Source) -> HashMap<String, Vec<usize>>
|
|
65
|
+
**Produces:** closure_spec::specialise(src: &mut ast::Source) -> Vec<String>
|
|
66
|
+
e.g. `specialise` on `xs.map(|x| x * 2)` with std List -> `["List.map$c0", "mapCells$c0"]` (names of emitted copies)
|
|
67
|
+
**Review Focus:** state R2/R3: a call site passing a closure literal or a fn name directly to a non-escaping param gets a copy of the callee where calls of that param become the closure body inlined (fresh locals per call, captures added as extra params in order), recursive calls inside the copy target the copy; dedup by (callee, closure source span / fn name); cap 8 copies per callee then leave the call alone; run in typedSource on a re-checked clone (like trmc, before trmc and markTailCalls so TRMC/tail calls apply to copies), keep the original program if the re-check fails.
|
|
68
|
+
**Test code:** closure_spec_test.rs (compile+run and wasmparser): `map_literal_no_call_ref` (specialised List.map has no call_ref), `capture_becomes_param` (closure capturing a local), `recursive_copy_calls_itself`, `fn_name_argument`, `escaping_closure_untouched`, `dedup_same_closure_twice`, `cap_eight_copies`, `results_equal_original` (several pipelines), `list_pipeline_million_timed` (map/filter/fold 1M on List; report ms; target within 3-5x of the Slice for-chain 19 ms).
|
|
69
|
+
|
|
70
|
+
## Pure scene graph
|
|
71
|
+
|
|
72
|
+
### T3 scene profile probes · Complexity 2 · Est. context 30k
|
|
73
|
+
**Owns:** plum-game/web-test/fixtures/probe_scene_no_actions.plum, plum-game/web-test/fixtures/probe_scene_no_dispatch.plum, plum-game/web-test/fixtures/probe_scene_keep_lists.plum
|
|
74
|
+
**Components:** 1
|
|
75
|
+
**LOC owned:** 120
|
|
76
|
+
**Tests:** fast
|
|
77
|
+
**Model:** sonnet
|
|
78
|
+
**Consumes:** none
|
|
79
|
+
**Produces:** none
|
|
80
|
+
**Review Focus:** numeric: each is a copy of stress_pure_scene.plum (std PureScene/SceneTree/SceneInput on own) with ONE cost removed: (a) no action stepping, (b) update without trait dispatch for act/kids (direct field traversal written in the fixture), (c) action lists not rebuilt (finished actions kept). Same actor count and draw path.
|
|
81
|
+
**Test code:** each fixture `plum test` passes and `plum compile` succeeds; draw output count equals stress_pure_scene (non-headed `--frames-wait 6 --json`).
|
|
82
|
+
|
|
83
|
+
### T4 in-place Actions · Complexity 4 · Est. context 45k
|
|
84
|
+
**Owns:** plum-std/game/Actions.plum, plum-std/game/SceneTree.plum
|
|
85
|
+
**Components:** 1
|
|
86
|
+
**LOC owned:** 160
|
|
87
|
+
**Tests:** fast
|
|
88
|
+
**Model:** sonnet
|
|
89
|
+
**Consumes:** none
|
|
90
|
+
**Produces:** none
|
|
91
|
+
**Review Focus:** state R4: stepAll/stepAction allocate nothing per frame for a running action: results as a struct (pose, spare, done), actions stepped in place on own records, finished actions dropped by compacting in place (Slice takeAt/putAt or a write index), no Stepped/SteppedAll record per actor; ownership checker accepts it with no checker change.
|
|
92
|
+
**Test code:** Actions.plum and SceneTree.plum tests keep names and asserted values (11 and 13); add `step_all_no_alloc_steady_state` (wasmparser or a counter: no struct.new in the steady-state step path for MoveBy/RotateBy).
|
|
93
|
+
|
|
94
|
+
### T5 direct traversal · Complexity 3 · Est. context 40k
|
|
95
|
+
**Owns:** plum-std/game/PureScene.plum, plum-std/game/SceneInput.plum, plum-examples/game_pure_scene.plum, plum-game/web-test/fixtures/stress_pure_scene.plum
|
|
96
|
+
**Components:** 2
|
|
97
|
+
**LOC owned:** 120
|
|
98
|
+
**Tests:** fast
|
|
99
|
+
**Model:** sonnet
|
|
100
|
+
**Consumes:** none
|
|
101
|
+
**Produces:** none
|
|
102
|
+
**Review Focus:** state: the update/draw traversal reaches children by a direct `Group` check and field read, not `kids() -> Slice[Actor]?` per node; behaviour identical; example and fixture follow any internal API change.
|
|
103
|
+
**Test code:** PureScene 5, SceneInput 9, game_pure_scene 21 keep names and values; `cargo test --manifest-path plum-game/Cargo.toml --test host_test pureScene`; `cargo test --manifest-path plum-game/web-test/Cargo.toml --test web_check pure_scene_renders_and_animates`.
|
|
104
|
+
|
|
105
|
+
## Wrap-up
|
|
106
|
+
|
|
107
|
+
### T6 benchmark and docs · Complexity 2 · Est. context 30k
|
|
108
|
+
**Owns:** README.md, docs/plans/2026-11-03-zero-cost-closures-scene-perf.benchmark.md
|
|
109
|
+
**Components:** 1
|
|
110
|
+
**LOC owned:** 60
|
|
111
|
+
**Tests:** fast
|
|
112
|
+
**Model:** sonnet
|
|
113
|
+
**Consumes:** none
|
|
114
|
+
**Produces:** none
|
|
115
|
+
**Review Focus:** numeric: closure table (List pipeline before 581 ms, after; Slice for-chain 19 ms) from scratch probes; scene table filled in by the coordinator's headed runs (leave the rows, coordinator fills numbers); README sentence on automatic closure specialisation and its limits (escaping closures, cap 8).
|
|
116
|
+
**Test code:** README example compiles (scratch); benchmark doc present.
|
|
117
|
+
|
|
118
|
+
## Review Focus
|
|
119
|
+
- compiler: deep, state. Risks: wrong escape classification; capture order; recursion in copies; code growth.
|
|
120
|
+
- std/game: state, numeric. Risks: behaviour drift in actions; in-place compaction order.
|
|
121
|
+
**Probe entry:** run=./target/debug/plum test
|
docs/plans/2026-11-03-zero-cost-closures-scene-perf.progress.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Zero-cost closures + scene perf run
|
|
2
|
+
- start e04834f
|
|
3
|
+
- Ruling: [scope] blocks/for-chains on List dropped - user: List keeps closure methods, made fast by specialisation - cost: closure pipelines still allocate per stage
|
|
4
|
+
- Ruling: skip T0 Wave 0 - no stub generation for Plum/Rust - cost: none
|
|
5
|
+
- unblocked: dispatched T1, T4+T3 batch at 18:40:44; waiting on fixes: T2 (SIZE) T6
|
|
6
|
+
- checkpoint T1 6a629e5ab5 escape analysis for fn-typed params
|
|
7
|
+
- T1 checkpointed 6a629e5 (nonEscapingFnParams, greatest fixpoint, name-resolved methods - conservative)
|
|
8
|
+
- unblocked: dispatched T2 at 18:44:02
|
|
9
|
+
- checkpoint T3 030cd64325 scene profile probes
|
|
10
|
+
- checkpoint T4 3998a4206f in-place Actions
|
|
11
|
+
- T3 checkpointed (3 probe fixtures; no_dispatch partial: no downcast), T4 checkpointed (in-place Actions, no struct.new in step path; step_does_not_change_input -> step_moves_the_action_in_place)
|
|
12
|
+
- unblocked: dispatched T5 at 18:47:02; headed benchmarks deferred until builds quiet
|
|
13
|
+
- checkpoint T5 dc68ad92c6 direct traversal
|
|
14
|
+
- T5 checkpointed (drawTree matches Group directly). Ruling: T5 extends to plum-std/game/SceneTree.plum (T4 done) to drop kids() on the update path - cost: none
|
|
15
|
+
- checkpoint T4 2aa86ac4b9 in-place Actions
|
|
16
|
+
- SceneTree kids() sites replaced by Group match (T4 file re-checkpointed via T5 agent)
|
|
17
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=163.5
|
|
18
|
+
- T2 DONE_WITH_CONCERNS: specialisation works (10 tests) but no gain: List.map ~470ms is TRMC mapCells, not closure calls (filter/fold ~30ms). trmc_test name regression -> T2 agent. F-T2-1 slow TRMC map -> fixer-opus
|
|
19
|
+
- checkpoint T2 6d7cb25999 specialise closure arguments
|
|
20
|
+
- T2 checkpointed (+trmc_test fix committed); F-T2-1 dispatched to fixer-opus at 19:04:02
|
|
21
|
+
- Scene benchmark (headed): mutable 1.830/1.843 p95 2.0/2.1 upd 0.048; pure 1.777/1.778 p95 2.0 upd 0.126 (was 0.32) -> BAR MET; no_actions probe 1.717
|
|
22
|
+
- F-T2-1 NOT_FIXED by design: slow map = wasmtime copying GC heap growth; host gc_heap_initial_size is a user decision (follow-up)
|
|
23
|
+
- Ruling: drop the 3 probe fixtures (pre-T4 diagnostics; no_dispatch no longer compiles); T6 deletes them - cost: none
|
|
24
|
+
- checkpoint T6 572b884d72 benchmark and docs
|
|
25
|
+
- Final review CHANGES_REQUESTED (test gaps: closure deep/cap, escape state, Actions golden values, compaction order) -> F-FR-2 fixer-sonnet-high
|
|
26
|
+
- tests full passed=80 failed=1 skipped=0 seconds=46.7
|
|
27
|
+
- checkpoint T1 b6a2f96274 escape analysis for fn-typed params
|
|
28
|
+
- checkpoint T2 a49cb785ad specialise closure arguments
|
|
29
|
+
- checkpoint T4 9730da76f3 in-place Actions
|
|
30
|
+
- tests full passed=80 failed=1 skipped=0 seconds=17.6
|
|
31
|
+
- F-FR-2 FIXED (6 probe tests, no defects); full suite: only known generic_funstar_wired. Final review findings addressed.
|
docs/plans/2026-11-03-zero-cost-closures-scene-perf.report.json
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
{
|
|
2
|
+
"projection_note": "projection: fixed forecast saved at lint 2026-11-03T18:40:15 (plan edited since)",
|
|
3
|
+
"headline": {
|
|
4
|
+
"wall_min": {
|
|
5
|
+
"projected": 18.647,
|
|
6
|
+
"actual": 30.202
|
|
7
|
+
},
|
|
8
|
+
"cost_usd_agents": {
|
|
9
|
+
"projected": 0.733869,
|
|
10
|
+
"actual": 3.975266
|
|
11
|
+
},
|
|
12
|
+
"task_min": {
|
|
13
|
+
"projected": 35.475,
|
|
14
|
+
"actual": 31.458
|
|
15
|
+
},
|
|
16
|
+
"agents": {
|
|
17
|
+
"projected": 6,
|
|
18
|
+
"actual": 15
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"feature": null,
|
|
22
|
+
"cost_split": {
|
|
23
|
+
"agents_usd": 3.080342,
|
|
24
|
+
"rework_usd": 0.894924,
|
|
25
|
+
"coordinator_usd": 50.066135,
|
|
26
|
+
"total_usd": 54.041401
|
|
27
|
+
},
|
|
28
|
+
"concurrency": {
|
|
29
|
+
"peak": 2,
|
|
30
|
+
"projected_peak": 3
|
|
31
|
+
},
|
|
32
|
+
"critical_path": {
|
|
33
|
+
"actual": [
|
|
34
|
+
"T1",
|
|
35
|
+
"T2",
|
|
36
|
+
"T6"
|
|
37
|
+
],
|
|
38
|
+
"projected": [
|
|
39
|
+
"T1",
|
|
40
|
+
"T2",
|
|
41
|
+
"T6"
|
|
42
|
+
],
|
|
43
|
+
"projected_min": 18.647
|
|
44
|
+
},
|
|
45
|
+
"model_mix": {
|
|
46
|
+
"sonnet": {
|
|
47
|
+
"tasks": 5,
|
|
48
|
+
"cost_usd": 3.9752660000000004
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"coordinator_cost_by_model": {
|
|
52
|
+
"opus": 50.066135
|
|
53
|
+
},
|
|
54
|
+
"quality": {
|
|
55
|
+
"tests": {
|
|
56
|
+
"passed": 80,
|
|
57
|
+
"failed": 1,
|
|
58
|
+
"skipped": 0,
|
|
59
|
+
"seconds": 17.6,
|
|
60
|
+
"mode": "full",
|
|
61
|
+
"failures": [
|
|
62
|
+
"-::generic_funstar_wired: failed"
|
|
63
|
+
],
|
|
64
|
+
"skipped_tests": []
|
|
65
|
+
},
|
|
66
|
+
"first_time_pass": 0,
|
|
67
|
+
"first_time_n": 0,
|
|
68
|
+
"rework_dispatches": 1,
|
|
69
|
+
"open_incidents": 2
|
|
70
|
+
},
|
|
71
|
+
"tasks": [
|
|
72
|
+
{
|
|
73
|
+
"task": "T2",
|
|
74
|
+
"model": "sonnet",
|
|
75
|
+
"pred_min": 9.316,
|
|
76
|
+
"actual_min": 19.803,
|
|
77
|
+
"delta": {
|
|
78
|
+
"abs": 10.4873,
|
|
79
|
+
"pct": 112.6
|
|
80
|
+
},
|
|
81
|
+
"cost_usd": 2.670396,
|
|
82
|
+
"first_time_pass": null
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"task": "T1",
|
|
86
|
+
"model": "sonnet",
|
|
87
|
+
"pred_min": 6.454,
|
|
88
|
+
"actual_min": 2.937,
|
|
89
|
+
"delta": {
|
|
90
|
+
"abs": -3.5173,
|
|
91
|
+
"pct": -54.5
|
|
92
|
+
},
|
|
93
|
+
"cost_usd": 0.42209,
|
|
94
|
+
"first_time_pass": null
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"task": "T5",
|
|
98
|
+
"model": "sonnet",
|
|
99
|
+
"pred_min": 5.351,
|
|
100
|
+
"actual_min": 2.17,
|
|
101
|
+
"delta": {
|
|
102
|
+
"abs": -3.181,
|
|
103
|
+
"pct": -59.4
|
|
104
|
+
},
|
|
105
|
+
"cost_usd": 0.161398,
|
|
106
|
+
"first_time_pass": null
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"task": "T6",
|
|
110
|
+
"model": "sonnet",
|
|
111
|
+
"pred_min": 2.877,
|
|
112
|
+
"actual_min": 0.882,
|
|
113
|
+
"delta": {
|
|
114
|
+
"abs": -1.9953,
|
|
115
|
+
"pct": -69.4
|
|
116
|
+
},
|
|
117
|
+
"cost_usd": 0.122338,
|
|
118
|
+
"first_time_pass": null
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"task": "T4",
|
|
122
|
+
"model": "sonnet",
|
|
123
|
+
"pred_min": 6.454,
|
|
124
|
+
"actual_min": 5.667,
|
|
125
|
+
"delta": {
|
|
126
|
+
"abs": -0.7873,
|
|
127
|
+
"pct": -12.2
|
|
128
|
+
},
|
|
129
|
+
"cost_usd": 0.599044,
|
|
130
|
+
"first_time_pass": null
|
|
131
|
+
}
|
|
132
|
+
],
|
|
133
|
+
"cost_table": {
|
|
134
|
+
"source": "est",
|
|
135
|
+
"usd": 4.6578,
|
|
136
|
+
"tokens": 11810387,
|
|
137
|
+
"wall_min": 209266448.2716,
|
|
138
|
+
"rows": [
|
|
139
|
+
{
|
|
140
|
+
"agent": "a28e70b6c5466655c",
|
|
141
|
+
"model": "sonnet",
|
|
142
|
+
"wall_min": 19.8119,
|
|
143
|
+
"tokens": 5477333,
|
|
144
|
+
"usd": 1.7755
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"agent": "a2b7551e7401c0456",
|
|
148
|
+
"model": "opus",
|
|
149
|
+
"wall_min": 3.3515,
|
|
150
|
+
"tokens": 1249546,
|
|
151
|
+
"usd": 0.8949
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"agent": "a2e8f511b95e838f0",
|
|
155
|
+
"model": "sonnet",
|
|
156
|
+
"wall_min": 29895213.7276,
|
|
157
|
+
"tokens": 0,
|
|
158
|
+
"usd": 0.0
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"agent": "a320c4ae073438df6",
|
|
162
|
+
"model": "sonnet",
|
|
163
|
+
"wall_min": 29895199.3553,
|
|
164
|
+
"tokens": 0,
|
|
165
|
+
"usd": 0.0
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"agent": "a32ee8f042610711a",
|
|
169
|
+
"model": "sonnet",
|
|
170
|
+
"wall_min": 4.6229,
|
|
171
|
+
"tokens": 1500746,
|
|
172
|
+
"usd": 0.5738
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"agent": "a3de6206b1938a855",
|
|
176
|
+
"model": "sonnet",
|
|
177
|
+
"wall_min": 29895191.2474,
|
|
178
|
+
"tokens": 0,
|
|
179
|
+
"usd": 0.0
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"agent": "a5aadd250ae9d099e",
|
|
183
|
+
"model": "sonnet",
|
|
184
|
+
"wall_min": 29895214.1158,
|
|
185
|
+
"tokens": 0,
|
|
186
|
+
"usd": 0.0
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"agent": "a6114011bcd91ade6",
|
|
190
|
+
"model": "sonnet",
|
|
191
|
+
"wall_min": 29895194.2183,
|
|
192
|
+
"tokens": 0,
|
|
193
|
+
"usd": 0.0
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"agent": "a7c51f47306b0e4dc",
|
|
197
|
+
"model": "sonnet",
|
|
198
|
+
"wall_min": 2.9432,
|
|
199
|
+
"tokens": 919141,
|
|
200
|
+
"usd": 0.4221
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"agent": "a8b053cb024a339f1",
|
|
204
|
+
"model": "sonnet",
|
|
205
|
+
"wall_min": 5.7614,
|
|
206
|
+
"tokens": 2034497,
|
|
207
|
+
"usd": 0.599
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"agent": "ab13c7ca907e649de",
|
|
211
|
+
"model": "sonnet",
|
|
212
|
+
"wall_min": 29895198.5619,
|
|
213
|
+
"tokens": 0,
|
|
214
|
+
"usd": 0.0
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"agent": "ac2dd171e416a6d86",
|
|
218
|
+
"model": "sonnet",
|
|
219
|
+
"wall_min": 29895197.2252,
|
|
220
|
+
"tokens": 0,
|
|
221
|
+
"usd": 0.0
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"agent": "acc89408dbc5a17c2",
|
|
225
|
+
"model": "sonnet",
|
|
226
|
+
"wall_min": 0.8919,
|
|
227
|
+
"tokens": 229216,
|
|
228
|
+
"usd": 0.1223
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"agent": "ad1f1f069e873e799",
|
|
232
|
+
"model": "opus",
|
|
233
|
+
"wall_min": 0.2504,
|
|
234
|
+
"tokens": 29894,
|
|
235
|
+
"usd": 0.1088
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"agent": "af1dfd427ea4862a9",
|
|
239
|
+
"model": "sonnet",
|
|
240
|
+
"wall_min": 2.1869,
|
|
241
|
+
"tokens": 370014,
|
|
242
|
+
"usd": 0.1614
|
|
243
|
+
}
|
|
244
|
+
]
|
|
245
|
+
}
|
|
246
|
+
}
|
docs/plans/2026-11-03-zero-cost-closures-scene-perf.report.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# superlite build report
|
|
2
|
+
|
|
3
|
+
projection: fixed forecast saved at lint 2026-11-03T18:40:15 (plan edited since)
|
|
4
|
+
|
|
5
|
+
Projected Actual Delta
|
|
6
|
+
wall-clock (min) 18.6 30.2 +11.6 (+62%)
|
|
7
|
+
agent cost (USD, +rework) 0.73 3.98 +3.24 (+442%)
|
|
8
|
+
task-minutes 35.5 31.5 -4.0 (-11%)
|
|
9
|
+
agents 6 15 +9 (+150%)
|
|
10
|
+
cost split: agents $3.08, rework $0.89, coordinator $50.07, total $54.04
|
|
11
|
+
concurrency: peak 2 at once (projected 3); critical path actual T1 T2 T6; projected T1 > T2 > T6 (18.6 min)
|
|
12
|
+
model mix: sonnet 5 tasks $3.98, coordinator opus $50.07
|
|
13
|
+
quality: tests 80 passed, 1 failed; first-time pass n/a; rework dispatches 1; open incidents 2
|
|
14
|
+
|
|
15
|
+
task model pred actual delta cost
|
|
16
|
+
T2 sonnet 9.3 19.8 +10.5 (+113%) $2.670
|
|
17
|
+
T1 sonnet 6.5 2.9 -3.5 (-54%) $0.422
|
|
18
|
+
T5 sonnet 5.4 2.2 -3.2 (-59%) $0.161
|
|
19
|
+
T6 sonnet 2.9 0.9 -2.0 (-69%) $0.122
|
|
20
|
+
T4 sonnet 6.5 5.7 -0.8 (-12%) $0.599
|
|
21
|
+
|
|
22
|
+
agent cost table:
|
|
23
|
+
| agent | model | wall_min | tokens | usd |
|
|
24
|
+
| --- | --- | --- | --- | --- |
|
|
25
|
+
| a28e70b6c5466655c | sonnet | 19.8119 | 5477333 | 1.7755 |
|
|
26
|
+
| a2b7551e7401c0456 | opus | 3.3515 | 1249546 | 0.8949 |
|
|
27
|
+
| a2e8f511b95e838f0 | sonnet | 29895213.7276 | 0 | 0.0 |
|
|
28
|
+
| a320c4ae073438df6 | sonnet | 29895199.3553 | 0 | 0.0 |
|
|
29
|
+
| a32ee8f042610711a | sonnet | 4.6229 | 1500746 | 0.5738 |
|
|
30
|
+
| a3de6206b1938a855 | sonnet | 29895191.2474 | 0 | 0.0 |
|
|
31
|
+
| a5aadd250ae9d099e | sonnet | 29895214.1158 | 0 | 0.0 |
|
|
32
|
+
| a6114011bcd91ade6 | sonnet | 29895194.2183 | 0 | 0.0 |
|
|
33
|
+
| a7c51f47306b0e4dc | sonnet | 2.9432 | 919141 | 0.4221 |
|
|
34
|
+
| a8b053cb024a339f1 | sonnet | 5.7614 | 2034497 | 0.599 |
|
|
35
|
+
| ab13c7ca907e649de | sonnet | 29895198.5619 | 0 | 0.0 |
|
|
36
|
+
| ac2dd171e416a6d86 | sonnet | 29895197.2252 | 0 | 0.0 |
|
|
37
|
+
| acc89408dbc5a17c2 | sonnet | 0.8919 | 229216 | 0.1223 |
|
|
38
|
+
| ad1f1f069e873e799 | opus | 0.2504 | 29894 | 0.1088 |
|
|
39
|
+
| af1dfd427ea4862a9 | sonnet | 2.1869 | 370014 | 0.1614 |
|
|
40
|
+
| totals | | 209266448.2716 | 11810387 | 4.6578 |
|
|
41
|
+
cost source: est
|
|
42
|
+
|
|
43
|
+
takeaway: ran 62% slower than projected; cost 442% over; biggest miss T2
|
docs/plans/2026-11-03-zero-cost-closures-scene-perf.review.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Zero-cost closures, blocks on List, and pure-scene-graph speed: review plan
|
|
2
|
+
|
|
3
|
+
**Builds on:** branch `pure-scene-values` at e04834f: unboxed variants, std cons `List`, `Slice`, tail calls, opt-level 3 wasmtime in dev.
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
Measured 2026-11-03, 1M elements, `map(×2)` → `filter` → `fold(+)`, dev build:
|
|
7
|
+
|
|
8
|
+
| style | time |
|
|
9
|
+
|---|---|
|
|
10
|
+
| `Slice`, fused `for`-chains (blocks) | 19 ms |
|
|
11
|
+
| `Slice` / `List`, plain `for` loop | 2–3 ms |
|
|
12
|
+
| `List`, closure methods `xs.map(\|x\| x * 2)` | 581 ms |
|
|
13
|
+
|
|
14
|
+
Closure methods are slow for two reasons. Every closure call is an indirect `call_ref` that wasm can't inline, and every stage allocates a whole new list. The pure scene graph is about +12% average against the mutable scene, and almost all of that is in update (0.32 ms per frame vs 0.04).
|
|
15
|
+
|
|
16
|
+
This plan has two parts (user decision, 2026-11-03: no for-chains on List; List keeps its closure methods, made fast by specialisation):
|
|
17
|
+
1. Rust-style zero-cost closures: a closure passed straight to a function gets specialised in.
|
|
18
|
+
2. A profile-first speed-up of the pure scene graph's update path.
|
|
19
|
+
|
|
20
|
+
## What you will get
|
|
21
|
+
- **`for`-chains work on `List`.** `evens := for x := range xs.filter()` with the predicate in the body, `for acc, x := range xs.fold(0)`, plus `map`, `take`, `skip`, `count`, `any`, `all` and `find`. Anything that declares `fun* items` gets the Seq adapters and terminals (std `List` does), and results are `List`. Expected speed is in line with `Slice` for-chains.
|
|
22
|
+
- **Zero-cost closures.** When a closure literal (or a named function) is passed directly to a `fn`-typed parameter that doesn't escape (isn't stored, returned or captured), the compiler emits a copy of the callee specialised to that closure, with the closure body inlined at each call. This includes recursive callees (`List.map` calls its own specialised copy) and methods on `List` and `Slice`. Copies are deduplicated by (callee, closure identity), and escaping closures stay as today. The target is `xs.map(|x| x * 2).filter(...).fold(...)` on `List` running within 3x of the for-chain version; the intermediate lists remain.
|
|
23
|
+
- **Pure scene graph update, measured, then fixed.**
|
|
24
|
+
1. **Profile.** Variants of the stress fixture each remove one cost (no action stepping; no trait dispatch for `act`/`kids`; action lists not rebuilt), so each cost is measured in isolation.
|
|
25
|
+
2. **Allocation-free `Actions.stepAll`.** It currently builds `Stepped`/`SteppedAll` records and new action lists for every actor every frame. It becomes in-place on `own` actions, returning a struct (pose and spare time) and dropping finished actions without rebuilding the list.
|
|
26
|
+
3. **Less dispatch.** `kids()` returning `List[Actor]?` becomes a direct `Group` check and field read on the traversal path.
|
|
27
|
+
4. **Benchmark** against the mutable scene in the same session. The bar is the same as before: average within 10% and p95 within 15%.
|
|
28
|
+
|
|
29
|
+
## What will be built
|
|
30
|
+
| Part | What it does | Size |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| Seq over items | Seq adapters and terminals accept any type with `fun* items`; List results stay List | S–M |
|
|
33
|
+
| Closure specialisation: analysis | Find direct closure / fn-name arguments to non-escaping fn params; escape check | M |
|
|
34
|
+
| Closure specialisation: codegen | Emit specialised callee copies (dedup, recursion, methods, generics) and inline the closure body | M–L |
|
|
35
|
+
| Scene profile probes | Stress-fixture variants plus a headed benchmark table | S |
|
|
36
|
+
| Allocation-free actions | Actions.stepAll / stepAction in place on own actions; struct results | M |
|
|
37
|
+
| Scene traversal | SceneTree update/draw without `kids()` optional plus dispatch | S–M |
|
|
38
|
+
| Benchmark and docs | Five-row scene table, closure table, README notes | S |
|
|
39
|
+
|
|
40
|
+
## How it fits together
|
|
41
|
+
```mermaid
|
|
42
|
+
flowchart LR
|
|
43
|
+
subgraph C["closures"]
|
|
44
|
+
A1["call f(|x| ...)"] --> A2["non-escaping fn param?"] --> A3["specialised copy, body inlined"]
|
|
45
|
+
end
|
|
46
|
+
subgraph B["blocks"]
|
|
47
|
+
B1["for x := range xs.filter()"] --> B2["Seq over fun* items"]
|
|
48
|
+
end
|
|
49
|
+
subgraph S["scene"]
|
|
50
|
+
S1["profile variants"] --> S2["in-place Actions on own"] --> S3["direct Group traversal"] --> S4["benchmark"]
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Decisions you can change
|
|
55
|
+
- **Specialisation trigger.**
|
|
56
|
+
- Default: automatic for closure literals and fn names passed directly to non-escaping params.
|
|
57
|
+
- Alternative: an explicit `inline fun` marker on the callee, as in Kotlin.
|
|
58
|
+
- **Code size cap.**
|
|
59
|
+
- Default: at most 8 specialised copies per callee; beyond that, fall back to `call_ref`.
|
|
60
|
+
- Alternative: no cap.
|
|
61
|
+
- **Scene API changes.**
|
|
62
|
+
- Default: `Actions` and the scene modules may change their internal signatures, and `game_pure_scene` follows.
|
|
63
|
+
- Alternative: keep every public signature unchanged.
|
|
64
|
+
|
|
65
|
+
## Risks
|
|
66
|
+
- **R1: escape analysis.** A closure stored in a field or list, returned, or captured by another closure must not be specialised. The check is conservative: anything unclear is treated as escaping.
|
|
67
|
+
- **R2: code growth.** Mitigated by the per-callee cap and deduplication.
|
|
68
|
+
- **R3: profile may point elsewhere.** If the measured cost is not stepping or dispatch, the scene part reports what is costing time and stops, as with phase B's R4.
|
|
69
|
+
- **R4: ownership.** In-place actions rely on the `own` rules. The checker must accept the new shapes without special cases.
|
|
70
|
+
|
|
71
|
+
## Out of scope
|
|
72
|
+
- Fusing `map(f).filter(g)` into one pass without intermediate lists (supercompiler/deforestation, the later plan).
|
|
73
|
+
- Changing `Slice`'s for-chains, apart from accepting `items` types.
|
|
74
|
+
|
|
75
|
+
## How we will know it works
|
|
76
|
+
- **Closures:**
|
|
77
|
+
- wasmparser shows no `call_ref` for a specialised call site;
|
|
78
|
+
- escaping-closure tests unchanged;
|
|
79
|
+
- recursion and generic-method tests;
|
|
80
|
+
- the 1M closure-style `List` pipeline within 3x of the for-chain version (timed).
|
|
81
|
+
- **Blocks:** `filter`, `map`, `fold`, `take` and `count` for-chains on `List` with exact results, at 1M in time similar to `Slice`.
|
|
82
|
+
- **Scene:**
|
|
83
|
+
- a profile table showing each cost's share;
|
|
84
|
+
- after the changes, update time and average/p95 against the mutable scene in the same session, judged against the bar;
|
|
85
|
+
- `game_pure_scene` tests unchanged in names and values;
|
|
86
|
+
- host parity and browser checks green.
|
|
87
|
+
- **Every existing suite stays green** (629+ Plum, Rust crates).
|
|
88
|
+
|
|
89
|
+
## Rough time and cost
|
|
90
|
+
About 7 parts in three tracks; the closure codegen part is the largest. Rough agent time is 80–120 minutes, plus two headed benchmark runs.
|
plum-checker/src/lib.rs
CHANGED
|
@@ -1180,6 +1180,27 @@ pub fn checkSource(source: &ast::Source) -> CheckResult<()> {
|
|
|
1180
1180
|
/// set of generic instantiations (see `TypedInfo`) for codegen.
|
|
1181
1181
|
pub fn typedSource(source: &ast::Source) -> CheckResult<TypedInfo> {
|
|
1182
1182
|
let (mut result, mut info) = checkSourceFull(source);
|
|
1183
|
+
// Closure specialisation: copies of callees with the closure argument inlined. Same
|
|
1184
|
+
// re-check discipline as the TRMC rewrite below; it runs first so TRMC and tail calls
|
|
1185
|
+
// apply to the copies. On a failed re-check the original program stays.
|
|
1186
|
+
if result.is_ok() {
|
|
1187
|
+
if let Some(lowered) = info.lowered.as_ref() {
|
|
1188
|
+
let hints = closureHints(lowered, &info);
|
|
1189
|
+
let mut copy = (**lowered).clone();
|
|
1190
|
+
if !plum_core::closure_spec::specialiseWith(&mut copy, &hints).is_empty() {
|
|
1191
|
+
let saved = plum_core::unions::takePlan();
|
|
1192
|
+
plum_core::unions::beginPlan();
|
|
1193
|
+
let (r2, mut i2) = checkSourceOnce(©);
|
|
1194
|
+
plum_core::unions::takePlan();
|
|
1195
|
+
unions::restorePlan(&saved);
|
|
1196
|
+
if r2.is_ok() {
|
|
1197
|
+
i2.lowered = Some(std::rc::Rc::new(copy));
|
|
1198
|
+
result = r2;
|
|
1199
|
+
info = i2;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1183
1204
|
// Tail recursion modulo cons: rewrite the lowered program, then check the
|
|
1184
1205
|
// rewrite again (types are recorded per expression address). If the rewrite
|
|
1185
1206
|
// does not check, keep the original program.
|
|
@@ -1208,6 +1229,56 @@ pub fn typedSource(source: &ast::Source) -> CheckResult<TypedInfo> {
|
|
|
1208
1229
|
result.map(|()| info)
|
|
1209
1230
|
}
|
|
1210
1231
|
|
|
1232
|
+
/// Receiver type names of the method calls of `lowered`, for `closure_spec` (a call site
|
|
1233
|
+
/// whose receiver has no single known type gets no hint).
|
|
1234
|
+
fn closureHints(lowered: &ast::Source, info: &TypedInfo) -> plum_core::closure_spec::Hints {
|
|
1235
|
+
fn head(t: &PlumType) -> Option<String> {
|
|
1236
|
+
match t {
|
|
1237
|
+
PlumType::TNamed(n) | PlumType::TApp(n, _) => Some(n.clone()),
|
|
1238
|
+
PlumType::TInt => Some("Int".to_string()),
|
|
1239
|
+
PlumType::TFloat => Some("Float".to_string()),
|
|
1240
|
+
PlumType::TBool => Some("Bool".to_string()),
|
|
1241
|
+
PlumType::TByte => Some("Byte".to_string()),
|
|
1242
|
+
_ => None,
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
let mut inst: HashMap<usize, Vec<&PlumType>> = HashMap::new();
|
|
1246
|
+
for ((_, addr), t) in &info.inst_expr_types {
|
|
1247
|
+
inst.entry(*addr).or_default().push(t);
|
|
1248
|
+
}
|
|
1249
|
+
let mut hints = plum_core::closure_spec::Hints::new();
|
|
1250
|
+
let mut bad: std::collections::HashSet<(String, usize, usize, String)> = std::collections::HashSet::new();
|
|
1251
|
+
for (key, addr) in plum_core::closure_spec::methodSites(lowered) {
|
|
1252
|
+
let addr = addr as usize;
|
|
1253
|
+
let mut names: std::collections::HashSet<Option<String>> = std::collections::HashSet::new();
|
|
1254
|
+
if let Some(t) = info.expr_types.get(&addr) {
|
|
1255
|
+
names.insert(head(t));
|
|
1256
|
+
}
|
|
1257
|
+
for t in inst.get(&addr).into_iter().flatten() {
|
|
1258
|
+
names.insert(head(t));
|
|
1259
|
+
}
|
|
1260
|
+
let name = match (names.len(), names.into_iter().next()) {
|
|
1261
|
+
(1, Some(Some(n))) => n,
|
|
1262
|
+
_ => {
|
|
1263
|
+
bad.insert(key);
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
match hints.get(&key) {
|
|
1268
|
+
Some(prev) if *prev != name => {
|
|
1269
|
+
bad.insert(key);
|
|
1270
|
+
}
|
|
1271
|
+
_ => {
|
|
1272
|
+
hints.insert(key, name);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
for k in bad {
|
|
1277
|
+
hints.remove(&k);
|
|
1278
|
+
}
|
|
1279
|
+
hints
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1211
1282
|
fn checkSourceFull(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
1212
1283
|
if let Some(applied) = applyOwnGenerics(source) {
|
|
1213
1284
|
// Check (and let codegen compile) the copy with bare own-generic names applied.
|
plum-core/src/closure_escape.rs
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
//! Escape analysis for fn-typed parameters. A fn-typed param does not escape when
|
|
2
|
+
//! the callee only calls it directly (`f(x)`) or hands it, as a bare argument, to
|
|
3
|
+
//! a fn-typed param that itself does not escape (itself, recursively, included).
|
|
4
|
+
//! Anything else -- storing, returning, aliasing, assigning, capturing in a closure,
|
|
5
|
+
//! passing to an unknown or escaping callee -- makes it escape.
|
|
6
|
+
//!
|
|
7
|
+
//! Pure AST analysis, conservative: calls are resolved by name only. A free fn call
|
|
8
|
+
//! resolves to the free fn of that name; a method call `x.m(..)` (receiver type unknown)
|
|
9
|
+
//! resolves to every method named `m` that takes that many args (`Type.m(..)` to that
|
|
10
|
+
//! type's), and the argument is safe only if it is safe in all of them. No candidate
|
|
11
|
+
//! (extern, closure value, constructor) means escape. Solved as a greatest fixpoint, so
|
|
12
|
+
//! recursion with the same param stays non-escaping.
|
|
13
|
+
use crate::ast::*;
|
|
14
|
+
use std::collections::{HashMap, HashSet};
|
|
15
|
+
|
|
16
|
+
/// For each fn that has fn-typed params that do not escape: key = free fn name or
|
|
17
|
+
/// `Type.method`, value = the indices (self excluded) of those params, ascending.
|
|
18
|
+
pub fn nonEscapingFnParams(src: &Source) -> HashMap<String, Vec<usize>> {
|
|
19
|
+
let mut fns: Vec<(String, &Fn)> = Vec::new();
|
|
20
|
+
for item in &src.items {
|
|
21
|
+
if let Item::Fn(f) = item {
|
|
22
|
+
if f.is_extern || f.is_star || f.name.starts_with("__test_") {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
let key = match &f.type_param {
|
|
26
|
+
Some(t) => format!("{}.{}", t, f.name),
|
|
27
|
+
None => f.name.clone(),
|
|
28
|
+
};
|
|
29
|
+
fns.push((key, f));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
let mut free: HashMap<&str, usize> = HashMap::new();
|
|
33
|
+
let mut methods: HashMap<&str, Vec<usize>> = HashMap::new();
|
|
34
|
+
for (i, (_, f)) in fns.iter().enumerate() {
|
|
35
|
+
match f.type_param {
|
|
36
|
+
None => {
|
|
37
|
+
free.insert(f.name.as_str(), i);
|
|
38
|
+
}
|
|
39
|
+
Some(_) => methods.entry(f.name.as_str()).or_default().push(i),
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
let mut ok: HashSet<(usize, usize)> = HashSet::new();
|
|
43
|
+
for (i, (_, f)) in fns.iter().enumerate() {
|
|
44
|
+
for (j, p) in f.params.iter().enumerate() {
|
|
45
|
+
if isFnParam(p) {
|
|
46
|
+
ok.insert((i, j));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
loop {
|
|
51
|
+
let mut drop = Vec::new();
|
|
52
|
+
for &(i, j) in &ok {
|
|
53
|
+
let f = fns[i].1;
|
|
54
|
+
let cx = Cx { fns: &fns, free: &free, methods: &methods, ok: &ok, p: &f.params[j].name };
|
|
55
|
+
if cx.fnEscapes(f) {
|
|
56
|
+
drop.push((i, j));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if drop.is_empty() {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
for d in drop {
|
|
63
|
+
ok.remove(&d);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
let mut out: HashMap<String, Vec<usize>> = HashMap::new();
|
|
67
|
+
for (i, j) in ok {
|
|
68
|
+
out.entry(fns[i].0.clone()).or_default().push(j);
|
|
69
|
+
}
|
|
70
|
+
for v in out.values_mut() {
|
|
71
|
+
v.sort();
|
|
72
|
+
}
|
|
73
|
+
out
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fn isFnParam(p: &Param) -> bool {
|
|
77
|
+
match &p.ty {
|
|
78
|
+
ParamType::Fn(..) => !p.isOwn,
|
|
79
|
+
ParamType::Type(t) => t.asFn().is_some() && !p.isOwn,
|
|
80
|
+
_ => false,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
struct Cx<'a> {
|
|
85
|
+
fns: &'a [(String, &'a Fn)],
|
|
86
|
+
free: &'a HashMap<&'a str, usize>,
|
|
87
|
+
methods: &'a HashMap<&'a str, Vec<usize>>,
|
|
88
|
+
ok: &'a HashSet<(usize, usize)>,
|
|
89
|
+
/// The param under test.
|
|
90
|
+
p: &'a str,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
impl<'a> Cx<'a> {
|
|
94
|
+
fn fnEscapes(&self, f: &Fn) -> bool {
|
|
95
|
+
match &f.body {
|
|
96
|
+
FnBody::Expr(e) => self.expr(e, false),
|
|
97
|
+
FnBody::Block(b) => self.block(b, false),
|
|
98
|
+
FnBody::Extern => true,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fn block(&self, b: &Block, cl: bool) -> bool {
|
|
103
|
+
b.stmts.iter().any(|s| self.stmt(s, cl))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fn stmt(&self, s: &Stmt, cl: bool) -> bool {
|
|
107
|
+
match s {
|
|
108
|
+
Stmt::Assign(a) => {
|
|
109
|
+
a.targets.iter().any(|t| match t {
|
|
110
|
+
AssignTarget::Var(n) => n == self.p,
|
|
111
|
+
AssignTarget::Field(o, _) => self.expr(o, cl),
|
|
112
|
+
}) || a.values.iter().any(|v| self.expr(v, cl))
|
|
113
|
+
}
|
|
114
|
+
Stmt::Break | Stmt::Continue | Stmt::Todo => false,
|
|
115
|
+
Stmt::Assert(c) => self.expr(&c.cond, cl),
|
|
116
|
+
Stmt::ForLoop(l) => {
|
|
117
|
+
self.expr(&l.source, cl)
|
|
118
|
+
|| l.clauses.iter().any(|c| match c {
|
|
119
|
+
ForClause::If(e) | ForClause::Bind(_, e) => self.expr(e, cl),
|
|
120
|
+
})
|
|
121
|
+
|| self.block(&l.body, cl)
|
|
122
|
+
}
|
|
123
|
+
Stmt::While(w) => self.expr(&w.condition, cl) || self.block(&w.body, cl),
|
|
124
|
+
Stmt::If(i) => {
|
|
125
|
+
self.expr(&i.condition, cl)
|
|
126
|
+
|| self.block(&i.body, cl)
|
|
127
|
+
|| i.else_ifs.iter().any(|e| self.expr(&e.condition, cl) || self.block(&e.body, cl))
|
|
128
|
+
|| i.else_.as_ref().is_some_and(|b| self.block(b, cl))
|
|
129
|
+
}
|
|
130
|
+
Stmt::Match(m) => {
|
|
131
|
+
m.subjects.iter().any(|e| self.expr(e, cl)) || m.cases.iter().any(|c| self.case(c, cl))
|
|
132
|
+
}
|
|
133
|
+
Stmt::Return(e) => e.as_ref().is_some_and(|e| self.expr(e, cl)),
|
|
134
|
+
Stmt::Crash(e) => self.expr(e, cl),
|
|
135
|
+
Stmt::Expr(e, _, _) => self.expr(e, cl),
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fn case(&self, c: &Case, cl: bool) -> bool {
|
|
140
|
+
c.guard.as_ref().is_some_and(|g| self.expr(g, cl)) || self.block(&c.body, cl)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fn isParam(&self, e: &Expr) -> bool {
|
|
144
|
+
matches!(e, Expr::Var(n) if n == self.p)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// Whether the param escapes through this call's arguments. `callees` are the
|
|
148
|
+
/// candidate fns; an empty list means the callee is unknown.
|
|
149
|
+
fn args(&self, args: &[Arg], callees: &[usize], cl: bool) -> bool {
|
|
150
|
+
for (pos, a) in args.iter().enumerate() {
|
|
151
|
+
let (v, name) = match a {
|
|
152
|
+
Arg::Positional(v) => (v, None),
|
|
153
|
+
Arg::Keyword { name, value } => (value, Some(name)),
|
|
154
|
+
};
|
|
155
|
+
if self.isParam(v) {
|
|
156
|
+
if cl || callees.is_empty() {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
for &c in callees {
|
|
160
|
+
let params = &self.fns[c].1.params;
|
|
161
|
+
let idx = match name {
|
|
162
|
+
None => Some(pos),
|
|
163
|
+
Some(n) => params.iter().position(|p| &p.name == n),
|
|
164
|
+
};
|
|
165
|
+
match idx {
|
|
166
|
+
Some(j) if self.ok.contains(&(c, j)) => {}
|
|
167
|
+
_ => return true,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} else if self.expr(v, cl) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
false
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fn call(&self, c: &FnCall, cl: bool) -> bool {
|
|
178
|
+
if c.name == self.p {
|
|
179
|
+
// Direct call of the param; inside a closure it is a capture.
|
|
180
|
+
return cl || self.args(&c.args, &[], cl);
|
|
181
|
+
}
|
|
182
|
+
let callees: Vec<usize> = self.free.get(c.name.as_str()).copied().into_iter().collect();
|
|
183
|
+
self.args(&c.args, &callees, cl)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fn method(&self, object: &Expr, c: &FnCall, cl: bool) -> bool {
|
|
187
|
+
if self.expr(object, cl) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
let want = if let Expr::TypeName(t) = object { Some(t.as_str()) } else { None };
|
|
191
|
+
let callees: Vec<usize> = self
|
|
192
|
+
.methods
|
|
193
|
+
.get(c.name.as_str())
|
|
194
|
+
.map(|v| {
|
|
195
|
+
v.iter()
|
|
196
|
+
.copied()
|
|
197
|
+
.filter(|&i| {
|
|
198
|
+
let f = self.fns[i].1;
|
|
199
|
+
let ownerOk = match want {
|
|
200
|
+
Some(t) => f.type_param.as_deref() == Some(t),
|
|
201
|
+
None => f.has_self,
|
|
202
|
+
};
|
|
203
|
+
ownerOk
|
|
204
|
+
&& (f.params.iter().any(|p| matches!(p.ty, ParamType::Variadic(_)))
|
|
205
|
+
|| f.params.len() >= c.args.len())
|
|
206
|
+
})
|
|
207
|
+
.collect()
|
|
208
|
+
})
|
|
209
|
+
.unwrap_or_default();
|
|
210
|
+
self.args(&c.args, &callees, cl)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
fn expr(&self, e: &Expr, cl: bool) -> bool {
|
|
214
|
+
match e {
|
|
215
|
+
Expr::Var(n) => n == self.p,
|
|
216
|
+
Expr::Binary(b) => self.expr(&b.left, cl) || self.expr(&b.right, cl),
|
|
217
|
+
Expr::Unary(u) => self.expr(&u.operand, cl),
|
|
218
|
+
Expr::Bool(b) => self.expr(&b.left, cl) || self.expr(&b.right, cl),
|
|
219
|
+
Expr::Not(x) | Expr::Try(x) | Expr::Paren(x) => self.expr(x, cl),
|
|
220
|
+
Expr::Compare(c) => self.expr(&c.left, cl) || self.expr(&c.right, cl),
|
|
221
|
+
Expr::InlineIf(i) => {
|
|
222
|
+
self.expr(&i.condition, cl) || self.expr(&i.then, cl) || self.expr(&i.else_, cl)
|
|
223
|
+
}
|
|
224
|
+
Expr::FnCall(c) => self.call(c, cl),
|
|
225
|
+
Expr::Yield(xs) => xs.iter().any(|x| self.expr(x, cl)),
|
|
226
|
+
Expr::ClassCall(c) => {
|
|
227
|
+
c.fields.iter().any(|f| self.expr(&f.value, cl))
|
|
228
|
+
|| c.spread.as_ref().is_some_and(|s| self.expr(s, cl))
|
|
229
|
+
}
|
|
230
|
+
Expr::Attribute(a) => match &a.attr {
|
|
231
|
+
AttrKind::Field(_) => self.expr(&a.object, cl),
|
|
232
|
+
AttrKind::Method(c) => self.method(&a.object, c, cl),
|
|
233
|
+
},
|
|
234
|
+
Expr::Bind(b) => self.expr(&b.value, cl) || self.case(&b.case, cl),
|
|
235
|
+
Expr::Bang(b) => self.expr(&b.value, cl),
|
|
236
|
+
Expr::Catch(c) => {
|
|
237
|
+
self.expr(&c.value, cl)
|
|
238
|
+
|| match &c.handler {
|
|
239
|
+
CatchHandler::Default(d) => self.expr(d, cl),
|
|
240
|
+
CatchHandler::Block { body, .. } => self.block(body, cl),
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
Expr::String(s) => s.parts.iter().any(|p| match p {
|
|
244
|
+
StringPart::Interp(x) => self.expr(x, cl),
|
|
245
|
+
StringPart::Text(_) => false,
|
|
246
|
+
}),
|
|
247
|
+
Expr::Closure(c) => self.block(&c.body, true),
|
|
248
|
+
Expr::ErrorValue(_)
|
|
249
|
+
| Expr::Int(_)
|
|
250
|
+
| Expr::Float(_)
|
|
251
|
+
| Expr::BoolLit(_)
|
|
252
|
+
| Expr::Self_
|
|
253
|
+
| Expr::TypeName(_) => false,
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
plum-core/src/closure_spec.rs
ADDED
|
@@ -0,0 +1,1373 @@
|
|
|
1
|
+
//! Closure specialisation. A call that passes a closure literal (or a free fn name)
|
|
2
|
+
//! directly to a fn-typed parameter that does not escape (see `closure_escape`) gets a
|
|
3
|
+
//! copy of the callee, named `name$c0`, `name$c1`, ..., in which every call of that
|
|
4
|
+
//! parameter is replaced by the closure body (params bound to the call's arguments,
|
|
5
|
+
//! binders renamed fresh). Variables the closure captures become extra trailing params of
|
|
6
|
+
//! the copy (generic, named `__cap<spec>_<i>`) that the call site passes after the
|
|
7
|
+
//! original arguments. The fn-typed parameter itself stays in the copy's signature (so
|
|
8
|
+
//! generics keep being inferred from the argument) but is unused apart from being passed
|
|
9
|
+
//! on to the copies of callees it was handed to. Recursive calls passing the parameter
|
|
10
|
+
//! along go to the copy. At most `MAX_COPIES` copies per callee; past that, or when the
|
|
11
|
+
//! closure cannot be inlined, the call is left alone.
|
|
12
|
+
use crate::ast::*;
|
|
13
|
+
use crate::closure_escape::nonEscapingFnParams;
|
|
14
|
+
use std::collections::{HashMap, HashSet};
|
|
15
|
+
|
|
16
|
+
pub const MAX_COPIES: usize = 8;
|
|
17
|
+
|
|
18
|
+
/// Receiver type of a method call site: (declaring fn's file, call line, call column,
|
|
19
|
+
/// method name) -> type name. Supplied by the checker, which knows the types.
|
|
20
|
+
pub type Hints = HashMap<(String, usize, usize, String), String>;
|
|
21
|
+
|
|
22
|
+
pub fn specialise(src: &mut Source) -> Vec<String> {
|
|
23
|
+
specialiseWith(src, &Hints::new())
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// The method call sites of `src` (hint key, address of the receiver expression), so the
|
|
27
|
+
/// checker can look the receivers' types up.
|
|
28
|
+
pub fn methodSites(src: &Source) -> Vec<((String, usize, usize, String), *const Expr)> {
|
|
29
|
+
let mut out = Vec::new();
|
|
30
|
+
for item in &src.items {
|
|
31
|
+
if let Item::Fn(f) = item {
|
|
32
|
+
let file = f.file.clone();
|
|
33
|
+
let mut cb = |e: &Expr| {
|
|
34
|
+
if let Expr::Attribute(a) = e {
|
|
35
|
+
if let AttrKind::Method(c) = &a.attr {
|
|
36
|
+
let key = (file.clone(), c.line, c.column, baseName(&c.name).to_string());
|
|
37
|
+
out.push((key, &a.object as *const Expr));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
match &f.body {
|
|
42
|
+
FnBody::Expr(e) => walkRef(e, &mut cb),
|
|
43
|
+
FnBody::Block(b) => walkBlockRef(b, &mut cb),
|
|
44
|
+
FnBody::Extern => {}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
out
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fn baseName(n: &str) -> &str {
|
|
52
|
+
n.split('$').next().unwrap_or(n)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
pub fn specialiseWith(src: &mut Source, hints: &Hints) -> Vec<String> {
|
|
56
|
+
let ne = nonEscapingFnParams(src);
|
|
57
|
+
if ne.is_empty() {
|
|
58
|
+
return Vec::new();
|
|
59
|
+
}
|
|
60
|
+
let mut cx = Ctx {
|
|
61
|
+
hints,
|
|
62
|
+
ne,
|
|
63
|
+
fns: HashMap::new(),
|
|
64
|
+
methodsByName: HashMap::new(),
|
|
65
|
+
specs: Vec::new(),
|
|
66
|
+
copies: HashMap::new(),
|
|
67
|
+
perCallee: HashMap::new(),
|
|
68
|
+
out: Vec::new(),
|
|
69
|
+
outKeys: Vec::new(),
|
|
70
|
+
fresh: 0,
|
|
71
|
+
};
|
|
72
|
+
for item in &src.items {
|
|
73
|
+
if let Item::Fn(f) = item {
|
|
74
|
+
if f.is_extern || f.is_star || f.name.starts_with("__test_") {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
let key = fnKey(f);
|
|
78
|
+
if f.type_param.is_some() && f.has_self {
|
|
79
|
+
cx.methodsByName.entry(f.name.clone()).or_default().push(key.clone());
|
|
80
|
+
}
|
|
81
|
+
cx.fns.insert(key, f.clone());
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for i in 0..src.items.len() {
|
|
85
|
+
let mut f = match &src.items[i] {
|
|
86
|
+
Item::Fn(f) if !f.is_extern && !f.is_star && !f.name.starts_with("__test_") => f.clone(),
|
|
87
|
+
_ => continue,
|
|
88
|
+
};
|
|
89
|
+
if cx.processFn(&mut f) {
|
|
90
|
+
src.items[i] = Item::Fn(f);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
let mut done = 0;
|
|
94
|
+
while done < cx.out.len() {
|
|
95
|
+
if let Some(mut f) = cx.out[done].clone() {
|
|
96
|
+
if cx.processFn(&mut f) {
|
|
97
|
+
cx.out[done] = Some(f);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
done += 1;
|
|
101
|
+
}
|
|
102
|
+
let mut names = Vec::new();
|
|
103
|
+
for (i, f) in cx.out.into_iter().enumerate() {
|
|
104
|
+
if let Some(f) = f {
|
|
105
|
+
names.push(cx.outKeys[i].clone());
|
|
106
|
+
src.items.push(Item::Fn(f));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
names
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fn fnKey(f: &Fn) -> String {
|
|
113
|
+
match &f.type_param {
|
|
114
|
+
Some(t) => format!("{}.{}", t, f.name),
|
|
115
|
+
None => f.name.clone(),
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fn capName(spec: usize, i: usize) -> String {
|
|
120
|
+
format!("__cap{}_{}", spec, i)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#[derive(Clone, PartialEq)]
|
|
124
|
+
enum Kind {
|
|
125
|
+
Lit(Closure),
|
|
126
|
+
Name(String),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#[derive(Clone, PartialEq)]
|
|
130
|
+
struct Spec {
|
|
131
|
+
kind: Kind,
|
|
132
|
+
/// The closure (or fn name) without source positions: the dedup identity.
|
|
133
|
+
norm: String,
|
|
134
|
+
/// Captured variables at the originating call site (`"self"` for the receiver).
|
|
135
|
+
caps: Vec<String>,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#[derive(Clone)]
|
|
139
|
+
struct Enc {
|
|
140
|
+
file: String,
|
|
141
|
+
ty: Option<String>,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
struct Site {
|
|
145
|
+
enc: Enc,
|
|
146
|
+
locals: HashSet<String>,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
struct Tx {
|
|
150
|
+
p: String,
|
|
151
|
+
spec: usize,
|
|
152
|
+
enc: Enc,
|
|
153
|
+
handled: usize,
|
|
154
|
+
err: bool,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
struct Ctx<'h> {
|
|
158
|
+
hints: &'h Hints,
|
|
159
|
+
ne: HashMap<String, Vec<usize>>,
|
|
160
|
+
fns: HashMap<String, Fn>,
|
|
161
|
+
methodsByName: HashMap<String, Vec<String>>,
|
|
162
|
+
specs: Vec<Spec>,
|
|
163
|
+
/// (callee key, param index, spec) -> copy fn name; `None` when it cannot be made.
|
|
164
|
+
copies: HashMap<(String, usize, usize), Option<String>>,
|
|
165
|
+
perCallee: HashMap<String, usize>,
|
|
166
|
+
out: Vec<Option<Fn>>,
|
|
167
|
+
outKeys: Vec<String>,
|
|
168
|
+
fresh: usize,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
impl<'h> Ctx<'h> {
|
|
172
|
+
// ---------- call sites ----------
|
|
173
|
+
|
|
174
|
+
fn processFn(&mut self, f: &mut Fn) -> bool {
|
|
175
|
+
let mut locals: HashSet<String> = f.params.iter().map(|p| p.name.clone()).collect();
|
|
176
|
+
collectBinders(&mut f.body, &mut locals);
|
|
177
|
+
let site = Site {
|
|
178
|
+
enc: Enc {
|
|
179
|
+
file: f.file.clone(),
|
|
180
|
+
ty: f.type_param.clone(),
|
|
181
|
+
},
|
|
182
|
+
locals,
|
|
183
|
+
};
|
|
184
|
+
let mut changed = false;
|
|
185
|
+
let mut cb = |e: &mut Expr| {
|
|
186
|
+
if self.siteExpr(&site, e) {
|
|
187
|
+
changed = true;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
match &mut f.body {
|
|
191
|
+
FnBody::Expr(e) => visitExpr(e, &mut cb),
|
|
192
|
+
FnBody::Block(b) => visitBlock(b, &mut cb),
|
|
193
|
+
FnBody::Extern => {}
|
|
194
|
+
}
|
|
195
|
+
changed
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
fn siteExpr(&mut self, site: &Site, e: &mut Expr) -> bool {
|
|
199
|
+
match e {
|
|
200
|
+
Expr::FnCall(c) => {
|
|
201
|
+
if site.locals.contains(&c.name) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
match self.resolve(&site.enc, None, c) {
|
|
205
|
+
Some(key) => self.rewriteCall(site, key, c),
|
|
206
|
+
None => false,
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
Expr::Attribute(a) => {
|
|
210
|
+
let AttributeExpr { object, attr } = &mut **a;
|
|
211
|
+
if let AttrKind::Method(c) = attr {
|
|
212
|
+
match self.resolve(&site.enc, Some(object), c) {
|
|
213
|
+
Some(key) => self.rewriteCall(site, key, c),
|
|
214
|
+
None => false,
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
false
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
_ => false,
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
fn resolve(&self, enc: &Enc, object: Option<&Expr>, c: &FnCall) -> Option<String> {
|
|
225
|
+
match object {
|
|
226
|
+
None => {
|
|
227
|
+
let f = self.fns.get(&c.name)?;
|
|
228
|
+
if f.type_param.is_none() {
|
|
229
|
+
Some(c.name.clone())
|
|
230
|
+
} else {
|
|
231
|
+
None
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
Some(Expr::TypeName(t)) => {
|
|
235
|
+
let key = format!("{}.{}", t, c.name);
|
|
236
|
+
self.fns.contains_key(&key).then_some(key)
|
|
237
|
+
}
|
|
238
|
+
Some(obj) => {
|
|
239
|
+
let ty = if matches!(obj, Expr::Self_) {
|
|
240
|
+
enc.ty.clone()
|
|
241
|
+
} else {
|
|
242
|
+
let k = (enc.file.clone(), c.line, c.column, baseName(&c.name).to_string());
|
|
243
|
+
self.hints.get(&k).cloned()
|
|
244
|
+
};
|
|
245
|
+
match ty {
|
|
246
|
+
Some(t) => {
|
|
247
|
+
let key = format!("{}.{}", t, c.name);
|
|
248
|
+
self.fns.contains_key(&key).then_some(key)
|
|
249
|
+
}
|
|
250
|
+
None => {
|
|
251
|
+
let v = self.methodsByName.get(&c.name)?;
|
|
252
|
+
if v.len() == 1 {
|
|
253
|
+
Some(v[0].clone())
|
|
254
|
+
} else {
|
|
255
|
+
None
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
fn rewriteCall(&mut self, site: &Site, mut key: String, c: &mut FnCall) -> bool {
|
|
264
|
+
let mut changed = false;
|
|
265
|
+
for _ in 0..8 {
|
|
266
|
+
let Some(f) = self.fns.get(&key) else { break };
|
|
267
|
+
if c.args.len() != f.params.len()
|
|
268
|
+
|| c.args.iter().any(|a| matches!(a, Arg::Keyword { .. }))
|
|
269
|
+
|| f.params.iter().any(|p| !matches!(p.ty, ParamType::Type(_) | ParamType::Fn(..)))
|
|
270
|
+
{
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
let idxs = self.ne.get(&key).cloned().unwrap_or_default();
|
|
274
|
+
let mut done = false;
|
|
275
|
+
for j in idxs {
|
|
276
|
+
let Arg::Positional(arg) = &c.args[j] else { continue };
|
|
277
|
+
let Some(spec) = self.mkSpec(arg, site) else { continue };
|
|
278
|
+
let sid = self.specId(spec);
|
|
279
|
+
let Some(name) = self.getCopy(&key, j, sid) else { continue };
|
|
280
|
+
for (i, cap) in self.specs[sid].caps.iter().enumerate() {
|
|
281
|
+
let _ = i;
|
|
282
|
+
c.args.push(Arg::Positional(if cap == "self" {
|
|
283
|
+
Expr::Self_
|
|
284
|
+
} else {
|
|
285
|
+
Expr::Var(cap.clone())
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
288
|
+
let f = &self.fns[&key];
|
|
289
|
+
let newKey = match &f.type_param {
|
|
290
|
+
Some(t) => format!("{}.{}", t, name),
|
|
291
|
+
None => name.clone(),
|
|
292
|
+
};
|
|
293
|
+
c.name = name;
|
|
294
|
+
key = newKey;
|
|
295
|
+
changed = true;
|
|
296
|
+
done = true;
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
if !done {
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
changed
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
fn specId(&mut self, spec: Spec) -> usize {
|
|
307
|
+
if let Some(i) = self.specs.iter().position(|s| s.norm == spec.norm && s.caps == spec.caps) {
|
|
308
|
+
return i;
|
|
309
|
+
}
|
|
310
|
+
self.specs.push(spec);
|
|
311
|
+
self.specs.len() - 1
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
fn mkSpec(&self, arg: &Expr, site: &Site) -> Option<Spec> {
|
|
315
|
+
match arg {
|
|
316
|
+
Expr::Closure(cl) => {
|
|
317
|
+
let caps = self.analyze(cl, &site.locals)?;
|
|
318
|
+
let mut z = (**cl).clone();
|
|
319
|
+
zeroBlock(&mut z.body);
|
|
320
|
+
Some(Spec {
|
|
321
|
+
kind: Kind::Lit((**cl).clone()),
|
|
322
|
+
norm: format!("{:?}", z),
|
|
323
|
+
caps,
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
Expr::Var(n) if !site.locals.contains(n) => {
|
|
327
|
+
let f = self.fns.get(n)?;
|
|
328
|
+
if f.type_param.is_none() && !f.is_export {
|
|
329
|
+
Some(Spec {
|
|
330
|
+
kind: Kind::Name(n.clone()),
|
|
331
|
+
norm: format!("fn {}", n),
|
|
332
|
+
caps: Vec::new(),
|
|
333
|
+
})
|
|
334
|
+
} else {
|
|
335
|
+
None
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
_ => None,
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/// The captured variables of a closure, or `None` if it cannot be inlined.
|
|
343
|
+
fn analyze(&self, cl: &Closure, locals: &HashSet<String>) -> Option<Vec<String>> {
|
|
344
|
+
if anyReturn(&cl.body.stmts) {
|
|
345
|
+
return None;
|
|
346
|
+
}
|
|
347
|
+
let mut c = cl.clone();
|
|
348
|
+
let mut binders: HashSet<String> = cl.params.iter().cloned().collect();
|
|
349
|
+
let mut plain: Vec<String> = Vec::new();
|
|
350
|
+
let mut refs: Vec<String> = Vec::new();
|
|
351
|
+
renameStmts(&mut c.body.stmts, &mut |n, kind| match kind {
|
|
352
|
+
Name::Decl => {
|
|
353
|
+
binders.insert(n.clone());
|
|
354
|
+
}
|
|
355
|
+
Name::Plain => plain.push(n.clone()),
|
|
356
|
+
Name::Ref => refs.push(n.clone()),
|
|
357
|
+
});
|
|
358
|
+
// `x = v` must update a binder of the closure itself, never a captured variable.
|
|
359
|
+
if plain.iter().any(|n| !binders.contains(n)) {
|
|
360
|
+
return None;
|
|
361
|
+
}
|
|
362
|
+
let mut bad = false;
|
|
363
|
+
let mut usesSelf = false;
|
|
364
|
+
visitBlock(&mut c.body, &mut |e| match e {
|
|
365
|
+
Expr::Try(_) => bad = true,
|
|
366
|
+
Expr::Self_ => usesSelf = true,
|
|
367
|
+
Expr::FnCall(call) => {
|
|
368
|
+
if binders.contains(&call.name)
|
|
369
|
+
|| (locals.contains(&call.name)
|
|
370
|
+
&& !self.fns.get(&call.name).is_some_and(|f| f.type_param.is_none()))
|
|
371
|
+
{
|
|
372
|
+
bad = true;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
Expr::Yield(_) => bad = true,
|
|
376
|
+
_ => {}
|
|
377
|
+
});
|
|
378
|
+
if bad {
|
|
379
|
+
return None;
|
|
380
|
+
}
|
|
381
|
+
let mut caps: Vec<String> = Vec::new();
|
|
382
|
+
for n in refs {
|
|
383
|
+
if !binders.contains(&n) && locals.contains(&n) && !caps.contains(&n) {
|
|
384
|
+
caps.push(n);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if usesSelf {
|
|
388
|
+
caps.push("self".to_string());
|
|
389
|
+
}
|
|
390
|
+
Some(caps)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ---------- copies ----------
|
|
394
|
+
|
|
395
|
+
fn getCopy(&mut self, key: &str, j: usize, sid: usize) -> Option<String> {
|
|
396
|
+
let ck = (key.to_string(), j, sid);
|
|
397
|
+
if let Some(v) = self.copies.get(&ck) {
|
|
398
|
+
return v.clone();
|
|
399
|
+
}
|
|
400
|
+
let n = *self.perCallee.get(key).unwrap_or(&0);
|
|
401
|
+
if n >= MAX_COPIES {
|
|
402
|
+
return None;
|
|
403
|
+
}
|
|
404
|
+
let orig = self.fns.get(key)?.clone();
|
|
405
|
+
if orig.is_extern
|
|
406
|
+
|| orig.is_star
|
|
407
|
+
|| orig.is_export
|
|
408
|
+
|| orig.params.iter().any(|p| !matches!(p.ty, ParamType::Type(_) | ParamType::Fn(..)))
|
|
409
|
+
{
|
|
410
|
+
return None;
|
|
411
|
+
}
|
|
412
|
+
let name = format!("{}$c{}", orig.name, n);
|
|
413
|
+
let newKey = match &orig.type_param {
|
|
414
|
+
Some(t) => format!("{}.{}", t, name),
|
|
415
|
+
None => name.clone(),
|
|
416
|
+
};
|
|
417
|
+
self.perCallee.insert(key.to_string(), n + 1);
|
|
418
|
+
self.copies.insert(ck.clone(), Some(name.clone()));
|
|
419
|
+
let idx = self.out.len();
|
|
420
|
+
self.out.push(None);
|
|
421
|
+
self.outKeys.push(newKey.clone());
|
|
422
|
+
// Everything made from here on belongs to this copy until it succeeds.
|
|
423
|
+
let copiesBefore: HashSet<(String, usize, usize)> = self.copies.keys().cloned().collect();
|
|
424
|
+
let perBefore = self.perCallee.clone();
|
|
425
|
+
match self.buildCopy(orig, name.clone(), j, sid) {
|
|
426
|
+
Some(f) => {
|
|
427
|
+
if f.type_param.is_some() && f.has_self {
|
|
428
|
+
self.methodsByName.entry(f.name.clone()).or_default().push(newKey.clone());
|
|
429
|
+
}
|
|
430
|
+
let mut rest = self.ne.get(key).cloned().unwrap_or_default();
|
|
431
|
+
rest.retain(|&x| x != j);
|
|
432
|
+
if !rest.is_empty() {
|
|
433
|
+
self.ne.insert(newKey.clone(), rest);
|
|
434
|
+
}
|
|
435
|
+
self.fns.insert(newKey, f.clone());
|
|
436
|
+
self.out[idx] = Some(f);
|
|
437
|
+
Some(name)
|
|
438
|
+
}
|
|
439
|
+
None => {
|
|
440
|
+
// Undo this copy and every one made while building it.
|
|
441
|
+
for k in &self.outKeys[idx..] {
|
|
442
|
+
self.fns.remove(k);
|
|
443
|
+
self.ne.remove(k);
|
|
444
|
+
for v in self.methodsByName.values_mut() {
|
|
445
|
+
v.retain(|x| x != k);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
self.out.truncate(idx);
|
|
449
|
+
self.outKeys.truncate(idx);
|
|
450
|
+
let now: Vec<_> = self.copies.keys().cloned().collect();
|
|
451
|
+
for k in now {
|
|
452
|
+
if !copiesBefore.contains(&k) {
|
|
453
|
+
self.copies.remove(&k);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
self.perCallee = perBefore;
|
|
457
|
+
self.perCallee.insert(key.to_string(), n);
|
|
458
|
+
self.copies.insert(ck, None);
|
|
459
|
+
None
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
fn buildCopy(&mut self, orig: Fn, name: String, j: usize, sid: usize) -> Option<Fn> {
|
|
465
|
+
let spec = self.specs[sid].clone();
|
|
466
|
+
let mut f = orig;
|
|
467
|
+
f.name = name;
|
|
468
|
+
let p = f.params[j].name.clone();
|
|
469
|
+
let mut binders = HashSet::new();
|
|
470
|
+
collectBinders(&mut f.body, &mut binders);
|
|
471
|
+
if binders.contains(&p) {
|
|
472
|
+
return None;
|
|
473
|
+
}
|
|
474
|
+
if let Kind::Name(g) = &spec.kind {
|
|
475
|
+
if binders.contains(g) || f.params.iter().any(|q| &q.name == g) {
|
|
476
|
+
return None;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// Extra params for the captures, each a fresh generic type.
|
|
480
|
+
if !spec.caps.is_empty() {
|
|
481
|
+
let mut used = HashSet::new();
|
|
482
|
+
for q in &f.params {
|
|
483
|
+
paramTypeNames(&q.ty, &mut used);
|
|
484
|
+
}
|
|
485
|
+
if let Some(r) = &f.returns {
|
|
486
|
+
typeNames(r, &mut used);
|
|
487
|
+
}
|
|
488
|
+
bodyTypeNames(&mut f.body, &mut used);
|
|
489
|
+
let mut letters = ('A'..='Z').rev().map(|c| c.to_string()).filter(|l| !used.contains(l));
|
|
490
|
+
for i in 0..spec.caps.len() {
|
|
491
|
+
let letter = letters.next()?;
|
|
492
|
+
f.params.push(Param {
|
|
493
|
+
name: capName(sid, i),
|
|
494
|
+
ty: ParamType::Type(Type {
|
|
495
|
+
name: letter,
|
|
496
|
+
generics: vec![],
|
|
497
|
+
}),
|
|
498
|
+
default: None,
|
|
499
|
+
isOwn: false,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
let before = countVar(&mut f.body, &p);
|
|
504
|
+
let mut tx = Tx {
|
|
505
|
+
p: p.clone(),
|
|
506
|
+
spec: sid,
|
|
507
|
+
enc: Enc {
|
|
508
|
+
file: f.file.clone(),
|
|
509
|
+
ty: f.type_param.clone(),
|
|
510
|
+
},
|
|
511
|
+
handled: 0,
|
|
512
|
+
err: false,
|
|
513
|
+
};
|
|
514
|
+
let wasExpr = matches!(f.body, FnBody::Expr(_));
|
|
515
|
+
let mut block = match std::mem::replace(&mut f.body, FnBody::Extern) {
|
|
516
|
+
FnBody::Expr(e) => Block {
|
|
517
|
+
stmts: vec![Stmt::Expr(e, 0, 0)],
|
|
518
|
+
},
|
|
519
|
+
FnBody::Block(b) => b,
|
|
520
|
+
FnBody::Extern => return None,
|
|
521
|
+
};
|
|
522
|
+
self.txBlock(&mut tx, &mut block);
|
|
523
|
+
if tx.err || tx.handled != before {
|
|
524
|
+
return None;
|
|
525
|
+
}
|
|
526
|
+
f.body = if wasExpr && block.stmts.len() == 1 && matches!(block.stmts[0], Stmt::Expr(..)) {
|
|
527
|
+
match block.stmts.pop() {
|
|
528
|
+
Some(Stmt::Expr(e, ..)) => FnBody::Expr(e),
|
|
529
|
+
_ => return None,
|
|
530
|
+
}
|
|
531
|
+
} else {
|
|
532
|
+
FnBody::Block(block)
|
|
533
|
+
};
|
|
534
|
+
Some(f)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
fn txBlock(&mut self, tx: &mut Tx, b: &mut Block) {
|
|
538
|
+
let old = std::mem::take(&mut b.stmts);
|
|
539
|
+
let mut out = Vec::new();
|
|
540
|
+
for mut s in old {
|
|
541
|
+
if tx.err {
|
|
542
|
+
out.push(s);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
match self.spliceStmt(tx, &mut s) {
|
|
546
|
+
Some(v) => out.extend(v),
|
|
547
|
+
None => {
|
|
548
|
+
for e in stmtExprs(&mut s) {
|
|
549
|
+
visitExpr(e, &mut |x| self.txExpr(tx, x));
|
|
550
|
+
}
|
|
551
|
+
for blk in stmtBlocks(&mut s) {
|
|
552
|
+
self.txBlock(tx, blk);
|
|
553
|
+
}
|
|
554
|
+
out.push(s);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
b.stmts = out;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/// A statement that is exactly a call of the parameter (as an expression
|
|
562
|
+
/// statement, the value of a single assignment, or a `return`).
|
|
563
|
+
fn spliceStmt(&mut self, tx: &mut Tx, s: &mut Stmt) -> Option<Vec<Stmt>> {
|
|
564
|
+
let cl = match &self.specs[tx.spec].kind {
|
|
565
|
+
Kind::Lit(c) => c.clone(),
|
|
566
|
+
Kind::Name(_) => return None,
|
|
567
|
+
};
|
|
568
|
+
let call = match &*s {
|
|
569
|
+
Stmt::Expr(Expr::FnCall(c), ..) | Stmt::Return(Some(Expr::FnCall(c))) if c.name == tx.p => c.clone(),
|
|
570
|
+
Stmt::Assign(a) if a.targets.len() == 1 && a.values.len() == 1 => match &a.values[0] {
|
|
571
|
+
Expr::FnCall(c) if c.name == tx.p => c.clone(),
|
|
572
|
+
_ => return None,
|
|
573
|
+
},
|
|
574
|
+
_ => return None,
|
|
575
|
+
};
|
|
576
|
+
let mut args = Vec::new();
|
|
577
|
+
for a in call.args {
|
|
578
|
+
match a {
|
|
579
|
+
Arg::Positional(mut e) => {
|
|
580
|
+
visitExpr(&mut e, &mut |x| self.txExpr(tx, x));
|
|
581
|
+
args.push(e);
|
|
582
|
+
}
|
|
583
|
+
Arg::Keyword { .. } => {
|
|
584
|
+
tx.err = true;
|
|
585
|
+
return Some(vec![]);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
let Some((binds, mut body)) = self.instantiate(&cl, tx.spec, args, false) else {
|
|
590
|
+
tx.err = true;
|
|
591
|
+
return Some(vec![]);
|
|
592
|
+
};
|
|
593
|
+
let mut out = binds;
|
|
594
|
+
match s {
|
|
595
|
+
Stmt::Expr(..) => out.extend(body),
|
|
596
|
+
Stmt::Return(_) | Stmt::Assign(_) => {
|
|
597
|
+
match body.pop() {
|
|
598
|
+
Some(Stmt::Expr(e, ..)) => {
|
|
599
|
+
out.extend(body);
|
|
600
|
+
match s {
|
|
601
|
+
Stmt::Return(_) => out.push(Stmt::Return(Some(e))),
|
|
602
|
+
Stmt::Assign(a) => {
|
|
603
|
+
let mut a = a.clone();
|
|
604
|
+
a.values = vec![e];
|
|
605
|
+
out.push(Stmt::Assign(a));
|
|
606
|
+
}
|
|
607
|
+
_ => unreachable!(),
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
_ => {
|
|
611
|
+
tx.err = true;
|
|
612
|
+
return Some(vec![]);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
_ => unreachable!(),
|
|
617
|
+
}
|
|
618
|
+
Some(out)
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
fn txExpr(&mut self, tx: &mut Tx, e: &mut Expr) {
|
|
622
|
+
if tx.err {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
match e {
|
|
626
|
+
Expr::FnCall(c) => {
|
|
627
|
+
if c.name == tx.p {
|
|
628
|
+
let call = c.clone();
|
|
629
|
+
match self.inlineExpr(tx, &call) {
|
|
630
|
+
Some(new) => *e = new,
|
|
631
|
+
None => tx.err = true,
|
|
632
|
+
}
|
|
633
|
+
} else if hasPArg(c, &tx.p) {
|
|
634
|
+
self.passThrough(tx, None, c);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
Expr::Attribute(a) => {
|
|
638
|
+
let AttributeExpr { object, attr } = &mut **a;
|
|
639
|
+
if let AttrKind::Method(c) = attr {
|
|
640
|
+
if hasPArg(c, &tx.p) {
|
|
641
|
+
self.passThrough(tx, Some(object), c);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
_ => {}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
fn passThrough(&mut self, tx: &mut Tx, object: Option<&Expr>, c: &mut FnCall) {
|
|
650
|
+
let fail = |tx: &mut Tx| tx.err = true;
|
|
651
|
+
if c.args.iter().any(|a| matches!(a, Arg::Keyword { .. })) {
|
|
652
|
+
return fail(tx);
|
|
653
|
+
}
|
|
654
|
+
let Some(j) = c
|
|
655
|
+
.args
|
|
656
|
+
.iter()
|
|
657
|
+
.position(|a| matches!(a, Arg::Positional(Expr::Var(n)) if *n == tx.p))
|
|
658
|
+
else {
|
|
659
|
+
return fail(tx);
|
|
660
|
+
};
|
|
661
|
+
let Some(key) = self.resolve(&tx.enc, object, c) else {
|
|
662
|
+
return fail(tx);
|
|
663
|
+
};
|
|
664
|
+
let ok = self.fns.get(&key).is_some_and(|g| g.params.len() == c.args.len())
|
|
665
|
+
&& self.ne.get(&key).is_some_and(|v| v.contains(&j));
|
|
666
|
+
if !ok {
|
|
667
|
+
return fail(tx);
|
|
668
|
+
}
|
|
669
|
+
let Some(name) = self.getCopy(&key, j, tx.spec) else {
|
|
670
|
+
return fail(tx);
|
|
671
|
+
};
|
|
672
|
+
c.name = name;
|
|
673
|
+
for i in 0..self.specs[tx.spec].caps.len() {
|
|
674
|
+
c.args.push(Arg::Positional(Expr::Var(capName(tx.spec, i))));
|
|
675
|
+
}
|
|
676
|
+
tx.handled += 1;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/// The expression a call `p(args)` becomes.
|
|
680
|
+
fn inlineExpr(&mut self, tx: &Tx, call: &FnCall) -> Option<Expr> {
|
|
681
|
+
let mut args = Vec::new();
|
|
682
|
+
for a in &call.args {
|
|
683
|
+
match a {
|
|
684
|
+
Arg::Positional(e) => args.push(e.clone()),
|
|
685
|
+
Arg::Keyword { .. } => return None,
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
let cl = match &self.specs[tx.spec].kind {
|
|
689
|
+
Kind::Name(g) => {
|
|
690
|
+
let mut c = call.clone();
|
|
691
|
+
c.name = g.clone();
|
|
692
|
+
return Some(Expr::FnCall(c));
|
|
693
|
+
}
|
|
694
|
+
Kind::Lit(c) => c.clone(),
|
|
695
|
+
};
|
|
696
|
+
let (binds, mut body) = self.instantiate(&cl, tx.spec, args, true)?;
|
|
697
|
+
if !binds.is_empty() || body.len() != 1 {
|
|
698
|
+
return None;
|
|
699
|
+
}
|
|
700
|
+
match body.pop() {
|
|
701
|
+
Some(Stmt::Expr(e, ..)) => Some(e),
|
|
702
|
+
_ => None,
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/// The closure body with its binders renamed fresh, captures replaced by the copy's
|
|
707
|
+
/// capture params and trivial arguments substituted; the other arguments are bound to
|
|
708
|
+
/// fresh locals (the returned statements). In `exprMode` they cannot be bound, so each
|
|
709
|
+
/// must be used exactly once, unconditionally (and at most one is non-trivial).
|
|
710
|
+
fn instantiate(
|
|
711
|
+
&mut self,
|
|
712
|
+
cl: &Closure,
|
|
713
|
+
sid: usize,
|
|
714
|
+
args: Vec<Expr>,
|
|
715
|
+
exprMode: bool,
|
|
716
|
+
) -> Option<(Vec<Stmt>, Vec<Stmt>)> {
|
|
717
|
+
if cl.params.len() != args.len() {
|
|
718
|
+
return None;
|
|
719
|
+
}
|
|
720
|
+
let caps = self.specs[sid].caps.clone();
|
|
721
|
+
let k = self.fresh;
|
|
722
|
+
self.fresh += 1;
|
|
723
|
+
let mut body = cl.body.clone();
|
|
724
|
+
let mut binders: HashSet<String> = cl.params.iter().cloned().collect();
|
|
725
|
+
let mut assigned: HashSet<String> = HashSet::new();
|
|
726
|
+
renameStmts(&mut body.stmts, &mut |n, kind| match kind {
|
|
727
|
+
Name::Decl => {
|
|
728
|
+
binders.insert(n.clone());
|
|
729
|
+
}
|
|
730
|
+
Name::Plain => {
|
|
731
|
+
assigned.insert(n.clone());
|
|
732
|
+
}
|
|
733
|
+
Name::Ref => {}
|
|
734
|
+
});
|
|
735
|
+
let mut map: HashMap<String, String> = HashMap::new();
|
|
736
|
+
for b in &binders {
|
|
737
|
+
map.insert(b.clone(), format!("__i{}_{}", k, b));
|
|
738
|
+
}
|
|
739
|
+
for (i, c) in caps.iter().enumerate() {
|
|
740
|
+
if c != "self" {
|
|
741
|
+
map.insert(c.clone(), capName(sid, i));
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
renameStmts(&mut body.stmts, &mut |n, _| {
|
|
745
|
+
if let Some(new) = map.get(n.as_str()) {
|
|
746
|
+
*n = new.clone();
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
if let Some(i) = caps.iter().position(|c| c == "self") {
|
|
750
|
+
let selfCap = capName(sid, i);
|
|
751
|
+
visitBlock(&mut body, &mut |e| {
|
|
752
|
+
if matches!(e, Expr::Self_) {
|
|
753
|
+
*e = Expr::Var(selfCap.clone());
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
let fresh: Vec<String> = cl.params.iter().map(|p| map[p].clone()).collect();
|
|
758
|
+
let mut subst: HashMap<String, Expr> = HashMap::new();
|
|
759
|
+
let mut binds = Vec::new();
|
|
760
|
+
let mut nontrivial = 0;
|
|
761
|
+
for (i, a) in args.into_iter().enumerate() {
|
|
762
|
+
if trivial(&a) && !assigned.contains(&cl.params[i]) && !assigned.contains(&fresh[i]) {
|
|
763
|
+
subst.insert(fresh[i].clone(), a);
|
|
764
|
+
} else if exprMode {
|
|
765
|
+
nontrivial += 1;
|
|
766
|
+
if nontrivial > 1 || body.stmts.len() != 1 {
|
|
767
|
+
return None;
|
|
768
|
+
}
|
|
769
|
+
let Stmt::Expr(be, ..) = &mut body.stmts[0] else { return None };
|
|
770
|
+
let (total, lazy) = usesOf(be, &fresh[i]);
|
|
771
|
+
if total != 1 || lazy != 0 {
|
|
772
|
+
return None;
|
|
773
|
+
}
|
|
774
|
+
subst.insert(fresh[i].clone(), a);
|
|
775
|
+
} else {
|
|
776
|
+
binds.push(Stmt::Assign(Assign {
|
|
777
|
+
targets: vec![AssignTarget::Var(fresh[i].clone())],
|
|
778
|
+
values: vec![a],
|
|
779
|
+
declare: true,
|
|
780
|
+
ty: cl.param_types.get(i).cloned().flatten(),
|
|
781
|
+
}));
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if !subst.is_empty() {
|
|
785
|
+
visitBlock(&mut body, &mut |e| {
|
|
786
|
+
if let Expr::Var(n) = e {
|
|
787
|
+
if let Some(r) = subst.get(n.as_str()) {
|
|
788
|
+
*e = r.clone();
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
Some((binds, body.stmts))
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/// Clears every source position inside `b`.
|
|
798
|
+
fn zeroBlock(b: &mut Block) {
|
|
799
|
+
for s in &mut b.stmts {
|
|
800
|
+
match s {
|
|
801
|
+
Stmt::Expr(_, l, c) => {
|
|
802
|
+
*l = 0;
|
|
803
|
+
*c = 0;
|
|
804
|
+
}
|
|
805
|
+
Stmt::Assert(c) => {
|
|
806
|
+
c.line = 0;
|
|
807
|
+
c.column = 0;
|
|
808
|
+
c.text.clear();
|
|
809
|
+
}
|
|
810
|
+
_ => {}
|
|
811
|
+
}
|
|
812
|
+
for e in stmtExprs(s) {
|
|
813
|
+
visitExpr(e, &mut |x| match x {
|
|
814
|
+
Expr::FnCall(c) => {
|
|
815
|
+
c.line = 0;
|
|
816
|
+
c.column = 0;
|
|
817
|
+
}
|
|
818
|
+
Expr::Attribute(a) => {
|
|
819
|
+
if let AttrKind::Method(c) = &mut a.attr {
|
|
820
|
+
c.line = 0;
|
|
821
|
+
c.column = 0;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
Expr::Bang(b) => {
|
|
825
|
+
b.line = 0;
|
|
826
|
+
b.column = 0;
|
|
827
|
+
}
|
|
828
|
+
Expr::Catch(c) => {
|
|
829
|
+
c.line = 0;
|
|
830
|
+
c.column = 0;
|
|
831
|
+
}
|
|
832
|
+
Expr::ErrorValue(v) => {
|
|
833
|
+
v.line = 0;
|
|
834
|
+
v.column = 0;
|
|
835
|
+
}
|
|
836
|
+
Expr::Closure(c) => zeroBlock(&mut c.body),
|
|
837
|
+
_ => {}
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
for blk in stmtBlocks(s) {
|
|
841
|
+
zeroBlock(blk);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
fn hasPArg(c: &FnCall, p: &str) -> bool {
|
|
847
|
+
c.args.iter().any(|a| match a {
|
|
848
|
+
Arg::Positional(Expr::Var(n)) | Arg::Keyword { value: Expr::Var(n), .. } => n == p,
|
|
849
|
+
_ => false,
|
|
850
|
+
})
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
fn trivial(e: &Expr) -> bool {
|
|
854
|
+
matches!(e, Expr::Var(_) | Expr::Int(_) | Expr::Float(_) | Expr::BoolLit(_) | Expr::Self_)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/// (all occurrences of `name`, those in a lazily evaluated position) in `e`.
|
|
858
|
+
fn usesOf(e: &Expr, name: &str) -> (usize, usize) {
|
|
859
|
+
let mut tmp = e.clone();
|
|
860
|
+
let mut total = 0;
|
|
861
|
+
let mut lazy = 0;
|
|
862
|
+
visitExpr(&mut tmp, &mut |x| {
|
|
863
|
+
if matches!(x, Expr::Var(n) if n == name) {
|
|
864
|
+
total += 1;
|
|
865
|
+
}
|
|
866
|
+
for l in lazyChildren(x) {
|
|
867
|
+
visitExpr(l, &mut |y| {
|
|
868
|
+
if matches!(y, Expr::Var(n) if n == name) {
|
|
869
|
+
lazy += 1;
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
match x {
|
|
874
|
+
Expr::Closure(c) => visitBlock(&mut c.body, &mut |y| {
|
|
875
|
+
if matches!(y, Expr::Var(n) if n == name) {
|
|
876
|
+
lazy += 1;
|
|
877
|
+
}
|
|
878
|
+
}),
|
|
879
|
+
Expr::Catch(c) => {
|
|
880
|
+
if let CatchHandler::Block { body, .. } = &mut c.handler {
|
|
881
|
+
visitBlock(body, &mut |y| {
|
|
882
|
+
if matches!(y, Expr::Var(n) if n == name) {
|
|
883
|
+
lazy += 1;
|
|
884
|
+
}
|
|
885
|
+
})
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
_ => {}
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
(total, lazy)
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
fn lazyChildren(e: &mut Expr) -> Vec<&mut Expr> {
|
|
895
|
+
match e {
|
|
896
|
+
Expr::Bool(b) => vec![&mut b.right],
|
|
897
|
+
Expr::InlineIf(i) => vec![&mut i.then, &mut i.else_],
|
|
898
|
+
Expr::Catch(c) => match &mut c.handler {
|
|
899
|
+
CatchHandler::Default(d) => vec![d],
|
|
900
|
+
CatchHandler::Block { .. } => vec![],
|
|
901
|
+
},
|
|
902
|
+
Expr::Bind(b) => b.case.guard.iter_mut().collect(),
|
|
903
|
+
_ => vec![],
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
fn countVar(body: &mut FnBody, name: &str) -> usize {
|
|
908
|
+
let mut n = 0;
|
|
909
|
+
let mut cb = |e: &mut Expr| {
|
|
910
|
+
if matches!(e, Expr::Var(v) if v == name) {
|
|
911
|
+
n += 1;
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
match body {
|
|
915
|
+
FnBody::Expr(e) => visitExpr(e, &mut cb),
|
|
916
|
+
FnBody::Block(b) => visitBlock(b, &mut cb),
|
|
917
|
+
FnBody::Extern => {}
|
|
918
|
+
}
|
|
919
|
+
n
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
fn anyReturn(stmts: &[Stmt]) -> bool {
|
|
923
|
+
stmts.iter().any(|s| match s {
|
|
924
|
+
Stmt::Return(_) => true,
|
|
925
|
+
Stmt::While(w) => anyReturn(&w.body.stmts),
|
|
926
|
+
Stmt::ForLoop(f) => anyReturn(&f.body.stmts),
|
|
927
|
+
Stmt::If(i) => {
|
|
928
|
+
anyReturn(&i.body.stmts)
|
|
929
|
+
|| i.else_ifs.iter().any(|e| anyReturn(&e.body.stmts))
|
|
930
|
+
|| i.else_.as_ref().is_some_and(|b| anyReturn(&b.stmts))
|
|
931
|
+
}
|
|
932
|
+
Stmt::Match(m) => m.cases.iter().any(|c| anyReturn(&c.body.stmts)),
|
|
933
|
+
_ => false,
|
|
934
|
+
})
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
fn typeNames(t: &Type, out: &mut HashSet<String>) {
|
|
938
|
+
out.insert(t.name.clone());
|
|
939
|
+
t.generics.iter().for_each(|g| typeNames(g, out));
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
fn paramTypeNames(t: &ParamType, out: &mut HashSet<String>) {
|
|
943
|
+
match t {
|
|
944
|
+
ParamType::Type(t) | ParamType::Variadic(t) | ParamType::Seq(t) => typeNames(t, out),
|
|
945
|
+
ParamType::Fn(ps, r) => {
|
|
946
|
+
ps.iter().for_each(|p| typeNames(p, out));
|
|
947
|
+
if let Some(r) = r {
|
|
948
|
+
typeNames(r, out);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/// Type names written inside a body (annotated locals, closure params, explicit generics).
|
|
955
|
+
fn bodyTypeNames(body: &mut FnBody, out: &mut HashSet<String>) {
|
|
956
|
+
let mut stmtCb = |e: &mut Expr| match e {
|
|
957
|
+
Expr::Closure(c) => c.param_types.iter().flatten().for_each(|t| typeNames(t, out)),
|
|
958
|
+
Expr::ClassCall(c) => c.generics.iter().for_each(|t| typeNames(t, out)),
|
|
959
|
+
_ => {}
|
|
960
|
+
};
|
|
961
|
+
match body {
|
|
962
|
+
FnBody::Expr(e) => visitExpr(e, &mut stmtCb),
|
|
963
|
+
FnBody::Block(b) => {
|
|
964
|
+
visitBlock(b, &mut stmtCb);
|
|
965
|
+
annotations(&b.stmts, out);
|
|
966
|
+
}
|
|
967
|
+
FnBody::Extern => {}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
fn annotations(stmts: &[Stmt], out: &mut HashSet<String>) {
|
|
972
|
+
for s in stmts {
|
|
973
|
+
match s {
|
|
974
|
+
Stmt::Assign(a) => {
|
|
975
|
+
if let Some(t) = &a.ty {
|
|
976
|
+
typeNames(t, out);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
Stmt::While(w) => annotations(&w.body.stmts, out),
|
|
980
|
+
Stmt::ForLoop(f) => annotations(&f.body.stmts, out),
|
|
981
|
+
Stmt::If(i) => {
|
|
982
|
+
annotations(&i.body.stmts, out);
|
|
983
|
+
i.else_ifs.iter().for_each(|e| annotations(&e.body.stmts, out));
|
|
984
|
+
if let Some(b) = &i.else_ {
|
|
985
|
+
annotations(&b.stmts, out);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
Stmt::Match(m) => m.cases.iter().for_each(|c| annotations(&c.body.stmts, out)),
|
|
989
|
+
_ => {}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
fn collectBinders(body: &mut FnBody, out: &mut HashSet<String>) {
|
|
995
|
+
let mut cb = |n: &mut String, k: Name| {
|
|
996
|
+
if matches!(k, Name::Decl) {
|
|
997
|
+
out.insert(n.clone());
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
match body {
|
|
1001
|
+
FnBody::Block(b) => renameStmts(&mut b.stmts, &mut cb),
|
|
1002
|
+
FnBody::Expr(e) => renameExpr(e, &mut cb),
|
|
1003
|
+
FnBody::Extern => {}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// ---------- mutable traversal ----------
|
|
1008
|
+
|
|
1009
|
+
fn visitBlock(b: &mut Block, f: &mut dyn FnMut(&mut Expr)) {
|
|
1010
|
+
for s in &mut b.stmts {
|
|
1011
|
+
for e in stmtExprs(s) {
|
|
1012
|
+
visitExpr(e, f);
|
|
1013
|
+
}
|
|
1014
|
+
for blk in stmtBlocks(s) {
|
|
1015
|
+
visitBlock(blk, f);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/// Post-order over `e`, into closure bodies and catch blocks too.
|
|
1021
|
+
fn visitExpr(e: &mut Expr, f: &mut dyn FnMut(&mut Expr)) {
|
|
1022
|
+
for c in childExprs(e) {
|
|
1023
|
+
visitExpr(c, f);
|
|
1024
|
+
}
|
|
1025
|
+
match e {
|
|
1026
|
+
Expr::Closure(c) => visitBlock(&mut c.body, f),
|
|
1027
|
+
Expr::Catch(c) => {
|
|
1028
|
+
if let CatchHandler::Block { body, .. } = &mut c.handler {
|
|
1029
|
+
visitBlock(body, f);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
_ => {}
|
|
1033
|
+
}
|
|
1034
|
+
f(e);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
fn argExpr(a: &mut Arg) -> &mut Expr {
|
|
1038
|
+
match a {
|
|
1039
|
+
Arg::Positional(e) | Arg::Keyword { value: e, .. } => e,
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
fn childExprs(e: &mut Expr) -> Vec<&mut Expr> {
|
|
1044
|
+
match e {
|
|
1045
|
+
Expr::Binary(b) => vec![&mut b.left, &mut b.right],
|
|
1046
|
+
Expr::Unary(u) => vec![&mut u.operand],
|
|
1047
|
+
Expr::Bool(b) => vec![&mut b.left, &mut b.right],
|
|
1048
|
+
Expr::Not(x) | Expr::Try(x) | Expr::Paren(x) => vec![x.as_mut()],
|
|
1049
|
+
Expr::Bang(b) => vec![&mut b.value],
|
|
1050
|
+
Expr::Bind(b) => {
|
|
1051
|
+
let mut v = vec![&mut b.value];
|
|
1052
|
+
v.extend(b.case.guard.iter_mut());
|
|
1053
|
+
v
|
|
1054
|
+
}
|
|
1055
|
+
Expr::Catch(c) => {
|
|
1056
|
+
let CatchExpr { value, handler, .. } = &mut **c;
|
|
1057
|
+
match handler {
|
|
1058
|
+
CatchHandler::Default(d) => vec![value, d],
|
|
1059
|
+
CatchHandler::Block { .. } => vec![value],
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
Expr::Compare(c) => vec![&mut c.left, &mut c.right],
|
|
1063
|
+
Expr::InlineIf(t) => vec![&mut t.condition, &mut t.then, &mut t.else_],
|
|
1064
|
+
Expr::FnCall(c) => c.args.iter_mut().map(argExpr).collect(),
|
|
1065
|
+
Expr::Yield(vs) => vs.iter_mut().collect(),
|
|
1066
|
+
Expr::ClassCall(c) => {
|
|
1067
|
+
let mut v: Vec<&mut Expr> = c.fields.iter_mut().map(|f| &mut f.value).collect();
|
|
1068
|
+
if let Some(s) = &mut c.spread {
|
|
1069
|
+
v.push(s.as_mut());
|
|
1070
|
+
}
|
|
1071
|
+
v
|
|
1072
|
+
}
|
|
1073
|
+
Expr::Attribute(a) => {
|
|
1074
|
+
let AttributeExpr { object, attr } = &mut **a;
|
|
1075
|
+
let mut v = vec![object];
|
|
1076
|
+
if let AttrKind::Method(c) = attr {
|
|
1077
|
+
v.extend(c.args.iter_mut().map(argExpr));
|
|
1078
|
+
}
|
|
1079
|
+
v
|
|
1080
|
+
}
|
|
1081
|
+
Expr::String(s) => s
|
|
1082
|
+
.parts
|
|
1083
|
+
.iter_mut()
|
|
1084
|
+
.filter_map(|p| match p {
|
|
1085
|
+
StringPart::Interp(e) => Some(e),
|
|
1086
|
+
StringPart::Text(_) => None,
|
|
1087
|
+
})
|
|
1088
|
+
.collect(),
|
|
1089
|
+
Expr::Closure(_)
|
|
1090
|
+
| Expr::Int(_)
|
|
1091
|
+
| Expr::Float(_)
|
|
1092
|
+
| Expr::BoolLit(_)
|
|
1093
|
+
| Expr::Self_
|
|
1094
|
+
| Expr::ErrorValue(_)
|
|
1095
|
+
| Expr::Var(_)
|
|
1096
|
+
| Expr::TypeName(_) => vec![],
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
fn stmtExprs(s: &mut Stmt) -> Vec<&mut Expr> {
|
|
1101
|
+
match s {
|
|
1102
|
+
Stmt::Assign(a) => {
|
|
1103
|
+
let mut v: Vec<&mut Expr> = Vec::new();
|
|
1104
|
+
for t in &mut a.targets {
|
|
1105
|
+
if let AssignTarget::Field(obj, _) = t {
|
|
1106
|
+
v.push(obj.as_mut());
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
v.extend(a.values.iter_mut());
|
|
1110
|
+
v
|
|
1111
|
+
}
|
|
1112
|
+
Stmt::Assert(c) => vec![&mut c.cond],
|
|
1113
|
+
Stmt::Crash(e) | Stmt::Expr(e, ..) => vec![e],
|
|
1114
|
+
Stmt::Return(e) => e.iter_mut().collect(),
|
|
1115
|
+
Stmt::ForLoop(f) => {
|
|
1116
|
+
let mut v: Vec<&mut Expr> = vec![&mut f.source];
|
|
1117
|
+
for c in &mut f.clauses {
|
|
1118
|
+
match c {
|
|
1119
|
+
ForClause::If(e) | ForClause::Bind(_, e) => v.push(e),
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
v
|
|
1123
|
+
}
|
|
1124
|
+
Stmt::While(w) => vec![&mut w.condition],
|
|
1125
|
+
Stmt::If(i) => {
|
|
1126
|
+
let mut v = vec![&mut i.condition];
|
|
1127
|
+
v.extend(i.else_ifs.iter_mut().map(|e| &mut e.condition));
|
|
1128
|
+
v
|
|
1129
|
+
}
|
|
1130
|
+
Stmt::Match(m) => {
|
|
1131
|
+
let mut v: Vec<&mut Expr> = m.subjects.iter_mut().collect();
|
|
1132
|
+
v.extend(m.cases.iter_mut().filter_map(|c| c.guard.as_mut()));
|
|
1133
|
+
v
|
|
1134
|
+
}
|
|
1135
|
+
Stmt::Break | Stmt::Continue | Stmt::Todo => vec![],
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
fn stmtBlocks(s: &mut Stmt) -> Vec<&mut Block> {
|
|
1140
|
+
match s {
|
|
1141
|
+
Stmt::ForLoop(f) => vec![&mut f.body],
|
|
1142
|
+
Stmt::While(w) => vec![&mut w.body],
|
|
1143
|
+
Stmt::If(i) => {
|
|
1144
|
+
let mut v = vec![&mut i.body];
|
|
1145
|
+
v.extend(i.else_ifs.iter_mut().map(|e| &mut e.body));
|
|
1146
|
+
v.extend(i.else_.iter_mut());
|
|
1147
|
+
v
|
|
1148
|
+
}
|
|
1149
|
+
Stmt::Match(m) => m.cases.iter_mut().map(|c| &mut c.body).collect(),
|
|
1150
|
+
_ => vec![],
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
#[derive(Clone, Copy)]
|
|
1155
|
+
enum Name {
|
|
1156
|
+
/// A declaration site: `x := v`, a loop binder, a pattern name, a closure param.
|
|
1157
|
+
Decl,
|
|
1158
|
+
/// The target of a plain `x = v`.
|
|
1159
|
+
Plain,
|
|
1160
|
+
/// A use.
|
|
1161
|
+
Ref,
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
fn renameStmts(stmts: &mut [Stmt], rn: &mut dyn FnMut(&mut String, Name)) {
|
|
1165
|
+
for s in stmts {
|
|
1166
|
+
match s {
|
|
1167
|
+
Stmt::Assign(a) => {
|
|
1168
|
+
let kind = if a.declare { Name::Decl } else { Name::Plain };
|
|
1169
|
+
for t in &mut a.targets {
|
|
1170
|
+
if let AssignTarget::Var(n) = t {
|
|
1171
|
+
rn(n, kind);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
Stmt::ForLoop(f) => {
|
|
1176
|
+
f.binders.iter_mut().for_each(|v| rn(v, Name::Decl));
|
|
1177
|
+
for c in &mut f.clauses {
|
|
1178
|
+
if let ForClause::Bind(n, _) = c {
|
|
1179
|
+
rn(n, Name::Decl);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
if let Some((n, _)) = &mut f.result {
|
|
1183
|
+
rn(n, Name::Decl);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
Stmt::Match(m) => {
|
|
1187
|
+
for c in &mut m.cases {
|
|
1188
|
+
for p in &mut c.patterns {
|
|
1189
|
+
renamePattern(p, rn);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
_ => {}
|
|
1194
|
+
}
|
|
1195
|
+
for e in stmtExprs(s) {
|
|
1196
|
+
renameExpr(e, rn);
|
|
1197
|
+
}
|
|
1198
|
+
for b in stmtBlocks(s) {
|
|
1199
|
+
renameStmts(&mut b.stmts, rn);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
fn renamePattern(p: &mut CasePattern, rn: &mut dyn FnMut(&mut String, Name)) {
|
|
1205
|
+
match p {
|
|
1206
|
+
CasePattern::Name(n) => rn(n, Name::Decl),
|
|
1207
|
+
CasePattern::Class { fields, .. } => fields.iter_mut().for_each(|f| renamePattern(f, rn)),
|
|
1208
|
+
_ => {}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
fn renameExpr(e: &mut Expr, rn: &mut dyn FnMut(&mut String, Name)) {
|
|
1213
|
+
match e {
|
|
1214
|
+
Expr::Var(n) => rn(n, Name::Ref),
|
|
1215
|
+
Expr::Bind(b) => renamePattern(&mut b.case.patterns[0], rn),
|
|
1216
|
+
Expr::Closure(c) => {
|
|
1217
|
+
c.params.iter_mut().for_each(|p| rn(p, Name::Decl));
|
|
1218
|
+
renameStmts(&mut c.body.stmts, rn);
|
|
1219
|
+
}
|
|
1220
|
+
Expr::Catch(c) => {
|
|
1221
|
+
if let CatchHandler::Block { binder, body } = &mut c.handler {
|
|
1222
|
+
rn(binder, Name::Decl);
|
|
1223
|
+
renameStmts(&mut body.stmts, rn);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
_ => {}
|
|
1227
|
+
}
|
|
1228
|
+
for ch in childExprs(e) {
|
|
1229
|
+
renameExpr(ch, rn);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// ---------- shared traversal ----------
|
|
1234
|
+
|
|
1235
|
+
fn walkBlockRef(b: &Block, f: &mut dyn FnMut(&Expr)) {
|
|
1236
|
+
for s in &b.stmts {
|
|
1237
|
+
for e in stmtExprRefs(s) {
|
|
1238
|
+
walkRef(e, f);
|
|
1239
|
+
}
|
|
1240
|
+
for blk in stmtBlockRefs(s) {
|
|
1241
|
+
walkBlockRef(blk, f);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
fn walkRef(e: &Expr, f: &mut dyn FnMut(&Expr)) {
|
|
1247
|
+
f(e);
|
|
1248
|
+
for c in childRefs(e) {
|
|
1249
|
+
walkRef(c, f);
|
|
1250
|
+
}
|
|
1251
|
+
match e {
|
|
1252
|
+
Expr::Closure(c) => walkBlockRef(&c.body, f),
|
|
1253
|
+
Expr::Catch(c) => {
|
|
1254
|
+
if let CatchHandler::Block { body, .. } = &c.handler {
|
|
1255
|
+
walkBlockRef(body, f);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
_ => {}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
fn argRef(a: &Arg) -> &Expr {
|
|
1263
|
+
match a {
|
|
1264
|
+
Arg::Positional(e) | Arg::Keyword { value: e, .. } => e,
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
fn childRefs(e: &Expr) -> Vec<&Expr> {
|
|
1269
|
+
match e {
|
|
1270
|
+
Expr::Binary(b) => vec![&b.left, &b.right],
|
|
1271
|
+
Expr::Unary(u) => vec![&u.operand],
|
|
1272
|
+
Expr::Bool(b) => vec![&b.left, &b.right],
|
|
1273
|
+
Expr::Not(x) | Expr::Try(x) | Expr::Paren(x) => vec![x.as_ref()],
|
|
1274
|
+
Expr::Bang(b) => vec![&b.value],
|
|
1275
|
+
Expr::Bind(b) => {
|
|
1276
|
+
let mut v = vec![&b.value];
|
|
1277
|
+
v.extend(b.case.guard.iter());
|
|
1278
|
+
v
|
|
1279
|
+
}
|
|
1280
|
+
Expr::Catch(c) => match &c.handler {
|
|
1281
|
+
CatchHandler::Default(d) => vec![&c.value, d],
|
|
1282
|
+
CatchHandler::Block { .. } => vec![&c.value],
|
|
1283
|
+
},
|
|
1284
|
+
Expr::Compare(c) => vec![&c.left, &c.right],
|
|
1285
|
+
Expr::InlineIf(t) => vec![&t.condition, &t.then, &t.else_],
|
|
1286
|
+
Expr::FnCall(c) => c.args.iter().map(argRef).collect(),
|
|
1287
|
+
Expr::Yield(vs) => vs.iter().collect(),
|
|
1288
|
+
Expr::ClassCall(c) => {
|
|
1289
|
+
let mut v: Vec<&Expr> = c.fields.iter().map(|f| &f.value).collect();
|
|
1290
|
+
if let Some(s) = &c.spread {
|
|
1291
|
+
v.push(s.as_ref());
|
|
1292
|
+
}
|
|
1293
|
+
v
|
|
1294
|
+
}
|
|
1295
|
+
Expr::Attribute(a) => {
|
|
1296
|
+
let mut v = vec![&a.object];
|
|
1297
|
+
if let AttrKind::Method(c) = &a.attr {
|
|
1298
|
+
v.extend(c.args.iter().map(argRef));
|
|
1299
|
+
}
|
|
1300
|
+
v
|
|
1301
|
+
}
|
|
1302
|
+
Expr::String(s) => s
|
|
1303
|
+
.parts
|
|
1304
|
+
.iter()
|
|
1305
|
+
.filter_map(|p| match p {
|
|
1306
|
+
StringPart::Interp(e) => Some(e),
|
|
1307
|
+
StringPart::Text(_) => None,
|
|
1308
|
+
})
|
|
1309
|
+
.collect(),
|
|
1310
|
+
Expr::Closure(_)
|
|
1311
|
+
| Expr::Int(_)
|
|
1312
|
+
| Expr::Float(_)
|
|
1313
|
+
| Expr::BoolLit(_)
|
|
1314
|
+
| Expr::Self_
|
|
1315
|
+
| Expr::ErrorValue(_)
|
|
1316
|
+
| Expr::Var(_)
|
|
1317
|
+
| Expr::TypeName(_) => vec![],
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
fn stmtExprRefs(s: &Stmt) -> Vec<&Expr> {
|
|
1322
|
+
match s {
|
|
1323
|
+
Stmt::Assign(a) => {
|
|
1324
|
+
let mut v: Vec<&Expr> = Vec::new();
|
|
1325
|
+
for t in &a.targets {
|
|
1326
|
+
if let AssignTarget::Field(obj, _) = t {
|
|
1327
|
+
v.push(obj.as_ref());
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
v.extend(a.values.iter());
|
|
1331
|
+
v
|
|
1332
|
+
}
|
|
1333
|
+
Stmt::Assert(c) => vec![&c.cond],
|
|
1334
|
+
Stmt::Crash(e) | Stmt::Expr(e, ..) => vec![e],
|
|
1335
|
+
Stmt::Return(e) => e.iter().collect(),
|
|
1336
|
+
Stmt::ForLoop(f) => {
|
|
1337
|
+
let mut v: Vec<&Expr> = vec![&f.source];
|
|
1338
|
+
for c in &f.clauses {
|
|
1339
|
+
match c {
|
|
1340
|
+
ForClause::If(e) | ForClause::Bind(_, e) => v.push(e),
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
v
|
|
1344
|
+
}
|
|
1345
|
+
Stmt::While(w) => vec![&w.condition],
|
|
1346
|
+
Stmt::If(i) => {
|
|
1347
|
+
let mut v = vec![&i.condition];
|
|
1348
|
+
v.extend(i.else_ifs.iter().map(|e| &e.condition));
|
|
1349
|
+
v
|
|
1350
|
+
}
|
|
1351
|
+
Stmt::Match(m) => {
|
|
1352
|
+
let mut v: Vec<&Expr> = m.subjects.iter().collect();
|
|
1353
|
+
v.extend(m.cases.iter().filter_map(|c| c.guard.as_ref()));
|
|
1354
|
+
v
|
|
1355
|
+
}
|
|
1356
|
+
Stmt::Break | Stmt::Continue | Stmt::Todo => vec![],
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
fn stmtBlockRefs(s: &Stmt) -> Vec<&Block> {
|
|
1361
|
+
match s {
|
|
1362
|
+
Stmt::ForLoop(f) => vec![&f.body],
|
|
1363
|
+
Stmt::While(w) => vec![&w.body],
|
|
1364
|
+
Stmt::If(i) => {
|
|
1365
|
+
let mut v = vec![&i.body];
|
|
1366
|
+
v.extend(i.else_ifs.iter().map(|e| &e.body));
|
|
1367
|
+
v.extend(i.else_.iter());
|
|
1368
|
+
v
|
|
1369
|
+
}
|
|
1370
|
+
Stmt::Match(m) => m.cases.iter().map(|c| &c.body).collect(),
|
|
1371
|
+
_ => vec![],
|
|
1372
|
+
}
|
|
1373
|
+
}
|
plum-core/src/lib.rs
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
pub mod ast;
|
|
6
6
|
pub mod builtin_usage;
|
|
7
|
+
pub mod closure_escape;
|
|
8
|
+
pub mod closure_spec;
|
|
7
9
|
pub mod ctors;
|
|
8
10
|
pub mod embeds;
|
|
9
11
|
pub mod formatter;
|
plum-core/tests/closure_escape_test.rs
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
use plum_core::closure_escape::nonEscapingFnParams;
|
|
3
|
+
use plum_core::AstParser;
|
|
4
|
+
|
|
5
|
+
fn parse(src: &str) -> plum_core::ast::Source {
|
|
6
|
+
let mut parser = tree_sitter::Parser::new();
|
|
7
|
+
parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
|
|
8
|
+
let tree = parser.parse(src, None).unwrap();
|
|
9
|
+
assert!(!tree.root_node().has_error(), "{}", tree.root_node().to_sexp());
|
|
10
|
+
AstParser::new(src).parseSource(tree.root_node())
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
fn get(src: &str, key: &str) -> Vec<usize> {
|
|
14
|
+
nonEscapingFnParams(&parse(src)).get(key).cloned().unwrap_or_default()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#[test]
|
|
18
|
+
fn direct_call_param_non_escaping() {
|
|
19
|
+
assert_eq!(get("fun apply(x: Int, f: fn(Int) -> Int) -> Int = f(x)\n", "apply"), vec![1]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#[test]
|
|
23
|
+
fn recursive_same_param_non_escaping() {
|
|
24
|
+
let src = "\
|
|
25
|
+
enum Link[T] =
|
|
26
|
+
| Nil
|
|
27
|
+
| Cons(head: T, tail: Link[T])
|
|
28
|
+
|
|
29
|
+
fun mapCells(xs: Link[A], f: fn(A) -> B) -> Link[B] = match xs
|
|
30
|
+
Nil => Nil
|
|
31
|
+
Cons(h, t) => Cons(head: f(h), tail: mapCells(t, f))
|
|
32
|
+
";
|
|
33
|
+
assert_eq!(get(src, "mapCells"), vec![1]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#[test]
|
|
37
|
+
fn stored_in_field_escapes() {
|
|
38
|
+
let src = "\
|
|
39
|
+
record Box(cb: fn(Int) -> Int)
|
|
40
|
+
|
|
41
|
+
fun keep(f: fn(Int) -> Int) -> Box = Box(cb: f)
|
|
42
|
+
";
|
|
43
|
+
assert!(get(src, "keep").is_empty());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#[test]
|
|
47
|
+
fn returned_escapes() {
|
|
48
|
+
assert!(get("fun id(f: fn(Int) -> Int) -> fn(Int) -> Int = f\n", "id").is_empty());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[test]
|
|
52
|
+
fn captured_by_closure_escapes() {
|
|
53
|
+
let src = "fun wrap(f: fn(Int) -> Int) -> fn(Int) -> Int = |x| f(x)\n";
|
|
54
|
+
assert!(get(src, "wrap").is_empty());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#[test]
|
|
58
|
+
fn passed_to_escaping_param_escapes() {
|
|
59
|
+
let src = "\
|
|
60
|
+
record Box(cb: fn(Int) -> Int)
|
|
61
|
+
|
|
62
|
+
fun keep(f: fn(Int) -> Int) -> Box = Box(cb: f)
|
|
63
|
+
|
|
64
|
+
fun viaKeep(g: fn(Int) -> Int) -> Box = keep(g)
|
|
65
|
+
";
|
|
66
|
+
assert!(get(src, "viaKeep").is_empty());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[test]
|
|
70
|
+
fn passed_to_non_escaping_param_ok() {
|
|
71
|
+
let src = "\
|
|
72
|
+
fun apply(x: Int, f: fn(Int) -> Int) -> Int = f(x)
|
|
73
|
+
|
|
74
|
+
fun twice(g: fn(Int) -> Int) -> Int = apply(apply(1, g), g)
|
|
75
|
+
";
|
|
76
|
+
assert_eq!(get(src, "twice"), vec![0]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#[test]
|
|
80
|
+
fn method_keys() {
|
|
81
|
+
let src = "\
|
|
82
|
+
record Counter(n: Int) =
|
|
83
|
+
|
|
84
|
+
fun each(self, f: fn(Int) -> Int) -> Int = f(1)
|
|
85
|
+
|
|
86
|
+
fun make(f: fn(Int) -> Int) -> Int = f(2)
|
|
87
|
+
";
|
|
88
|
+
assert_eq!(get(src, "Counter.each"), vec![0]);
|
|
89
|
+
assert_eq!(get(src, "Counter.make"), vec![0]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#[test]
|
|
93
|
+
fn std_list_combinators_non_escaping() {
|
|
94
|
+
let dir = std::env::temp_dir().join(format!("plum_escape_{}", std::process::id()));
|
|
95
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
96
|
+
let path = dir.join("main.plum");
|
|
97
|
+
std::fs::write(&path, "import std/Int\nimport std/List\n\nfun main() =\n List(1)\n").unwrap();
|
|
98
|
+
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
|
|
99
|
+
let r = plum_core::loadAndMerge(&path, &root);
|
|
100
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
101
|
+
let m = nonEscapingFnParams(&r.unwrap_or_else(|e| panic!("load: {e}")));
|
|
102
|
+
assert_eq!(m.get("List.map"), Some(&vec![0]), "{m:?}");
|
|
103
|
+
assert_eq!(m.get("List.filter"), Some(&vec![0]));
|
|
104
|
+
assert_eq!(m.get("List.fold"), Some(&vec![1]));
|
|
105
|
+
assert_eq!(m.get("mapCells"), Some(&vec![1]));
|
|
106
|
+
assert_eq!(m.get("List.keepRev"), Some(&vec![0]));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#[test]
|
|
110
|
+
fn escapes_via_field_return_container() {
|
|
111
|
+
// One fn each: stores the param in a record field, returns it, puts it into a Slice (built from it, or pushed into one; parse-level only, the analysis does not type-check).
|
|
112
|
+
// `apply` is a non-escaping control so an empty answer cannot mean "nothing analysed".
|
|
113
|
+
let src = "\
|
|
114
|
+
record Box(cb: fn(Int) -> Int)
|
|
115
|
+
|
|
116
|
+
fun apply(x: Int, f: fn(Int) -> Int) -> Int = f(x)
|
|
117
|
+
|
|
118
|
+
fun inField(f: fn(Int) -> Int) -> Box = Box(cb: f)
|
|
119
|
+
|
|
120
|
+
fun inReturn(f: fn(Int) -> Int) -> fn(Int) -> Int = f
|
|
121
|
+
|
|
122
|
+
fun inSlice(f: fn(Int) -> Int) -> Int =
|
|
123
|
+
out := Slice(f)
|
|
124
|
+
out.length()
|
|
125
|
+
|
|
126
|
+
fun inPush(out: Slice[Box], f: fn(Int) -> Int) =
|
|
127
|
+
out.push(f)
|
|
128
|
+
";
|
|
129
|
+
let m = nonEscapingFnParams(&parse(src));
|
|
130
|
+
assert_eq!(m.get("apply"), Some(&vec![1]), "{m:?}");
|
|
131
|
+
for name in ["inField", "inReturn", "inSlice", "inPush"] {
|
|
132
|
+
assert!(m.get(name).map_or(true, |v| v.is_empty()), "{name} escapes: {m:?}");
|
|
133
|
+
}
|
|
134
|
+
}
|
plum-core/tests/closure_spec_test.rs
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
//! Closure specialisation (`plum_core::closure_spec`): which calls get a copy of the
|
|
3
|
+
//! callee with the closure inlined, and that the program still computes the same thing
|
|
4
|
+
//! (compiled and run through the real pipeline, where the pass is wired in).
|
|
5
|
+
use plum_wasm_codegen::compileSource;
|
|
6
|
+
|
|
7
|
+
fn gcEngine() -> wasmtime::Engine {
|
|
8
|
+
let mut config = wasmtime::Config::new();
|
|
9
|
+
config.wasm_gc(true);
|
|
10
|
+
config.wasm_function_references(true);
|
|
11
|
+
config.wasm_tail_call(true);
|
|
12
|
+
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fn repoRoot() -> std::path::PathBuf {
|
|
16
|
+
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fn load(name: &str, src: &str) -> plum_core::ast::Source {
|
|
20
|
+
let dir = std::env::temp_dir().join(format!("plum_cspec_{}_{}", name, std::process::id()));
|
|
21
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
22
|
+
let path = dir.join("main.plum");
|
|
23
|
+
std::fs::write(&path, src).unwrap();
|
|
24
|
+
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
25
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
26
|
+
r.unwrap_or_else(|e| panic!("load: {e}"))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn compile(name: &str, src: &str) -> Vec<u8> {
|
|
30
|
+
let source = load(name, src);
|
|
31
|
+
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
32
|
+
wasmparser::validate(&bytes).expect("valid wasm");
|
|
33
|
+
bytes
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fn runIntBytes(bytes: &[u8]) -> i64 {
|
|
37
|
+
let engine = gcEngine();
|
|
38
|
+
let module = wasmtime::Module::new(&engine, bytes).unwrap();
|
|
39
|
+
let mut store = wasmtime::Store::new(&engine, ());
|
|
40
|
+
let mut linker = wasmtime::Linker::new(&engine);
|
|
41
|
+
linker.define_unknown_imports_as_default_values(&mut store, &module).unwrap();
|
|
42
|
+
let instance = linker.instantiate(&mut store, &module).unwrap();
|
|
43
|
+
let main = instance.get_typed_func::<(), i64>(&mut store, "main").unwrap();
|
|
44
|
+
main.call(&mut store, ()).unwrap()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fn runInt(name: &str, src: &str) -> i64 {
|
|
48
|
+
runIntBytes(&compile(name, src))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fn lowered(name: &str, src: &str) -> std::rc::Rc<plum_core::ast::Source> {
|
|
52
|
+
let source = load(name, src);
|
|
53
|
+
let info = plum_checker::typedSource(&source).unwrap_or_else(|e| panic!("check: {e:?}"));
|
|
54
|
+
info.lowered.expect("lowered program")
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// The copies (`name$cN`) the checker's lowered program carries.
|
|
58
|
+
fn copies(name: &str, src: &str) -> Vec<String> {
|
|
59
|
+
lowered(name, src)
|
|
60
|
+
.items
|
|
61
|
+
.iter()
|
|
62
|
+
.filter_map(|i| match i {
|
|
63
|
+
plum_core::ast::Item::Fn(f) if f.name.contains("$c") => Some(match &f.type_param {
|
|
64
|
+
Some(t) => format!("{}.{}", t, f.name),
|
|
65
|
+
None => f.name.clone(),
|
|
66
|
+
}),
|
|
67
|
+
_ => None,
|
|
68
|
+
})
|
|
69
|
+
.collect()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Number of closure calls (`call_ref` / `call_indirect`) in the module.
|
|
73
|
+
fn callRefs(bytes: &[u8]) -> usize {
|
|
74
|
+
let mut n = 0;
|
|
75
|
+
for payload in wasmparser::Parser::new(0).parse_all(bytes) {
|
|
76
|
+
if let Ok(wasmparser::Payload::CodeSectionEntry(body)) = payload {
|
|
77
|
+
let mut r = body.get_operators_reader().unwrap();
|
|
78
|
+
while !r.eof() {
|
|
79
|
+
if matches!(
|
|
80
|
+
r.read().unwrap(),
|
|
81
|
+
wasmparser::Operator::CallRef { .. }
|
|
82
|
+
| wasmparser::Operator::ReturnCallRef { .. }
|
|
83
|
+
| wasmparser::Operator::CallIndirect { .. }
|
|
84
|
+
| wasmparser::Operator::ReturnCallIndirect { .. }
|
|
85
|
+
) {
|
|
86
|
+
n += 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
n
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Names of the functions containing a closure call (needs the name section).
|
|
95
|
+
fn callerNames(bytes: &[u8]) -> Vec<String> {
|
|
96
|
+
let mut names: std::collections::HashMap<u32, String> = Default::default();
|
|
97
|
+
let mut imports = 0u32;
|
|
98
|
+
let mut with: Vec<u32> = Vec::new();
|
|
99
|
+
let mut idx = 0u32;
|
|
100
|
+
for payload in wasmparser::Parser::new(0).parse_all(bytes) {
|
|
101
|
+
match payload.unwrap() {
|
|
102
|
+
wasmparser::Payload::ImportSection(r) => {
|
|
103
|
+
for i in r {
|
|
104
|
+
if matches!(i.unwrap().ty, wasmparser::TypeRef::Func(_)) {
|
|
105
|
+
imports += 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
idx = imports;
|
|
109
|
+
}
|
|
110
|
+
wasmparser::Payload::CodeSectionEntry(body) => {
|
|
111
|
+
let mut r = body.get_operators_reader().unwrap();
|
|
112
|
+
while !r.eof() {
|
|
113
|
+
if matches!(
|
|
114
|
+
r.read().unwrap(),
|
|
115
|
+
wasmparser::Operator::CallRef { .. }
|
|
116
|
+
| wasmparser::Operator::ReturnCallRef { .. }
|
|
117
|
+
| wasmparser::Operator::CallIndirect { .. }
|
|
118
|
+
| wasmparser::Operator::ReturnCallIndirect { .. }
|
|
119
|
+
) {
|
|
120
|
+
with.push(idx);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
idx += 1;
|
|
124
|
+
}
|
|
125
|
+
wasmparser::Payload::CustomSection(c) => {
|
|
126
|
+
if let wasmparser::KnownCustom::Name(r) = c.as_known() {
|
|
127
|
+
for n in r {
|
|
128
|
+
if let Ok(wasmparser::Name::Function(m)) = n {
|
|
129
|
+
for e in m.into_iter().flatten() {
|
|
130
|
+
names.insert(e.index, e.name.to_string());
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
_ => {}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
with.iter().map(|i| names.get(i).cloned().unwrap_or(format!("#{i}"))).collect()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// No List function (`map`, `fold`, `mapCells`, ...) of the module makes a closure call.
|
|
143
|
+
fn noListCalls(bytes: &[u8]) {
|
|
144
|
+
let bad: Vec<String> = callerNames(bytes)
|
|
145
|
+
.into_iter()
|
|
146
|
+
.filter(|n| n.contains("List") || n.contains("mapCells") || n.contains("fold") || n.contains("map"))
|
|
147
|
+
.collect();
|
|
148
|
+
assert!(bad.is_empty(), "closure calls left in {bad:?}");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const HEAD: &str = "\
|
|
152
|
+
import std/List
|
|
153
|
+
import std/Str
|
|
154
|
+
import std/Int
|
|
155
|
+
import std/Bool
|
|
156
|
+
|
|
157
|
+
";
|
|
158
|
+
|
|
159
|
+
fn withHead(body: &str) -> String {
|
|
160
|
+
format!("{HEAD}{body}")
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#[test]
|
|
164
|
+
fn map_literal_no_call_ref() {
|
|
165
|
+
let src = withHead(
|
|
166
|
+
"fun main() -> Int =\n xs := List(1, 2, 3, 4)\n return xs.map(|x| x * 2).fold(0, |a, x| a + x)\n",
|
|
167
|
+
);
|
|
168
|
+
let names = copies("map_lit", &src);
|
|
169
|
+
assert!(names.contains(&"List.map$c0".to_string()), "{names:?}");
|
|
170
|
+
assert!(names.contains(&"mapCells$c0".to_string()), "{names:?}");
|
|
171
|
+
let bytes = compile("map_lit", &src);
|
|
172
|
+
noListCalls(&bytes);
|
|
173
|
+
assert_eq!(runIntBytes(&bytes), 20);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
#[test]
|
|
177
|
+
fn capture_becomes_param() {
|
|
178
|
+
let src = withHead(
|
|
179
|
+
"fun main() -> Int =\n k := 10\n xs := List(1, 2, 3)\n return xs.map(|x| x + k).fold(0, |a, x| a + x)\n",
|
|
180
|
+
);
|
|
181
|
+
let names = copies("capture", &src);
|
|
182
|
+
assert!(names.contains(&"List.map$c0".to_string()), "{names:?}");
|
|
183
|
+
let bytes = compile("capture", &src);
|
|
184
|
+
noListCalls(&bytes);
|
|
185
|
+
assert_eq!(runIntBytes(&bytes), 36);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#[test]
|
|
189
|
+
fn recursive_copy_calls_itself() {
|
|
190
|
+
let src = withHead(
|
|
191
|
+
"fun main() -> Int =\n xs := List(1, 2, 3, 4, 5)\n return xs.fold(0, |a, x| a + x)\n",
|
|
192
|
+
);
|
|
193
|
+
let l = lowered("recursive", &src);
|
|
194
|
+
let f = l
|
|
195
|
+
.items
|
|
196
|
+
.iter()
|
|
197
|
+
.find_map(|i| match i {
|
|
198
|
+
plum_core::ast::Item::Fn(f) if f.name == "fold$c0" => Some(f),
|
|
199
|
+
_ => None,
|
|
200
|
+
})
|
|
201
|
+
.expect("fold$c0 emitted");
|
|
202
|
+
let text = format!("{:?}", f.body);
|
|
203
|
+
assert!(text.contains("fold$c0"), "recursion goes to the copy: {text}");
|
|
204
|
+
assert_eq!(runInt("recursive", &src), 15);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#[test]
|
|
208
|
+
fn fn_name_argument() {
|
|
209
|
+
let src = withHead(
|
|
210
|
+
"fun double(x: Int) -> Int = x * 2\n\nfun main() -> Int =\n xs := List(1, 2, 3)\n return xs.map(double).fold(0, |a, x| a + x)\n",
|
|
211
|
+
);
|
|
212
|
+
let names = copies("fnname", &src);
|
|
213
|
+
assert!(names.contains(&"List.map$c0".to_string()), "{names:?}");
|
|
214
|
+
let bytes = compile("fnname", &src);
|
|
215
|
+
noListCalls(&bytes);
|
|
216
|
+
assert_eq!(runIntBytes(&bytes), 12);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#[test]
|
|
220
|
+
fn escaping_closure_untouched() {
|
|
221
|
+
let src = withHead(
|
|
222
|
+
"record Box(cb: fn(Int) -> Int)\n\nfun keep(f: fn(Int) -> Int) -> Box = Box(cb: f)\n\nfun main() -> Int =\n b := keep(|x| x + 1)\n g := b.cb\n return g(41)\n",
|
|
223
|
+
);
|
|
224
|
+
assert!(copies("escaping", &src).is_empty());
|
|
225
|
+
assert_eq!(runInt("escaping", &src), 42);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
#[test]
|
|
229
|
+
fn dedup_same_closure_twice() {
|
|
230
|
+
let src = withHead(
|
|
231
|
+
"fun main() -> Int =\n xs := List(1, 2, 3)\n a := xs.map(|x| x * 2).fold(0, |a, x| a + x)\n b := xs.map(|x| x * 2).fold(0, |a, x| a + x)\n return a + b\n",
|
|
232
|
+
);
|
|
233
|
+
let names = copies("dedup", &src);
|
|
234
|
+
assert_eq!(names.iter().filter(|n| n.starts_with("List.map$")).count(), 1, "{names:?}");
|
|
235
|
+
assert_eq!(runInt("dedup", &src), 24);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#[test]
|
|
239
|
+
fn cap_eight_copies() {
|
|
240
|
+
let mut body = String::from("fun main() -> Int =\n xs := List(1, 2, 3)\n total := 0\n");
|
|
241
|
+
for i in 0..10 {
|
|
242
|
+
body.push_str(&format!(" total = total + xs.map(|x| x + {}).fold(0, |a, x| a + x)\n", i));
|
|
243
|
+
}
|
|
244
|
+
body.push_str(" return total\n");
|
|
245
|
+
let src = withHead(&body);
|
|
246
|
+
let names = copies("cap8", &src);
|
|
247
|
+
assert_eq!(names.iter().filter(|n| n.starts_with("List.map$")).count(), 8, "{names:?}");
|
|
248
|
+
// sum over i of (6 + 3i) = 60 + 3 * 45
|
|
249
|
+
assert_eq!(runInt("cap8", &src), 195);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#[test]
|
|
253
|
+
fn results_equal_original() {
|
|
254
|
+
let src = withHead(
|
|
255
|
+
"fun main() -> Int =\n xs := List(1, 2, 3, 4, 5, 6, 7, 8)\n k := 3\n a := xs.map(|x| x * k).filter(|x| x % 2 == 0).fold(0, |a, x| a + x)\n b := xs.filter(|x| x > k).map(|x| x - 1).fold(100, |a, x| a - x)\n c := xs.fold(1, |a, x| if x > 4 then a + x else a)\n return a * 10000 + b * 100 + c\n",
|
|
256
|
+
);
|
|
257
|
+
// Same program through closures held in locals, which are not specialised.
|
|
258
|
+
let plain = withHead(
|
|
259
|
+
"fun main() -> Int =\n xs := List(1, 2, 3, 4, 5, 6, 7, 8)\n k := 3\n m1 := |x| x * k\n f1 := |x| x % 2 == 0\n s1 := |a, x| a + x\n a := xs.map(m1).filter(f1).fold(0, s1)\n f2 := |x| x > k\n m2 := |x| x - 1\n s2 := |a, x| a - x\n b := xs.filter(f2).map(m2).fold(100, s2)\n s3 := |a, x| if x > 4 then a + x else a\n c := xs.fold(1, s3)\n return a * 10000 + b * 100 + c\n",
|
|
260
|
+
);
|
|
261
|
+
assert!(!copies("equal", &src).is_empty());
|
|
262
|
+
assert_eq!(runInt("equal", &src), runInt("equal_plain", &plain));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
#[test]
|
|
266
|
+
fn list_pipeline_million_timed() {
|
|
267
|
+
let src = withHead(
|
|
268
|
+
"fun build(n: Int) -> List[Int] =\n out: List[Int] := Nil\n i := n\n while i > 0\n out = Cons(head: i, tail: out)\n i = i - 1\n return out\n\nfun main() -> Int =\n xs := build(1000000)\n return xs.map(|x| x * 2).filter(|x| x % 4 == 0).fold(0, |a, x| a + x)\n",
|
|
269
|
+
);
|
|
270
|
+
let bytes = compile("million", &src);
|
|
271
|
+
let t = std::time::Instant::now();
|
|
272
|
+
let r = runIntBytes(&bytes);
|
|
273
|
+
eprintln!(
|
|
274
|
+
"list_pipeline_million_timed: {} ms (closure calls {:?})",
|
|
275
|
+
t.elapsed().as_millis(),
|
|
276
|
+
callerNames(&bytes)
|
|
277
|
+
);
|
|
278
|
+
assert_eq!(r, 500001000000);
|
|
279
|
+
// Reference: the same pipeline through closures held in locals (not specialised).
|
|
280
|
+
let plain = withHead(
|
|
281
|
+
"fun build(n: Int) -> List[Int] =\n out: List[Int] := Nil\n i := n\n while i > 0\n out = Cons(head: i, tail: out)\n i = i - 1\n return out\n\nfun main() -> Int =\n xs := build(1000000)\n m := |x| x * 2\n f := |x| x % 4 == 0\n s := |a, x| a + x\n return xs.map(m).filter(f).fold(0, s)\n",
|
|
282
|
+
);
|
|
283
|
+
let buildOnly = withHead(
|
|
284
|
+
"fun build(n: Int) -> List[Int] =\n out: List[Int] := Nil\n i := n\n while i > 0\n out = Cons(head: i, tail: out)\n i = i - 1\n return out\n\nfun main() -> Int =\n xs := build(1000000)\n return xs.length()\n",
|
|
285
|
+
);
|
|
286
|
+
let bb = compile("million_build", &buildOnly);
|
|
287
|
+
let t = std::time::Instant::now();
|
|
288
|
+
runIntBytes(&bb);
|
|
289
|
+
eprintln!("list build + length only: {} ms", t.elapsed().as_millis());
|
|
290
|
+
for (label, expr) in [
|
|
291
|
+
("map only", "xs.map(|x| x * 2).length()"),
|
|
292
|
+
("filter only", "xs.filter(|x| x % 4 == 0).length()"),
|
|
293
|
+
("fold only", "xs.fold(0, |a, x| a + x)"),
|
|
294
|
+
] {
|
|
295
|
+
let one = withHead(&format!(
|
|
296
|
+
"fun build(n: Int) -> List[Int] =\n out: List[Int] := Nil\n i := n\n while i > 0\n out = Cons(head: i, tail: out)\n i = i - 1\n return out\n\nfun main() -> Int =\n xs := build(1000000)\n return {expr}\n"
|
|
297
|
+
));
|
|
298
|
+
let b = compile("million_stage", &one);
|
|
299
|
+
let t = std::time::Instant::now();
|
|
300
|
+
runIntBytes(&b);
|
|
301
|
+
eprintln!(" stage {label} (incl. build): {} ms", t.elapsed().as_millis());
|
|
302
|
+
}
|
|
303
|
+
let pb = compile("million_plain", &plain);
|
|
304
|
+
let t = std::time::Instant::now();
|
|
305
|
+
assert_eq!(runIntBytes(&pb), 500001000000);
|
|
306
|
+
eprintln!("list_pipeline_million_timed (call_ref version, {} call_ref): {} ms", callRefs(&pb), t.elapsed().as_millis());
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#[test]
|
|
310
|
+
fn copies_get_trmc_and_tail_marks() {
|
|
311
|
+
let src = withHead(
|
|
312
|
+
"fun main() -> Int =\n xs := List(1, 2, 3)\n return xs.map(|x| x * 2).fold(0, |a, x| a + x)\n",
|
|
313
|
+
);
|
|
314
|
+
let l = lowered("trmc_copy", &src);
|
|
315
|
+
let text = |n: &str| {
|
|
316
|
+
l.items
|
|
317
|
+
.iter()
|
|
318
|
+
.find_map(|i| match i {
|
|
319
|
+
plum_core::ast::Item::Fn(f) if f.name == n => Some(format!("{:?}", f.body)),
|
|
320
|
+
_ => None,
|
|
321
|
+
})
|
|
322
|
+
.unwrap_or_else(|| panic!("{n} missing"))
|
|
323
|
+
};
|
|
324
|
+
assert!(text("mapCells$c0").contains("__tc_acc"), "TRMC applies to the copy");
|
|
325
|
+
assert!(text("fold$c0").contains("tail: Other"), "tail call marked in the copy");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/// Copies of one callee (`name$cN`), whatever its receiver type.
|
|
329
|
+
fn copiesOf(all: &[String], name: &str) -> usize {
|
|
330
|
+
all.iter().filter(|n| n.rsplit('.').next().unwrap_or(n).starts_with(&format!("{name}$c"))).count()
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
#[test]
|
|
334
|
+
fn deep_recursive_closure_callee() {
|
|
335
|
+
// `loop` recurses 100,000 deep through a tail call; the closure travels through two
|
|
336
|
+
// levels of non-escaping params (`outer` -> `mid` -> `loop`) and is also used by
|
|
337
|
+
// `mid` itself.
|
|
338
|
+
let src = withHead(
|
|
339
|
+
"fun loop(n: Int, acc: Int, f: fn(Int) -> Int) -> Int =\n if n == 0 then acc else loop(n - 1, f(acc), f)\n\nfun mid(n: Int, f: fn(Int) -> Int) -> Int = loop(n, f(0), f)\n\nfun outer(n: Int, g: fn(Int) -> Int) -> Int = mid(n, g)\n\nfun main() -> Int =\n k := 2\n return outer(100000, |x| x + 1) + outer(50000, |x| x + k)\n",
|
|
340
|
+
);
|
|
341
|
+
let names = copies("deep", &src);
|
|
342
|
+
for callee in ["loop", "mid", "outer"] {
|
|
343
|
+
let n = copiesOf(&names, callee);
|
|
344
|
+
assert!(n >= 1 && n <= 8, "{callee}: {n} copies in {names:?}");
|
|
345
|
+
}
|
|
346
|
+
let bytes = compile("deep", &src);
|
|
347
|
+
// Same program through closures held in locals (not specialised).
|
|
348
|
+
let plain = withHead(
|
|
349
|
+
"fun loop(n: Int, acc: Int, f: fn(Int) -> Int) -> Int =\n if n == 0 then acc else loop(n - 1, f(acc), f)\n\nfun mid(n: Int, f: fn(Int) -> Int) -> Int = loop(n, f(0), f)\n\nfun outer(n: Int, g: fn(Int) -> Int) -> Int = mid(n, g)\n\nfun main() -> Int =\n k := 2\n a := |x| x + 1\n b := |x| x + k\n return outer(100000, a) + outer(50000, b)\n",
|
|
350
|
+
);
|
|
351
|
+
assert!(copies("deep_plain", &plain).is_empty());
|
|
352
|
+
let r = runIntBytes(&bytes);
|
|
353
|
+
assert_eq!(r, runInt("deep_plain", &plain));
|
|
354
|
+
assert_eq!(r, 100001 + 100002);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
#[test]
|
|
358
|
+
fn many_call_sites_capped() {
|
|
359
|
+
// 20 distinct closures to one callee: exactly MAX_COPIES (8) copies, the rest left as
|
|
360
|
+
// ordinary closure calls, every result right.
|
|
361
|
+
let mut body = String::from("fun apply(x: Int, f: fn(Int) -> Int) -> Int = f(x)\n\nfun main() -> Int =\n total := 0\n");
|
|
362
|
+
for i in 0..20 {
|
|
363
|
+
body.push_str(&format!(" total = total + apply(5, |x| x * {} + {})\n", i + 1, i));
|
|
364
|
+
}
|
|
365
|
+
body.push_str(" return total\n");
|
|
366
|
+
let src = withHead(&body);
|
|
367
|
+
let names = copies("many20", &src);
|
|
368
|
+
assert_eq!(copiesOf(&names, "apply"), 8, "{names:?}");
|
|
369
|
+
// sum over i of (5 * (i + 1) + i) = 5 * 210 + 190
|
|
370
|
+
let bytes = compile("many20", &src);
|
|
371
|
+
assert!(callRefs(&bytes) > 0, "the 12 call sites past the cap still use closures");
|
|
372
|
+
assert_eq!(runIntBytes(&bytes), 1240);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
#[test]
|
|
376
|
+
fn capture_read_after_mutation() {
|
|
377
|
+
// The closure reads `k` when it runs; `k` was reassigned before the call. A plain
|
|
378
|
+
// closure held in a local (not specialised) created at the same point must agree
|
|
379
|
+
// with the specialised call, and the value is the reassigned one.
|
|
380
|
+
let spec = withHead(
|
|
381
|
+
"fun main() -> Int =\n xs := List(1, 2, 3)\n k := 1\n k = 10\n a := xs.map(|x| x + k).fold(0, |a, x| a + x)\n k = 100\n b := xs.map(|x| x + k).fold(0, |a, x| a + x)\n return a * 1000 + b\n",
|
|
382
|
+
);
|
|
383
|
+
let plain = withHead(
|
|
384
|
+
"fun main() -> Int =\n xs := List(1, 2, 3)\n k := 1\n k = 10\n m := |x| x + k\n s := |a, x| a + x\n a := xs.map(m).fold(0, s)\n k = 100\n m2 := |x| x + k\n b := xs.map(m2).fold(0, s)\n return a * 1000 + b\n",
|
|
385
|
+
);
|
|
386
|
+
assert!(!copies("capmut", &spec).is_empty());
|
|
387
|
+
assert!(copies("capmut_plain", &plain).is_empty());
|
|
388
|
+
let r = runInt("capmut", &spec);
|
|
389
|
+
assert_eq!(r, runInt("capmut_plain", &plain));
|
|
390
|
+
assert_eq!(r, 36 * 1000 + 306);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
#[test]
|
|
394
|
+
fn capture_reassigned_between_closure_and_call() {
|
|
395
|
+
// A closure local created BEFORE the reassignment is not a direct argument, so it is
|
|
396
|
+
// never specialised. Record what plain closures do (value at creation vs at call) and
|
|
397
|
+
// check the direct-argument (specialised) form sees the reassigned value.
|
|
398
|
+
let held = withHead(
|
|
399
|
+
"fun main() -> Int =\n xs := List(1, 2, 3)\n k := 1\n m := |x| x + k\n k = 10\n return xs.map(m).fold(0, |a, x| a + x)\n",
|
|
400
|
+
);
|
|
401
|
+
let direct = withHead(
|
|
402
|
+
"fun main() -> Int =\n xs := List(1, 2, 3)\n k := 1\n k = 10\n return xs.map(|x| x + k).fold(0, |a, x| a + x)\n",
|
|
403
|
+
);
|
|
404
|
+
let heldResult = runInt("held", &held);
|
|
405
|
+
assert_eq!(runInt("direct", &direct), 36);
|
|
406
|
+
eprintln!("closure created before reassignment gives {heldResult} (9 = captured by value, 36 = by reference)");
|
|
407
|
+
assert!(heldResult == 9 || heldResult == 36, "held closure gave {heldResult}");
|
|
408
|
+
}
|
plum-std/game/Actions.plum
CHANGED
|
@@ -3,26 +3,28 @@ import std/Slice
|
|
|
3
3
|
import std/Int
|
|
4
4
|
import std/Float
|
|
5
5
|
|
|
6
|
-
# scene2d-style actions as plain data plus a
|
|
6
|
+
# scene2d-style actions as plain data plus a step function. `Actions.stepAction`
|
|
7
|
+
# steps an action IN PLACE on its own record (a running action allocates nothing:
|
|
7
|
-
#
|
|
8
|
+
# the result is a struct, flattened into multi-value returns) and reports the new
|
|
8
|
-
#
|
|
9
|
+
# pose, the part of `dt` it did not use and whether it finished. `stepAll` drops
|
|
9
|
-
#
|
|
10
|
+
# finished actions by compacting the Slice in place. Sequences hand the spare time
|
|
10
|
-
# chained and looping animations do not drift.
|
|
11
|
+
# to the next step, so chained and looping animations do not drift. A `Forever`
|
|
12
|
+
# restarts a lap from a fresh copy of its template (one allocation per lap).
|
|
11
13
|
|
|
12
14
|
# The animated part of an actor's transform.
|
|
13
15
|
struct Pose(x: Float, y: Float, rotation: Float, scaleX: Float, scaleY: Float)
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
record MoveBy(dx: Float, dy: Float, duration: Float, elapsed: Float)
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
record RotateBy(radians: Float, duration: Float, elapsed: Float)
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
record Delay(duration: Float, elapsed: Float)
|
|
20
22
|
|
|
21
23
|
record Sequence(steps: Slice[Action])
|
|
22
24
|
|
|
23
25
|
record Parallel(steps: Slice[Action])
|
|
24
26
|
|
|
25
|
-
# `template` is the action restarted each lap, `current` the lap in progress.
|
|
27
|
+
# `template` is the pristine action restarted each lap, `current` the lap in progress.
|
|
26
28
|
record Forever(template: Action, current: Action)
|
|
27
29
|
|
|
28
30
|
enum Action = MoveBy | RotateBy | Delay | Sequence | Parallel | Forever
|
|
@@ -31,12 +33,9 @@ enum Action = MoveBy | RotateBy | Delay | Sequence | Parallel | Forever
|
|
|
31
33
|
# once and applies all of itself (no 0/0).
|
|
32
34
|
struct Tick(used: Float, frac: Float, done: Bool)
|
|
33
35
|
|
|
34
|
-
# The result of
|
|
36
|
+
# The result of one step: the pose, the unused part of dt (0 while running) and
|
|
37
|
+
# whether the action(s) finished. For `stepAll`, `done` means none are left.
|
|
35
|
-
|
|
38
|
+
struct Step(pose: Pose, spare: Float, done: Bool)
|
|
36
|
-
|
|
37
|
-
# The result of stepping actions side by side: the unfinished ones, and the
|
|
38
|
-
# smallest spare time among the finished ones.
|
|
39
|
-
record SteppedAll(pose: Pose, kept: Slice[Action], spare: Float)
|
|
40
39
|
|
|
41
40
|
primitive Actions =
|
|
42
41
|
fun moveBy(dx: Float, dy: Float, secs: Float) -> Action =
|
|
@@ -55,7 +54,25 @@ primitive Actions =
|
|
|
55
54
|
Parallel(steps: steps)
|
|
56
55
|
|
|
57
56
|
fun forever(a: Action) -> Action =
|
|
58
|
-
Forever(template: a, current: a)
|
|
57
|
+
Forever(template: Actions.fresh(a), current: a)
|
|
58
|
+
|
|
59
|
+
# An unstarted copy of `a` (elapsed 0, all steps present).
|
|
60
|
+
fun fresh(a: Action) -> Action =
|
|
61
|
+
match a
|
|
62
|
+
MoveBy => MoveBy(dx: a.dx, dy: a.dy, duration: a.duration, elapsed: 0.0)
|
|
63
|
+
RotateBy => RotateBy(radians: a.radians, duration: a.duration, elapsed: 0.0)
|
|
64
|
+
Delay => Delay(duration: a.duration, elapsed: 0.0)
|
|
65
|
+
Sequence =>
|
|
66
|
+
out := Slice[Action]()
|
|
67
|
+
for i := range a.steps.length()
|
|
68
|
+
out.push(Actions.fresh(a.steps.at(i)))
|
|
69
|
+
Sequence(steps: out)
|
|
70
|
+
Parallel =>
|
|
71
|
+
out := Slice[Action]()
|
|
72
|
+
for i := range a.steps.length()
|
|
73
|
+
out.push(Actions.fresh(a.steps.at(i)))
|
|
74
|
+
Parallel(steps: out)
|
|
75
|
+
Forever => Forever(template: a.template, current: Actions.fresh(a.template))
|
|
59
76
|
|
|
60
77
|
fun tick(duration: Float, elapsed: Float, dt: Float) -> Tick =
|
|
61
78
|
if duration <= 0.0
|
|
@@ -63,78 +80,67 @@ primitive Actions =
|
|
|
63
80
|
used := dt.min(duration - elapsed)
|
|
64
81
|
Tick(used: used, frac: used / duration, done: elapsed + used >= duration)
|
|
65
82
|
|
|
66
|
-
fun stepAction(p: Pose, a: Action, dt: Float) ->
|
|
83
|
+
fun stepAction(p: Pose, a: Action, dt: Float) -> Step =
|
|
67
84
|
match a
|
|
68
85
|
MoveBy =>
|
|
69
86
|
k := Actions.tick(a.duration, a.elapsed, dt)
|
|
87
|
+
a.elapsed = a.elapsed + k.used
|
|
70
88
|
q := Pose(x: p.x + a.dx * k.frac, y: p.y + a.dy * k.frac, rotation: p.rotation, scaleX: p.scaleX, scaleY: p.scaleY)
|
|
71
|
-
if k.done
|
|
72
|
-
|
|
89
|
+
Step(pose: q, spare: if k.done then dt - k.used else 0.0, done: k.done)
|
|
73
|
-
next: Action := MoveBy(dx: a.dx, dy: a.dy, duration: a.duration, elapsed: a.elapsed + k.used)
|
|
74
|
-
Stepped(pose: q, left: next, spare: 0.0)
|
|
75
90
|
RotateBy =>
|
|
76
91
|
k := Actions.tick(a.duration, a.elapsed, dt)
|
|
92
|
+
a.elapsed = a.elapsed + k.used
|
|
77
93
|
q := Pose(x: p.x, y: p.y, rotation: p.rotation + a.radians * k.frac, scaleX: p.scaleX, scaleY: p.scaleY)
|
|
78
|
-
if k.done
|
|
79
|
-
|
|
94
|
+
Step(pose: q, spare: if k.done then dt - k.used else 0.0, done: k.done)
|
|
80
|
-
next: Action := RotateBy(radians: a.radians, duration: a.duration, elapsed: a.elapsed + k.used)
|
|
81
|
-
Stepped(pose: q, left: next, spare: 0.0)
|
|
82
95
|
Delay =>
|
|
83
96
|
k := Actions.tick(a.duration, a.elapsed, dt)
|
|
84
|
-
|
|
97
|
+
a.elapsed = a.elapsed + k.used
|
|
85
|
-
|
|
98
|
+
Step(pose: p, spare: if k.done then dt - k.used else 0.0, done: k.done)
|
|
86
|
-
next: Action := Delay(duration: a.duration, elapsed: a.elapsed + k.used)
|
|
87
|
-
Stepped(pose: p, left: next, spare: 0.0)
|
|
88
99
|
Sequence =>
|
|
89
100
|
if a.steps.length() == 0
|
|
90
|
-
return
|
|
101
|
+
return Step(pose: p, spare: dt, done: true)
|
|
91
102
|
s := Actions.stepAction(p, a.steps.at(0), dt)
|
|
92
|
-
rest := a.steps.drop(1)
|
|
93
|
-
if
|
|
103
|
+
if !s.done
|
|
94
|
-
kept := Slice[Action]()
|
|
95
|
-
kept.push(cur)
|
|
96
|
-
for i := range rest.length()
|
|
97
|
-
kept.push(rest.at(i))
|
|
98
|
-
seq: Action := Sequence(steps: kept)
|
|
99
|
-
return
|
|
104
|
+
return Step(pose: s.pose, spare: 0.0, done: false)
|
|
100
105
|
# The rest starts in this same frame with the time left over; the list
|
|
101
106
|
# is one shorter on every call, so this terminates.
|
|
102
|
-
|
|
107
|
+
a.steps.removeAt(0)
|
|
103
|
-
Actions.stepAction(s.pose,
|
|
108
|
+
Actions.stepAction(s.pose, a, s.spare)
|
|
104
109
|
Parallel =>
|
|
105
|
-
|
|
110
|
+
Actions.stepAll(p, a.steps, dt)
|
|
106
|
-
if all.kept.length() == 0
|
|
107
|
-
return Stepped(pose: all.pose, left: null, spare: all.spare)
|
|
108
|
-
par: Action := Parallel(steps: all.kept)
|
|
109
|
-
Stepped(pose: all.pose, left: par, spare: 0.0)
|
|
110
111
|
Forever =>
|
|
111
112
|
# Restarts at most once per step, with the spare time, so a zero-length
|
|
112
113
|
# template cannot spin forever inside one step.
|
|
113
114
|
s := Actions.stepAction(p, a.current, dt)
|
|
114
|
-
if
|
|
115
|
+
if !s.done
|
|
115
|
-
again: Action := Forever(template: a.template, current: cur)
|
|
116
|
-
return
|
|
116
|
+
return Step(pose: s.pose, spare: 0.0, done: false)
|
|
117
|
+
a.current = Actions.fresh(a.template)
|
|
117
|
-
r := Actions.stepAction(s.pose, a.
|
|
118
|
+
r := Actions.stepAction(s.pose, a.current, s.spare)
|
|
118
|
-
lap := a.template
|
|
119
|
-
if
|
|
119
|
+
if r.done
|
|
120
|
-
lap = nxt
|
|
121
|
-
|
|
120
|
+
a.current = Actions.fresh(a.template)
|
|
122
|
-
|
|
121
|
+
Step(pose: r.pose, spare: 0.0, done: false)
|
|
122
|
+
|
|
123
|
-
|
|
123
|
+
# Steps every action side by side in place and drops the finished ones by
|
|
124
|
+
# compacting `acts`. `spare` is the smallest spare time among the finished
|
|
125
|
+
# ones, 0 while any action is left.
|
|
124
|
-
fun stepAll(p: Pose, acts: Slice[Action], dt: Float) ->
|
|
126
|
+
fun stepAll(p: Pose, acts: Slice[Action], dt: Float) -> Step =
|
|
125
127
|
pose := p
|
|
126
|
-
kept := Slice[Action]()
|
|
127
128
|
spare := dt
|
|
129
|
+
w := 0
|
|
128
130
|
for i := range acts.length()
|
|
129
131
|
s := Actions.stepAction(pose, acts.at(i), dt)
|
|
130
132
|
pose = s.pose
|
|
133
|
+
if !s.done
|
|
131
|
-
|
|
134
|
+
if w != i
|
|
132
|
-
|
|
135
|
+
acts.putAt(w, acts.takeAt(i))
|
|
136
|
+
w = w + 1
|
|
133
137
|
else
|
|
134
138
|
spare = spare.min(s.spare)
|
|
135
|
-
|
|
139
|
+
while acts.length() > w
|
|
140
|
+
acts.removeAt(acts.length() - 1)
|
|
141
|
+
if w > 0
|
|
136
142
|
spare = 0.0
|
|
137
|
-
|
|
143
|
+
Step(pose: pose, spare: spare, done: w == 0)
|
|
138
144
|
|
|
139
145
|
fun actNear(a: Float, b: Float) -> Bool =
|
|
140
146
|
{a - b}.abs() < 0.000001
|
|
@@ -144,32 +150,173 @@ fun actSeqLen(a: Action) -> Int =
|
|
|
144
150
|
Sequence => a.steps.length()
|
|
145
151
|
_ => 0 - 1
|
|
146
152
|
|
|
153
|
+
fun actElapsed(a: Action) -> Float =
|
|
154
|
+
match a
|
|
155
|
+
MoveBy => a.elapsed
|
|
156
|
+
_ => 0.0 - 1.0
|
|
157
|
+
|
|
147
158
|
fun actPose0() -> Pose =
|
|
148
159
|
Pose(x: 0.0, y: 0.0, rotation: 0.0, scaleX: 1.0, scaleY: 1.0)
|
|
149
160
|
|
|
161
|
+
# Golden values for actions, taken from the previous (pure, allocating) implementation
|
|
162
|
+
# (`git show e04834f:plum-std/game/Actions.plum`) run on the same inputs: pose, spare
|
|
163
|
+
# time and done flag at fixed checkpoints, stepping a fresh action with dt 0.016 x N
|
|
164
|
+
# and some large dts that span several sub-actions.
|
|
165
|
+
record GoldRun(a: Action, p: Pose, spare: Float, done: Bool)
|
|
166
|
+
|
|
167
|
+
fun goldRun(a: Action) -> GoldRun =
|
|
168
|
+
GoldRun(a: a, p: actPose0(), spare: 0.0, done: false)
|
|
169
|
+
|
|
170
|
+
fun goldAdv(r: GoldRun, n: Int, dt: Float) =
|
|
171
|
+
for i := range n
|
|
172
|
+
if !r.done
|
|
173
|
+
s := Actions.stepAction(r.p, r.a, dt)
|
|
174
|
+
r.p = s.pose
|
|
175
|
+
r.spare = s.spare
|
|
176
|
+
r.done = s.done
|
|
177
|
+
|
|
178
|
+
fun goldNear(a: Float, b: Float) -> Bool =
|
|
179
|
+
{a - b}.abs() < 0.00001
|
|
180
|
+
|
|
181
|
+
fun goldWant(r: GoldRun, x: Float, y: Float, rotation: Float, spare: Float, done: Bool) =
|
|
182
|
+
assert goldNear(r.p.x, x)
|
|
183
|
+
assert goldNear(r.p.y, y)
|
|
184
|
+
assert goldNear(r.p.rotation, rotation)
|
|
185
|
+
assert goldNear(r.spare, spare)
|
|
186
|
+
assert r.done == done
|
|
187
|
+
|
|
188
|
+
fun goldMove() =
|
|
189
|
+
r := goldRun(Actions.moveBy(10.0, 5.0, 1.0))
|
|
190
|
+
goldAdv(r, 1, 0.016)
|
|
191
|
+
goldWant(r, 0.16, 0.08, 0.0, 0.0, false)
|
|
192
|
+
goldAdv(r, 29, 0.016)
|
|
193
|
+
goldWant(r, 4.8, 2.4, 0.0, 0.0, false)
|
|
194
|
+
goldAdv(r, 32, 0.016)
|
|
195
|
+
goldWant(r, 9.92, 4.96, 0.0, 0.0, false)
|
|
196
|
+
goldAdv(r, 1, 0.016)
|
|
197
|
+
goldWant(r, 10.0, 5.0, 0.0, 0.008, true)
|
|
198
|
+
|
|
199
|
+
fun goldRotate() =
|
|
200
|
+
r := goldRun(Actions.rotateBy(2.0, 0.5))
|
|
201
|
+
goldAdv(r, 10, 0.016)
|
|
202
|
+
goldWant(r, 0.0, 0.0, 0.64, 0.0, false)
|
|
203
|
+
goldAdv(r, 21, 0.016)
|
|
204
|
+
goldWant(r, 0.0, 0.0, 1.984, 0.0, false)
|
|
205
|
+
goldAdv(r, 1, 0.016)
|
|
206
|
+
goldWant(r, 0.0, 0.0, 2.0, 0.012, true)
|
|
207
|
+
|
|
208
|
+
fun goldDelay() =
|
|
209
|
+
r := goldRun(Actions.delay(0.1))
|
|
210
|
+
goldAdv(r, 6, 0.016)
|
|
211
|
+
goldWant(r, 0.0, 0.0, 0.0, 0.0, false)
|
|
212
|
+
goldAdv(r, 1, 0.016)
|
|
213
|
+
goldWant(r, 0.0, 0.0, 0.0, 0.012, true)
|
|
214
|
+
|
|
215
|
+
fun goldSequence() =
|
|
216
|
+
r := goldRun(Actions.sequence(Slice(Actions.moveBy(10.0, 0.0, 0.2), Actions.delay(0.1), Actions.rotateBy(1.0, 0.2))))
|
|
217
|
+
goldAdv(r, 12, 0.016)
|
|
218
|
+
goldWant(r, 9.6, 0.0, 0.0, 0.0, false)
|
|
219
|
+
goldAdv(r, 1, 0.016)
|
|
220
|
+
goldWant(r, 10.0, 0.0, 0.0, 0.0, false)
|
|
221
|
+
goldAdv(r, 6, 0.016)
|
|
222
|
+
goldWant(r, 10.0, 0.0, 0.02, 0.0, false)
|
|
223
|
+
goldAdv(r, 1, 0.016)
|
|
224
|
+
goldWant(r, 10.0, 0.0, 0.1, 0.0, false)
|
|
225
|
+
goldAdv(r, 5, 0.016)
|
|
226
|
+
goldWant(r, 10.0, 0.0, 0.5, 0.0, false)
|
|
227
|
+
goldAdv(r, 10, 0.016)
|
|
228
|
+
goldWant(r, 10.0, 0.0, 1.0, 0.012, true)
|
|
229
|
+
goldAdv(r, 5, 0.016)
|
|
230
|
+
goldWant(r, 10.0, 0.0, 1.0, 0.012, true)
|
|
231
|
+
|
|
232
|
+
fun goldParallel() =
|
|
233
|
+
r := goldRun(Actions.parallel(Slice(Actions.moveBy(10.0, 0.0, 0.3), Actions.rotateBy(1.0, 0.1), Actions.delay(0.2))))
|
|
234
|
+
goldAdv(r, 5, 0.016)
|
|
235
|
+
goldWant(r, 2.666667, 0.0, 0.8, 0.0, false)
|
|
236
|
+
goldAdv(r, 2, 0.016)
|
|
237
|
+
goldWant(r, 3.733333, 0.0, 1.0, 0.0, false)
|
|
238
|
+
goldAdv(r, 5, 0.016)
|
|
239
|
+
goldWant(r, 6.4, 0.0, 1.0, 0.0, false)
|
|
240
|
+
goldAdv(r, 7, 0.016)
|
|
241
|
+
goldWant(r, 10.0, 0.0, 1.0, 0.004, true)
|
|
242
|
+
goldAdv(r, 1, 0.016)
|
|
243
|
+
goldWant(r, 10.0, 0.0, 1.0, 0.004, true)
|
|
244
|
+
|
|
245
|
+
fun goldForever() =
|
|
246
|
+
r := goldRun(Actions.forever(Actions.sequence(Slice(Actions.moveBy(1.0, 0.0, 0.1), Actions.delay(0.05)))))
|
|
247
|
+
goldAdv(r, 9, 0.016)
|
|
248
|
+
goldWant(r, 1.0, 0.0, 0.0, 0.0, false)
|
|
249
|
+
goldAdv(r, 91, 0.016)
|
|
250
|
+
goldWant(r, 11.0, 0.0, 0.0, 0.0, false)
|
|
251
|
+
goldAdv(r, 900, 0.016)
|
|
252
|
+
goldWant(r, 107.0, 0.0, 0.0, 0.0, false)
|
|
253
|
+
|
|
254
|
+
fun goldNested() =
|
|
255
|
+
inner := Actions.parallel(Slice(Actions.moveBy(4.0, 0.0, 0.1), Actions.delay(0.2)))
|
|
256
|
+
loop := Actions.forever(Actions.parallel(Slice(Actions.rotateBy(1.0, 0.1), Actions.moveBy(0.0, 1.0, 0.3))))
|
|
257
|
+
r := goldRun(Actions.sequence(Slice(inner, loop)))
|
|
258
|
+
goldAdv(r, 6, 0.016)
|
|
259
|
+
goldWant(r, 3.84, 0.0, 0.0, 0.0, false)
|
|
260
|
+
goldAdv(r, 7, 0.016)
|
|
261
|
+
goldWant(r, 4.0, 0.026667, 0.08, 0.0, false)
|
|
262
|
+
goldAdv(r, 20, 0.016)
|
|
263
|
+
goldWant(r, 4.0, 1.093333, 1.28, 0.0, false)
|
|
264
|
+
goldAdv(r, 200, 0.016)
|
|
265
|
+
goldWant(r, 4.0, 11.76, 12.0, 0.0, false)
|
|
266
|
+
|
|
267
|
+
fun goldLargeDt() =
|
|
268
|
+
r := goldRun(Actions.sequence(Slice(Actions.moveBy(1.0, 0.0, 0.1), Actions.moveBy(0.0, 1.0, 0.1), Actions.rotateBy(1.0, 0.1), Actions.delay(0.1))))
|
|
269
|
+
goldAdv(r, 1, 0.25)
|
|
270
|
+
goldWant(r, 1.0, 1.0, 0.5, 0.0, false)
|
|
271
|
+
goldAdv(r, 1, 0.05)
|
|
272
|
+
goldWant(r, 1.0, 1.0, 1.0, 0.0, false)
|
|
273
|
+
goldAdv(r, 1, 0.5)
|
|
274
|
+
goldWant(r, 1.0, 1.0, 1.0, 0.4, true)
|
|
275
|
+
|
|
276
|
+
fun goldLargeDtParallel() =
|
|
277
|
+
r := goldRun(Actions.parallel(Slice(Actions.moveBy(2.0, 0.0, 0.1), Actions.sequence(Slice(Actions.delay(0.1), Actions.rotateBy(1.0, 0.1))), Actions.delay(0.3))))
|
|
278
|
+
goldAdv(r, 1, 0.15)
|
|
279
|
+
goldWant(r, 2.0, 0.0, 0.5, 0.0, false)
|
|
280
|
+
goldAdv(r, 1, 0.1)
|
|
281
|
+
goldWant(r, 2.0, 0.0, 1.0, 0.0, false)
|
|
282
|
+
goldAdv(r, 1, 0.4)
|
|
283
|
+
goldWant(r, 2.0, 0.0, 1.0, 0.35, true)
|
|
284
|
+
|
|
285
|
+
fun goldLargeDtForever() =
|
|
286
|
+
r := goldRun(Actions.forever(Actions.sequence(Slice(Actions.moveBy(1.0, 0.0, 0.1), Actions.rotateBy(1.0, 0.1)))))
|
|
287
|
+
goldAdv(r, 1, 0.25)
|
|
288
|
+
goldWant(r, 1.5, 0.0, 1.0, 0.0, false)
|
|
289
|
+
goldAdv(r, 1, 0.5)
|
|
290
|
+
goldWant(r, 3.0, 0.0, 3.0, 0.0, false)
|
|
291
|
+
goldAdv(r, 1, 0.016)
|
|
292
|
+
goldWant(r, 3.16, 0.0, 3.0, 0.0, false)
|
|
293
|
+
|
|
150
294
|
test "stepAll moves partway and keeps the action"
|
|
151
|
-
|
|
295
|
+
acts := Slice(Actions.moveBy(10.0, 0.0, 1.0))
|
|
296
|
+
r := Actions.stepAll(actPose0(), acts, 0.5)
|
|
152
297
|
assert actNear(r.pose.x, 5.0)
|
|
153
|
-
assert
|
|
298
|
+
assert acts.length() == 1
|
|
154
299
|
assert actNear(r.spare, 0.0)
|
|
155
300
|
|
|
156
301
|
test "stepAll finishes every action and reports no spare"
|
|
157
|
-
|
|
302
|
+
acts := Slice(Actions.moveBy(10.0, 0.0, 1.0), Actions.rotateBy(1.0, 0.5))
|
|
303
|
+
r := Actions.stepAll(actPose0(), acts, 1.0)
|
|
158
304
|
assert actNear(r.pose.x, 10.0)
|
|
159
305
|
assert actNear(r.pose.rotation, 1.0)
|
|
160
|
-
assert
|
|
306
|
+
assert acts.length() == 0
|
|
161
307
|
assert actNear(r.spare, 0.0)
|
|
162
308
|
|
|
163
309
|
test "stepAll with no actions is a no-op"
|
|
310
|
+
acts := Slice[Action]()
|
|
164
|
-
r := Actions.stepAll(actPose0(),
|
|
311
|
+
r := Actions.stepAll(actPose0(), acts, 0.016)
|
|
165
|
-
assert
|
|
312
|
+
assert acts.length() == 0
|
|
166
313
|
assert actNear(r.spare, 0.016)
|
|
167
314
|
assert actNear(r.pose.x, 0.0)
|
|
168
315
|
|
|
169
316
|
test "zero_length_move_applies_at_once"
|
|
170
317
|
r := Actions.stepAction(actPose0(), Actions.moveBy(10.0, 0.0, 0.0), 0.016)
|
|
171
318
|
assert actNear(r.pose.x, 10.0)
|
|
172
|
-
assert r.
|
|
319
|
+
assert r.done
|
|
173
320
|
assert actNear(r.spare, 0.016)
|
|
174
321
|
|
|
175
322
|
test "forever_rotate_no_drift"
|
|
@@ -178,9 +325,7 @@ test "forever_rotate_no_drift"
|
|
|
178
325
|
for i := range 625
|
|
179
326
|
r := Actions.stepAction(p, a, 0.016)
|
|
180
327
|
p = r.pose
|
|
181
|
-
assert r.
|
|
328
|
+
assert !r.done
|
|
182
|
-
if nxt := r.left
|
|
183
|
-
a = nxt
|
|
184
329
|
assert actNear(p.rotation, 10.0)
|
|
185
330
|
|
|
186
331
|
test "sequence_spills_spare_time"
|
|
@@ -189,24 +334,22 @@ test "sequence_spills_spare_time"
|
|
|
189
334
|
assert actNear(r.pose.x, 10.0)
|
|
190
335
|
assert actNear(r.pose.y, 5.0)
|
|
191
336
|
assert actNear(r.spare, 0.0)
|
|
192
|
-
|
|
337
|
+
assert !r.done
|
|
193
|
-
|
|
338
|
+
assert actSeqLen(a) == 1
|
|
194
|
-
else
|
|
195
|
-
assert false
|
|
196
339
|
|
|
197
340
|
test "empty_sequence_finishes_with_full_spare"
|
|
198
341
|
r := Actions.stepAction(actPose0(), Actions.sequence(Slice[Action]()), 0.3)
|
|
199
|
-
assert r.
|
|
342
|
+
assert r.done
|
|
200
343
|
assert actNear(r.spare, 0.3)
|
|
201
344
|
|
|
202
345
|
test "parallel_spare_is_min_of_finished"
|
|
203
346
|
r := Actions.stepAction(actPose0(), Actions.parallel(Slice(Actions.delay(0.2), Actions.delay(0.5))), 1.0)
|
|
204
|
-
assert r.
|
|
347
|
+
assert r.done
|
|
205
348
|
assert actNear(r.spare, 0.5)
|
|
206
349
|
|
|
207
350
|
test "forever_zero_length_template_terminates"
|
|
208
351
|
r := Actions.stepAction(actPose0(), Actions.forever(Actions.delay(0.0)), 1.0)
|
|
209
|
-
assert r.
|
|
352
|
+
assert !r.done
|
|
210
353
|
|
|
211
354
|
test "forever of a nested sequence keeps looping"
|
|
212
355
|
a := Actions.forever(Actions.sequence(Slice(Actions.moveBy(1.0, 0.0, 0.1), Actions.delay(0.1))))
|
|
@@ -214,12 +357,39 @@ test "forever of a nested sequence keeps looping"
|
|
|
214
357
|
for i := range 100
|
|
215
358
|
r := Actions.stepAction(p, a, 0.05)
|
|
216
359
|
p = r.pose
|
|
217
|
-
if nxt := r.left
|
|
218
|
-
a = nxt
|
|
219
360
|
assert actNear(p.x, 25.0)
|
|
220
361
|
|
|
221
|
-
test "
|
|
362
|
+
test "step_moves_the_action_in_place"
|
|
222
|
-
|
|
363
|
+
a := Actions.moveBy(10.0, 0.0, 1.0)
|
|
223
|
-
r := Actions.stepAction(actPose0(),
|
|
364
|
+
r := Actions.stepAction(actPose0(), a, 0.5)
|
|
224
|
-
assert actNear(m.elapsed, 0.0)
|
|
225
365
|
assert actNear(r.pose.x, 5.0)
|
|
366
|
+
assert actElapsed(a) == 0.5
|
|
367
|
+
|
|
368
|
+
# Steady state: the held MoveBy and RotateBy are the very records stepped (their
|
|
369
|
+
# elapsed moves, no replacement lists or actions), and the slice keeps its length.
|
|
370
|
+
# The wasm of this path has no struct.new (checked with wasm-tools print).
|
|
371
|
+
test "step_all_no_alloc_steady_state"
|
|
372
|
+
m := Actions.moveBy(100.0, 0.0, 100.0)
|
|
373
|
+
r := Actions.rotateBy(1.0, 100.0)
|
|
374
|
+
acts := Slice(m, r)
|
|
375
|
+
p := actPose0()
|
|
376
|
+
for i := range 10
|
|
377
|
+
s := Actions.stepAll(p, acts, 0.5)
|
|
378
|
+
p = s.pose
|
|
379
|
+
assert !s.done
|
|
380
|
+
assert acts.length() == 2
|
|
381
|
+
assert actNear(actElapsed(m), 5.0)
|
|
382
|
+
assert actNear(p.x, 5.0)
|
|
383
|
+
assert actNear(p.rotation, 0.05)
|
|
384
|
+
|
|
385
|
+
test "actions_golden_values"
|
|
386
|
+
goldMove()
|
|
387
|
+
goldRotate()
|
|
388
|
+
goldDelay()
|
|
389
|
+
goldSequence()
|
|
390
|
+
goldParallel()
|
|
391
|
+
goldForever()
|
|
392
|
+
goldNested()
|
|
393
|
+
goldLargeDt()
|
|
394
|
+
goldLargeDtParallel()
|
|
395
|
+
goldLargeDtForever()
|
plum-std/game/PureScene.plum
CHANGED
|
@@ -173,9 +173,11 @@ primitive PureScene =
|
|
|
173
173
|
m := parent * PureScene.localMatrix(a)
|
|
174
174
|
t := pureMulColor(tint, a.color)
|
|
175
175
|
a.draw(m, t)
|
|
176
|
+
match a
|
|
176
|
-
|
|
177
|
+
Group(_, _, _, _, _, _, _, _, _, _, _, _, _, _, g) =>
|
|
177
|
-
|
|
178
|
+
for i := range g.length()
|
|
178
|
-
|
|
179
|
+
PureScene.drawTree(g.at(i), m, t)
|
|
180
|
+
_ => 0
|
|
179
181
|
|
|
180
182
|
# The children of a group, or null for a leaf.
|
|
181
183
|
fun childrenOf(a: Actor) -> Slice[Actor]? =
|
plum-std/game/SceneTree.plum
CHANGED
|
@@ -27,9 +27,10 @@ primitive SceneTree =
|
|
|
27
27
|
b.rotation = r.pose.rotation
|
|
28
28
|
b.scaleX = r.pose.scaleX
|
|
29
29
|
b.scaleY = r.pose.scaleY
|
|
30
|
-
|
|
30
|
+
match b
|
|
31
|
-
|
|
31
|
+
Group(_, _, _, _, _, _, _, _, _, _, _, _, _, _, ks) =>
|
|
32
|
-
|
|
32
|
+
_ := ks.mapInPlace(|k| SceneTree.update(k, dt))
|
|
33
|
+
_ => 0
|
|
33
34
|
b
|
|
34
35
|
|
|
35
36
|
# The path (child indexes from `a`) to the topmost actor under `p`, which is
|
|
@@ -44,13 +45,15 @@ primitive SceneTree =
|
|
|
44
45
|
lp := Vec2(x: 0.0, y: 0.0)
|
|
45
46
|
if q := inv
|
|
46
47
|
lp = q.apply(p)
|
|
47
|
-
|
|
48
|
+
match a
|
|
49
|
+
Group(_, _, _, _, _, _, _, _, _, _, _, _, _, _, ks) =>
|
|
48
|
-
|
|
50
|
+
i := ks.length() - 1
|
|
49
|
-
|
|
51
|
+
while i >= 0
|
|
50
|
-
|
|
52
|
+
sub := SceneTree.findHit(ks.at(i), m, p)
|
|
51
|
-
|
|
53
|
+
if s := sub
|
|
52
|
-
|
|
54
|
+
return treePrepend(i, s)
|
|
53
|
-
|
|
55
|
+
i = i - 1
|
|
56
|
+
_ => 0
|
|
54
57
|
if a.hit(lp)
|
|
55
58
|
return Slice[Int]()
|
|
56
59
|
null
|
|
@@ -59,21 +62,25 @@ primitive SceneTree =
|
|
|
59
62
|
fun idAt(a: Actor, path: Slice[Int], depth: Int) -> Int =
|
|
60
63
|
if depth >= path.length()
|
|
61
64
|
return a.id
|
|
62
|
-
|
|
65
|
+
match a
|
|
66
|
+
Group(_, _, _, _, _, _, _, _, _, _, _, _, _, _, ks) =>
|
|
63
|
-
|
|
67
|
+
i := path.at(depth)
|
|
64
|
-
|
|
68
|
+
if i >= 0 && i < ks.length()
|
|
65
|
-
|
|
69
|
+
return SceneTree.idAt(ks.at(i), path, depth + 1)
|
|
70
|
+
_ => 0
|
|
66
71
|
-1
|
|
67
72
|
|
|
68
73
|
# The path to the first actor with `id`, depth first; null if none.
|
|
69
74
|
fun findPath(a: Actor, id: Int) -> Slice[Int]? =
|
|
70
75
|
if a.id == id
|
|
71
76
|
return Slice[Int]()
|
|
72
|
-
|
|
77
|
+
match a
|
|
78
|
+
Group(_, _, _, _, _, _, _, _, _, _, _, _, _, _, ks) =>
|
|
73
|
-
|
|
79
|
+
for i := range ks.length()
|
|
74
|
-
|
|
80
|
+
sub := SceneTree.findPath(ks.at(i), id)
|
|
75
|
-
|
|
81
|
+
if s := sub
|
|
76
|
-
|
|
82
|
+
return treePrepend(i, s)
|
|
83
|
+
_ => 0
|
|
77
84
|
null
|
|
78
85
|
|
|
79
86
|
# The tree where the first actor with `id` is replaced by `f` of it.
|
|
@@ -86,11 +93,13 @@ primitive SceneTree =
|
|
|
86
93
|
fun modifyAt(own a: Actor, path: Slice[Int], depth: Int, f: fn(Actor) -> Actor) -> Actor =
|
|
87
94
|
if depth >= path.length()
|
|
88
95
|
return f(a)
|
|
89
|
-
|
|
96
|
+
match a
|
|
97
|
+
Group(_, _, _, _, _, _, _, _, _, _, _, _, _, _, ks) =>
|
|
90
|
-
|
|
98
|
+
i := path.at(depth)
|
|
91
|
-
|
|
99
|
+
if i >= 0 && i < ks.length()
|
|
92
|
-
|
|
100
|
+
c := ks.takeAt(i)
|
|
93
|
-
|
|
101
|
+
ks.putAt(i, SceneTree.modifyAt(c, path, depth + 1, f))
|
|
102
|
+
_ => 0
|
|
94
103
|
a
|
|
95
104
|
|
|
96
105
|
# Delivers `e` (in the coordinates of `a`'s parent) along `path`: the child on
|
|
@@ -109,11 +118,13 @@ primitive SceneTree =
|
|
|
109
118
|
lp = q.apply(lp)
|
|
110
119
|
local := Pointer(kind: e.kind, x: lp.x, y: lp.y, over: e.over)
|
|
111
120
|
if depth < path.length()
|
|
112
|
-
|
|
121
|
+
match a
|
|
122
|
+
Group(_, _, _, _, _, _, _, _, _, _, _, _, _, _, ks) =>
|
|
113
|
-
|
|
123
|
+
i := path.at(depth)
|
|
114
|
-
|
|
124
|
+
if i >= 0 && i < ks.length()
|
|
115
|
-
|
|
125
|
+
c := ks.takeAt(i)
|
|
116
|
-
|
|
126
|
+
ks.putAt(i, SceneTree.routeInto(c, path, depth + 1, local, events))
|
|
127
|
+
_ => 0
|
|
117
128
|
h := a.onPointer(local)
|
|
118
129
|
for j := range h.events.length()
|
|
119
130
|
events.push(h.events.at(j))
|
|
@@ -161,15 +172,17 @@ fun ttMovingTree() -> Actor =
|
|
|
161
172
|
me
|
|
162
173
|
|
|
163
174
|
fun ttFirstKidX(a: Actor) -> Float =
|
|
164
|
-
ks := a.kids()
|
|
165
|
-
|
|
175
|
+
match a
|
|
176
|
+
Group(_, _, _, _, _, _, _, _, _, _, _, _, _, _, k) =>
|
|
166
|
-
|
|
177
|
+
return k.at(0).x
|
|
178
|
+
_ => 0
|
|
167
179
|
0.0 - 1.0
|
|
168
180
|
|
|
169
181
|
fun ttFirstKidActions(a: Actor) -> Int =
|
|
170
|
-
ks := a.kids()
|
|
171
|
-
|
|
182
|
+
match a
|
|
183
|
+
Group(_, _, _, _, _, _, _, _, _, _, _, _, _, _, k) =>
|
|
172
|
-
|
|
184
|
+
return k.at(0).actions.length()
|
|
185
|
+
_ => 0
|
|
173
186
|
0 - 1
|
|
174
187
|
|
|
175
188
|
fun ttSetTouch(own t: Actor, id: Int, tc: Touchable) -> Actor =
|
|
@@ -188,9 +201,10 @@ fun ttSetX9(own a: Actor) -> Actor =
|
|
|
188
201
|
a
|
|
189
202
|
|
|
190
203
|
fun ttKidX(a: Actor, i: Int) -> Float =
|
|
191
|
-
ks := a.kids()
|
|
192
|
-
|
|
204
|
+
match a
|
|
205
|
+
Group(_, _, _, _, _, _, _, _, _, _, _, _, _, _, k) =>
|
|
193
|
-
|
|
206
|
+
return k.at(i).x
|
|
207
|
+
_ => 0
|
|
194
208
|
0.0 - 1.0
|
|
195
209
|
|
|
196
210
|
fun ttButtonRoot() -> Actor =
|
|
@@ -205,6 +219,33 @@ fun ttZero() -> Slice[Int] =
|
|
|
205
219
|
out.push(0)
|
|
206
220
|
out
|
|
207
221
|
|
|
222
|
+
# Five actions on one sprite: the 1st, 3rd and 5th finish in the first 0.1s step,
|
|
223
|
+
# the 2nd (MoveBy) and 4th (RotateBy) keep running.
|
|
224
|
+
fun ttCompactTree() -> Actor =
|
|
225
|
+
s := PureScene.sprite(2, 0, 0.0, 0.0, 10.0, 10.0)
|
|
226
|
+
s.actions = Slice(Actions.moveBy(1.0, 0.0, 0.1), Actions.moveBy(0.0, 10.0, 10.0), Actions.rotateBy(1.0, 0.1), Actions.rotateBy(2.0, 10.0), Actions.delay(0.1))
|
|
227
|
+
kids := Slice[Actor]()
|
|
228
|
+
kids.push(s)
|
|
229
|
+
root := PureScene.group(1, 0.0, 0.0, 100.0, 100.0, kids)
|
|
230
|
+
me: Actor := root
|
|
231
|
+
me
|
|
232
|
+
|
|
233
|
+
fun ttKid(a: Actor) -> Actor =
|
|
234
|
+
match a
|
|
235
|
+
Group(_, _, _, _, _, _, _, _, _, _, _, _, _, _, k) =>
|
|
236
|
+
return k.at(0)
|
|
237
|
+
_ => 0
|
|
238
|
+
none: Actor := PureScene.sprite(0, 0, 0.0, 0.0, 0.0, 0.0)
|
|
239
|
+
none
|
|
240
|
+
|
|
241
|
+
# 1 MoveBy, 2 RotateBy, 3 Delay, 0 anything else.
|
|
242
|
+
fun ttActKind(a: Action) -> Int =
|
|
243
|
+
match a
|
|
244
|
+
MoveBy => 1
|
|
245
|
+
RotateBy => 2
|
|
246
|
+
Delay => 3
|
|
247
|
+
_ => 0
|
|
248
|
+
|
|
208
249
|
test "find_hit_nested"
|
|
209
250
|
r := SceneTree.findHit(ttFixture(), Affine.Identity, ttAt(21.0, 1.0))
|
|
210
251
|
assert ttPathEq(r, ttInts(1, 0))
|
|
@@ -290,3 +331,23 @@ test "stale_path_reaches_group_only"
|
|
|
290
331
|
nine.push(9)
|
|
291
332
|
h := SceneTree.route(ttButtonRoot(), nine, 0, Pointer(kind: Down, x: 20.0, y: 20.0, over: true))
|
|
292
333
|
assert h.events.length() == 0
|
|
334
|
+
|
|
335
|
+
test "compaction_keeps_order"
|
|
336
|
+
n := SceneTree.update(ttCompactTree(), 0.1)
|
|
337
|
+
acts := ttKid(n).actions
|
|
338
|
+
assert acts.length() == 2
|
|
339
|
+
assert ttActKind(acts.at(0)) == 1
|
|
340
|
+
assert ttActKind(acts.at(1)) == 2
|
|
341
|
+
assert {ttKid(n).x - 1.0}.abs() < 1e-6
|
|
342
|
+
assert {ttKid(n).y - 0.1}.abs() < 1e-6
|
|
343
|
+
assert {ttKid(n).rotation - 1.02}.abs() < 1e-6
|
|
344
|
+
# The next step advances the right survivors: MoveBy 0.1 -> 0.6, RotateBy 0.1 -> 0.6.
|
|
345
|
+
m := SceneTree.update(n, 0.5)
|
|
346
|
+
acts2 := ttKid(m).actions
|
|
347
|
+
assert acts2.length() == 2
|
|
348
|
+
assert ttActKind(acts2.at(0)) == 1
|
|
349
|
+
assert ttActKind(acts2.at(1)) == 2
|
|
350
|
+
assert actElapsed(acts2.at(0)) == 0.6
|
|
351
|
+
assert {ttKid(m).y - 0.6}.abs() < 1e-6
|
|
352
|
+
assert {ttKid(m).rotation - 1.12}.abs() < 1e-6
|
|
353
|
+
assert {ttKid(m).x - 1.0}.abs() < 1e-6
|