plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


84d23ae — Peter John 2026-11-04T18:04:25+05:30
Compiler cleanup phase 2a: deterministic output, shared walker, generics in one place, plum-host

Generated files not shown: Cargo.lock, plum-game/Cargo.lock, plum-runtime/Cargo.lock

Files changed (48) hide show
  1. Cargo.toml +1 -1
  2. docs/plans/2026-11-04-compiler-cleanup-phase2a.progress.md +35 -0
  3. docs/plans/2026-11-04-compiler-cleanup-phase2a.report.json +339 -0
  4. docs/plans/2026-11-04-compiler-cleanup-phase2a.report.md +40 -0
  5. docs/plans/2026-11-04-compiler-cleanup-phase2a.results.md +38 -0
  6. plum-checker/src/lib.rs +4 -24
  7. plum-checker/tests/features/mutability.rs +1 -6
  8. plum-checker/tests/trait_fields_tests.rs +1 -8
  9. plum-checker/tests/traits_tests.rs +1 -8
  10. plum-cli/Cargo.toml +1 -1
  11. plum-cli/src/main.rs +5 -596
  12. plum-core/src/closure_spec.rs +19 -376
  13. plum-core/src/drive.rs +15 -454
  14. plum-core/src/forms.rs +10 -191
  15. plum-core/src/generics.rs +45 -0
  16. plum-core/src/lib.rs +3 -0
  17. plum-core/src/rename.rs +159 -0
  18. plum-core/src/reuse.rs +3 -22
  19. plum-core/src/simplify.rs +30 -369
  20. plum-core/src/tail.rs +13 -46
  21. plum-core/src/trmc.rs +30 -20
  22. plum-core/src/union_lower.rs +1 -12
  23. plum-core/src/uniqueness.rs +5 -20
  24. plum-core/src/visit.rs +336 -0
  25. plum-core/tests/closure_spec_test.rs +23 -0
  26. plum-core/tests/closure_types_parse_test.rs +1 -12
  27. plum-core/tests/drive_test.rs +1 -6
  28. plum-core/tests/generics_test.rs +34 -0
  29. plum-core/tests/match_body_parse_test.rs +1 -12
  30. plum-core/tests/override_parse_test.rs +1 -12
  31. plum-core/tests/parser_test.rs +5 -24
  32. plum-core/tests/record_embed_test.rs +1 -8
  33. plum-core/tests/record_init_test.rs +1 -8
  34. plum-core/tests/rename_test.rs +88 -0
  35. plum-core/tests/simplify_test.rs +1 -6
  36. plum-core/tests/traits_test.rs +1 -8
  37. plum-core/tests/trmc_test.rs +43 -0
  38. plum-core/tests/uniqueness_test.rs +1 -6
  39. plum-core/tests/visit_test.rs +88 -0
  40. plum-game/Cargo.toml +1 -0
  41. plum-game/src/host.rs +3 -29
  42. plum-host/Cargo.toml +12 -0
  43. plum-host/src/lib.rs +659 -0
  44. plum-lsp/src/completion.rs +51 -14
  45. plum-runtime/Cargo.toml +1 -1
  46. plum-runtime/src/main.rs +3 -371
  47. plum-test-support/src/lib.rs +26 -0
  48. plum-wasm-codegen/src/lib.rs +23 -6
Cargo.toml CHANGED
@@ -1,5 +1,5 @@
1
1
  [workspace]
2
- members = ["plum-core", "plum-cli", "plum-checker", "plum-wasm-codegen", "plum-lsp", "plum-test-support"]
2
+ members = ["plum-core", "plum-cli", "plum-checker", "plum-wasm-codegen", "plum-lsp", "plum-test-support", "plum-host"]
3
3
  resolver = "2"
4
4
 
5
5
  [profile.dev.package.wasmtime]
docs/plans/2026-11-04-compiler-cleanup-phase2a.progress.md ADDED
@@ -0,0 +1,35 @@
1
+ - start f198426f7274f44ecc6e0d2839b4fb57ca27a6d2
2
+ - unblocked: dispatched T0 T1 T13 at 17:04:41; waiting on fixes: T2 T3 T4 T5 T6 T7 T8 T10 T9
3
+ - unblocked: dispatched T2 T3 T4 T5 T6 T7 T8 T9 T10 T14 T15 T16 at 17:06:55; waiting on fixes: none
4
+ - Ruling: accept 15.9k-char plan and predicted 36 min - walker conversions are file-sequential behind visit+rename - cost: larger prime
5
+ - Ruling: agents after the plan split get a shortened prime (rules + constraints verbatim; per-task interfaces via superlite-cli task Tn) - saves coordinator context - cost: agents read neighbour interfaces on demand
6
+ - checkpoint T10 848f3057a2 plum-host crate
7
+ - checkpoint T13 f6963abadb test helpers and LSP generic fix
8
+ - Ruling: T16 also owns plum-game/Cargo.lock and plum-runtime/Cargo.lock (adding the plum-host path dep rewrites them) - cost: none
9
+ - checkpoint T2 a56eb78c97 visit module
10
+ - checkpoint T1 8bedc92ea5 deterministic output
11
+ - checkpoint T16 7959112e72 hosts use plum-host
12
+ - Ruling: game host keeps its own new_str (module strNew/strSetByte); plum_host::newStr deleted via F-T10-1 - a standalone Str type is not the module's type - cost: none
13
+ - checkpoint T6 e945caae3d uniqueness, reuse, tail share helpers
14
+ - checkpoint T10 d3c65d6241 plum-host crate
15
+ - checkpoint T7 e1281c7e5d trmc real tree queries
16
+ - checkpoint T14 f07538dcdb shared rename and type substitution
17
+ - Ruling: T4 also owns plum-core/tests/closure_spec_test.rs for its pinning test - cost: none
18
+ - Ruling: simplify keeps pub(crate) walkRef/walkBlockRef/argRef/isTag (visit.rs lacks them); moving them into visit.rs deferred to phase 2b with the Paren removal - cost: ~40 lines stay in simplify one more phase
19
+ - checkpoint T3 2f1acb0518 drive and simplify use visit
20
+ - checkpoint T5 e776fb51f6 forms and union_lower use visit
21
+ - Ruling: union_lower keeps its scope-aware rn* renamers (shadow cut-offs, condBinds, Self_→Var don't fit the callback API) - only substType shared - cost: ~200 lines stay
22
+ - Note for T9: T5 saw hashes equal to its own control but different from T1's A.txt (8bedc92) with in-progress visit/rename/generics/lib.rs overlaid — T9 must diff T1 sha vs final tree and find any output change
23
+ - checkpoint T4 41eb4b34ee closure_spec uses visit
24
+ - checkpoint T8 6f482d5fef generics module and checker
25
+ - Note: T5's A.txt mismatch likely the shared-scratchpad clobbering T8 reported (agents overwrote each other's hA/hB/z.wasm); T9 re-checks cleanly
26
+ - checkpoint T15 16311c5728 passes ask generics
27
+ - checkpoint T10 39befc76e8 plum-host crate
28
+ - Ruling: visit::patternBinders accepts names not starting uppercase (forms previously required a lowercase first char; differs only for _x / digit-led names) - left as is; examples byte-identical - cost: a _x binder in a forms loop pattern could now be treated as bound
29
+ - checkpoint T9 4b4e45242d verify 2a
30
+ - Ruling: final-review incident [1] closed - T3 never meant to edit closure_spec_test.rs; the checkpoint saw T4's in-progress pinning test; T4 owns and checkpointed it (41eb4b3) - cost: none
31
+ - Ruling: final-review incident [3] closed - plum-game/Cargo.lock was regenerated by cargo when T16 added plum-host; `cargo build --locked` in plum-game passes and url/idna/icu come from plum-host's ureq dep - cost: none
32
+ - Ruling: final-review finding 1 → F-T2-1: binder = not uppercase-led and not exactly `_` (not lowercase-only, which would change reuse/uniqueness) - cost: none
33
+ - tests full passed=791 failed=0 skipped=0 seconds=121.2
34
+ - checkpoint T2 80885bfbfa visit module
35
+ - tests full passed=792 failed=0 skipped=0 seconds=120.7
docs/plans/2026-11-04-compiler-cleanup-phase2a.report.json ADDED
@@ -0,0 +1,339 @@
1
+ {
2
+ "projection_note": "projection: fixed forecast saved at lint 2026-11-04T17:04:34 (plan edited since)",
3
+ "headline": {
4
+ "wall_min": {
5
+ "projected": 30.213,
6
+ "actual": 56.868
7
+ },
8
+ "cost_usd_agents": {
9
+ "projected": 1.089968,
10
+ "actual": 5.418017
11
+ },
12
+ "task_min": {
13
+ "projected": 75.453,
14
+ "actual": 111.062
15
+ },
16
+ "agents": {
17
+ "projected": 11,
18
+ "actual": 11
19
+ }
20
+ },
21
+ "feature": null,
22
+ "cost_split": {
23
+ "agents_usd": 5.12597,
24
+ "rework_usd": 0.292047,
25
+ "coordinator_usd": 16.455458,
26
+ "total_usd": 21.873475
27
+ },
28
+ "concurrency": {
29
+ "peak": 7,
30
+ "projected_peak": 6
31
+ },
32
+ "critical_path": {
33
+ "actual": [
34
+ "T2"
35
+ ],
36
+ "projected": [
37
+ "T2",
38
+ "T5",
39
+ "T8",
40
+ "T9"
41
+ ],
42
+ "projected_min": 30.213
43
+ },
44
+ "model_mix": {
45
+ "haiku": {
46
+ "tasks": 1,
47
+ "cost_usd": 0.006801
48
+ },
49
+ "sonnet": {
50
+ "tasks": 14,
51
+ "cost_usd": 5.411216
52
+ }
53
+ },
54
+ "coordinator_cost_by_model": {
55
+ "opus": 16.455458
56
+ },
57
+ "quality": {
58
+ "tests": {
59
+ "passed": 792,
60
+ "failed": 0,
61
+ "skipped": 0,
62
+ "seconds": 120.7,
63
+ "mode": "full",
64
+ "failures": [],
65
+ "skipped_tests": []
66
+ },
67
+ "first_time_pass": 1,
68
+ "first_time_n": 1,
69
+ "rework_dispatches": 2,
70
+ "open_incidents": 4
71
+ },
72
+ "tasks": [
73
+ {
74
+ "task": "T1",
75
+ "model": "sonnet",
76
+ "pred_min": 6.092,
77
+ "actual_min": 15.482,
78
+ "delta": {
79
+ "abs": 9.3897,
80
+ "pct": 154.1
81
+ },
82
+ "cost_usd": 0.749085,
83
+ "first_time_pass": null
84
+ },
85
+ {
86
+ "task": "T10",
87
+ "model": "sonnet",
88
+ "pred_min": 11.138,
89
+ "actual_min": 3.223,
90
+ "delta": {
91
+ "abs": -7.9147,
92
+ "pct": -71.1
93
+ },
94
+ "cost_usd": 0.315439,
95
+ "first_time_pass": null
96
+ },
97
+ {
98
+ "task": "T9",
99
+ "model": "sonnet",
100
+ "pred_min": 5.443,
101
+ "actual_min": 9.328,
102
+ "delta": {
103
+ "abs": 3.8853,
104
+ "pct": 71.4
105
+ },
106
+ "cost_usd": 0.204816,
107
+ "first_time_pass": null
108
+ },
109
+ {
110
+ "task": "T2",
111
+ "model": "sonnet",
112
+ "pred_min": 9.641,
113
+ "actual_min": 6.097,
114
+ "delta": {
115
+ "abs": -3.5443,
116
+ "pct": -36.8
117
+ },
118
+ "cost_usd": 0.362905,
119
+ "first_time_pass": null
120
+ },
121
+ {
122
+ "task": "T4",
123
+ "model": "sonnet",
124
+ "pred_min": 5.767,
125
+ "actual_min": 8.897,
126
+ "delta": {
127
+ "abs": 3.1297,
128
+ "pct": 54.3
129
+ },
130
+ "cost_usd": 0.715729,
131
+ "first_time_pass": null
132
+ },
133
+ {
134
+ "task": "T6",
135
+ "model": "sonnet",
136
+ "pred_min": 5.767,
137
+ "actual_min": 2.86,
138
+ "delta": {
139
+ "abs": -2.907,
140
+ "pct": -50.4
141
+ },
142
+ "cost_usd": 0.224629,
143
+ "first_time_pass": null
144
+ },
145
+ {
146
+ "task": "T8",
147
+ "model": "sonnet",
148
+ "pred_min": 8.539,
149
+ "actual_min": 11.207,
150
+ "delta": {
151
+ "abs": 2.6677,
152
+ "pct": 31.2
153
+ },
154
+ "cost_usd": 0.456153,
155
+ "first_time_pass": null
156
+ },
157
+ {
158
+ "task": "T3",
159
+ "model": "sonnet",
160
+ "pred_min": 6.092,
161
+ "actual_min": 3.807,
162
+ "delta": {
163
+ "abs": -2.2853,
164
+ "pct": -37.5
165
+ },
166
+ "cost_usd": 0.207505,
167
+ "first_time_pass": null
168
+ },
169
+ {
170
+ "task": "T13",
171
+ "model": "sonnet",
172
+ "pred_min": 4.617,
173
+ "actual_min": 5.797,
174
+ "delta": {
175
+ "abs": 1.1797,
176
+ "pct": 25.6
177
+ },
178
+ "cost_usd": 0.290999,
179
+ "first_time_pass": null
180
+ },
181
+ {
182
+ "task": "T7",
183
+ "model": "sonnet",
184
+ "pred_min": 5.767,
185
+ "actual_min": 6.69,
186
+ "delta": {
187
+ "abs": 0.923,
188
+ "pct": 16.0
189
+ },
190
+ "cost_usd": 0.520137,
191
+ "first_time_pass": null
192
+ },
193
+ {
194
+ "task": "T5",
195
+ "model": "sonnet",
196
+ "pred_min": 6.59,
197
+ "actual_min": 6.508,
198
+ "delta": {
199
+ "abs": -0.0817,
200
+ "pct": -1.2
201
+ },
202
+ "cost_usd": 0.53137,
203
+ "first_time_pass": null
204
+ },
205
+ {
206
+ "task": "T0",
207
+ "model": "haiku",
208
+ "pred_min": null,
209
+ "actual_min": 2.893,
210
+ "delta": {
211
+ "abs": null,
212
+ "pct": null
213
+ },
214
+ "cost_usd": 0.006801,
215
+ "first_time_pass": null
216
+ },
217
+ {
218
+ "task": "T14",
219
+ "model": "sonnet",
220
+ "pred_min": null,
221
+ "actual_min": 18.025,
222
+ "delta": {
223
+ "abs": null,
224
+ "pct": null
225
+ },
226
+ "cost_usd": 0.277055,
227
+ "first_time_pass": true
228
+ },
229
+ {
230
+ "task": "T15",
231
+ "model": "sonnet",
232
+ "pred_min": null,
233
+ "actual_min": 2.443,
234
+ "delta": {
235
+ "abs": null,
236
+ "pct": null
237
+ },
238
+ "cost_usd": 0.305867,
239
+ "first_time_pass": null
240
+ },
241
+ {
242
+ "task": "T16",
243
+ "model": "sonnet",
244
+ "pred_min": null,
245
+ "actual_min": 7.805,
246
+ "delta": {
247
+ "abs": null,
248
+ "pct": null
249
+ },
250
+ "cost_usd": 0.249527,
251
+ "first_time_pass": null
252
+ }
253
+ ],
254
+ "cost_table": {
255
+ "source": "est",
256
+ "usd": 5.5691,
257
+ "tokens": 18749454,
258
+ "wall_min": 59793267.1409,
259
+ "rows": [
260
+ {
261
+ "agent": "a0cae94e2d76719ad",
262
+ "model": "opus",
263
+ "wall_min": 0.3041,
264
+ "tokens": 39051,
265
+ "usd": 0.1512
266
+ },
267
+ {
268
+ "agent": "a13504a681e9cf11d",
269
+ "model": "sonnet",
270
+ "wall_min": 35.8644,
271
+ "tokens": 5293527,
272
+ "usd": 1.5269
273
+ },
274
+ {
275
+ "agent": "a1a1ab48d656e2ec2",
276
+ "model": "sonnet",
277
+ "wall_min": 29896558.3686,
278
+ "tokens": 0,
279
+ "usd": 0.0
280
+ },
281
+ {
282
+ "agent": "a5202618b0967e37e",
283
+ "model": "sonnet",
284
+ "wall_min": 5.2626,
285
+ "tokens": 435148,
286
+ "usd": 0.1574
287
+ },
288
+ {
289
+ "agent": "a6e699cc2dd6bde54",
290
+ "model": "sonnet",
291
+ "wall_min": 2.918,
292
+ "tokens": 368258,
293
+ "usd": 0.1346
294
+ },
295
+ {
296
+ "agent": "a9d497e6031d81928",
297
+ "model": "sonnet",
298
+ "wall_min": 50.5232,
299
+ "tokens": 5111004,
300
+ "usd": 1.2541
301
+ },
302
+ {
303
+ "agent": "ab132a2529d4328b9",
304
+ "model": "haiku",
305
+ "wall_min": 2.8954,
306
+ "tokens": 158294,
307
+ "usd": 0.0068
308
+ },
309
+ {
310
+ "agent": "ac4d4ba9e71166959",
311
+ "model": "sonnet",
312
+ "wall_min": 29896538.3599,
313
+ "tokens": 0,
314
+ "usd": 0.0
315
+ },
316
+ {
317
+ "agent": "ac9525b38a34abd21",
318
+ "model": "sonnet",
319
+ "wall_min": 31.3069,
320
+ "tokens": 3404547,
321
+ "usd": 0.9617
322
+ },
323
+ {
324
+ "agent": "aceb0ba8bba475169",
325
+ "model": "sonnet",
326
+ "wall_min": 38.6507,
327
+ "tokens": 2630761,
328
+ "usd": 0.9959
329
+ },
330
+ {
331
+ "agent": "aee83ad12eadfac81",
332
+ "model": "sonnet",
333
+ "wall_min": 2.6871,
334
+ "tokens": 1308864,
335
+ "usd": 0.3805
336
+ }
337
+ ]
338
+ }
339
+ }
docs/plans/2026-11-04-compiler-cleanup-phase2a.report.md ADDED
@@ -0,0 +1,40 @@
1
+ # superlite build report
2
+
3
+ projection: fixed forecast saved at lint 2026-11-04T17:04:34 (plan edited since)
4
+
5
+ Projected Actual Delta
6
+ wall-clock (min) 30.2 56.9 +26.7 (+88%)
7
+ agent cost (USD, +rework) 1.09 5.42 +4.33 (+397%)
8
+ task-minutes 75.5 111.1 +35.6 (+47%)
9
+ agents 11 11 +0 (+0%)
10
+ cost split: agents $5.13, rework $0.29, coordinator $16.46, total $21.87
11
+ concurrency: peak 7 at once (projected 6); critical path actual T2; projected T2 > T5 > T8 > T9 (30.2 min)
12
+ model mix: haiku 1 tasks $0.01, sonnet 14 tasks $5.41, coordinator opus $16.46
13
+ quality: tests 792 passed, 0 failed; first-time pass 1/1 (100%); rework dispatches 2; open incidents 4
14
+
15
+ task model pred actual delta cost
16
+ T1 sonnet 6.1 15.5 +9.4 (+154%) $0.749
17
+ T10 sonnet 11.1 3.2 -7.9 (-71%) $0.315
18
+ T9 sonnet 5.4 9.3 +3.9 (+71%) $0.205
19
+ T2 sonnet 9.6 6.1 -3.5 (-37%) $0.363
20
+ T4 sonnet 5.8 8.9 +3.1 (+54%) $0.716
21
+ (+10 smaller misses; `superlite-cli report --json` lists all)
22
+
23
+ agent cost table:
24
+ | agent | model | wall_min | tokens | usd |
25
+ | --- | --- | --- | --- | --- |
26
+ | a0cae94e2d76719ad | opus | 0.3041 | 39051 | 0.1512 |
27
+ | a13504a681e9cf11d | sonnet | 35.8644 | 5293527 | 1.5269 |
28
+ | a1a1ab48d656e2ec2 | sonnet | 29896558.3686 | 0 | 0.0 |
29
+ | a5202618b0967e37e | sonnet | 5.2626 | 435148 | 0.1574 |
30
+ | a6e699cc2dd6bde54 | sonnet | 2.918 | 368258 | 0.1346 |
31
+ | a9d497e6031d81928 | sonnet | 50.5232 | 5111004 | 1.2541 |
32
+ | ab132a2529d4328b9 | haiku | 2.8954 | 158294 | 0.0068 |
33
+ | ac4d4ba9e71166959 | sonnet | 29896538.3599 | 0 | 0.0 |
34
+ | ac9525b38a34abd21 | sonnet | 31.3069 | 3404547 | 0.9617 |
35
+ | aceb0ba8bba475169 | sonnet | 38.6507 | 2630761 | 0.9959 |
36
+ | aee83ad12eadfac81 | sonnet | 2.6871 | 1308864 | 0.3805 |
37
+ | totals | | 59793267.1409 | 18749454 | 5.5691 |
38
+ cost source: est
39
+
40
+ takeaway: ran 88% slower than projected; cost 397% over; biggest miss T1
docs/plans/2026-11-04-compiler-cleanup-phase2a.results.md ADDED
@@ -0,0 +1,38 @@
1
+ # Compiler cleanup phase 2a: results
2
+
3
+ Measured on the final working tree of `superlite/compiler-cleanup-2a` (tasks T1-T16 applied), against the phase 1 merge `8445080`.
4
+
5
+ ## Byte identity
6
+ - Reference: `8bedc92` (T1, deterministic output) plus `rename.rs` from the current tree (8bedc92 alone does not build: its `lib.rs` declares `rename`; `rename.rs` only adds a module and does not change output).
7
+ - All 25 `plum-examples/**/*.plum` compile to sha256-identical wasm in reference and current tree; 0 compile failures.
8
+ - A second run of the current tree is identical to the first (deterministic). The mismatch T5 saw earlier came from comparing against a hash file in the shared scratchpad that another task had overwritten; a clean run shows no difference, so no bisect was needed.
9
+
10
+ ## Tests
11
+ - Workspace test binaries (`cargo build --workspace --tests`, each binary run in its crate directory): 57 binaries, 790 passed, 0 failed.
12
+ - `plum-game` `host_test`: 18 passed, 0 failed.
13
+ - `plum test plum-std plum-examples`: 678 passed, 0 failed, 63/63 files.
14
+ - `plum build` of a hello-world program (builds `plum-runtime` with the new `plum-host` crate) runs and prints `hi`.
15
+
16
+ ## Rust size (all tracked and new `.rs` files outside `plum-tooling`)
17
+ | | 8445080 | now |
18
+ |---|---|---|
19
+ | lines | 55404 | 54455 (-949) |
20
+ | `#[test]` functions | 805 | 828 (+23) |
21
+
22
+ `git diff --shortstat 8445080 -- '*.rs'`: 32 files changed, 327 insertions, 2674 deletions (new files excluded).
23
+
24
+ ## plum-core: lines added / removed per file vs 8445080
25
+ | file | added | removed |
26
+ |---|---|---|
27
+ | closure_spec.rs | 19 | 376 |
28
+ | drive.rs | 15 | 454 |
29
+ | simplify.rs | 30 | 369 |
30
+ | forms.rs | 10 | 191 |
31
+ | tail.rs | 13 | 46 |
32
+ | reuse.rs | 3 | 22 |
33
+ | uniqueness.rs | 5 | 20 |
34
+ | trmc.rs | 30 | 20 |
35
+ | union_lower.rs | 1 | 12 |
36
+ | lib.rs | 3 | 0 |
37
+
38
+ New files: `visit.rs` (336 lines), `rename.rs` (159), `generics.rs` (45).
plum-checker/src/lib.rs CHANGED
@@ -320,32 +320,12 @@ fn bindMatch(pattern: &PlumType, actual: &PlumType, b: &mut Binds) {
320
320
  }
321
321
  }
322
322
 
323
- /// A single uppercase letter (`T`, `U`, `K`, ...) is the grammar's only legal
324
- /// spelling for a generic type parameter.
325
- pub fn isGenericName(n: &str) -> bool {
326
- let mut chars = n.chars();
327
- match (chars.next(), chars.next()) {
328
- (Some(c), None) => c.is_ascii_uppercase(),
323
+ pub use plum_core::generics::isGenericName;
329
- _ => false,
330
- }
331
- }
332
324
 
333
- /// The generic parameter names an `Enum` introduces: its own declared
325
+ /// The generic parameter names an `Enum` introduces: its own declared generics
334
- /// generics (`enum Link[T] = ...`), else every distinct single-uppercase-letter
326
+ /// (`enum Link[T] = ...`); none if it declares none.
335
- /// variant field type name in first-appearance order.
336
327
  pub fn enumGenericParams(e: &ast::Enum) -> Vec<String> {
337
- if !e.generics.is_empty() {
338
- return e.generics.iter().map(|g| g.name.clone()).collect();
328
+ e.generics.iter().map(|g| g.name.clone()).collect()
339
- }
340
- let mut names: Vec<String> = Vec::new();
341
- for v in &e.variants {
342
- for field_ty in &v.fields {
343
- if isGenericName(&field_ty.name) && !names.iter().any(|x| x == &field_ty.name) {
344
- names.push(field_ty.name.clone());
345
- }
346
- }
347
- }
348
- names
349
329
  }
350
330
 
351
331
  /// Structural type conversion for original (erased) source: `List[Int]` ->
plum-checker/tests/features/mutability.rs CHANGED
@@ -1,14 +1,9 @@
1
1
  //! Field immutability: assigning a field not declared `mut` is an error.
2
2
  use plum_checker::checkSource;
3
3
  use plum_core::unions::{beginPlan, hoistInlineVariants, resolveMembers, takePlan};
4
- use plum_core::AstParser;
5
4
 
6
5
  fn run(src: &str) -> Result<(), String> {
7
- let mut parser = tree_sitter::Parser::new();
8
- parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
9
- let tree = parser.parse(src, None).unwrap();
10
- assert!(!tree.root_node().has_error(), "parse errors:\n{src}");
11
- let mut source = AstParser::new(src).parseSourceRaw(tree.root_node());
6
+ let mut source = plum_test_support::parseRawSource(src);
12
7
  let errs = plum_core::traits::lowerTraits(&mut source);
13
8
  assert!(errs.is_empty(), "{errs:?}");
14
9
  let errs = hoistInlineVariants(&mut source);
plum-checker/tests/trait_fields_tests.rs CHANGED
@@ -11,7 +11,6 @@ use plum_core::ast::*;
11
11
  use plum_core::traits::lowerTraits;
12
12
  use plum_core::union_lower::applyPlan;
13
13
  use plum_core::unions::{beginPlan, resolveMembers, takePlan};
14
- use plum_core::AstParser;
15
14
 
16
15
  const DEFS: &str = "\
17
16
  struct Vec2(x: Float, y: Float)
@@ -34,13 +33,7 @@ enum Shape = Circle | Square
34
33
 
35
34
  fn lowered(body: &str) -> Source {
36
35
  let src = format!("{DEFS}\n{body}\n");
37
- let mut parser = tree_sitter::Parser::new();
38
- parser
39
- .set_language(&tree_sitter_plum::LANGUAGE.into())
40
- .unwrap();
41
- let tree = parser.parse(&src, None).unwrap();
42
- assert!(!tree.root_node().has_error(), "{}", tree.root_node().to_sexp());
43
- let mut s = AstParser::new(&src).parseSourceRaw(tree.root_node());
36
+ let mut s = plum_test_support::parseRawSource(&src);
44
37
  assert_eq!(lowerTraits(&mut s), Vec::<String>::new());
45
38
  let errs = resolveMembers(&mut s);
46
39
  assert!(errs.is_empty(), "{errs:?}");
plum-checker/tests/traits_tests.rs CHANGED
@@ -4,7 +4,6 @@
4
4
  use plum_checker::checkSource;
5
5
  use plum_core::traits::lowerTraits;
6
6
  use plum_core::unions::resolveMembers;
7
- use plum_core::AstParser;
8
7
 
9
8
  const DEFS: &str = "\
10
9
  record Vec2(x: Float, y: Float)
@@ -30,13 +29,7 @@ record Soldier(Entity(pos, z)) =
30
29
 
31
30
  fn run(defs: &str, body: &str) -> Result<(), String> {
32
31
  let src = format!("{defs}\n{body}\n");
33
- let mut parser = tree_sitter::Parser::new();
34
- parser
35
- .set_language(&tree_sitter_plum::LANGUAGE.into())
36
- .unwrap();
37
- let tree = parser.parse(&src, None).unwrap();
38
- assert!(!tree.root_node().has_error(), "parse errors:\n{src}");
39
- let mut source = AstParser::new(&src).parseSourceRaw(tree.root_node());
32
+ let mut source = plum_test_support::parseRawSource(&src);
40
33
  let errs = lowerTraits(&mut source);
41
34
  assert!(errs.is_empty(), "{errs:?}");
42
35
  let errs = resolveMembers(&mut source);
plum-cli/Cargo.toml CHANGED
@@ -18,7 +18,7 @@ tree-sitter = "0.26"
18
18
  tree-sitter-plum = { path = "../plum-tooling/tree-sitter-plum" }
19
19
  toml_edit = "0.22"
20
20
  wasmtime = "47"
21
- ureq = "2.10"
21
+ plum-host = { path = "../plum-host" }
22
22
 
23
23
  [dev-dependencies]
24
24
  plum-test-support = { path = "../plum-test-support" }
plum-cli/src/main.rs CHANGED
@@ -275,585 +275,17 @@ fn cmdCompile(
275
275
  Ok(())
276
276
  }
277
277
 
278
- /// Builds the `Extern`s that satisfy a compiled Plum module's host imports (today,
279
- /// only `plum::printLn`) in declaration order, matching each one's exact `FuncType`
280
- /// (its `Str` param is a concrete wasm-gc array type, so the host function must be
281
- /// built from the module's own reported type rather than a generic `Rooted<ArrayRef>`
282
- /// wrapper, which would carry the wrong, unrelated top-array type and fail to
283
- /// instantiate).
284
- fn hostImports(
285
- store: &mut wasmtime::Store<()>,
286
- module: &wasmtime::Module,
287
- ) -> Result<Vec<wasmtime::Extern>> {
288
- module
289
- .imports()
290
- .map(|imp| {
291
- match (imp.module(), imp.name()) {
292
- ("plum", "printLn") => {
293
- let func_ty = imp.ty().func().cloned().ok_or_else(|| {
294
- anyhow::anyhow!("plum::printLn import must be a function")
295
- })?;
296
- Ok(wasmtime::Extern::Func(wasmtime::Func::new(
297
- &mut *store,
298
- func_ty,
299
- |mut caller, params, _results| {
300
- let wasmtime::Val::AnyRef(Some(s)) = &params[0] else {
301
- return Err(wasmtime::Error::msg("printLn expects a Str argument"));
302
- };
303
- let buf = strBytes(&mut caller, s)?;
304
- println!("{}", String::from_utf8_lossy(&buf));
305
- Ok(())
306
- },
307
- )))
308
- }
309
- ("plum", "crash") => {
310
- let func_ty =
311
- imp.ty().func().cloned().ok_or_else(|| {
312
- anyhow::anyhow!("plum::crash import must be a function")
313
- })?;
314
- Ok(wasmtime::Extern::Func(wasmtime::Func::new(
315
- &mut *store,
316
- func_ty,
317
- |mut caller, params, _results| {
318
- let wasmtime::Val::AnyRef(Some(s)) = &params[0] else {
319
- return Err(wasmtime::Error::msg("crash expects a Str argument"));
320
- };
321
- let buf = strBytes(&mut caller, s)?;
322
- eprintln!("crash: {}", String::from_utf8_lossy(&buf));
323
- Ok(())
324
- },
325
- )))
326
- }
327
- ("plum", "rawRandomHalf") => {
328
- let func_ty = imp.ty().func().cloned().ok_or_else(|| {
329
- anyhow::anyhow!("plum::rawRandomHalf import must be a function")
330
- })?;
331
- Ok(wasmtime::Extern::Func(wasmtime::Func::new(
332
- &mut *store,
333
- func_ty,
334
- |_caller, _params, results| {
335
- results[0] = wasmtime::Val::I32((randomI64() >> 32) as i32);
336
- Ok(())
337
- },
338
- )))
339
- }
340
- ("plum", "rawNowMillis") => {
341
- let func_ty = imp.ty().func().cloned().ok_or_else(|| {
342
- anyhow::anyhow!("plum::rawNowMillis import must be a function")
343
- })?;
344
- Ok(wasmtime::Extern::Func(wasmtime::Func::new(
345
- &mut *store,
346
- func_ty,
347
- |_caller, _params, results| {
348
- results[0] = wasmtime::Val::F64((nowMillis() as f64).to_bits());
349
- Ok(())
350
- },
351
- )))
352
- }
353
- ("plum", "rawReadFile") => {
354
- let func_ty = imp.ty().func().cloned().ok_or_else(|| {
355
- anyhow::anyhow!("plum::rawReadFile import must be a function")
356
- })?;
357
- Ok(wasmtime::Extern::Func(wasmtime::Func::new(
358
- &mut *store,
359
- func_ty.clone(),
360
- move |mut caller, params, results| {
361
- let path = readStrArg(&mut caller, &params[0])?;
362
- let bytes = std::fs::read(&path).unwrap_or_default();
363
- results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(
364
- &mut caller,
365
- &func_ty,
366
- &bytes,
367
- )?));
368
- Ok(())
369
- },
370
- )))
371
- }
372
- ("plum", "rawWriteFile") => {
373
- let func_ty = imp.ty().func().cloned().ok_or_else(|| {
374
- anyhow::anyhow!("plum::rawWriteFile import must be a function")
375
- })?;
376
- Ok(wasmtime::Extern::Func(wasmtime::Func::new(
377
- &mut *store,
378
- func_ty,
379
- |mut caller, params, results| {
380
- let path = readStrArg(&mut caller, &params[0])?;
381
- let data = readStrArg(&mut caller, &params[1])?;
382
- results[0] =
383
- wasmtime::Val::I32(std::fs::write(&path, &data).is_ok() as i32);
384
- Ok(())
385
- },
386
- )))
387
- }
388
- ("plum", "rawExists") => {
389
- let func_ty = imp.ty().func().cloned().ok_or_else(|| {
390
- anyhow::anyhow!("plum::rawExists import must be a function")
391
- })?;
392
- Ok(wasmtime::Extern::Func(wasmtime::Func::new(
393
- &mut *store,
394
- func_ty,
395
- |mut caller, params, results| {
396
- let path = readStrArg(&mut caller, &params[0])?;
397
- // `symlink_metadata`, not `Path::exists`, so a dangling symlink still
398
- // reports as existing (it does exist, just not what it points to) —
399
- // otherwise `remove` on a broken symlink would report "no such file"
400
- // and never get to call `rawRemove` on the symlink itself.
401
- results[0] =
402
- wasmtime::Val::I32(std::fs::symlink_metadata(&path).is_ok() as i32);
403
- Ok(())
404
- },
405
- )))
406
- }
407
- ("plum", "rawMkdir") => {
408
- let func_ty = imp.ty().func().cloned().ok_or_else(|| {
409
- anyhow::anyhow!("plum::rawMkdir import must be a function")
410
- })?;
411
- Ok(wasmtime::Extern::Func(wasmtime::Func::new(
412
- &mut *store,
413
- func_ty,
414
- |mut caller, params, results| {
415
- let path = readStrArg(&mut caller, &params[0])?;
416
- results[0] =
417
- wasmtime::Val::I32(std::fs::create_dir_all(&path).is_ok() as i32);
418
- Ok(())
419
- },
420
- )))
421
- }
422
- ("plum", "rawRemove") => {
423
- let func_ty = imp.ty().func().cloned().ok_or_else(|| {
424
- anyhow::anyhow!("plum::rawRemove import must be a function")
425
- })?;
426
- Ok(wasmtime::Extern::Func(wasmtime::Func::new(
427
- &mut *store,
428
- func_ty,
429
- |mut caller, params, results| {
430
- let path = readStrArg(&mut caller, &params[0])?;
431
- results[0] =
432
- wasmtime::Val::I32(std::fs::remove_file(&path).is_ok() as i32);
433
- Ok(())
434
- },
435
- )))
436
- }
437
- ("plum", "rawRemoveDir") => {
438
- let func_ty = imp.ty().func().cloned().ok_or_else(|| {
439
- anyhow::anyhow!("plum::rawRemoveDir import must be a function")
440
- })?;
441
- Ok(wasmtime::Extern::Func(wasmtime::Func::new(
442
- &mut *store,
443
- func_ty,
444
- |mut caller, params, results| {
445
- let path = readStrArg(&mut caller, &params[0])?;
446
- results[0] =
447
- wasmtime::Val::I32(std::fs::remove_dir(&path).is_ok() as i32);
448
- Ok(())
449
- },
450
- )))
451
- }
452
- ("plum", "rawRename") => {
453
- let func_ty = imp.ty().func().cloned().ok_or_else(|| {
454
- anyhow::anyhow!("plum::rawRename import must be a function")
455
- })?;
456
- Ok(wasmtime::Extern::Func(wasmtime::Func::new(
457
- &mut *store,
458
- func_ty,
459
- |mut caller, params, results| {
460
- let old_path = readStrArg(&mut caller, &params[0])?;
461
- let new_path = readStrArg(&mut caller, &params[1])?;
462
- results[0] = wasmtime::Val::I32(
463
- std::fs::rename(&old_path, &new_path).is_ok() as i32,
464
- );
465
- Ok(())
466
- },
467
- )))
468
- }
469
- ("plum", "rawTruncate") => {
470
- let func_ty = imp.ty().func().cloned().ok_or_else(|| {
471
- anyhow::anyhow!("plum::rawTruncate import must be a function")
472
- })?;
473
- Ok(wasmtime::Extern::Func(wasmtime::Func::new(
474
- &mut *store,
475
- func_ty,
476
- |mut caller, params, results| {
477
- let path = readStrArg(&mut caller, &params[0])?;
478
- let wasmtime::Val::I32(len) = params[1] else {
479
- return Err(wasmtime::Error::msg(
480
- "rawTruncate expects an Int length argument",
481
- ));
482
- };
483
- let ok = std::fs::OpenOptions::new()
484
- .write(true)
485
- .open(&path)
486
- .and_then(|f| f.set_len(len.max(0) as u64))
487
- .is_ok();
488
- results[0] = wasmtime::Val::I32(ok as i32);
489
- Ok(())
490
- },
491
- )))
492
- }
493
- ("plum", "rawLink") => {
494
- let func_ty = imp.ty().func().cloned().ok_or_else(|| {
495
- anyhow::anyhow!("plum::rawLink import must be a function")
496
- })?;
497
- Ok(wasmtime::Extern::Func(wasmtime::Func::new(
498
- &mut *store,
499
- func_ty,
500
- |mut caller, params, results| {
501
- let existing_path = readStrArg(&mut caller, &params[0])?;
502
- let new_path = readStrArg(&mut caller, &params[1])?;
503
- results[0] = wasmtime::Val::I32(
504
- std::fs::hard_link(&existing_path, &new_path).is_ok() as i32,
505
- );
506
- Ok(())
507
- },
508
- )))
509
- }
510
- ("plum", "rawSymlink") => {
511
- let func_ty = imp.ty().func().cloned().ok_or_else(|| {
512
- anyhow::anyhow!("plum::rawSymlink import must be a function")
513
- })?;
514
- Ok(wasmtime::Extern::Func(wasmtime::Func::new(
515
- &mut *store,
516
- func_ty,
517
- |mut caller, params, results| {
518
- let target = readStrArg(&mut caller, &params[0])?;
519
- let path = readStrArg(&mut caller, &params[1])?;
520
- #[cfg(unix)]
521
- let ok = std::os::unix::fs::symlink(&target, &path).is_ok();
522
- #[cfg(not(unix))]
523
- let ok = false;
524
- results[0] = wasmtime::Val::I32(ok as i32);
525
- Ok(())
526
- },
527
- )))
528
- }
529
- ("plum", "rawReadlink") => {
530
- let func_ty = imp.ty().func().cloned().ok_or_else(|| {
531
- anyhow::anyhow!("plum::rawReadlink import must be a function")
532
- })?;
533
- Ok(wasmtime::Extern::Func(wasmtime::Func::new(
534
- &mut *store,
535
- func_ty.clone(),
536
- move |mut caller, params, results| {
537
- let path = readStrArg(&mut caller, &params[0])?;
538
- let target = std::fs::read_link(&path)
539
- .ok()
540
- .map(|p| p.to_string_lossy().into_owned())
541
- .unwrap_or_default();
542
- results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(
543
- &mut caller,
544
- &func_ty,
545
- target.as_bytes(),
546
- )?));
547
- Ok(())
548
- },
549
- )))
550
- }
551
- ("plum", "rawRealpath") => {
552
- let func_ty = imp.ty().func().cloned().ok_or_else(|| {
553
- anyhow::anyhow!("plum::rawRealpath import must be a function")
554
- })?;
555
- Ok(wasmtime::Extern::Func(wasmtime::Func::new(
556
- &mut *store,
557
- func_ty.clone(),
558
- move |mut caller, params, results| {
559
- let path = readStrArg(&mut caller, &params[0])?;
560
- let resolved = std::fs::canonicalize(&path)
561
- .ok()
562
- .map(|p| p.to_string_lossy().into_owned())
563
- .unwrap_or_default();
564
- results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(
565
- &mut caller,
566
- &func_ty,
567
- resolved.as_bytes(),
568
- )?));
569
- Ok(())
570
- },
571
- )))
572
- }
573
- ("plum", "rawChmod") => {
574
- let func_ty = imp.ty().func().cloned().ok_or_else(|| {
575
- anyhow::anyhow!("plum::rawChmod import must be a function")
576
- })?;
577
- Ok(wasmtime::Extern::Func(wasmtime::Func::new(
578
- &mut *store,
579
- func_ty,
580
- |mut caller, params, results| {
581
- let path = readStrArg(&mut caller, &params[0])?;
582
- let wasmtime::Val::I32(mode) = params[1] else {
583
- return Err(wasmtime::Error::msg(
584
- "rawChmod expects an Int mode argument",
585
- ));
586
- };
587
- #[cfg(unix)]
588
- let ok = {
589
- use std::os::unix::fs::PermissionsExt;
590
- std::fs::set_permissions(
591
- &path,
592
- std::fs::Permissions::from_mode(mode as u32),
593
- )
594
- .is_ok()
595
- };
596
- #[cfg(not(unix))]
597
- let ok = false;
598
- results[0] = wasmtime::Val::I32(ok as i32);
599
- Ok(())
600
- },
601
- )))
602
- }
603
- ("plum", "rawReadDir") => {
604
- let func_ty = imp.ty().func().cloned().ok_or_else(|| {
605
- anyhow::anyhow!("plum::rawReadDir import must be a function")
606
- })?;
607
- Ok(wasmtime::Extern::Func(wasmtime::Func::new(
608
- &mut *store,
609
- func_ty.clone(),
610
- move |mut caller, params, results| {
611
- let path = readStrArg(&mut caller, &params[0])?;
612
- let joined = std::fs::read_dir(&path)
613
- .ok()
614
- .map(|entries| {
615
- entries
616
- .filter_map(|e| e.ok())
617
- .map(|e| e.file_name().to_string_lossy().into_owned())
618
- .collect::<Vec<_>>()
619
- .join("\n")
620
- })
621
- .unwrap_or_default();
622
- results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(
623
- &mut caller,
624
- &func_ty,
625
- joined.as_bytes(),
626
- )?));
627
- Ok(())
628
- },
629
- )))
630
- }
631
- ("plum", "rawHttpRequest") => {
632
- let func_ty = imp.ty().func().cloned().ok_or_else(|| {
633
- anyhow::anyhow!("plum::rawHttpRequest import must be a function")
634
- })?;
635
- Ok(wasmtime::Extern::Func(wasmtime::Func::new(
636
- &mut *store,
637
- func_ty.clone(),
638
- move |mut caller, params, results| {
639
- let method = readStrArg(&mut caller, &params[0])?;
640
- let url = readStrArg(&mut caller, &params[1])?;
641
- let headers = readStrArg(&mut caller, &params[2])?;
642
- let body = readStrArg(&mut caller, &params[3])?;
643
- let raw = performHttpRequest(&method, &url, &headers, &body);
644
- results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(
645
- &mut caller,
646
- &func_ty,
647
- raw.as_bytes(),
648
- )?));
649
- Ok(())
650
- },
651
- )))
652
- }
653
- // A `plum` import this host doesn't define (the game API of `std/game/Game`)
654
- // is stubbed with a function that traps when called, so a program
655
- // that imports it can still run or test code that never calls it.
656
- ("plum", n) => {
657
- let func_ty =
658
- imp.ty().func().cloned().ok_or_else(|| {
659
- anyhow::anyhow!("plum::{n} import must be a function")
660
- })?;
661
- let name = n.to_string();
662
- Ok(wasmtime::Extern::Func(wasmtime::Func::new(
663
- &mut *store,
664
- func_ty,
665
- move |_caller, _params, _results| {
666
- Err(wasmtime::Error::msg(format!(
667
- "host function '{name}' is not available here"
668
- )))
669
- },
670
- )))
671
- }
672
- (m, n) => anyhow::bail!("unsupported host import {m}::{n}"),
673
- }
674
- })
675
- .collect()
676
- }
677
-
678
- /// Performs one blocking HTTP round trip for `plum::rawHttpRequest` and packs
679
- /// the outcome into the wire format `plum-std/Http.plum`'s `request` expects
680
- /// (see that extern's doc comment): `"1\x01<status>\x01<headers>\x00<body>"`
681
- /// on success, `"0\x01\x01<message>\x00"` for a transport-level failure. A
682
- /// non-2xx HTTP status (404, 500, ...) is NOT a failure here — it's still a
683
- /// complete response, so it goes through the success branch with its own
684
- /// status code.
685
- fn performHttpRequest(method: &str, url: &str, headers: &str, body: &str) -> String {
686
- let agent = ureq::Agent::new();
687
- let mut req = agent.request(method, url);
688
- for line in headers.split('\n') {
689
- if let Some((k, v)) = line.split_once(": ") {
690
- req = req.set(k, v);
691
- }
692
- }
693
- let result = if body.is_empty() {
694
- req.call()
695
- } else {
696
- req.send_string(body)
697
- };
698
- match result {
699
- Ok(resp) | Err(ureq::Error::Status(_, resp)) => {
700
- let status = resp.status();
701
- let mut header_lines = String::new();
702
- for name in resp.headers_names() {
703
- if let Some(v) = resp.header(&name) {
704
- if !header_lines.is_empty() {
705
- header_lines.push('\n');
706
- }
707
- header_lines.push_str(&format!("{name}: {v}"));
708
- }
709
- }
710
- let body_text = resp.into_string().unwrap_or_default();
711
- format!("1\x01{status}\x01{header_lines}\x00{body_text}")
712
- }
713
- Err(e) => format!("0\x01\x01{e}\x00"),
714
- }
715
- }
716
-
717
- /// A pseudo-random `i64`: splitmix64 over process-wide state seeded once. Cheap,
718
- /// dependency-free, not cryptographically secure.
719
- fn randomI64() -> i64 {
720
- use std::sync::atomic::{AtomicU64, Ordering};
721
- // splitmix64 over a process-wide counter: seeded once from the clock and
722
- // an address, then advanced by a fixed odd increment per call, so
723
- // back-to-back calls can never repeat.
724
- static STATE: AtomicU64 = AtomicU64::new(0);
725
- let mut cur = STATE.load(Ordering::Relaxed);
726
- if cur == 0 {
727
- let nanos = std::time::SystemTime::now()
728
- .duration_since(std::time::UNIX_EPOCH)
729
- .map(|d| d.as_nanos() as u64)
730
- .unwrap_or(0);
731
- let seed = (nanos ^ (&STATE as *const AtomicU64 as u64)) | 1;
732
- cur = match STATE.compare_exchange(0, seed, Ordering::Relaxed, Ordering::Relaxed) {
733
- Ok(_) => seed,
734
- Err(v) => v,
735
- };
736
- }
737
- let _ = cur;
738
- let mut z = STATE
739
- .fetch_add(0x9E3779B97F4A7C15, Ordering::Relaxed)
740
- .wrapping_add(0x9E3779B97F4A7C15);
741
- z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
742
- z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
743
- (z ^ (z >> 31)) as i64
744
- }
745
-
746
- /// The bytes of a `Str` value: `Str(data: Array[Byte], start: Int, len: Int)`
747
- /// (see `plum-std/Str.plum`), a view `[start, start + len)` of a packed byte array.
748
- fn strBytes(
749
- mut store: impl wasmtime::AsContextMut,
750
- s: &wasmtime::Rooted<wasmtime::AnyRef>,
751
- ) -> wasmtime::Result<Vec<u8>> {
752
- let st = s.unwrap_struct(&store)?;
753
- let wasmtime::Val::AnyRef(Some(data)) = st.field(&mut store, 0)? else {
754
- return Err(wasmtime::Error::msg("Str.data is not a byte array"));
755
- };
756
- let start = st.field(&mut store, 1)?.unwrap_i64() as usize;
757
- let len = st.field(&mut store, 2)?.unwrap_i64() as usize;
758
- let arr = data.unwrap_array(&store)?;
759
- let mut buf = vec![0u8; arr.len(&store)? as usize];
760
- arr.copy_to_i8_slice(&mut store, &mut buf)?;
761
- Ok(buf[start..start + len].to_vec())
762
- }
763
-
764
- /// Reads a `Str` argument's bytes into a Rust `String`, for the
765
- /// `rawReadFile`/`rawWriteFile`/`rawExists`/`rawMkdir`/`rawRemove` host
766
- /// imports below — factored out since every filesystem import needs to read
767
- /// at least one `Str` arg.
768
- fn readStrArg(
769
- caller: &mut wasmtime::Caller<'_, ()>,
770
- val: &wasmtime::Val,
771
- ) -> wasmtime::Result<String> {
772
- let wasmtime::Val::AnyRef(Some(s)) = val else {
773
- return Err(wasmtime::Error::msg("expected a Str argument"));
774
- };
775
- let buf = strBytes(&mut *caller, s)?;
776
- Ok(String::from_utf8_lossy(&buf).into_owned())
777
- }
778
-
779
- /// Builds a new `Str` value from raw bytes, to return from `rawReadFile` —
780
- /// the host-side counterpart to `readStrArg`. Allocates the raw `array<i8>`
781
- /// (needs the import's own reported `FuncType` to find the concrete nested
782
- /// array type — a generic `ArrayRef` type would be the wrong, unrelated top
783
- /// array type), then wraps it in a fresh `Str` struct, mirroring `str.plum`'s real shape.
784
- fn makeStrResult(
785
- mut caller: impl wasmtime::AsContextMut,
786
- func_ty: &wasmtime::FuncType,
787
- bytes: &[u8],
788
- ) -> wasmtime::Result<wasmtime::Rooted<wasmtime::AnyRef>> {
789
- let result_ty = func_ty
790
- .results()
791
- .next()
792
- .ok_or_else(|| wasmtime::Error::msg("expected a Str-returning function"))?;
793
- let wasmtime::ValType::Ref(ref_ty) = result_ty else {
794
- return Err(wasmtime::Error::msg("expected a Str (ref) return type"));
795
- };
796
- let str_ty = ref_ty
797
- .heap_type()
798
- .as_concrete_struct()
799
- .ok_or_else(|| wasmtime::Error::msg("expected a concrete struct return type"))?
800
- .clone();
801
- let array_ty = str_ty
802
- .field(0)
803
- .and_then(|f| match f.element_type() {
804
- wasmtime::StorageType::ValType(wasmtime::ValType::Ref(rt)) => {
805
- rt.heap_type().as_concrete_array().cloned()
806
- }
807
- _ => None,
808
- })
809
- .ok_or_else(|| {
810
- wasmtime::Error::msg("expected Str.data to be a concrete byte array type")
811
- })?;
812
-
813
- let array_pre = wasmtime::ArrayRefPre::new(&mut caller, array_ty);
814
- let arr = wasmtime::ArrayRef::new_from_i8_slice(&mut caller, &array_pre, bytes)?;
815
-
816
- let str_pre = wasmtime::StructRefPre::new(&mut caller, str_ty);
817
- let str_val = wasmtime::StructRef::new(
818
- &mut caller,
819
- &str_pre,
820
- &[
821
- wasmtime::Val::AnyRef(Some(arr.to_anyref())),
822
- wasmtime::Val::I64(0),
823
- wasmtime::Val::I64(bytes.len() as i64),
824
- ],
825
- )?;
826
- Ok(str_val.to_anyref())
827
- }
828
-
829
- fn nowMillis() -> i64 {
830
- std::time::SystemTime::now()
831
- .duration_since(std::time::UNIX_EPOCH)
832
- .map(|d| d.as_millis() as i64)
833
- .unwrap_or(0)
834
- }
835
-
836
278
  fn cmdRun(file: std::path::PathBuf, lib_path: std::path::PathBuf) -> Result<()> {
837
279
  let wasm_bytes = compileToWasm(&file, &lib_path)?;
838
280
 
839
281
  // GC + typed function references are required to load modules containing
840
282
  // Plum's structs/enums/strings/closures (see docs/superpowers/plans/2026-07-25-wasm-gc-migration.md).
841
- let mut config = wasmtime::Config::new();
842
- config.wasm_gc(true);
843
- config.wasm_function_references(true);
844
- config.wasm_tail_call(true);
845
- config.max_wasm_stack(4 << 20);
846
- // Start the GC heap at 64 MiB (reserved, committed lazily) so list-heavy programs
847
- // skip the early full-heap copies of each doubling growth (docs/plans/2026-11-03-gc-report.md).
848
- config.gc_heap_initial_size(64 << 20);
849
- config.async_stack_size(8 << 20);
850
- let engine = wasmtime::Engine::new(&config)
283
+ let engine = plum_host::engine();
851
- .map_err(|e| anyhow::anyhow!("failed to construct wasmtime engine: {e}"))?;
852
284
 
853
285
  let module = wasmtime::Module::new(&engine, &wasm_bytes)
854
286
  .map_err(|e| anyhow::anyhow!("compiled wasm failed to load: {e:#}"))?;
855
287
  let mut store = wasmtime::Store::new(&engine, ());
856
- let imports = hostImports(&mut store, &module)?;
288
+ let imports = plum_host::hostImports(&mut store, &module, plum_host::Caps::all())?;
857
289
  let instance = wasmtime::Instance::new(&mut store, &module, &imports)
858
290
  .map_err(|e| anyhow::anyhow!("module failed to instantiate: {e}"))?;
859
291
 
@@ -994,22 +426,12 @@ fn runTestFile(file: &std::path::Path, lib_path: &std::path::Path) -> Result<(us
994
426
  let _ = std::fs::write(path, &wasm_bytes);
995
427
  }
996
428
 
997
- let mut config = wasmtime::Config::new();
998
- config.wasm_gc(true);
999
- config.wasm_function_references(true);
1000
- config.wasm_tail_call(true);
1001
- config.max_wasm_stack(4 << 20);
1002
- // Start the GC heap at 64 MiB (reserved, committed lazily) so list-heavy programs
1003
- // skip the early full-heap copies of each doubling growth (docs/plans/2026-11-03-gc-report.md).
1004
- config.gc_heap_initial_size(64 << 20);
1005
- config.async_stack_size(8 << 20);
1006
- let engine = wasmtime::Engine::new(&config)
429
+ let engine = plum_host::engine();
1007
- .map_err(|e| anyhow::anyhow!("failed to construct wasmtime engine: {e}"))?;
1008
430
 
1009
431
  let module = wasmtime::Module::new(&engine, &wasm_bytes)
1010
432
  .map_err(|e| anyhow::anyhow!("compiled wasm failed to load: {e:#}"))?;
1011
433
  let mut store = wasmtime::Store::new(&engine, ());
1012
- let imports = hostImports(&mut store, &module)?;
434
+ let imports = plum_host::hostImports(&mut store, &module, plum_host::Caps::all())?;
1013
435
  let instance = wasmtime::Instance::new(&mut store, &module, &imports)
1014
436
  .map_err(|e| anyhow::anyhow!("module failed to instantiate: {e}"))?;
1015
437
 
@@ -1103,7 +525,7 @@ fn readStrResult(store: &mut wasmtime::Store<()>, val: &wasmtime::Val) -> Result
1103
525
  let wasmtime::Val::AnyRef(Some(s)) = val else {
1104
526
  anyhow::bail!("internal error: expected a Str result");
1105
527
  };
1106
- let buf = strBytes(&mut *store, s)?;
528
+ let buf = plum_host::strBytes(&mut *store, s)?;
1107
529
  Ok(String::from_utf8_lossy(&buf).into_owned())
1108
530
  }
1109
531
 
@@ -1164,16 +586,3 @@ fn cmdBuild(
1164
586
  eprintln!("built {} → {}", file.display(), out_path.display());
1165
587
  Ok(())
1166
588
  }
1167
-
1168
- #[cfg(test)]
1169
- mod rng_tests {
1170
- #[test]
1171
- fn random_i64_never_repeats_back_to_back() {
1172
- let mut prev = super::randomI64();
1173
- for _ in 0..100_000 {
1174
- let next = super::randomI64();
1175
- assert_ne!(prev, next);
1176
- prev = next;
1177
- }
1178
- }
1179
- }
plum-core/src/closure_spec.rs CHANGED
@@ -11,6 +11,8 @@
11
11
  //! closure cannot be inlined, the call is left alone.
12
12
  use crate::ast::*;
13
13
  use crate::closure_escape::nonEscapingFnParams;
14
+ use crate::rename::{renameStmts, Name};
15
+ use crate::visit::{anyExpr, countVar, stmtBlockRefs, stmtBlocks, stmtExprRefs, stmtExprs, visitBlock, visitExpr};
14
16
  use std::collections::{HashMap, HashSet};
15
17
 
16
18
  pub const MAX_COPIES: usize = 8;
@@ -126,7 +128,7 @@ enum Kind {
126
128
  struct Spec {
127
129
  kind: Kind,
128
130
  /// The closure (or fn name) without source positions: the dedup identity.
129
- norm: String,
131
+ norm: Kind,
130
132
  /// Captured variables at the originating call site (`"self"` for the receiver).
131
133
  caps: Vec<String>,
132
134
  }
@@ -315,7 +317,7 @@ impl<'h> Ctx<'h> {
315
317
  zeroBlock(&mut z.body);
316
318
  Some(Spec {
317
319
  kind: Kind::Lit((**cl).clone()),
318
- norm: format!("{:?}", z),
320
+ norm: Kind::Lit(z),
319
321
  caps,
320
322
  })
321
323
  }
@@ -324,7 +326,7 @@ impl<'h> Ctx<'h> {
324
326
  if f.type_param.is_none() && !f.is_export {
325
327
  Some(Spec {
326
328
  kind: Kind::Name(n.clone()),
327
- norm: format!("fn {}", n),
329
+ norm: Kind::Name(n.clone()),
328
330
  caps: Vec::new(),
329
331
  })
330
332
  } else {
@@ -345,7 +347,7 @@ impl<'h> Ctx<'h> {
345
347
  let mut plain: Vec<String> = Vec::new();
346
348
  let mut refs: Vec<String> = Vec::new();
347
349
  renameStmts(&mut c.body.stmts, &mut |n, kind| match kind {
348
- Name::Decl => {
350
+ Name::Decl | Name::Pat => {
349
351
  binders.insert(n.clone());
350
352
  }
351
353
  Name::Plain => plain.push(n.clone()),
@@ -495,7 +497,7 @@ impl<'h> Ctx<'h> {
495
497
  });
496
498
  }
497
499
  }
498
- let before = countVar(&mut f.body, &p);
500
+ let before = countVar(&f.body, &p);
499
501
  let mut tx = Tx {
500
502
  p: p.clone(),
501
503
  spec: sid,
@@ -719,7 +721,7 @@ impl<'h> Ctx<'h> {
719
721
  let mut binders: HashSet<String> = cl.params.iter().cloned().collect();
720
722
  let mut assigned: HashSet<String> = HashSet::new();
721
723
  renameStmts(&mut body.stmts, &mut |n, kind| match kind {
722
- Name::Decl => {
724
+ Name::Decl | Name::Pat => {
723
725
  binders.insert(n.clone());
724
726
  }
725
727
  Name::Plain => {
@@ -900,21 +902,6 @@ fn lazyChildren(e: &mut Expr) -> Vec<&mut Expr> {
900
902
  }
901
903
  }
902
904
 
903
- fn countVar(body: &mut FnBody, name: &str) -> usize {
904
- let mut n = 0;
905
- let mut cb = |e: &mut Expr| {
906
- if matches!(e, Expr::Var(v) if v == name) {
907
- n += 1;
908
- }
909
- };
910
- match body {
911
- FnBody::Expr(e) => visitExpr(e, &mut cb),
912
- FnBody::Block(b) => visitBlock(b, &mut cb),
913
- FnBody::Extern => {}
914
- }
915
- n
916
- }
917
-
918
905
  fn anyReturn(stmts: &[Stmt]) -> bool {
919
906
  stmts.iter().any(|s| match s {
920
907
  Stmt::Return(_) => true,
@@ -989,244 +976,21 @@ fn annotations(stmts: &[Stmt], out: &mut HashSet<String>) {
989
976
 
990
977
  fn collectBinders(body: &mut FnBody, out: &mut HashSet<String>) {
991
978
  let mut cb = |n: &mut String, k: Name| {
992
- if matches!(k, Name::Decl) {
979
+ if matches!(k, Name::Decl | Name::Pat) {
993
980
  out.insert(n.clone());
994
981
  }
995
982
  };
996
983
  match body {
997
984
  FnBody::Block(b) => renameStmts(&mut b.stmts, &mut cb),
998
- FnBody::Expr(e) => renameExpr(e, &mut cb),
999
- FnBody::Extern => {}
985
+ FnBody::Expr(e) => {
1000
- }
1001
- }
1002
-
1003
- // ---------- mutable traversal ----------
1004
-
1005
- fn visitBlock(b: &mut Block, f: &mut dyn FnMut(&mut Expr)) {
1006
- for s in &mut b.stmts {
1007
- for e in stmtExprs(s) {
1008
- visitExpr(e, f);
1009
- }
1010
- for blk in stmtBlocks(s) {
1011
- visitBlock(blk, f);
1012
- }
1013
- }
1014
- }
1015
-
1016
- /// Post-order over `e`, into closure bodies and catch blocks too.
1017
- fn visitExpr(e: &mut Expr, f: &mut dyn FnMut(&mut Expr)) {
1018
- for c in childExprs(e) {
1019
- visitExpr(c, f);
1020
- }
1021
- match e {
1022
- Expr::Closure(c) => visitBlock(&mut c.body, f),
1023
- Expr::Catch(c) => {
1024
- if let CatchHandler::Block { body, .. } = &mut c.handler {
1025
- visitBlock(body, f);
1026
- }
1027
- }
1028
- _ => {}
1029
- }
1030
- f(e);
1031
- }
1032
-
1033
- fn argExpr(a: &mut Arg) -> &mut Expr {
1034
- match a {
1035
- Arg::Positional(e) | Arg::Keyword { value: e, .. } => e,
1036
- }
1037
- }
1038
-
1039
- fn childExprs(e: &mut Expr) -> Vec<&mut Expr> {
1040
- match e {
1041
- Expr::Binary(b) => vec![&mut b.left, &mut b.right],
1042
- Expr::Unary(u) => vec![&mut u.operand],
1043
- Expr::Bool(b) => vec![&mut b.left, &mut b.right],
1044
- Expr::Not(x) | Expr::Try(x) | Expr::Paren(x) => vec![x.as_mut()],
1045
- Expr::Bang(b) => vec![&mut b.value],
1046
- Expr::Bind(b) => {
1047
- let mut v = vec![&mut b.value];
1048
- v.extend(b.case.guard.iter_mut());
1049
- v
1050
- }
1051
- Expr::Catch(c) => {
1052
- let CatchExpr { value, handler, .. } = &mut **c;
1053
- match handler {
1054
- CatchHandler::Default(d) => vec![value, d],
1055
- CatchHandler::Block { .. } => vec![value],
1056
- }
1057
- }
1058
- Expr::Compare(c) => vec![&mut c.left, &mut c.right],
1059
- Expr::InlineIf(t) => vec![&mut t.condition, &mut t.then, &mut t.else_],
1060
- Expr::FnCall(c) => c.args.iter_mut().map(argExpr).collect(),
1061
- Expr::Yield(vs) => vs.iter_mut().collect(),
1062
- Expr::ClassCall(c) => {
1063
- let mut v: Vec<&mut Expr> = c.fields.iter_mut().map(|f| &mut f.value).collect();
1064
- if let Some(s) = &mut c.spread {
1065
- v.push(s.as_mut());
1066
- }
1067
- v
1068
- }
1069
- Expr::Attribute(a) => {
1070
- let AttributeExpr { object, attr } = &mut **a;
1071
- let mut v = vec![object];
1072
- if let AttrKind::Method(c) = attr {
1073
- v.extend(c.args.iter_mut().map(argExpr));
1074
- }
1075
- v
1076
- }
1077
- Expr::String(s) => s
1078
- .parts
1079
- .iter_mut()
1080
- .filter_map(|p| match p {
1081
- StringPart::Interp(e) => Some(e),
1082
- StringPart::Text(_) => None,
1083
- })
1084
- .collect(),
1085
- Expr::Closure(_)
1086
- | Expr::Int(_)
1087
- | Expr::Float(_)
1088
- | Expr::BoolLit(_)
1089
- | Expr::Self_
1090
- | Expr::ErrorValue(_)
1091
- | Expr::Var(_)
1092
- | Expr::TypeName(_) => vec![],
1093
- }
1094
- }
1095
-
1096
- fn stmtExprs(s: &mut Stmt) -> Vec<&mut Expr> {
1097
- match s {
1098
- Stmt::Assign(a) => {
1099
- let mut v: Vec<&mut Expr> = Vec::new();
1100
- for t in &mut a.targets {
1101
- if let AssignTarget::Field(obj, _) = t {
1102
- v.push(obj.as_mut());
1103
- }
1104
- }
1105
- v.extend(a.values.iter_mut());
1106
- v
1107
- }
1108
- Stmt::Assert(c) => vec![&mut c.cond],
1109
- Stmt::Crash(e) | Stmt::Expr(e, ..) => vec![e],
986
+ let mut stmts = [Stmt::Expr(e.clone(), 0, 0)];
1110
- Stmt::Return(e) => e.iter_mut().collect(),
1111
- Stmt::ForLoop(f) => {
1112
- let mut v: Vec<&mut Expr> = vec![&mut f.source];
1113
- for c in &mut f.clauses {
1114
- match c {
1115
- ForClause::If(e) | ForClause::Bind(_, e) => v.push(e),
1116
- }
1117
- }
1118
- v
1119
- }
1120
- Stmt::While(w) => vec![&mut w.condition],
1121
- Stmt::If(i) => {
1122
- let mut v = vec![&mut i.condition];
1123
- v.extend(i.else_ifs.iter_mut().map(|e| &mut e.condition));
1124
- v
1125
- }
1126
- Stmt::Match(m) => {
1127
- let mut v: Vec<&mut Expr> = m.subjects.iter_mut().collect();
1128
- v.extend(m.cases.iter_mut().filter_map(|c| c.guard.as_mut()));
1129
- v
1130
- }
1131
- Stmt::Break | Stmt::Continue | Stmt::Todo => vec![],
1132
- }
1133
- }
1134
-
1135
- fn stmtBlocks(s: &mut Stmt) -> Vec<&mut Block> {
1136
- match s {
1137
- Stmt::ForLoop(f) => vec![&mut f.body],
1138
- Stmt::While(w) => vec![&mut w.body],
1139
- Stmt::If(i) => {
1140
- let mut v = vec![&mut i.body];
1141
- v.extend(i.else_ifs.iter_mut().map(|e| &mut e.body));
1142
- v.extend(i.else_.iter_mut());
1143
- v
1144
- }
1145
- Stmt::Match(m) => m.cases.iter_mut().map(|c| &mut c.body).collect(),
1146
- _ => vec![],
1147
- }
1148
- }
1149
-
1150
- #[derive(Clone, Copy)]
1151
- enum Name {
1152
- /// A declaration site: `x := v`, a loop binder, a pattern name, a closure param.
1153
- Decl,
1154
- /// The target of a plain `x = v`.
1155
- Plain,
1156
- /// A use.
1157
- Ref,
1158
- }
1159
-
1160
- fn renameStmts(stmts: &mut [Stmt], rn: &mut dyn FnMut(&mut String, Name)) {
1161
- for s in stmts {
1162
- match s {
1163
- Stmt::Assign(a) => {
1164
- let kind = if a.declare { Name::Decl } else { Name::Plain };
1165
- for t in &mut a.targets {
1166
- if let AssignTarget::Var(n) = t {
1167
- rn(n, kind);
1168
- }
1169
- }
1170
- }
1171
- Stmt::ForLoop(f) => {
1172
- f.binders.iter_mut().for_each(|v| rn(v, Name::Decl));
1173
- for c in &mut f.clauses {
1174
- if let ForClause::Bind(n, _) = c {
1175
- rn(n, Name::Decl);
1176
- }
1177
- }
1178
- if let Some((n, _)) = &mut f.result {
1179
- rn(n, Name::Decl);
1180
- }
1181
- }
1182
- Stmt::Match(m) => {
1183
- for c in &mut m.cases {
1184
- for p in &mut c.patterns {
1185
- renamePattern(p, rn);
1186
- }
1187
- }
1188
- }
1189
- _ => {}
1190
- }
1191
- for e in stmtExprs(s) {
1192
- renameExpr(e, rn);
1193
- }
1194
- for b in stmtBlocks(s) {
1195
- renameStmts(&mut b.stmts, rn);
987
+ renameStmts(&mut stmts, &mut cb);
1196
988
  }
989
+ FnBody::Extern => {}
1197
990
  }
1198
991
  }
1199
992
 
1200
- fn renamePattern(p: &mut CasePattern, rn: &mut dyn FnMut(&mut String, Name)) {
1201
- match p {
1202
- CasePattern::Name(n) => rn(n, Name::Decl),
1203
- CasePattern::Class { fields, .. } => fields.iter_mut().for_each(|f| renamePattern(f, rn)),
1204
- _ => {}
1205
- }
1206
- }
1207
-
1208
- fn renameExpr(e: &mut Expr, rn: &mut dyn FnMut(&mut String, Name)) {
1209
- match e {
1210
- Expr::Var(n) => rn(n, Name::Ref),
1211
- Expr::Bind(b) => renamePattern(&mut b.case.patterns[0], rn),
1212
- Expr::Closure(c) => {
1213
- c.params.iter_mut().for_each(|p| rn(p, Name::Decl));
1214
- renameStmts(&mut c.body.stmts, rn);
1215
- }
1216
- Expr::Catch(c) => {
1217
- if let CatchHandler::Block { binder, body } = &mut c.handler {
1218
- rn(binder, Name::Decl);
1219
- renameStmts(&mut body.stmts, rn);
1220
- }
1221
- }
1222
- _ => {}
1223
- }
1224
- for ch in childExprs(e) {
1225
- renameExpr(ch, rn);
1226
- }
1227
- }
1228
-
1229
- // ---------- shared traversal ----------
993
+ // ---------- read-only traversal ----------
1230
994
 
1231
995
  fn walkBlockRef(b: &Block, f: &mut dyn FnMut(&Expr)) {
1232
996
  for s in &b.stmts {
@@ -1239,131 +1003,10 @@ fn walkBlockRef(b: &Block, f: &mut dyn FnMut(&Expr)) {
1239
1003
  }
1240
1004
  }
1241
1005
 
1006
+ /// Pre-order over `e`, closure bodies and catch blocks included.
1242
1007
  fn walkRef(e: &Expr, f: &mut dyn FnMut(&Expr)) {
1008
+ anyExpr(e, &mut |x| {
1243
- f(e);
1009
+ f(x);
1244
- for c in childRefs(e) {
1245
- walkRef(c, f);
1246
- }
1247
- match e {
1248
- Expr::Closure(c) => walkBlockRef(&c.body, f),
1249
- Expr::Catch(c) => {
1250
- if let CatchHandler::Block { body, .. } = &c.handler {
1251
- walkBlockRef(body, f);
1252
- }
1253
- }
1254
- _ => {}
1255
- }
1256
- }
1257
-
1258
- fn argRef(a: &Arg) -> &Expr {
1259
- match a {
1260
- Arg::Positional(e) | Arg::Keyword { value: e, .. } => e,
1261
- }
1262
- }
1263
-
1264
- fn childRefs(e: &Expr) -> Vec<&Expr> {
1265
- match e {
1266
- Expr::Binary(b) => vec![&b.left, &b.right],
1267
- Expr::Unary(u) => vec![&u.operand],
1268
- Expr::Bool(b) => vec![&b.left, &b.right],
1269
- Expr::Not(x) | Expr::Try(x) | Expr::Paren(x) => vec![x.as_ref()],
1270
- Expr::Bang(b) => vec![&b.value],
1271
- Expr::Bind(b) => {
1272
- let mut v = vec![&b.value];
1273
- v.extend(b.case.guard.iter());
1274
- v
1275
- }
1276
- Expr::Catch(c) => match &c.handler {
1277
- CatchHandler::Default(d) => vec![&c.value, d],
1278
- CatchHandler::Block { .. } => vec![&c.value],
1279
- },
1280
- Expr::Compare(c) => vec![&c.left, &c.right],
1281
- Expr::InlineIf(t) => vec![&t.condition, &t.then, &t.else_],
1282
- Expr::FnCall(c) => c.args.iter().map(argRef).collect(),
1283
- Expr::Yield(vs) => vs.iter().collect(),
1284
- Expr::ClassCall(c) => {
1285
- let mut v: Vec<&Expr> = c.fields.iter().map(|f| &f.value).collect();
1286
- if let Some(s) = &c.spread {
1287
- v.push(s.as_ref());
1288
- }
1289
- v
1290
- }
1291
- Expr::Attribute(a) => {
1292
- let mut v = vec![&a.object];
1293
- if let AttrKind::Method(c) = &a.attr {
1294
- v.extend(c.args.iter().map(argRef));
1295
- }
1296
- v
1297
- }
1298
- Expr::String(s) => s
1299
- .parts
1010
+ false
1300
- .iter()
1301
- .filter_map(|p| match p {
1302
- StringPart::Interp(e) => Some(e),
1303
- StringPart::Text(_) => None,
1304
- })
1011
+ });
1305
- .collect(),
1306
- Expr::Closure(_)
1307
- | Expr::Int(_)
1308
- | Expr::Float(_)
1309
- | Expr::BoolLit(_)
1310
- | Expr::Self_
1311
- | Expr::ErrorValue(_)
1312
- | Expr::Var(_)
1313
- | Expr::TypeName(_) => vec![],
1314
- }
1315
- }
1316
-
1317
- fn stmtExprRefs(s: &Stmt) -> Vec<&Expr> {
1318
- match s {
1319
- Stmt::Assign(a) => {
1320
- let mut v: Vec<&Expr> = Vec::new();
1321
- for t in &a.targets {
1322
- if let AssignTarget::Field(obj, _) = t {
1323
- v.push(obj.as_ref());
1324
- }
1325
- }
1326
- v.extend(a.values.iter());
1327
- v
1328
- }
1329
- Stmt::Assert(c) => vec![&c.cond],
1330
- Stmt::Crash(e) | Stmt::Expr(e, ..) => vec![e],
1331
- Stmt::Return(e) => e.iter().collect(),
1332
- Stmt::ForLoop(f) => {
1333
- let mut v: Vec<&Expr> = vec![&f.source];
1334
- for c in &f.clauses {
1335
- match c {
1336
- ForClause::If(e) | ForClause::Bind(_, e) => v.push(e),
1337
- }
1338
- }
1339
- v
1340
- }
1341
- Stmt::While(w) => vec![&w.condition],
1342
- Stmt::If(i) => {
1343
- let mut v = vec![&i.condition];
1344
- v.extend(i.else_ifs.iter().map(|e| &e.condition));
1345
- v
1346
- }
1347
- Stmt::Match(m) => {
1348
- let mut v: Vec<&Expr> = m.subjects.iter().collect();
1349
- v.extend(m.cases.iter().filter_map(|c| c.guard.as_ref()));
1350
- v
1351
- }
1352
- Stmt::Break | Stmt::Continue | Stmt::Todo => vec![],
1353
- }
1354
- }
1355
-
1356
- fn stmtBlockRefs(s: &Stmt) -> Vec<&Block> {
1357
- match s {
1358
- Stmt::ForLoop(f) => vec![&f.body],
1359
- Stmt::While(w) => vec![&w.body],
1360
- Stmt::If(i) => {
1361
- let mut v = vec![&i.body];
1362
- v.extend(i.else_ifs.iter().map(|e| &e.body));
1363
- v.extend(i.else_.iter());
1364
- v
1365
- }
1366
- Stmt::Match(m) => m.cases.iter().map(|c| &c.body).collect(),
1367
- _ => vec![],
1368
- }
1369
1012
  }
plum-core/src/drive.rs CHANGED
@@ -9,7 +9,10 @@
9
9
  //!
10
10
  //! `drive` returns notes: "fused <name>", "shared result ...", "whistle ...", "cap ...".
11
11
  use crate::ast::*;
12
- use crate::simplify::simplifyBlock;
12
+ use crate::generics::isGenericName;
13
+ use crate::rename::{renameStmts, substPT, substStmts, substType, Name};
14
+ use crate::simplify::{argRef, isTag, simplifyBlock, walkBlockRef, walkRef};
15
+ use crate::visit::{childRefs, stmtBlockRefs, stmtBlocks, stmtExprRefs, unparen, visitBlock, visitExpr};
13
16
  use crate::whistle::embeds;
14
17
  use std::collections::{HashMap, HashSet};
15
18
 
@@ -81,13 +84,6 @@ fn keyOf(f: &Fn) -> String {
81
84
  }
82
85
  }
83
86
 
84
- fn unparen(e: &Expr) -> &Expr {
85
- match e {
86
- Expr::Paren(i) => unparen(i),
87
- _ => e,
88
- }
89
- }
90
-
91
87
  fn positional(args: &[Arg]) -> Option<Vec<Expr>> {
92
88
  args.iter()
93
89
  .map(|a| match a {
@@ -114,75 +110,8 @@ fn pnames(f: &Fn) -> Vec<String> {
114
110
  v
115
111
  }
116
112
 
117
- fn isGen(n: &str) -> bool {
118
- n.len() == 1 && n.chars().all(|c| c.is_ascii_uppercase())
119
- }
120
-
121
- fn substType(t: &Type, m: &HashMap<String, Type>) -> Type {
122
- if t.generics.is_empty() {
123
- if let Some(r) = m.get(&t.name) {
124
- return r.clone();
125
- }
126
- }
127
- Type { name: t.name.clone(), generics: t.generics.iter().map(|g| substType(g, m)).collect() }
128
- }
129
-
130
- fn substPT(p: &ParamType, m: &HashMap<String, Type>) -> Option<ParamType> {
131
- match p {
132
- ParamType::Type(t) => Some(ParamType::Type(substType(t, m))),
133
- ParamType::Fn(ps, r) => Some(ParamType::Fn(
134
- ps.iter().map(|t| substType(t, m)).collect(),
135
- r.as_ref().map(|t| Box::new(substType(t, m))),
136
- )),
137
- _ => None,
138
- }
139
- }
140
-
141
- /// Applies a generic substitution to the types written inside a body: annotated locals,
142
- /// closure parameter types and explicit constructor type arguments.
143
- fn substStmts(stmts: &mut [Stmt], m: &HashMap<String, Type>) {
144
- for s in stmts {
145
- if let Stmt::Assign(a) = s {
146
- if let Some(t) = &a.ty {
147
- a.ty = Some(substType(t, m));
148
- }
149
- }
150
- for e in stmtExprs(s) {
151
- substExpr(e, m);
152
- }
153
- for b in stmtBlocks(s) {
154
- substStmts(&mut b.stmts, m);
155
- }
156
- }
157
- }
158
-
159
- fn substExpr(e: &mut Expr, m: &HashMap<String, Type>) {
160
- match e {
161
- Expr::Closure(c) => {
162
- for t in c.param_types.iter_mut().flatten() {
163
- *t = substType(t, m);
164
- }
165
- substStmts(&mut c.body.stmts, m);
166
- }
167
- Expr::ClassCall(c) => {
168
- for t in &mut c.generics {
169
- *t = substType(t, m);
170
- }
171
- }
172
- Expr::Catch(c) => {
173
- if let CatchHandler::Block { body, .. } = &mut c.handler {
174
- substStmts(&mut body.stmts, m);
175
- }
176
- }
177
- _ => {}
178
- }
179
- for ch in childExprs(e) {
180
- substExpr(ch, m);
181
- }
182
- }
183
-
184
113
  fn collectGens(t: &Type, out: &mut HashSet<String>) {
185
- if t.generics.is_empty() && isGen(&t.name) {
114
+ if t.generics.is_empty() && isGenericName(&t.name) {
186
115
  out.insert(t.name.clone());
187
116
  }
188
117
  t.generics.iter().for_each(|g| collectGens(g, out));
@@ -201,7 +130,7 @@ fn ptGens(p: &ParamType, out: &mut HashSet<String>) {
201
130
  }
202
131
 
203
132
  fn unify(a: &Type, b: &Type, map: &mut HashMap<String, Type>) -> Option<()> {
204
- if a.generics.is_empty() && isGen(&a.name) {
133
+ if a.generics.is_empty() && isGenericName(&a.name) {
205
134
  map.insert(a.name.clone(), b.clone());
206
135
  return Some(());
207
136
  }
@@ -217,11 +146,11 @@ fn unify(a: &Type, b: &Type, map: &mut HashMap<String, Type>) -> Option<()> {
217
146
  /// Like `unify`, but a bare generic of the producer's result `b` (`List[B]` from a
218
147
  /// `map`) binds to the consumer's concrete type too, into `pmap`.
219
148
  fn unify2(a: &Type, b: &Type, map: &mut HashMap<String, Type>, pmap: &mut HashMap<String, Type>) -> Option<()> {
220
- if a.generics.is_empty() && isGen(&a.name) {
149
+ if a.generics.is_empty() && isGenericName(&a.name) {
221
150
  map.insert(a.name.clone(), b.clone());
222
151
  return Some(());
223
152
  }
224
- if b.generics.is_empty() && isGen(&b.name) {
153
+ if b.generics.is_empty() && isGenericName(&b.name) {
225
154
  if pmap.get(&b.name).is_some_and(|t| t != a) {
226
155
  return None;
227
156
  }
@@ -1332,14 +1261,14 @@ impl Driver {
1332
1261
  bound.insert(n.clone());
1333
1262
  }
1334
1263
  Name::Pat => {
1335
- if !isTagName(n) {
1264
+ if !isTag(n) {
1336
1265
  bound.insert(n.clone());
1337
1266
  }
1338
1267
  }
1339
1268
  _ => {}
1340
1269
  });
1341
1270
  renameStmts(&mut body.stmts, &mut |n, kind| {
1342
- let isPatCtor = matches!(kind, Name::Pat) && isTagName(n);
1271
+ let isPatCtor = matches!(kind, Name::Pat) && isTag(n);
1343
1272
  if !isPatCtor && bound.contains(n.as_str()) {
1344
1273
  n.push_str(suf);
1345
1274
  }
@@ -1559,7 +1488,11 @@ impl Driver {
1559
1488
  for c in &mut cases {
1560
1489
  substStmts(&mut c.body.stmts, &map);
1561
1490
  if let Some(g) = &mut c.guard {
1491
+ // rename.rs keeps its expression walker private: run it over a one-statement block.
1492
+ let mut one = [Stmt::Expr(std::mem::replace(g, Expr::Int(0)), 0, 0)];
1562
- substExpr(g, &map);
1493
+ substStmts(&mut one, &map);
1494
+ let [Stmt::Expr(e, ..)] = one else { unreachable!() };
1495
+ *g = e;
1563
1496
  }
1564
1497
  }
1565
1498
  let tcx = Tcx { cases, callee: c.f.clone(), k, cn: cn.clone(), ctors: self.ctors.clone(), built: Default::default(), called: Default::default() };
@@ -1895,375 +1828,3 @@ impl Driver {
1895
1828
  /// The builtin numeric conversions (pure).
1896
1829
  const CASTS: &[&str] = &["Int", "Float", "Byte"];
1897
1830
 
1898
- fn visitBlock(b: &mut Block, f: &mut dyn FnMut(&mut Expr)) {
1899
- for s in &mut b.stmts {
1900
- for e in stmtExprs(s) {
1901
- visitExpr(e, f);
1902
- }
1903
- for blk in stmtBlocks(s) {
1904
- visitBlock(blk, f);
1905
- }
1906
- }
1907
- }
1908
-
1909
- /// Post-order over `e`, into closure bodies and catch blocks too.
1910
- fn visitExpr(e: &mut Expr, f: &mut dyn FnMut(&mut Expr)) {
1911
- for c in childExprs(e) {
1912
- visitExpr(c, f);
1913
- }
1914
- match e {
1915
- Expr::Closure(c) => visitBlock(&mut c.body, f),
1916
- Expr::Catch(c) => {
1917
- if let CatchHandler::Block { body, .. } = &mut c.handler {
1918
- visitBlock(body, f);
1919
- }
1920
- }
1921
- _ => {}
1922
- }
1923
- f(e);
1924
- }
1925
-
1926
- fn argExpr(a: &mut Arg) -> &mut Expr {
1927
- match a {
1928
- Arg::Positional(e) | Arg::Keyword { value: e, .. } => e,
1929
- }
1930
- }
1931
-
1932
- fn childExprs(e: &mut Expr) -> Vec<&mut Expr> {
1933
- match e {
1934
- Expr::Binary(b) => vec![&mut b.left, &mut b.right],
1935
- Expr::Unary(u) => vec![&mut u.operand],
1936
- Expr::Bool(b) => vec![&mut b.left, &mut b.right],
1937
- Expr::Not(x) | Expr::Try(x) | Expr::Paren(x) => vec![x.as_mut()],
1938
- Expr::Bang(b) => vec![&mut b.value],
1939
- Expr::Bind(b) => {
1940
- let mut v = vec![&mut b.value];
1941
- v.extend(b.case.guard.iter_mut());
1942
- v
1943
- }
1944
- Expr::Catch(c) => {
1945
- let CatchExpr { value, handler, .. } = &mut **c;
1946
- match handler {
1947
- CatchHandler::Default(d) => vec![value, d],
1948
- CatchHandler::Block { .. } => vec![value],
1949
- }
1950
- }
1951
- Expr::Compare(c) => vec![&mut c.left, &mut c.right],
1952
- Expr::InlineIf(t) => vec![&mut t.condition, &mut t.then, &mut t.else_],
1953
- Expr::FnCall(c) => c.args.iter_mut().map(argExpr).collect(),
1954
- Expr::Yield(vs) => vs.iter_mut().collect(),
1955
- Expr::ClassCall(c) => {
1956
- let mut v: Vec<&mut Expr> = c.fields.iter_mut().map(|f| &mut f.value).collect();
1957
- if let Some(s) = &mut c.spread {
1958
- v.push(s.as_mut());
1959
- }
1960
- v
1961
- }
1962
- Expr::Attribute(a) => {
1963
- let AttributeExpr { object, attr } = &mut **a;
1964
- let mut v = vec![object];
1965
- if let AttrKind::Method(c) = attr {
1966
- v.extend(c.args.iter_mut().map(argExpr));
1967
- }
1968
- v
1969
- }
1970
- Expr::String(s) => s
1971
- .parts
1972
- .iter_mut()
1973
- .filter_map(|p| match p {
1974
- StringPart::Interp(e) => Some(e),
1975
- StringPart::Text(_) => None,
1976
- })
1977
- .collect(),
1978
- Expr::Closure(_)
1979
- | Expr::Int(_)
1980
- | Expr::Float(_)
1981
- | Expr::BoolLit(_)
1982
- | Expr::Self_
1983
- | Expr::ErrorValue(_)
1984
- | Expr::Var(_)
1985
- | Expr::TypeName(_) => vec![],
1986
- }
1987
- }
1988
-
1989
- fn stmtExprs(s: &mut Stmt) -> Vec<&mut Expr> {
1990
- match s {
1991
- Stmt::Assign(a) => {
1992
- let mut v: Vec<&mut Expr> = Vec::new();
1993
- for t in &mut a.targets {
1994
- if let AssignTarget::Field(obj, _) = t {
1995
- v.push(obj.as_mut());
1996
- }
1997
- }
1998
- v.extend(a.values.iter_mut());
1999
- v
2000
- }
2001
- Stmt::Assert(c) => vec![&mut c.cond],
2002
- Stmt::Crash(e) | Stmt::Expr(e, ..) => vec![e],
2003
- Stmt::Return(e) => e.iter_mut().collect(),
2004
- Stmt::ForLoop(f) => {
2005
- let mut v: Vec<&mut Expr> = vec![&mut f.source];
2006
- for c in &mut f.clauses {
2007
- match c {
2008
- ForClause::If(e) | ForClause::Bind(_, e) => v.push(e),
2009
- }
2010
- }
2011
- v
2012
- }
2013
- Stmt::While(w) => vec![&mut w.condition],
2014
- Stmt::If(i) => {
2015
- let mut v = vec![&mut i.condition];
2016
- v.extend(i.else_ifs.iter_mut().map(|e| &mut e.condition));
2017
- v
2018
- }
2019
- Stmt::Match(m) => {
2020
- let mut v: Vec<&mut Expr> = m.subjects.iter_mut().collect();
2021
- v.extend(m.cases.iter_mut().filter_map(|c| c.guard.as_mut()));
2022
- v
2023
- }
2024
- Stmt::Break | Stmt::Continue | Stmt::Todo => vec![],
2025
- }
2026
- }
2027
-
2028
- fn stmtBlocks(s: &mut Stmt) -> Vec<&mut Block> {
2029
- match s {
2030
- Stmt::ForLoop(f) => vec![&mut f.body],
2031
- Stmt::While(w) => vec![&mut w.body],
2032
- Stmt::If(i) => {
2033
- let mut v = vec![&mut i.body];
2034
- v.extend(i.else_ifs.iter_mut().map(|e| &mut e.body));
2035
- v.extend(i.else_.iter_mut());
2036
- v
2037
- }
2038
- Stmt::Match(m) => m.cases.iter_mut().map(|c| &mut c.body).collect(),
2039
- _ => vec![],
2040
- }
2041
- }
2042
-
2043
- #[derive(Clone, Copy)]
2044
- enum Name {
2045
- /// A declaration site: `x := v`, a loop binder, a pattern name, a closure param.
2046
- Decl,
2047
- /// A name in a pattern (a constructor name is not a binder).
2048
- Pat,
2049
- /// The target of a plain `x = v`.
2050
- Plain,
2051
- /// A use.
2052
- Ref,
2053
- }
2054
-
2055
- fn renameStmts(stmts: &mut [Stmt], rn: &mut dyn FnMut(&mut String, Name)) {
2056
- for s in stmts {
2057
- match s {
2058
- Stmt::Assign(a) => {
2059
- let kind = if a.declare { Name::Decl } else { Name::Plain };
2060
- for t in &mut a.targets {
2061
- if let AssignTarget::Var(n) = t {
2062
- rn(n, kind);
2063
- }
2064
- }
2065
- }
2066
- Stmt::ForLoop(f) => {
2067
- f.binders.iter_mut().for_each(|v| rn(v, Name::Decl));
2068
- for c in &mut f.clauses {
2069
- if let ForClause::Bind(n, _) = c {
2070
- rn(n, Name::Decl);
2071
- }
2072
- }
2073
- if let Some((n, _)) = &mut f.result {
2074
- rn(n, Name::Decl);
2075
- }
2076
- }
2077
- Stmt::Match(m) => {
2078
- for c in &mut m.cases {
2079
- for p in &mut c.patterns {
2080
- renamePattern(p, rn);
2081
- }
2082
- }
2083
- }
2084
- _ => {}
2085
- }
2086
- for e in stmtExprs(s) {
2087
- renameExpr(e, rn);
2088
- }
2089
- for b in stmtBlocks(s) {
2090
- renameStmts(&mut b.stmts, rn);
2091
- }
2092
- }
2093
- }
2094
-
2095
- /// Pattern names that test a tag (known ctor or capitalised error name) are never binders.
2096
- fn isTagName(n: &str) -> bool {
2097
- n.chars().next().is_some_and(|c| c.is_uppercase())
2098
- }
2099
-
2100
- fn renamePattern(p: &mut CasePattern, rn: &mut dyn FnMut(&mut String, Name)) {
2101
- match p {
2102
- CasePattern::Name(n) => rn(n, Name::Pat),
2103
- CasePattern::Class { fields, .. } => fields.iter_mut().for_each(|f| renamePattern(f, rn)),
2104
- _ => {}
2105
- }
2106
- }
2107
-
2108
- fn renameExpr(e: &mut Expr, rn: &mut dyn FnMut(&mut String, Name)) {
2109
- match e {
2110
- Expr::Var(n) => rn(n, Name::Ref),
2111
- Expr::Bind(b) => renamePattern(&mut b.case.patterns[0], rn),
2112
- Expr::Closure(c) => {
2113
- c.params.iter_mut().for_each(|p| rn(p, Name::Decl));
2114
- renameStmts(&mut c.body.stmts, rn);
2115
- }
2116
- Expr::Catch(c) => {
2117
- if let CatchHandler::Block { binder, body } = &mut c.handler {
2118
- rn(binder, Name::Decl);
2119
- renameStmts(&mut body.stmts, rn);
2120
- }
2121
- }
2122
- _ => {}
2123
- }
2124
- for ch in childExprs(e) {
2125
- renameExpr(ch, rn);
2126
- }
2127
- }
2128
-
2129
- // ---------- shared traversal ----------
2130
-
2131
- fn walkBlockRef(b: &Block, f: &mut dyn FnMut(&Expr)) {
2132
- for s in &b.stmts {
2133
- for e in stmtExprRefs(s) {
2134
- walkRef(e, f);
2135
- }
2136
- for blk in stmtBlockRefs(s) {
2137
- walkBlockRef(blk, f);
2138
- }
2139
- }
2140
- }
2141
-
2142
- fn walkRef(e: &Expr, f: &mut dyn FnMut(&Expr)) {
2143
- f(e);
2144
- for c in childRefs(e) {
2145
- walkRef(c, f);
2146
- }
2147
- match e {
2148
- Expr::Closure(c) => walkBlockRef(&c.body, f),
2149
- Expr::Catch(c) => {
2150
- if let CatchHandler::Block { body, .. } = &c.handler {
2151
- walkBlockRef(body, f);
2152
- }
2153
- }
2154
- _ => {}
2155
- }
2156
- }
2157
-
2158
- fn argRef(a: &Arg) -> &Expr {
2159
- match a {
2160
- Arg::Positional(e) | Arg::Keyword { value: e, .. } => e,
2161
- }
2162
- }
2163
-
2164
- fn childRefs(e: &Expr) -> Vec<&Expr> {
2165
- match e {
2166
- Expr::Binary(b) => vec![&b.left, &b.right],
2167
- Expr::Unary(u) => vec![&u.operand],
2168
- Expr::Bool(b) => vec![&b.left, &b.right],
2169
- Expr::Not(x) | Expr::Try(x) | Expr::Paren(x) => vec![x.as_ref()],
2170
- Expr::Bang(b) => vec![&b.value],
2171
- Expr::Bind(b) => {
2172
- let mut v = vec![&b.value];
2173
- v.extend(b.case.guard.iter());
2174
- v
2175
- }
2176
- Expr::Catch(c) => match &c.handler {
2177
- CatchHandler::Default(d) => vec![&c.value, d],
2178
- CatchHandler::Block { .. } => vec![&c.value],
2179
- },
2180
- Expr::Compare(c) => vec![&c.left, &c.right],
2181
- Expr::InlineIf(t) => vec![&t.condition, &t.then, &t.else_],
2182
- Expr::FnCall(c) => c.args.iter().map(argRef).collect(),
2183
- Expr::Yield(vs) => vs.iter().collect(),
2184
- Expr::ClassCall(c) => {
2185
- let mut v: Vec<&Expr> = c.fields.iter().map(|f| &f.value).collect();
2186
- if let Some(s) = &c.spread {
2187
- v.push(s.as_ref());
2188
- }
2189
- v
2190
- }
2191
- Expr::Attribute(a) => {
2192
- let mut v = vec![&a.object];
2193
- if let AttrKind::Method(c) = &a.attr {
2194
- v.extend(c.args.iter().map(argRef));
2195
- }
2196
- v
2197
- }
2198
- Expr::String(s) => s
2199
- .parts
2200
- .iter()
2201
- .filter_map(|p| match p {
2202
- StringPart::Interp(e) => Some(e),
2203
- StringPart::Text(_) => None,
2204
- })
2205
- .collect(),
2206
- Expr::Closure(_)
2207
- | Expr::Int(_)
2208
- | Expr::Float(_)
2209
- | Expr::BoolLit(_)
2210
- | Expr::Self_
2211
- | Expr::ErrorValue(_)
2212
- | Expr::Var(_)
2213
- | Expr::TypeName(_) => vec![],
2214
- }
2215
- }
2216
-
2217
- fn stmtExprRefs(s: &Stmt) -> Vec<&Expr> {
2218
- match s {
2219
- Stmt::Assign(a) => {
2220
- let mut v: Vec<&Expr> = Vec::new();
2221
- for t in &a.targets {
2222
- if let AssignTarget::Field(obj, _) = t {
2223
- v.push(obj.as_ref());
2224
- }
2225
- }
2226
- v.extend(a.values.iter());
2227
- v
2228
- }
2229
- Stmt::Assert(c) => vec![&c.cond],
2230
- Stmt::Crash(e) | Stmt::Expr(e, ..) => vec![e],
2231
- Stmt::Return(e) => e.iter().collect(),
2232
- Stmt::ForLoop(f) => {
2233
- let mut v: Vec<&Expr> = vec![&f.source];
2234
- for c in &f.clauses {
2235
- match c {
2236
- ForClause::If(e) | ForClause::Bind(_, e) => v.push(e),
2237
- }
2238
- }
2239
- v
2240
- }
2241
- Stmt::While(w) => vec![&w.condition],
2242
- Stmt::If(i) => {
2243
- let mut v = vec![&i.condition];
2244
- v.extend(i.else_ifs.iter().map(|e| &e.condition));
2245
- v
2246
- }
2247
- Stmt::Match(m) => {
2248
- let mut v: Vec<&Expr> = m.subjects.iter().collect();
2249
- v.extend(m.cases.iter().filter_map(|c| c.guard.as_ref()));
2250
- v
2251
- }
2252
- Stmt::Break | Stmt::Continue | Stmt::Todo => vec![],
2253
- }
2254
- }
2255
-
2256
- fn stmtBlockRefs(s: &Stmt) -> Vec<&Block> {
2257
- match s {
2258
- Stmt::ForLoop(f) => vec![&f.body],
2259
- Stmt::While(w) => vec![&w.body],
2260
- Stmt::If(i) => {
2261
- let mut v = vec![&i.body];
2262
- v.extend(i.else_ifs.iter().map(|e| &e.body));
2263
- v.extend(i.else_.iter());
2264
- v
2265
- }
2266
- Stmt::Match(m) => m.cases.iter().map(|c| &c.body).collect(),
2267
- _ => vec![],
2268
- }
2269
- }
plum-core/src/forms.rs CHANGED
@@ -57,6 +57,9 @@ use std::rc::Rc;
57
57
  use std::sync::atomic::{AtomicUsize, Ordering};
58
58
 
59
59
  use crate::ast::*;
60
+ use crate::generics::isGenericName;
61
+ use crate::rename;
62
+ use crate::visit::{childExprs, patternBinders, stmtBlocks, stmtExprs};
60
63
 
61
64
  type FormError = (usize, usize, String);
62
65
  type FormUse = (String, String, usize, usize);
@@ -563,10 +566,6 @@ pub fn lowerLoopTyped(
563
566
  lowerLoopWith(fl, typeOf, Some(fullTypeOf), false)
564
567
  }
565
568
 
566
- fn isGenericName(n: &str) -> bool {
567
- n.len() == 1 && n.chars().all(|c| c.is_ascii_uppercase())
568
- }
569
-
570
569
  /// Binds the generic names of `pattern` against `actual`; the first binding wins.
571
570
  fn bindGenerics(pattern: &Type, actual: &Type, map: &mut HashMap<String, Type>) {
572
571
  if pattern.generics.is_empty() && isGenericName(&pattern.name) {
@@ -579,15 +578,7 @@ fn bindGenerics(pattern: &Type, actual: &Type, map: &mut HashMap<String, Type>)
579
578
  }
580
579
 
581
580
  fn substType(t: &mut Type, map: &HashMap<String, Type>) {
582
- if t.generics.is_empty() {
583
- if let Some(r) = map.get(&t.name) {
584
- *t = r.clone();
585
- }
586
- return;
587
- }
588
- for g in &mut t.generics {
589
- substType(g, map);
581
+ *t = rename::substType(t, map);
590
- }
591
582
  }
592
583
 
593
584
  fn substStmtTypes(s: &mut Stmt, map: &HashMap<String, Type>) {
@@ -1601,159 +1592,10 @@ fn argExpr(a: &mut Arg) -> &mut Expr {
1601
1592
  }
1602
1593
  }
1603
1594
 
1604
- /// Direct sub-expressions (closure bodies are statements, not included).
1605
- fn childExprs(e: &mut Expr) -> Vec<&mut Expr> {
1606
- match e {
1607
- Expr::Binary(b) => vec![&mut b.left, &mut b.right],
1608
- Expr::Unary(u) => vec![&mut u.operand],
1609
- Expr::Bool(b) => vec![&mut b.left, &mut b.right],
1610
- Expr::Not(x) | Expr::Try(x) | Expr::Paren(x) => vec![x.as_mut()],
1611
- Expr::Bang(b) => vec![&mut b.value],
1612
- Expr::Bind(b) => {
1613
- let mut v = vec![&mut b.value];
1614
- v.extend(b.case.guard.iter_mut());
1615
- v
1616
- }
1617
- Expr::Catch(c) => {
1618
- let CatchExpr { value, handler, .. } = &mut **c;
1619
- match handler {
1620
- CatchHandler::Default(d) => vec![value, d],
1621
- CatchHandler::Block { .. } => vec![value],
1622
- }
1623
- }
1624
- Expr::Compare(c) => vec![&mut c.left, &mut c.right],
1625
- Expr::InlineIf(t) => vec![&mut t.condition, &mut t.then, &mut t.else_],
1626
- Expr::FnCall(c) => c.args.iter_mut().map(argExpr).collect(),
1627
- Expr::Yield(vs) => vs.iter_mut().collect(),
1628
- Expr::ClassCall(c) => {
1629
- let mut v: Vec<&mut Expr> = c.fields.iter_mut().map(|f| &mut f.value).collect();
1630
- if let Some(s) = &mut c.spread {
1631
- v.push(s.as_mut());
1632
- }
1633
- v
1634
- }
1635
- Expr::Attribute(a) => {
1636
- let AttributeExpr { object, attr } = &mut **a;
1637
- let mut v = vec![object];
1638
- if let AttrKind::Method(c) = attr {
1639
- v.extend(c.args.iter_mut().map(argExpr));
1640
- }
1641
- v
1642
- }
1643
- Expr::String(s) => s
1644
- .parts
1645
- .iter_mut()
1646
- .filter_map(|p| match p {
1647
- StringPart::Interp(e) => Some(e),
1648
- StringPart::Text(_) => None,
1649
- })
1650
- .collect(),
1651
- Expr::Closure(_)
1652
- | Expr::Int(_)
1653
- | Expr::Float(_)
1654
- | Expr::BoolLit(_)
1655
- | Expr::Self_
1656
- | Expr::ErrorValue(_)
1657
- | Expr::Var(_)
1658
- | Expr::TypeName(_) => vec![],
1659
- }
1660
- }
1661
-
1662
- /// The expressions a statement evaluates directly (not those of nested blocks).
1663
- fn stmtExprs(s: &mut Stmt) -> Vec<&mut Expr> {
1664
- match s {
1665
- Stmt::Assign(a) => {
1666
- let mut v: Vec<&mut Expr> = Vec::new();
1667
- for t in &mut a.targets {
1668
- if let AssignTarget::Field(obj, _) = t {
1669
- v.push(obj.as_mut());
1670
- }
1671
- }
1672
- v.extend(a.values.iter_mut());
1673
- v
1674
- }
1675
- Stmt::Assert(c) => vec![&mut c.cond],
1676
- Stmt::Crash(e) | Stmt::Expr(e, ..) => vec![e],
1677
- Stmt::Return(e) => e.iter_mut().collect(),
1678
- Stmt::ForLoop(f) => {
1679
- let mut v: Vec<&mut Expr> = Vec::new();
1680
- v.push(&mut f.source);
1681
- for c in &mut f.clauses {
1682
- match c {
1683
- ForClause::If(e) | ForClause::Bind(_, e) => v.push(e),
1684
- }
1685
- }
1686
- v
1687
- }
1688
- Stmt::While(w) => vec![&mut w.condition],
1689
- Stmt::If(i) => {
1690
- let mut v = vec![&mut i.condition];
1691
- v.extend(i.else_ifs.iter_mut().map(|e| &mut e.condition));
1692
- v
1693
- }
1694
- Stmt::Match(m) => {
1695
- let mut v: Vec<&mut Expr> = m.subjects.iter_mut().collect();
1696
- v.extend(m.cases.iter_mut().filter_map(|c| c.guard.as_mut()));
1697
- v
1698
- }
1699
- Stmt::Break | Stmt::Continue | Stmt::Todo => vec![],
1700
- }
1701
- }
1702
-
1703
- fn stmtBlocks(s: &mut Stmt) -> Vec<&mut Block> {
1704
- match s {
1705
- Stmt::ForLoop(f) => vec![&mut f.body],
1706
- Stmt::While(w) => vec![&mut w.body],
1707
- Stmt::If(i) => {
1708
- let mut v = vec![&mut i.body];
1709
- v.extend(i.else_ifs.iter_mut().map(|e| &mut e.body));
1710
- v.extend(i.else_.iter_mut());
1711
- v
1712
- }
1713
- Stmt::Match(m) => m.cases.iter_mut().map(|c| &mut c.body).collect(),
1714
- _ => vec![],
1715
- }
1716
- }
1717
-
1718
1595
  /// Visits every variable name in `stmts`; the flag is true at a declaration
1719
1596
  /// site (assignment target, loop binder, pattern name, closure parameter).
1720
1597
  fn renameStmts(stmts: &mut [Stmt], rn: &mut dyn FnMut(&mut String, bool)) {
1721
- for s in stmts {
1722
- match s {
1723
- Stmt::Assign(a) => {
1724
- for t in &mut a.targets {
1725
- if let AssignTarget::Var(n) = t {
1726
- rn(n, true);
1727
- }
1728
- }
1729
- }
1730
- Stmt::ForLoop(f) => {
1731
- f.binders.iter_mut().for_each(|v| rn(v, true));
1732
- for c in &mut f.clauses {
1733
- if let ForClause::Bind(n, _) = c {
1734
- rn(n, true);
1735
- }
1736
- }
1737
- if let Some((n, _)) = &mut f.result {
1738
- rn(n, true);
1739
- }
1740
- }
1741
- Stmt::Match(m) => {
1742
- for c in &mut m.cases {
1743
- for p in &mut c.patterns {
1744
- renamePattern(p, rn);
1745
- }
1746
- }
1747
- }
1748
- _ => {}
1749
- }
1750
- for e in stmtExprs(s) {
1751
- renameExpr(e, rn);
1752
- }
1753
- for b in stmtBlocks(s) {
1754
- renameStmts(&mut b.stmts, rn);
1598
+ rename::renameStmts(stmts, &mut |n, k| rn(n, k != rename::Name::Ref));
1755
- }
1756
- }
1757
1599
  }
1758
1600
 
1759
1601
  fn renamePattern(p: &mut CasePattern, rn: &mut dyn FnMut(&mut String, bool)) {
@@ -1765,24 +1607,11 @@ fn renamePattern(p: &mut CasePattern, rn: &mut dyn FnMut(&mut String, bool)) {
1765
1607
  }
1766
1608
 
1767
1609
  fn renameExpr(e: &mut Expr, rn: &mut dyn FnMut(&mut String, bool)) {
1768
- match e {
1769
- Expr::Var(n) => rn(n, false),
1610
+ let placeholder = Expr::Int(0);
1770
- Expr::Bind(b) => renamePattern(&mut b.case.patterns[0], rn),
1611
+ let mut one = [Stmt::Expr(std::mem::replace(e, placeholder), 0, 0)];
1771
- Expr::Closure(c) => {
1772
- c.params.iter_mut().for_each(|p| rn(p, true));
1773
- renameStmts(&mut c.body.stmts, rn);
1774
- }
1775
- Expr::Catch(c) => {
1776
- if let CatchHandler::Block { binder, body } = &mut c.handler {
1777
- rn(binder, true);
1778
- renameStmts(&mut body.stmts, rn);
1612
+ renameStmts(&mut one, rn);
1779
- }
1613
+ let [Stmt::Expr(back, ..)] = one else { unreachable!() };
1780
- }
1781
- _ => {}
1614
+ *e = back;
1782
- }
1783
- for ch in childExprs(e) {
1784
- renameExpr(ch, rn);
1785
- }
1786
1615
  }
1787
1616
 
1788
1617
  /// Prefix of the ordinary-fn copy of a `fun*` that is kept in the program so
@@ -2074,16 +1903,6 @@ fn uniqueCatchBindersExpr(e: &mut Expr, counter: &mut usize) {
2074
1903
  }
2075
1904
  }
2076
1905
 
2077
- fn patternBinders(p: &CasePattern, out: &mut Vec<String>) {
2078
- match p {
2079
- CasePattern::Name(n) if n.chars().next().is_some_and(|c| c.is_lowercase()) => {
2080
- out.push(n.clone())
2081
- }
2082
- CasePattern::Class { fields, .. } => fields.iter().for_each(|f| patternBinders(f, out)),
2083
- _ => {}
2084
- }
2085
- }
2086
-
2087
1906
  /// Visits every statement of `stmts`, nested blocks and closure bodies included.
2088
1907
  pub fn walkStmts(stmts: &mut [Stmt], fs: &mut dyn FnMut(&mut Stmt)) {
2089
1908
  walkDeep(stmts, fs, &mut |_| {});
plum-core/src/generics.rs ADDED
@@ -0,0 +1,45 @@
1
+ //! The one rule for "is this name a generic type parameter" and a fn's generic set.
2
+ use crate::ast::{Fn, ParamType, Type};
3
+
4
+ /// A single uppercase letter (`T`, `U`, `K`, ...) is the grammar's only legal
5
+ /// spelling for a generic type parameter.
6
+ pub fn isGenericName(n: &str) -> bool {
7
+ let mut chars = n.chars();
8
+ match (chars.next(), chars.next()) {
9
+ (Some(c), None) => c.is_ascii_uppercase(),
10
+ _ => false,
11
+ }
12
+ }
13
+
14
+ fn typeGenerics(t: &Type, out: &mut Vec<String>) {
15
+ if isGenericName(&t.name) && !out.contains(&t.name) {
16
+ out.push(t.name.clone());
17
+ }
18
+ t.generics.iter().for_each(|g| typeGenerics(g, out));
19
+ }
20
+
21
+ /// The generic names `f` is parameterised by: the `owner` type's generics, then every
22
+ /// further generic name its parameter, yield and return types mention, in first-appearance order.
23
+ pub fn fnGenerics(f: &Fn, owner: &[String]) -> Vec<String> {
24
+ let mut out: Vec<String> = owner.to_vec();
25
+ for p in &f.params {
26
+ match &p.ty {
27
+ ParamType::Type(t) | ParamType::Variadic(t) | ParamType::Seq(t) => {
28
+ typeGenerics(t, &mut out)
29
+ }
30
+ ParamType::Fn(ps, r) => {
31
+ ps.iter().for_each(|t| typeGenerics(t, &mut out));
32
+ if let Some(r) = r {
33
+ typeGenerics(r, &mut out);
34
+ }
35
+ }
36
+ }
37
+ }
38
+ if let Some(ys) = &f.yields {
39
+ ys.iter().for_each(|t| typeGenerics(t, &mut out));
40
+ }
41
+ if let Some(r) = &f.returns {
42
+ typeGenerics(r, &mut out);
43
+ }
44
+ out
45
+ }
plum-core/src/lib.rs CHANGED
@@ -11,9 +11,11 @@ pub mod drive;
11
11
  pub mod embeds;
12
12
  pub mod formatter;
13
13
  pub mod forms;
14
+ pub mod generics;
14
15
  pub mod loader;
15
16
  pub mod parser;
16
17
  pub mod purity;
18
+ pub mod rename;
17
19
  pub mod reuse;
18
20
  pub mod simplify;
19
21
  pub mod tail;
@@ -22,6 +24,7 @@ pub mod trmc;
22
24
  pub mod union_lower;
23
25
  pub mod uniqueness;
24
26
  pub mod unions;
27
+ pub mod visit;
25
28
  pub mod whistle;
26
29
 
27
30
  pub use formatter::{
plum-core/src/rename.rs ADDED
@@ -0,0 +1,159 @@
1
+ //! Shared renaming and generic-type substitution over statements and expressions.
2
+ //!
3
+ //! `renameStmts` reports every variable occurrence to a callback with its `Name` kind;
4
+ //! the callback rewrites the string in place. Scoping (shadowing by closure params,
5
+ //! loop binders, ...) is the callback's business: it sees `Decl` before the uses in the
6
+ //! scope it opens. A caller that does not distinguish pattern names from other
7
+ //! declarations maps `Name::Pat` to `Name::Decl` itself; one that wants a boolean
8
+ //! "is declaration" tests `matches!(k, Name::Decl | Name::Pat)`.
9
+ //! A field-assign target (`self.x = v`) is not a variable and is never reported as one;
10
+ //! only its object expression is walked.
11
+ use crate::ast::*;
12
+ use crate::visit::{childExprs, stmtBlocks, stmtExprs};
13
+ use std::collections::HashMap;
14
+
15
+ #[derive(Clone, Copy, PartialEq, Eq, Debug)]
16
+ pub enum Name {
17
+ /// A declaration site: `x := v`, a loop binder, a pattern name, a closure param.
18
+ Decl,
19
+ /// A name in a pattern (a constructor name is not a binder).
20
+ Pat,
21
+ /// The target of a plain `x = v`.
22
+ Plain,
23
+ /// A use.
24
+ Ref,
25
+ }
26
+
27
+ pub fn renameStmts(stmts: &mut [Stmt], rn: &mut dyn FnMut(&mut String, Name)) {
28
+ for s in stmts {
29
+ match s {
30
+ Stmt::Assign(a) => {
31
+ let kind = if a.declare { Name::Decl } else { Name::Plain };
32
+ for t in &mut a.targets {
33
+ if let AssignTarget::Var(n) = t {
34
+ rn(n, kind);
35
+ }
36
+ }
37
+ }
38
+ Stmt::ForLoop(f) => {
39
+ f.binders.iter_mut().for_each(|v| rn(v, Name::Decl));
40
+ for c in &mut f.clauses {
41
+ if let ForClause::Bind(n, _) = c {
42
+ rn(n, Name::Decl);
43
+ }
44
+ }
45
+ if let Some((n, _)) = &mut f.result {
46
+ rn(n, Name::Decl);
47
+ }
48
+ }
49
+ Stmt::Match(m) => {
50
+ for c in &mut m.cases {
51
+ for p in &mut c.patterns {
52
+ renamePattern(p, rn);
53
+ }
54
+ }
55
+ }
56
+ _ => {}
57
+ }
58
+ for e in stmtExprs(s) {
59
+ renameExpr(e, rn);
60
+ }
61
+ for b in stmtBlocks(s) {
62
+ renameStmts(&mut b.stmts, rn);
63
+ }
64
+ }
65
+ }
66
+
67
+ fn renamePattern(p: &mut CasePattern, rn: &mut dyn FnMut(&mut String, Name)) {
68
+ match p {
69
+ CasePattern::Name(n) => rn(n, Name::Pat),
70
+ CasePattern::Class { fields, .. } => fields.iter_mut().for_each(|f| renamePattern(f, rn)),
71
+ _ => {}
72
+ }
73
+ }
74
+
75
+ fn renameExpr(e: &mut Expr, rn: &mut dyn FnMut(&mut String, Name)) {
76
+ match e {
77
+ Expr::Var(n) => rn(n, Name::Ref),
78
+ Expr::Bind(b) => renamePattern(&mut b.case.patterns[0], rn),
79
+ Expr::Closure(c) => {
80
+ c.params.iter_mut().for_each(|p| rn(p, Name::Decl));
81
+ renameStmts(&mut c.body.stmts, rn);
82
+ }
83
+ Expr::Catch(c) => {
84
+ if let CatchHandler::Block { binder, body } = &mut c.handler {
85
+ rn(binder, Name::Decl);
86
+ renameStmts(&mut body.stmts, rn);
87
+ }
88
+ }
89
+ _ => {}
90
+ }
91
+ for ch in childExprs(e) {
92
+ renameExpr(ch, rn);
93
+ }
94
+ }
95
+
96
+
97
+ pub fn substType(t: &Type, m: &HashMap<String, Type>) -> Type {
98
+ if t.generics.is_empty() {
99
+ if let Some(r) = m.get(&t.name) {
100
+ return r.clone();
101
+ }
102
+ }
103
+ Type { name: t.name.clone(), generics: t.generics.iter().map(|g| substType(g, m)).collect() }
104
+ }
105
+
106
+ pub fn substPT(p: &ParamType, m: &HashMap<String, Type>) -> Option<ParamType> {
107
+ match p {
108
+ ParamType::Type(t) => Some(ParamType::Type(substType(t, m))),
109
+ ParamType::Fn(ps, r) => Some(ParamType::Fn(
110
+ ps.iter().map(|t| substType(t, m)).collect(),
111
+ r.as_ref().map(|t| Box::new(substType(t, m))),
112
+ )),
113
+ _ => None,
114
+ }
115
+ }
116
+
117
+ /// Applies a generic substitution to the types written inside a body: annotated locals,
118
+ /// closure parameter types and explicit constructor type arguments.
119
+ pub fn substStmts(stmts: &mut [Stmt], m: &HashMap<String, Type>) {
120
+ for s in stmts {
121
+ if let Stmt::Assign(a) = s {
122
+ if let Some(t) = &a.ty {
123
+ a.ty = Some(substType(t, m));
124
+ }
125
+ }
126
+ for e in stmtExprs(s) {
127
+ substExpr(e, m);
128
+ }
129
+ for b in stmtBlocks(s) {
130
+ substStmts(&mut b.stmts, m);
131
+ }
132
+ }
133
+ }
134
+
135
+ fn substExpr(e: &mut Expr, m: &HashMap<String, Type>) {
136
+ match e {
137
+ Expr::Closure(c) => {
138
+ for t in c.param_types.iter_mut().flatten() {
139
+ *t = substType(t, m);
140
+ }
141
+ substStmts(&mut c.body.stmts, m);
142
+ }
143
+ Expr::ClassCall(c) => {
144
+ for t in &mut c.generics {
145
+ *t = substType(t, m);
146
+ }
147
+ }
148
+ Expr::Catch(c) => {
149
+ if let CatchHandler::Block { body, .. } = &mut c.handler {
150
+ substStmts(&mut body.stmts, m);
151
+ }
152
+ }
153
+ _ => {}
154
+ }
155
+ for ch in childExprs(e) {
156
+ substExpr(ch, m);
157
+ }
158
+ }
159
+
plum-core/src/reuse.rs CHANGED
@@ -16,25 +16,6 @@ use crate::ast::*;
16
16
  use crate::forms::{mapExprs, walkStmts};
17
17
  use std::collections::HashMap;
18
18
 
19
- fn countVar(body: &FnBody, name: &str) -> usize {
20
- let mut b = body.clone();
21
- let mut n = 0;
22
- mapExprs(&mut b, &mut |e| {
23
- if matches!(e, Expr::Var(v) if v == name) {
24
- n += 1;
25
- }
26
- });
27
- n
28
- }
29
-
30
- fn binders(p: &CasePattern, out: &mut Vec<String>) {
31
- match p {
32
- CasePattern::Name(n) => out.push(n.clone()),
33
- CasePattern::Class { fields, .. } => fields.iter().for_each(|f| binders(f, out)),
34
- _ => {}
35
- }
36
- }
37
-
38
19
  fn deepFresh(e: &Expr) -> bool {
39
20
  match e {
40
21
  Expr::ClassCall(c) => c.spread.is_none() && c.fields.iter().all(|f| deepFresh(&f.value)),
@@ -93,7 +74,7 @@ pub fn markReuse(src: &mut Source) -> Vec<String> {
93
74
  if !matches!(p.ty, ParamType::Type(_)) || p.default.is_some() {
94
75
  continue;
95
76
  }
96
- if countVar(&f.body, v) != 1 {
77
+ if crate::visit::countVar(&f.body, v) != 1 {
97
78
  continue;
98
79
  }
99
80
  let mut bad = false;
@@ -124,7 +105,7 @@ pub fn markReuse(src: &mut Source) -> Vec<String> {
124
105
  let mut inner: Vec<String> = Vec::new();
125
106
  for case in &m.cases {
126
107
  for p in &case.patterns {
127
- binders(p, &mut inner);
108
+ crate::visit::patternBinders(p, &mut inner);
128
109
  }
129
110
  }
130
111
  for (idx, it) in src.items.iter().enumerate() {
@@ -139,7 +120,7 @@ pub fn markReuse(src: &mut Source) -> Vec<String> {
139
120
  mapExprs(&mut body, &mut |e| match e {
140
121
  Expr::FnCall(call) if call.name == fname => match argFor(call, c.param, &c.v) {
141
122
  Some(a) if deepFresh(a) => {}
142
- Some(Expr::Var(t)) if inSelf && inner.contains(t) && countVar(&selfBody, t) == 1 => {}
123
+ Some(Expr::Var(t)) if inSelf && inner.contains(t) && crate::visit::countVar(&selfBody, t) == 1 => {}
143
124
  _ => ok[ci] = false,
144
125
  },
145
126
  Expr::Var(n) if *n == fname => ok[ci] = false,
plum-core/src/simplify.rs CHANGED
@@ -5,6 +5,8 @@
5
5
  //! Plum has no `match` expression: a match is a statement, so the match rewrites work on
6
6
  //! blocks (`simplifyBlock`); `simplifyExpr` covers the expression-level rewrites.
7
7
  use crate::ast::*;
8
+ use crate::rename::{renameStmts, Name};
9
+ use crate::visit::{anyExpr, childExprs, childRefs, stmtBlockRefs, stmtBlocks, stmtExprRefs, stmtExprs, visitBlock};
8
10
  use std::collections::{HashMap, HashSet};
9
11
  use std::sync::atomic::{AtomicUsize, Ordering};
10
12
 
@@ -73,7 +75,7 @@ struct Cx<'a> {
73
75
  }
74
76
 
75
77
  /// Pattern casing rule: a Capitalised name is always a tag, a lowercase name always binds.
76
- fn isTag(n: &str) -> bool {
78
+ pub(crate) fn isTag(n: &str) -> bool {
77
79
  n.chars().next().is_some_and(|c| c.is_uppercase())
78
80
  }
79
81
 
@@ -674,374 +676,6 @@ impl<'a> Cx<'a> {
674
676
  }
675
677
  }
676
678
 
677
- fn visitBlock(b: &mut Block, f: &mut dyn FnMut(&mut Expr)) {
678
- for s in &mut b.stmts {
679
- for e in stmtExprs(s) {
680
- visitExpr(e, f);
681
- }
682
- for blk in stmtBlocks(s) {
683
- visitBlock(blk, f);
684
- }
685
- }
686
- }
687
-
688
- /// Post-order over `e`, into closure bodies and catch blocks too.
689
- fn visitExpr(e: &mut Expr, f: &mut dyn FnMut(&mut Expr)) {
690
- for c in childExprs(e) {
691
- visitExpr(c, f);
692
- }
693
- match e {
694
- Expr::Closure(c) => visitBlock(&mut c.body, f),
695
- Expr::Catch(c) => {
696
- if let CatchHandler::Block { body, .. } = &mut c.handler {
697
- visitBlock(body, f);
698
- }
699
- }
700
- _ => {}
701
- }
702
- f(e);
703
- }
704
-
705
- fn argExpr(a: &mut Arg) -> &mut Expr {
706
- match a {
707
- Arg::Positional(e) | Arg::Keyword { value: e, .. } => e,
708
- }
709
- }
710
-
711
- fn childExprs(e: &mut Expr) -> Vec<&mut Expr> {
712
- match e {
713
- Expr::Binary(b) => vec![&mut b.left, &mut b.right],
714
- Expr::Unary(u) => vec![&mut u.operand],
715
- Expr::Bool(b) => vec![&mut b.left, &mut b.right],
716
- Expr::Not(x) | Expr::Try(x) | Expr::Paren(x) => vec![x.as_mut()],
717
- Expr::Bang(b) => vec![&mut b.value],
718
- Expr::Bind(b) => {
719
- let mut v = vec![&mut b.value];
720
- v.extend(b.case.guard.iter_mut());
721
- v
722
- }
723
- Expr::Catch(c) => {
724
- let CatchExpr { value, handler, .. } = &mut **c;
725
- match handler {
726
- CatchHandler::Default(d) => vec![value, d],
727
- CatchHandler::Block { .. } => vec![value],
728
- }
729
- }
730
- Expr::Compare(c) => vec![&mut c.left, &mut c.right],
731
- Expr::InlineIf(t) => vec![&mut t.condition, &mut t.then, &mut t.else_],
732
- Expr::FnCall(c) => c.args.iter_mut().map(argExpr).collect(),
733
- Expr::Yield(vs) => vs.iter_mut().collect(),
734
- Expr::ClassCall(c) => {
735
- let mut v: Vec<&mut Expr> = c.fields.iter_mut().map(|f| &mut f.value).collect();
736
- if let Some(s) = &mut c.spread {
737
- v.push(s.as_mut());
738
- }
739
- v
740
- }
741
- Expr::Attribute(a) => {
742
- let AttributeExpr { object, attr } = &mut **a;
743
- let mut v = vec![object];
744
- if let AttrKind::Method(c) = attr {
745
- v.extend(c.args.iter_mut().map(argExpr));
746
- }
747
- v
748
- }
749
- Expr::String(s) => s
750
- .parts
751
- .iter_mut()
752
- .filter_map(|p| match p {
753
- StringPart::Interp(e) => Some(e),
754
- StringPart::Text(_) => None,
755
- })
756
- .collect(),
757
- Expr::Closure(_)
758
- | Expr::Int(_)
759
- | Expr::Float(_)
760
- | Expr::BoolLit(_)
761
- | Expr::Self_
762
- | Expr::ErrorValue(_)
763
- | Expr::Var(_)
764
- | Expr::TypeName(_) => vec![],
765
- }
766
- }
767
-
768
- fn stmtExprs(s: &mut Stmt) -> Vec<&mut Expr> {
769
- match s {
770
- Stmt::Assign(a) => {
771
- let mut v: Vec<&mut Expr> = Vec::new();
772
- for t in &mut a.targets {
773
- if let AssignTarget::Field(obj, _) = t {
774
- v.push(obj.as_mut());
775
- }
776
- }
777
- v.extend(a.values.iter_mut());
778
- v
779
- }
780
- Stmt::Assert(c) => vec![&mut c.cond],
781
- Stmt::Crash(e) | Stmt::Expr(e, ..) => vec![e],
782
- Stmt::Return(e) => e.iter_mut().collect(),
783
- Stmt::ForLoop(f) => {
784
- let mut v: Vec<&mut Expr> = vec![&mut f.source];
785
- for c in &mut f.clauses {
786
- match c {
787
- ForClause::If(e) | ForClause::Bind(_, e) => v.push(e),
788
- }
789
- }
790
- v
791
- }
792
- Stmt::While(w) => vec![&mut w.condition],
793
- Stmt::If(i) => {
794
- let mut v = vec![&mut i.condition];
795
- v.extend(i.else_ifs.iter_mut().map(|e| &mut e.condition));
796
- v
797
- }
798
- Stmt::Match(m) => {
799
- let mut v: Vec<&mut Expr> = m.subjects.iter_mut().collect();
800
- v.extend(m.cases.iter_mut().filter_map(|c| c.guard.as_mut()));
801
- v
802
- }
803
- Stmt::Break | Stmt::Continue | Stmt::Todo => vec![],
804
- }
805
- }
806
-
807
- fn stmtBlocks(s: &mut Stmt) -> Vec<&mut Block> {
808
- match s {
809
- Stmt::ForLoop(f) => vec![&mut f.body],
810
- Stmt::While(w) => vec![&mut w.body],
811
- Stmt::If(i) => {
812
- let mut v = vec![&mut i.body];
813
- v.extend(i.else_ifs.iter_mut().map(|e| &mut e.body));
814
- v.extend(i.else_.iter_mut());
815
- v
816
- }
817
- Stmt::Match(m) => m.cases.iter_mut().map(|c| &mut c.body).collect(),
818
- _ => vec![],
819
- }
820
- }
821
-
822
- #[derive(Clone, Copy)]
823
- enum Name {
824
- /// A declaration site: `x := v`, a loop binder, a pattern name, a closure param.
825
- Decl,
826
- /// A name in a pattern (a constructor name is not a binder).
827
- Pat,
828
- /// The target of a plain `x = v`.
829
- Plain,
830
- /// A use.
831
- Ref,
832
- }
833
-
834
- fn renameStmts(stmts: &mut [Stmt], rn: &mut dyn FnMut(&mut String, Name)) {
835
- for s in stmts {
836
- match s {
837
- Stmt::Assign(a) => {
838
- let kind = if a.declare { Name::Decl } else { Name::Plain };
839
- for t in &mut a.targets {
840
- if let AssignTarget::Var(n) = t {
841
- rn(n, kind);
842
- }
843
- }
844
- }
845
- Stmt::ForLoop(f) => {
846
- f.binders.iter_mut().for_each(|v| rn(v, Name::Decl));
847
- for c in &mut f.clauses {
848
- if let ForClause::Bind(n, _) = c {
849
- rn(n, Name::Decl);
850
- }
851
- }
852
- if let Some((n, _)) = &mut f.result {
853
- rn(n, Name::Decl);
854
- }
855
- }
856
- Stmt::Match(m) => {
857
- for c in &mut m.cases {
858
- for p in &mut c.patterns {
859
- renamePattern(p, rn);
860
- }
861
- }
862
- }
863
- _ => {}
864
- }
865
- for e in stmtExprs(s) {
866
- renameExpr(e, rn);
867
- }
868
- for b in stmtBlocks(s) {
869
- renameStmts(&mut b.stmts, rn);
870
- }
871
- }
872
- }
873
-
874
- fn renamePattern(p: &mut CasePattern, rn: &mut dyn FnMut(&mut String, Name)) {
875
- match p {
876
- CasePattern::Name(n) => rn(n, Name::Pat),
877
- CasePattern::Class { fields, .. } => fields.iter_mut().for_each(|f| renamePattern(f, rn)),
878
- _ => {}
879
- }
880
- }
881
-
882
- fn renameExpr(e: &mut Expr, rn: &mut dyn FnMut(&mut String, Name)) {
883
- match e {
884
- Expr::Var(n) => rn(n, Name::Ref),
885
- Expr::Bind(b) => renamePattern(&mut b.case.patterns[0], rn),
886
- Expr::Closure(c) => {
887
- c.params.iter_mut().for_each(|p| rn(p, Name::Decl));
888
- renameStmts(&mut c.body.stmts, rn);
889
- }
890
- Expr::Catch(c) => {
891
- if let CatchHandler::Block { binder, body } = &mut c.handler {
892
- rn(binder, Name::Decl);
893
- renameStmts(&mut body.stmts, rn);
894
- }
895
- }
896
- _ => {}
897
- }
898
- for ch in childExprs(e) {
899
- renameExpr(ch, rn);
900
- }
901
- }
902
-
903
- // ---------- shared traversal ----------
904
-
905
- fn walkBlockRef(b: &Block, f: &mut dyn FnMut(&Expr)) {
906
- for s in &b.stmts {
907
- for e in stmtExprRefs(s) {
908
- walkRef(e, f);
909
- }
910
- for blk in stmtBlockRefs(s) {
911
- walkBlockRef(blk, f);
912
- }
913
- }
914
- }
915
-
916
- fn walkRef(e: &Expr, f: &mut dyn FnMut(&Expr)) {
917
- f(e);
918
- for c in childRefs(e) {
919
- walkRef(c, f);
920
- }
921
- match e {
922
- Expr::Closure(c) => walkBlockRef(&c.body, f),
923
- Expr::Catch(c) => {
924
- if let CatchHandler::Block { body, .. } = &c.handler {
925
- walkBlockRef(body, f);
926
- }
927
- }
928
- _ => {}
929
- }
930
- }
931
-
932
- fn argRef(a: &Arg) -> &Expr {
933
- match a {
934
- Arg::Positional(e) | Arg::Keyword { value: e, .. } => e,
935
- }
936
- }
937
-
938
- fn childRefs(e: &Expr) -> Vec<&Expr> {
939
- match e {
940
- Expr::Binary(b) => vec![&b.left, &b.right],
941
- Expr::Unary(u) => vec![&u.operand],
942
- Expr::Bool(b) => vec![&b.left, &b.right],
943
- Expr::Not(x) | Expr::Try(x) | Expr::Paren(x) => vec![x.as_ref()],
944
- Expr::Bang(b) => vec![&b.value],
945
- Expr::Bind(b) => {
946
- let mut v = vec![&b.value];
947
- v.extend(b.case.guard.iter());
948
- v
949
- }
950
- Expr::Catch(c) => match &c.handler {
951
- CatchHandler::Default(d) => vec![&c.value, d],
952
- CatchHandler::Block { .. } => vec![&c.value],
953
- },
954
- Expr::Compare(c) => vec![&c.left, &c.right],
955
- Expr::InlineIf(t) => vec![&t.condition, &t.then, &t.else_],
956
- Expr::FnCall(c) => c.args.iter().map(argRef).collect(),
957
- Expr::Yield(vs) => vs.iter().collect(),
958
- Expr::ClassCall(c) => {
959
- let mut v: Vec<&Expr> = c.fields.iter().map(|f| &f.value).collect();
960
- if let Some(s) = &c.spread {
961
- v.push(s.as_ref());
962
- }
963
- v
964
- }
965
- Expr::Attribute(a) => {
966
- let mut v = vec![&a.object];
967
- if let AttrKind::Method(c) = &a.attr {
968
- v.extend(c.args.iter().map(argRef));
969
- }
970
- v
971
- }
972
- Expr::String(s) => s
973
- .parts
974
- .iter()
975
- .filter_map(|p| match p {
976
- StringPart::Interp(e) => Some(e),
977
- StringPart::Text(_) => None,
978
- })
979
- .collect(),
980
- Expr::Closure(_)
981
- | Expr::Int(_)
982
- | Expr::Float(_)
983
- | Expr::BoolLit(_)
984
- | Expr::Self_
985
- | Expr::ErrorValue(_)
986
- | Expr::Var(_)
987
- | Expr::TypeName(_) => vec![],
988
- }
989
- }
990
-
991
- fn stmtExprRefs(s: &Stmt) -> Vec<&Expr> {
992
- match s {
993
- Stmt::Assign(a) => {
994
- let mut v: Vec<&Expr> = Vec::new();
995
- for t in &a.targets {
996
- if let AssignTarget::Field(obj, _) = t {
997
- v.push(obj.as_ref());
998
- }
999
- }
1000
- v.extend(a.values.iter());
1001
- v
1002
- }
1003
- Stmt::Assert(c) => vec![&c.cond],
1004
- Stmt::Crash(e) | Stmt::Expr(e, ..) => vec![e],
1005
- Stmt::Return(e) => e.iter().collect(),
1006
- Stmt::ForLoop(f) => {
1007
- let mut v: Vec<&Expr> = vec![&f.source];
1008
- for c in &f.clauses {
1009
- match c {
1010
- ForClause::If(e) | ForClause::Bind(_, e) => v.push(e),
1011
- }
1012
- }
1013
- v
1014
- }
1015
- Stmt::While(w) => vec![&w.condition],
1016
- Stmt::If(i) => {
1017
- let mut v = vec![&i.condition];
1018
- v.extend(i.else_ifs.iter().map(|e| &e.condition));
1019
- v
1020
- }
1021
- Stmt::Match(m) => {
1022
- let mut v: Vec<&Expr> = m.subjects.iter().collect();
1023
- v.extend(m.cases.iter().filter_map(|c| c.guard.as_ref()));
1024
- v
1025
- }
1026
- Stmt::Break | Stmt::Continue | Stmt::Todo => vec![],
1027
- }
1028
- }
1029
-
1030
- fn stmtBlockRefs(s: &Stmt) -> Vec<&Block> {
1031
- match s {
1032
- Stmt::ForLoop(f) => vec![&f.body],
1033
- Stmt::While(w) => vec![&w.body],
1034
- Stmt::If(i) => {
1035
- let mut v = vec![&i.body];
1036
- v.extend(i.else_ifs.iter().map(|e| &e.body));
1037
- v.extend(i.else_.iter());
1038
- v
1039
- }
1040
- Stmt::Match(m) => m.cases.iter().map(|c| &c.body).collect(),
1041
- _ => vec![],
1042
- }
1043
- }
1044
-
1045
679
 
1046
680
  /// Scope-aware renamer: a declaration renames only the uses in its own scope (the rest of its
1047
681
  /// block and nested blocks/closures); uses before it, or in sibling scopes, keep the outer name.
@@ -1186,3 +820,30 @@ impl Scoper {
1186
820
  }
1187
821
  }
1188
822
  }
823
+
824
+ /// Pre-order callback over every expression of `b`, closure bodies and catch blocks included.
825
+ pub(crate) fn walkBlockRef(b: &Block, f: &mut dyn FnMut(&Expr)) {
826
+ for s in &b.stmts {
827
+ for e in stmtExprRefs(s) {
828
+ walkRef(e, f);
829
+ }
830
+ for blk in stmtBlockRefs(s) {
831
+ walkBlockRef(blk, f);
832
+ }
833
+ }
834
+ }
835
+
836
+ /// Pre-order callback over `e` and everything under it.
837
+ pub(crate) fn walkRef(e: &Expr, f: &mut dyn FnMut(&Expr)) {
838
+ anyExpr(e, &mut |x| {
839
+ f(x);
840
+ false
841
+ });
842
+ }
843
+
844
+ /// The expression of a call argument (visit.rs keeps its own copy private).
845
+ pub(crate) fn argRef(a: &Arg) -> &Expr {
846
+ match a {
847
+ Arg::Positional(e) | Arg::Keyword { value: e, .. } => e,
848
+ }
849
+ }
plum-core/src/tail.rs CHANGED
@@ -195,26 +195,12 @@ fn expr(cx: &mut Ctx, e: &mut Expr, tail: bool) {
195
195
  expr(cx, &mut i.then, tail);
196
196
  expr(cx, &mut i.else_, tail);
197
197
  }
198
- Expr::Binary(b) => {
198
+ Expr::Catch(_) => {
199
- expr(cx, &mut b.left, false);
199
+ for ch in crate::visit::childExprs(e) {
200
- expr(cx, &mut b.right, false);
200
+ expr(cx, ch, false);
201
- }
201
+ }
202
- Expr::Unary(u) => expr(cx, &mut u.operand, false),
203
- Expr::Bool(b) => {
204
- expr(cx, &mut b.left, false);
205
- expr(cx, &mut b.right, false);
206
- }
207
- Expr::Compare(c) => {
208
- expr(cx, &mut c.left, false);
209
- expr(cx, &mut c.right, false);
210
- }
211
- Expr::Not(x) | Expr::Try(x) => expr(cx, x, false),
212
- Expr::Bang(b) => expr(cx, &mut b.value, false),
213
- Expr::Catch(c) => {
202
+ if let Expr::Catch(c) = e {
214
- expr(cx, &mut c.value, false);
215
- match &mut c.handler {
216
- CatchHandler::Default(d) => expr(cx, d, false),
217
- CatchHandler::Block { binder, body } => {
203
+ if let CatchHandler::Block { binder, body } = &mut c.handler {
218
204
  cx.locals.insert(binder.clone());
219
205
  block(cx, body, false, false);
220
206
  }
@@ -222,25 +208,8 @@ fn expr(cx: &mut Ctx, e: &mut Expr, tail: bool) {
222
208
  }
223
209
  Expr::Bind(b) => {
224
210
  pattern(cx, &b.case.patterns[0]);
225
- expr(cx, &mut b.value, false);
211
+ for ch in crate::visit::childExprs(e) {
226
- if let Some(g) = &mut b.case.guard {
227
- expr(cx, g, false);
212
+ expr(cx, ch, false);
228
- }
229
- }
230
- Expr::Yield(v) => v.iter_mut().for_each(|x| expr(cx, x, false)),
231
- Expr::ClassCall(c) => {
232
- for f in &mut c.fields {
233
- expr(cx, &mut f.value, false);
234
- }
235
- if let Some(s) = &mut c.spread {
236
- expr(cx, s, false);
237
- }
238
- }
239
- Expr::String(s) => {
240
- for p in &mut s.parts {
241
- if let StringPart::Interp(x) = p {
242
- expr(cx, x, false);
243
- }
244
213
  }
245
214
  }
246
215
  Expr::Closure(c) => {
@@ -248,13 +217,11 @@ fn expr(cx: &mut Ctx, e: &mut Expr, tail: bool) {
248
217
  cx.locals.extend(c.params.iter().cloned());
249
218
  block(cx, &mut c.body, true, true);
250
219
  }
220
+ _ => {
221
+ for ch in crate::visit::childExprs(e) {
251
- Expr::ErrorValue(_)
222
+ expr(cx, ch, false);
252
- | Expr::Int(_)
253
- | Expr::Float(_)
254
- | Expr::BoolLit(_)
255
- | Expr::Self_
223
+ }
256
- | Expr::Var(_)
257
- | Expr::TypeName(_) => {}
224
+ }
258
225
  }
259
226
  }
260
227
 
plum-core/src/trmc.rs CHANGED
@@ -26,6 +26,9 @@
26
26
  //! `try`/`!`/`catch`, closures or `yield`, no other `return`s outside tail
27
27
  //! position, no variadic params.
28
28
  use crate::ast::*;
29
+ use crate::generics::isGenericName;
30
+ use crate::rename::substType;
31
+ use std::collections::HashMap;
29
32
 
30
33
  /// Rewrites every eligible fn of `src`; returns their names.
31
34
  pub fn rewrite(src: &mut Source) -> Vec<String> {
@@ -75,21 +78,13 @@ impl Cx<'_> {
75
78
  if owner.generics.len() != self.ret.generics.len() {
76
79
  return None;
77
80
  }
78
- let map: Vec<(&str, &Type)> =
81
+ let map: HashMap<String, Type> =
79
- owner.generics.iter().map(|g| g.name.as_str()).zip(self.ret.generics.iter()).collect();
82
+ owner.generics.iter().map(|g| g.name.clone()).zip(self.ret.generics.iter().cloned()).collect();
80
- fn subst(t: &Type, map: &[(&str, &Type)]) -> Type {
81
- if t.generics.is_empty() {
82
- if let Some((_, r)) = map.iter().find(|(n, _)| *n == t.name) {
83
- return (*r).clone();
84
- }
85
- }
86
- Type { name: t.name.clone(), generics: t.generics.iter().map(|g| subst(g, map)).collect() }
87
- }
88
83
  fn open(t: &Type) -> bool {
89
- (t.name.len() == 1 && t.name.chars().all(|c| c.is_ascii_uppercase())) || t.generics.iter().any(open)
84
+ isGenericName(&t.name) || t.generics.iter().any(open)
90
85
  }
91
86
  // Only a concrete type helps (and a generic fn's own `B` would not re-check).
92
- let t = subst(declared, &map);
87
+ let t = substType(declared, &map);
93
88
  if open(&t) {
94
89
  return None;
95
90
  }
@@ -124,8 +119,7 @@ fn rewriteFn(f: &Fn, enums: &[Enum]) -> Option<Fn> {
124
119
  // `= if .. then .. else ..` bodies are expressions; handled below.
125
120
  return rewriteExprBody(f, enums, ret, nullary);
126
121
  };
127
- let dbg = format!("{:?}", body);
128
- if bannedText(&dbg) {
122
+ if bannedBlock(body) {
129
123
  return None;
130
124
  }
131
125
  let mut cx = Cx { f, enums, ret, cons: None, nullary, wrapper: None, consGenerics: vec![], counter: 0, found: false };
@@ -185,7 +179,7 @@ fn substSelf(body: &mut FnBody) -> Option<()> {
185
179
 
186
180
  fn rewriteExprBody(f: &Fn, enums: &[Enum], ret: Type, nullary: String) -> Option<Fn> {
187
181
  let FnBody::Expr(e) = &f.body else { return None };
188
- if bannedText(&format!("{:?}", e)) {
182
+ if bannedExpr(e) {
189
183
  return None;
190
184
  }
191
185
  let mut cx = Cx { f, enums, ret, cons: None, nullary, wrapper: None, consGenerics: vec![], counter: 0, found: false };
@@ -200,10 +194,21 @@ fn rewriteExprBody(f: &Fn, enums: &[Enum], ret: Type, nullary: String) -> Option
200
194
  cx.finish(Block { stmts })
201
195
  }
202
196
 
197
+ /// Constructs the rewrite does not handle: `?`/`!`/catch, `yield`, closures, `return`, `todo`, `crash`.
198
+ fn bannedExpr(e: &Expr) -> bool {
199
+ crate::visit::anyExpr(e, &mut |x| {
200
+ matches!(x, Expr::Try(_) | Expr::Bang(_) | Expr::Catch(_) | Expr::Yield(_) | Expr::Closure(_))
201
+ })
202
+ }
203
+
203
- fn bannedText(dbg: &str) -> bool {
204
+ fn bannedStmt(s: &Stmt) -> bool {
204
- ["Try(", "Bang(", "Catch(", "Yield(", "Closure(", "Return(", "Todo", "Crash("]
205
+ matches!(s, Stmt::Return(_) | Stmt::Todo | Stmt::Crash(_))
205
- .iter()
206
+ || crate::visit::stmtExprRefs(s).into_iter().any(bannedExpr)
207
+ || crate::visit::stmtBlockRefs(s).into_iter().any(bannedBlock)
208
+ }
209
+
210
+ fn bannedBlock(b: &Block) -> bool {
206
- .any(|b| dbg.contains(b))
211
+ b.stmts.iter().any(bannedStmt)
207
212
  }
208
213
 
209
214
  fn wrap(w: &Option<String>, e: Expr) -> Expr {
@@ -251,8 +256,13 @@ fn pure(e: &Expr) -> bool {
251
256
  }
252
257
  }
253
258
 
259
+ /// Whether `e` calls a fn or method called `name` (a field or keyword named `name` does not count).
254
260
  fn mentions(e: &Expr, name: &str) -> bool {
261
+ crate::visit::anyExpr(e, &mut |x| match x {
262
+ Expr::FnCall(c) => c.name == name,
255
- format!("{:?}", e).contains(&format!("name: {:?}", name))
263
+ Expr::Attribute(a) => matches!(&a.attr, AttrKind::Method(c) if c.name == name),
264
+ _ => false,
265
+ })
256
266
  }
257
267
 
258
268
  impl<'a> Cx<'a> {
plum-core/src/union_lower.rs CHANGED
@@ -3,6 +3,7 @@
3
3
  //! synthesizes dispatch methods on enums. Post-order (children first), so every
4
4
  //! recorded address is still valid when its node is visited.
5
5
  use crate::ast::*;
6
+ use crate::rename::substType;
6
7
  use crate::unions::UnionPlan;
7
8
  use std::collections::{HashMap, HashSet};
8
9
 
@@ -595,18 +596,6 @@ fn rnExpr(e: &mut Expr, from: &str, to: &str) {
595
596
 
596
597
  // ---- dispatch method synthesis ---------------------------------------------
597
598
 
598
- fn substType(t: &Type, map: &HashMap<String, Type>) -> Type {
599
- if t.generics.is_empty() {
600
- if let Some(r) = map.get(&t.name) {
601
- return r.clone();
602
- }
603
- }
604
- Type {
605
- name: t.name.clone(),
606
- generics: t.generics.iter().map(|g| substType(g, map)).collect(),
607
- }
608
- }
609
-
610
599
  fn substParam(p: &ParamType, map: &HashMap<String, Type>) -> ParamType {
611
600
  match p {
612
601
  ParamType::Type(t) => ParamType::Type(substType(t, map)),
plum-core/src/uniqueness.rs CHANGED
@@ -374,20 +374,13 @@ struct Site {
374
374
  inClosure: bool,
375
375
  }
376
376
 
377
- fn stripParen(e: &Expr) -> &Expr {
378
- match e {
379
- Expr::Paren(x) => stripParen(x),
380
- _ => e,
381
- }
382
- }
383
-
384
377
  fn isVar(e: &Expr, v: &str) -> bool {
385
378
  varName(e) == Some(v)
386
379
  }
387
380
 
388
381
  /// The name of a plain variable use; `self` counts as the variable "self".
389
382
  fn varName(e: &Expr) -> Option<&str> {
390
- match stripParen(e) {
383
+ match crate::visit::unparen(e) {
391
384
  Expr::Var(n) => Some(n.as_str()),
392
385
  Expr::Self_ => Some("self"),
393
386
  _ => None,
@@ -445,14 +438,6 @@ fn collectTails<'a>(b: &'a Block, out: &mut Vec<&'a Expr>) -> bool {
445
438
  }
446
439
  }
447
440
 
448
- fn patternNames(p: &CasePattern, out: &mut Vec<String>) {
449
- match p {
450
- CasePattern::Name(n) => out.push(n.clone()),
451
- CasePattern::Class { fields, .. } => fields.iter().for_each(|f| patternNames(f, out)),
452
- _ => {}
453
- }
454
- }
455
-
456
441
  impl An {
457
442
  fn isFree(&self, name: &str) -> Option<usize> {
458
443
  self.freeIdx.get(name).copied()
@@ -566,7 +551,7 @@ impl An {
566
551
  if site.inClosure {
567
552
  return Err("argument inside a closure".to_string());
568
553
  }
569
- match stripParen(e) {
554
+ match crate::visit::unparen(e) {
570
555
  Expr::Var(_) | Expr::Self_ => self.varOk(site.caller, varName(e).unwrap()),
571
556
  Expr::Attribute(a) => {
572
557
  let AttrKind::Field(f) = &a.attr else {
@@ -632,7 +617,7 @@ impl<'a> Scan<'a> {
632
617
 
633
618
  fn checkPatterns(&mut self, ps: &[CasePattern]) {
634
619
  let mut names = vec![];
635
- ps.iter().for_each(|p| patternNames(p, &mut names));
620
+ ps.iter().for_each(|p| crate::visit::patternBinders(p, &mut names));
636
621
  if names.iter().any(|n| n == self.v) {
637
622
  self.fail(format!("{} rebound by a pattern", self.v));
638
623
  }
@@ -1108,7 +1093,7 @@ fn collectFacts(an: &mut An) {
1108
1093
  walkBlock(&u.block, &mut |n| {
1109
1094
  match n {
1110
1095
  Node::S(Stmt::Assign(asg)) if asg.targets.len() == 1 && asg.values.len() == 1 => {
1111
- match stripParen(&asg.values[0]) {
1096
+ match crate::visit::unparen(&asg.values[0]) {
1112
1097
  Expr::FnCall(c) => {
1113
1098
  abMap.insert(c as *const FnCall, asg.targets[0].clone());
1114
1099
  }
@@ -1179,7 +1164,7 @@ fn collectFacts(an: &mut An) {
1179
1164
  allowed.insert(&**o as *const Expr);
1180
1165
  }
1181
1166
  if single {
1182
- let call: Option<(&FnCall, Option<&Expr>)> = match stripParen(&asg.values[0]) {
1167
+ let call: Option<(&FnCall, Option<&Expr>)> = match crate::visit::unparen(&asg.values[0]) {
1183
1168
  Expr::FnCall(c) => Some((c, None)),
1184
1169
  Expr::Attribute(x) => match &x.attr {
1185
1170
  AttrKind::Method(c) => Some((c, Some(&x.object))),
plum-core/src/visit.rs ADDED
@@ -0,0 +1,336 @@
1
+ //! Shared AST traversal: child accessors, post-order visitors and small queries used by
2
+ //! the lowering and optimisation passes. Closure bodies and catch blocks are entered by
3
+ //! `visitBlock`/`visitExpr`/`anyExpr`/`countVar`, not by `childExprs`/`childRefs`.
4
+ use crate::ast::*;
5
+
6
+ pub fn unparen(e: &Expr) -> &Expr {
7
+ match e {
8
+ Expr::Paren(i) => unparen(i),
9
+ _ => e,
10
+ }
11
+ }
12
+
13
+ pub fn visitBlock(b: &mut Block, f: &mut dyn FnMut(&mut Expr)) {
14
+ for s in &mut b.stmts {
15
+ for e in stmtExprs(s) {
16
+ visitExpr(e, f);
17
+ }
18
+ for blk in stmtBlocks(s) {
19
+ visitBlock(blk, f);
20
+ }
21
+ }
22
+ }
23
+
24
+ /// Post-order over `e`, into closure bodies and catch blocks too.
25
+ pub fn visitExpr(e: &mut Expr, f: &mut dyn FnMut(&mut Expr)) {
26
+ for c in childExprs(e) {
27
+ visitExpr(c, f);
28
+ }
29
+ match e {
30
+ Expr::Closure(c) => visitBlock(&mut c.body, f),
31
+ Expr::Catch(c) => {
32
+ if let CatchHandler::Block { body, .. } = &mut c.handler {
33
+ visitBlock(body, f);
34
+ }
35
+ }
36
+ _ => {}
37
+ }
38
+ f(e);
39
+ }
40
+
41
+ fn argExpr(a: &mut Arg) -> &mut Expr {
42
+ match a {
43
+ Arg::Positional(e) | Arg::Keyword { value: e, .. } => e,
44
+ }
45
+ }
46
+
47
+ pub fn childExprs(e: &mut Expr) -> Vec<&mut Expr> {
48
+ match e {
49
+ Expr::Binary(b) => vec![&mut b.left, &mut b.right],
50
+ Expr::Unary(u) => vec![&mut u.operand],
51
+ Expr::Bool(b) => vec![&mut b.left, &mut b.right],
52
+ Expr::Not(x) | Expr::Try(x) | Expr::Paren(x) => vec![x.as_mut()],
53
+ Expr::Bang(b) => vec![&mut b.value],
54
+ Expr::Bind(b) => {
55
+ let mut v = vec![&mut b.value];
56
+ v.extend(b.case.guard.iter_mut());
57
+ v
58
+ }
59
+ Expr::Catch(c) => {
60
+ let CatchExpr { value, handler, .. } = &mut **c;
61
+ match handler {
62
+ CatchHandler::Default(d) => vec![value, d],
63
+ CatchHandler::Block { .. } => vec![value],
64
+ }
65
+ }
66
+ Expr::Compare(c) => vec![&mut c.left, &mut c.right],
67
+ Expr::InlineIf(t) => vec![&mut t.condition, &mut t.then, &mut t.else_],
68
+ Expr::FnCall(c) => c.args.iter_mut().map(argExpr).collect(),
69
+ Expr::Yield(vs) => vs.iter_mut().collect(),
70
+ Expr::ClassCall(c) => {
71
+ let mut v: Vec<&mut Expr> = c.fields.iter_mut().map(|f| &mut f.value).collect();
72
+ if let Some(s) = &mut c.spread {
73
+ v.push(s.as_mut());
74
+ }
75
+ v
76
+ }
77
+ Expr::Attribute(a) => {
78
+ let AttributeExpr { object, attr } = &mut **a;
79
+ let mut v = vec![object];
80
+ if let AttrKind::Method(c) = attr {
81
+ v.extend(c.args.iter_mut().map(argExpr));
82
+ }
83
+ v
84
+ }
85
+ Expr::String(s) => s
86
+ .parts
87
+ .iter_mut()
88
+ .filter_map(|p| match p {
89
+ StringPart::Interp(e) => Some(e),
90
+ StringPart::Text(_) => None,
91
+ })
92
+ .collect(),
93
+ Expr::Closure(_)
94
+ | Expr::Int(_)
95
+ | Expr::Float(_)
96
+ | Expr::BoolLit(_)
97
+ | Expr::Self_
98
+ | Expr::ErrorValue(_)
99
+ | Expr::Var(_)
100
+ | Expr::TypeName(_) => vec![],
101
+ }
102
+ }
103
+
104
+ pub fn stmtExprs(s: &mut Stmt) -> Vec<&mut Expr> {
105
+ match s {
106
+ Stmt::Assign(a) => {
107
+ let mut v: Vec<&mut Expr> = Vec::new();
108
+ for t in &mut a.targets {
109
+ if let AssignTarget::Field(obj, _) = t {
110
+ v.push(obj.as_mut());
111
+ }
112
+ }
113
+ v.extend(a.values.iter_mut());
114
+ v
115
+ }
116
+ Stmt::Assert(c) => vec![&mut c.cond],
117
+ Stmt::Crash(e) | Stmt::Expr(e, ..) => vec![e],
118
+ Stmt::Return(e) => e.iter_mut().collect(),
119
+ Stmt::ForLoop(f) => {
120
+ let mut v: Vec<&mut Expr> = vec![&mut f.source];
121
+ for c in &mut f.clauses {
122
+ match c {
123
+ ForClause::If(e) | ForClause::Bind(_, e) => v.push(e),
124
+ }
125
+ }
126
+ v
127
+ }
128
+ Stmt::While(w) => vec![&mut w.condition],
129
+ Stmt::If(i) => {
130
+ let mut v = vec![&mut i.condition];
131
+ v.extend(i.else_ifs.iter_mut().map(|e| &mut e.condition));
132
+ v
133
+ }
134
+ Stmt::Match(m) => {
135
+ let mut v: Vec<&mut Expr> = m.subjects.iter_mut().collect();
136
+ v.extend(m.cases.iter_mut().filter_map(|c| c.guard.as_mut()));
137
+ v
138
+ }
139
+ Stmt::Break | Stmt::Continue | Stmt::Todo => vec![],
140
+ }
141
+ }
142
+
143
+ pub fn stmtBlocks(s: &mut Stmt) -> Vec<&mut Block> {
144
+ match s {
145
+ Stmt::ForLoop(f) => vec![&mut f.body],
146
+ Stmt::While(w) => vec![&mut w.body],
147
+ Stmt::If(i) => {
148
+ let mut v = vec![&mut i.body];
149
+ v.extend(i.else_ifs.iter_mut().map(|e| &mut e.body));
150
+ v.extend(i.else_.iter_mut());
151
+ v
152
+ }
153
+ Stmt::Match(m) => m.cases.iter_mut().map(|c| &mut c.body).collect(),
154
+ _ => vec![],
155
+ }
156
+ }
157
+
158
+ fn argRef(a: &Arg) -> &Expr {
159
+ match a {
160
+ Arg::Positional(e) | Arg::Keyword { value: e, .. } => e,
161
+ }
162
+ }
163
+
164
+ pub fn childRefs(e: &Expr) -> Vec<&Expr> {
165
+ match e {
166
+ Expr::Binary(b) => vec![&b.left, &b.right],
167
+ Expr::Unary(u) => vec![&u.operand],
168
+ Expr::Bool(b) => vec![&b.left, &b.right],
169
+ Expr::Not(x) | Expr::Try(x) | Expr::Paren(x) => vec![x.as_ref()],
170
+ Expr::Bang(b) => vec![&b.value],
171
+ Expr::Bind(b) => {
172
+ let mut v = vec![&b.value];
173
+ v.extend(b.case.guard.iter());
174
+ v
175
+ }
176
+ Expr::Catch(c) => match &c.handler {
177
+ CatchHandler::Default(d) => vec![&c.value, d],
178
+ CatchHandler::Block { .. } => vec![&c.value],
179
+ },
180
+ Expr::Compare(c) => vec![&c.left, &c.right],
181
+ Expr::InlineIf(t) => vec![&t.condition, &t.then, &t.else_],
182
+ Expr::FnCall(c) => c.args.iter().map(argRef).collect(),
183
+ Expr::Yield(vs) => vs.iter().collect(),
184
+ Expr::ClassCall(c) => {
185
+ let mut v: Vec<&Expr> = c.fields.iter().map(|f| &f.value).collect();
186
+ if let Some(s) = &c.spread {
187
+ v.push(s.as_ref());
188
+ }
189
+ v
190
+ }
191
+ Expr::Attribute(a) => {
192
+ let mut v = vec![&a.object];
193
+ if let AttrKind::Method(c) = &a.attr {
194
+ v.extend(c.args.iter().map(argRef));
195
+ }
196
+ v
197
+ }
198
+ Expr::String(s) => s
199
+ .parts
200
+ .iter()
201
+ .filter_map(|p| match p {
202
+ StringPart::Interp(e) => Some(e),
203
+ StringPart::Text(_) => None,
204
+ })
205
+ .collect(),
206
+ Expr::Closure(_)
207
+ | Expr::Int(_)
208
+ | Expr::Float(_)
209
+ | Expr::BoolLit(_)
210
+ | Expr::Self_
211
+ | Expr::ErrorValue(_)
212
+ | Expr::Var(_)
213
+ | Expr::TypeName(_) => vec![],
214
+ }
215
+ }
216
+
217
+ pub fn stmtExprRefs(s: &Stmt) -> Vec<&Expr> {
218
+ match s {
219
+ Stmt::Assign(a) => {
220
+ let mut v: Vec<&Expr> = Vec::new();
221
+ for t in &a.targets {
222
+ if let AssignTarget::Field(obj, _) = t {
223
+ v.push(obj.as_ref());
224
+ }
225
+ }
226
+ v.extend(a.values.iter());
227
+ v
228
+ }
229
+ Stmt::Assert(c) => vec![&c.cond],
230
+ Stmt::Crash(e) | Stmt::Expr(e, ..) => vec![e],
231
+ Stmt::Return(e) => e.iter().collect(),
232
+ Stmt::ForLoop(f) => {
233
+ let mut v: Vec<&Expr> = vec![&f.source];
234
+ for c in &f.clauses {
235
+ match c {
236
+ ForClause::If(e) | ForClause::Bind(_, e) => v.push(e),
237
+ }
238
+ }
239
+ v
240
+ }
241
+ Stmt::While(w) => vec![&w.condition],
242
+ Stmt::If(i) => {
243
+ let mut v = vec![&i.condition];
244
+ v.extend(i.else_ifs.iter().map(|e| &e.condition));
245
+ v
246
+ }
247
+ Stmt::Match(m) => {
248
+ let mut v: Vec<&Expr> = m.subjects.iter().collect();
249
+ v.extend(m.cases.iter().filter_map(|c| c.guard.as_ref()));
250
+ v
251
+ }
252
+ Stmt::Break | Stmt::Continue | Stmt::Todo => vec![],
253
+ }
254
+ }
255
+
256
+ pub fn stmtBlockRefs(s: &Stmt) -> Vec<&Block> {
257
+ match s {
258
+ Stmt::ForLoop(f) => vec![&f.body],
259
+ Stmt::While(w) => vec![&w.body],
260
+ Stmt::If(i) => {
261
+ let mut v = vec![&i.body];
262
+ v.extend(i.else_ifs.iter().map(|e| &e.body));
263
+ v.extend(i.else_.iter());
264
+ v
265
+ }
266
+ Stmt::Match(m) => m.cases.iter().map(|c| &c.body).collect(),
267
+ _ => vec![],
268
+ }
269
+ }
270
+
271
+ fn anyBlock(b: &Block, pred: &mut dyn FnMut(&Expr) -> bool) -> bool {
272
+ for s in &b.stmts {
273
+ if stmtExprRefs(s).into_iter().any(|e| anyExpr(e, pred)) {
274
+ return true;
275
+ }
276
+ if stmtBlockRefs(s).into_iter().any(|blk| anyBlock(blk, pred)) {
277
+ return true;
278
+ }
279
+ }
280
+ false
281
+ }
282
+
283
+ /// Pre-order search of real AST nodes (closure bodies and catch blocks included).
284
+ pub fn anyExpr(e: &Expr, pred: &mut dyn FnMut(&Expr) -> bool) -> bool {
285
+ if pred(e) {
286
+ return true;
287
+ }
288
+ if childRefs(e).into_iter().any(|c| anyExpr(c, pred)) {
289
+ return true;
290
+ }
291
+ match e {
292
+ Expr::Closure(c) => anyBlock(&c.body, pred),
293
+ Expr::Catch(c) => match &c.handler {
294
+ CatchHandler::Block { body, .. } => anyBlock(body, pred),
295
+ _ => false,
296
+ },
297
+ _ => false,
298
+ }
299
+ }
300
+
301
+ /// Names a pattern binds. Capitalised names test a tag and constructor names are not binders.
302
+ pub fn patternBinders(p: &CasePattern, out: &mut Vec<String>) {
303
+ match p {
304
+ CasePattern::Name(n) => {
305
+ if n != "_" && !n.chars().next().is_some_and(|c| c.is_uppercase()) {
306
+ out.push(n.clone());
307
+ }
308
+ }
309
+ CasePattern::Class { fields, .. } => fields.iter().for_each(|f| patternBinders(f, out)),
310
+ _ => {}
311
+ }
312
+ }
313
+
314
+ /// Uses of `name` (as `Expr::Var`) in a fn body, closures and catch blocks included.
315
+ pub fn countVar(body: &FnBody, name: &str) -> usize {
316
+ fn inExpr(e: &Expr, name: &str) -> usize {
317
+ let mut n = 0;
318
+ anyExpr(e, &mut |x| {
319
+ n += usize::from(matches!(x, Expr::Var(v) if v == name));
320
+ false
321
+ });
322
+ n
323
+ }
324
+ match body {
325
+ FnBody::Expr(e) => inExpr(e, name),
326
+ FnBody::Block(b) => {
327
+ let mut n = 0;
328
+ anyBlock(b, &mut |x| {
329
+ n += usize::from(matches!(x, Expr::Var(v) if v == name));
330
+ false
331
+ });
332
+ n
333
+ }
334
+ FnBody::Extern => 0,
335
+ }
336
+ }
plum-core/tests/closure_spec_test.rs CHANGED
@@ -396,3 +396,26 @@ fn capture_reassigned_between_closure_and_call() {
396
396
  eprintln!("closure created before reassignment gives {heldResult} (9 = captured by value, 36 = by reference)");
397
397
  assert!(heldResult == 9 || heldResult == 36, "held closure gave {heldResult}");
398
398
  }
399
+
400
+ #[test]
401
+ fn pattern_decl() {
402
+ // A name bound by a pattern inside the closure is the closure's own binder, not a capture
403
+ // (here it shadows the outer `n`). Block-bodied closures are not inlined today, so this
404
+ // pins the program's result through whichever path the pass takes.
405
+ let src = withHead(
406
+ "fun main() -> Int =\n k := 10\n n := 1000\n xs := List(0, 1, 2)\n return xs.fold(0, |a, x|\n r := a\n match x\n 0 =>\n r = r + 100\n n =>\n r = r + n + k\n r\n )\n",
407
+ );
408
+ assert_eq!(runInt("patdecl", &src), 100 + 11 + 12);
409
+ }
410
+
411
+ #[test]
412
+ fn identical_closures_share_a_copy() {
413
+ // Same closure text at two positions: one copy (the dedup key ignores positions).
414
+ let src = withHead(
415
+ "fun main() -> Int =\n xs := List(1, 2, 3)\n a := xs.map(|x| x + 1).fold(0, |a, x| a + x)\n b := xs.map(|x| x + 1).fold(0, |a, x| a + x)\n return a + b\n",
416
+ );
417
+ let names = copies("dedup", &src);
418
+ assert!(names.contains(&"List.map$c0".to_string()), "{names:?}");
419
+ assert!(!names.contains(&"List.map$c1".to_string()), "{names:?}");
420
+ assert_eq!(runInt("dedup", &src), 18);
421
+ }
plum-core/tests/closure_types_parse_test.rs CHANGED
@@ -1,19 +1,8 @@
1
1
  #![allow(non_snake_case)]
2
2
  use plum_core::ast::*;
3
- use plum_core::AstParser;
4
3
 
5
4
  fn parseFns(src: &str) -> Vec<Fn> {
6
- let mut parser = tree_sitter::Parser::new();
7
- parser
8
- .set_language(&tree_sitter_plum::LANGUAGE.into())
9
- .unwrap();
10
- let tree = parser.parse(src, None).unwrap();
11
- assert!(
12
- !tree.root_node().has_error(),
13
- "parse error:\n{}",
14
- tree.root_node().to_sexp()
15
- );
16
- let source = AstParser::new(src).parseSourceRaw(tree.root_node());
5
+ let source = plum_test_support::parseRawSource(src);
17
6
  source
18
7
  .items
19
8
  .iter()
plum-core/tests/drive_test.rs CHANGED
@@ -2,16 +2,11 @@
2
2
  use plum_core::ast::*;
3
3
  use plum_core::drive::drive;
4
4
  use plum_core::purity::pureFns;
5
- use plum_core::AstParser;
6
5
 
7
6
  const PRE: &str = "enum Lst = Nil | Cons(head: Int, tail: Lst)\n";
8
7
 
9
8
  fn parse(src: &str) -> Source {
10
- let mut parser = tree_sitter::Parser::new();
11
- parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
12
- let tree = parser.parse(src, None).unwrap();
9
+ plum_test_support::parseRawSource(src)
13
- assert!(!tree.root_node().has_error(), "parse error:\n{}", tree.root_node().to_sexp());
14
- AstParser::new(src).parseSourceRaw(tree.root_node())
15
10
  }
16
11
 
17
12
  fn run(body: &str) -> (Source, Vec<String>) {
plum-core/tests/generics_test.rs ADDED
@@ -0,0 +1,34 @@
1
+ #![allow(non_snake_case)]
2
+ use plum_core::ast::*;
3
+ use plum_core::generics::{fnGenerics, isGenericName};
4
+
5
+ fn firstFn(src: &str) -> Fn {
6
+ let s = plum_test_support::parseRawSource(src);
7
+ s.items
8
+ .into_iter()
9
+ .find_map(|i| match i {
10
+ Item::Fn(f) => Some(f),
11
+ _ => None,
12
+ })
13
+ .expect("a fn")
14
+ }
15
+
16
+ #[test]
17
+ fn methodExtendsOwnerGenerics() {
18
+ let f = firstFn("fun map(self, f: fn(T) -> B) -> List[B] = self\n");
19
+ assert_eq!(fnGenerics(&f, &["T".to_string()]), vec!["T", "B"]);
20
+ }
21
+
22
+ #[test]
23
+ fn nonGenericFnHasNone() {
24
+ let f = firstFn("fun len(self) -> Int = 0\n");
25
+ assert!(fnGenerics(&f, &[]).is_empty());
26
+ }
27
+
28
+ #[test]
29
+ fn genericNames() {
30
+ assert!(isGenericName("T"));
31
+ assert!(!isGenericName("Int"));
32
+ assert!(!isGenericName("t"));
33
+ assert!(!isGenericName(""));
34
+ }
plum-core/tests/match_body_parse_test.rs CHANGED
@@ -1,19 +1,8 @@
1
1
  #![allow(non_snake_case)]
2
2
  use plum_core::ast::*;
3
- use plum_core::AstParser;
4
3
 
5
4
  fn parseFns(src: &str) -> Vec<Fn> {
6
- let mut parser = tree_sitter::Parser::new();
7
- parser
8
- .set_language(&tree_sitter_plum::LANGUAGE.into())
9
- .unwrap();
10
- let tree = parser.parse(src, None).unwrap();
11
- assert!(
12
- !tree.root_node().has_error(),
13
- "parse error:\n{}",
14
- tree.root_node().to_sexp()
15
- );
16
- let source = AstParser::new(src).parseSourceRaw(tree.root_node());
5
+ let source = plum_test_support::parseRawSource(src);
17
6
  source
18
7
  .items
19
8
  .iter()
plum-core/tests/override_parse_test.rs CHANGED
@@ -1,19 +1,8 @@
1
1
  #![allow(non_snake_case)]
2
2
  use plum_core::ast::*;
3
- use plum_core::AstParser;
4
3
 
5
4
  fn parseFns(src: &str) -> Vec<Fn> {
6
- let mut parser = tree_sitter::Parser::new();
7
- parser
8
- .set_language(&tree_sitter_plum::LANGUAGE.into())
9
- .unwrap();
10
- let tree = parser.parse(src, None).unwrap();
11
- assert!(
12
- !tree.root_node().has_error(),
13
- "parse error:\n{}",
14
- tree.root_node().to_sexp()
15
- );
16
- let source = AstParser::new(src).parseSourceRaw(tree.root_node());
5
+ let source = plum_test_support::parseRawSource(src);
17
6
  source
18
7
  .items
19
8
  .iter()
plum-core/tests/parser_test.rs CHANGED
@@ -2,15 +2,11 @@
2
2
  use plum_core::ast::*;
3
3
  use plum_core::AstParser;
4
4
 
5
- fn parse(src: &str) -> Source {
6
- parseWith(src, false)
7
- }
8
-
9
5
  fn parseRaw(src: &str) -> Source {
10
- parseWith(src, true)
6
+ plum_test_support::parseRawSource(src)
11
7
  }
12
8
 
13
- fn parseWith(src: &str, raw: bool) -> Source {
9
+ fn parse(src: &str) -> Source {
14
10
  let mut parser = tree_sitter::Parser::new();
15
11
  parser
16
12
  .set_language(&tree_sitter_plum::LANGUAGE.into())
@@ -21,12 +17,7 @@ fn parseWith(src: &str, raw: bool) -> Source {
21
17
  "parse error:\n{}",
22
18
  tree.root_node().to_sexp()
23
19
  );
24
- let ap = AstParser::new(src);
25
- if raw {
26
- ap.parseSourceRaw(tree.root_node())
27
- } else {
28
- ap.parseSource(tree.root_node())
20
+ AstParser::new(src).parseSource(tree.root_node())
29
- }
30
21
  }
31
22
 
32
23
  fn onlyFn(source: &Source) -> &Fn {
@@ -382,12 +373,7 @@ fn range_keyword_required() {
382
373
  fn fun_star_with_yield_types_parses() {
383
374
  let src = "fun* fold(init: A, src: *T) *(A, T) -> A =\n acc := init\n for x := range src\n acc = yield acc, x\n acc\n";
384
375
  assert!(!hasParseError(src));
385
- let mut parser = tree_sitter::Parser::new();
386
- parser
387
- .set_language(&tree_sitter_plum::LANGUAGE.into())
388
- .unwrap();
389
- let tree = parser.parse(src, None).unwrap();
390
- let source = AstParser::new(src).parseSourceRaw(tree.root_node());
376
+ let source = plum_test_support::parseRawSource(src);
391
377
  let f = onlyFn(&source);
392
378
  assert!(f.is_star);
393
379
  assert_eq!(f.yields.as_ref().map(|y| y.len()), Some(2));
@@ -1024,12 +1010,7 @@ fn annotatedWithEqualsAndWithoutValueStillParse() {
1024
1010
  fn exportFunParses() {
1025
1011
  let src = "export fun step(n: Int) -> Int =\n n\n";
1026
1012
  assert!(!hasParseError(src));
1027
- let mut parser = tree_sitter::Parser::new();
1028
- parser
1029
- .set_language(&tree_sitter_plum::LANGUAGE.into())
1030
- .unwrap();
1031
- let tree = parser.parse(src, None).unwrap();
1032
- let source = AstParser::new(src).parseSourceRaw(tree.root_node());
1013
+ let source = plum_test_support::parseRawSource(src);
1033
1014
  let f = onlyFn(&source);
1034
1015
  assert!(f.is_export);
1035
1016
  assert!(!f.is_extern);
plum-core/tests/record_embed_test.rs CHANGED
@@ -3,16 +3,9 @@ use plum_core::ast::*;
3
3
  use plum_core::ctors::lowerCtors;
4
4
  use plum_core::embeds::lowerEmbeds;
5
5
  use plum_core::traits::lowerTraits;
6
- use plum_core::AstParser;
7
6
 
8
7
  fn parseRaw(src: &str) -> Source {
9
- let mut parser = tree_sitter::Parser::new();
10
- parser
11
- .set_language(&tree_sitter_plum::LANGUAGE.into())
12
- .unwrap();
13
- let tree = parser.parse(src, None).unwrap();
8
+ plum_test_support::parseRawSource(src)
14
- assert!(!tree.root_node().has_error(), "{}", tree.root_node().to_sexp());
15
- AstParser::new(src).parseSourceRaw(tree.root_node())
16
9
  }
17
10
 
18
11
  fn lowerAll(src: &str) -> (Source, Vec<String>) {
plum-core/tests/record_init_test.rs CHANGED
@@ -2,16 +2,9 @@
2
2
  use plum_core::ast::*;
3
3
  use plum_core::ctors::lowerCtors;
4
4
  use plum_core::traits::lowerTraits;
5
- use plum_core::AstParser;
6
5
 
7
6
  fn parseRaw(src: &str) -> Source {
8
- let mut parser = tree_sitter::Parser::new();
9
- parser
10
- .set_language(&tree_sitter_plum::LANGUAGE.into())
11
- .unwrap();
12
- let tree = parser.parse(src, None).unwrap();
7
+ plum_test_support::parseRawSource(src)
13
- assert!(!tree.root_node().has_error(), "{}", tree.root_node().to_sexp());
14
- AstParser::new(src).parseSourceRaw(tree.root_node())
15
8
  }
16
9
 
17
10
  fn lower(src: &str) -> (Source, Vec<String>) {
plum-core/tests/rename_test.rs ADDED
@@ -0,0 +1,88 @@
1
+ #![allow(non_snake_case)]
2
+ use plum_core::ast::*;
3
+ use plum_core::rename::*;
4
+ use std::collections::HashMap;
5
+
6
+ fn body(src: &str) -> Block {
7
+ let s = plum_test_support::parseRawSource(src);
8
+ for i in s.items {
9
+ if let Item::Fn(f) = i {
10
+ if f.name == "t" {
11
+ return match f.body {
12
+ FnBody::Block(b) => b,
13
+ FnBody::Expr(e) => Block { stmts: vec![Stmt::Expr(e, 0, 0)] },
14
+ FnBody::Extern => panic!(),
15
+ };
16
+ }
17
+ }
18
+ }
19
+ panic!("no fn t")
20
+ }
21
+
22
+ fn events(src: &str) -> Vec<(String, &'static str)> {
23
+ let mut b = body(src);
24
+ let mut out = vec![];
25
+ renameStmts(&mut b.stmts, &mut |n, k| {
26
+ out.push((
27
+ n.clone(),
28
+ match k {
29
+ Name::Decl => "decl",
30
+ Name::Pat => "pat",
31
+ Name::Plain => "plain",
32
+ Name::Ref => "ref",
33
+ },
34
+ ));
35
+ });
36
+ out
37
+ }
38
+
39
+ #[test]
40
+ fn renamesDeclThenRef() {
41
+ let src = "fun t() -> Int =\n x := 1\n y := x\n return y\n";
42
+ let mut b = body(src);
43
+ renameStmts(&mut b.stmts, &mut |n, _| {
44
+ if n == "x" {
45
+ *n = "x2".into();
46
+ }
47
+ });
48
+ let ev = events(src);
49
+ assert_eq!(ev[0], ("x".into(), "decl"));
50
+ assert!(ev.contains(&("x".into(), "ref")));
51
+ assert!(matches!(&b.stmts[0], Stmt::Assign(a) if matches!(&a.targets[0], AssignTarget::Var(n) if n == "x2")));
52
+ }
53
+
54
+ #[test]
55
+ fn plainAssign() {
56
+ let ev = events("fun t() -> Int =\n x := 1\n x = 2\n return x\n");
57
+ assert!(ev.contains(&("x".into(), "plain")));
58
+ }
59
+
60
+ #[test]
61
+ fn patternNames() {
62
+ let ev = events("fun t(l: Int) -> Int = match l\n Cons(h, t) => h\n _ => 0\n");
63
+ assert!(ev.contains(&("h".into(), "pat")));
64
+ }
65
+
66
+ #[test]
67
+ fn rename_shadowing() {
68
+ let ev = events("fun t(x: Int) -> Int =\n f := |x| x + 1\n return f(x)\n");
69
+ let decl = ev.iter().position(|e| *e == ("x".into(), "decl")).unwrap();
70
+ let inner = ev.iter().position(|e| *e == ("x".into(), "ref")).unwrap();
71
+ assert!(decl < inner, "{ev:?}");
72
+ }
73
+
74
+ #[test]
75
+ fn field_assign_target() {
76
+ let ev = events("fun t(p: Int) -> Int =\n self.x = 5\n return 0\n");
77
+ assert!(!ev.iter().any(|(n, _)| n == "x"), "{ev:?}");
78
+ }
79
+
80
+ #[test]
81
+ fn substTypeReplacesKeys() {
82
+ let t = Type { name: "List".into(), generics: vec![Type { name: "T".into(), generics: vec![] }] };
83
+ let mut m = HashMap::new();
84
+ m.insert("T".to_string(), Type { name: "Int".into(), generics: vec![] });
85
+ let r = substType(&t, &m);
86
+ assert_eq!(r.name, "List");
87
+ assert_eq!(r.generics[0].name, "Int");
88
+ }
plum-core/tests/simplify_test.rs CHANGED
@@ -1,17 +1,12 @@
1
1
  #![allow(non_snake_case)]
2
2
  use plum_core::ast::*;
3
3
  use plum_core::simplify::{simplify, simplifyBlock, simplifyExpr};
4
- use plum_core::AstParser;
5
4
  use std::collections::HashSet;
6
5
 
7
6
  const PRE: &str = "enum Lst = Nil | Cons(head: Int, tail: Lst)\nrecord Pt(x: Int, y: Int)\n";
8
7
 
9
8
  fn parse(src: &str) -> Source {
10
- let mut parser = tree_sitter::Parser::new();
11
- parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
12
- let tree = parser.parse(src, None).unwrap();
9
+ plum_test_support::parseRawSource(src)
13
- assert!(!tree.root_node().has_error(), "parse error:\n{}", tree.root_node().to_sexp());
14
- AstParser::new(src).parseSourceRaw(tree.root_node())
15
10
  }
16
11
 
17
12
  fn pureSet(names: &[&str]) -> HashSet<String> {
plum-core/tests/traits_test.rs CHANGED
@@ -2,16 +2,9 @@
2
2
  use plum_core::ast::*;
3
3
  use plum_core::traits::{getter, lowerTraits, setter};
4
4
  use plum_core::unions::resolveMembers;
5
- use plum_core::AstParser;
6
5
 
7
6
  fn parseRaw(src: &str) -> Source {
8
- let mut parser = tree_sitter::Parser::new();
9
- parser
10
- .set_language(&tree_sitter_plum::LANGUAGE.into())
11
- .unwrap();
12
- let tree = parser.parse(src, None).unwrap();
7
+ plum_test_support::parseRawSource(src)
13
- assert!(!tree.root_node().has_error(), "{}", tree.root_node().to_sexp());
14
- AstParser::new(src).parseSourceRaw(tree.root_node())
15
8
  }
16
9
 
17
10
  fn enumOf<'a>(s: &'a Source, name: &str) -> &'a Enum {
plum-core/tests/trmc_test.rs CHANGED
@@ -592,3 +592,46 @@ fun main() -> Int =
592
592
  assert!(isRewritten("tailw1", &p, "map"));
593
593
  assert_eq!(runInt("tailw2", &p), 1);
594
594
  }
595
+
596
+ #[test]
597
+ fn try_in_string() {
598
+ // The old debug-text scan saw "Try(" / "Closure(" inside a string literal and refused.
599
+ let p = prog(
600
+ "\
601
+ fun fromToS(a: Int, b: Int) -> Link[Int] =
602
+ if a >= b then Nil else Cons(head: a + \"Try(Closure(\".length() - 12, tail: fromToS(a + 1, b))
603
+
604
+ fun main() -> Int =
605
+ xs := fromToS(0, 5)
606
+ if count(xs) == 5 && sum(xs) == 10 then 1 else 0
607
+ ",
608
+ );
609
+ assert!(isRewritten("trys1", &p, "fromToS"));
610
+ assert_eq!(runInt("trys2", &p), 1);
611
+ }
612
+
613
+ #[test]
614
+ fn mentions_field_not_var() {
615
+ // A field named like the fn is not a call of it; the old debug-text
616
+ // match counted it and saw two recursive fields.
617
+ let p = prog(
618
+ "\
619
+ record Wrap(build: Int)
620
+
621
+ fun pick(w: Wrap) -> Int =
622
+ r := 0
623
+ while r < w.build
624
+ r = r + 1
625
+ r
626
+
627
+ fun build(a: Int, b: Int) -> Link[Int] =
628
+ if a >= b then Nil else Cons(head: pick(Wrap(build: a)), tail: build(a + 1, b))
629
+
630
+ fun main() -> Int =
631
+ xs := build(0, 5)
632
+ if count(xs) == 5 && sum(xs) == 10 then 1 else 0
633
+ ",
634
+ );
635
+ assert!(isRewritten("ment1", &p, "build"));
636
+ assert_eq!(runInt("ment2", &p), 1);
637
+ }
plum-core/tests/uniqueness_test.rs CHANGED
@@ -1,14 +1,9 @@
1
1
  #![allow(non_snake_case)]
2
2
  use plum_core::ast::*;
3
3
  use plum_core::uniqueness::{markUnique, UniqueReport};
4
- use plum_core::AstParser;
5
4
 
6
5
  fn run(src: &str) -> (Source, UniqueReport) {
7
- let mut parser = tree_sitter::Parser::new();
6
+ let mut s = plum_test_support::parseRawSource(src);
8
- parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
9
- let tree = parser.parse(src, None).unwrap();
10
- assert!(!tree.root_node().has_error(), "parse error:\n{}", tree.root_node().to_sexp());
11
- let mut s = AstParser::new(src).parseSourceRaw(tree.root_node());
12
7
  let r = markUnique(&mut s);
13
8
  (s, r)
14
9
  }
plum-core/tests/visit_test.rs ADDED
@@ -0,0 +1,88 @@
1
+ #![allow(non_snake_case)]
2
+ use plum_core::ast::*;
3
+ use plum_core::visit::*;
4
+
5
+ fn fnBody(src: &str) -> FnBody {
6
+ let s = plum_test_support::parseRawSource(src);
7
+ for i in s.items {
8
+ if let Item::Fn(f) = i {
9
+ if f.name == "t" {
10
+ return f.body;
11
+ }
12
+ }
13
+ }
14
+ panic!("no fn t")
15
+ }
16
+
17
+ fn bodyExpr(src: &str) -> Expr {
18
+ match fnBody(&format!("fun t() = {src}\n")) {
19
+ FnBody::Expr(e) => e,
20
+ FnBody::Block(mut b) => match b.stmts.remove(0) {
21
+ Stmt::Expr(e, ..) => e,
22
+ s => panic!("{s:?}"),
23
+ },
24
+ FnBody::Extern => panic!(),
25
+ }
26
+ }
27
+
28
+ fn isCall(e: &Expr) -> bool {
29
+ matches!(e, Expr::FnCall(_))
30
+ }
31
+
32
+ #[test]
33
+ fn anyExprFindsNestedCall() {
34
+ let e = bodyExpr("f(Try(1))");
35
+ assert!(anyExpr(&e, &mut |x| isCall(x)));
36
+ }
37
+
38
+ #[test]
39
+ fn anyExprIgnoresStringContents() {
40
+ let e = bodyExpr("\"Try(\"");
41
+ assert!(!anyExpr(&e, &mut |x| isCall(x)));
42
+ }
43
+
44
+ #[test]
45
+ fn patternBindersSkipsConstructor() {
46
+ let p = CasePattern::Class {
47
+ name: "Cons".into(),
48
+ fields: vec![CasePattern::Name("h".into()), CasePattern::Name("t".into()), CasePattern::Name("Nil".into()), CasePattern::Wildcard],
49
+ };
50
+ let mut out = vec![];
51
+ patternBinders(&p, &mut out);
52
+ assert_eq!(out, vec!["h", "t"]);
53
+ }
54
+
55
+ #[test]
56
+ fn patternBindersIgnoresUnderscore() {
57
+ let mut out = vec![];
58
+ patternBinders(&CasePattern::Name("_".into()), &mut out);
59
+ assert!(out.is_empty());
60
+ patternBinders(&CasePattern::Name("x".into()), &mut out);
61
+ assert_eq!(out, vec!["x"]);
62
+ patternBinders(&CasePattern::Name("Nil".into()), &mut out);
63
+ assert_eq!(out, vec!["x"]);
64
+ }
65
+
66
+ #[test]
67
+ fn statementWalkers() {
68
+ let src = "fun t(x: Int) -> Int =\n if x > 1\n return 1\n else\n return 2\n";
69
+ let FnBody::Block(mut b) = fnBody(src) else { panic!() };
70
+ let s = &mut b.stmts[0];
71
+ assert_eq!(stmtExprs(s).len(), 1);
72
+ assert_eq!(stmtBlocks(s).len(), 2);
73
+ let mut n = 0;
74
+ visitBlock(&mut b, &mut |_| n += 1);
75
+ assert!(n >= 4);
76
+ }
77
+
78
+ #[test]
79
+ fn countVarSeesClosures() {
80
+ let src = "fun t(x: Int) -> Int =\n f := |y| y + x\n return x + f(x)\n";
81
+ assert_eq!(countVar(&fnBody(src), "x"), 3);
82
+ }
83
+
84
+ #[test]
85
+ fn unparenStrips() {
86
+ let e = Expr::Paren(Box::new(Expr::Paren(Box::new(Expr::Int(1)))));
87
+ assert!(matches!(unparen(&e), Expr::Int(1)));
88
+ }
plum-game/Cargo.toml CHANGED
@@ -6,6 +6,7 @@ description = "Headless test runner for Plum game programs (the browser bridge l
6
6
 
7
7
  [dependencies]
8
8
  wasmtime = "47"
9
+ plum-host = { path = "../plum-host" }
9
10
  # Image headers only: texture sizes for `textureWidth`/`textureHeight`.
10
11
  image = { version = "0.24", default-features = false, features = ["png", "jpeg", "gif", "bmp"] }
11
12
 
plum-game/src/host.rs CHANGED
@@ -2,7 +2,7 @@
2
2
  //! import to a [`Backend`].
3
3
 
4
4
  use crate::api::{Backend, Color};
5
- use wasmtime::{AnyRef, Caller, Engine, Func, Instance, Linker, Module, OwnedRooted, Rooted, Store, Val};
5
+ use wasmtime::{AnyRef, Caller, Func, Instance, Linker, Module, OwnedRooted, Store, Val};
6
6
 
7
7
  pub use wasmtime::Result;
8
8
 
@@ -39,28 +39,11 @@ fn fv(v: f32) -> Val {
39
39
  Val::F64((v as f64).to_bits())
40
40
  }
41
41
 
42
- /// Reads the bytes of a Plum `Str` (a struct of `data`, `start`, `len`).
43
- fn str_bytes(store: &mut impl wasmtime::AsContextMut, s: &Rooted<AnyRef>) -> Result<Vec<u8>> {
44
- let st = s.unwrap_struct(&*store)?;
45
- let Val::AnyRef(Some(data)) = st.field(&mut *store, 0)? else {
46
- return Err(wasmtime::Error::msg("Str.data is not a byte array"));
47
- };
48
- let (Val::I64(start), Val::I64(len)) = (st.field(&mut *store, 1)?, st.field(&mut *store, 2)?) else {
49
- return Err(wasmtime::Error::msg("Str.start/len are not Int"));
50
- };
51
- let (start, len) = (usize::try_from(start)?, usize::try_from(len)?);
52
- let end = start.checked_add(len).ok_or_else(|| wasmtime::Error::msg("Str view out of range"))?;
53
- let arr = data.unwrap_array(&*store)?;
54
- let mut buf = vec![0u8; arr.len(&*store)? as usize];
55
- arr.copy_to_i8_slice(&mut *store, &mut buf)?;
56
- buf.get(start..end).map(|b| b.to_vec()).ok_or_else(|| wasmtime::Error::msg("Str view out of range"))
57
- }
58
-
59
42
  fn str_arg<B: Backend>(caller: &mut Caller<'_, HostState<B>>, p: &[Val], i: usize) -> Result<String> {
60
43
  let Val::AnyRef(Some(s)) = &p[i] else {
61
44
  return Err(wasmtime::Error::msg("expected a Str argument"));
62
45
  };
63
- Ok(String::from_utf8_lossy(&str_bytes(caller, s)?).into_owned())
46
+ Ok(String::from_utf8_lossy(&plum_host::strBytes(caller, s)?).into_owned())
64
47
  }
65
48
 
66
49
  /// Builds a Plum `Str` holding `bytes` with the module's `strNew` and
@@ -175,16 +158,7 @@ fn dispatch<B: Backend>(name: &str, caller: &mut Caller<'_, HostState<B>>, p: &[
175
158
  impl<B: Backend> PlumGame<B> {
176
159
  /// Instantiates `wasm`, requiring `init`, `update` and `draw` exports.
177
160
  pub fn load(wasm: &[u8], backend: B) -> Result<Self> {
178
- let mut config = wasmtime::Config::new();
179
- config.wasm_gc(true);
180
- config.wasm_function_references(true);
181
- config.wasm_tail_call(true);
182
- config.max_wasm_stack(4 << 20);
183
- // Start the GC heap at 64 MiB (reserved, committed lazily) so list-heavy programs
184
- // skip the early full-heap copies of each doubling growth (docs/plans/2026-11-03-gc-report.md).
185
- config.gc_heap_initial_size(64 << 20);
186
- config.async_stack_size(8 << 20);
187
- let engine = Engine::new(&config)?;
161
+ let engine = plum_host::engine();
188
162
  let module = Module::new(&engine, wasm)?;
189
163
  for name in ["init", "update", "draw"] {
190
164
  if module.get_export(name).is_none() {
plum-host/Cargo.toml ADDED
@@ -0,0 +1,12 @@
1
+ [package]
2
+ name = "plum-host"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ [dependencies]
7
+ anyhow = "1"
8
+ wasmtime = "47"
9
+ ureq = "2.10"
10
+
11
+ [dev-dependencies]
12
+ wat = "1"
plum-host/src/lib.rs ADDED
@@ -0,0 +1,659 @@
1
+ //! Shared wasmtime host for Plum programs: engine setup, Str helpers and the
2
+ //! `plum::*` host imports used by the CLI, runtime and game host.
3
+ //!
4
+ //! Creating a Str goes through the compiled module's `strNew`/`strSetByte` exports
5
+ //! (as plum-game's host does); only reading one (`strBytes`) lives here.
6
+ #![allow(non_snake_case)]
7
+ use anyhow::Result;
8
+
9
+ /// Which groups of host imports are real; a disabled group is stubbed with a trap.
10
+ #[derive(Clone, Copy, Debug, Default)]
11
+ pub struct Caps {
12
+ pub fs: bool,
13
+ pub http: bool,
14
+ pub clock: bool,
15
+ pub rng: bool,
16
+ }
17
+
18
+ impl Caps {
19
+ pub fn all() -> Caps {
20
+ Caps { fs: true, http: true, clock: true, rng: true }
21
+ }
22
+ }
23
+
24
+ /// The engine every Plum host uses: GC, typed function refs, tail calls, 64 MiB initial GC heap.
25
+ pub fn engine() -> wasmtime::Engine {
26
+ let mut config = wasmtime::Config::new();
27
+ config.wasm_gc(true);
28
+ config.wasm_function_references(true);
29
+ config.wasm_tail_call(true);
30
+ config.max_wasm_stack(4 << 20);
31
+ // Start the GC heap at 64 MiB (reserved, committed lazily) so list-heavy programs
32
+ // skip the early full-heap copies of each doubling growth (docs/plans/2026-11-03-gc-report.md).
33
+ config.gc_heap_initial_size(64 << 20);
34
+ config.async_stack_size(8 << 20);
35
+ wasmtime::Engine::new(&config).expect("failed to construct wasmtime engine")
36
+ }
37
+
38
+ /// Builds the `Extern`s that satisfy a compiled Plum module's host imports (today,
39
+ /// only `plum::printLn`) in declaration order, matching each one's exact `FuncType`
40
+ /// (its `Str` param is a concrete wasm-gc array type, so the host function must be
41
+ /// built from the module's own reported type rather than a generic `Rooted<ArrayRef>`
42
+ /// wrapper, which would carry the wrong, unrelated top-array type and fail to
43
+ /// instantiate).
44
+ pub fn hostImports(
45
+ store: &mut wasmtime::Store<()>,
46
+ module: &wasmtime::Module,
47
+ caps: Caps,
48
+ ) -> Result<Vec<wasmtime::Extern>> {
49
+ module
50
+ .imports()
51
+ .map(|imp| {
52
+ match (imp.module(), imp.name()) {
53
+ ("plum", "printLn") => {
54
+ let func_ty = imp.ty().func().cloned().ok_or_else(|| {
55
+ anyhow::anyhow!("plum::printLn import must be a function")
56
+ })?;
57
+ Ok(wasmtime::Extern::Func(wasmtime::Func::new(
58
+ &mut *store,
59
+ func_ty,
60
+ |mut caller, params, _results| {
61
+ let wasmtime::Val::AnyRef(Some(s)) = &params[0] else {
62
+ return Err(wasmtime::Error::msg("printLn expects a Str argument"));
63
+ };
64
+ let buf = strBytes(&mut caller, s)?;
65
+ println!("{}", String::from_utf8_lossy(&buf));
66
+ Ok(())
67
+ },
68
+ )))
69
+ }
70
+ ("plum", "crash") => {
71
+ let func_ty =
72
+ imp.ty().func().cloned().ok_or_else(|| {
73
+ anyhow::anyhow!("plum::crash import must be a function")
74
+ })?;
75
+ Ok(wasmtime::Extern::Func(wasmtime::Func::new(
76
+ &mut *store,
77
+ func_ty,
78
+ |mut caller, params, _results| {
79
+ let wasmtime::Val::AnyRef(Some(s)) = &params[0] else {
80
+ return Err(wasmtime::Error::msg("crash expects a Str argument"));
81
+ };
82
+ let buf = strBytes(&mut caller, s)?;
83
+ eprintln!("crash: {}", String::from_utf8_lossy(&buf));
84
+ Ok(())
85
+ },
86
+ )))
87
+ }
88
+ ("plum", "rawRandomHalf") if caps.rng => {
89
+ let func_ty = imp.ty().func().cloned().ok_or_else(|| {
90
+ anyhow::anyhow!("plum::rawRandomHalf import must be a function")
91
+ })?;
92
+ Ok(wasmtime::Extern::Func(wasmtime::Func::new(
93
+ &mut *store,
94
+ func_ty,
95
+ |_caller, _params, results| {
96
+ results[0] = wasmtime::Val::I32((randomI64() >> 32) as i32);
97
+ Ok(())
98
+ },
99
+ )))
100
+ }
101
+ ("plum", "rawNowMillis") if caps.clock => {
102
+ let func_ty = imp.ty().func().cloned().ok_or_else(|| {
103
+ anyhow::anyhow!("plum::rawNowMillis import must be a function")
104
+ })?;
105
+ Ok(wasmtime::Extern::Func(wasmtime::Func::new(
106
+ &mut *store,
107
+ func_ty,
108
+ |_caller, _params, results| {
109
+ results[0] = wasmtime::Val::F64((nowMillis() as f64).to_bits());
110
+ Ok(())
111
+ },
112
+ )))
113
+ }
114
+ ("plum", "rawReadFile") if caps.fs => {
115
+ let func_ty = imp.ty().func().cloned().ok_or_else(|| {
116
+ anyhow::anyhow!("plum::rawReadFile import must be a function")
117
+ })?;
118
+ Ok(wasmtime::Extern::Func(wasmtime::Func::new(
119
+ &mut *store,
120
+ func_ty.clone(),
121
+ move |mut caller, params, results| {
122
+ let path = readStrArg(&mut caller, &params[0])?;
123
+ let bytes = std::fs::read(&path).unwrap_or_default();
124
+ results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(
125
+ &mut caller,
126
+ &func_ty,
127
+ &bytes,
128
+ )?));
129
+ Ok(())
130
+ },
131
+ )))
132
+ }
133
+ ("plum", "rawWriteFile") if caps.fs => {
134
+ let func_ty = imp.ty().func().cloned().ok_or_else(|| {
135
+ anyhow::anyhow!("plum::rawWriteFile import must be a function")
136
+ })?;
137
+ Ok(wasmtime::Extern::Func(wasmtime::Func::new(
138
+ &mut *store,
139
+ func_ty,
140
+ |mut caller, params, results| {
141
+ let path = readStrArg(&mut caller, &params[0])?;
142
+ let data = readStrArg(&mut caller, &params[1])?;
143
+ results[0] =
144
+ wasmtime::Val::I32(std::fs::write(&path, &data).is_ok() as i32);
145
+ Ok(())
146
+ },
147
+ )))
148
+ }
149
+ ("plum", "rawExists") if caps.fs => {
150
+ let func_ty = imp.ty().func().cloned().ok_or_else(|| {
151
+ anyhow::anyhow!("plum::rawExists import must be a function")
152
+ })?;
153
+ Ok(wasmtime::Extern::Func(wasmtime::Func::new(
154
+ &mut *store,
155
+ func_ty,
156
+ |mut caller, params, results| {
157
+ let path = readStrArg(&mut caller, &params[0])?;
158
+ // `symlink_metadata`, not `Path::exists`, so a dangling symlink still
159
+ // reports as existing (it does exist, just not what it points to) —
160
+ // otherwise `remove` on a broken symlink would report "no such file"
161
+ // and never get to call `rawRemove` on the symlink itself.
162
+ results[0] =
163
+ wasmtime::Val::I32(std::fs::symlink_metadata(&path).is_ok() as i32);
164
+ Ok(())
165
+ },
166
+ )))
167
+ }
168
+ ("plum", "rawMkdir") if caps.fs => {
169
+ let func_ty = imp.ty().func().cloned().ok_or_else(|| {
170
+ anyhow::anyhow!("plum::rawMkdir import must be a function")
171
+ })?;
172
+ Ok(wasmtime::Extern::Func(wasmtime::Func::new(
173
+ &mut *store,
174
+ func_ty,
175
+ |mut caller, params, results| {
176
+ let path = readStrArg(&mut caller, &params[0])?;
177
+ results[0] =
178
+ wasmtime::Val::I32(std::fs::create_dir_all(&path).is_ok() as i32);
179
+ Ok(())
180
+ },
181
+ )))
182
+ }
183
+ ("plum", "rawRemove") if caps.fs => {
184
+ let func_ty = imp.ty().func().cloned().ok_or_else(|| {
185
+ anyhow::anyhow!("plum::rawRemove import must be a function")
186
+ })?;
187
+ Ok(wasmtime::Extern::Func(wasmtime::Func::new(
188
+ &mut *store,
189
+ func_ty,
190
+ |mut caller, params, results| {
191
+ let path = readStrArg(&mut caller, &params[0])?;
192
+ results[0] =
193
+ wasmtime::Val::I32(std::fs::remove_file(&path).is_ok() as i32);
194
+ Ok(())
195
+ },
196
+ )))
197
+ }
198
+ ("plum", "rawRemoveDir") if caps.fs => {
199
+ let func_ty = imp.ty().func().cloned().ok_or_else(|| {
200
+ anyhow::anyhow!("plum::rawRemoveDir import must be a function")
201
+ })?;
202
+ Ok(wasmtime::Extern::Func(wasmtime::Func::new(
203
+ &mut *store,
204
+ func_ty,
205
+ |mut caller, params, results| {
206
+ let path = readStrArg(&mut caller, &params[0])?;
207
+ results[0] =
208
+ wasmtime::Val::I32(std::fs::remove_dir(&path).is_ok() as i32);
209
+ Ok(())
210
+ },
211
+ )))
212
+ }
213
+ ("plum", "rawRename") if caps.fs => {
214
+ let func_ty = imp.ty().func().cloned().ok_or_else(|| {
215
+ anyhow::anyhow!("plum::rawRename import must be a function")
216
+ })?;
217
+ Ok(wasmtime::Extern::Func(wasmtime::Func::new(
218
+ &mut *store,
219
+ func_ty,
220
+ |mut caller, params, results| {
221
+ let old_path = readStrArg(&mut caller, &params[0])?;
222
+ let new_path = readStrArg(&mut caller, &params[1])?;
223
+ results[0] = wasmtime::Val::I32(
224
+ std::fs::rename(&old_path, &new_path).is_ok() as i32,
225
+ );
226
+ Ok(())
227
+ },
228
+ )))
229
+ }
230
+ ("plum", "rawTruncate") if caps.fs => {
231
+ let func_ty = imp.ty().func().cloned().ok_or_else(|| {
232
+ anyhow::anyhow!("plum::rawTruncate import must be a function")
233
+ })?;
234
+ Ok(wasmtime::Extern::Func(wasmtime::Func::new(
235
+ &mut *store,
236
+ func_ty,
237
+ |mut caller, params, results| {
238
+ let path = readStrArg(&mut caller, &params[0])?;
239
+ let wasmtime::Val::I32(len) = params[1] else {
240
+ return Err(wasmtime::Error::msg(
241
+ "rawTruncate expects an Int length argument",
242
+ ));
243
+ };
244
+ let ok = std::fs::OpenOptions::new()
245
+ .write(true)
246
+ .open(&path)
247
+ .and_then(|f| f.set_len(len.max(0) as u64))
248
+ .is_ok();
249
+ results[0] = wasmtime::Val::I32(ok as i32);
250
+ Ok(())
251
+ },
252
+ )))
253
+ }
254
+ ("plum", "rawLink") if caps.fs => {
255
+ let func_ty = imp.ty().func().cloned().ok_or_else(|| {
256
+ anyhow::anyhow!("plum::rawLink import must be a function")
257
+ })?;
258
+ Ok(wasmtime::Extern::Func(wasmtime::Func::new(
259
+ &mut *store,
260
+ func_ty,
261
+ |mut caller, params, results| {
262
+ let existing_path = readStrArg(&mut caller, &params[0])?;
263
+ let new_path = readStrArg(&mut caller, &params[1])?;
264
+ results[0] = wasmtime::Val::I32(
265
+ std::fs::hard_link(&existing_path, &new_path).is_ok() as i32,
266
+ );
267
+ Ok(())
268
+ },
269
+ )))
270
+ }
271
+ ("plum", "rawSymlink") if caps.fs => {
272
+ let func_ty = imp.ty().func().cloned().ok_or_else(|| {
273
+ anyhow::anyhow!("plum::rawSymlink import must be a function")
274
+ })?;
275
+ Ok(wasmtime::Extern::Func(wasmtime::Func::new(
276
+ &mut *store,
277
+ func_ty,
278
+ |mut caller, params, results| {
279
+ let target = readStrArg(&mut caller, &params[0])?;
280
+ let path = readStrArg(&mut caller, &params[1])?;
281
+ #[cfg(unix)]
282
+ let ok = std::os::unix::fs::symlink(&target, &path).is_ok();
283
+ #[cfg(not(unix))]
284
+ let ok = false;
285
+ results[0] = wasmtime::Val::I32(ok as i32);
286
+ Ok(())
287
+ },
288
+ )))
289
+ }
290
+ ("plum", "rawReadlink") if caps.fs => {
291
+ let func_ty = imp.ty().func().cloned().ok_or_else(|| {
292
+ anyhow::anyhow!("plum::rawReadlink import must be a function")
293
+ })?;
294
+ Ok(wasmtime::Extern::Func(wasmtime::Func::new(
295
+ &mut *store,
296
+ func_ty.clone(),
297
+ move |mut caller, params, results| {
298
+ let path = readStrArg(&mut caller, &params[0])?;
299
+ let target = std::fs::read_link(&path)
300
+ .ok()
301
+ .map(|p| p.to_string_lossy().into_owned())
302
+ .unwrap_or_default();
303
+ results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(
304
+ &mut caller,
305
+ &func_ty,
306
+ target.as_bytes(),
307
+ )?));
308
+ Ok(())
309
+ },
310
+ )))
311
+ }
312
+ ("plum", "rawRealpath") if caps.fs => {
313
+ let func_ty = imp.ty().func().cloned().ok_or_else(|| {
314
+ anyhow::anyhow!("plum::rawRealpath import must be a function")
315
+ })?;
316
+ Ok(wasmtime::Extern::Func(wasmtime::Func::new(
317
+ &mut *store,
318
+ func_ty.clone(),
319
+ move |mut caller, params, results| {
320
+ let path = readStrArg(&mut caller, &params[0])?;
321
+ let resolved = std::fs::canonicalize(&path)
322
+ .ok()
323
+ .map(|p| p.to_string_lossy().into_owned())
324
+ .unwrap_or_default();
325
+ results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(
326
+ &mut caller,
327
+ &func_ty,
328
+ resolved.as_bytes(),
329
+ )?));
330
+ Ok(())
331
+ },
332
+ )))
333
+ }
334
+ ("plum", "rawChmod") if caps.fs => {
335
+ let func_ty = imp.ty().func().cloned().ok_or_else(|| {
336
+ anyhow::anyhow!("plum::rawChmod import must be a function")
337
+ })?;
338
+ Ok(wasmtime::Extern::Func(wasmtime::Func::new(
339
+ &mut *store,
340
+ func_ty,
341
+ |mut caller, params, results| {
342
+ let path = readStrArg(&mut caller, &params[0])?;
343
+ let wasmtime::Val::I32(mode) = params[1] else {
344
+ return Err(wasmtime::Error::msg(
345
+ "rawChmod expects an Int mode argument",
346
+ ));
347
+ };
348
+ #[cfg(unix)]
349
+ let ok = {
350
+ use std::os::unix::fs::PermissionsExt;
351
+ std::fs::set_permissions(
352
+ &path,
353
+ std::fs::Permissions::from_mode(mode as u32),
354
+ )
355
+ .is_ok()
356
+ };
357
+ #[cfg(not(unix))]
358
+ let ok = false;
359
+ results[0] = wasmtime::Val::I32(ok as i32);
360
+ Ok(())
361
+ },
362
+ )))
363
+ }
364
+ ("plum", "rawReadDir") if caps.fs => {
365
+ let func_ty = imp.ty().func().cloned().ok_or_else(|| {
366
+ anyhow::anyhow!("plum::rawReadDir import must be a function")
367
+ })?;
368
+ Ok(wasmtime::Extern::Func(wasmtime::Func::new(
369
+ &mut *store,
370
+ func_ty.clone(),
371
+ move |mut caller, params, results| {
372
+ let path = readStrArg(&mut caller, &params[0])?;
373
+ let joined = std::fs::read_dir(&path)
374
+ .ok()
375
+ .map(|entries| {
376
+ entries
377
+ .filter_map(|e| e.ok())
378
+ .map(|e| e.file_name().to_string_lossy().into_owned())
379
+ .collect::<Vec<_>>()
380
+ .join("\n")
381
+ })
382
+ .unwrap_or_default();
383
+ results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(
384
+ &mut caller,
385
+ &func_ty,
386
+ joined.as_bytes(),
387
+ )?));
388
+ Ok(())
389
+ },
390
+ )))
391
+ }
392
+ ("plum", "rawHttpRequest") if caps.http => {
393
+ let func_ty = imp.ty().func().cloned().ok_or_else(|| {
394
+ anyhow::anyhow!("plum::rawHttpRequest import must be a function")
395
+ })?;
396
+ Ok(wasmtime::Extern::Func(wasmtime::Func::new(
397
+ &mut *store,
398
+ func_ty.clone(),
399
+ move |mut caller, params, results| {
400
+ let method = readStrArg(&mut caller, &params[0])?;
401
+ let url = readStrArg(&mut caller, &params[1])?;
402
+ let headers = readStrArg(&mut caller, &params[2])?;
403
+ let body = readStrArg(&mut caller, &params[3])?;
404
+ let raw = performHttpRequest(&method, &url, &headers, &body);
405
+ results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(
406
+ &mut caller,
407
+ &func_ty,
408
+ raw.as_bytes(),
409
+ )?));
410
+ Ok(())
411
+ },
412
+ )))
413
+ }
414
+ // A `plum` import this host doesn't define (the game API of `std/game/Game`)
415
+ // is stubbed with a function that traps when called, so a program
416
+ // that imports it can still run or test code that never calls it.
417
+ ("plum", n) => {
418
+ let func_ty =
419
+ imp.ty().func().cloned().ok_or_else(|| {
420
+ anyhow::anyhow!("plum::{n} import must be a function")
421
+ })?;
422
+ let name = n.to_string();
423
+ Ok(wasmtime::Extern::Func(wasmtime::Func::new(
424
+ &mut *store,
425
+ func_ty,
426
+ move |_caller, _params, _results| {
427
+ Err(wasmtime::Error::msg(format!(
428
+ "host function '{name}' is not available here"
429
+ )))
430
+ },
431
+ )))
432
+ }
433
+ (m, n) => anyhow::bail!("unsupported host import {m}::{n}"),
434
+ }
435
+ })
436
+ .collect()
437
+ }
438
+
439
+ /// Performs one blocking HTTP round trip for `plum::rawHttpRequest` and packs
440
+ /// the outcome into the wire format `plum-std/Http.plum`'s `request` expects
441
+ /// (see that extern's doc comment): `"1\x01<status>\x01<headers>\x00<body>"`
442
+ /// on success, `"0\x01\x01<message>\x00"` for a transport-level failure. A
443
+ /// non-2xx HTTP status (404, 500, ...) is NOT a failure here — it's still a
444
+ /// complete response, so it goes through the success branch with its own
445
+ /// status code.
446
+ fn performHttpRequest(method: &str, url: &str, headers: &str, body: &str) -> String {
447
+ let agent = ureq::Agent::new();
448
+ let mut req = agent.request(method, url);
449
+ for line in headers.split('\n') {
450
+ if let Some((k, v)) = line.split_once(": ") {
451
+ req = req.set(k, v);
452
+ }
453
+ }
454
+ let result = if body.is_empty() {
455
+ req.call()
456
+ } else {
457
+ req.send_string(body)
458
+ };
459
+ match result {
460
+ Ok(resp) | Err(ureq::Error::Status(_, resp)) => {
461
+ let status = resp.status();
462
+ let mut header_lines = String::new();
463
+ for name in resp.headers_names() {
464
+ if let Some(v) = resp.header(&name) {
465
+ if !header_lines.is_empty() {
466
+ header_lines.push('\n');
467
+ }
468
+ header_lines.push_str(&format!("{name}: {v}"));
469
+ }
470
+ }
471
+ let body_text = resp.into_string().unwrap_or_default();
472
+ format!("1\x01{status}\x01{header_lines}\x00{body_text}")
473
+ }
474
+ Err(e) => format!("0\x01\x01{e}\x00"),
475
+ }
476
+ }
477
+
478
+ /// A pseudo-random `i64`: splitmix64 over process-wide state seeded once. Cheap,
479
+ /// dependency-free, not cryptographically secure.
480
+ fn randomI64() -> i64 {
481
+ use std::sync::atomic::{AtomicU64, Ordering};
482
+ // splitmix64 over a process-wide counter: seeded once from the clock and
483
+ // an address, then advanced by a fixed odd increment per call, so
484
+ // back-to-back calls can never repeat.
485
+ static STATE: AtomicU64 = AtomicU64::new(0);
486
+ let mut cur = STATE.load(Ordering::Relaxed);
487
+ if cur == 0 {
488
+ let nanos = std::time::SystemTime::now()
489
+ .duration_since(std::time::UNIX_EPOCH)
490
+ .map(|d| d.as_nanos() as u64)
491
+ .unwrap_or(0);
492
+ let seed = (nanos ^ (&STATE as *const AtomicU64 as u64)) | 1;
493
+ cur = match STATE.compare_exchange(0, seed, Ordering::Relaxed, Ordering::Relaxed) {
494
+ Ok(_) => seed,
495
+ Err(v) => v,
496
+ };
497
+ }
498
+ let _ = cur;
499
+ let mut z = STATE
500
+ .fetch_add(0x9E3779B97F4A7C15, Ordering::Relaxed)
501
+ .wrapping_add(0x9E3779B97F4A7C15);
502
+ z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
503
+ z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
504
+ (z ^ (z >> 31)) as i64
505
+ }
506
+
507
+ /// The bytes of a `Str` value: `Str(data: Array[Byte], start: Int, len: Int)`
508
+ /// (see `plum-std/Str.plum`), a view `[start, start + len)` of a packed byte array.
509
+ pub fn strBytes(
510
+ store: &mut impl wasmtime::AsContextMut,
511
+ s: &wasmtime::Rooted<wasmtime::AnyRef>,
512
+ ) -> wasmtime::Result<Vec<u8>> {
513
+ let st = s.unwrap_struct(&*store)?;
514
+ let wasmtime::Val::AnyRef(Some(data)) = st.field(&mut *store, 0)? else {
515
+ return Err(wasmtime::Error::msg("Str.data is not a byte array"));
516
+ };
517
+ let (wasmtime::Val::I64(start), wasmtime::Val::I64(len)) =
518
+ (st.field(&mut *store, 1)?, st.field(&mut *store, 2)?)
519
+ else {
520
+ return Err(wasmtime::Error::msg("Str.start/len are not Int"));
521
+ };
522
+ let (start, len) = (usize::try_from(start)?, usize::try_from(len)?);
523
+ let end = start
524
+ .checked_add(len)
525
+ .ok_or_else(|| wasmtime::Error::msg("Str view out of range"))?;
526
+ let arr = data.unwrap_array(&*store)?;
527
+ let mut buf = vec![0u8; arr.len(&*store)? as usize];
528
+ arr.copy_to_i8_slice(&mut *store, &mut buf)?;
529
+ buf.get(start..end)
530
+ .map(|b| b.to_vec())
531
+ .ok_or_else(|| wasmtime::Error::msg("Str view out of range"))
532
+ }
533
+
534
+ /// Reads a `Str` argument's bytes into a Rust `String`, for the
535
+ /// `rawReadFile`/`rawWriteFile`/`rawExists`/`rawMkdir`/`rawRemove` host
536
+ /// imports below — factored out since every filesystem import needs to read
537
+ /// at least one `Str` arg.
538
+ fn readStrArg(
539
+ caller: &mut wasmtime::Caller<'_, ()>,
540
+ val: &wasmtime::Val,
541
+ ) -> wasmtime::Result<String> {
542
+ let wasmtime::Val::AnyRef(Some(s)) = val else {
543
+ return Err(wasmtime::Error::msg("expected a Str argument"));
544
+ };
545
+ let buf = strBytes(caller, s)?;
546
+ Ok(String::from_utf8_lossy(&buf).into_owned())
547
+ }
548
+
549
+ /// Builds a new `Str` value from raw bytes, to return from `rawReadFile` —
550
+ /// the host-side counterpart to `readStrArg`. Allocates the raw `array<i8>`
551
+ /// (needs the import's own reported `FuncType` to find the concrete nested
552
+ /// array type — a generic `ArrayRef` type would be the wrong, unrelated top
553
+ /// array type), then wraps it in a fresh `Str` struct, mirroring `str.plum`'s real shape.
554
+ fn makeStrResult(
555
+ mut caller: impl wasmtime::AsContextMut,
556
+ func_ty: &wasmtime::FuncType,
557
+ bytes: &[u8],
558
+ ) -> wasmtime::Result<wasmtime::Rooted<wasmtime::AnyRef>> {
559
+ let result_ty = func_ty
560
+ .results()
561
+ .next()
562
+ .ok_or_else(|| wasmtime::Error::msg("expected a Str-returning function"))?;
563
+ let wasmtime::ValType::Ref(ref_ty) = result_ty else {
564
+ return Err(wasmtime::Error::msg("expected a Str (ref) return type"));
565
+ };
566
+ let str_ty = ref_ty
567
+ .heap_type()
568
+ .as_concrete_struct()
569
+ .ok_or_else(|| wasmtime::Error::msg("expected a concrete struct return type"))?
570
+ .clone();
571
+ let array_ty = str_ty
572
+ .field(0)
573
+ .and_then(|f| match f.element_type() {
574
+ wasmtime::StorageType::ValType(wasmtime::ValType::Ref(rt)) => {
575
+ rt.heap_type().as_concrete_array().cloned()
576
+ }
577
+ _ => None,
578
+ })
579
+ .ok_or_else(|| {
580
+ wasmtime::Error::msg("expected Str.data to be a concrete byte array type")
581
+ })?;
582
+
583
+ let array_pre = wasmtime::ArrayRefPre::new(&mut caller, array_ty);
584
+ let arr = wasmtime::ArrayRef::new_from_i8_slice(&mut caller, &array_pre, bytes)?;
585
+
586
+ let str_pre = wasmtime::StructRefPre::new(&mut caller, str_ty);
587
+ let str_val = wasmtime::StructRef::new(
588
+ &mut caller,
589
+ &str_pre,
590
+ &[
591
+ wasmtime::Val::AnyRef(Some(arr.to_anyref())),
592
+ wasmtime::Val::I64(0),
593
+ wasmtime::Val::I64(bytes.len() as i64),
594
+ ],
595
+ )?;
596
+ Ok(str_val.to_anyref())
597
+ }
598
+
599
+ fn nowMillis() -> i64 {
600
+ std::time::SystemTime::now()
601
+ .duration_since(std::time::UNIX_EPOCH)
602
+ .map(|d| d.as_millis() as i64)
603
+ .unwrap_or(0)
604
+ }
605
+
606
+
607
+ #[cfg(test)]
608
+ mod tests {
609
+ use super::*;
610
+
611
+ const WAT: &str = r#"(module
612
+ (type $arr (array (mut i8)))
613
+ (type $str (struct (field (mut (ref $arr))) (field (mut i64)) (field (mut i64))))
614
+ (import "plum" "printLn" (func (param (ref $str))))
615
+ (import "plum" "rawNowMillis" (func (result f64))))"#;
616
+
617
+ #[test]
618
+ fn random_i64_never_repeats_back_to_back() {
619
+ let mut prev = randomI64();
620
+ for _ in 0..100_000 {
621
+ let next = randomI64();
622
+ assert_ne!(prev, next);
623
+ prev = next;
624
+ }
625
+ }
626
+
627
+ #[test]
628
+ fn strBytesRejectsBadViews() {
629
+ let wat = r#"(module
630
+ (type $arr (array (mut i8)))
631
+ (type $str (struct (field (mut (ref $arr))) (field (mut i64)) (field (mut i64))))
632
+ (func (export "mk") (param i64 i64) (result (ref $str))
633
+ (struct.new $str (array.new_fixed $arr 2 (i32.const 65) (i32.const 66)) (local.get 0) (local.get 1))))"#;
634
+ let engine = engine();
635
+ let module = wasmtime::Module::new(&engine, wat::parse_str(wat).unwrap()).unwrap();
636
+ let mut store = wasmtime::Store::new(&engine, ());
637
+ let inst = wasmtime::Instance::new(&mut store, &module, &[]).unwrap();
638
+ let mk = inst.get_func(&mut store, "mk").unwrap();
639
+ let mut read = |s: i64, l: i64| {
640
+ let mut out = [wasmtime::Val::I32(0)];
641
+ mk.call(&mut store, &[wasmtime::Val::I64(s), wasmtime::Val::I64(l)], &mut out).unwrap();
642
+ let wasmtime::Val::AnyRef(Some(r)) = &out[0] else { panic!() };
643
+ strBytes(&mut store, r)
644
+ };
645
+ assert_eq!(read(1, 1).unwrap(), b"B");
646
+ assert!(read(1, 5).is_err());
647
+ assert!(read(-1, 1).is_err());
648
+ assert!(read(i64::MAX, i64::MAX).is_err());
649
+ }
650
+
651
+ #[test]
652
+ fn hostImportsOneExtern() {
653
+ let engine = engine();
654
+ let module = wasmtime::Module::new(&engine, wat::parse_str(WAT).unwrap()).unwrap();
655
+ let mut store = wasmtime::Store::new(&engine, ());
656
+ assert_eq!(hostImports(&mut store, &module, Caps::default()).unwrap().len(), 2);
657
+ assert_eq!(hostImports(&mut store, &module, Caps::all()).unwrap().len(), 2);
658
+ }
659
+ }
plum-lsp/src/completion.rs CHANGED
@@ -20,7 +20,7 @@ use std::collections::HashMap;
20
20
  use std::path::Path;
21
21
 
22
22
  use plum_checker::types::{PlumType, TypeEnv, TypeScheme};
23
- use plum_checker::{inferExpr, methodReceiverName, plumTypeFromAst, CheckCtx};
23
+ use plum_checker::{inferExpr, methodReceiverName, plumTypeFromAst, plumTypeFromAstG, CheckCtx};
24
24
  use plum_core::ast;
25
25
  use tower_lsp::lsp_types::*;
26
26
 
@@ -327,6 +327,22 @@ pub fn enclosingFn(source: &ast::Source, cursor_line: usize) -> Option<&ast::Fn>
327
327
  .max_by_key(|f| f.line)
328
328
  }
329
329
 
330
+ /// A parameter's type with generic args kept (`List[Int]` -> `TApp`, `T` -> `TParam`).
331
+ fn paramPlumType(pt: &ast::ParamType) -> PlumType {
332
+ match pt {
333
+ ast::ParamType::Type(t) | ast::ParamType::Seq(t) => plumTypeFromAstG(t),
334
+ ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(plumTypeFromAstG(t))),
335
+ ast::ParamType::Fn(params, ret) => PlumType::TFun(
336
+ params.iter().map(plumTypeFromAstG).collect(),
337
+ Box::new(
338
+ ret.as_ref()
339
+ .map(|r| plumTypeFromAstG(r))
340
+ .unwrap_or(PlumType::TUnit),
341
+ ),
342
+ ),
343
+ }
344
+ }
345
+
330
346
  /// Best-effort local variable types for `f`'s body, built by walking every
331
347
  /// `Stmt::Assign` (at any nesting depth) top-to-bottom and re-inferring with
332
348
  /// `inferExpr` each time — see this module's doc comment for the tradeoffs.
@@ -342,18 +358,7 @@ fn localVarTypes(f: &ast::Fn, global: &TypeEnv, ctx: &CheckCtx) -> HashMap<Strin
342
358
  );
343
359
  }
344
360
  for p in &f.params {
345
- let ty = match &p.ty {
361
+ let ty = paramPlumType(&p.ty);
346
- ast::ParamType::Type(t) | ast::ParamType::Seq(t) => plumTypeFromAst(t),
347
- ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(plumTypeFromAst(t))),
348
- ast::ParamType::Fn(params, ret) => PlumType::TFun(
349
- params.iter().map(plumTypeFromAst).collect(),
350
- Box::new(
351
- ret.as_ref()
352
- .map(|r| plumTypeFromAst(r))
353
- .unwrap_or(PlumType::TUnit),
354
- ),
355
- ),
356
- };
357
362
  env.insert(p.name.clone(), ty);
358
363
  }
359
364
  if let ast::FnBody::Block(block) = &f.body {
@@ -389,7 +394,7 @@ fn walkStmt(
389
394
  }
390
395
  // `name: Type := value`: the annotation is the local's type.
391
396
  if let Some(ann) = a.ty.as_ref().filter(|_| a.declare) {
392
- env.insert(name.clone(), plum_checker::plumTypeFromAstG(ann));
397
+ env.insert(name.clone(), plumTypeFromAstG(ann));
393
398
  } else if let Ok(t) = inferExpr(value, &combined, ctx) {
394
399
  env.insert(name.clone(), t);
395
400
  }
@@ -673,3 +678,35 @@ pub fn importInsertionEdit(text: &str, import_path: &str) -> TextEdit {
673
678
  new_text: format!("import {import_path}\n"),
674
679
  }
675
680
  }
681
+
682
+ #[cfg(test)]
683
+ mod tests {
684
+ use super::*;
685
+
686
+ fn ty(name: &str, generics: Vec<ast::Type>) -> ast::Type {
687
+ ast::Type {
688
+ name: name.to_string(),
689
+ generics,
690
+ }
691
+ }
692
+
693
+ #[test]
694
+ fn paramPlumTypeKeepsGenericArgs() {
695
+ let list = ty("List", vec![ty("Int", vec![])]);
696
+ assert_eq!(
697
+ paramPlumType(&ast::ParamType::Type(list.clone())),
698
+ PlumType::TApp("List".into(), vec![PlumType::TInt])
699
+ );
700
+ assert_eq!(
701
+ paramPlumType(&ast::ParamType::Variadic(list)),
702
+ PlumType::TVariadic(Box::new(PlumType::TApp(
703
+ "List".into(),
704
+ vec![PlumType::TInt]
705
+ )))
706
+ );
707
+ assert_eq!(
708
+ paramPlumType(&ast::ParamType::Type(ty("T", vec![]))),
709
+ PlumType::TParam("T".into())
710
+ );
711
+ }
712
+ }
plum-runtime/Cargo.toml CHANGED
@@ -11,7 +11,7 @@ path = "src/main.rs"
11
11
 
12
12
  [dependencies]
13
13
  wasmtime = "47"
14
- ureq = "2.10"
14
+ plum-host = { path = "../plum-host" }
15
15
 
16
16
  # Stand on its own rather than joining the parent directory's workspace: `plum
17
17
  # build` compiles this crate on demand with PLUM_WASM_PATH set, and it would
plum-runtime/src/main.rs CHANGED
@@ -8,370 +8,15 @@
8
8
  // dependency on a `wasmtime` install or a separate .wasm file.
9
9
  const WASM_BYTES: &[u8] = include_bytes!(env!("PLUM_WASM_PATH"));
10
10
 
11
- /// Builds the `Extern`s that satisfy a compiled Plum module's host imports (today,
12
- /// only `plum::printLn`) in declaration order, matching each one's exact `FuncType`
13
- /// (its `Str` param is a concrete wasm-gc array type, so the host function must be
14
- /// built from the module's own reported type rather than a generic `Rooted<ArrayRef>`
15
- /// wrapper, which would carry the wrong, unrelated top-array type and fail to
16
- /// instantiate).
17
- fn hostImports(store: &mut wasmtime::Store<()>, module: &wasmtime::Module) -> Vec<wasmtime::Extern> {
18
- module.imports().map(|imp| {
19
- match (imp.module(), imp.name()) {
20
- ("plum", "printLn") => {
21
- let func_ty = imp.ty().func().cloned().expect("plum::printLn import should be a function");
22
- wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, _results| {
23
- let wasmtime::Val::AnyRef(Some(s)) = &params[0] else {
24
- return Err(wasmtime::Error::msg("printLn expects a Str argument"));
25
- };
26
- let buf = strBytes(&mut caller, s)?;
27
- println!("{}", String::from_utf8_lossy(&buf));
28
- Ok(())
29
- }))
30
- }
31
- ("plum", "crash") => {
32
- let func_ty = imp.ty().func().cloned().expect("plum::crash import should be a function");
33
- wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, _results| {
34
- let wasmtime::Val::AnyRef(Some(s)) = &params[0] else {
35
- return Err(wasmtime::Error::msg("crash expects a Str argument"));
36
- };
37
- let buf = strBytes(&mut caller, s)?;
38
- eprintln!("crash: {}", String::from_utf8_lossy(&buf));
39
- Ok(())
40
- }))
41
- }
42
- ("plum", "rawRandomHalf") => {
43
- let func_ty = imp.ty().func().cloned().expect("plum::rawRandomHalf import should be a function");
44
- wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |_caller, _params, results| {
45
- results[0] = wasmtime::Val::I32((randomI64() >> 32) as i32);
46
- Ok(())
47
- }))
48
- }
49
- ("plum", "rawNowMillis") => {
50
- let func_ty = imp.ty().func().cloned().expect("plum::rawNowMillis import should be a function");
51
- wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |_caller, _params, results| {
52
- results[0] = wasmtime::Val::F64((nowMillis() as f64).to_bits());
53
- Ok(())
54
- }))
55
- }
56
- ("plum", "rawReadFile") => {
57
- let func_ty = imp.ty().func().cloned().expect("plum::rawReadFile import should be a function");
58
- wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty.clone(), move |mut caller, params, results| {
59
- let path = readStrArg(&mut caller, &params[0])?;
60
- let bytes = std::fs::read(&path).unwrap_or_default();
61
- results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(&mut caller, &func_ty, &bytes)?));
62
- Ok(())
63
- }))
64
- }
65
- ("plum", "rawWriteFile") => {
66
- let func_ty = imp.ty().func().cloned().expect("plum::rawWriteFile import should be a function");
67
- wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
68
- let path = readStrArg(&mut caller, &params[0])?;
69
- let data = readStrArg(&mut caller, &params[1])?;
70
- results[0] = wasmtime::Val::I32(std::fs::write(&path, &data).is_ok() as i32);
71
- Ok(())
72
- }))
73
- }
74
- ("plum", "rawExists") => {
75
- let func_ty = imp.ty().func().cloned().expect("plum::rawExists import should be a function");
76
- wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
77
- let path = readStrArg(&mut caller, &params[0])?;
78
- // See the identical `rawExists` in plum-cli for why this uses
79
- // `symlink_metadata` rather than `Path::exists`.
80
- results[0] = wasmtime::Val::I32(std::fs::symlink_metadata(&path).is_ok() as i32);
81
- Ok(())
82
- }))
83
- }
84
- ("plum", "rawMkdir") => {
85
- let func_ty = imp.ty().func().cloned().expect("plum::rawMkdir import should be a function");
86
- wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
87
- let path = readStrArg(&mut caller, &params[0])?;
88
- results[0] = wasmtime::Val::I32(std::fs::create_dir_all(&path).is_ok() as i32);
89
- Ok(())
90
- }))
91
- }
92
- ("plum", "rawRemove") => {
93
- let func_ty = imp.ty().func().cloned().expect("plum::rawRemove import should be a function");
94
- wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
95
- let path = readStrArg(&mut caller, &params[0])?;
96
- results[0] = wasmtime::Val::I32(std::fs::remove_file(&path).is_ok() as i32);
97
- Ok(())
98
- }))
99
- }
100
- ("plum", "rawRemoveDir") => {
101
- let func_ty = imp.ty().func().cloned().expect("plum::rawRemoveDir import should be a function");
102
- wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
103
- let path = readStrArg(&mut caller, &params[0])?;
104
- results[0] = wasmtime::Val::I32(std::fs::remove_dir(&path).is_ok() as i32);
105
- Ok(())
106
- }))
107
- }
108
- ("plum", "rawRename") => {
109
- let func_ty = imp.ty().func().cloned().expect("plum::rawRename import should be a function");
110
- wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
111
- let old_path = readStrArg(&mut caller, &params[0])?;
112
- let new_path = readStrArg(&mut caller, &params[1])?;
113
- results[0] = wasmtime::Val::I32(std::fs::rename(&old_path, &new_path).is_ok() as i32);
114
- Ok(())
115
- }))
116
- }
117
- ("plum", "rawTruncate") => {
118
- let func_ty = imp.ty().func().cloned().expect("plum::rawTruncate import should be a function");
119
- wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
120
- let path = readStrArg(&mut caller, &params[0])?;
121
- let wasmtime::Val::I32(len) = params[1] else {
122
- return Err(wasmtime::Error::msg("rawTruncate expects an Int length argument"));
123
- };
124
- let ok = std::fs::OpenOptions::new().write(true).open(&path)
125
- .and_then(|f| f.set_len(len.max(0) as u64)).is_ok();
126
- results[0] = wasmtime::Val::I32(ok as i32);
127
- Ok(())
128
- }))
129
- }
130
- ("plum", "rawLink") => {
131
- let func_ty = imp.ty().func().cloned().expect("plum::rawLink import should be a function");
132
- wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
133
- let existing_path = readStrArg(&mut caller, &params[0])?;
134
- let new_path = readStrArg(&mut caller, &params[1])?;
135
- results[0] = wasmtime::Val::I32(std::fs::hard_link(&existing_path, &new_path).is_ok() as i32);
136
- Ok(())
137
- }))
138
- }
139
- ("plum", "rawSymlink") => {
140
- let func_ty = imp.ty().func().cloned().expect("plum::rawSymlink import should be a function");
141
- wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
142
- let target = readStrArg(&mut caller, &params[0])?;
143
- let path = readStrArg(&mut caller, &params[1])?;
144
- #[cfg(unix)]
145
- let ok = std::os::unix::fs::symlink(&target, &path).is_ok();
146
- #[cfg(not(unix))]
147
- let ok = false;
148
- results[0] = wasmtime::Val::I32(ok as i32);
149
- Ok(())
150
- }))
151
- }
152
- ("plum", "rawReadlink") => {
153
- let func_ty = imp.ty().func().cloned().expect("plum::rawReadlink import should be a function");
154
- wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty.clone(), move |mut caller, params, results| {
155
- let path = readStrArg(&mut caller, &params[0])?;
156
- let target = std::fs::read_link(&path).ok()
157
- .map(|p| p.to_string_lossy().into_owned()).unwrap_or_default();
158
- results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(&mut caller, &func_ty, target.as_bytes())?));
159
- Ok(())
160
- }))
161
- }
162
- ("plum", "rawRealpath") => {
163
- let func_ty = imp.ty().func().cloned().expect("plum::rawRealpath import should be a function");
164
- wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty.clone(), move |mut caller, params, results| {
165
- let path = readStrArg(&mut caller, &params[0])?;
166
- let resolved = std::fs::canonicalize(&path).ok()
167
- .map(|p| p.to_string_lossy().into_owned()).unwrap_or_default();
168
- results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(&mut caller, &func_ty, resolved.as_bytes())?));
169
- Ok(())
170
- }))
171
- }
172
- ("plum", "rawChmod") => {
173
- let func_ty = imp.ty().func().cloned().expect("plum::rawChmod import should be a function");
174
- wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
175
- let path = readStrArg(&mut caller, &params[0])?;
176
- let wasmtime::Val::I32(mode) = params[1] else {
177
- return Err(wasmtime::Error::msg("rawChmod expects an Int mode argument"));
178
- };
179
- #[cfg(unix)]
180
- let ok = {
181
- use std::os::unix::fs::PermissionsExt;
182
- std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode as u32)).is_ok()
183
- };
184
- #[cfg(not(unix))]
185
- let ok = false;
186
- results[0] = wasmtime::Val::I32(ok as i32);
187
- Ok(())
188
- }))
189
- }
190
- ("plum", "rawReadDir") => {
191
- let func_ty = imp.ty().func().cloned().expect("plum::rawReadDir import should be a function");
192
- wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty.clone(), move |mut caller, params, results| {
193
- let path = readStrArg(&mut caller, &params[0])?;
194
- let joined = std::fs::read_dir(&path).ok().map(|entries| {
195
- entries.filter_map(|e| e.ok())
196
- .map(|e| e.file_name().to_string_lossy().into_owned())
197
- .collect::<Vec<_>>()
198
- .join("\n")
199
- }).unwrap_or_default();
200
- results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(&mut caller, &func_ty, joined.as_bytes())?));
201
- Ok(())
202
- }))
203
- }
204
- ("plum", "rawHttpRequest") => {
205
- let func_ty = imp.ty().func().cloned().expect("plum::rawHttpRequest import should be a function");
206
- wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty.clone(), move |mut caller, params, results| {
207
- let method = readStrArg(&mut caller, &params[0])?;
208
- let url = readStrArg(&mut caller, &params[1])?;
209
- let headers = readStrArg(&mut caller, &params[2])?;
210
- let body = readStrArg(&mut caller, &params[3])?;
211
- let raw = performHttpRequest(&method, &url, &headers, &body);
212
- results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(&mut caller, &func_ty, raw.as_bytes())?));
213
- Ok(())
214
- }))
215
- }
216
- (m, n) => panic!("unsupported host import {m}::{n}"),
217
- }
218
- }).collect()
219
- }
220
-
221
- /// See the identical helper in `plum-cli/src/main.rs` — this crate is built
222
- /// fresh per `plum build` invocation, not linked against `plum-cli`, so the
223
- /// logic is duplicated rather than shared.
224
- fn performHttpRequest(method: &str, url: &str, headers: &str, body: &str) -> String {
225
- let agent = ureq::Agent::new();
226
- let mut req = agent.request(method, url);
227
- for line in headers.split('\n') {
228
- if let Some((k, v)) = line.split_once(": ") {
229
- req = req.set(k, v);
230
- }
231
- }
232
- let result = if body.is_empty() { req.call() } else { req.send_string(body) };
233
- match result {
234
- Ok(resp) | Err(ureq::Error::Status(_, resp)) => {
235
- let status = resp.status();
236
- let mut header_lines = String::new();
237
- for name in resp.headers_names() {
238
- if let Some(v) = resp.header(&name) {
239
- if !header_lines.is_empty() {
240
- header_lines.push('\n');
241
- }
242
- header_lines.push_str(&format!("{name}: {v}"));
243
- }
244
- }
245
- let body_text = resp.into_string().unwrap_or_default();
246
- format!("1\x01{status}\x01{header_lines}\x00{body_text}")
247
- }
248
- Err(e) => format!("0\x01\x01{e}\x00"),
249
- }
250
- }
251
-
252
- /// See the identical helper in `plum-cli/src/main.rs` — this crate is built
253
- /// fresh per `plum build` invocation, not linked against `plum-cli`, so the
254
- /// logic is duplicated rather than shared.
255
- fn randomI64() -> i64 {
256
- use std::sync::atomic::{AtomicU64, Ordering};
257
- // splitmix64 over a process-wide counter: seeded once from the clock and
258
- // an address, then advanced by a fixed odd increment per call, so
259
- // back-to-back calls can never repeat.
260
- static STATE: AtomicU64 = AtomicU64::new(0);
261
- let mut cur = STATE.load(Ordering::Relaxed);
262
- if cur == 0 {
263
- let nanos = std::time::SystemTime::now()
264
- .duration_since(std::time::UNIX_EPOCH)
265
- .map(|d| d.as_nanos() as u64)
266
- .unwrap_or(0);
267
- let seed = (nanos ^ (&STATE as *const AtomicU64 as u64)) | 1;
268
- cur = match STATE.compare_exchange(0, seed, Ordering::Relaxed, Ordering::Relaxed) {
269
- Ok(_) => seed,
270
- Err(v) => v,
271
- };
272
- }
273
- let _ = cur;
274
- let mut z = STATE
275
- .fetch_add(0x9E3779B97F4A7C15, Ordering::Relaxed)
276
- .wrapping_add(0x9E3779B97F4A7C15);
277
- z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
278
- z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
279
- (z ^ (z >> 31)) as i64
280
- }
281
-
282
- /// The bytes of a `Str` value: `Str(data: Array[Byte], start: Int, len: Int)`
283
- /// (see `plum-std/Str.plum`), a view `[start, start + len)` of a packed byte array.
284
- fn strBytes(
285
- mut store: impl wasmtime::AsContextMut,
286
- s: &wasmtime::Rooted<wasmtime::AnyRef>,
287
- ) -> wasmtime::Result<Vec<u8>> {
288
- let st = s.unwrap_struct(&store)?;
289
- let wasmtime::Val::AnyRef(Some(data)) = st.field(&mut store, 0)? else {
290
- return Err(wasmtime::Error::msg("Str.data is not a byte array"));
291
- };
292
- let start = st.field(&mut store, 1)?.unwrap_i64() as usize;
293
- let len = st.field(&mut store, 2)?.unwrap_i64() as usize;
294
- let arr = data.unwrap_array(&store)?;
295
- let mut buf = vec![0u8; arr.len(&store)? as usize];
296
- arr.copy_to_i8_slice(&mut store, &mut buf)?;
297
- Ok(buf[start..start + len].to_vec())
298
- }
299
-
300
- fn readStrArg(caller: &mut wasmtime::Caller<'_, ()>, val: &wasmtime::Val) -> wasmtime::Result<String> {
301
- let wasmtime::Val::AnyRef(Some(s)) = val else {
302
- return Err(wasmtime::Error::msg("expected a Str argument"));
303
- };
304
- let buf = strBytes(&mut *caller, s)?;
305
- Ok(String::from_utf8_lossy(&buf).into_owned())
306
- }
307
-
308
- /// Builds a new `Str` value from raw bytes — allocates the raw `array<i8>`
309
- /// (needs the import's own reported `FuncType` to find the concrete nested
310
- /// array type — a generic `ArrayRef` type would be the wrong, unrelated top
311
- /// array type), then wraps it in a fresh `Str` struct, mirroring `str.plum`'s real shape.
312
- fn makeStrResult(
313
- mut caller: impl wasmtime::AsContextMut,
314
- func_ty: &wasmtime::FuncType,
315
- bytes: &[u8],
316
- ) -> wasmtime::Result<wasmtime::Rooted<wasmtime::AnyRef>> {
317
- let result_ty = func_ty.results().next()
318
- .ok_or_else(|| wasmtime::Error::msg("expected a Str-returning function"))?;
319
- let wasmtime::ValType::Ref(ref_ty) = result_ty else {
320
- return Err(wasmtime::Error::msg("expected a Str (ref) return type"));
321
- };
322
- let str_ty = ref_ty.heap_type().as_concrete_struct()
323
- .ok_or_else(|| wasmtime::Error::msg("expected a concrete struct return type"))?
324
- .clone();
325
- let array_ty = str_ty
326
- .field(0)
327
- .and_then(|f| match f.element_type() {
328
- wasmtime::StorageType::ValType(wasmtime::ValType::Ref(rt)) => {
329
- rt.heap_type().as_concrete_array().cloned()
330
- }
331
- _ => None,
332
- })
333
- .ok_or_else(|| wasmtime::Error::msg("expected Str.data to be a concrete byte array type"))?;
334
-
335
- let array_pre = wasmtime::ArrayRefPre::new(&mut caller, array_ty);
336
- let arr = wasmtime::ArrayRef::new_from_i8_slice(&mut caller, &array_pre, bytes)?;
337
-
338
- let str_pre = wasmtime::StructRefPre::new(&mut caller, str_ty);
339
- let str_val = wasmtime::StructRef::new(
340
- &mut caller,
341
- &str_pre,
342
- &[
343
- wasmtime::Val::AnyRef(Some(arr.to_anyref())),
344
- wasmtime::Val::I64(0),
345
- wasmtime::Val::I64(bytes.len() as i64),
346
- ],
347
- )?;
348
- Ok(str_val.to_anyref())
349
- }
350
-
351
- fn nowMillis() -> i64 {
352
- std::time::SystemTime::now()
353
- .duration_since(std::time::UNIX_EPOCH)
354
- .map(|d| d.as_millis() as i64)
355
- .unwrap_or(0)
356
- }
357
-
358
11
  fn main() {
359
12
  // GC + typed function references are required to load modules containing
360
13
  // Plum's structs/enums/strings/closures (see docs/superpowers/plans/2026-07-25-wasm-gc-migration.md).
361
- let mut config = wasmtime::Config::new();
14
+ let engine = plum_host::engine();
362
- config.wasm_gc(true);
363
- config.wasm_function_references(true);
364
- config.wasm_tail_call(true);
365
- config.max_wasm_stack(4 << 20);
366
- // Start the GC heap at 64 MiB (reserved, committed lazily) so list-heavy programs
367
- // skip the early full-heap copies of each doubling growth (docs/plans/2026-11-03-gc-report.md).
368
- config.gc_heap_initial_size(64 << 20);
369
- config.async_stack_size(8 << 20);
370
- let engine = wasmtime::Engine::new(&config).expect("engine with GC config should construct");
371
15
 
372
16
  let module = wasmtime::Module::new(&engine, WASM_BYTES).expect("embedded wasm should be valid");
373
17
  let mut store = wasmtime::Store::new(&engine, ());
374
- let imports = hostImports(&mut store, &module);
18
+ let imports = plum_host::hostImports(&mut store, &module, plum_host::Caps::all())
19
+ .expect("host imports should resolve");
375
20
  let instance = wasmtime::Instance::new(&mut store, &module, &imports).expect("module should instantiate");
376
21
 
377
22
  // `main` returns `i64` unless it's declared `Unit` (no return value), in
@@ -395,16 +40,3 @@ fn main() {
395
40
  std::process::exit(1);
396
41
  }
397
42
  }
398
-
399
- #[cfg(test)]
400
- mod rng_tests {
401
- #[test]
402
- fn random_i64_never_repeats_back_to_back() {
403
- let mut prev = super::randomI64();
404
- for _ in 0..100_000 {
405
- let next = super::randomI64();
406
- assert_ne!(prev, next);
407
- prev = next;
408
- }
409
- }
410
- }
plum-test-support/src/lib.rs CHANGED
@@ -36,6 +36,21 @@ pub fn parse(src: &str) -> Source {
36
36
  ast
37
37
  }
38
38
 
39
+ /// Parses `src` with `parseSourceRaw` (no lowering of sugar); panics with the tree if it has syntax errors.
40
+ pub fn parseRawSource(src: &str) -> Source {
41
+ let mut parser = tree_sitter::Parser::new();
42
+ parser
43
+ .set_language(&tree_sitter_plum::LANGUAGE.into())
44
+ .unwrap();
45
+ let tree = parser.parse(src, None).unwrap();
46
+ assert!(
47
+ !tree.root_node().has_error(),
48
+ "parse error:\n{}",
49
+ tree.root_node().to_sexp()
50
+ );
51
+ plum_core::AstParser::new(src).parseSourceRaw(tree.root_node())
52
+ }
53
+
39
54
  static COUNTER: AtomicUsize = AtomicUsize::new(0);
40
55
 
41
56
  /// Loads `src` as `main.plum` through the real loader (imports resolve against the repo root).
@@ -136,6 +151,17 @@ mod tests {
136
151
  parse("fun main( ->");
137
152
  }
138
153
 
154
+ #[test]
155
+ fn parseRawSourceCounts() {
156
+ assert_eq!(parseRawSource("fun main() -> Int = 1").items.len(), 1);
157
+ }
158
+
159
+ #[test]
160
+ #[should_panic]
161
+ fn parseRawSourceRejectsSyntaxErrors() {
162
+ parseRawSource("fun main( ->");
163
+ }
164
+
139
165
  #[test]
140
166
  fn runIntValue() {
141
167
  assert_eq!(runInt("t1", &format!("{PRE}fun main() -> Int = 40 + 2")), 42);
plum-wasm-codegen/src/lib.rs CHANGED
@@ -2573,6 +2573,11 @@ fn compileSourceInner(
2573
2573
  tc.visit(t);
2574
2574
  }
2575
2575
  }
2576
+ // The maps visited above iterate in hash order; fix the collected order so the
2577
+ // emitted type section does not depend on it.
2578
+ for v in [&mut tc.apps, &mut tc.funs, &mut tc.variadics, &mut tc.errs] {
2579
+ v.sort_by_cached_key(|t| format!("{:?}", t));
2580
+ }
2576
2581
 
2577
2582
  let mut module = WasmModule::new();
2578
2583
 
@@ -3046,6 +3051,8 @@ fn compileSourceInner(
3046
3051
 
3047
3052
  // Zero-capture trampolines for top-level functions used as values.
3048
3053
  let mut named_fn_values: HashMap<(String, PlumType), u32> = HashMap::new();
3054
+ let mut named_fn_refs: Vec<(String, PlumType)> = named_fn_refs.into_iter().collect();
3055
+ named_fn_refs.sort_by_cached_key(|(n, t)| (n.clone(), format!("{:?}", t)));
3049
3056
  for (name, fty) in &named_fn_refs {
3050
3057
  let sig = func_sigs
3051
3058
  .get(name)
@@ -4468,6 +4475,8 @@ struct Collector<'a> {
4468
4475
  named_set: std::collections::HashSet<String>,
4469
4476
  /// One scratch-local type per subject (usually one, more for `match a, b, ...`).
4470
4477
  match_scratch: HashMap<usize, Vec<PlumType>>,
4478
+ /// Keys of `match_scratch` in first-insertion order (local slots are allocated in this order).
4479
+ match_scratch_order: Vec<usize>,
4471
4480
  /// `CasePattern::Class` identity -> its scratch local. Covers EVERY constructor
4472
4481
  /// pattern, including top-level ones — under wasm-gc, matching `Next(v)` needs a
4473
4482
  /// `ref.cast` from the subject's static supertype down to the concrete variant
@@ -4484,6 +4493,12 @@ struct Collector<'a> {
4484
4493
  }
4485
4494
 
4486
4495
  impl<'a> Collector<'a> {
4496
+ fn addMatchScratch(&mut self, key: usize, types: Vec<PlumType>) {
4497
+ if self.match_scratch.insert(key, types).is_none() {
4498
+ self.match_scratch_order.push(key);
4499
+ }
4500
+ }
4501
+
4487
4502
  fn bind(&mut self, name: &str, ty: PlumType) {
4488
4503
  if self.named_set.insert(name.to_string()) {
4489
4504
  self.named.push((name.to_string(), ty));
@@ -4608,8 +4623,7 @@ impl<'a> Collector<'a> {
4608
4623
  self.ty(s)
4609
4624
  })
4610
4625
  .collect();
4611
- self.match_scratch
4612
- .insert(m as *const ast::Match as usize, subject_types.clone());
4626
+ self.addMatchScratch(m as *const ast::Match as usize, subject_types.clone());
4613
4627
  for case in &m.cases {
4614
4628
  for (pat, subject_ty) in case.patterns.iter().zip(subject_types.iter()) {
4615
4629
  self.collectPattern(pat, subject_ty);
@@ -4645,8 +4659,7 @@ impl<'a> Collector<'a> {
4645
4659
  } else {
4646
4660
  // A pattern test: the subject goes into a scratch local, then
4647
4661
  // the pattern binds exactly like a `match` arm's.
4648
- self.match_scratch
4649
- .insert(&**b as *const ast::BindExpr as usize, vec![ty.clone()]);
4662
+ self.addMatchScratch(&**b as *const ast::BindExpr as usize, vec![ty.clone()]);
4650
4663
  self.collectPattern(b.pattern(), &ty);
4651
4664
  }
4652
4665
  self.walkExpr(&b.value);
@@ -4847,6 +4860,7 @@ fn compileFnBodyImpl(
4847
4860
  named: Vec::new(),
4848
4861
  named_set: Default::default(),
4849
4862
  match_scratch: HashMap::new(),
4863
+ match_scratch_order: Vec::new(),
4850
4864
  nested_class_scratch: HashMap::new(),
4851
4865
  nested_class_scratch_types: Vec::new(),
4852
4866
  next_nested_class_slot: 0,
@@ -4893,7 +4907,8 @@ fn compileFnBodyImpl(
4893
4907
 
4894
4908
  let match_scratch_base = idx;
4895
4909
  let mut match_scratch_index: HashMap<usize, u32> = HashMap::new();
4896
- for (ptr, types) in collector.match_scratch.iter() {
4910
+ for ptr in collector.match_scratch_order.iter() {
4911
+ let types = &collector.match_scratch[ptr];
4897
4912
  match_scratch_index.insert(*ptr, idx - match_scratch_base);
4898
4913
  for ty in types {
4899
4914
  groups.push(plumTypeToValtype(ty));
@@ -8906,6 +8921,7 @@ fn compileClosureBody(
8906
8921
  named: Vec::new(),
8907
8922
  named_set: Default::default(),
8908
8923
  match_scratch: HashMap::new(),
8924
+ match_scratch_order: Vec::new(),
8909
8925
  nested_class_scratch: HashMap::new(),
8910
8926
  nested_class_scratch_types: Vec::new(),
8911
8927
  next_nested_class_slot: 0,
@@ -8939,7 +8955,8 @@ fn compileClosureBody(
8939
8955
 
8940
8956
  let match_scratch_base = idx;
8941
8957
  let mut match_scratch_index: HashMap<usize, u32> = HashMap::new();
8942
- for (ptr, types) in collector.match_scratch.iter() {
8958
+ for ptr in collector.match_scratch_order.iter() {
8959
+ let types = &collector.match_scratch[ptr];
8943
8960
  match_scratch_index.insert(*ptr, idx - match_scratch_base);
8944
8961
  for ty in types {
8945
8962
  groups.push(plumTypeToValtype(ty));