plum
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
- Cargo.toml +1 -1
- docs/plans/2026-11-04-compiler-cleanup-phase2a.progress.md +35 -0
- docs/plans/2026-11-04-compiler-cleanup-phase2a.report.json +339 -0
- docs/plans/2026-11-04-compiler-cleanup-phase2a.report.md +40 -0
- docs/plans/2026-11-04-compiler-cleanup-phase2a.results.md +38 -0
- plum-checker/src/lib.rs +4 -24
- plum-checker/tests/features/mutability.rs +1 -6
- plum-checker/tests/trait_fields_tests.rs +1 -8
- plum-checker/tests/traits_tests.rs +1 -8
- plum-cli/Cargo.toml +1 -1
- plum-cli/src/main.rs +5 -596
- plum-core/src/closure_spec.rs +19 -376
- plum-core/src/drive.rs +15 -454
- plum-core/src/forms.rs +10 -191
- plum-core/src/generics.rs +45 -0
- plum-core/src/lib.rs +3 -0
- plum-core/src/rename.rs +159 -0
- plum-core/src/reuse.rs +3 -22
- plum-core/src/simplify.rs +30 -369
- plum-core/src/tail.rs +13 -46
- plum-core/src/trmc.rs +30 -20
- plum-core/src/union_lower.rs +1 -12
- plum-core/src/uniqueness.rs +5 -20
- plum-core/src/visit.rs +336 -0
- plum-core/tests/closure_spec_test.rs +23 -0
- plum-core/tests/closure_types_parse_test.rs +1 -12
- plum-core/tests/drive_test.rs +1 -6
- plum-core/tests/generics_test.rs +34 -0
- plum-core/tests/match_body_parse_test.rs +1 -12
- plum-core/tests/override_parse_test.rs +1 -12
- plum-core/tests/parser_test.rs +5 -24
- plum-core/tests/record_embed_test.rs +1 -8
- plum-core/tests/record_init_test.rs +1 -8
- plum-core/tests/rename_test.rs +88 -0
- plum-core/tests/simplify_test.rs +1 -6
- plum-core/tests/traits_test.rs +1 -8
- plum-core/tests/trmc_test.rs +43 -0
- plum-core/tests/uniqueness_test.rs +1 -6
- plum-core/tests/visit_test.rs +88 -0
- plum-game/Cargo.toml +1 -0
- plum-game/src/host.rs +3 -29
- plum-host/Cargo.toml +12 -0
- plum-host/src/lib.rs +659 -0
- plum-lsp/src/completion.rs +51 -14
- plum-runtime/Cargo.toml +1 -1
- plum-runtime/src/main.rs +3 -371
- plum-test-support/src/lib.rs +26 -0
- 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
|
-
|
|
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
|
-
///
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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)) = ¶ms[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)) = ¶ms[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, ¶ms[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, ¶ms[0])?;
|
|
381
|
-
let data = readStrArg(&mut caller, ¶ms[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, ¶ms[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, ¶ms[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, ¶ms[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, ¶ms[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, ¶ms[0])?;
|
|
461
|
-
let new_path = readStrArg(&mut caller, ¶ms[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, ¶ms[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, ¶ms[0])?;
|
|
502
|
-
let new_path = readStrArg(&mut caller, ¶ms[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, ¶ms[0])?;
|
|
519
|
-
let path = readStrArg(&mut caller, ¶ms[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, ¶ms[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, ¶ms[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, ¶ms[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, ¶ms[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, ¶ms[0])?;
|
|
640
|
-
let url = readStrArg(&mut caller, ¶ms[1])?;
|
|
641
|
-
let headers = readStrArg(&mut caller, ¶ms[2])?;
|
|
642
|
-
let body = readStrArg(&mut caller, ¶ms[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 =
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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(&
|
|
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::
|
|
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
|
-
|
|
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
|
|
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
|
-
// ----------
|
|
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
|
-
|
|
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
|
-
|
|
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::
|
|
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() &&
|
|
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() &&
|
|
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() &&
|
|
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() &&
|
|
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 !
|
|
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) &&
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1610
|
+
let placeholder = Expr::Int(0);
|
|
1770
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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::
|
|
198
|
+
Expr::Catch(_) => {
|
|
199
|
-
|
|
199
|
+
for ch in crate::visit::childExprs(e) {
|
|
200
|
-
|
|
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
|
-
|
|
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
|
-
|
|
211
|
+
for ch in crate::visit::childExprs(e) {
|
|
226
|
-
if let Some(g) = &mut b.case.guard {
|
|
227
|
-
expr(cx,
|
|
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
|
-
|
|
222
|
+
expr(cx, ch, false);
|
|
252
|
-
| Expr::Int(_)
|
|
253
|
-
| Expr::Float(_)
|
|
254
|
-
| Expr::BoolLit(_)
|
|
255
|
-
|
|
223
|
+
}
|
|
256
|
-
| Expr::Var(_)
|
|
257
|
-
|
|
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:
|
|
81
|
+
let map: HashMap<String, Type> =
|
|
79
|
-
owner.generics.iter().map(|g| g.name.
|
|
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
|
-
(
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
204
|
+
fn bannedStmt(s: &Stmt) -> bool {
|
|
204
|
-
|
|
205
|
+
matches!(s, Stmt::Return(_) | Stmt::Todo | Stmt::Crash(_))
|
|
205
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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|
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
6
|
+
plum_test_support::parseRawSource(src)
|
|
11
7
|
}
|
|
12
8
|
|
|
13
|
-
fn
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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(&
|
|
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 =
|
|
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)) = ¶ms[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)) = ¶ms[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, ¶ms[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, ¶ms[0])?;
|
|
142
|
+
let data = readStrArg(&mut caller, ¶ms[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, ¶ms[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, ¶ms[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, ¶ms[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, ¶ms[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, ¶ms[0])?;
|
|
222
|
+
let new_path = readStrArg(&mut caller, ¶ms[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, ¶ms[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, ¶ms[0])?;
|
|
263
|
+
let new_path = readStrArg(&mut caller, ¶ms[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, ¶ms[0])?;
|
|
280
|
+
let path = readStrArg(&mut caller, ¶ms[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, ¶ms[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, ¶ms[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, ¶ms[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, ¶ms[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, ¶ms[0])?;
|
|
401
|
+
let url = readStrArg(&mut caller, ¶ms[1])?;
|
|
402
|
+
let headers = readStrArg(&mut caller, ¶ms[2])?;
|
|
403
|
+
let body = readStrArg(&mut caller, ¶ms[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 =
|
|
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(),
|
|
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
|
-
|
|
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)) = ¶ms[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)) = ¶ms[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, ¶ms[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, ¶ms[0])?;
|
|
69
|
-
let data = readStrArg(&mut caller, ¶ms[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, ¶ms[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, ¶ms[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, ¶ms[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, ¶ms[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, ¶ms[0])?;
|
|
112
|
-
let new_path = readStrArg(&mut caller, ¶ms[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, ¶ms[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, ¶ms[0])?;
|
|
134
|
-
let new_path = readStrArg(&mut caller, ¶ms[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, ¶ms[0])?;
|
|
143
|
-
let path = readStrArg(&mut caller, ¶ms[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, ¶ms[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, ¶ms[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, ¶ms[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, ¶ms[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, ¶ms[0])?;
|
|
208
|
-
let url = readStrArg(&mut caller, ¶ms[1])?;
|
|
209
|
-
let headers = readStrArg(&mut caller, ¶ms[2])?;
|
|
210
|
-
let body = readStrArg(&mut caller, ¶ms[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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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));
|