plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
614ddf5
— Peter John
2026-11-03T18:02:43+05:30
Tail calls round 2: closure and error-union tail calls, single-pass TRMC, fast dev GC
- Cargo.toml +9 -0
- docs/plans/2026-11-03-gc-report.md +43 -0
- docs/plans/2026-11-03-tail-calls-2.progress.md +23 -0
- docs/plans/2026-11-03-tail-calls-2.report.json +225 -0
- docs/plans/2026-11-03-tail-calls-2.report.md +40 -0
- plum-core/src/trmc.rs +52 -60
- plum-core/tests/trmc_test.rs +82 -0
- plum-examples/functional.plum +1 -2
- plum-examples/tail_calls.plum +35 -15
- plum-wasm-codegen/src/lib.rs +95 -4
- plum-wasm-codegen/tests/closure_tail_e2e_test.rs +83 -0
- plum-wasm-codegen/tests/error_tail_e2e_test.rs +91 -0
Cargo.toml
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
[workspace]
|
|
2
2
|
members = ["plum-core", "plum-cli", "plum-checker", "plum-wasm-codegen", "plum-lsp"]
|
|
3
3
|
resolver = "2"
|
|
4
|
+
|
|
5
|
+
[profile.dev.package.wasmtime]
|
|
6
|
+
opt-level = 3
|
|
7
|
+
|
|
8
|
+
[profile.dev.package.cranelift-codegen]
|
|
9
|
+
opt-level = 3
|
|
10
|
+
|
|
11
|
+
[profile.dev.package.wasmtime-internal-cranelift]
|
|
12
|
+
opt-level = 3
|
docs/plans/2026-11-03-gc-report.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# GC investigation: why a 1M-cell Link list takes ~35 s under `plum test`
|
|
2
|
+
|
|
3
|
+
Date 2026-11-03. Probes: /private/tmp/claude-501/gc/. Program: `enum Link[T] = Nil | Cons(head: T, tail: Link[T])`,
|
|
4
|
+
`fromTo(a, b)` (tail modulo cons) then `length`; also `build` (accumulator) then `sum`. Wall time of the whole
|
|
5
|
+
`plum test <file>` run (compile included, about 100 ms) unless noted.
|
|
6
|
+
|
|
7
|
+
## Results
|
|
8
|
+
|
|
9
|
+
| Variant | fromTo+length 100k ms | fromTo+length 1M ms | build+sum 1M ms |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| dev build, wasmtime 47 default (Auto = copying) | 1329 | 36947 | 3480 |
|
|
12
|
+
| dev, collector DRC | - | 12382 | 4329 |
|
|
13
|
+
| dev, collector Copying (explicit) | - | 35973 | 3186 |
|
|
14
|
+
| dev, Null collector (no GC) | - | 198 | 170 |
|
|
15
|
+
| dev, gc_heap_initial_size 64 MiB | - | 31555 | - |
|
|
16
|
+
| dev, gc_heap_initial_size 256 MiB | - | 206 | - |
|
|
17
|
+
| dev, wasmtime crate at opt-level 3 (`--config profile.dev.package.wasmtime.opt-level=3`) | 1830 (cold) | 1238 | 280 |
|
|
18
|
+
| release build of plum-cli | 68 | 448 | 97 |
|
|
19
|
+
| V8 (node 26, same engine as Chrome), `plum compile` output, `main` | - | 128 | - |
|
|
20
|
+
|
|
21
|
+
Chrome itself was not driven: plum-game/web-test only runs `plum game` pages (playwright-rs, needs a
|
|
22
|
+
renderer and frames), so node (V8) running the compiled wasm directly was used. Its only import is `crash`.
|
|
23
|
+
|
|
24
|
+
## Root cause
|
|
25
|
+
|
|
26
|
+
1. The collector is the wasmtime 47 semi-space copying collector (Auto resolves to it). `sample` shows nearly all
|
|
27
|
+
time in `copying::` / `GcHeap::index` / `is_i31` helpers, with tiny frames such as `NonZero::get` and
|
|
28
|
+
`Option::is_some` not inlined: wasmtime is compiled at the dev profile (opt-level 0). The same collector is
|
|
29
|
+
about 80x faster optimised (37 s to 0.45 s release; 1.2 s with only the wasmtime crate at opt-level 3).
|
|
30
|
+
2. Cost is superlinear (100k to 1M is 28x): each collection copies all live cells and the heap grows by doubling
|
|
31
|
+
from 0, so many full-heap copies happen while the list is live. Avoiding GC (Null, or a 256 MiB initial heap)
|
|
32
|
+
removes it; a 64 MiB initial heap is not enough.
|
|
33
|
+
3. Codegen was not examined in depth (read-only scope); the cost is in the collector, and V8 handles the same
|
|
34
|
+
module in 128 ms, so the layout does not defeat a fast path.
|
|
35
|
+
|
|
36
|
+
## Recommendation
|
|
37
|
+
|
|
38
|
+
- Not an engine limitation and not a codegen problem: it is the unoptimised dev-profile wasmtime. Add to the
|
|
39
|
+
root Cargo.toml (not owned by T5): `[profile.dev.package.wasmtime] opt-level = 3`. Expected 1M list
|
|
40
|
+
build+length about 1 s; the 200,000-cell limits in depth tests could then go to 1,000,000.
|
|
41
|
+
- `gc_heap_initial_size(256 MiB)` also fixes it (lazily committed) but moved the full sweep only from 39 s to
|
|
42
|
+
33 s (607 passed), not the 2x bar, and is redundant with the profile fix, so no host config was changed.
|
|
43
|
+
- Collector switch (DRC) is 3x better on this program only and worse on build+sum; not recommended.
|
docs/plans/2026-11-03-tail-calls-2.progress.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Tail calls round 2 run
|
|
2
|
+
- start 184c054
|
|
3
|
+
- Ruling: skip T0 Wave 0 - no Plum/Rust stub generation - cost if wrong: none
|
|
4
|
+
- Ruling: sequential [codegen] T1-T4 share plum-wasm-codegen/src/lib.rs - cost: longer critical path
|
|
5
|
+
- unblocked: dispatched T1 T5 at 17:36:47; T2-T4, T6 follow sequentially
|
|
6
|
+
- checkpoint T1 4333a5e1ef closure tail calls and recursive local closures
|
|
7
|
+
- T1 checkpointed (closure-body tail calls; := closure self-recursion via own table slot; concern: checker does not bind closure's own name for return-type inference)
|
|
8
|
+
- unblocked: dispatched T2 at 17:46:04
|
|
9
|
+
- checkpoint T2 c121cb0f42 error-union tail calls
|
|
10
|
+
- T2 checkpointed c121cb0 (error-union tail calls when result type identical, no ERR_* coercion)
|
|
11
|
+
- unblocked: T3 sent to T2's agent (low context) at 17:49:06
|
|
12
|
+
- Ruling: [scope] T3 worker/wrapper dropped - self calls never need conversion; Int->Number is an AST wrap; remaining case (mutual recursion across T->T?) needs whole-body workers and a function-reservation redesign; review plan listed skip as the alternative - cost if wrong: mutual recursion through a conversion still grows the stack
|
|
13
|
+
- T4 sent to a5ec608 at 17:50:06
|
|
14
|
+
- T5 checkpointed: root cause = wasmtime compiled at dev opt-level 0 (copying GC 30x slow); fix [profile.dev.package.wasmtime] opt-level=3 (37s -> 1.2s for 1M list); V8 128ms. Ruling: apply after T4 (full wasmtime rebuild would collide with T4 builds) - cost: none
|
|
15
|
+
- checkpoint T4 4563fa23ad single-pass tail recursion modulo cons
|
|
16
|
+
- checkpoint T5 a8c6d5e639 GC investigation
|
|
17
|
+
- T4 checkpointed 4563fa2 (single-pass TRMC: Nil placeholder, fill hidden-record field; 2 struct.new per cell = hidden record + union wrapper: representation cost, follow-up). T5 checkpointed
|
|
18
|
+
- Ruling: T6 also owns root Cargo.toml for [profile.dev.package.wasmtime] opt-level=3 (+cranelift) - GC report; 30x dev GC speedup - cost if wrong: longer first rebuild
|
|
19
|
+
- unblocked: dispatched T6 at 17:58:42 (T3 dropped)
|
|
20
|
+
- checkpoint T6 8de6916e9d depth tests round 2
|
|
21
|
+
- T6 checkpointed: Cargo dev opt-level 3 for wasmtime/cranelift-codegen/wasmtime-internal-cranelift; tail_calls 13/13 (1M lists, 2.2s); sweep 611 in 21s (was 38s)
|
|
22
|
+
- tests full passed=80 failed=1 skipped=0 seconds=79.4
|
|
23
|
+
- Final review APPROVED; full suite 1 failure = known generic_funstar_wired (reads untracked repro in main checkout) - Ruling: pre-existing, not ours - cost if wrong: none
|
docs/plans/2026-11-03-tail-calls-2.report.json
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
{
|
|
2
|
+
"projection_note": "projection: fixed forecast saved at lint 2026-11-03T17:36:25",
|
|
3
|
+
"headline": {
|
|
4
|
+
"wall_min": {
|
|
5
|
+
"projected": 28.497,
|
|
6
|
+
"actual": 24.167
|
|
7
|
+
},
|
|
8
|
+
"cost_usd_agents": {
|
|
9
|
+
"projected": 0.645714,
|
|
10
|
+
"actual": 1.954344
|
|
11
|
+
},
|
|
12
|
+
"task_min": {
|
|
13
|
+
"projected": 31.399,
|
|
14
|
+
"actual": 36.683
|
|
15
|
+
},
|
|
16
|
+
"agents": {
|
|
17
|
+
"projected": 6,
|
|
18
|
+
"actual": 12
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"feature": null,
|
|
22
|
+
"cost_split": {
|
|
23
|
+
"agents_usd": 1.954344,
|
|
24
|
+
"rework_usd": 0,
|
|
25
|
+
"coordinator_usd": 34.877211,
|
|
26
|
+
"total_usd": 36.831555
|
|
27
|
+
},
|
|
28
|
+
"concurrency": {
|
|
29
|
+
"peak": 2,
|
|
30
|
+
"projected_peak": 2
|
|
31
|
+
},
|
|
32
|
+
"critical_path": {
|
|
33
|
+
"actual": [
|
|
34
|
+
"T6"
|
|
35
|
+
],
|
|
36
|
+
"projected": [
|
|
37
|
+
"T1",
|
|
38
|
+
"T2",
|
|
39
|
+
"T3",
|
|
40
|
+
"T4",
|
|
41
|
+
"T6"
|
|
42
|
+
],
|
|
43
|
+
"projected_min": 28.497
|
|
44
|
+
},
|
|
45
|
+
"model_mix": {
|
|
46
|
+
"sonnet": {
|
|
47
|
+
"tasks": 5,
|
|
48
|
+
"cost_usd": 1.9543439999999999
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"coordinator_cost_by_model": {
|
|
52
|
+
"opus": 34.877211
|
|
53
|
+
},
|
|
54
|
+
"quality": {
|
|
55
|
+
"tests": {
|
|
56
|
+
"passed": 80,
|
|
57
|
+
"failed": 1,
|
|
58
|
+
"skipped": 0,
|
|
59
|
+
"seconds": 79.4,
|
|
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": 0,
|
|
69
|
+
"open_incidents": 3
|
|
70
|
+
},
|
|
71
|
+
"tasks": [
|
|
72
|
+
{
|
|
73
|
+
"task": "T5",
|
|
74
|
+
"model": "sonnet",
|
|
75
|
+
"pred_min": 2.902,
|
|
76
|
+
"actual_min": 14.237,
|
|
77
|
+
"delta": {
|
|
78
|
+
"abs": 11.3347,
|
|
79
|
+
"pct": 390.6
|
|
80
|
+
},
|
|
81
|
+
"cost_usd": 0.463526,
|
|
82
|
+
"first_time_pass": null
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"task": "T1",
|
|
86
|
+
"model": "sonnet",
|
|
87
|
+
"pred_min": 7.071,
|
|
88
|
+
"actual_min": 8.822,
|
|
89
|
+
"delta": {
|
|
90
|
+
"abs": 1.7507,
|
|
91
|
+
"pct": 24.8
|
|
92
|
+
},
|
|
93
|
+
"cost_usd": 0.593103,
|
|
94
|
+
"first_time_pass": null
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"task": "T6",
|
|
98
|
+
"model": "sonnet",
|
|
99
|
+
"pred_min": 3.655,
|
|
100
|
+
"actual_min": 2.078,
|
|
101
|
+
"delta": {
|
|
102
|
+
"abs": -1.5767,
|
|
103
|
+
"pct": -43.1
|
|
104
|
+
},
|
|
105
|
+
"cost_usd": 0.120844,
|
|
106
|
+
"first_time_pass": null
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"task": "T3",
|
|
110
|
+
"model": "sonnet",
|
|
111
|
+
"pred_min": 6.292,
|
|
112
|
+
"actual_min": 7.742,
|
|
113
|
+
"delta": {
|
|
114
|
+
"abs": 1.4497,
|
|
115
|
+
"pct": 23.0
|
|
116
|
+
},
|
|
117
|
+
"cost_usd": 0.511579,
|
|
118
|
+
"first_time_pass": null
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"task": "T2",
|
|
122
|
+
"model": "sonnet",
|
|
123
|
+
"pred_min": 4.032,
|
|
124
|
+
"actual_min": 3.805,
|
|
125
|
+
"delta": {
|
|
126
|
+
"abs": -0.227,
|
|
127
|
+
"pct": -5.6
|
|
128
|
+
},
|
|
129
|
+
"cost_usd": 0.265292,
|
|
130
|
+
"first_time_pass": null
|
|
131
|
+
}
|
|
132
|
+
],
|
|
133
|
+
"cost_table": {
|
|
134
|
+
"source": "est",
|
|
135
|
+
"usd": 2.1147,
|
|
136
|
+
"tokens": 7044653,
|
|
137
|
+
"wall_min": 209266028.3251,
|
|
138
|
+
"rows": [
|
|
139
|
+
{
|
|
140
|
+
"agent": "a0f105af8ea244c3a",
|
|
141
|
+
"model": "opus",
|
|
142
|
+
"wall_min": 0.5564,
|
|
143
|
+
"tokens": 36884,
|
|
144
|
+
"usd": 0.1604
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"agent": "a22bfa5a8592be805",
|
|
148
|
+
"model": "sonnet",
|
|
149
|
+
"wall_min": 29895141.3545,
|
|
150
|
+
"tokens": 0,
|
|
151
|
+
"usd": 0.0
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"agent": "a2815b33cc0ba0797",
|
|
155
|
+
"model": "sonnet",
|
|
156
|
+
"wall_min": 29895136.1811,
|
|
157
|
+
"tokens": 0,
|
|
158
|
+
"usd": 0.0
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"agent": "a3a65d262ba03d8fc",
|
|
162
|
+
"model": "sonnet",
|
|
163
|
+
"wall_min": 2.0893,
|
|
164
|
+
"tokens": 277089,
|
|
165
|
+
"usd": 0.1208
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"agent": "a404dd243c627057d",
|
|
169
|
+
"model": "sonnet",
|
|
170
|
+
"wall_min": 29895149.0868,
|
|
171
|
+
"tokens": 0,
|
|
172
|
+
"usd": 0.0
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"agent": "a4a70cf22b4155982",
|
|
176
|
+
"model": "sonnet",
|
|
177
|
+
"wall_min": 29895134.2054,
|
|
178
|
+
"tokens": 0,
|
|
179
|
+
"usd": 0.0
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"agent": "a5ec60874cf21dddc",
|
|
183
|
+
"model": "sonnet",
|
|
184
|
+
"wall_min": 11.7861,
|
|
185
|
+
"tokens": 2965290,
|
|
186
|
+
"usd": 0.7769
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"agent": "a9452d14b2f6c0237",
|
|
190
|
+
"model": "sonnet",
|
|
191
|
+
"wall_min": 8.8463,
|
|
192
|
+
"tokens": 2032735,
|
|
193
|
+
"usd": 0.5931
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"agent": "ab9989c3eeb17d419",
|
|
197
|
+
"model": "sonnet",
|
|
198
|
+
"wall_min": 29895150.5785,
|
|
199
|
+
"tokens": 0,
|
|
200
|
+
"usd": 0.0
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"agent": "adea4ce43c6426be0",
|
|
204
|
+
"model": "sonnet",
|
|
205
|
+
"wall_min": 29895139.1863,
|
|
206
|
+
"tokens": 0,
|
|
207
|
+
"usd": 0.0
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"agent": "af440f21f781f3db9",
|
|
211
|
+
"model": "sonnet",
|
|
212
|
+
"wall_min": 29895140.2048,
|
|
213
|
+
"tokens": 0,
|
|
214
|
+
"usd": 0.0
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"agent": "afdd594a53598281f",
|
|
218
|
+
"model": "sonnet",
|
|
219
|
+
"wall_min": 14.2496,
|
|
220
|
+
"tokens": 1732655,
|
|
221
|
+
"usd": 0.4635
|
|
222
|
+
}
|
|
223
|
+
]
|
|
224
|
+
}
|
|
225
|
+
}
|
docs/plans/2026-11-03-tail-calls-2.report.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# superlite build report
|
|
2
|
+
|
|
3
|
+
projection: fixed forecast saved at lint 2026-11-03T17:36:25
|
|
4
|
+
|
|
5
|
+
Projected Actual Delta
|
|
6
|
+
wall-clock (min) 28.5 24.2 -4.3 (-15%)
|
|
7
|
+
agent cost (USD, +rework) 0.65 1.95 +1.31 (+203%)
|
|
8
|
+
task-minutes 31.4 36.7 +5.3 (+17%)
|
|
9
|
+
agents 6 12 +6 (+100%)
|
|
10
|
+
cost split: agents $1.95, rework $0.00, coordinator $34.88, total $36.83
|
|
11
|
+
concurrency: peak 2 at once (projected 2); critical path actual T6; projected T1 > T2 > T3 > T4 > T6 (28.5 min)
|
|
12
|
+
model mix: sonnet 5 tasks $1.95, coordinator opus $34.88
|
|
13
|
+
quality: tests 80 passed, 1 failed; first-time pass n/a; rework dispatches 0; open incidents 3
|
|
14
|
+
|
|
15
|
+
task model pred actual delta cost
|
|
16
|
+
T5 sonnet 2.9 14.2 +11.3 (+391%) $0.464
|
|
17
|
+
T1 sonnet 7.1 8.8 +1.8 (+25%) $0.593
|
|
18
|
+
T6 sonnet 3.7 2.1 -1.6 (-43%) $0.121
|
|
19
|
+
T3 sonnet 6.3 7.7 +1.4 (+23%) $0.512
|
|
20
|
+
T2 sonnet 4.0 3.8 -0.2 (-6%) $0.265
|
|
21
|
+
|
|
22
|
+
agent cost table:
|
|
23
|
+
| agent | model | wall_min | tokens | usd |
|
|
24
|
+
| --- | --- | --- | --- | --- |
|
|
25
|
+
| a0f105af8ea244c3a | opus | 0.5564 | 36884 | 0.1604 |
|
|
26
|
+
| a22bfa5a8592be805 | sonnet | 29895141.3545 | 0 | 0.0 |
|
|
27
|
+
| a2815b33cc0ba0797 | sonnet | 29895136.1811 | 0 | 0.0 |
|
|
28
|
+
| a3a65d262ba03d8fc | sonnet | 2.0893 | 277089 | 0.1208 |
|
|
29
|
+
| a404dd243c627057d | sonnet | 29895149.0868 | 0 | 0.0 |
|
|
30
|
+
| a4a70cf22b4155982 | sonnet | 29895134.2054 | 0 | 0.0 |
|
|
31
|
+
| a5ec60874cf21dddc | sonnet | 11.7861 | 2965290 | 0.7769 |
|
|
32
|
+
| a9452d14b2f6c0237 | sonnet | 8.8463 | 2032735 | 0.5931 |
|
|
33
|
+
| ab9989c3eeb17d419 | sonnet | 29895150.5785 | 0 | 0.0 |
|
|
34
|
+
| adea4ce43c6426be0 | sonnet | 29895139.1863 | 0 | 0.0 |
|
|
35
|
+
| af440f21f781f3db9 | sonnet | 29895140.2048 | 0 | 0.0 |
|
|
36
|
+
| afdd594a53598281f | sonnet | 14.2496 | 1732655 | 0.4635 |
|
|
37
|
+
| totals | | 209266028.3251 | 7044653 | 2.1147 |
|
|
38
|
+
cost source: est
|
|
39
|
+
|
|
40
|
+
takeaway: ran 15% faster than projected; cost 203% over; biggest miss T5
|
plum-core/src/trmc.rs
CHANGED
|
@@ -171,9 +171,10 @@ impl<'a> Cx<'a> {
|
|
|
171
171
|
if !self.found {
|
|
172
172
|
return None;
|
|
173
173
|
}
|
|
174
|
-
|
|
174
|
+
self.cons.as_ref()?;
|
|
175
175
|
let mut stmts = vec![
|
|
176
176
|
declare("__tc_acc", Some(self.ret.clone()), Expr::TypeName(self.nullary.clone())),
|
|
177
|
+
declare("__tc_res", Some(self.ret.clone()), Expr::TypeName(self.nullary.clone())),
|
|
177
178
|
declare("__tc_r", Some(self.ret.clone()), Expr::TypeName(self.nullary.clone())),
|
|
178
179
|
declare("__tc_go", None, Expr::Int(1)),
|
|
179
180
|
Stmt::While(While {
|
|
@@ -185,67 +186,54 @@ impl<'a> Cx<'a> {
|
|
|
185
186
|
body: loopBody,
|
|
186
187
|
}),
|
|
187
188
|
];
|
|
188
|
-
//
|
|
189
|
+
// Fill the last cell's recursive field with the base value (or, when no
|
|
189
|
-
let binders: Vec<String> = (0..fields.len()).map(|i| format!("__tc_p{i}")).collect();
|
|
190
|
-
let args: Vec<FieldArg> = fields
|
|
191
|
-
.iter()
|
|
192
|
-
.zip(&binders)
|
|
193
|
-
.enumerate()
|
|
194
|
-
.map(|(i, (n, b))| FieldArg {
|
|
195
|
-
name: n.clone(),
|
|
196
|
-
value: if i == rec { var("__tc_r") } else { var(b) },
|
|
197
|
-
})
|
|
198
|
-
.collect();
|
|
199
|
-
let rebuild = Case {
|
|
200
|
-
patterns: vec![{
|
|
201
|
-
let inner = CasePattern::Class {
|
|
202
|
-
name: cons.clone(),
|
|
203
|
-
fields: binders.iter().map(|b| CasePattern::Name(b.clone())).collect(),
|
|
204
|
-
};
|
|
205
|
-
match &self.wrapper {
|
|
206
|
-
Some(w) => CasePattern::Class { name: w.clone(), fields: vec![inner] },
|
|
207
|
-
None => inner,
|
|
208
|
-
}
|
|
209
|
-
}],
|
|
210
|
-
guard: None,
|
|
211
|
-
body: Block {
|
|
212
|
-
stmts: vec![
|
|
213
|
-
set(
|
|
214
|
-
"__tc_r",
|
|
215
|
-
wrap(
|
|
216
|
-
&self.wrapper,
|
|
217
|
-
Expr::ClassCall(ClassCall {
|
|
218
|
-
type_name: cons,
|
|
219
|
-
fields: args,
|
|
220
|
-
generics: self.consGenerics.clone(),
|
|
221
|
-
spread: None,
|
|
222
|
-
}),
|
|
223
|
-
),
|
|
224
|
-
),
|
|
225
|
-
|
|
190
|
+
// cell was built, the base value is the result).
|
|
226
|
-
],
|
|
227
|
-
},
|
|
228
|
-
};
|
|
229
|
-
let stop = Case {
|
|
230
|
-
patterns: vec![CasePattern::Wildcard],
|
|
231
|
-
guard: None,
|
|
232
|
-
body: Block { stmts: vec![Stmt::Break] },
|
|
233
|
-
};
|
|
234
|
-
stmts.push(Stmt::While(While {
|
|
235
|
-
condition: Expr::BoolLit(true),
|
|
236
|
-
body: Block {
|
|
237
|
-
stmts: vec![Stmt::Match(Match {
|
|
238
|
-
|
|
191
|
+
stmts.extend(self.fill(var("__tc_r")));
|
|
239
|
-
cases: vec![rebuild, stop],
|
|
240
|
-
})],
|
|
241
|
-
},
|
|
242
|
-
}));
|
|
243
|
-
stmts.push(Stmt::Return(Some(var("
|
|
192
|
+
stmts.push(Stmt::Return(Some(var("__tc_res"))));
|
|
244
193
|
let mut nf = self.f.clone();
|
|
245
194
|
nf.body = FnBody::Block(Block { stmts });
|
|
246
195
|
Some(nf)
|
|
247
196
|
}
|
|
248
197
|
|
|
198
|
+
/// Stores `value` in the recursive field of the last built cell, or in
|
|
199
|
+
/// `__tc_res` when no cell exists yet. Cells are mutated in place through the
|
|
200
|
+
/// record behind the wrapper variant; `__tc_acc` always holds the last cell.
|
|
201
|
+
fn fill(&self, value: Expr) -> Vec<Stmt> {
|
|
202
|
+
let (cons, fields, rec) = self.cons.clone().expect("cons known");
|
|
203
|
+
// Before union lowering a cell is not wrapped (only the unit tests of the
|
|
204
|
+
// pass see that shape); the real pipeline always has the wrapper variant.
|
|
205
|
+
let (w, target) = match &self.wrapper {
|
|
206
|
+
Some(w) => (w.clone(), "__tc_c"),
|
|
207
|
+
None => (cons, "__tc_acc"),
|
|
208
|
+
};
|
|
209
|
+
let patFields = if self.wrapper.is_some() { vec![CasePattern::Name("__tc_c".to_string())] } else { vec![] };
|
|
210
|
+
vec![Stmt::Match(Match {
|
|
211
|
+
subjects: vec![var("__tc_acc")],
|
|
212
|
+
cases: vec![
|
|
213
|
+
Case {
|
|
214
|
+
patterns: vec![CasePattern::Class {
|
|
215
|
+
name: w,
|
|
216
|
+
fields: patFields,
|
|
217
|
+
}],
|
|
218
|
+
guard: None,
|
|
219
|
+
body: Block {
|
|
220
|
+
stmts: vec![Stmt::Assign(Assign {
|
|
221
|
+
targets: vec![AssignTarget::Field(Box::new(var(target)), fields[rec].clone())],
|
|
222
|
+
values: vec![value.clone()],
|
|
223
|
+
declare: false,
|
|
224
|
+
ty: None,
|
|
225
|
+
})],
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
Case {
|
|
229
|
+
patterns: vec![CasePattern::Wildcard],
|
|
230
|
+
guard: None,
|
|
231
|
+
body: Block { stmts: vec![set("__tc_res", value)] },
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
})]
|
|
235
|
+
}
|
|
236
|
+
|
|
249
237
|
// ---- tail-position walk ----
|
|
250
238
|
|
|
251
239
|
fn tailBlock(&mut self, b: Block) -> Option<Block> {
|
|
@@ -405,7 +393,7 @@ impl<'a> Cx<'a> {
|
|
|
405
393
|
let mut fields = Vec::new();
|
|
406
394
|
for (i, f) in c.fields.iter().enumerate() {
|
|
407
395
|
if i == k {
|
|
408
|
-
fields.push(FieldArg { name: f.name.clone(), value:
|
|
396
|
+
fields.push(FieldArg { name: f.name.clone(), value: Expr::TypeName(self.nullary.clone()) });
|
|
409
397
|
} else if i < k && !pure(&f.value) {
|
|
410
398
|
let t = self.fresh("f");
|
|
411
399
|
stmts.push(declare(&t, None, f.value.clone()));
|
|
@@ -428,8 +416,10 @@ impl<'a> Cx<'a> {
|
|
|
428
416
|
moves.push(set(&p.name, var(&t)));
|
|
429
417
|
}
|
|
430
418
|
}
|
|
419
|
+
let cell = self.fresh("cell");
|
|
431
|
-
stmts.push(
|
|
420
|
+
stmts.push(declare(
|
|
432
|
-
|
|
421
|
+
&cell,
|
|
422
|
+
None,
|
|
433
423
|
wrap(
|
|
434
424
|
&wrapper,
|
|
435
425
|
Expr::ClassCall(ClassCall {
|
|
@@ -440,6 +430,8 @@ impl<'a> Cx<'a> {
|
|
|
440
430
|
}),
|
|
441
431
|
),
|
|
442
432
|
));
|
|
433
|
+
stmts.extend(self.fill(var(&cell)));
|
|
434
|
+
stmts.push(set("__tc_acc", var(&cell)));
|
|
443
435
|
stmts.extend(moves);
|
|
444
436
|
Some(Some(stmts))
|
|
445
437
|
}
|
plum-core/tests/trmc_test.rs
CHANGED
|
@@ -258,6 +258,88 @@ fun main() -> Int =
|
|
|
258
258
|
assert_eq!(runInt("order2", &p), 192939);
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
/// Number of `struct.new` in the wasm fn whose debug name is `name`.
|
|
262
|
+
fn structNewCount(wasm: &[u8], name: &str) -> usize {
|
|
263
|
+
let mut names = std::collections::HashMap::new();
|
|
264
|
+
let mut imports = 0u32;
|
|
265
|
+
for p in wasmparser::Parser::new(0).parse_all(wasm) {
|
|
266
|
+
match p.unwrap() {
|
|
267
|
+
wasmparser::Payload::ImportSection(r) => imports = r.count(),
|
|
268
|
+
wasmparser::Payload::CustomSection(c) => {
|
|
269
|
+
if let wasmparser::KnownCustom::Name(r) = c.as_known() {
|
|
270
|
+
for n in r {
|
|
271
|
+
if let Ok(wasmparser::Name::Function(m)) = n {
|
|
272
|
+
for e in m.into_iter().flatten() {
|
|
273
|
+
names.insert(e.index, e.name.to_string());
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
_ => {}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
let mut idx = imports;
|
|
283
|
+
let mut found = None;
|
|
284
|
+
for p in wasmparser::Parser::new(0).parse_all(wasm) {
|
|
285
|
+
if let wasmparser::Payload::CodeSectionEntry(b) = p.unwrap() {
|
|
286
|
+
if names.get(&idx).is_some_and(|n| n == name || n.starts_with(&format!("{name}["))) {
|
|
287
|
+
let mut r = b.get_operators_reader().unwrap();
|
|
288
|
+
let mut n = 0;
|
|
289
|
+
while !r.eof() {
|
|
290
|
+
if matches!(r.read().unwrap(), wasmparser::Operator::StructNew { .. }) {
|
|
291
|
+
n += 1;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
found = Some(n);
|
|
295
|
+
}
|
|
296
|
+
idx += 1;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
found.unwrap_or_else(|| panic!("no fn {name}: {names:?}"))
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#[test]
|
|
303
|
+
fn map_single_pass_one_alloc_per_cell() {
|
|
304
|
+
let p = prog("fun main() -> Int = count(map(fromTo(0, 3), |x| x + 1))\n");
|
|
305
|
+
let bytes = compileSource(&load("single", &p)).unwrap();
|
|
306
|
+
// One record + its wrapper variant per cell, in one loop (two loops before).
|
|
307
|
+
assert_eq!(structNewCount(&bytes, "map"), 2);
|
|
308
|
+
assert_eq!(structNewCount(&bytes, "fromTo"), 2);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
#[test]
|
|
312
|
+
fn user_code_cannot_assign_variant_field() {
|
|
313
|
+
let p = prog("fun poke(xs: Link[Int]) -> Int =\n xs.tail = Nil\n 0\n\nfun main() -> Int = 0\n");
|
|
314
|
+
let source = load("poke", &p);
|
|
315
|
+
let e = plum_checker::checkSource(&source).expect_err("must be rejected");
|
|
316
|
+
assert!(format!("{e:?}").contains("no field"), "{e:?}");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
#[test]
|
|
320
|
+
fn every_cell_filled_200k() {
|
|
321
|
+
let p = prog(
|
|
322
|
+
"\
|
|
323
|
+
fun main() -> Int =
|
|
324
|
+
big := map(fromTo(0, 200000), |x| x + 1)
|
|
325
|
+
if count(big) == 200000 && sum(big) == 20000100000 then 1 else 0
|
|
326
|
+
",
|
|
327
|
+
);
|
|
328
|
+
assert_eq!(runInt("fill200k", &p), 1);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
#[test]
|
|
332
|
+
fn fromTo_single_pass_200k() {
|
|
333
|
+
let p = prog(
|
|
334
|
+
"\
|
|
335
|
+
fun main() -> Int =
|
|
336
|
+
big := fromTo(0, 200000)
|
|
337
|
+
if count(big) == 200000 && sum(big) == 19999900000 then 1 else 0
|
|
338
|
+
",
|
|
339
|
+
);
|
|
340
|
+
assert_eq!(runInt("ft200k", &p), 1);
|
|
341
|
+
}
|
|
342
|
+
|
|
261
343
|
#[test]
|
|
262
344
|
fn pass_reports_names() {
|
|
263
345
|
let p = prog("fun main() -> Int = 0\n");
|
plum-examples/functional.plum
CHANGED
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
#
|
|
19
19
|
# 6. Loops, functionally: the same job as methods, a `for` and a `while`
|
|
20
20
|
#
|
|
21
|
-
# Calls in tail position compile to real tail calls, so a tail-recursive walk
|
|
22
|
-
#
|
|
21
|
+
# Tail calls and constructor-wrapped self calls run in constant stack; other recursion is limited to about 260,000 deep.
|
|
23
22
|
# Scene trees are shallow, so the rest recurses plainly.
|
|
24
23
|
module functional
|
|
25
24
|
import std/Str
|
plum-examples/tail_calls.plum
CHANGED
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
#
|
|
3
3
|
# Self tail calls compile to loops, other tail calls use `return_call`, and a
|
|
4
4
|
# constructor wrapped self call (`Cons(head: f(h), tail: map(t, f))`) is rewritten
|
|
5
|
-
# to a loop (tail recursion modulo cons).
|
|
5
|
+
# to a loop (tail recursion modulo cons). All deep tests use 1,000,000 steps.
|
|
6
|
-
# wasmtime GC, so list building tests use 200,000 cells; allocation free loops
|
|
7
|
-
# use 1,000,000.
|
|
8
6
|
module tail_calls
|
|
9
7
|
import std/Int
|
|
10
8
|
import std/Float
|
|
@@ -52,29 +50,36 @@ fun spin(own c: Counter) -> Counter =
|
|
|
52
50
|
fun depth(n: Int) -> Int =
|
|
53
51
|
if n == 0 then 0 else 1 + depth(n - 1)
|
|
54
52
|
|
|
53
|
+
fun down(n: Int) -> error{Neg}!Int =
|
|
54
|
+
if n < 0
|
|
55
|
+
return error.Neg
|
|
56
|
+
if n == 0
|
|
57
|
+
return 5
|
|
58
|
+
down(n - 1)
|
|
59
|
+
|
|
55
60
|
test "tail_sum_million"
|
|
56
61
|
assert sumTo(1000000, 0) == 500000500000
|
|
57
62
|
|
|
58
|
-
test "
|
|
63
|
+
test "tail_build_million"
|
|
59
|
-
assert length(build(
|
|
64
|
+
assert length(build(1000000, Nil), 0) == 1000000
|
|
60
65
|
|
|
61
|
-
test "
|
|
66
|
+
test "fold_million"
|
|
62
|
-
xs := fromTo(0,
|
|
67
|
+
xs := fromTo(0, 1000000)
|
|
63
|
-
assert foldl(xs, 0, |a, x| a + x) ==
|
|
68
|
+
assert foldl(xs, 0, |a, x| a + x) == 499999500000
|
|
64
69
|
|
|
65
70
|
test "mutual_even_odd_million"
|
|
66
71
|
assert isEven(1000000)
|
|
67
72
|
assert !isOdd(1000000)
|
|
68
73
|
assert isOdd(999999)
|
|
69
74
|
|
|
70
|
-
test "
|
|
75
|
+
test "trmc_map_million"
|
|
71
|
-
ys := map(fromTo(0,
|
|
76
|
+
ys := map(fromTo(0, 1000000), |x| x + 1)
|
|
72
|
-
assert length(ys, 0) ==
|
|
77
|
+
assert length(ys, 0) == 1000000
|
|
73
|
-
assert foldl(ys, 0, |a, x| a + x) ==
|
|
78
|
+
assert foldl(ys, 0, |a, x| a + x) == 500000500000
|
|
74
79
|
|
|
75
|
-
test "
|
|
80
|
+
test "trmc_fromTo_million"
|
|
76
|
-
xs := fromTo(0,
|
|
81
|
+
xs := fromTo(0, 1000000)
|
|
77
|
-
assert length(xs, 0) ==
|
|
82
|
+
assert length(xs, 0) == 1000000
|
|
78
83
|
|
|
79
84
|
test "closure_tail_million"
|
|
80
85
|
assert dec(1000000) == 0
|
|
@@ -84,6 +89,21 @@ test "own_record_loop_100k"
|
|
|
84
89
|
assert c.n == 0
|
|
85
90
|
assert c.total == 200000
|
|
86
91
|
|
|
92
|
+
test "recursive_closure_million"
|
|
93
|
+
go := |n, acc| if n == 0 then acc else go(n - 1, acc + n)
|
|
94
|
+
assert go(1000000, 0) == 500000500000
|
|
95
|
+
|
|
96
|
+
test "closure_body_tail_million"
|
|
97
|
+
fin := |n: Int| sumTo(n, 0)
|
|
98
|
+
assert fin(1000000) == 500000500000
|
|
99
|
+
|
|
100
|
+
test "error_union_loop_million"
|
|
101
|
+
assert { down(1000000) catch 99 } == 5
|
|
102
|
+
|
|
103
|
+
test "map_single_pass_million"
|
|
104
|
+
ys := map(fromTo(0, 1000000), |x| x * 2)
|
|
105
|
+
assert foldl(ys, 0, |a, x| a + x) == 999999000000
|
|
106
|
+
|
|
87
107
|
# Non-tail recursion still uses the wasm stack. Measured limit under the 4 MiB
|
|
88
108
|
# host stack: about 265,000 frames (260,000 passes, 270,000 traps) of `1 + depth(n - 1)`; tested well below it.
|
|
89
109
|
test "tree_recursion_depth_recorded"
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -388,6 +388,9 @@ pub struct ClosureInfo {
|
|
|
388
388
|
/// Free variables captured by value, in a stable (first-appearance) order; the
|
|
389
389
|
/// index into this vec IS the variable's field index in the env struct.
|
|
390
390
|
pub free_vars: Vec<(String, PlumType)>,
|
|
391
|
+
/// The local this literal is bound to (`name := |..| ..`): inside the body that
|
|
392
|
+
/// name refers to the closure itself.
|
|
393
|
+
pub self_name: Option<String>,
|
|
391
394
|
}
|
|
392
395
|
|
|
393
396
|
/// Closure ABI: every closure compiles to the wasm function type
|
|
@@ -520,6 +523,9 @@ struct LocalCtx<'a> {
|
|
|
520
523
|
tail_depth: Cell<u32>,
|
|
521
524
|
/// Imported function count: calls to imports are never tail calls.
|
|
522
525
|
import_count: u32,
|
|
526
|
+
/// In a closure body bound as `name := |..| ..`: `name` and the closure's table
|
|
527
|
+
/// index. The name resolves to the closure itself (env is wasm local 0).
|
|
528
|
+
self_closure: Option<(String, u32)>,
|
|
523
529
|
}
|
|
524
530
|
|
|
525
531
|
/// What tail-call emission needs to know about the fn being compiled.
|
|
@@ -2797,6 +2803,7 @@ fn compileSourceInner(
|
|
|
2797
2803
|
locals,
|
|
2798
2804
|
named_fn_refs: HashSet::new(),
|
|
2799
2805
|
fn_locals,
|
|
2806
|
+
pending_self: None,
|
|
2800
2807
|
};
|
|
2801
2808
|
match &f.body {
|
|
2802
2809
|
ast::FnBody::Block(block) => walker.walkBlock(block),
|
|
@@ -2868,6 +2875,7 @@ fn compileSourceInner(
|
|
|
2868
2875
|
param_ptypes: rc.param_ptypes.clone(),
|
|
2869
2876
|
ret_ptype: rc.ret_ptype.clone(),
|
|
2870
2877
|
free_vars: rc.free_vars.clone(),
|
|
2878
|
+
self_name: rc.self_name.clone(),
|
|
2871
2879
|
},
|
|
2872
2880
|
);
|
|
2873
2881
|
closure_asts.insert((*ui, rc.ptr), rc.closure);
|
|
@@ -3746,6 +3754,7 @@ struct RawClosure<'a> {
|
|
|
3746
3754
|
param_ptypes: Vec<PlumType>,
|
|
3747
3755
|
ret_ptype: PlumType,
|
|
3748
3756
|
free_vars: Vec<(String, PlumType)>,
|
|
3757
|
+
self_name: Option<String>,
|
|
3749
3758
|
}
|
|
3750
3759
|
|
|
3751
3760
|
/// The `fn(...)` type a bare reference to top-level fn `name` is used at.
|
|
@@ -3793,6 +3802,8 @@ struct ClosureWalker<'a, 't> {
|
|
|
3793
3802
|
/// The in-scope locals/params whose type is `fn(...)` — a closure body that
|
|
3794
3803
|
/// CALLS one (`|x| cb(x)`) captures it (see `fvCollectRefsExpr`'s `FnCall` arm).
|
|
3795
3804
|
fn_locals: HashMap<String, PlumType>,
|
|
3805
|
+
/// The name a closure literal about to be recorded is bound to.
|
|
3806
|
+
pending_self: Option<String>,
|
|
3796
3807
|
}
|
|
3797
3808
|
|
|
3798
3809
|
impl<'a, 't> ClosureWalker<'a, 't> {
|
|
@@ -3807,7 +3818,13 @@ impl<'a, 't> ClosureWalker<'a, 't> {
|
|
|
3807
3818
|
ast::Stmt::ForLoop(_) => {}
|
|
3808
3819
|
ast::Stmt::Assign(a) => {
|
|
3809
3820
|
for (target, value) in a.targets.iter().zip(a.values.iter()) {
|
|
3821
|
+
if let (ast::Expr::Closure(_), ast::AssignTarget::Var(name), 1) =
|
|
3822
|
+
(value, target, a.targets.len())
|
|
3823
|
+
{
|
|
3824
|
+
self.pending_self = Some(name.clone());
|
|
3825
|
+
}
|
|
3810
3826
|
self.walkExpr(value);
|
|
3827
|
+
self.pending_self = None;
|
|
3811
3828
|
match target {
|
|
3812
3829
|
ast::AssignTarget::Var(name) if name == "_" => {}
|
|
3813
3830
|
ast::AssignTarget::Var(name) => {
|
|
@@ -3972,13 +3989,16 @@ impl<'a, 't> ClosureWalker<'a, 't> {
|
|
|
3972
3989
|
// every param as an Int and the result as Unit.
|
|
3973
3990
|
_ => (vec![PlumType::TInt; cl.params.len()], PlumType::TUnit),
|
|
3974
3991
|
};
|
|
3992
|
+
let self_name = self.pending_self.take();
|
|
3993
|
+
let free_vars =
|
|
3975
|
-
|
|
3994
|
+
collectFreeVars(cl, self_name.as_deref(), self.types, self.fn_decls, &self.fn_locals);
|
|
3976
3995
|
self.found.push(RawClosure {
|
|
3977
3996
|
ptr: cl as *const ast::Closure as usize,
|
|
3978
3997
|
closure: cl,
|
|
3979
3998
|
param_ptypes,
|
|
3980
3999
|
ret_ptype,
|
|
3981
4000
|
free_vars,
|
|
4001
|
+
self_name,
|
|
3982
4002
|
});
|
|
3983
4003
|
|
|
3984
4004
|
// Recurse into this closure's own body to find any closure literals nested
|
|
@@ -4008,12 +4028,15 @@ impl<'a, 't> ClosureWalker<'a, 't> {
|
|
|
4008
4028
|
/// scope's type env.
|
|
4009
4029
|
fn collectFreeVars(
|
|
4010
4030
|
cl: &ast::Closure,
|
|
4031
|
+
self_name: Option<&str>,
|
|
4011
4032
|
env: &HashMap<usize, PlumType>,
|
|
4012
4033
|
fn_decls: &HashMap<String, &ast::Fn>,
|
|
4013
4034
|
fn_locals: &HashMap<String, PlumType>,
|
|
4014
4035
|
) -> Vec<(String, PlumType)> {
|
|
4015
4036
|
let mut bound: std::collections::HashSet<String> = cl.params.iter().cloned().collect();
|
|
4016
4037
|
fvCollectBoundBlock(&cl.body, &mut bound);
|
|
4038
|
+
// The closure's own name is not captured: it resolves to the closure itself.
|
|
4039
|
+
bound.extend(self_name.map(str::to_string));
|
|
4017
4040
|
|
|
4018
4041
|
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
|
|
4019
4042
|
let mut free: Vec<(String, PlumType)> = Vec::new();
|
|
@@ -4667,7 +4690,7 @@ fn compileFnBodyImpl(
|
|
|
4667
4690
|
}
|
|
4668
4691
|
|
|
4669
4692
|
// Tail calls: only real fns with a plain (non error-union) result.
|
|
4670
|
-
let self_func = if const_init.is_some()
|
|
4693
|
+
let self_func = if const_init.is_some() {
|
|
4671
4694
|
None
|
|
4672
4695
|
} else {
|
|
4673
4696
|
let own = match unit.inst {
|
|
@@ -4804,6 +4827,7 @@ fn compileFnBodyImpl(
|
|
|
4804
4827
|
self_func,
|
|
4805
4828
|
tail_depth: Cell::new(0),
|
|
4806
4829
|
import_count: ctx.import_count,
|
|
4830
|
+
self_closure: None,
|
|
4807
4831
|
};
|
|
4808
4832
|
|
|
4809
4833
|
let result_vt = if f.returns.is_none() || matches!(ret_decl, PlumType::TUnit) {
|
|
@@ -7227,6 +7251,14 @@ fn compileExprCore(
|
|
|
7227
7251
|
// Not a local: a bare reference to a top-level function used as a
|
|
7228
7252
|
// value (e.g. `each(double)`) — push its zero-capture trampoline
|
|
7229
7253
|
// closure's (pre-built by the `start` function) global.
|
|
7254
|
+
None if ctx.self_closure.as_ref().is_some_and(|(n, _)| n == name) => {
|
|
7255
|
+
// The closure's own name used as a value: rebuild its closure
|
|
7256
|
+
// struct from the same table index and the running env.
|
|
7257
|
+
let (_, table_idx) = ctx.self_closure.as_ref().expect("just checked");
|
|
7258
|
+
Instruction::I32Const(*table_idx as i32).encode(body);
|
|
7259
|
+
Instruction::LocalGet(0).encode(body);
|
|
7260
|
+
Instruction::StructNew(ctx.gc_types.closure_type_idx).encode(body);
|
|
7261
|
+
}
|
|
7230
7262
|
None => {
|
|
7231
7263
|
let fty = namedRefType(name, expr, ctx.types, ctx.func_sigs);
|
|
7232
7264
|
let global_idx = *ctx
|
|
@@ -7477,6 +7509,12 @@ fn compileExprCore(
|
|
|
7477
7509
|
}
|
|
7478
7510
|
} else if is_closure_call {
|
|
7479
7511
|
compileClosureCall(call, expr, body, ctx, state)?;
|
|
7512
|
+
} else if ctx
|
|
7513
|
+
.self_closure
|
|
7514
|
+
.as_ref()
|
|
7515
|
+
.is_some_and(|(n, _)| *n == call.name && !ctx.locals.contains_key(n))
|
|
7516
|
+
{
|
|
7517
|
+
compileSelfClosureCall(call, body, ctx, state)?;
|
|
7480
7518
|
} else if isValueTy(&PlumType::TNamed(call.name.clone()))
|
|
7481
7519
|
&& ctx
|
|
7482
7520
|
.enum_variants
|
|
@@ -7823,7 +7861,7 @@ fn tailMode(
|
|
|
7823
7861
|
return TailMode::Plain;
|
|
7824
7862
|
}
|
|
7825
7863
|
let want = ctx.ret_type.clone().unwrap_or(PlumType::TUnit);
|
|
7826
|
-
if
|
|
7864
|
+
if &want != result_ty {
|
|
7827
7865
|
return TailMode::Plain;
|
|
7828
7866
|
}
|
|
7829
7867
|
match func_idx {
|
|
@@ -7845,6 +7883,16 @@ fn emitCall(
|
|
|
7845
7883
|
ctx: &LocalCtx,
|
|
7846
7884
|
) {
|
|
7847
7885
|
let ty = inferLocalType(result_expr, ctx);
|
|
7886
|
+
let ptr = result_expr as *const ast::Expr as usize;
|
|
7887
|
+
let converted = [
|
|
7888
|
+
plum_checker::ERR_WRAP,
|
|
7889
|
+
plum_checker::ERR_WRAP2,
|
|
7890
|
+
plum_checker::ERR_NULL_NESTED,
|
|
7891
|
+
plum_checker::ERR_FROM_NAME,
|
|
7892
|
+
]
|
|
7893
|
+
.iter()
|
|
7894
|
+
.any(|k| ctx.types.contains_key(&(ptr + k)));
|
|
7895
|
+
let tail = if converted { ast::TailKind::NotTail } else { tail };
|
|
7848
7896
|
match tailMode(tail, Some(func_idx), &sig.ret, &ty, ctx) {
|
|
7849
7897
|
TailMode::Plain => Instruction::Call(func_idx).encode(body),
|
|
7850
7898
|
TailMode::Return => Instruction::ReturnCall(func_idx).encode(body),
|
|
@@ -8592,6 +8640,38 @@ fn compileClosureCallArgs(
|
|
|
8592
8640
|
Ok(())
|
|
8593
8641
|
}
|
|
8594
8642
|
|
|
8643
|
+
/// A closure calling itself by name: the running env (wasm local 0) is passed
|
|
8644
|
+
/// straight through, the callee is this closure's own table slot.
|
|
8645
|
+
fn compileSelfClosureCall(
|
|
8646
|
+
call: &ast::FnCall,
|
|
8647
|
+
body: &mut Vec<u8>,
|
|
8648
|
+
ctx: &LocalCtx,
|
|
8649
|
+
state: &mut ModuleState,
|
|
8650
|
+
) -> Result<(), String> {
|
|
8651
|
+
let (_, table_idx) = ctx.self_closure.as_ref().expect("self closure call");
|
|
8652
|
+
let Some(PlumType::TFun(ps, r)) = ctx.local_decl_types.get(&call.name) else {
|
|
8653
|
+
return Err(format!("codegen: '{}' is not a function-typed local", call.name));
|
|
8654
|
+
};
|
|
8655
|
+
let key = closureSigKey(ps, r, ctx.gc_types);
|
|
8656
|
+
let type_index = *ctx.closure_call_types.get(&key).ok_or_else(|| {
|
|
8657
|
+
format!("internal codegen error: no call_indirect type for closure '{}'", call.name)
|
|
8658
|
+
})?;
|
|
8659
|
+
Instruction::LocalGet(0).encode(body);
|
|
8660
|
+
for (i, arg) in call.args.iter().enumerate() {
|
|
8661
|
+
match ps.get(i) {
|
|
8662
|
+
Some(p) => compileExprInto(argExprOf(arg), p, body, ctx, state)?,
|
|
8663
|
+
None => compileExpr(argExprOf(arg), body, ctx, state)?,
|
|
8664
|
+
}
|
|
8665
|
+
}
|
|
8666
|
+
Instruction::I32Const(*table_idx as i32).encode(body);
|
|
8667
|
+
if tailMode(call.tail, None, &key.1, r, ctx) == TailMode::Plain {
|
|
8668
|
+
Instruction::CallIndirect { type_index, table_index: 0 }.encode(body);
|
|
8669
|
+
} else {
|
|
8670
|
+
Instruction::ReturnCallIndirect { type_index, table_index: 0 }.encode(body);
|
|
8671
|
+
}
|
|
8672
|
+
Ok(())
|
|
8673
|
+
}
|
|
8674
|
+
|
|
8595
8675
|
/// Whether the closure-typed local `name` is declared to return `Unit`.
|
|
8596
8676
|
fn closureCallReturnsUnit(name: &str, ctx: &LocalCtx) -> bool {
|
|
8597
8677
|
match ctx.local_decl_types.get(name) {
|
|
@@ -8623,6 +8703,11 @@ fn compileClosureBody(
|
|
|
8623
8703
|
base_env.insert(name.clone(), TypeScheme::mono(ty.clone()));
|
|
8624
8704
|
decl_types.insert(name.clone(), ty.clone());
|
|
8625
8705
|
}
|
|
8706
|
+
if let Some(n) = &info.self_name {
|
|
8707
|
+
let fty = PlumType::TFun(info.param_ptypes.clone(), Box::new(info.ret_ptype.clone()));
|
|
8708
|
+
base_env.insert(n.clone(), TypeScheme::mono(fty.clone()));
|
|
8709
|
+
decl_types.insert(n.clone(), fty);
|
|
8710
|
+
}
|
|
8626
8711
|
for (name, pty) in cl.params.iter().zip(info.param_ptypes.iter()) {
|
|
8627
8712
|
base_env.insert(name.clone(), TypeScheme::mono(pty.clone()));
|
|
8628
8713
|
decl_types.insert(name.clone(), pty.clone());
|
|
@@ -8740,9 +8825,15 @@ fn compileClosureBody(
|
|
|
8740
8825
|
ret_type: Some(info.ret_ptype.clone()),
|
|
8741
8826
|
match_subject_types: RefCell::new(HashMap::new()),
|
|
8742
8827
|
subject_ptypes: RefCell::new(HashMap::new()),
|
|
8828
|
+
// Tail calls inside the closure body are relative to the closure's own wasm fn.
|
|
8743
|
-
self_func:
|
|
8829
|
+
self_func: Some(SelfFunc {
|
|
8830
|
+
idx: u32::MAX,
|
|
8831
|
+
ret: closureSigKey(&info.param_ptypes, &info.ret_ptype, &ctx.gc_types).1,
|
|
8832
|
+
looped: false,
|
|
8833
|
+
}),
|
|
8744
8834
|
tail_depth: Cell::new(0),
|
|
8745
8835
|
import_count: ctx.import_count,
|
|
8836
|
+
self_closure: info.self_name.clone().map(|n| (n, info.table_idx)),
|
|
8746
8837
|
};
|
|
8747
8838
|
|
|
8748
8839
|
if matches!(info.ret_ptype, PlumType::TUnit) {
|
plum-wasm-codegen/tests/closure_tail_e2e_test.rs
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
use plum_wasm_codegen::compileSource;
|
|
3
|
+
|
|
4
|
+
fn gcEngine() -> wasmtime::Engine {
|
|
5
|
+
let mut config = wasmtime::Config::new();
|
|
6
|
+
config.wasm_gc(true);
|
|
7
|
+
config.wasm_function_references(true);
|
|
8
|
+
config.wasm_tail_call(true);
|
|
9
|
+
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
fn repoRoot() -> std::path::PathBuf {
|
|
13
|
+
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
fn compile(name: &str, src: &str) -> Vec<u8> {
|
|
17
|
+
let dir = std::env::temp_dir().join(format!("plum_tail_{}_{}", name, std::process::id()));
|
|
18
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
19
|
+
let path = dir.join("main.plum");
|
|
20
|
+
std::fs::write(&path, src).unwrap();
|
|
21
|
+
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
22
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
23
|
+
let source = r.unwrap_or_else(|e| panic!("load: {e}"));
|
|
24
|
+
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
25
|
+
wasmparser::validate(&bytes).expect("valid wasm");
|
|
26
|
+
bytes
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn runInt(name: &str, src: &str) -> i64 {
|
|
30
|
+
let bytes = compile(name, src);
|
|
31
|
+
let engine = gcEngine();
|
|
32
|
+
let module = wasmtime::Module::new(&engine, &bytes).expect("module");
|
|
33
|
+
let mut store = wasmtime::Store::new(&engine, ());
|
|
34
|
+
let mut linker = wasmtime::Linker::new(&engine);
|
|
35
|
+
linker.define_unknown_imports_as_traps(&module).unwrap();
|
|
36
|
+
let inst = linker.instantiate(&mut store, &module).expect("instantiate");
|
|
37
|
+
let main = inst.get_typed_func::<(), i64>(&mut store, "main").expect("main");
|
|
38
|
+
main.call(&mut store, ()).expect("run")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const HEAD: &str = "import std/Int\n\n";
|
|
42
|
+
|
|
43
|
+
#[test]
|
|
44
|
+
fn closure_body_tail_call_million() {
|
|
45
|
+
let src = format!("{HEAD}fun countDown(n: Int) -> Int = if n == 0 then 0 else countDown(n - 1)\n\nfun main() -> Int =\n dec := |n: Int| if n == 0 then 7 else countDown(n - 1) + 0\n fin := |n: Int| countDown(n)\n fin(1000000) + dec(0)\n");
|
|
46
|
+
assert_eq!(runInt("cbody", &src), 7);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#[test]
|
|
50
|
+
fn recursive_local_closure_million() {
|
|
51
|
+
let src = format!("{HEAD}fun main() -> Int =\n go := |n, acc| if n == 0 then acc else go(n - 1, acc + n)\n go(1000000, 0)\n");
|
|
52
|
+
assert_eq!(runInt("rec", &src), 500000500000);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#[test]
|
|
56
|
+
fn recursive_closure_captures_outer() {
|
|
57
|
+
let src = format!("{HEAD}fun main() -> Int =\n k := 3\n go := |n, acc| if n == 0 then acc else go(n - 1, acc + k)\n go(1000000, 0)\n");
|
|
58
|
+
assert_eq!(runInt("reccap", &src), 3000000);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#[test]
|
|
62
|
+
fn closure_not_recursive_unchanged() {
|
|
63
|
+
let src = format!("{HEAD}fun main() -> Int =\n k := 10\n f := |x| x + k\n f(5)\n");
|
|
64
|
+
assert_eq!(runInt("plain", &src), 15);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#[test]
|
|
68
|
+
fn wasm_closure_has_return_call() {
|
|
69
|
+
let src = format!("{HEAD}fun main() -> Int =\n go := |n, acc| if n == 0 then acc else go(n - 1, acc + n)\n go(10, 0)\n");
|
|
70
|
+
let bytes = compile("wasmrc", &src);
|
|
71
|
+
let mut found = false;
|
|
72
|
+
for payload in wasmparser::Parser::new(0).parse_all(&bytes) {
|
|
73
|
+
if let wasmparser::Payload::CodeSectionEntry(body) = payload.unwrap() {
|
|
74
|
+
let mut r = body.get_operators_reader().unwrap();
|
|
75
|
+
while !r.eof() {
|
|
76
|
+
if matches!(r.read().unwrap(), wasmparser::Operator::ReturnCallIndirect { .. }) {
|
|
77
|
+
found = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
assert!(found, "expected return_call_indirect in a closure body");
|
|
83
|
+
}
|
plum-wasm-codegen/tests/error_tail_e2e_test.rs
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
use plum_wasm_codegen::compileSource;
|
|
3
|
+
|
|
4
|
+
fn gcEngine() -> wasmtime::Engine {
|
|
5
|
+
let mut config = wasmtime::Config::new();
|
|
6
|
+
config.wasm_gc(true);
|
|
7
|
+
config.wasm_function_references(true);
|
|
8
|
+
config.wasm_tail_call(true);
|
|
9
|
+
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
fn repoRoot() -> std::path::PathBuf {
|
|
13
|
+
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
fn compile(name: &str, src: &str) -> Vec<u8> {
|
|
17
|
+
let dir = std::env::temp_dir().join(format!("plum_tail_{}_{}", name, std::process::id()));
|
|
18
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
19
|
+
let path = dir.join("main.plum");
|
|
20
|
+
std::fs::write(&path, src).unwrap();
|
|
21
|
+
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
22
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
23
|
+
let source = r.unwrap_or_else(|e| panic!("load: {e}"));
|
|
24
|
+
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
25
|
+
wasmparser::validate(&bytes).expect("valid wasm");
|
|
26
|
+
bytes
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn runInt(name: &str, src: &str) -> i64 {
|
|
30
|
+
let bytes = compile(name, src);
|
|
31
|
+
let engine = gcEngine();
|
|
32
|
+
let module = wasmtime::Module::new(&engine, &bytes).expect("module");
|
|
33
|
+
let mut store = wasmtime::Store::new(&engine, ());
|
|
34
|
+
let mut linker = wasmtime::Linker::new(&engine);
|
|
35
|
+
linker.define_unknown_imports_as_traps(&module).unwrap();
|
|
36
|
+
let inst = linker.instantiate(&mut store, &module).expect("instantiate");
|
|
37
|
+
let main = inst.get_typed_func::<(), i64>(&mut store, "main").expect("main");
|
|
38
|
+
main.call(&mut store, ()).expect("run")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fn countReturnCalls(bytes: &[u8]) -> usize {
|
|
42
|
+
let mut n = 0;
|
|
43
|
+
for p in wasmparser::Parser::new(0).parse_all(bytes) {
|
|
44
|
+
if let Ok(wasmparser::Payload::CodeSectionEntry(b)) = p {
|
|
45
|
+
let mut r = b.get_operators_reader().unwrap();
|
|
46
|
+
while !r.eof() {
|
|
47
|
+
if let Ok(wasmparser::Operator::ReturnCall { .. } | wasmparser::Operator::ReturnCallIndirect { .. }) = r.read() {
|
|
48
|
+
n += 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
n
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const HEAD: &str = "import std/Int\n\n";
|
|
57
|
+
|
|
58
|
+
#[test]
|
|
59
|
+
fn error_union_self_loop_million() {
|
|
60
|
+
let src = format!("{HEAD}fun down(n: Int) -> error{{Neg}}!Int =\n if n < 0\n return error.Neg\n if n == 0\n return 5\n down(n - 1)\n\nfun main() -> Int = down(1000000) catch 99\n");
|
|
61
|
+
assert_eq!(runInt("error_union_self_loop_million", &src), 5);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#[test]
|
|
65
|
+
fn error_union_self_loop_returns_error() {
|
|
66
|
+
let src = format!("{HEAD}fun down(n: Int) -> error{{Neg}}!Int =\n if n == 0\n return error.Neg\n down(n - 1)\n\nfun main() -> Int = down(1000000) catch 42\n");
|
|
67
|
+
assert_eq!(runInt("error_union_self_loop_returns_error", &src), 42);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[test]
|
|
71
|
+
fn error_union_mutual_million() {
|
|
72
|
+
let src = format!("{HEAD}fun ping(n: Int) -> error{{Neg}}!Int =\n if n == 0\n return 1\n pong(n - 1)\n\nfun pong(n: Int) -> error{{Neg}}!Int =\n if n == 0\n return 2\n ping(n - 1)\n\nfun main() -> Int = ping(1000000) catch 99\n");
|
|
73
|
+
assert_eq!(runInt("error_union_mutual_million", &src), 1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#[test]
|
|
77
|
+
fn try_in_tail_not_tail_call() {
|
|
78
|
+
let src = format!("{HEAD}fun down(n: Int) -> error{{Neg}}!Int =\n if n == 0\n return 5\n try down(n - 1)\n\nfun main() -> Int = down(100) catch 99\n");
|
|
79
|
+
let base = countReturnCalls(&compile("ebase", &format!("{HEAD}fun main() -> Int = 5\n")));
|
|
80
|
+
assert_eq!(countReturnCalls(&compile("etry", &src)), base);
|
|
81
|
+
assert_eq!(runInt("try_in_tail_not_tail_call", &src), 5);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#[test]
|
|
85
|
+
fn different_error_sets_normal_call() {
|
|
86
|
+
let src = format!("{HEAD}fun inner(n: Int) -> error{{A}}!Int =\n if n == 0\n return error.A\n 1\n\nfun outer(n: Int) -> error{{A, B}}!Int =\n if n == 9\n return error.B\n inner(n)\n\nfun main() -> Int = outer(0) catch 7\n");
|
|
87
|
+
let base = countReturnCalls(&compile("ebase2", &format!("{HEAD}fun main() -> Int = 5\n")));
|
|
88
|
+
assert_eq!(countReturnCalls(&compile("eset", &src)), base);
|
|
89
|
+
assert_eq!(runInt("different_error_sets_normal_call", &src), 7);
|
|
90
|
+
}
|
|
91
|
+
|