plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
529a106
— Peter John
2026-11-04T19:25:01+05:30
Compiler cleanup phase 2b: one front end, one compile entry, no Expr::Paren
- docs/plans/2026-11-04-compiler-cleanup-phase2b.progress.md +28 -0
- docs/plans/2026-11-04-compiler-cleanup-phase2b.report.json +269 -0
- docs/plans/2026-11-04-compiler-cleanup-phase2b.report.md +38 -0
- docs/plans/2026-11-04-compiler-cleanup-phase2b.results.md +19 -0
- plum-checker/src/lib.rs +67 -7
- plum-checker/tests/checker_tests.rs +1 -1
- plum-cli/src/main.rs +29 -16
- plum-core/src/ast.rs +1 -3
- plum-core/src/builtin_usage.rs +0 -1
- plum-core/src/closure_escape.rs +1 -1
- plum-core/src/closure_spec.rs +1 -22
- plum-core/src/ctors.rs +0 -1
- plum-core/src/drive.rs +12 -13
- plum-core/src/lib.rs +2 -0
- plum-core/src/loader.rs +3 -27
- plum-core/src/lower.rs +40 -0
- plum-core/src/parser.rs +5 -5
- plum-core/src/purity.rs +1 -2
- plum-core/src/reuse.rs +0 -1
- plum-core/src/simplify.rs +2 -38
- plum-core/src/tail.rs +0 -1
- plum-core/src/tests_lower.rs +265 -0
- plum-core/src/trmc.rs +0 -3
- plum-core/src/union_lower.rs +2 -3
- plum-core/src/unions.rs +1 -1
- plum-core/src/uniqueness.rs +6 -8
- plum-core/src/visit.rs +28 -10
- plum-core/src/whistle.rs +0 -1
- plum-core/tests/inline_variants_test.rs +2 -2
- plum-core/tests/loader_test.rs +31 -0
- plum-core/tests/lower_test.rs +19 -0
- plum-core/tests/parser_test.rs +11 -3
- plum-core/tests/tests_lower_test.rs +39 -0
- plum-core/tests/uniqueness_test.rs +0 -1
- plum-core/tests/visit_test.rs +0 -6
- plum-game/tests/host_test.rs +1 -2
- plum-lsp/src/completion.rs +1 -1
- plum-lsp/src/main.rs +6 -6
- plum-wasm-codegen/src/lib.rs +71 -305
- plum-wasm-codegen/tests/codegen_tests.rs +7 -2
docs/plans/2026-11-04-compiler-cleanup-phase2b.progress.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
- start bb462a8697f7486294f44f87a74a2f915eea82ca
|
|
2
|
+
- unblocked: dispatched T0 T1 T2 T6 at 18:13:17; waiting on fixes: T3 T4 T5 T7 T8
|
|
3
|
+
- Ruling: T0 skipped - test-cmd.json copied from 2a (no stubs needed); tasks sent with the prime in one message to save a round trip - cost: none
|
|
4
|
+
- unblocked: dispatched T3 T4 T5 T7 T8 T9 T10 at 18:14:29; waiting on fixes: none
|
|
5
|
+
- Ruling: accept predicted 47 min - codegen lib.rs edits are sequential (tests lowering, compile entry, Paren) - cost: time
|
|
6
|
+
- checkpoint T6 8f258a247c last walker helpers into visit
|
|
7
|
+
- Ruling: closure_spec's private walkRef/walkBlockRef switch to visit in T7 (T7 owns plum-core/src/*.rs) - cost: none
|
|
8
|
+
- checkpoint T1 f5405c9ca3 front-end entry
|
|
9
|
+
- checkpoint T10 c61a9566d8 checker takes fusion options
|
|
10
|
+
- Ruling: T2 Owns narrowed to the files it changed (parser.rs, loader_test.rs, LSP main/completion) - the 'except' globs were not parsed by checkpoint - cost: none
|
|
11
|
+
- checkpoint T2 d634497c6d parseSource runs the front end
|
|
12
|
+
- checkpoint T2 f92fc40647 parseSource runs the front end
|
|
13
|
+
- checkpoint T1 4ca8c7e354 front-end entry
|
|
14
|
+
- Ruling: T3 checkpointed from its files (tests_lower_test 3/3 pass) - its agent hung on a background job for 30+ min without reporting - cost: none
|
|
15
|
+
- checkpoint T3 85f7a1dba9 tests lowering in plum-core
|
|
16
|
+
- checkpoint T9 a9fbb02c24 codegen uses core tests lowering
|
|
17
|
+
- Note: stale desugarTestsToFns doc comments at plum-cli/src/main.rs:477 (T5 fixes) and plum-core/src/ast.rs:387 (T7 fixes)
|
|
18
|
+
- checkpoint T4 13d786ee00 one compile entry
|
|
19
|
+
- checkpoint T5 8c24ab957f compile callers
|
|
20
|
+
- Ruling: CLI keeps one OnceLock for the fusion flags (set once from argv; game.rs calls compileToWasm without options) - the checker thread-local still goes in T7 - cost: one CLI-local global
|
|
21
|
+
- Ruling: compile's empty file name + CLI nameFileInErrors text rewrite stay until phase 3's positioned errors - cost: one string hack survives a phase
|
|
22
|
+
- Ruling: FUSION_OPTIONS thread-local stays (fusion_alloc_test, e2e_mem/reuse, trmc_test set it via withFusionOptions); removing it means moving those tests to compile(opts) - deferred to phase 3 state work - cost: one checker thread-local survives
|
|
23
|
+
- Ruling: T7 owns uniqueness_test/visit_test (unparen removal) and the 3 failing tests' files for the follow-up fix F-T7-1
|
|
24
|
+
- checkpoint T7 946052a832 remove Expr::Paren and deprecated wrappers
|
|
25
|
+
- checkpoint T7 76cdc81c69 remove Expr::Paren and deprecated wrappers
|
|
26
|
+
- tests full passed=802 failed=0 skipped=0 seconds=170.5
|
|
27
|
+
- checkpoint T8 b0a3f49179 verify 2b
|
|
28
|
+
- Ruling: final-review incident [1] closed - loader_test.rs joined T2's Owns (ae151f9), T2 checkpointed it (d634497) and F-T2-1 re-checkpointed it with the inline-variant case (f92fc40); the +31 lines are single_file_equals_loader - cost: none
|
docs/plans/2026-11-04-compiler-cleanup-phase2b.report.json
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
{
|
|
2
|
+
"projection_note": "projection: fixed forecast saved at lint 2026-11-04T18:13:08 (plan edited since)",
|
|
3
|
+
"headline": {
|
|
4
|
+
"wall_min": {
|
|
5
|
+
"projected": 45.935,
|
|
6
|
+
"actual": 70.79
|
|
7
|
+
},
|
|
8
|
+
"cost_usd_agents": {
|
|
9
|
+
"projected": 0.723599,
|
|
10
|
+
"actual": 2.656721
|
|
11
|
+
},
|
|
12
|
+
"task_min": {
|
|
13
|
+
"projected": 53.418,
|
|
14
|
+
"actual": 73.672
|
|
15
|
+
},
|
|
16
|
+
"agents": {
|
|
17
|
+
"projected": 8,
|
|
18
|
+
"actual": 9
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"feature": null,
|
|
22
|
+
"cost_split": {
|
|
23
|
+
"agents_usd": 2.1839,
|
|
24
|
+
"rework_usd": 0.472821,
|
|
25
|
+
"coordinator_usd": 24.04081,
|
|
26
|
+
"total_usd": 26.697531
|
|
27
|
+
},
|
|
28
|
+
"concurrency": {
|
|
29
|
+
"peak": 3,
|
|
30
|
+
"projected_peak": 2
|
|
31
|
+
},
|
|
32
|
+
"critical_path": {
|
|
33
|
+
"actual": [
|
|
34
|
+
"T1",
|
|
35
|
+
"T3",
|
|
36
|
+
"T4",
|
|
37
|
+
"T5",
|
|
38
|
+
"T7",
|
|
39
|
+
"T8",
|
|
40
|
+
"T9"
|
|
41
|
+
],
|
|
42
|
+
"projected": [
|
|
43
|
+
"T1",
|
|
44
|
+
"T3",
|
|
45
|
+
"T4",
|
|
46
|
+
"T5",
|
|
47
|
+
"T7",
|
|
48
|
+
"T8"
|
|
49
|
+
],
|
|
50
|
+
"projected_min": 45.935
|
|
51
|
+
},
|
|
52
|
+
"model_mix": {
|
|
53
|
+
"sonnet": {
|
|
54
|
+
"tasks": 10,
|
|
55
|
+
"cost_usd": 2.656721
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"coordinator_cost_by_model": {
|
|
59
|
+
"opus": 24.04081
|
|
60
|
+
},
|
|
61
|
+
"quality": {
|
|
62
|
+
"tests": {
|
|
63
|
+
"passed": 802,
|
|
64
|
+
"failed": 0,
|
|
65
|
+
"skipped": 0,
|
|
66
|
+
"seconds": 170.5,
|
|
67
|
+
"mode": "full",
|
|
68
|
+
"failures": [],
|
|
69
|
+
"skipped_tests": []
|
|
70
|
+
},
|
|
71
|
+
"first_time_pass": 0,
|
|
72
|
+
"first_time_n": 0,
|
|
73
|
+
"rework_dispatches": 2,
|
|
74
|
+
"open_incidents": 1
|
|
75
|
+
},
|
|
76
|
+
"tasks": [
|
|
77
|
+
{
|
|
78
|
+
"task": "T3",
|
|
79
|
+
"model": "sonnet",
|
|
80
|
+
"pred_min": 12.875,
|
|
81
|
+
"actual_min": 32.065,
|
|
82
|
+
"delta": {
|
|
83
|
+
"abs": 19.19,
|
|
84
|
+
"pct": 149.0
|
|
85
|
+
},
|
|
86
|
+
"cost_usd": 0.213797,
|
|
87
|
+
"first_time_pass": null
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"task": "T4",
|
|
91
|
+
"model": "sonnet",
|
|
92
|
+
"pred_min": 8.859,
|
|
93
|
+
"actual_min": 2.787,
|
|
94
|
+
"delta": {
|
|
95
|
+
"abs": -6.0723,
|
|
96
|
+
"pct": -68.5
|
|
97
|
+
},
|
|
98
|
+
"cost_usd": 0.15933,
|
|
99
|
+
"first_time_pass": null
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"task": "T8",
|
|
103
|
+
"model": "sonnet",
|
|
104
|
+
"pred_min": 5.973,
|
|
105
|
+
"actual_min": 10.852,
|
|
106
|
+
"delta": {
|
|
107
|
+
"abs": 4.8787,
|
|
108
|
+
"pct": 81.7
|
|
109
|
+
},
|
|
110
|
+
"cost_usd": 0.276194,
|
|
111
|
+
"first_time_pass": null
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"task": "T5",
|
|
115
|
+
"model": "sonnet",
|
|
116
|
+
"pred_min": 6.6,
|
|
117
|
+
"actual_min": 3.933,
|
|
118
|
+
"delta": {
|
|
119
|
+
"abs": -2.6667,
|
|
120
|
+
"pct": -40.4
|
|
121
|
+
},
|
|
122
|
+
"cost_usd": 0.306469,
|
|
123
|
+
"first_time_pass": null
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"task": "T2",
|
|
127
|
+
"model": "sonnet",
|
|
128
|
+
"pred_min": 4.4,
|
|
129
|
+
"actual_min": 6.353,
|
|
130
|
+
"delta": {
|
|
131
|
+
"abs": 1.9533,
|
|
132
|
+
"pct": 44.4
|
|
133
|
+
},
|
|
134
|
+
"cost_usd": 0.548189,
|
|
135
|
+
"first_time_pass": null
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"task": "T7",
|
|
139
|
+
"model": "sonnet",
|
|
140
|
+
"pred_min": 7.918,
|
|
141
|
+
"actual_min": 6.223,
|
|
142
|
+
"delta": {
|
|
143
|
+
"abs": -1.6947,
|
|
144
|
+
"pct": -21.4
|
|
145
|
+
},
|
|
146
|
+
"cost_usd": 0.574766,
|
|
147
|
+
"first_time_pass": null
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"task": "T6",
|
|
151
|
+
"model": "sonnet",
|
|
152
|
+
"pred_min": 3.083,
|
|
153
|
+
"actual_min": 1.62,
|
|
154
|
+
"delta": {
|
|
155
|
+
"abs": -1.463,
|
|
156
|
+
"pct": -47.5
|
|
157
|
+
},
|
|
158
|
+
"cost_usd": 0.108093,
|
|
159
|
+
"first_time_pass": null
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"task": "T1",
|
|
163
|
+
"model": "sonnet",
|
|
164
|
+
"pred_min": 3.71,
|
|
165
|
+
"actual_min": 2.915,
|
|
166
|
+
"delta": {
|
|
167
|
+
"abs": -0.795,
|
|
168
|
+
"pct": -21.4
|
|
169
|
+
},
|
|
170
|
+
"cost_usd": 0.149792,
|
|
171
|
+
"first_time_pass": null
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"task": "T10",
|
|
175
|
+
"model": "sonnet",
|
|
176
|
+
"pred_min": null,
|
|
177
|
+
"actual_min": 1.973,
|
|
178
|
+
"delta": {
|
|
179
|
+
"abs": null,
|
|
180
|
+
"pct": null
|
|
181
|
+
},
|
|
182
|
+
"cost_usd": 0.119462,
|
|
183
|
+
"first_time_pass": null
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"task": "T9",
|
|
187
|
+
"model": "sonnet",
|
|
188
|
+
"pred_min": null,
|
|
189
|
+
"actual_min": 4.95,
|
|
190
|
+
"delta": {
|
|
191
|
+
"abs": null,
|
|
192
|
+
"pct": null
|
|
193
|
+
},
|
|
194
|
+
"cost_usd": 0.200629,
|
|
195
|
+
"first_time_pass": null
|
|
196
|
+
}
|
|
197
|
+
],
|
|
198
|
+
"cost_table": {
|
|
199
|
+
"source": "est",
|
|
200
|
+
"usd": 2.7859,
|
|
201
|
+
"tokens": 9375859,
|
|
202
|
+
"wall_min": 89690001.3646,
|
|
203
|
+
"rows": [
|
|
204
|
+
{
|
|
205
|
+
"agent": "a2884f220f51ad6d6",
|
|
206
|
+
"model": "sonnet",
|
|
207
|
+
"wall_min": 5.2402,
|
|
208
|
+
"tokens": 615127,
|
|
209
|
+
"usd": 0.2049
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"agent": "a2df9f2025916c02a",
|
|
213
|
+
"model": "sonnet",
|
|
214
|
+
"wall_min": 6.886,
|
|
215
|
+
"tokens": 850853,
|
|
216
|
+
"usd": 0.2679
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"agent": "a3820f0f9eb086e58",
|
|
220
|
+
"model": "sonnet",
|
|
221
|
+
"wall_min": 53.1566,
|
|
222
|
+
"tokens": 5047272,
|
|
223
|
+
"usd": 1.1558
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"agent": "a4d4e8f4797e03d68",
|
|
227
|
+
"model": "opus",
|
|
228
|
+
"wall_min": 0.2249,
|
|
229
|
+
"tokens": 35927,
|
|
230
|
+
"usd": 0.1291
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"agent": "a6fae2e1ec0b23393",
|
|
234
|
+
"model": "sonnet",
|
|
235
|
+
"wall_min": 29896601.3284,
|
|
236
|
+
"tokens": 0,
|
|
237
|
+
"usd": 0.0
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
"agent": "a70bd6caeed71f519",
|
|
241
|
+
"model": "sonnet",
|
|
242
|
+
"wall_min": 70.8345,
|
|
243
|
+
"tokens": 2113838,
|
|
244
|
+
"usd": 0.6646
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
"agent": "a933bd66026369ed1",
|
|
248
|
+
"model": "sonnet",
|
|
249
|
+
"wall_min": 29896604.2455,
|
|
250
|
+
"tokens": 0,
|
|
251
|
+
"usd": 0.0
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
"agent": "ad1dfc0fc5bf84734",
|
|
255
|
+
"model": "sonnet",
|
|
256
|
+
"wall_min": 35.3493,
|
|
257
|
+
"tokens": 712842,
|
|
258
|
+
"usd": 0.3636
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"agent": "af62a03b093783026",
|
|
262
|
+
"model": "sonnet",
|
|
263
|
+
"wall_min": 29896624.0992,
|
|
264
|
+
"tokens": 0,
|
|
265
|
+
"usd": 0.0
|
|
266
|
+
}
|
|
267
|
+
]
|
|
268
|
+
}
|
|
269
|
+
}
|
docs/plans/2026-11-04-compiler-cleanup-phase2b.report.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# superlite build report
|
|
2
|
+
|
|
3
|
+
projection: fixed forecast saved at lint 2026-11-04T18:13:08 (plan edited since)
|
|
4
|
+
|
|
5
|
+
Projected Actual Delta
|
|
6
|
+
wall-clock (min) 45.9 70.8 +24.9 (+54%)
|
|
7
|
+
agent cost (USD, +rework) 0.72 2.66 +1.93 (+267%)
|
|
8
|
+
task-minutes 53.4 73.7 +20.3 (+38%)
|
|
9
|
+
agents 8 9 +1 (+12%)
|
|
10
|
+
cost split: agents $2.18, rework $0.47, coordinator $24.04, total $26.70
|
|
11
|
+
concurrency: peak 3 at once (projected 2); critical path actual T1 T3 T4 T5 T7 T8 T9; projected T1 > T3 > T4 > T5 > T7 > T8 (45.9 min)
|
|
12
|
+
model mix: sonnet 10 tasks $2.66, coordinator opus $24.04
|
|
13
|
+
quality: tests 802 passed, 0 failed; first-time pass n/a; rework dispatches 2; open incidents 1
|
|
14
|
+
|
|
15
|
+
task model pred actual delta cost
|
|
16
|
+
T3 sonnet 12.9 32.1 +19.2 (+149%) $0.214
|
|
17
|
+
T4 sonnet 8.9 2.8 -6.1 (-68%) $0.159
|
|
18
|
+
T8 sonnet 6.0 10.9 +4.9 (+82%) $0.276
|
|
19
|
+
T5 sonnet 6.6 3.9 -2.7 (-40%) $0.306
|
|
20
|
+
T2 sonnet 4.4 6.4 +2.0 (+44%) $0.548
|
|
21
|
+
(+5 smaller misses; `superlite-cli report --json` lists all)
|
|
22
|
+
|
|
23
|
+
agent cost table:
|
|
24
|
+
| agent | model | wall_min | tokens | usd |
|
|
25
|
+
| --- | --- | --- | --- | --- |
|
|
26
|
+
| a2884f220f51ad6d6 | sonnet | 5.2402 | 615127 | 0.2049 |
|
|
27
|
+
| a2df9f2025916c02a | sonnet | 6.886 | 850853 | 0.2679 |
|
|
28
|
+
| a3820f0f9eb086e58 | sonnet | 53.1566 | 5047272 | 1.1558 |
|
|
29
|
+
| a4d4e8f4797e03d68 | opus | 0.2249 | 35927 | 0.1291 |
|
|
30
|
+
| a6fae2e1ec0b23393 | sonnet | 29896601.3284 | 0 | 0.0 |
|
|
31
|
+
| a70bd6caeed71f519 | sonnet | 70.8345 | 2113838 | 0.6646 |
|
|
32
|
+
| a933bd66026369ed1 | sonnet | 29896604.2455 | 0 | 0.0 |
|
|
33
|
+
| ad1dfc0fc5bf84734 | sonnet | 35.3493 | 712842 | 0.3636 |
|
|
34
|
+
| af62a03b093783026 | sonnet | 29896624.0992 | 0 | 0.0 |
|
|
35
|
+
| totals | | 89690001.3646 | 9375859 | 2.7859 |
|
|
36
|
+
cost source: est
|
|
37
|
+
|
|
38
|
+
takeaway: ran 54% slower than projected; cost 267% over; biggest miss T3
|
docs/plans/2026-11-04-compiler-cleanup-phase2b.results.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Phase 2b results
|
|
2
|
+
|
|
3
|
+
Start sha: bb462a8697f7486294f44f87a74a2f915eea82ca. Measured on the uncommitted worktree tree.
|
|
4
|
+
|
|
5
|
+
- Wasm hashes: all 25 plum-examples files compile, no FAIL, sha256 identical (diff of start-sha vs current lists empty).
|
|
6
|
+
- `plum test plum-std plum-examples`: 678 passed, 0 failed (63/63 files).
|
|
7
|
+
- plum-game host_test: 18 passed, 0 failed.
|
|
8
|
+
- `plum game plum-examples/games/imperative/game.plum -o DIR`: wrote index.html, plum.wasm, plum_bridge.js, preload.txt, assets.
|
|
9
|
+
- `grep -rn "Expr::Paren" plum-*/src`: empty.
|
|
10
|
+
- Full workspace test binaries: not run by this task (the coordinator ran `cargo test`).
|
|
11
|
+
|
|
12
|
+
| metric | start | now |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| Rust LOC (plum-*/**/*.rs, incl. plum-game) | 54538 | 54700 |
|
|
15
|
+
| .rs files | 126 | 130 |
|
|
16
|
+
| integration test files (plum-*/tests/*.rs) | 45 | 47 |
|
|
17
|
+
| `#[test]` fns | 830 | 840 |
|
|
18
|
+
|
|
19
|
+
New files: plum-core/src/lower.rs, tests_lower.rs, tests/lower_test.rs, tests/tests_lower_test.rs.
|
plum-checker/src/lib.rs
CHANGED
|
@@ -925,7 +925,6 @@ pub fn unify(t1: &PlumType, t2: &PlumType, ctx: &CheckCtx) -> Result<(), String>
|
|
|
925
925
|
/// or names one.
|
|
926
926
|
fn exprIsVariant(expr: &ast::Expr, variant_name: &str) -> bool {
|
|
927
927
|
match expr {
|
|
928
|
-
ast::Expr::Paren(inner) => exprIsVariant(inner, variant_name),
|
|
929
928
|
ast::Expr::TypeName(n) => n == variant_name,
|
|
930
929
|
ast::Expr::FnCall(call) => call.name == variant_name,
|
|
931
930
|
ast::Expr::ClassCall(call) => call.type_name == variant_name,
|
|
@@ -1267,6 +1266,11 @@ fn immutableFieldOwner(obj_ty: &PlumType, field: &str, ctx: &CheckCtx) -> Option
|
|
|
1267
1266
|
}
|
|
1268
1267
|
|
|
1269
1268
|
pub fn typedSource(source: &ast::Source) -> CheckResult<TypedInfo> {
|
|
1269
|
+
typedSourceWith(source, fusionOptions())
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/// Like `typedSource` with explicit fusion options (no thread-local read).
|
|
1273
|
+
pub fn typedSourceWith(source: &ast::Source, fusion: FusionOptions) -> CheckResult<TypedInfo> {
|
|
1270
1274
|
let (mut result, mut info) = checkSourceFull(source);
|
|
1271
1275
|
// Closure specialisation and fusion run on the checked program BEFORE union lowering
|
|
1272
1276
|
// (`pre`: own generics applied, `for` loops lowered), so the passes never see the
|
|
@@ -1287,10 +1291,10 @@ pub fn typedSource(source: &ast::Source) -> CheckResult<TypedInfo> {
|
|
|
1287
1291
|
}
|
|
1288
1292
|
// Supercompiler stage: purity -> simplify -> drive -> uniqueness -> reuse.
|
|
1289
1293
|
// `--no-fusion` skips it.
|
|
1290
|
-
if
|
|
1294
|
+
if fusion.enabled {
|
|
1291
1295
|
let mut copy = pre.clone();
|
|
1292
1296
|
let notes = fusionRewrite(&mut copy);
|
|
1293
|
-
if
|
|
1297
|
+
if fusion.explain {
|
|
1294
1298
|
for n in ¬es {
|
|
1295
1299
|
eprintln!("fusion: {n}");
|
|
1296
1300
|
}
|
|
@@ -3047,7 +3051,7 @@ fn firstCallInExpr<'a>(expr: &'a ast::Expr, name: &str) -> Option<ClosureUse<'a>
|
|
|
3047
3051
|
ast::Expr::Binary(b) => firstCallInExpr(&b.left, name).or_else(|| firstCallInExpr(&b.right, name)),
|
|
3048
3052
|
ast::Expr::Bool(b) => firstCallInExpr(&b.left, name).or_else(|| firstCallInExpr(&b.right, name)),
|
|
3049
3053
|
ast::Expr::Compare(c) => firstCallInExpr(&c.left, name).or_else(|| firstCallInExpr(&c.right, name)),
|
|
3050
|
-
ast::Expr::Not(e) | ast::Expr::
|
|
3054
|
+
ast::Expr::Not(e) | ast::Expr::Try(e) => firstCallInExpr(e, name),
|
|
3051
3055
|
ast::Expr::Unary(u) => firstCallInExpr(&u.operand, name),
|
|
3052
3056
|
ast::Expr::InlineIf(t) => firstCallInExpr(&t.condition, name)
|
|
3053
3057
|
.or_else(|| firstCallInExpr(&t.then, name))
|
|
@@ -3611,7 +3615,7 @@ fn discardedCall(e: &ast::Expr) -> Option<&ast::FnCall> {
|
|
|
3611
3615
|
ast::AttrKind::Field(_) => None,
|
|
3612
3616
|
},
|
|
3613
3617
|
ast::Expr::Bang(b) => discardedCall(&b.value),
|
|
3614
|
-
ast::Expr::Try(inner)
|
|
3618
|
+
ast::Expr::Try(inner) => discardedCall(inner),
|
|
3615
3619
|
_ => None,
|
|
3616
3620
|
}
|
|
3617
3621
|
}
|
|
@@ -4367,7 +4371,7 @@ fn resolveClosureParamFromFieldUsage(
|
|
|
4367
4371
|
scanExpr(&b.left, params, ctx, resolved);
|
|
4368
4372
|
scanExpr(&b.right, params, ctx, resolved);
|
|
4369
4373
|
}
|
|
4370
|
-
ast::Expr::Not(inner)
|
|
4374
|
+
ast::Expr::Not(inner) => {
|
|
4371
4375
|
scanExpr(inner, params, ctx, resolved)
|
|
4372
4376
|
}
|
|
4373
4377
|
ast::Expr::Unary(u) => scanExpr(&u.operand, params, ctx, resolved),
|
|
@@ -5167,7 +5171,6 @@ fn inferExprInner(
|
|
|
5167
5171
|
},
|
|
5168
5172
|
},
|
|
5169
5173
|
},
|
|
5170
|
-
ast::Expr::Paren(inner) => inferExprWith(inner, env, ctx, successExpected(expected)),
|
|
5171
5174
|
// `try expr`: the operand's error set must fit the enclosing fn's set.
|
|
5172
5175
|
ast::Expr::Try(inner) => {
|
|
5173
5176
|
if let (Some(star), true) = (env.get(STAR_KEY), env.contains_key(RETURN_KEY)) {
|
|
@@ -5730,3 +5733,60 @@ fn nameFormErrors(source: &ast::Source, errs: Vec<CheckError>) -> Vec<CheckError
|
|
|
5730
5733
|
})
|
|
5731
5734
|
.collect()
|
|
5732
5735
|
}
|
|
5736
|
+
|
|
5737
|
+
#[cfg(test)]
|
|
5738
|
+
mod fusion_options_tests {
|
|
5739
|
+
use super::*;
|
|
5740
|
+
use plum_test_support::parse;
|
|
5741
|
+
|
|
5742
|
+
const SRC: &str = "\
|
|
5743
|
+
enum Cells =
|
|
5744
|
+
| Cons(head: Int, tail: Cells)
|
|
5745
|
+
| Nil
|
|
5746
|
+
|
|
5747
|
+
fun mapCells(xs: Cells) -> Cells = match xs
|
|
5748
|
+
Cons(h, t) => Cons(head: h * 2, tail: mapCells(t))
|
|
5749
|
+
Nil => Nil
|
|
5750
|
+
|
|
5751
|
+
fun sumL(xs: Cells) -> Int = match xs
|
|
5752
|
+
Cons(h, t) => h + sumL(t)
|
|
5753
|
+
Nil => 0
|
|
5754
|
+
|
|
5755
|
+
fun total(xs: Cells) -> Int = sumL(mapCells(xs))
|
|
5756
|
+
|
|
5757
|
+
fun count(n: Int) -> Int =
|
|
5758
|
+
s := 0
|
|
5759
|
+
for i := range n
|
|
5760
|
+
s = s + i
|
|
5761
|
+
s
|
|
5762
|
+
";
|
|
5763
|
+
|
|
5764
|
+
fn fnNames(info: &TypedInfo) -> Vec<String> {
|
|
5765
|
+
info.lowered
|
|
5766
|
+
.as_ref()
|
|
5767
|
+
.unwrap()
|
|
5768
|
+
.items
|
|
5769
|
+
.iter()
|
|
5770
|
+
.filter_map(|i| match i {
|
|
5771
|
+
ast::Item::Fn(f) => Some(f.name.clone()),
|
|
5772
|
+
_ => None,
|
|
5773
|
+
})
|
|
5774
|
+
.collect()
|
|
5775
|
+
}
|
|
5776
|
+
|
|
5777
|
+
#[test]
|
|
5778
|
+
fn defaultOptionsMatchTypedSource() {
|
|
5779
|
+
let src = parse(SRC);
|
|
5780
|
+
let a = typedSource(&src).expect("checks");
|
|
5781
|
+
let b = typedSourceWith(&src, FusionOptions::default()).expect("checks");
|
|
5782
|
+
assert_eq!(fnNames(&a), fnNames(&b));
|
|
5783
|
+
assert!(fnNames(&b).iter().any(|n| n.contains("sumL$mapCells")));
|
|
5784
|
+
}
|
|
5785
|
+
|
|
5786
|
+
#[test]
|
|
5787
|
+
fn disabledHasNoFusedFns() {
|
|
5788
|
+
let src = parse(SRC);
|
|
5789
|
+
let info = typedSourceWith(&src, FusionOptions { enabled: false, explain: false }).expect("checks");
|
|
5790
|
+
assert!(!fnNames(&info).iter().any(|n| n.contains('$')), "{:?}", fnNames(&info));
|
|
5791
|
+
}
|
|
5792
|
+
}
|
plum-checker/tests/checker_tests.rs
CHANGED
|
@@ -657,7 +657,7 @@ fn rejects_basic_expressions_and_calls() {
|
|
|
657
657
|
r(&format!("{cat} fun breakCat(self) =\n self.nope = 1\n"), &["no field 'nope' on type 'Cat'"]),
|
|
658
658
|
r("enum Shape =\n | Square(x: Int, y: Int)\n\nfun bad() -> Shape =\n Square(x: 1)\n", &["variant 'Square' missing field 'y'"]),
|
|
659
659
|
r("enum Shape =\n | Square(x: Int, y: Int)\n\nfun bad() -> Shape =\n Square(x: 1, y: 2, z: 3)\n", &["unknown field 'z' on variant 'Square'"]),
|
|
660
|
-
r("enum Shape =\n | Circle(radius: Int)\n | Square(radius: Int)\n\nfun bad() -> Int =\n r := Circle(radius: 5)\n return r.radius\n", &["field 'radius'
|
|
660
|
+
r("enum Shape =\n | Circle(radius: Int)\n | Square(radius: Int)\n\nfun bad() -> Int =\n r := Circle(radius: 5)\n return r.radius\n", &["no field 'radius' on type 'Shape'"]),
|
|
661
661
|
r("enum Maybe =\n | Just(Int)\n | Nothing\n\nfun bad() -> Maybe =\n Just(\"x\")\n", &["variant 'Just' arg 0: type mismatch"]),
|
|
662
662
|
r("enum Shape =\n | Rect(Float, Float)\n | Circle(Float)\n\nfun bad() -> Shape =\n Rect(1.0)\n", &["variant 'Rect': expected 2 arg(s), got 1"]),
|
|
663
663
|
// enum discriminants
|
plum-cli/src/main.rs
CHANGED
|
@@ -137,9 +137,22 @@ fn main() {
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
/// `--no-fusion` / `--explain-fusion`
|
|
140
|
+
/// The `--no-fusion` / `--explain-fusion` flags of the running command, set once in `run`
|
|
141
|
+
/// and passed to every compile as explicit `CompileOptions`.
|
|
142
|
+
static FUSION: std::sync::OnceLock<plum_checker::FusionOptions> = std::sync::OnceLock::new();
|
|
143
|
+
|
|
141
144
|
fn setFusion(explain: bool, no_fusion: bool) {
|
|
142
|
-
|
|
145
|
+
let _ = FUSION.set(plum_checker::FusionOptions { enabled: !no_fusion, explain });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// `compile` reports positioned type errors without a file name; name `file` in them
|
|
149
|
+
/// (the entry file, since positions refer to the merged source).
|
|
150
|
+
fn nameFileInErrors(msg: &str, file: &std::path::Path) -> String {
|
|
151
|
+
msg.replace("type error at line", &format!("type error in '{}' at line", file.display()))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fn compileOptions(tests: bool) -> plum_wasm_codegen::CompileOptions {
|
|
155
|
+
plum_wasm_codegen::CompileOptions { tests, fusion: FUSION.get().copied().unwrap_or_default() }
|
|
143
156
|
}
|
|
144
157
|
|
|
145
158
|
fn run() -> Result<()> {
|
|
@@ -223,16 +236,13 @@ fn cmdFormat(file: Option<std::path::PathBuf>, check: bool, use_stdin: bool) ->
|
|
|
223
236
|
fn compileToWasm(file: &std::path::Path, lib_path: &std::path::Path) -> Result<Vec<u8>> {
|
|
224
237
|
let ast = plum_core::loadAndMerge(file, lib_path).map_err(|e| anyhow::anyhow!("{e}"))?;
|
|
225
238
|
|
|
226
|
-
// Type check once; codegen reuses the result.
|
|
227
|
-
|
|
239
|
+
plum_wasm_codegen::compile(&ast, &compileOptions(false)).map(|o| o.wasm).map_err(|e| {
|
|
228
|
-
Ok(t) => t,
|
|
229
|
-
Err(messages) => {
|
|
230
|
-
|
|
240
|
+
if e.starts_with("type error") {
|
|
241
|
+
eprintln!("{}", nameFileInErrors(&e, file));
|
|
231
242
|
process::exit(1);
|
|
232
243
|
}
|
|
244
|
+
anyhow::anyhow!("codegen error: {e}")
|
|
233
|
-
}
|
|
245
|
+
})
|
|
234
|
-
|
|
235
|
-
plum_wasm_codegen::compileTyped(&ast, typed).map_err(|e| anyhow::anyhow!("codegen error: {e}"))
|
|
236
246
|
}
|
|
237
247
|
|
|
238
248
|
fn compileToTestWasm(
|
|
@@ -248,11 +258,14 @@ fn compileToTestWasm(
|
|
|
248
258
|
// continue to the rest of the directory instead of the whole run
|
|
249
259
|
// dying on the first file with a type error. Checked once, on the
|
|
250
260
|
// test-desugared source codegen then compiles.
|
|
251
|
-
let prog = plum_wasm_codegen::desugarTests(&ast);
|
|
252
|
-
let
|
|
261
|
+
let out = plum_wasm_codegen::compile(&ast, &compileOptions(true)).map_err(|e| {
|
|
253
|
-
.
|
|
262
|
+
if e.starts_with("type error") {
|
|
254
|
-
|
|
263
|
+
anyhow::anyhow!(nameFileInErrors(&e, file))
|
|
264
|
+
} else {
|
|
255
|
-
|
|
265
|
+
anyhow::anyhow!("codegen error: {e}")
|
|
266
|
+
}
|
|
267
|
+
})?;
|
|
268
|
+
let compiled = (out.wasm, out.test_names);
|
|
256
269
|
// Debug aid: `PLUM_DUMP_WASM=out.wasm plum test file.plum` keeps the module.
|
|
257
270
|
if let Some(path) = std::env::var_os("PLUM_DUMP_WASM") {
|
|
258
271
|
let _ = fs::write(path, &compiled.0);
|
|
@@ -474,7 +487,7 @@ fn runTestFile(file: &std::path::Path, lib_path: &std::path::Path) -> Result<(us
|
|
|
474
487
|
let export_name = format!("test#{name}");
|
|
475
488
|
// Each test function returns a `Str` (empty = every `assert` inside it
|
|
476
489
|
// passed; otherwise a newline-separated report, one "line L, column C:
|
|
477
|
-
// ..." entry per failing `assert` — see `
|
|
490
|
+
// ..." entry per failing `assert` — see `plum_core::tests_lower`) rather
|
|
478
491
|
// than a plain `i64`, so this goes through the untyped call API
|
|
479
492
|
// (`Val::AnyRef`, same GC-ref handling `hostImports`' own Str-returning
|
|
480
493
|
// imports already use) instead of `get_typed_func`.
|
plum-core/src/ast.rs
CHANGED
|
@@ -384,7 +384,7 @@ pub enum Stmt {
|
|
|
384
384
|
/// Outside a `test` body, traps (hard failure) if `cond` is false. Inside
|
|
385
385
|
/// one, desugars to a non-fatal check instead — records a failure report
|
|
386
386
|
/// (with `Check`'s own source location/text) and keeps running rather
|
|
387
|
-
/// than trapping — see `
|
|
387
|
+
/// than trapping — see `plum_core::tests_lower`.
|
|
388
388
|
Assert(Check),
|
|
389
389
|
/// A surface `for` over an iterable or a for-form call, not yet lowered
|
|
390
390
|
/// (see `forms.rs`); never seen by the checker or codegen.
|
|
@@ -577,8 +577,6 @@ pub enum Expr {
|
|
|
577
577
|
Catch(Box<CatchExpr>),
|
|
578
578
|
/// `error.Name` — an error value, of the singleton set `error{Name}`.
|
|
579
579
|
ErrorValue(ErrorValue),
|
|
580
|
-
/// `{expr}` — grouped/parenthesized expression
|
|
581
|
-
Paren(Box<Expr>),
|
|
582
580
|
String(StringExpr),
|
|
583
581
|
Int(i64),
|
|
584
582
|
Float(f64),
|
plum-core/src/builtin_usage.rs
CHANGED
|
@@ -295,7 +295,6 @@ fn walkExpr(expr: &Expr, names: &mut Acc) {
|
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
|
-
Expr::Paren(e) => walkExpr(e, names),
|
|
299
298
|
Expr::String(s) => {
|
|
300
299
|
names.insert("Str".to_string());
|
|
301
300
|
for part in &s.parts {
|
plum-core/src/closure_escape.rs
CHANGED
|
@@ -216,7 +216,7 @@ impl<'a> Cx<'a> {
|
|
|
216
216
|
Expr::Binary(b) => self.expr(&b.left, cl) || self.expr(&b.right, cl),
|
|
217
217
|
Expr::Unary(u) => self.expr(&u.operand, cl),
|
|
218
218
|
Expr::Bool(b) => self.expr(&b.left, cl) || self.expr(&b.right, cl),
|
|
219
|
-
Expr::Not(x) | Expr::Try(x)
|
|
219
|
+
Expr::Not(x) | Expr::Try(x) => self.expr(x, cl),
|
|
220
220
|
Expr::Compare(c) => self.expr(&c.left, cl) || self.expr(&c.right, cl),
|
|
221
221
|
Expr::InlineIf(i) => {
|
|
222
222
|
self.expr(&i.condition, cl) || self.expr(&i.then, cl) || self.expr(&i.else_, cl)
|
plum-core/src/closure_spec.rs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
use crate::ast::*;
|
|
13
13
|
use crate::closure_escape::nonEscapingFnParams;
|
|
14
14
|
use crate::rename::{renameStmts, Name};
|
|
15
|
-
use crate::visit::{
|
|
15
|
+
use crate::visit::{walkBlockRef, walkRef, countVar, stmtBlocks, stmtExprs, visitBlock, visitExpr};
|
|
16
16
|
use std::collections::{HashMap, HashSet};
|
|
17
17
|
|
|
18
18
|
pub const MAX_COPIES: usize = 8;
|
|
@@ -989,24 +989,3 @@ fn collectBinders(body: &mut FnBody, out: &mut HashSet<String>) {
|
|
|
989
989
|
FnBody::Extern => {}
|
|
990
990
|
}
|
|
991
991
|
}
|
|
992
|
-
|
|
993
|
-
// ---------- read-only traversal ----------
|
|
994
|
-
|
|
995
|
-
fn walkBlockRef(b: &Block, f: &mut dyn FnMut(&Expr)) {
|
|
996
|
-
for s in &b.stmts {
|
|
997
|
-
for e in stmtExprRefs(s) {
|
|
998
|
-
walkRef(e, f);
|
|
999
|
-
}
|
|
1000
|
-
for blk in stmtBlockRefs(s) {
|
|
1001
|
-
walkBlockRef(blk, f);
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
/// Pre-order over `e`, closure bodies and catch blocks included.
|
|
1007
|
-
fn walkRef(e: &Expr, f: &mut dyn FnMut(&Expr)) {
|
|
1008
|
-
anyExpr(e, &mut |x| {
|
|
1009
|
-
f(x);
|
|
1010
|
-
false
|
|
1011
|
-
});
|
|
1012
|
-
}
|
plum-core/src/ctors.rs
CHANGED
|
@@ -68,7 +68,6 @@ pub(crate) fn inferType(e: &Expr) -> Option<Type> {
|
|
|
68
68
|
Expr::Float(_) => Some(ty("Float")),
|
|
69
69
|
Expr::BoolLit(_) => Some(ty("Bool")),
|
|
70
70
|
Expr::String(_) => Some(ty("Str")),
|
|
71
|
-
Expr::Paren(x) => inferType(x),
|
|
72
71
|
Expr::Unary(u) => inferType(&u.operand),
|
|
73
72
|
Expr::ClassCall(c) => Some(Type {
|
|
74
73
|
name: c.type_name.clone(),
|
plum-core/src/drive.rs
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
use crate::ast::*;
|
|
12
12
|
use crate::generics::isGenericName;
|
|
13
13
|
use crate::rename::{renameStmts, substPT, substStmts, substType, Name};
|
|
14
|
+
use crate::simplify::simplifyBlock;
|
|
14
|
-
use crate::
|
|
15
|
+
use crate::visit::{argRef, isTag, walkBlockRef, walkRef};
|
|
15
|
-
use crate::visit::{childRefs, stmtBlockRefs, stmtBlocks, stmtExprRefs,
|
|
16
|
+
use crate::visit::{childRefs, stmtBlockRefs, stmtBlocks, stmtExprRefs, visitBlock, visitExpr};
|
|
16
17
|
use crate::whistle::embeds;
|
|
17
18
|
use std::collections::{HashMap, HashSet};
|
|
18
19
|
|
|
@@ -236,7 +237,6 @@ fn pathStmts(stmts: &[Stmt], name: &str) -> usize {
|
|
|
236
237
|
fn cheap(e: &Expr) -> bool {
|
|
237
238
|
match e {
|
|
238
239
|
Expr::Var(_) | Expr::Int(_) | Expr::Float(_) | Expr::BoolLit(_) | Expr::TypeName(_) | Expr::Self_ => true,
|
|
239
|
-
Expr::Paren(x) => cheap(x),
|
|
240
240
|
Expr::Attribute(a) => matches!(a.attr, AttrKind::Field(_)) && cheap(&a.object),
|
|
241
241
|
_ => false,
|
|
242
242
|
}
|
|
@@ -319,7 +319,6 @@ impl Tcx {
|
|
|
319
319
|
|
|
320
320
|
fn tail(&self, e: Expr) -> Vec<Stmt> {
|
|
321
321
|
match e {
|
|
322
|
-
Expr::Paren(i) => self.tail(*i),
|
|
323
322
|
Expr::ClassCall(_) | Expr::TypeName(_) => self.pushed(e),
|
|
324
323
|
Expr::Var(ref n) if self.ctors.contains(n) => self.pushed(e),
|
|
325
324
|
Expr::InlineIf(i) => {
|
|
@@ -647,7 +646,7 @@ impl Driver {
|
|
|
647
646
|
// ---------- resolving calls ----------
|
|
648
647
|
|
|
649
648
|
fn calls(&self, e: &Expr) -> Vec<Call> {
|
|
650
|
-
match
|
|
649
|
+
match e {
|
|
651
650
|
Expr::FnCall(c) if self.free.contains(&c.name) => {
|
|
652
651
|
let f = &self.fns[&c.name];
|
|
653
652
|
match positional(&c.args) {
|
|
@@ -674,7 +673,7 @@ impl Driver {
|
|
|
674
673
|
};
|
|
675
674
|
}
|
|
676
675
|
let mut out = Vec::new();
|
|
677
|
-
let recv = match
|
|
676
|
+
let recv = match &a.object {
|
|
678
677
|
Expr::Var(n) => self.vtypes.get(n),
|
|
679
678
|
Expr::Self_ => self.vtypes.get("self"),
|
|
680
679
|
_ => None,
|
|
@@ -814,7 +813,7 @@ impl Driver {
|
|
|
814
813
|
|
|
815
814
|
/// The enum (or record) a just-built value `e` belongs to, if `e` is a constructor.
|
|
816
815
|
fn ctorType(&self, e: &Expr) -> Option<String> {
|
|
817
|
-
match
|
|
816
|
+
match e {
|
|
818
817
|
Expr::ClassCall(c) if c.spread.is_none() => self.ctorOwner.get(&c.type_name).cloned().or(Some(c.type_name.clone())),
|
|
819
818
|
Expr::FnCall(c) => self.ctorOwner.get(&c.name).cloned(),
|
|
820
819
|
Expr::TypeName(n) | Expr::Var(n) => self.ctorOwner.get(n).cloned(),
|
|
@@ -1311,7 +1310,7 @@ impl Driver {
|
|
|
1311
1310
|
for st in &blk.stmts {
|
|
1312
1311
|
if let Stmt::Match(m) = st {
|
|
1313
1312
|
if let [subject] = &m.subjects[..] {
|
|
1314
|
-
let st = match
|
|
1313
|
+
let st = match subject {
|
|
1315
1314
|
Expr::Var(n) => out.get(n).cloned(),
|
|
1316
1315
|
Expr::Self_ => out.get("self").cloned(),
|
|
1317
1316
|
_ => None,
|
|
@@ -1355,7 +1354,7 @@ impl Driver {
|
|
|
1355
1354
|
/// when the receiver's type is known (`io.clear` is `Io.clear`), else every method of
|
|
1356
1355
|
/// that name. Empty (an unknown callee) when there is none.
|
|
1357
1356
|
fn methodKeys(&self, object: &Expr, name: &str) -> Vec<String> {
|
|
1358
|
-
let recv = match
|
|
1357
|
+
let recv = match object {
|
|
1359
1358
|
Expr::Var(n) => self.vtypes.get(n),
|
|
1360
1359
|
Expr::Self_ => self.vtypes.get("self"),
|
|
1361
1360
|
_ => None,
|
|
@@ -1371,7 +1370,7 @@ impl Driver {
|
|
|
1371
1370
|
|
|
1372
1371
|
/// The type of a variable, `self` or a field read of one, when known and concrete.
|
|
1373
1372
|
fn exprType(&self, e: &Expr) -> Option<Type> {
|
|
1374
|
-
let t = match
|
|
1373
|
+
let t = match e {
|
|
1375
1374
|
Expr::Var(n) => self.vtypes.get(n).cloned()?,
|
|
1376
1375
|
Expr::Self_ => self.vtypes.get("self").cloned()?,
|
|
1377
1376
|
Expr::Attribute(a) => {
|
|
@@ -1704,7 +1703,7 @@ impl Driver {
|
|
|
1704
1703
|
/// `List(a, b, ..)` (the variadic `List.init`) as the cons cells it builds,
|
|
1705
1704
|
/// `Cons(head: a, tail: ..Nil)`, so a consumer can pick its arms on it.
|
|
1706
1705
|
fn listLit(&self, e: &Expr) -> Option<Expr> {
|
|
1707
|
-
let Expr::FnCall(c) =
|
|
1706
|
+
let Expr::FnCall(c) = e else { return None };
|
|
1708
1707
|
if c.name != "List" || !self.ctors.contains("Cons") || !self.ctors.contains("Nil") {
|
|
1709
1708
|
return None;
|
|
1710
1709
|
}
|
|
@@ -1733,7 +1732,7 @@ impl Driver {
|
|
|
1733
1732
|
walkBlockRef(&Block { stmts: stmts.to_vec() }, &mut |e| {
|
|
1734
1733
|
for c in self.calls(e) {
|
|
1735
1734
|
let Some(k) = self.subj(&c.f) else { continue };
|
|
1736
|
-
let hit = matches!(c.args.get(k)
|
|
1735
|
+
let hit = matches!(c.args.get(k), Some(Expr::Var(n)) if n == p);
|
|
1737
1736
|
if hit && (self.pure.contains(&c.key) || self.readOnly.contains(&c.key)) {
|
|
1738
1737
|
good += 1;
|
|
1739
1738
|
break;
|
|
@@ -1761,7 +1760,7 @@ impl Driver {
|
|
|
1761
1760
|
/// `todo`, and every call is read-only (it may draw or stop, never mutate) or takes the
|
|
1762
1761
|
/// value `p` directly.
|
|
1763
1762
|
fn quiet(&self, stmts: &[Stmt], p: &str) -> bool {
|
|
1764
|
-
let isP = |e: &Expr| matches!(
|
|
1763
|
+
let isP = |e: &Expr| matches!(e, Expr::Var(n) if n == p);
|
|
1765
1764
|
let takesP = |args: &[Arg]| args.iter().any(|a| isP(argRef(a)));
|
|
1766
1765
|
let mut ok = true;
|
|
1767
1766
|
for s in stmts {
|
plum-core/src/lib.rs
CHANGED
|
@@ -13,12 +13,14 @@ pub mod formatter;
|
|
|
13
13
|
pub mod forms;
|
|
14
14
|
pub mod generics;
|
|
15
15
|
pub mod loader;
|
|
16
|
+
pub mod lower;
|
|
16
17
|
pub mod parser;
|
|
17
18
|
pub mod purity;
|
|
18
19
|
pub mod rename;
|
|
19
20
|
pub mod reuse;
|
|
20
21
|
pub mod simplify;
|
|
21
22
|
pub mod tail;
|
|
23
|
+
pub mod tests_lower;
|
|
22
24
|
pub mod traits;
|
|
23
25
|
pub mod trmc;
|
|
24
26
|
pub mod union_lower;
|
plum-core/src/loader.rs
CHANGED
|
@@ -81,23 +81,7 @@ pub fn loadAndMergeWithOverrides(
|
|
|
81
81
|
primitives: prims,
|
|
82
82
|
traits: Vec::new(),
|
|
83
83
|
};
|
|
84
|
-
let traitErrors = crate::traits::lowerTraits(&mut merged);
|
|
85
|
-
if !traitErrors.is_empty() {
|
|
86
|
-
return Err(traitErrors.join("\n"));
|
|
87
|
-
}
|
|
88
|
-
let ctorErrors = crate::ctors::lowerCtors(&mut merged);
|
|
89
|
-
if !ctorErrors.is_empty() {
|
|
90
|
-
return Err(ctorErrors.join("\n"));
|
|
91
|
-
}
|
|
92
|
-
let typeErrors = crate::unions::inlineVariantTypeErrors(&merged);
|
|
93
|
-
if !typeErrors.is_empty() {
|
|
94
|
-
return Err(typeErrors.join("\n"));
|
|
95
|
-
}
|
|
96
|
-
let memberErrors = crate::unions::resolveMembers(&mut merged);
|
|
97
|
-
if !memberErrors.is_empty() {
|
|
98
|
-
return Err(memberErrors.join("\n"));
|
|
99
|
-
}
|
|
100
|
-
crate::
|
|
84
|
+
crate::lower::frontEnd(&mut merged)?;
|
|
101
85
|
Ok(merged)
|
|
102
86
|
}
|
|
103
87
|
|
|
@@ -540,17 +524,9 @@ fn mergeItems(
|
|
|
540
524
|
names: &mut HashMap<String, PathBuf>,
|
|
541
525
|
prims: &mut Vec<PrimitiveDecl>,
|
|
542
526
|
) -> Result<(), String> {
|
|
543
|
-
// Removed forms (lone self-named variant).
|
|
544
|
-
let legacy: Vec<String> = crate::unions::legacyErrors(source);
|
|
545
|
-
if !legacy.is_empty() {
|
|
546
|
-
return Err(legacy.join("\n"));
|
|
547
|
-
}
|
|
548
|
-
//
|
|
527
|
+
// Removed forms, then inline variants as hidden records (per-file lowering).
|
|
549
528
|
let mut hoisted = source.clone();
|
|
550
|
-
|
|
529
|
+
crate::lower::perFile(&mut hoisted)?;
|
|
551
|
-
if !hoistErrors.is_empty() {
|
|
552
|
-
return Err(hoistErrors.join("\n"));
|
|
553
|
-
}
|
|
554
530
|
let source = &hoisted;
|
|
555
531
|
// Parse-time literal errors (out-of-range integers) of every merged file.
|
|
556
532
|
MERGED_LIT_ERRORS.with(|e| e.borrow_mut().extend(source.formErrors.iter().cloned()));
|
plum-core/src/lower.rs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
use crate::ast::Source;
|
|
2
|
+
|
|
3
|
+
/// Per-file steps, run on each file before it is merged (and by single-file
|
|
4
|
+
/// `parseSource`): removed-form errors, then inline variants become hidden
|
|
5
|
+
/// records right after their enum.
|
|
6
|
+
pub fn perFile(source: &mut Source) -> Result<(), String> {
|
|
7
|
+
let legacy: Vec<String> = crate::unions::legacyErrors(source);
|
|
8
|
+
if !legacy.is_empty() {
|
|
9
|
+
return Err(legacy.join("\n"));
|
|
10
|
+
}
|
|
11
|
+
let hoistErrors = crate::unions::hoistInlineVariants(source);
|
|
12
|
+
if !hoistErrors.is_empty() {
|
|
13
|
+
return Err(hoistErrors.join("\n"));
|
|
14
|
+
}
|
|
15
|
+
Ok(())
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Front-end lowering of a merged program: traits, constructors, inline-variant
|
|
19
|
+
/// type checks, union members, then forms. The first failing stage returns its
|
|
20
|
+
/// errors joined with "\n".
|
|
21
|
+
pub fn frontEnd(source: &mut Source) -> Result<(), String> {
|
|
22
|
+
let traitErrors = crate::traits::lowerTraits(source);
|
|
23
|
+
if !traitErrors.is_empty() {
|
|
24
|
+
return Err(traitErrors.join("\n"));
|
|
25
|
+
}
|
|
26
|
+
let ctorErrors = crate::ctors::lowerCtors(source);
|
|
27
|
+
if !ctorErrors.is_empty() {
|
|
28
|
+
return Err(ctorErrors.join("\n"));
|
|
29
|
+
}
|
|
30
|
+
let typeErrors = crate::unions::inlineVariantTypeErrors(source);
|
|
31
|
+
if !typeErrors.is_empty() {
|
|
32
|
+
return Err(typeErrors.join("\n"));
|
|
33
|
+
}
|
|
34
|
+
let memberErrors = crate::unions::resolveMembers(source);
|
|
35
|
+
if !memberErrors.is_empty() {
|
|
36
|
+
return Err(memberErrors.join("\n"));
|
|
37
|
+
}
|
|
38
|
+
crate::forms::prepareForms(source);
|
|
39
|
+
Ok(())
|
|
40
|
+
}
|
plum-core/src/parser.rs
CHANGED
|
@@ -63,9 +63,9 @@ impl<'a> AstParser<'a> {
|
|
|
63
63
|
/// defined in another file resolves.
|
|
64
64
|
pub fn parseSource(&self, node: Node) -> Source {
|
|
65
65
|
let mut source = self.parseSourceRaw(node);
|
|
66
|
-
// Single-file path:
|
|
66
|
+
// Single-file path: errors are discarded; callers needing them call `lower::perFile` and `lower::frontEnd`.
|
|
67
|
-
let _ = crate::
|
|
67
|
+
let _ = crate::lower::perFile(&mut source);
|
|
68
|
-
crate::
|
|
68
|
+
let _ = crate::lower::frontEnd(&mut source);
|
|
69
69
|
source
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -1374,7 +1374,7 @@ impl<'a> AstParser<'a> {
|
|
|
1374
1374
|
"fn_call" => Expr::FnCall(self.parseFnCall(node)),
|
|
1375
1375
|
"class_call" => Expr::ClassCall(self.parseClassCall(node)),
|
|
1376
1376
|
"parenthesized_expression" => {
|
|
1377
|
-
// parenthesized_expression: "{" expression "}"
|
|
1377
|
+
// parenthesized_expression: "{" expression "}" — grouping only, no AST node
|
|
1378
1378
|
let inner = node
|
|
1379
1379
|
.named_child(0)
|
|
1380
1380
|
.map(|n| {
|
|
@@ -1382,7 +1382,7 @@ impl<'a> AstParser<'a> {
|
|
|
1382
1382
|
self.parseExpression(u)
|
|
1383
1383
|
})
|
|
1384
1384
|
.unwrap_or(Expr::Int(0));
|
|
1385
|
-
|
|
1385
|
+
inner
|
|
1386
1386
|
}
|
|
1387
1387
|
"string" => Expr::String(self.parseString(node)),
|
|
1388
1388
|
"integer" => Expr::Int(self.parseInteger(node)),
|
plum-core/src/purity.rs
CHANGED
|
@@ -372,7 +372,6 @@ impl<'a> Walker<'a> {
|
|
|
372
372
|
fn freshRoot(&self, e: &Expr) -> bool {
|
|
373
373
|
match e {
|
|
374
374
|
Expr::Var(n) => self.fresh.contains(n),
|
|
375
|
-
Expr::Paren(x) => self.freshRoot(x),
|
|
376
375
|
Expr::Attribute(a) => matches!(a.attr, AttrKind::Field(_)) && self.freshRoot(&a.object),
|
|
377
376
|
_ => false,
|
|
378
377
|
}
|
|
@@ -397,7 +396,7 @@ impl<'a> Walker<'a> {
|
|
|
397
396
|
self.expr(&b.left);
|
|
398
397
|
self.expr(&b.right);
|
|
399
398
|
}
|
|
400
|
-
Expr::Not(x) | Expr::Try(x)
|
|
399
|
+
Expr::Not(x) | Expr::Try(x) => self.expr(x),
|
|
401
400
|
Expr::Compare(c) => {
|
|
402
401
|
self.expr(&c.left);
|
|
403
402
|
self.expr(&c.right);
|
plum-core/src/reuse.rs
CHANGED
|
@@ -19,7 +19,6 @@ use std::collections::HashMap;
|
|
|
19
19
|
fn deepFresh(e: &Expr) -> bool {
|
|
20
20
|
match e {
|
|
21
21
|
Expr::ClassCall(c) => c.spread.is_none() && c.fields.iter().all(|f| deepFresh(&f.value)),
|
|
22
|
-
Expr::Paren(x) => deepFresh(x),
|
|
23
22
|
Expr::TypeName(_) | Expr::Int(_) | Expr::Float(_) | Expr::BoolLit(_) => true,
|
|
24
23
|
Expr::Binary(_) | Expr::Unary(_) | Expr::Compare(_) => true,
|
|
25
24
|
_ => false,
|
plum-core/src/simplify.rs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
//! blocks (`simplifyBlock`); `simplifyExpr` covers the expression-level rewrites.
|
|
7
7
|
use crate::ast::*;
|
|
8
8
|
use crate::rename::{renameStmts, Name};
|
|
9
|
-
use crate::visit::{
|
|
9
|
+
use crate::visit::{argRef, isTag, walkBlockRef, walkRef, childExprs, childRefs, stmtBlocks, stmtExprs, visitBlock};
|
|
10
10
|
use std::collections::{HashMap, HashSet};
|
|
11
11
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
12
12
|
|
|
@@ -74,11 +74,6 @@ struct Cx<'a> {
|
|
|
74
74
|
depth: usize,
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
/// Pattern casing rule: a Capitalised name is always a tag, a lowercase name always binds.
|
|
78
|
-
pub(crate) fn isTag(n: &str) -> bool {
|
|
79
|
-
n.chars().next().is_some_and(|c| c.is_uppercase())
|
|
80
|
-
}
|
|
81
|
-
|
|
82
77
|
fn trivial(e: &Expr) -> bool {
|
|
83
78
|
matches!(e, Expr::Var(_) | Expr::Int(_) | Expr::Float(_) | Expr::BoolLit(_) | Expr::TypeName(_))
|
|
84
79
|
}
|
|
@@ -274,10 +269,7 @@ impl<'a> Cx<'a> {
|
|
|
274
269
|
/// `ctor(..).f` -> the field's value, when every other field is droppable.
|
|
275
270
|
fn fieldOfCtor(&self, a: AttributeExpr) -> Result<Expr, AttributeExpr> {
|
|
276
271
|
let AttrKind::Field(f) = &a.attr else { return Err(a) };
|
|
277
|
-
let obj =
|
|
272
|
+
let obj = &a.object;
|
|
278
|
-
Expr::Paren(p) => p.as_ref(),
|
|
279
|
-
o => o,
|
|
280
|
-
};
|
|
281
273
|
let Expr::ClassCall(c) = obj else { return Err(a) };
|
|
282
274
|
if c.spread.is_some() || !c.fields.iter().any(|x| &x.name == f) {
|
|
283
275
|
return Err(a);
|
|
@@ -503,7 +495,6 @@ impl<'a> Cx<'a> {
|
|
|
503
495
|
/// `(ctor name, field values in declared order)` of a just-built value.
|
|
504
496
|
fn ctorOf(&self, e: &Expr) -> Option<(String, Vec<Expr>)> {
|
|
505
497
|
match e {
|
|
506
|
-
Expr::Paren(p) => self.ctorOf(p),
|
|
507
498
|
Expr::TypeName(n) | Expr::Var(n) if self.ctors.get(n).is_some_and(|f| f.is_empty()) => {
|
|
508
499
|
Some((n.clone(), vec![]))
|
|
509
500
|
}
|
|
@@ -820,30 +811,3 @@ impl Scoper {
|
|
|
820
811
|
}
|
|
821
812
|
}
|
|
822
813
|
}
|
|
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
|
@@ -189,7 +189,6 @@ fn expr(cx: &mut Ctx, e: &mut Expr, tail: bool) {
|
|
|
189
189
|
site(cx, c);
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
|
-
Expr::Paren(x) => expr(cx, x, tail),
|
|
193
192
|
Expr::InlineIf(i) => {
|
|
194
193
|
expr(cx, &mut i.condition, false);
|
|
195
194
|
expr(cx, &mut i.then, tail);
|
plum-core/src/tests_lower.rs
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
use crate::ast;
|
|
2
|
+
|
|
3
|
+
/// A source with its `test` blocks lowered into ordinary fns. Type-check
|
|
4
|
+
/// `source` (not the original), then compile it with `extraExports`.
|
|
5
|
+
pub struct TestProgram {
|
|
6
|
+
pub source: ast::Source,
|
|
7
|
+
pub extraExports: Vec<(String, String)>,
|
|
8
|
+
pub names: Vec<String>,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/// Temp names (`__lhs_N`) restart at 0 on every call.
|
|
12
|
+
pub fn lowerTests(source: &ast::Source) -> TestProgram {
|
|
13
|
+
let (lowered, extraExports) = desugarTestsToFns(source);
|
|
14
|
+
let names = extraExports
|
|
15
|
+
.iter()
|
|
16
|
+
.map(|(_, export_name)| export_name["test#".len()..].to_string())
|
|
17
|
+
.collect();
|
|
18
|
+
TestProgram {
|
|
19
|
+
source: lowered,
|
|
20
|
+
extraExports,
|
|
21
|
+
names,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn strLit(s: String) -> crate::ast::Expr {
|
|
26
|
+
crate::ast::Expr::String(crate::ast::StringExpr {
|
|
27
|
+
parts: vec![crate::ast::StringPart::Text(s)],
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Builds a failing `assert`'s report line: the literal condition text, plus —
|
|
32
|
+
/// when the condition is a top-level comparison (`left OP right`) — the
|
|
33
|
+
/// ACTUAL (left) and EXPECTED (right) sides' own runtime values underneath,
|
|
34
|
+
/// `assert computed() == literal` being the overwhelmingly common shape a
|
|
35
|
+
/// test writes such a comparison in. Reuses `"{expr}"` string interpolation's
|
|
36
|
+
/// own existing codegen (rather than any special stringification of its
|
|
37
|
+
/// own) — including `Float`, which interpolation now supports by rendering
|
|
38
|
+
/// through `Float.toStr` (`plum-std/Float.plum`), so no type needs
|
|
39
|
+
/// special-casing out here any more. A non-comparison condition (`assert
|
|
40
|
+
/// isEven(4)`) has no meaningful actual/expected split — a boolean condition
|
|
41
|
+
/// is always false by the time this fires — so it's just the bare text.
|
|
42
|
+
/// First part of an assert report string: tells `compileInterpolatedString`
|
|
43
|
+
/// that if any interpolated side has no renderable type (no `toStr`), only the
|
|
44
|
+
/// plain condition-text line (the next part) is emitted.
|
|
45
|
+
pub const ASSERT_REPORT_MARK: &str = "\u{1}assert-report";
|
|
46
|
+
|
|
47
|
+
fn assertFailureMessage(c: &crate::ast::Check, lhs: &crate::ast::Expr, rhs: &crate::ast::Expr) -> crate::ast::Expr {
|
|
48
|
+
let isNullLit =
|
|
49
|
+
|e: &crate::ast::Expr| matches!(e, crate::ast::Expr::ErrorValue(v) if v.name == crate::ast::NULL_ERR_NAME);
|
|
50
|
+
let plain = match &c.cond {
|
|
51
|
+
crate::ast::Expr::Compare(cmp) => isNullLit(&cmp.left) || isNullLit(&cmp.right),
|
|
52
|
+
_ => true,
|
|
53
|
+
};
|
|
54
|
+
if plain {
|
|
55
|
+
return strLit(format!("{}\n", c.text));
|
|
56
|
+
}
|
|
57
|
+
crate::ast::Expr::String(crate::ast::StringExpr {
|
|
58
|
+
parts: vec![
|
|
59
|
+
crate::ast::StringPart::Text(ASSERT_REPORT_MARK.to_string()),
|
|
60
|
+
crate::ast::StringPart::Text(format!("{}\n", c.text)),
|
|
61
|
+
crate::ast::StringPart::Text(" Expected: ".to_string()),
|
|
62
|
+
crate::ast::StringPart::Interp(rhs.clone()),
|
|
63
|
+
crate::ast::StringPart::Text("\n Actual: ".to_string()),
|
|
64
|
+
crate::ast::StringPart::Interp(lhs.clone()),
|
|
65
|
+
crate::ast::StringPart::Text("\n".to_string()),
|
|
66
|
+
],
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fn desugarTestsToFns(source: &crate::ast::Source) -> (crate::ast::Source, Vec<(String, String)>) {
|
|
71
|
+
let mut items = Vec::with_capacity(source.items.len());
|
|
72
|
+
let mut extraExports = Vec::new();
|
|
73
|
+
let mut test_idx = 0usize;
|
|
74
|
+
let mut counter = 0usize;
|
|
75
|
+
// Tests may use `io` when std/Io is loaded: the export wrapper passes the `Io`.
|
|
76
|
+
let hasIo = source.items.iter().any(|it| matches!(it, crate::ast::Item::Enum(c) if c.name == "Io"));
|
|
77
|
+
let ioParam = if hasIo {
|
|
78
|
+
vec![crate::ast::Param {
|
|
79
|
+
name: "io".to_string(),
|
|
80
|
+
ty: crate::ast::ParamType::Type(crate::ast::Type { name: "Io".to_string(), generics: vec![] }),
|
|
81
|
+
default: None,
|
|
82
|
+
}]
|
|
83
|
+
} else {
|
|
84
|
+
vec![]
|
|
85
|
+
};
|
|
86
|
+
for item in &source.items {
|
|
87
|
+
match item {
|
|
88
|
+
// Tests declared in imported files are not run: only the entry file's own.
|
|
89
|
+
crate::ast::Item::Test(t) if !t.file.is_empty() && t.file != source.file => {}
|
|
90
|
+
crate::ast::Item::Test(t) => {
|
|
91
|
+
// Leading "__" can never collide with a real Plum-declared name —
|
|
92
|
+
// `fn_identifier`/`var_identifier` must start with a lowercase letter.
|
|
93
|
+
let internal_name = format!("__test_{}", test_idx);
|
|
94
|
+
test_idx += 1;
|
|
95
|
+
|
|
96
|
+
let mut stmts = Vec::with_capacity(t.body.stmts.len() + 2);
|
|
97
|
+
stmts.push(crate::ast::Stmt::Assign(crate::ast::Assign {
|
|
98
|
+
isSynthetic: false,
|
|
99
|
+
targets: vec![crate::ast::AssignTarget::Var("__report".to_string())],
|
|
100
|
+
values: vec![strLit(String::new())],
|
|
101
|
+
declare: true,
|
|
102
|
+
ty: None,
|
|
103
|
+
}));
|
|
104
|
+
stmts.extend(desugarAssertStmts(&t.body.stmts, &mut counter));
|
|
105
|
+
stmts.push(crate::ast::Stmt::Expr(
|
|
106
|
+
crate::ast::Expr::Var("__report".to_string()),
|
|
107
|
+
0,
|
|
108
|
+
0,
|
|
109
|
+
));
|
|
110
|
+
|
|
111
|
+
items.push(crate::ast::Item::Fn(crate::ast::Fn {
|
|
112
|
+
file: source.file.clone(),
|
|
113
|
+
name: internal_name.clone(),
|
|
114
|
+
line: t.line,
|
|
115
|
+
column: t.column,
|
|
116
|
+
type_param: None,
|
|
117
|
+
is_extern: false,
|
|
118
|
+
is_export: false,
|
|
119
|
+
is_override: false,
|
|
120
|
+
is_star: false,
|
|
121
|
+
has_self: true,
|
|
122
|
+
yields: None,
|
|
123
|
+
params: ioParam.clone(),
|
|
124
|
+
returns: Some(crate::ast::Type {
|
|
125
|
+
name: crate::ast::STR_TYPE.to_string(),
|
|
126
|
+
generics: vec![],
|
|
127
|
+
}),
|
|
128
|
+
body: crate::ast::FnBody::Block(crate::ast::Block { stmts }),
|
|
129
|
+
}));
|
|
130
|
+
extraExports.push((internal_name, format!("test#{}", t.name)));
|
|
131
|
+
}
|
|
132
|
+
other => items.push(other.clone()),
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
(
|
|
136
|
+
crate::ast::Source {
|
|
137
|
+
file: source.file.clone(),
|
|
138
|
+
module: source.module.clone(),
|
|
139
|
+
imports: source.imports.clone(),
|
|
140
|
+
items,
|
|
141
|
+
topLevelOrder: Vec::new(),
|
|
142
|
+
formErrors: source.formErrors.clone(),
|
|
143
|
+
formUses: source.formUses.clone(),
|
|
144
|
+
stars: source.stars.clone(),
|
|
145
|
+
formLoops: source.formLoops,
|
|
146
|
+
primitives: source.primitives.clone(),
|
|
147
|
+
traits: source.traits.clone(),
|
|
148
|
+
},
|
|
149
|
+
extraExports,
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// Recursively rewrites every `assert cond` (`cond` carrying its own literal
|
|
154
|
+
/// source text, captured at parse time — see `crate::ast::Check`) into
|
|
155
|
+
/// `if !cond { __report = __report + "<text>\n" }`, descending into nested
|
|
156
|
+
/// control-flow blocks so an `assert` inside a `test`'s own
|
|
157
|
+
/// `if`/`while`/`for`/`match` still records into the same hidden accumulator.
|
|
158
|
+
fn desugarAssertStmts(stmts: &[crate::ast::Stmt], counter: &mut usize) -> Vec<crate::ast::Stmt> {
|
|
159
|
+
stmts.iter().flat_map(|s| desugarAssertStmt(s, counter)).collect()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fn desugarAssertStmt(stmt: &crate::ast::Stmt, counter: &mut usize) -> Vec<crate::ast::Stmt> {
|
|
163
|
+
match stmt {
|
|
164
|
+
crate::ast::Stmt::Assert(c) => {
|
|
165
|
+
// A comparison evaluates each operand exactly once into a
|
|
166
|
+
// temporary (left first), then compares and reports the temporaries.
|
|
167
|
+
let mut pre = Vec::new();
|
|
168
|
+
let mut cond = c.cond.clone();
|
|
169
|
+
let (mut lhs, mut rhs) = (crate::ast::Expr::Var(String::new()), crate::ast::Expr::Var(String::new()));
|
|
170
|
+
let isNullLit = |e: &crate::ast::Expr| matches!(e, crate::ast::Expr::ErrorValue(v) if v.name == crate::ast::NULL_ERR_NAME);
|
|
171
|
+
// `x == null` has no value to print for the null side: reported as a plain condition.
|
|
172
|
+
if let crate::ast::Expr::Compare(cmp) = &c.cond {
|
|
173
|
+
if isNullLit(&cmp.left) || isNullLit(&cmp.right) {
|
|
174
|
+
// falls through with the condition kept whole
|
|
175
|
+
} else {
|
|
176
|
+
let n = *counter;
|
|
177
|
+
*counter += 1;
|
|
178
|
+
let (l, r) = (format!("__lhs_{}", n), format!("__rhs_{}", n));
|
|
179
|
+
for (name, e) in [(&l, &cmp.left), (&r, &cmp.right)] {
|
|
180
|
+
pre.push(crate::ast::Stmt::Assign(crate::ast::Assign {
|
|
181
|
+
isSynthetic: false,
|
|
182
|
+
targets: vec![crate::ast::AssignTarget::Var(name.clone())],
|
|
183
|
+
values: vec![e.clone()],
|
|
184
|
+
declare: true,
|
|
185
|
+
ty: None,
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
lhs = crate::ast::Expr::Var(l);
|
|
189
|
+
rhs = crate::ast::Expr::Var(r);
|
|
190
|
+
cond = crate::ast::Expr::Compare(Box::new(crate::ast::CompareExpr {
|
|
191
|
+
op: cmp.op.clone(),
|
|
192
|
+
left: lhs.clone(),
|
|
193
|
+
right: rhs.clone(),
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
let appendReport = crate::ast::Stmt::Assign(crate::ast::Assign {
|
|
198
|
+
isSynthetic: false,
|
|
199
|
+
targets: vec![crate::ast::AssignTarget::Var("__report".to_string())],
|
|
200
|
+
values: vec![crate::ast::Expr::Binary(Box::new(crate::ast::BinaryExpr {
|
|
201
|
+
op: crate::ast::BinOp::Add,
|
|
202
|
+
left: crate::ast::Expr::Var("__report".to_string()),
|
|
203
|
+
right: assertFailureMessage(c, &lhs, &rhs),
|
|
204
|
+
}))],
|
|
205
|
+
declare: false,
|
|
206
|
+
ty: None,
|
|
207
|
+
});
|
|
208
|
+
pre.push(crate::ast::Stmt::If(crate::ast::If {
|
|
209
|
+
condition: crate::ast::Expr::Not(Box::new(cond)),
|
|
210
|
+
body: crate::ast::Block {
|
|
211
|
+
stmts: vec![appendReport],
|
|
212
|
+
},
|
|
213
|
+
else_ifs: vec![],
|
|
214
|
+
else_: None,
|
|
215
|
+
}));
|
|
216
|
+
pre
|
|
217
|
+
}
|
|
218
|
+
crate::ast::Stmt::If(i) => vec![crate::ast::Stmt::If(crate::ast::If {
|
|
219
|
+
condition: i.condition.clone(),
|
|
220
|
+
body: crate::ast::Block {
|
|
221
|
+
stmts: desugarAssertStmts(&i.body.stmts, counter),
|
|
222
|
+
},
|
|
223
|
+
else_ifs: i
|
|
224
|
+
.else_ifs
|
|
225
|
+
.iter()
|
|
226
|
+
.map(|ei| crate::ast::ElseIf {
|
|
227
|
+
condition: ei.condition.clone(),
|
|
228
|
+
body: crate::ast::Block {
|
|
229
|
+
stmts: desugarAssertStmts(&ei.body.stmts, counter),
|
|
230
|
+
},
|
|
231
|
+
})
|
|
232
|
+
.collect(),
|
|
233
|
+
else_: i.else_.as_ref().map(|b| crate::ast::Block {
|
|
234
|
+
stmts: desugarAssertStmts(&b.stmts, counter),
|
|
235
|
+
}),
|
|
236
|
+
})],
|
|
237
|
+
crate::ast::Stmt::While(w) => vec![crate::ast::Stmt::While(crate::ast::While {
|
|
238
|
+
condition: w.condition.clone(),
|
|
239
|
+
body: crate::ast::Block {
|
|
240
|
+
stmts: desugarAssertStmts(&w.body.stmts, counter),
|
|
241
|
+
},
|
|
242
|
+
})],
|
|
243
|
+
crate::ast::Stmt::ForLoop(f) => vec![crate::ast::Stmt::ForLoop(crate::ast::ForLoop {
|
|
244
|
+
body: crate::ast::Block {
|
|
245
|
+
stmts: desugarAssertStmts(&f.body.stmts, counter),
|
|
246
|
+
},
|
|
247
|
+
..f.clone()
|
|
248
|
+
})],
|
|
249
|
+
crate::ast::Stmt::Match(m) => vec![crate::ast::Stmt::Match(crate::ast::Match {
|
|
250
|
+
subjects: m.subjects.clone(),
|
|
251
|
+
cases: m
|
|
252
|
+
.cases
|
|
253
|
+
.iter()
|
|
254
|
+
.map(|c| crate::ast::Case {
|
|
255
|
+
patterns: c.patterns.clone(),
|
|
256
|
+
guard: c.guard.clone(),
|
|
257
|
+
body: crate::ast::Block {
|
|
258
|
+
stmts: desugarAssertStmts(&c.body.stmts, counter),
|
|
259
|
+
},
|
|
260
|
+
})
|
|
261
|
+
.collect(),
|
|
262
|
+
})],
|
|
263
|
+
other => vec![other.clone()],
|
|
264
|
+
}
|
|
265
|
+
}
|
plum-core/src/trmc.rs
CHANGED
|
@@ -143,7 +143,6 @@ fn assignsSelfField(body: &FnBody) -> bool {
|
|
|
143
143
|
fn rootedAtSelf(e: &Expr) -> bool {
|
|
144
144
|
match e {
|
|
145
145
|
Expr::Self_ => true,
|
|
146
|
-
Expr::Paren(x) => rootedAtSelf(x),
|
|
147
146
|
Expr::Attribute(a) => matches!(a.attr, AttrKind::Field(_)) && rootedAtSelf(&a.object),
|
|
148
147
|
_ => false,
|
|
149
148
|
}
|
|
@@ -251,7 +250,6 @@ fn set(n: &str, e: Expr) -> Stmt {
|
|
|
251
250
|
fn pure(e: &Expr) -> bool {
|
|
252
251
|
match e {
|
|
253
252
|
Expr::Var(_) | Expr::Int(_) | Expr::Float(_) | Expr::BoolLit(_) => true,
|
|
254
|
-
Expr::Paren(x) => pure(x),
|
|
255
253
|
_ => false,
|
|
256
254
|
}
|
|
257
255
|
}
|
|
@@ -383,7 +381,6 @@ impl<'a> Cx<'a> {
|
|
|
383
381
|
|
|
384
382
|
fn tailExpr(&mut self, e: Expr) -> Option<Vec<Stmt>> {
|
|
385
383
|
match e {
|
|
386
|
-
Expr::Paren(x) => self.tailExpr(*x),
|
|
387
384
|
Expr::InlineIf(i) => {
|
|
388
385
|
let i = *i;
|
|
389
386
|
let then = Block { stmts: self.tailExpr(i.then)? };
|
plum-core/src/union_lower.rs
CHANGED
|
@@ -272,7 +272,7 @@ impl<'a> Lowerer<'a> {
|
|
|
272
272
|
renameExpr(&mut b.right, &leftPairs);
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
|
-
Expr::Not(x) | Expr::Try(x)
|
|
275
|
+
Expr::Not(x) | Expr::Try(x) => self.expr(x),
|
|
276
276
|
Expr::Compare(c) => {
|
|
277
277
|
self.expr(&mut c.left);
|
|
278
278
|
self.expr(&mut c.right);
|
|
@@ -408,7 +408,6 @@ fn condBinds(e: &Expr, name: &str) -> bool {
|
|
|
408
408
|
match e {
|
|
409
409
|
Expr::Bind(b) => patBinds(b.pattern(), name),
|
|
410
410
|
Expr::Bool(b) => condBinds(&b.left, name) || condBinds(&b.right, name),
|
|
411
|
-
Expr::Paren(x) => condBinds(x, name),
|
|
412
411
|
_ => false,
|
|
413
412
|
}
|
|
414
413
|
}
|
|
@@ -530,7 +529,7 @@ fn rnExpr(e: &mut Expr, from: &str, to: &str) {
|
|
|
530
529
|
rnExpr(&mut b.right, from, to);
|
|
531
530
|
}
|
|
532
531
|
}
|
|
533
|
-
Expr::Not(x) | Expr::Try(x)
|
|
532
|
+
Expr::Not(x) | Expr::Try(x) => rnExpr(x, from, to),
|
|
534
533
|
Expr::Compare(c) => {
|
|
535
534
|
rnExpr(&mut c.left, from, to);
|
|
536
535
|
rnExpr(&mut c.right, from, to);
|
plum-core/src/unions.rs
CHANGED
|
@@ -413,7 +413,7 @@ impl TypeWalk<'_> {
|
|
|
413
413
|
self.expr(&b.left);
|
|
414
414
|
self.expr(&b.right);
|
|
415
415
|
}
|
|
416
|
-
Expr::Not(e) | Expr::Try(e)
|
|
416
|
+
Expr::Not(e) | Expr::Try(e) => self.expr(e),
|
|
417
417
|
Expr::Bang(b) => self.expr(&b.value),
|
|
418
418
|
Expr::Bind(b) => {
|
|
419
419
|
self.expr(&b.value);
|
plum-core/src/uniqueness.rs
CHANGED
|
@@ -122,7 +122,7 @@ fn walkExpr<'a>(e: &'a Expr, f: Visit<'a, '_>) {
|
|
|
122
122
|
walkExpr(&c.left, f);
|
|
123
123
|
walkExpr(&c.right, f);
|
|
124
124
|
}
|
|
125
|
-
Expr::Not(x) | Expr::Try(x)
|
|
125
|
+
Expr::Not(x) | Expr::Try(x) => walkExpr(x, f),
|
|
126
126
|
Expr::InlineIf(i) => {
|
|
127
127
|
walkExpr(&i.condition, f);
|
|
128
128
|
walkExpr(&i.then, f);
|
|
@@ -263,7 +263,7 @@ fn mutExpr(e: &mut Expr, f: &mut dyn FnMut(&mut ClassCall)) {
|
|
|
263
263
|
mutExpr(&mut c.left, f);
|
|
264
264
|
mutExpr(&mut c.right, f);
|
|
265
265
|
}
|
|
266
|
-
Expr::Not(x) | Expr::Try(x)
|
|
266
|
+
Expr::Not(x) | Expr::Try(x) => mutExpr(x, f),
|
|
267
267
|
Expr::InlineIf(i) => {
|
|
268
268
|
mutExpr(&mut i.condition, f);
|
|
269
269
|
mutExpr(&mut i.then, f);
|
|
@@ -380,7 +380,7 @@ fn isVar(e: &Expr, v: &str) -> bool {
|
|
|
380
380
|
|
|
381
381
|
/// The name of a plain variable use; `self` counts as the variable "self".
|
|
382
382
|
fn varName(e: &Expr) -> Option<&str> {
|
|
383
|
-
match
|
|
383
|
+
match e {
|
|
384
384
|
Expr::Var(n) => Some(n.as_str()),
|
|
385
385
|
Expr::Self_ => Some("self"),
|
|
386
386
|
_ => None,
|
|
@@ -446,7 +446,6 @@ impl An {
|
|
|
446
446
|
fn fresh(&self, e: &Expr) -> bool {
|
|
447
447
|
match e {
|
|
448
448
|
Expr::ClassCall(_) => true,
|
|
449
|
-
Expr::Paren(x) => self.fresh(x),
|
|
450
449
|
Expr::InlineIf(i) => self.fresh(&i.then) && self.fresh(&i.else_),
|
|
451
450
|
Expr::FnCall(c) => self.isFree(&c.name).is_some_and(|g| self.fresh_ret.borrow().contains(&g)),
|
|
452
451
|
Expr::Attribute(a) => match &a.attr {
|
|
@@ -551,7 +550,7 @@ impl An {
|
|
|
551
550
|
if site.inClosure {
|
|
552
551
|
return Err("argument inside a closure".to_string());
|
|
553
552
|
}
|
|
554
|
-
match
|
|
553
|
+
match e {
|
|
555
554
|
Expr::Var(_) | Expr::Self_ => self.varOk(site.caller, varName(e).unwrap()),
|
|
556
555
|
Expr::Attribute(a) => {
|
|
557
556
|
let AttrKind::Field(f) = &a.attr else {
|
|
@@ -783,7 +782,6 @@ impl<'a> Scan<'a> {
|
|
|
783
782
|
self.fail(m);
|
|
784
783
|
}
|
|
785
784
|
}
|
|
786
|
-
Expr::Paren(x) => self.expr(x, tail, ab),
|
|
787
785
|
Expr::InlineIf(i) => {
|
|
788
786
|
self.expr(&i.condition, false, false);
|
|
789
787
|
self.expr(&i.then, tail, ab);
|
|
@@ -1093,7 +1091,7 @@ fn collectFacts(an: &mut An) {
|
|
|
1093
1091
|
walkBlock(&u.block, &mut |n| {
|
|
1094
1092
|
match n {
|
|
1095
1093
|
Node::S(Stmt::Assign(asg)) if asg.targets.len() == 1 && asg.values.len() == 1 => {
|
|
1096
|
-
match
|
|
1094
|
+
match &asg.values[0] {
|
|
1097
1095
|
Expr::FnCall(c) => {
|
|
1098
1096
|
abMap.insert(c as *const FnCall, asg.targets[0].clone());
|
|
1099
1097
|
}
|
|
@@ -1164,7 +1162,7 @@ fn collectFacts(an: &mut An) {
|
|
|
1164
1162
|
allowed.insert(&**o as *const Expr);
|
|
1165
1163
|
}
|
|
1166
1164
|
if single {
|
|
1167
|
-
let call: Option<(&FnCall, Option<&Expr>)> = match
|
|
1165
|
+
let call: Option<(&FnCall, Option<&Expr>)> = match &asg.values[0] {
|
|
1168
1166
|
Expr::FnCall(c) => Some((c, None)),
|
|
1169
1167
|
Expr::Attribute(x) => match &x.attr {
|
|
1170
1168
|
AttrKind::Method(c) => Some((c, Some(&x.object))),
|
plum-core/src/visit.rs
CHANGED
|
@@ -3,13 +3,6 @@
|
|
|
3
3
|
//! `visitBlock`/`visitExpr`/`anyExpr`/`countVar`, not by `childExprs`/`childRefs`.
|
|
4
4
|
use crate::ast::*;
|
|
5
5
|
|
|
6
|
-
pub fn unparen(e: &Expr) -> &Expr {
|
|
7
|
-
match e {
|
|
8
|
-
Expr::Paren(i) => unparen(i),
|
|
9
|
-
_ => e,
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
6
|
pub fn visitBlock(b: &mut Block, f: &mut dyn FnMut(&mut Expr)) {
|
|
14
7
|
for s in &mut b.stmts {
|
|
15
8
|
for e in stmtExprs(s) {
|
|
@@ -49,7 +42,7 @@ pub fn childExprs(e: &mut Expr) -> Vec<&mut Expr> {
|
|
|
49
42
|
Expr::Binary(b) => vec![&mut b.left, &mut b.right],
|
|
50
43
|
Expr::Unary(u) => vec![&mut u.operand],
|
|
51
44
|
Expr::Bool(b) => vec![&mut b.left, &mut b.right],
|
|
52
|
-
Expr::Not(x) | Expr::Try(x)
|
|
45
|
+
Expr::Not(x) | Expr::Try(x) => vec![x.as_mut()],
|
|
53
46
|
Expr::Bang(b) => vec![&mut b.value],
|
|
54
47
|
Expr::Bind(b) => {
|
|
55
48
|
let mut v = vec![&mut b.value];
|
|
@@ -155,7 +148,7 @@ pub fn stmtBlocks(s: &mut Stmt) -> Vec<&mut Block> {
|
|
|
155
148
|
}
|
|
156
149
|
}
|
|
157
150
|
|
|
158
|
-
fn argRef(a: &Arg) -> &Expr {
|
|
151
|
+
pub fn argRef(a: &Arg) -> &Expr {
|
|
159
152
|
match a {
|
|
160
153
|
Arg::Positional(e) | Arg::Keyword { value: e, .. } => e,
|
|
161
154
|
}
|
|
@@ -166,7 +159,7 @@ pub fn childRefs(e: &Expr) -> Vec<&Expr> {
|
|
|
166
159
|
Expr::Binary(b) => vec![&b.left, &b.right],
|
|
167
160
|
Expr::Unary(u) => vec![&u.operand],
|
|
168
161
|
Expr::Bool(b) => vec![&b.left, &b.right],
|
|
169
|
-
Expr::Not(x) | Expr::Try(x)
|
|
162
|
+
Expr::Not(x) | Expr::Try(x) => vec![x.as_ref()],
|
|
170
163
|
Expr::Bang(b) => vec![&b.value],
|
|
171
164
|
Expr::Bind(b) => {
|
|
172
165
|
let mut v = vec![&b.value];
|
|
@@ -334,3 +327,28 @@ pub fn countVar(body: &FnBody, name: &str) -> usize {
|
|
|
334
327
|
FnBody::Extern => 0,
|
|
335
328
|
}
|
|
336
329
|
}
|
|
330
|
+
|
|
331
|
+
/// Pattern casing rule: a Capitalised name is always a tag, a lowercase name always binds.
|
|
332
|
+
pub fn isTag(n: &str) -> bool {
|
|
333
|
+
n.chars().next().is_some_and(|c| c.is_uppercase())
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/// Pre-order callback over every expression of `b`, closure bodies and catch blocks included.
|
|
337
|
+
pub fn walkBlockRef(b: &Block, f: &mut dyn FnMut(&Expr)) {
|
|
338
|
+
for s in &b.stmts {
|
|
339
|
+
for e in stmtExprRefs(s) {
|
|
340
|
+
walkRef(e, f);
|
|
341
|
+
}
|
|
342
|
+
for blk in stmtBlockRefs(s) {
|
|
343
|
+
walkBlockRef(blk, f);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/// Pre-order callback over `e` and everything under it.
|
|
349
|
+
pub fn walkRef(e: &Expr, f: &mut dyn FnMut(&Expr)) {
|
|
350
|
+
anyExpr(e, &mut |x| {
|
|
351
|
+
f(x);
|
|
352
|
+
false
|
|
353
|
+
});
|
|
354
|
+
}
|
plum-core/src/whistle.rs
CHANGED
|
@@ -32,7 +32,6 @@ fn argExprs(args: &[Arg]) -> (String, Vec<&Expr>) {
|
|
|
32
32
|
fn view(e: &Expr) -> View<'_> {
|
|
33
33
|
use View::*;
|
|
34
34
|
match e {
|
|
35
|
-
Expr::Paren(inner) => view(inner),
|
|
36
35
|
Expr::Var(_) | Expr::Self_ => Var,
|
|
37
36
|
Expr::Binary(b) => Node(format!("bin:{:?}", b.op), vec![&b.left, &b.right]),
|
|
38
37
|
Expr::Unary(u) => Node(format!("un:{:?}", u.op), vec![&u.operand]),
|
plum-core/tests/inline_variants_test.rs
CHANGED
|
@@ -107,11 +107,11 @@ fn type_position_errors() {
|
|
|
107
107
|
"fun fa(gg: fn(Cons[Int]) -> Int) -> Int = 1\n",
|
|
108
108
|
"fun fa() -> Int =\n cc: Cons[Int] := todo\n 1\n",
|
|
109
109
|
] {
|
|
110
|
-
let mut s =
|
|
110
|
+
let mut s = plum_test_support::parseRawSource(&format!("enum Link[T] = Nil | Cons(head: T)\n{src}"));
|
|
111
111
|
assert!(hoistInlineVariants(&mut s).is_empty());
|
|
112
112
|
assert_eq!(inlineVariantTypeErrors(&s), msg, "{src}");
|
|
113
113
|
}
|
|
114
|
-
let mut s =
|
|
114
|
+
let mut s = plum_test_support::parseRawSource(
|
|
115
115
|
"enum Link[T] = Nil | Cons(head: T)\nfun fa(cc: Link[Int]) -> Link[Int] = Cons(head: 1)\nfun gb(cc: Link[Int]) -> Int = match cc\n Cons(head) => head\n Nil => 0\n",
|
|
116
116
|
);
|
|
117
117
|
assert!(hoistInlineVariants(&mut s).is_empty());
|
plum-core/tests/loader_test.rs
CHANGED
|
@@ -532,3 +532,34 @@ fn nestedStdImportResolvesToASubdirectory() {
|
|
|
532
532
|
);
|
|
533
533
|
assert!(loadAndMerge(&missing, &lib_path).is_err());
|
|
534
534
|
}
|
|
535
|
+
|
|
536
|
+
/// Import-free programs (every plum-examples file imports std, so small inline programs are used):
|
|
537
|
+
/// the single-file `parseSource` front end must give the same AST as the loader.
|
|
538
|
+
#[test]
|
|
539
|
+
fn single_file_equals_loader() {
|
|
540
|
+
let programs = [
|
|
541
|
+
"fun id(a: Unit) -> Unit = a\nfun main(u: Unit) = id(u)\n",
|
|
542
|
+
"record Pt(x: Unit, y: Unit)\nfun mk(u: Unit) = Pt(x: u, y: u)\n",
|
|
543
|
+
"trait Actor(x: Unit)\nrecord Boss(Actor(x), hp: Unit)\nfun main(u: Unit) = Boss(x: u, hp: u)\n",
|
|
544
|
+
"enum Shape = Circle | Dot\nfun f(s: Shape) = s\n",
|
|
545
|
+
"enum Shape = Circle(r: Unit) | Sq\nfun f(s: Shape) = s\n",
|
|
546
|
+
];
|
|
547
|
+
for src in programs {
|
|
548
|
+
let dir = TempDir::new().unwrap();
|
|
549
|
+
let entry = dir.path().join("main.plum");
|
|
550
|
+
fs::write(&entry, src).unwrap();
|
|
551
|
+
let mut parser = tree_sitter::Parser::new();
|
|
552
|
+
parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
|
|
553
|
+
let tree = parser.parse(src, None).unwrap();
|
|
554
|
+
let single = plum_core::AstParser::new(src).parseSource(tree.root_node());
|
|
555
|
+
let merged = loadAndMerge(&entry, dir.path()).expect("loadAndMerge failed");
|
|
556
|
+
// The loader stamps each item with its path; blank those out before comparing.
|
|
557
|
+
let norm = |x: &plum_core::ast::Source| {
|
|
558
|
+
// the loader drops `topLevelOrder` after merging; items and traits are the lowered program
|
|
559
|
+
let d = format!("{:?} {:?}", x.items, x.traits);
|
|
560
|
+
d.replace(&format!("file: {:?}", entry.to_string_lossy()), "file: \"\"")
|
|
561
|
+
.replace(&format!("file: {:?}", fs::canonicalize(&entry).unwrap().to_string_lossy()), "file: \"\"")
|
|
562
|
+
};
|
|
563
|
+
assert_eq!(norm(&single), norm(&merged), "mismatch for {src}");
|
|
564
|
+
}
|
|
565
|
+
}
|
plum-core/tests/lower_test.rs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
use plum_core::ast::*;
|
|
3
|
+
use plum_core::lower::frontEnd;
|
|
4
|
+
|
|
5
|
+
#[test]
|
|
6
|
+
fn lowersTraits() {
|
|
7
|
+
let mut s = plum_test_support::parseRawSource(
|
|
8
|
+
"trait Actor =\n fun up(self)\n\nrecord Hut(Actor, k: Int) =\n override fun up(self) -> Int = 1\n",
|
|
9
|
+
);
|
|
10
|
+
assert_eq!(frontEnd(&mut s), Ok(()));
|
|
11
|
+
assert!(!s.items.iter().any(|i| matches!(i, Item::Trait(_))));
|
|
12
|
+
assert_eq!(s.traits.len(), 1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#[test]
|
|
16
|
+
fn unknownMemberErrors() {
|
|
17
|
+
let mut s = plum_test_support::parseRawSource("enum Shape = Nope[Int] | Red");
|
|
18
|
+
assert_eq!(frontEnd(&mut s), Err("unknown member type Nope".to_string()));
|
|
19
|
+
}
|
plum-core/tests/parser_test.rs
CHANGED
|
@@ -516,8 +516,7 @@ fn bang_vs_neq_lexing() {
|
|
|
516
516
|
let Expr::Compare(c) = bodyExpr(onlyFn(&s)) else {
|
|
517
517
|
panic!()
|
|
518
518
|
};
|
|
519
|
-
let Expr::Paren(p) = &c.left else { panic!() };
|
|
520
|
-
assert!(matches!(
|
|
519
|
+
assert!(matches!(c.left, Expr::Bang(_)));
|
|
521
520
|
}
|
|
522
521
|
|
|
523
522
|
// `catch` has the lowest precedence of any expression form: its default
|
|
@@ -953,7 +952,7 @@ fn one_line_generic_member() {
|
|
|
953
952
|
|
|
954
953
|
#[test]
|
|
955
954
|
fn inline_payload_still_parses() {
|
|
956
|
-
let source =
|
|
955
|
+
let source = parseRaw("enum Sh =\n | Circle(r: Float)\n | Dot(Int)\n");
|
|
957
956
|
let e = onlyEnum(&source);
|
|
958
957
|
assert!(e.variants[0].member.is_none());
|
|
959
958
|
assert_eq!(e.variants[0].fields.len(), 1);
|
|
@@ -1036,3 +1035,12 @@ fn field_less_record_parses() {
|
|
|
1036
1035
|
assert_eq!(e.variants[0].fields.len(), 0);
|
|
1037
1036
|
}
|
|
1038
1037
|
}
|
|
1038
|
+
|
|
1039
|
+
// Grouping braces leave no AST node, but precedence holds: `{1 + 2} * 3` is (1 + 2) * 3.
|
|
1040
|
+
#[test]
|
|
1041
|
+
fn groupingKeepsPrecedenceWithoutNode() {
|
|
1042
|
+
let s = parse("fun f() -> Int = {1 + 2} * 3\n");
|
|
1043
|
+
let Expr::Binary(b) = bodyExpr(onlyFn(&s)) else { panic!() };
|
|
1044
|
+
assert!(matches!(b.left, Expr::Binary(_)));
|
|
1045
|
+
assert!(matches!(b.right, Expr::Int(3)));
|
|
1046
|
+
}
|
plum-core/tests/tests_lower_test.rs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
use plum_core::ast::*;
|
|
3
|
+
use plum_core::tests_lower::{lowerTests, ASSERT_REPORT_MARK};
|
|
4
|
+
|
|
5
|
+
const SRC: &str = "test \"a\"\n assert 1 + 1 == 2\n assert 2 == 2\n";
|
|
6
|
+
|
|
7
|
+
fn fnNamed<'a>(s: &'a Source, n: &str) -> &'a Fn {
|
|
8
|
+
s.items.iter().find_map(|i| match i { Item::Fn(f) if f.name == n => Some(f), _ => None }).unwrap()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
#[test]
|
|
12
|
+
fn lowersOneTest() {
|
|
13
|
+
let src = plum_test_support::parseRawSource(SRC);
|
|
14
|
+
let p = lowerTests(&src);
|
|
15
|
+
assert_eq!(p.names, vec!["a"]);
|
|
16
|
+
assert_eq!(p.extraExports, vec![("__test_0".to_string(), "test#a".to_string())]);
|
|
17
|
+
assert!(!p.source.items.iter().any(|i| matches!(i, Item::Test(_))));
|
|
18
|
+
fnNamed(&p.source, "__test_0");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#[test]
|
|
22
|
+
fn assertNamesPerCompile() {
|
|
23
|
+
let src = plum_test_support::parseRawSource(SRC);
|
|
24
|
+
let a = lowerTests(&src);
|
|
25
|
+
let b = lowerTests(&src);
|
|
26
|
+
assert_eq!(format!("{:?}", fnNamed(&a.source, "__test_0").body), format!("{:?}", fnNamed(&b.source, "__test_0").body));
|
|
27
|
+
let dbg = format!("{:?}", fnNamed(&a.source, "__test_0").body);
|
|
28
|
+
assert!(dbg.contains("__lhs_0") && dbg.contains("__rhs_1") && !dbg.contains("__lhs_2"));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[test]
|
|
32
|
+
fn failureMessageText() {
|
|
33
|
+
let src = plum_test_support::parseRawSource(SRC);
|
|
34
|
+
let dbg = format!("{:?}", fnNamed(&lowerTests(&src).source, "__test_0").body);
|
|
35
|
+
assert!(dbg.contains("assert-report"));
|
|
36
|
+
assert!(dbg.contains(" Expected: "));
|
|
37
|
+
assert!(dbg.contains("\\n Actual: "));
|
|
38
|
+
assert_eq!(ASSERT_REPORT_MARK, "\u{1}assert-report");
|
|
39
|
+
}
|
plum-core/tests/uniqueness_test.rs
CHANGED
|
@@ -44,7 +44,6 @@ fn super_walk(b: &mut Block, found: &mut bool) {
|
|
|
44
44
|
*found = true;
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
Expr::Paren(x) => ex(x, found),
|
|
48
47
|
Expr::InlineIf(i) => {
|
|
49
48
|
ex(&i.then, found);
|
|
50
49
|
ex(&i.else_, found);
|
plum-core/tests/visit_test.rs
CHANGED
|
@@ -80,9 +80,3 @@ fn countVarSeesClosures() {
|
|
|
80
80
|
let src = "fun t(x: Int) -> Int =\n f := |y| y + x\n return x + f(x)\n";
|
|
81
81
|
assert_eq!(countVar(&fnBody(src), "x"), 3);
|
|
82
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/tests/host_test.rs
CHANGED
|
@@ -9,8 +9,7 @@ fn root() -> PathBuf {
|
|
|
9
9
|
|
|
10
10
|
fn compile(src_path: &Path) -> Vec<u8> {
|
|
11
11
|
let source = plum_core::loadAndMerge(src_path, &root()).expect("load");
|
|
12
|
-
let typed = plum_wasm_codegen::typeCheckIn(&source, &src_path.display().to_string()).expect("typecheck");
|
|
13
|
-
plum_wasm_codegen::
|
|
12
|
+
plum_wasm_codegen::compile(&source, &plum_wasm_codegen::CompileOptions::default()).expect("compile").wasm
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
fn fixture() -> Vec<u8> {
|
plum-lsp/src/completion.rs
CHANGED
|
@@ -610,7 +610,7 @@ pub fn stdLibIndex(lib_path: &Path) -> HashMap<String, String> {
|
|
|
610
610
|
let Some(tree) = parser.parse(&text, None) else {
|
|
611
611
|
continue;
|
|
612
612
|
};
|
|
613
|
-
let source = plum_core::AstParser::new(&text).
|
|
613
|
+
let source = plum_core::AstParser::new(&text).parseSourceRaw(tree.root_node());
|
|
614
614
|
for item in &source.items {
|
|
615
615
|
let name = match item {
|
|
616
616
|
ast::Item::Fn(f) if f.type_param.is_none() => Some(f.name.clone()),
|
plum-lsp/src/main.rs
CHANGED
|
@@ -113,7 +113,7 @@ impl Backend {
|
|
|
113
113
|
let line_text = text.lines().nth(pos.line as usize).unwrap_or("");
|
|
114
114
|
let sanitized = completion::sanitizeForParsing(&text, pos);
|
|
115
115
|
let tree = Self::parseTree(&sanitized);
|
|
116
|
-
let own_source = plum_core::AstParser::new(&sanitized).
|
|
116
|
+
let own_source = plum_core::AstParser::new(&sanitized).parseSourceRaw(tree.root_node());
|
|
117
117
|
|
|
118
118
|
if let Some(receiver) = completion::receiverBeforeDot(line_text, pos.character as usize) {
|
|
119
119
|
let Ok(path) = uri.to_file_path() else {
|
|
@@ -354,7 +354,7 @@ fn memberCompletionFor(
|
|
|
354
354
|
return Vec::new();
|
|
355
355
|
};
|
|
356
356
|
let tree = Backend::parseTree(text);
|
|
357
|
-
let own_source = plum_core::AstParser::new(text).
|
|
357
|
+
let own_source = plum_core::AstParser::new(text).parseSourceRaw(tree.root_node());
|
|
358
358
|
// `Type.`: its associated consts and static functions.
|
|
359
359
|
if receiver
|
|
360
360
|
.chars()
|
|
@@ -432,7 +432,7 @@ fn inheritedFrom(
|
|
|
432
432
|
}
|
|
433
433
|
};
|
|
434
434
|
let tree = Backend::parseTree(&content);
|
|
435
|
-
let src = plum_core::AstParser::new(&content).
|
|
435
|
+
let src = plum_core::AstParser::new(&content).parseSourceRaw(tree.root_node());
|
|
436
436
|
for i in &src.items {
|
|
437
437
|
if let ast::Item::Enum(e) = i {
|
|
438
438
|
if e.isRecord {
|
|
@@ -553,7 +553,7 @@ fn resolveTarget(
|
|
|
553
553
|
.unwrap_or(0);
|
|
554
554
|
let receiver = &before[rs..];
|
|
555
555
|
let tree = Backend::parseTree(text);
|
|
556
|
-
let own = plum_core::AstParser::new(text).
|
|
556
|
+
let own = plum_core::AstParser::new(text).parseSourceRaw(tree.root_node());
|
|
557
557
|
let (global_env, methods, enum_variants, enum_params) =
|
|
558
558
|
plum_checker::buildGlobalTables(&merged);
|
|
559
559
|
let min_required = plum_checker::buildMinRequiredArgs(&merged);
|
|
@@ -624,7 +624,7 @@ fn recordHover(text: &str, line0: usize, col: usize) -> Option<String> {
|
|
|
624
624
|
let (s, e) = identAt(lineText, col)?;
|
|
625
625
|
let word = &lineText[s..e];
|
|
626
626
|
let tree = Backend::parseTree(text);
|
|
627
|
-
let own = plum_core::AstParser::new(text).
|
|
627
|
+
let own = plum_core::AstParser::new(text).parseSourceRaw(tree.root_node());
|
|
628
628
|
let is = own.items.iter().any(|i| {
|
|
629
629
|
matches!(i, ast::Item::Enum(en) if en.name == word && (en.isRecord || en.isValue) && !en.isTrait)
|
|
630
630
|
});
|
|
@@ -841,7 +841,7 @@ mod tests {
|
|
|
841
841
|
fn complete_ctor_offers_only_ctor_params() {
|
|
842
842
|
let src = "module t\n\ntrait Actor(x: Int, y: Int, hp: Int)\n\nrecord Ship(Actor(x, y), tex: Int)\n";
|
|
843
843
|
let tree = Backend::parseTree(src);
|
|
844
|
-
let ast = plum_core::AstParser::new(src).
|
|
844
|
+
let ast = plum_core::AstParser::new(src).parseSourceRaw(tree.root_node());
|
|
845
845
|
let labels: Vec<String> = completion::ctorArgItems(&ast, &ast, "Ship").into_iter().map(|i| i.label).collect();
|
|
846
846
|
assert_eq!(labels, vec!["x:", "y:", "tex:"]);
|
|
847
847
|
assert_eq!(completion::enclosingCtor(" v := Vec(x: 1, "), Some("Vec".into()));
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -1728,7 +1728,15 @@ pub fn typeCheck(source: &ast::Source) -> Result<plum_checker::TypedInfo, String
|
|
|
1728
1728
|
/// Like `typeCheck`, naming `file` in positioned errors. Positions refer to the
|
|
1729
1729
|
/// merged source, so imported-file errors are attributed to the entry file.
|
|
1730
1730
|
pub fn typeCheckIn(source: &ast::Source, file: &str) -> Result<plum_checker::TypedInfo, String> {
|
|
1731
|
+
typeCheckWith(source, file, plum_checker::fusionOptions())
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
fn typeCheckWith(
|
|
1735
|
+
source: &ast::Source,
|
|
1736
|
+
file: &str,
|
|
1737
|
+
fusion: plum_checker::FusionOptions,
|
|
1738
|
+
) -> Result<plum_checker::TypedInfo, String> {
|
|
1731
|
-
plum_checker::
|
|
1739
|
+
plum_checker::typedSourceWith(source, fusion).map_err(|errs| {
|
|
1732
1740
|
errs.iter()
|
|
1733
1741
|
.map(|e| {
|
|
1734
1742
|
if e.line > 0 && !file.is_empty() {
|
|
@@ -1750,315 +1758,83 @@ pub fn typeCheckIn(source: &ast::Source, file: &str) -> Result<plum_checker::Typ
|
|
|
1750
1758
|
})
|
|
1751
1759
|
}
|
|
1752
1760
|
|
|
1753
|
-
/// Compiles a source already checked by `plum_checker::typedSource(source)`
|
|
1754
|
-
/// (the `TypedInfo` is keyed by expression address, so it must come from this
|
|
1755
|
-
/// very `source` value). Hosts that check once call this; `compileSource`
|
|
1756
|
-
///
|
|
1761
|
+
/// What `compile` does besides compiling.
|
|
1762
|
+
#[derive(Clone, Copy, Debug)]
|
|
1757
|
-
pub
|
|
1763
|
+
pub struct CompileOptions {
|
|
1764
|
+
/// Lower `test` blocks into exported `"test#<name>"` fns (`plum test`); otherwise
|
|
1765
|
+
/// they are stripped.
|
|
1758
|
-
|
|
1766
|
+
pub tests: bool,
|
|
1759
|
-
|
|
1767
|
+
pub fusion: plum_checker::FusionOptions,
|
|
1760
|
-
) -> Result<Vec<u8>, String> {
|
|
1761
|
-
compileSourceInner(source, &[], typed).map(|(bytes, _)| bytes)
|
|
1762
1768
|
}
|
|
1763
1769
|
|
|
1764
|
-
pub fn compileSource(source: &ast::Source) -> Result<Vec<u8>, String> {
|
|
1765
|
-
compileTyped(source, typeCheck(source)?)
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
/// Like `compileSource`, also returning the key of every compiled function
|
|
1769
|
-
/// (`name` or `Receiver::method`) — one entry per source function, since a
|
|
1770
|
-
/// generic function/method compiles exactly once however many types it is
|
|
1771
|
-
/// instantiated at.
|
|
1772
|
-
pub fn compileSourceWithFunctions(source: &ast::Source) -> Result<(Vec<u8>, Vec<String>), String> {
|
|
1773
|
-
compileSourceInner(source, &[], typeCheck(source)?)
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
/// Compiles `test "..." { ... }` blocks for `plum test`, stripped from every
|
|
1777
|
-
/// other build path. Each `test` is desugared into an ordinary zero-param `Fn`
|
|
1778
|
-
/// returning `Str` BEFORE the normal compile pipeline ever sees it — a hidden
|
|
1779
|
-
/// `__report` local starts at `""`, every `assert cond` inside the test body
|
|
1780
|
-
/// desugars into `if !cond { __report = __report + "line L, column C: assert
|
|
1781
|
-
/// failed: <source text>\n" }` (which is why a failing `assert` INSIDE a test
|
|
1782
|
-
/// doesn't stop the rest of it, unlike an `assert` anywhere else in the
|
|
1783
|
-
/// program, which still hard-traps — see the plain `ast::Stmt::Assert` compile
|
|
1784
|
-
/// arm), and the block's tail expression returns `__report` (empty = every
|
|
1785
|
-
/// assertion passed). Returns the compiled module alongside each test's
|
|
1786
|
-
/// original display name, in the same order as its exported `"test#<name>"`
|
|
1787
|
-
/// function.
|
|
1788
|
-
pub fn compileTestSource(source: &ast::Source) -> Result<(Vec<u8>, Vec<String>), String> {
|
|
1789
|
-
let prog = desugarTests(source);
|
|
1790
|
-
let typed = typeCheck(&prog.source)?;
|
|
1791
|
-
|
|
1770
|
+
impl Default for CompileOptions {
|
|
1792
|
-
}
|
|
1793
|
-
|
|
1794
|
-
/// A source with its `test` blocks desugared into ordinary fns. Type-check
|
|
1795
|
-
/// `source` (not the original) and pass the result to `compileTestTyped`.
|
|
1796
|
-
|
|
1771
|
+
fn default() -> Self {
|
|
1797
|
-
pub source: ast::Source,
|
|
1798
|
-
extra_exports: Vec<(String, String)>,
|
|
1799
|
-
pub names: Vec<String>,
|
|
1800
|
-
}
|
|
1801
|
-
|
|
1802
|
-
pub fn desugarTests(source: &ast::Source) -> TestProgram {
|
|
1803
|
-
|
|
1772
|
+
CompileOptions { tests: false, fusion: plum_checker::FusionOptions::default() }
|
|
1804
|
-
let names = extra_exports
|
|
1805
|
-
.iter()
|
|
1806
|
-
.map(|(_, export_name)| export_name["test#".len()..].to_string())
|
|
1807
|
-
.collect();
|
|
1808
|
-
TestProgram {
|
|
1809
|
-
source: desugared,
|
|
1810
|
-
extra_exports,
|
|
1811
|
-
names,
|
|
1812
1773
|
}
|
|
1813
1774
|
}
|
|
1814
1775
|
|
|
1776
|
+
/// The result of `compile`.
|
|
1777
|
+
pub struct Output {
|
|
1815
|
-
pub
|
|
1778
|
+
pub wasm: Vec<u8>,
|
|
1779
|
+
/// Key of every compiled function (`name` or `Receiver::method`) — one entry per
|
|
1816
|
-
|
|
1780
|
+
/// source function, since a generic compiles once however many types it is used at.
|
|
1817
|
-
|
|
1781
|
+
pub fn_keys: Vec<String>,
|
|
1782
|
+
/// With `opts.tests`: each test's display name, in the order of its `"test#<name>"` export.
|
|
1818
|
-
|
|
1783
|
+
pub test_names: Vec<String>,
|
|
1819
|
-
let (wasm, _) = compileSourceInner(&prog.source, &prog.extra_exports, typed)?;
|
|
1820
|
-
Ok((wasm, prog.names.clone()))
|
|
1821
1784
|
}
|
|
1822
1785
|
|
|
1786
|
+
/// The one compile entry: type-checks `source` (fusion per `opts.fusion`) and compiles it.
|
|
1823
|
-
|
|
1787
|
+
/// With `opts.tests`, `test` blocks are first lowered to ordinary fns (see
|
|
1824
|
-
|
|
1788
|
+
/// `plum_core::tests_lower`) and exported.
|
|
1789
|
+
pub fn compile(source: &ast::Source, opts: &CompileOptions) -> Result<Output, String> {
|
|
1790
|
+
if opts.tests {
|
|
1825
|
-
|
|
1791
|
+
let prog = plum_core::tests_lower::lowerTests(source);
|
|
1792
|
+
let typed = typeCheckWith(&prog.source, "", opts.fusion)?;
|
|
1793
|
+
let (wasm, fn_keys) = compileSourceInner(&prog.source, &prog.extraExports, typed)?;
|
|
1794
|
+
Ok(Output { wasm, fn_keys, test_names: prog.names })
|
|
1795
|
+
} else {
|
|
1796
|
+
let typed = typeCheckWith(source, "", opts.fusion)?;
|
|
1797
|
+
let (wasm, fn_keys) = compileSourceInner(source, &[], typed)?;
|
|
1798
|
+
Ok(Output { wasm, fn_keys, test_names: Vec::new() })
|
|
1826
|
-
}
|
|
1799
|
+
}
|
|
1827
1800
|
}
|
|
1828
1801
|
|
|
1829
|
-
/// Builds a failing `assert`'s report line: the literal condition text, plus —
|
|
1830
|
-
/// when the condition is a top-level comparison (`left OP right`) — the
|
|
1831
|
-
/// ACTUAL (left) and EXPECTED (right) sides' own runtime values underneath,
|
|
1832
|
-
/// `assert computed() == literal` being the overwhelmingly common shape a
|
|
1833
|
-
/// test writes such a comparison in. Reuses `"{expr}"` string interpolation's
|
|
1834
|
-
/// own existing codegen (rather than any special stringification of its
|
|
1835
|
-
/// own) — including `Float`, which interpolation now supports by rendering
|
|
1836
|
-
/// through `Float.toStr` (`plum-std/Float.plum`), so no type needs
|
|
1837
|
-
/// special-casing out here any more. A non-comparison condition (`assert
|
|
1838
|
-
/// isEven(4)`) has no meaningful actual/expected split — a boolean condition
|
|
1839
|
-
/// is always false by the time this fires — so it's just the bare text.
|
|
1840
|
-
/// First part of an assert report string: tells `compileInterpolatedString`
|
|
1841
|
-
/// that if any interpolated side has no renderable type (no `toStr`), only the
|
|
1842
|
-
/// plain condition-text line (the next part) is emitted.
|
|
1843
|
-
const ASSERT_REPORT_MARK: &str = "\u{1}assert-report";
|
|
1844
|
-
|
|
1845
|
-
fn assertFailureMessage(c: &ast::Check, lhs: &ast::Expr, rhs: &ast::Expr) -> ast::Expr {
|
|
1846
|
-
let isNullLit =
|
|
1847
|
-
|e: &ast::Expr| matches!(e, ast::Expr::ErrorValue(v) if v.name == ast::NULL_ERR_NAME);
|
|
1848
|
-
let plain = match &c.cond {
|
|
1849
|
-
ast::Expr::Compare(cmp) => isNullLit(&cmp.left) || isNullLit(&cmp.right),
|
|
1850
|
-
_ => true,
|
|
1851
|
-
};
|
|
1852
|
-
if plain {
|
|
1853
|
-
return strLit(format!("{}\n", c.text));
|
|
1854
|
-
}
|
|
1855
|
-
ast::Expr::String(ast::StringExpr {
|
|
1856
|
-
parts: vec![
|
|
1857
|
-
ast::StringPart::Text(ASSERT_REPORT_MARK.to_string()),
|
|
1858
|
-
|
|
1802
|
+
fn threadLocalOptions(tests: bool) -> CompileOptions {
|
|
1859
|
-
|
|
1803
|
+
CompileOptions { tests, fusion: plum_checker::fusionOptions() }
|
|
1860
|
-
ast::StringPart::Interp(rhs.clone()),
|
|
1861
|
-
ast::StringPart::Text("\n Actual: ".to_string()),
|
|
1862
|
-
ast::StringPart::Interp(lhs.clone()),
|
|
1863
|
-
ast::StringPart::Text("\n".to_string()),
|
|
1864
|
-
],
|
|
1865
|
-
})
|
|
1866
1804
|
}
|
|
1867
1805
|
|
|
1868
|
-
fn
|
|
1806
|
+
pub fn compileSource(source: &ast::Source) -> Result<Vec<u8>, String> {
|
|
1869
|
-
|
|
1807
|
+
compile(source, &threadLocalOptions(false)).map(|o| o.wasm)
|
|
1870
|
-
let mut extra_exports = Vec::new();
|
|
1871
|
-
let mut test_idx = 0usize;
|
|
1872
|
-
// Tests may use `io` when std/Io is loaded: the export wrapper passes the `Io`.
|
|
1873
|
-
let hasIo = source.items.iter().any(|it| matches!(it, ast::Item::Enum(c) if c.name == "Io"));
|
|
1874
|
-
let ioParam = if hasIo {
|
|
1875
|
-
vec![ast::Param {
|
|
1876
|
-
name: "io".to_string(),
|
|
1877
|
-
ty: ast::ParamType::Type(ast::Type { name: "Io".to_string(), generics: vec![] }),
|
|
1878
|
-
default: None,
|
|
1879
|
-
|
|
1808
|
+
}
|
|
1880
|
-
} else {
|
|
1881
|
-
vec![]
|
|
1882
|
-
};
|
|
1883
|
-
for item in &source.items {
|
|
1884
|
-
match item {
|
|
1885
|
-
// Tests declared in imported files are not run: only the entry file's own.
|
|
1886
|
-
ast::Item::Test(t) if !t.file.is_empty() && t.file != source.file => {}
|
|
1887
|
-
ast::Item::Test(t) => {
|
|
1888
|
-
// Leading "__" can never collide with a real Plum-declared name —
|
|
1889
|
-
// `fn_identifier`/`var_identifier` must start with a lowercase letter.
|
|
1890
|
-
let internal_name = format!("__test_{}", test_idx);
|
|
1891
|
-
test_idx += 1;
|
|
1892
|
-
|
|
1893
|
-
let mut stmts = Vec::with_capacity(t.body.stmts.len() + 2);
|
|
1894
|
-
stmts.push(ast::Stmt::Assign(ast::Assign {
|
|
1895
|
-
isSynthetic: false,
|
|
1896
|
-
targets: vec![ast::AssignTarget::Var("__report".to_string())],
|
|
1897
|
-
values: vec![strLit(String::new())],
|
|
1898
|
-
declare: true,
|
|
1899
|
-
ty: None,
|
|
1900
|
-
}));
|
|
1901
|
-
stmts.extend(desugarAssertStmts(&t.body.stmts));
|
|
1902
|
-
stmts.push(ast::Stmt::Expr(
|
|
1903
|
-
ast::Expr::Var("__report".to_string()),
|
|
1904
|
-
0,
|
|
1905
|
-
0,
|
|
1906
|
-
));
|
|
1907
1809
|
|
|
1908
|
-
|
|
1810
|
+
pub use plum_core::tests_lower::{TestProgram, ASSERT_REPORT_MARK};
|
|
1811
|
+
|
|
1812
|
+
#[cfg(test)]
|
|
1909
|
-
|
|
1813
|
+
mod compileEntryTests {
|
|
1910
|
-
name: internal_name.clone(),
|
|
1911
|
-
line: t.line,
|
|
1912
|
-
column: t.column,
|
|
1913
|
-
type_param: None,
|
|
1914
|
-
is_extern: false,
|
|
1915
|
-
is_export: false,
|
|
1916
|
-
is_override: false,
|
|
1917
|
-
is_star: false,
|
|
1918
|
-
has_self: true,
|
|
1919
|
-
|
|
1814
|
+
use super::*;
|
|
1920
|
-
|
|
1815
|
+
const SRC: &str = "import std/Str\nimport std/Int\n\nfun main() -> Int = 1 + 2\n\ntest \"a\"\n assert 1 == 1\n";
|
|
1816
|
+
|
|
1921
|
-
|
|
1817
|
+
fn load() -> ast::Source {
|
|
1922
|
-
name: ast::STR_TYPE.to_string(),
|
|
1923
|
-
generics: vec![],
|
|
1924
|
-
}),
|
|
1925
|
-
body: ast::FnBody::Block(ast::Block { stmts }),
|
|
1926
|
-
}));
|
|
1927
|
-
|
|
1818
|
+
let dir = std::env::temp_dir().join(format!("plum_compile_entry_{}", std::process::id()));
|
|
1928
|
-
|
|
1819
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
1820
|
+
let path = dir.join("main.plum");
|
|
1929
|
-
|
|
1821
|
+
std::fs::write(&path, SRC).unwrap();
|
|
1930
|
-
|
|
1822
|
+
plum_core::loadAndMerge(&path, &plum_test_support::repoRoot()).expect("load")
|
|
1931
1823
|
}
|
|
1932
|
-
(
|
|
1933
|
-
ast::Source {
|
|
1934
|
-
file: source.file.clone(),
|
|
1935
|
-
module: source.module.clone(),
|
|
1936
|
-
imports: source.imports.clone(),
|
|
1937
|
-
items,
|
|
1938
|
-
topLevelOrder: Vec::new(),
|
|
1939
|
-
formErrors: source.formErrors.clone(),
|
|
1940
|
-
formUses: source.formUses.clone(),
|
|
1941
|
-
stars: source.stars.clone(),
|
|
1942
|
-
formLoops: source.formLoops,
|
|
1943
|
-
primitives: source.primitives.clone(),
|
|
1944
|
-
traits: source.traits.clone(),
|
|
1945
|
-
},
|
|
1946
|
-
extra_exports,
|
|
1947
|
-
)
|
|
1948
|
-
}
|
|
1949
1824
|
|
|
1950
|
-
|
|
1825
|
+
#[test]
|
|
1951
|
-
|
|
1826
|
+
fn defaultMatchesCompileSource() {
|
|
1952
|
-
/// `if !cond { __report = __report + "<text>\n" }`, descending into nested
|
|
1953
|
-
|
|
1827
|
+
let src = load();
|
|
1954
|
-
/// `if`/`while`/`for`/`match` still records into the same hidden accumulator.
|
|
1955
|
-
|
|
1828
|
+
let out = compile(&src, &CompileOptions::default()).expect("compiles");
|
|
1956
|
-
|
|
1829
|
+
assert!(out.test_names.is_empty());
|
|
1830
|
+
assert_eq!(out.wasm, compileSource(&src).expect("compiles"));
|
|
1957
|
-
}
|
|
1831
|
+
}
|
|
1958
1832
|
|
|
1959
|
-
fn desugarAssertStmt(stmt: &ast::Stmt) -> Vec<ast::Stmt> {
|
|
1960
|
-
|
|
1833
|
+
#[test]
|
|
1961
|
-
ast::Stmt::Assert(c) => {
|
|
1962
|
-
// A comparison evaluates each operand exactly once into a
|
|
1963
|
-
// temporary (left first), then compares and reports the temporaries.
|
|
1964
|
-
static ASSERT_TMP: std::sync::atomic::AtomicUsize =
|
|
1965
|
-
std::sync::atomic::AtomicUsize::new(0);
|
|
1966
|
-
|
|
1834
|
+
fn testsOptionNamesTests() {
|
|
1967
|
-
let mut cond = c.cond.clone();
|
|
1968
|
-
let (mut lhs, mut rhs) = (ast::Expr::Var(String::new()), ast::Expr::Var(String::new()));
|
|
1969
|
-
let isNullLit = |e: &ast::Expr| matches!(e, ast::Expr::ErrorValue(v) if v.name == ast::NULL_ERR_NAME);
|
|
1970
|
-
// `x == null` has no value to print for the null side: reported as a plain condition.
|
|
1971
|
-
if let ast::Expr::Compare(cmp) = &c.cond {
|
|
1972
|
-
if isNullLit(&cmp.left) || isNullLit(&cmp.right) {
|
|
1973
|
-
// falls through with the condition kept whole
|
|
1974
|
-
} else {
|
|
1975
|
-
let n = ASSERT_TMP.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
1976
|
-
let (l, r) = (format!("__lhs_{}", n), format!("__rhs_{}", n));
|
|
1977
|
-
for (name, e) in [(&l, &cmp.left), (&r, &cmp.right)] {
|
|
1978
|
-
pre.push(ast::Stmt::Assign(ast::Assign {
|
|
1979
|
-
isSynthetic: false,
|
|
1980
|
-
targets: vec![ast::AssignTarget::Var(name.clone())],
|
|
1981
|
-
values: vec![e.clone()],
|
|
1982
|
-
declare: true,
|
|
1983
|
-
ty: None,
|
|
1984
|
-
}));
|
|
1985
|
-
}
|
|
1986
|
-
lhs = ast::Expr::Var(l);
|
|
1987
|
-
rhs = ast::Expr::Var(r);
|
|
1988
|
-
cond = ast::Expr::Compare(Box::new(ast::CompareExpr {
|
|
1989
|
-
op: cmp.op.clone(),
|
|
1990
|
-
|
|
1835
|
+
let src = load();
|
|
1991
|
-
right: rhs.clone(),
|
|
1992
|
-
}));
|
|
1993
|
-
}
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1836
|
+
let out = compile(&src, &CompileOptions { tests: true, ..CompileOptions::default() }).expect("compiles");
|
|
1996
|
-
isSynthetic: false,
|
|
1997
|
-
|
|
1837
|
+
assert_eq!(out.test_names, vec!["a".to_string()]);
|
|
1998
|
-
values: vec![ast::Expr::Binary(Box::new(ast::BinaryExpr {
|
|
1999
|
-
op: ast::BinOp::Add,
|
|
2000
|
-
left: ast::Expr::Var("__report".to_string()),
|
|
2001
|
-
right: assertFailureMessage(c, &lhs, &rhs),
|
|
2002
|
-
}))],
|
|
2003
|
-
declare: false,
|
|
2004
|
-
ty: None,
|
|
2005
|
-
});
|
|
2006
|
-
pre.push(ast::Stmt::If(ast::If {
|
|
2007
|
-
condition: ast::Expr::Not(Box::new(cond)),
|
|
2008
|
-
body: ast::Block {
|
|
2009
|
-
stmts: vec![appendReport],
|
|
2010
|
-
},
|
|
2011
|
-
else_ifs: vec![],
|
|
2012
|
-
else_: None,
|
|
2013
|
-
}));
|
|
2014
|
-
pre
|
|
2015
|
-
}
|
|
2016
|
-
ast::Stmt::If(i) => vec![ast::Stmt::If(ast::If {
|
|
2017
|
-
condition: i.condition.clone(),
|
|
2018
|
-
body: ast::Block {
|
|
2019
|
-
stmts: desugarAssertStmts(&i.body.stmts),
|
|
2020
|
-
},
|
|
2021
|
-
else_ifs: i
|
|
2022
|
-
.else_ifs
|
|
2023
|
-
.iter()
|
|
2024
|
-
.map(|ei| ast::ElseIf {
|
|
2025
|
-
condition: ei.condition.clone(),
|
|
2026
|
-
body: ast::Block {
|
|
2027
|
-
stmts: desugarAssertStmts(&ei.body.stmts),
|
|
2028
|
-
},
|
|
2029
|
-
})
|
|
2030
|
-
.collect(),
|
|
2031
|
-
else_: i.else_.as_ref().map(|b| ast::Block {
|
|
2032
|
-
stmts: desugarAssertStmts(&b.stmts),
|
|
2033
|
-
}),
|
|
2034
|
-
})],
|
|
2035
|
-
ast::Stmt::While(w) => vec![ast::Stmt::While(ast::While {
|
|
2036
|
-
condition: w.condition.clone(),
|
|
2037
|
-
body: ast::Block {
|
|
2038
|
-
stmts: desugarAssertStmts(&w.body.stmts),
|
|
2039
|
-
},
|
|
2040
|
-
})],
|
|
2041
|
-
ast::Stmt::ForLoop(f) => vec![ast::Stmt::ForLoop(ast::ForLoop {
|
|
2042
|
-
body: ast::Block {
|
|
2043
|
-
stmts: desugarAssertStmts(&f.body.stmts),
|
|
2044
|
-
},
|
|
2045
|
-
..f.clone()
|
|
2046
|
-
})],
|
|
2047
|
-
ast::Stmt::Match(m) => vec![ast::Stmt::Match(ast::Match {
|
|
2048
|
-
subjects: m.subjects.clone(),
|
|
2049
|
-
cases: m
|
|
2050
|
-
.cases
|
|
2051
|
-
.iter()
|
|
2052
|
-
.map(|c| ast::Case {
|
|
2053
|
-
patterns: c.patterns.clone(),
|
|
2054
|
-
guard: c.guard.clone(),
|
|
2055
|
-
body: ast::Block {
|
|
2056
|
-
stmts: desugarAssertStmts(&c.body.stmts),
|
|
2057
|
-
},
|
|
2058
|
-
})
|
|
2059
|
-
.collect(),
|
|
2060
|
-
})],
|
|
2061
|
-
other => vec![other.clone()],
|
|
2062
1838
|
}
|
|
2063
1839
|
}
|
|
2064
1840
|
|
|
@@ -4051,7 +3827,6 @@ impl<'a, 't> ClosureWalker<'a, 't> {
|
|
|
4051
3827
|
ast::Expr::Not(inner) => self.walkExpr(inner),
|
|
4052
3828
|
ast::Expr::Try(inner) => self.walkExpr(inner),
|
|
4053
3829
|
ast::Expr::Unary(u) => self.walkExpr(&u.operand),
|
|
4054
|
-
ast::Expr::Paren(inner) => self.walkExpr(inner),
|
|
4055
3830
|
ast::Expr::InlineIf(t) => {
|
|
4056
3831
|
self.walkExpr(&t.condition);
|
|
4057
3832
|
self.walkExpr(&t.then);
|
|
@@ -4381,9 +4156,6 @@ fn fvCollectRefsExpr(
|
|
|
4381
4156
|
ast::Expr::Unary(u) => {
|
|
4382
4157
|
fvCollectRefsExpr(&u.operand, bound, seen, free, env, fn_decls, fn_locals)
|
|
4383
4158
|
}
|
|
4384
|
-
ast::Expr::Paren(inner) => {
|
|
4385
|
-
fvCollectRefsExpr(inner, bound, seen, free, env, fn_decls, fn_locals)
|
|
4386
|
-
}
|
|
4387
4159
|
ast::Expr::InlineIf(t) => {
|
|
4388
4160
|
fvCollectRefsExpr(&t.condition, bound, seen, free, env, fn_decls, fn_locals);
|
|
4389
4161
|
if let ast::Expr::Bind(b) = &t.condition {
|
|
@@ -4711,7 +4483,6 @@ impl<'a> Collector<'a> {
|
|
|
4711
4483
|
self.walkExpr(inner);
|
|
4712
4484
|
}
|
|
4713
4485
|
ast::Expr::Unary(u) => self.walkExpr(&u.operand),
|
|
4714
|
-
ast::Expr::Paren(inner) => self.walkExpr(inner),
|
|
4715
4486
|
ast::Expr::InlineIf(t) => {
|
|
4716
4487
|
self.walkExpr(&t.condition);
|
|
4717
4488
|
self.walkExpr(&t.then);
|
|
@@ -7045,7 +6816,6 @@ fn calleeText(e: &ast::Expr) -> String {
|
|
|
7045
6816
|
ast::AttrKind::Field(f) => f.clone(),
|
|
7046
6817
|
},
|
|
7047
6818
|
ast::Expr::Var(n) => n.clone(),
|
|
7048
|
-
ast::Expr::Paren(i) => calleeText(i),
|
|
7049
6819
|
_ => "value".to_string(),
|
|
7050
6820
|
}
|
|
7051
6821
|
}
|
|
@@ -7414,9 +7184,6 @@ fn compileExprCore(
|
|
|
7414
7184
|
}
|
|
7415
7185
|
}
|
|
7416
7186
|
}
|
|
7417
|
-
ast::Expr::Paren(inner) => {
|
|
7418
|
-
compileExpr(inner, body, ctx, state)?;
|
|
7419
|
-
}
|
|
7420
7187
|
// `expr?` — if the error code is non-zero, return it from the enclosing
|
|
7421
7188
|
// function (see `compileErrTry`); otherwise the success value is the result.
|
|
7422
7189
|
ast::Expr::Try(inner) => compileErrTry(expr, inner, body, ctx, state)?,
|
|
@@ -8176,7 +7943,6 @@ fn valueLocalPlace(expr: &ast::Expr, ctx: &LocalCtx) -> Option<(u32, PlumType)>
|
|
|
8176
7943
|
let ty = ctx.local_decl_types.get("self").filter(|t| isValueTy(t))?;
|
|
8177
7944
|
Some((*ctx.locals.get("self")?, ty.clone()))
|
|
8178
7945
|
}
|
|
8179
|
-
ast::Expr::Paren(inner) => valueLocalPlace(inner, ctx),
|
|
8180
7946
|
ast::Expr::Attribute(a) => match &a.attr {
|
|
8181
7947
|
ast::AttrKind::Field(f) => {
|
|
8182
7948
|
let (base, bty) = valueLocalPlace(&a.object, ctx)?;
|
plum-wasm-codegen/tests/codegen_tests.rs
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
#![allow(non_snake_case)]
|
|
2
|
-
use plum_wasm_codegen::{compileSource,
|
|
2
|
+
use plum_wasm_codegen::{compile, compileSource, CompileOptions};
|
|
3
|
+
|
|
4
|
+
fn compileTestSource(source: &plum_core::ast::Source) -> Result<(Vec<u8>, Vec<String>), String> {
|
|
5
|
+
compile(source, &CompileOptions { tests: true, ..CompileOptions::default() }).map(|o| (o.wasm, o.test_names))
|
|
6
|
+
}
|
|
3
7
|
use wasm_encoder::Encode;
|
|
4
8
|
|
|
5
9
|
use plum_test_support::{engine as gcEngine, parse, repoRoot};
|
|
@@ -1079,7 +1083,8 @@ fn compiledKeys(name: &str, src: &str) -> (Vec<u8>, Vec<String>) {
|
|
|
1079
1083
|
let path = dir.join("main.plum");
|
|
1080
1084
|
std::fs::write(&path, src).unwrap();
|
|
1081
1085
|
let source = plum_core::loadAndMerge(&path, &repoRoot()).expect("load");
|
|
1082
|
-
let
|
|
1086
|
+
let o = compile(&source, &CompileOptions::default()).expect("compile");
|
|
1087
|
+
let r = (o.wasm, o.fn_keys);
|
|
1083
1088
|
let _ = std::fs::remove_dir_all(&dir);
|
|
1084
1089
|
wasmparser::validate(&r.0).expect("valid wasm");
|
|
1085
1090
|
r
|