plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
e8446dd
— Peter John
2026-11-04T20:26:00+05:30
Compiler cleanup phase 3a: per-compile state, no fusion globals
- docs/plans/2026-11-04-compiler-cleanup-phase3a.progress.md +20 -0
- docs/plans/2026-11-04-compiler-cleanup-phase3a.report.json +212 -0
- docs/plans/2026-11-04-compiler-cleanup-phase3a.report.md +38 -0
- docs/plans/2026-11-04-compiler-cleanup-phase3a.results.md +31 -0
- plum-checker/src/lib.rs +108 -181
- plum-checker/src/state.rs +126 -0
- plum-checker/src/traits.rs +7 -12
- plum-checker/src/unions.rs +14 -32
- plum-cli/src/game.rs +2 -1
- plum-cli/src/main.rs +37 -30
- plum-core/src/forms.rs +15 -3
- plum-core/src/lib.rs +1 -0
- plum-core/src/loader.rs +6 -0
- plum-core/src/lower.rs +1 -0
- plum-core/src/session.rs +15 -0
- plum-core/src/simplify.rs +9 -4
- plum-core/tests/session_test.rs +50 -0
- plum-core/tests/trmc_test.rs +8 -7
- plum-wasm-codegen/src/lib.rs +102 -98
- plum-wasm-codegen/tests/e2e_mem/reuse.rs +17 -11
- plum-wasm-codegen/tests/fusion_alloc_test.rs +7 -8
docs/plans/2026-11-04-compiler-cleanup-phase3a.progress.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
- start 529a1064cb6f0e203401d8c5524b57c7b546bd4b
|
|
2
|
+
- unblocked: dispatched T0 T2 T3 at 19:49:26; waiting on fixes: T1 T4 T5
|
|
3
|
+
- Ruling: T0 skipped (test-cmd.json copied), prime+task sent together - cost: none
|
|
4
|
+
- unblocked: dispatched T1 T4 T5 T6 at 19:50:23; waiting on fixes: none
|
|
5
|
+
- checkpoint T3 a2eb6574c6 core counters per compile
|
|
6
|
+
- Ruling: T3's hash check deferred to T5 (codegen mid-edit by T2 at the time) - cost: none
|
|
7
|
+
- checkpoint T2 95294015b3 codegen state value
|
|
8
|
+
- Note: after T2+T3, closure_spec_test 5 fail and trmc_test 1 fail while T1 is mid-edit on checker lib.rs; diagnose after T1 lands
|
|
9
|
+
- checkpoint T1 dbf61b1b2c checker state value
|
|
10
|
+
- checkpoint T6 dc61485351 checker unions/traits state
|
|
11
|
+
- Note: plum-checker/src/types.rs NAMED_ERR_SETS/FN_ERR_SETS thread-locals remain (not in any 3a task) - left for 3b
|
|
12
|
+
- checkpoint T3 bb7823d076 core counters per compile
|
|
13
|
+
- checkpoint T4 5d6a67b462 fusion globals out
|
|
14
|
+
- tests full passed=807 failed=1 skipped=0 seconds=214.9
|
|
15
|
+
- Ruling: remaining thread-locals accepted - core forms/simplify/loader/unions blocks are all reset by session::begin per compile; checker types.rs FN_ERR_SETS/NAMED_ERR_SETS go in 3b - cost: 4 small holders survive
|
|
16
|
+
- checkpoint T5 e70d20769d verify 3a
|
|
17
|
+
- Ruling: trmc_test::std_map_1m_within_3x_of_build failure in the full run is the known load-sensitive timing test (flaky since phase 1; T5's direct run passed 807/0) - cost: none
|
|
18
|
+
- Ruling: final-review finding (timing test) closed - std_map_1m_within_3x_of_build passes 3/3 run alone on this branch (0.75-0.77s); the full-suite failure was CPU load - cost: none
|
|
19
|
+
- tests full passed=809 failed=0 skipped=0 seconds=61.1
|
|
20
|
+
- checkpoint T1 f822efe1f8 checker state value
|
docs/plans/2026-11-04-compiler-cleanup-phase3a.report.json
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
{
|
|
2
|
+
"projection_note": "projection: fixed forecast saved at lint 2026-11-04T19:49:20 (plan edited since)",
|
|
3
|
+
"headline": {
|
|
4
|
+
"wall_min": {
|
|
5
|
+
"projected": 21.585,
|
|
6
|
+
"actual": 25.432
|
|
7
|
+
},
|
|
8
|
+
"cost_usd_agents": {
|
|
9
|
+
"projected": 0.400732,
|
|
10
|
+
"actual": 2.039698
|
|
11
|
+
},
|
|
12
|
+
"task_min": {
|
|
13
|
+
"projected": 32.577,
|
|
14
|
+
"actual": 33.037
|
|
15
|
+
},
|
|
16
|
+
"agents": {
|
|
17
|
+
"projected": 5,
|
|
18
|
+
"actual": 10
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"feature": null,
|
|
22
|
+
"cost_split": {
|
|
23
|
+
"agents_usd": 1.904189,
|
|
24
|
+
"rework_usd": 0.135509,
|
|
25
|
+
"coordinator_usd": 31.381217,
|
|
26
|
+
"total_usd": 33.420915
|
|
27
|
+
},
|
|
28
|
+
"concurrency": {
|
|
29
|
+
"peak": 3,
|
|
30
|
+
"projected_peak": 3
|
|
31
|
+
},
|
|
32
|
+
"critical_path": {
|
|
33
|
+
"actual": [
|
|
34
|
+
"T3",
|
|
35
|
+
"T5"
|
|
36
|
+
],
|
|
37
|
+
"projected": [
|
|
38
|
+
"T1",
|
|
39
|
+
"T4",
|
|
40
|
+
"T5"
|
|
41
|
+
],
|
|
42
|
+
"projected_min": 21.585
|
|
43
|
+
},
|
|
44
|
+
"model_mix": {
|
|
45
|
+
"sonnet": {
|
|
46
|
+
"tasks": 4,
|
|
47
|
+
"cost_usd": 1.594382
|
|
48
|
+
},
|
|
49
|
+
"opus": {
|
|
50
|
+
"tasks": 1,
|
|
51
|
+
"cost_usd": 0.445316
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"coordinator_cost_by_model": {
|
|
55
|
+
"opus": 31.381217
|
|
56
|
+
},
|
|
57
|
+
"quality": {
|
|
58
|
+
"tests": {
|
|
59
|
+
"passed": 809,
|
|
60
|
+
"failed": 0,
|
|
61
|
+
"skipped": 0,
|
|
62
|
+
"seconds": 61.1,
|
|
63
|
+
"mode": "full",
|
|
64
|
+
"failures": [],
|
|
65
|
+
"skipped_tests": []
|
|
66
|
+
},
|
|
67
|
+
"first_time_pass": 0,
|
|
68
|
+
"first_time_n": 0,
|
|
69
|
+
"rework_dispatches": 1,
|
|
70
|
+
"open_incidents": 2
|
|
71
|
+
},
|
|
72
|
+
"tasks": [
|
|
73
|
+
{
|
|
74
|
+
"task": "T5",
|
|
75
|
+
"model": "opus",
|
|
76
|
+
"pred_min": 5.742,
|
|
77
|
+
"actual_min": 8.483,
|
|
78
|
+
"delta": {
|
|
79
|
+
"abs": 2.7413,
|
|
80
|
+
"pct": 47.7
|
|
81
|
+
},
|
|
82
|
+
"cost_usd": 0.445316,
|
|
83
|
+
"first_time_pass": null
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"task": "T1",
|
|
87
|
+
"model": "sonnet",
|
|
88
|
+
"pred_min": 8.976,
|
|
89
|
+
"actual_min": 11.35,
|
|
90
|
+
"delta": {
|
|
91
|
+
"abs": 2.374,
|
|
92
|
+
"pct": 26.4
|
|
93
|
+
},
|
|
94
|
+
"cost_usd": 0.549345,
|
|
95
|
+
"first_time_pass": null
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"task": "T2",
|
|
99
|
+
"model": "sonnet",
|
|
100
|
+
"pred_min": 7.036,
|
|
101
|
+
"actual_min": 5.463,
|
|
102
|
+
"delta": {
|
|
103
|
+
"abs": -1.5727,
|
|
104
|
+
"pct": -22.4
|
|
105
|
+
},
|
|
106
|
+
"cost_usd": 0.288141,
|
|
107
|
+
"first_time_pass": null
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"task": "T3",
|
|
111
|
+
"model": "sonnet",
|
|
112
|
+
"pred_min": 3.956,
|
|
113
|
+
"actual_min": 3.398,
|
|
114
|
+
"delta": {
|
|
115
|
+
"abs": -0.5577,
|
|
116
|
+
"pct": -14.1
|
|
117
|
+
},
|
|
118
|
+
"cost_usd": 0.376171,
|
|
119
|
+
"first_time_pass": null
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"task": "T6",
|
|
123
|
+
"model": "sonnet",
|
|
124
|
+
"pred_min": null,
|
|
125
|
+
"actual_min": 4.342,
|
|
126
|
+
"delta": {
|
|
127
|
+
"abs": null,
|
|
128
|
+
"pct": null
|
|
129
|
+
},
|
|
130
|
+
"cost_usd": 0.380725,
|
|
131
|
+
"first_time_pass": null
|
|
132
|
+
}
|
|
133
|
+
],
|
|
134
|
+
"cost_table": {
|
|
135
|
+
"source": "est",
|
|
136
|
+
"usd": 3.2168,
|
|
137
|
+
"tokens": 7975937,
|
|
138
|
+
"wall_min": 89690167.9142,
|
|
139
|
+
"rows": [
|
|
140
|
+
{
|
|
141
|
+
"agent": "a5b9d5a1c5936dbc7",
|
|
142
|
+
"model": "sonnet",
|
|
143
|
+
"wall_min": 15.8987,
|
|
144
|
+
"tokens": 3762158,
|
|
145
|
+
"usd": 0.9301
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"agent": "a8a003b8140763d73",
|
|
149
|
+
"model": "opus",
|
|
150
|
+
"wall_min": 8.4877,
|
|
151
|
+
"tokens": 557627,
|
|
152
|
+
"usd": 0.4453
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"agent": "aadb1cc264b30bfb1",
|
|
156
|
+
"model": "sonnet",
|
|
157
|
+
"wall_min": 2.7002,
|
|
158
|
+
"tokens": 339143,
|
|
159
|
+
"usd": 0.1355
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"agent": "aaed74cd7cef5ea8b",
|
|
163
|
+
"model": "sonnet",
|
|
164
|
+
"wall_min": 29896696.8122,
|
|
165
|
+
"tokens": 0,
|
|
166
|
+
"usd": 0.0
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"agent": "ab29eca71a3d8584f",
|
|
170
|
+
"model": "opus",
|
|
171
|
+
"wall_min": 0.2177,
|
|
172
|
+
"tokens": 32198,
|
|
173
|
+
"usd": 0.111
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"agent": "ac4f885b3a637d9a2",
|
|
177
|
+
"model": "sonnet",
|
|
178
|
+
"wall_min": 3.4246,
|
|
179
|
+
"tokens": 614190,
|
|
180
|
+
"usd": 0.2407
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"agent": "acab0918e28f4b621",
|
|
184
|
+
"model": "opus",
|
|
185
|
+
"wall_min": 7.2594,
|
|
186
|
+
"tokens": 1916727,
|
|
187
|
+
"usd": 1.0661
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"agent": "ae8c7981719782a5f",
|
|
191
|
+
"model": "sonnet",
|
|
192
|
+
"wall_min": 29896700.1004,
|
|
193
|
+
"tokens": 0,
|
|
194
|
+
"usd": 0.0
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"agent": "aec9919ccca8c25c6",
|
|
198
|
+
"model": "sonnet",
|
|
199
|
+
"wall_min": 29896727.5257,
|
|
200
|
+
"tokens": 0,
|
|
201
|
+
"usd": 0.0
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"agent": "afc79a4ba65a52d67",
|
|
205
|
+
"model": "sonnet",
|
|
206
|
+
"wall_min": 5.4876,
|
|
207
|
+
"tokens": 753894,
|
|
208
|
+
"usd": 0.2881
|
|
209
|
+
}
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
}
|
docs/plans/2026-11-04-compiler-cleanup-phase3a.report.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# superlite build report
|
|
2
|
+
|
|
3
|
+
projection: fixed forecast saved at lint 2026-11-04T19:49:20 (plan edited since)
|
|
4
|
+
|
|
5
|
+
Projected Actual Delta
|
|
6
|
+
wall-clock (min) 21.6 25.4 +3.8 (+18%)
|
|
7
|
+
agent cost (USD, +rework) 0.40 2.04 +1.64 (+409%)
|
|
8
|
+
task-minutes 32.6 33.0 +0.5 (+1%)
|
|
9
|
+
agents 5 10 +5 (+100%)
|
|
10
|
+
cost split: agents $1.90, rework $0.14, coordinator $31.38, total $33.42
|
|
11
|
+
concurrency: peak 3 at once (projected 3); critical path actual T3 T5; projected T1 > T4 > T5 (21.6 min)
|
|
12
|
+
model mix: opus 1 tasks $0.45, sonnet 4 tasks $1.59, coordinator opus $31.38
|
|
13
|
+
quality: tests 809 passed, 0 failed; first-time pass n/a; rework dispatches 1; open incidents 2
|
|
14
|
+
|
|
15
|
+
task model pred actual delta cost
|
|
16
|
+
T5 opus 5.7 8.5 +2.7 (+48%) $0.445
|
|
17
|
+
T1 sonnet 9.0 11.3 +2.4 (+26%) $0.549
|
|
18
|
+
T2 sonnet 7.0 5.5 -1.6 (-22%) $0.288
|
|
19
|
+
T3 sonnet 4.0 3.4 -0.6 (-14%) $0.376
|
|
20
|
+
T6 sonnet n/a 4.3 n/a $0.381
|
|
21
|
+
|
|
22
|
+
agent cost table:
|
|
23
|
+
| agent | model | wall_min | tokens | usd |
|
|
24
|
+
| --- | --- | --- | --- | --- |
|
|
25
|
+
| a5b9d5a1c5936dbc7 | sonnet | 15.8987 | 3762158 | 0.9301 |
|
|
26
|
+
| a8a003b8140763d73 | opus | 8.4877 | 557627 | 0.4453 |
|
|
27
|
+
| aadb1cc264b30bfb1 | sonnet | 2.7002 | 339143 | 0.1355 |
|
|
28
|
+
| aaed74cd7cef5ea8b | sonnet | 29896696.8122 | 0 | 0.0 |
|
|
29
|
+
| ab29eca71a3d8584f | opus | 0.2177 | 32198 | 0.111 |
|
|
30
|
+
| ac4f885b3a637d9a2 | sonnet | 3.4246 | 614190 | 0.2407 |
|
|
31
|
+
| acab0918e28f4b621 | opus | 7.2594 | 1916727 | 1.0661 |
|
|
32
|
+
| ae8c7981719782a5f | sonnet | 29896700.1004 | 0 | 0.0 |
|
|
33
|
+
| aec9919ccca8c25c6 | sonnet | 29896727.5257 | 0 | 0.0 |
|
|
34
|
+
| afc79a4ba65a52d67 | sonnet | 5.4876 | 753894 | 0.2881 |
|
|
35
|
+
| totals | | 89690167.9142 | 7975937 | 3.2168 |
|
|
36
|
+
cost source: est
|
|
37
|
+
|
|
38
|
+
takeaway: ran 18% slower than projected; cost 409% over; biggest miss T5
|
docs/plans/2026-11-04-compiler-cleanup-phase3a.results.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Compiler cleanup phase 3a: results (T5)
|
|
2
|
+
|
|
3
|
+
Start sha 529a106; verified against the uncommitted worktree tree (T1, T2, T3, T4, T6 applied), using a fresh build in a separate CARGO_TARGET_DIR.
|
|
4
|
+
|
|
5
|
+
## Wasm hashes
|
|
6
|
+
`scripts/wasm-hashes.sh 529a106` compared with the current tree compiled by a fresh `plum` binary, one process per file: 25/25 examples compile in both, 0 FAIL, and `diff` is empty, so every hash is identical.
|
|
7
|
+
|
|
8
|
+
## Tests
|
|
9
|
+
- Workspace: `cargo build --workspace --tests`, then all 60 test executables run directly: 807 passed, 0 failed, 0 ignored.
|
|
10
|
+
- plum-cli binaries: plum (unit) 5, assert_once_test 1, compile_tests 3, crash_test 2, game_web_test 3, host_stub_test 3, test_scope_test 2. All ok.
|
|
11
|
+
- `plum test plum-std plum-examples`: 678 passed, 0 failed (63/63 files).
|
|
12
|
+
- plum-game `host_test` (built inside plum-game/): 18 passed, 0 failed.
|
|
13
|
+
|
|
14
|
+
## `thread_local!` blocks in plum-*/src
|
|
15
|
+
| crate | before (529a106) | after |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| plum-checker | 9 (lib.rs 3, traits.rs 1, types.rs 2, unions.rs 3) | 3 |
|
|
18
|
+
| plum-core | 3 (forms.rs 1, loader.rs 1, unions.rs 1) | 5 |
|
|
19
|
+
| plum-wasm-codegen | 3 (lib.rs) | 1 |
|
|
20
|
+
| total | 15 | 9 |
|
|
21
|
+
|
|
22
|
+
These are the blocks that remain after the change:
|
|
23
|
+
- plum-checker/src/state.rs:69 `CHECK: RefCell<CheckState>`: the new checker state holder.
|
|
24
|
+
- plum-checker/src/types.rs:41 `FN_ERR_SETS` and types.rs:48 `NAMED_ERR_SETS`: not in scope for this phase. T1 listed only lib.rs, and T6 listed only unions/traits.
|
|
25
|
+
- plum-wasm-codegen/src/lib.rs:591 `GEN: RefCell<GenState>`: the new codegen state holder.
|
|
26
|
+
- plum-core/src/forms.rs:113 `NEXT_LOOP_ID` (new) and forms.rs:121 `SEQ_KINDS/REG/COUNTER/LOWERED/ERRORS/USES`: the forms session, reset by `session::begin`.
|
|
27
|
+
- plum-core/src/simplify.rs:11 `FRESH` (new): reset by `session::begin`.
|
|
28
|
+
- plum-core/src/loader.rs:511 `USER_PRIMS/MERGED_LIT_ERRORS`: reset by `session::begin`.
|
|
29
|
+
- plum-core/src/unions.rs:516 `PLAN/FRESH`: reset by `session::begin`.
|
|
30
|
+
|
|
31
|
+
plum-core went up by 2 blocks because T3 turned the process-global `static AtomicUsize` counters (forms `NEXT_LOOP_ID`, simplify `FRESH`) into thread-locals that reset at the start of each compile. That is what the plan asked for, not a regression.
|
plum-checker/src/lib.rs
CHANGED
|
@@ -2,60 +2,19 @@
|
|
|
2
2
|
// naming convention), not Rust's idiomatic snake_case — silence the resulting lint.
|
|
3
3
|
#![allow(non_snake_case)]
|
|
4
4
|
|
|
5
|
+
mod state;
|
|
5
6
|
pub mod traits;
|
|
6
7
|
pub mod types;
|
|
7
8
|
pub mod unions;
|
|
8
9
|
|
|
9
10
|
use plum_core::ast;
|
|
10
|
-
use std::cell::{Cell, RefCell};
|
|
11
11
|
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
|
12
|
+
use state::{resetCheck, withCheck};
|
|
12
13
|
use types::{
|
|
13
14
|
errSetNames, CheckError, CheckResult, ErrSet, PlumType, TypeEnv, TypeScheme, FN_ERR_SETS,
|
|
14
15
|
NAMED_ERR_SETS,
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
thread_local! {
|
|
18
|
-
/// Param types for the closure literal about to be inferred as `name := |..| ..`,
|
|
19
|
-
/// taken from the first later call of `name` (see `closureHintFor`). `Some(vec![])`
|
|
20
|
-
/// means "assigned to a local, no call information"; the flag is whether an unresolved
|
|
21
|
-
/// param is an error (the local is never used as a plain value either).
|
|
22
|
-
static CLOSURE_HINT: RefCell<Option<(Vec<PlumType>, bool)>> = const { RefCell::new(None) };
|
|
23
|
-
/// Counter for the fresh `_t<N>` inference variables minted per generic call site.
|
|
24
|
-
static FRESH: Cell<u32> = const { Cell::new(0) };
|
|
25
|
-
/// When set, `inferExprWith` records every expression's type here (see `typedSource`).
|
|
26
|
-
static RECORD_BINDS: RefCell<Option<Binds>> = const { RefCell::new(None) };
|
|
27
|
-
static RECORD: RefCell<Option<HashMap<usize, PlumType>>> = const { RefCell::new(None) };
|
|
28
|
-
/// Generic-call sites seen while checking one body (see `CallSite`); `None` outside `checkSourceInner`.
|
|
29
|
-
static CALLS: RefCell<Option<Vec<CallSite>>> = const { RefCell::new(None) };
|
|
30
|
-
/// While checking a generic body for one instantiation: its generic parameter -> concrete type.
|
|
31
|
-
static TSUBST: RefCell<Option<BTreeMap<String, PlumType>>> = const { RefCell::new(None) };
|
|
32
|
-
/// True while checking an instantiated generic body (method lookups on declared types are strict).
|
|
33
|
-
static INST_MODE: Cell<bool> = const { Cell::new(false) };
|
|
34
|
-
/// Position (line, column) of the innermost call expression being inferred, or of the
|
|
35
|
-
/// declaration being checked outside any call: the "called at" site of recorded calls.
|
|
36
|
-
static CUR_POS: Cell<(usize, usize)> = const { Cell::new((0, 0)) };
|
|
37
|
-
/// Position of the innermost call whose inference failed, so a statement's
|
|
38
|
-
/// error points at that call rather than the enclosing fn/test header.
|
|
39
|
-
static ERR_POS: Cell<Option<(usize, usize)>> = const { Cell::new(None) };
|
|
40
|
-
/// Address of the expression `inferExprWith` is currently inferring (the innermost one).
|
|
41
|
-
static CUR_EXPR: Cell<usize> = const { Cell::new(0) };
|
|
42
|
-
/// While checking the last statement of a value-producing tail (an fn body's
|
|
43
|
-
/// final `if`/`match`/expression): the type that tail value must have, so its
|
|
44
|
-
/// generic arguments are pinned like a directly-returned expression's.
|
|
45
|
-
static TAIL: RefCell<Option<PlumType>> = const { RefCell::new(None) };
|
|
46
|
-
static MATCH_TAIL: RefCell<Option<PlumType>> = const { RefCell::new(None) };
|
|
47
|
-
/// Key of the fn whose inferred error set the pre-pass is collecting (`None` otherwise).
|
|
48
|
-
static CUR_FN: RefCell<Option<String>> = const { RefCell::new(None) };
|
|
49
|
-
/// Error names contributed to `CUR_FN`'s set by the body being checked.
|
|
50
|
-
static ERR_ACC: RefCell<BTreeSet<String>> = const { RefCell::new(BTreeSet::new()) };
|
|
51
|
-
/// Call edges `(caller key, callee key)` seen while collecting error sets.
|
|
52
|
-
static ERR_EDGES: RefCell<BTreeSet<(String, String)>> = const { RefCell::new(BTreeSet::new()) };
|
|
53
|
-
/// Error-union sites (`try`, `!`, `catch`, `?:`) recorded for codegen; `None` while collecting.
|
|
54
|
-
static ERR_SITES: RefCell<Option<HashMap<usize, ErrSite>>> = const { RefCell::new(None) };
|
|
55
|
-
/// Operator expressions (address) on non-primitive operands -> the synthetic method call
|
|
56
|
-
/// (`a + b` -> `a.add(b)`) they resolve to. Kept so the cloned arguments have stable addresses.
|
|
57
|
-
static OP_CALLS: RefCell<HashMap<usize, std::rc::Rc<ast::FnCall>>> = RefCell::new(HashMap::new());
|
|
58
|
-
}
|
|
59
18
|
|
|
60
19
|
/// A call to a generic fn/method seen in some body: callee identity plus the
|
|
61
20
|
/// (possibly still unresolved) type bound to each of its generic parameters.
|
|
@@ -73,7 +32,7 @@ struct CallSite {
|
|
|
73
32
|
}
|
|
74
33
|
|
|
75
34
|
fn recordCall(recv: Option<&str>, name: &str, args: BTreeMap<String, PlumType>) {
|
|
76
|
-
recordCallAt(
|
|
35
|
+
recordCallAt(withCheck(|s| { let e = &s.cur_expr; e.get() }), false, recv, name, args);
|
|
77
36
|
}
|
|
78
37
|
|
|
79
38
|
fn recordCallAt(
|
|
@@ -86,8 +45,8 @@ fn recordCallAt(
|
|
|
86
45
|
if args.is_empty() {
|
|
87
46
|
return;
|
|
88
47
|
}
|
|
89
|
-
let (line, column) =
|
|
48
|
+
let (line, column) = withCheck(|s| { let p = &s.cur_pos; p.get() });
|
|
90
|
-
|
|
49
|
+
withCheck(|s| { let c = &s.calls; {
|
|
91
50
|
if let Some(v) = c.borrow_mut().as_mut() {
|
|
92
51
|
v.push(CallSite {
|
|
93
52
|
recv: recv.map(|r| r.to_string()),
|
|
@@ -99,7 +58,7 @@ fn recordCallAt(
|
|
|
99
58
|
implicit,
|
|
100
59
|
});
|
|
101
60
|
}
|
|
102
|
-
});
|
|
61
|
+
} });
|
|
103
62
|
}
|
|
104
63
|
|
|
105
64
|
/// Which error-union operator an `ErrSite` is.
|
|
@@ -188,11 +147,11 @@ fn bindAcross(a: &PlumType, b: &PlumType) {
|
|
|
188
147
|
}
|
|
189
148
|
|
|
190
149
|
fn freshVar() -> PlumType {
|
|
191
|
-
|
|
150
|
+
withCheck(|s| { let c = &s.fresh; {
|
|
192
151
|
let n = c.get();
|
|
193
152
|
c.set(n + 1);
|
|
194
153
|
PlumType::TVar(format!("_t{}", n))
|
|
195
|
-
})
|
|
154
|
+
} })
|
|
196
155
|
}
|
|
197
156
|
|
|
198
157
|
/// Binding of `_t<N>` inference variables to types, local to one call site.
|
|
@@ -348,7 +307,7 @@ pub fn plumTypeFromAstG(ty: &ast::Type) -> PlumType {
|
|
|
348
307
|
);
|
|
349
308
|
}
|
|
350
309
|
if isGenericName(&ty.name) {
|
|
351
|
-
if let Some(t) =
|
|
310
|
+
if let Some(t) = withCheck(|s| { let s = &s.tsubst; s.borrow().as_ref().and_then(|m| m.get(&ty.name).cloned()) })
|
|
352
311
|
{
|
|
353
312
|
return t;
|
|
354
313
|
}
|
|
@@ -484,18 +443,18 @@ fn freshOwner(generics: &[String]) -> BTreeMap<String, PlumType> {
|
|
|
484
443
|
/// then `out.push(x)` bound `_t1`) to a type, so a later `.field` or method
|
|
485
444
|
/// call on a still-variable-typed value (`out.at(0).x`) sees the real type.
|
|
486
445
|
fn resolveRecorded(ty: &PlumType) -> PlumType {
|
|
487
|
-
|
|
446
|
+
withCheck(|s| { let r = &s.record_binds; match r.borrow().as_ref() {
|
|
488
447
|
Some(b) => substVars(ty, b),
|
|
489
448
|
None => ty.clone(),
|
|
490
|
-
})
|
|
449
|
+
} })
|
|
491
450
|
}
|
|
492
451
|
|
|
493
452
|
fn recordBind(name: &str, ty: &PlumType) {
|
|
494
|
-
|
|
453
|
+
withCheck(|s| { let r = &s.record_binds; {
|
|
495
454
|
if let Some(m) = r.borrow_mut().as_mut() {
|
|
496
455
|
m.insert(name.to_string(), ty.clone());
|
|
497
456
|
}
|
|
498
|
-
});
|
|
457
|
+
} });
|
|
499
458
|
}
|
|
500
459
|
|
|
501
460
|
/// Key offsets (added to an expression's address, which is 8-aligned) under which
|
|
@@ -507,14 +466,14 @@ pub const ERR_WRAP: usize = 4;
|
|
|
507
466
|
pub const ANNOTATED: usize = 3;
|
|
508
467
|
|
|
509
468
|
fn recordAnnotation(value: &ast::Expr, declared: &PlumType) {
|
|
510
|
-
|
|
469
|
+
withCheck(|s| { let r = &s.record; {
|
|
511
470
|
if let Some(m) = r.borrow_mut().as_mut() {
|
|
512
471
|
m.insert(
|
|
513
472
|
value as *const ast::Expr as usize + ANNOTATED,
|
|
514
473
|
declared.clone(),
|
|
515
474
|
);
|
|
516
475
|
}
|
|
517
|
-
});
|
|
476
|
+
} });
|
|
518
477
|
}
|
|
519
478
|
|
|
520
479
|
/// `+8` (expected a `!T` / `T?`) an error value (`error.X`, `null`, or a variable
|
|
@@ -579,7 +538,7 @@ fn recordErrCoercion(
|
|
|
579
538
|
_ => return,
|
|
580
539
|
};
|
|
581
540
|
let ptr = expr as *const ast::Expr as usize;
|
|
582
|
-
|
|
541
|
+
withCheck(|s| { let r = &s.record; {
|
|
583
542
|
if let Some(m) = r.borrow_mut().as_mut() {
|
|
584
543
|
for k in [ERR_WRAP, ERR_WRAP2, ERR_NULL_NESTED, ERR_FROM_NAME] {
|
|
585
544
|
m.remove(&(ptr + k));
|
|
@@ -588,7 +547,7 @@ fn recordErrCoercion(
|
|
|
588
547
|
m.insert(ptr + k, t);
|
|
589
548
|
}
|
|
590
549
|
}
|
|
591
|
-
});
|
|
550
|
+
} });
|
|
592
551
|
}
|
|
593
552
|
|
|
594
553
|
/// `T?` with a reference-type `T` is represented as a nullable reference to `T`
|
|
@@ -612,11 +571,11 @@ pub fn nullableErrPayload(ty: &PlumType) -> Option<&PlumType> {
|
|
|
612
571
|
}
|
|
613
572
|
|
|
614
573
|
fn recordType(expr: &ast::Expr, ty: &PlumType) {
|
|
615
|
-
|
|
574
|
+
withCheck(|s| { let r = &s.record; {
|
|
616
575
|
if let Some(m) = r.borrow_mut().as_mut() {
|
|
617
576
|
m.insert(expr as *const ast::Expr as usize, ty.clone());
|
|
618
577
|
}
|
|
619
|
-
});
|
|
578
|
+
} });
|
|
620
579
|
}
|
|
621
580
|
|
|
622
581
|
/// Base name of a nominal type (`TNamed`/`TApp`).
|
|
@@ -762,8 +721,8 @@ fn addToErrSet(set: &ErrSet, names: &BTreeSet<String>) -> Result<(), String> {
|
|
|
762
721
|
None => Ok(()),
|
|
763
722
|
},
|
|
764
723
|
ErrSet::Inferred(k) => {
|
|
765
|
-
if !k.is_empty() &&
|
|
724
|
+
if !k.is_empty() && withCheck(|s| { let c = &s.cur_fn; c.borrow().as_deref() == Some(k.as_str()) }) {
|
|
766
|
-
|
|
725
|
+
withCheck(|s| { let a = &s.err_acc; a.borrow_mut().extend(names.iter().cloned()) });
|
|
767
726
|
}
|
|
768
727
|
Ok(())
|
|
769
728
|
}
|
|
@@ -1201,7 +1160,7 @@ pub fn checkSource(source: &ast::Source) -> CheckResult<()> {
|
|
|
1201
1160
|
/// Type-checks the original source like `checkSource` and also returns the
|
|
1202
1161
|
/// instantiated type of every expression (keyed by expression address) and the
|
|
1203
1162
|
/// set of generic instantiations (see `TypedInfo`) for codegen.
|
|
1204
|
-
/// Switches for the supercompiler stage of `typedSource`
|
|
1163
|
+
/// Switches for the supercompiler stage of `typedSource`.
|
|
1205
1164
|
#[derive(Clone, Copy, Debug)]
|
|
1206
1165
|
pub struct FusionOptions {
|
|
1207
1166
|
/// Run the fusion stage (`--no-fusion` clears it).
|
|
@@ -1216,32 +1175,6 @@ impl Default for FusionOptions {
|
|
|
1216
1175
|
}
|
|
1217
1176
|
}
|
|
1218
1177
|
|
|
1219
|
-
thread_local! {
|
|
1220
|
-
static FUSION_OPTIONS: Cell<FusionOptions> = Cell::new(FusionOptions::default());
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
pub fn setFusionOptions(o: FusionOptions) {
|
|
1224
|
-
FUSION_OPTIONS.with(|c| c.set(o));
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
pub fn fusionOptions() -> FusionOptions {
|
|
1228
|
-
FUSION_OPTIONS.with(|c| c.get())
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
/// Runs `f` with `o` as this thread's fusion options, restoring the previous ones after
|
|
1232
|
-
/// (also on panic).
|
|
1233
|
-
pub fn withFusionOptions<R>(o: FusionOptions, f: impl FnOnce() -> R) -> R {
|
|
1234
|
-
struct Restore(FusionOptions);
|
|
1235
|
-
impl Drop for Restore {
|
|
1236
|
-
fn drop(&mut self) {
|
|
1237
|
-
setFusionOptions(self.0);
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
let _restore = Restore(fusionOptions());
|
|
1241
|
-
setFusionOptions(o);
|
|
1242
|
-
f()
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
1178
|
/// Whether `obj_ty.field` is declared immutable (a record or enum field without `mut`).
|
|
1246
1179
|
/// `None` for structs (value-local assignment stays allowed) and unknown types.
|
|
1247
1180
|
fn immutableFieldOwner(obj_ty: &PlumType, field: &str, ctx: &CheckCtx) -> Option<String> {
|
|
@@ -1266,7 +1199,7 @@ fn immutableFieldOwner(obj_ty: &PlumType, field: &str, ctx: &CheckCtx) -> Option
|
|
|
1266
1199
|
}
|
|
1267
1200
|
|
|
1268
1201
|
pub fn typedSource(source: &ast::Source) -> CheckResult<TypedInfo> {
|
|
1269
|
-
typedSourceWith(source,
|
|
1202
|
+
typedSourceWith(source, FusionOptions::default())
|
|
1270
1203
|
}
|
|
1271
1204
|
|
|
1272
1205
|
/// Like `typedSource` with explicit fusion options (no thread-local read).
|
|
@@ -1578,11 +1511,13 @@ fn ioCapabilityErrors(source: &ast::Source) -> Vec<CheckError> {
|
|
|
1578
1511
|
}
|
|
1579
1512
|
|
|
1580
1513
|
fn checkSourceOnce(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
1514
|
+
resetCheck();
|
|
1515
|
+
withCheck(|s| {
|
|
1516
|
+
*s.record.borrow_mut() = Some(HashMap::new());
|
|
1517
|
+
*s.record_binds.borrow_mut() = Some(Binds::new());
|
|
1518
|
+
*s.calls.borrow_mut() = Some(Vec::new());
|
|
1581
|
-
|
|
1519
|
+
*s.err_sites.borrow_mut() = Some(HashMap::new());
|
|
1582
|
-
RECORD_BINDS.with(|r| *r.borrow_mut() = Some(Binds::new()));
|
|
1583
|
-
CALLS.with(|r| *r.borrow_mut() = Some(Vec::new()));
|
|
1584
|
-
|
|
1520
|
+
});
|
|
1585
|
-
ERR_SITES.with(|r| *r.borrow_mut() = Some(HashMap::new()));
|
|
1586
1521
|
let (result, mut info) = checkSourceInner(source);
|
|
1587
1522
|
let result = {
|
|
1588
1523
|
let own = ioCapabilityErrors(source);
|
|
@@ -1594,19 +1529,17 @@ fn checkSourceOnce(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
|
1594
1529
|
Err(errs)
|
|
1595
1530
|
}
|
|
1596
1531
|
};
|
|
1597
|
-
info.err_sites = ERR_SITES
|
|
1598
|
-
|
|
1532
|
+
info.err_sites = withCheck(|s| { let r = &s.err_sites; r.borrow_mut().take() })
|
|
1599
1533
|
.unwrap_or_default();
|
|
1600
1534
|
fillErrorTables(source, &mut info);
|
|
1601
1535
|
let result = result.map_err(|errs| nameFormErrors(source, errs));
|
|
1602
|
-
info.op_calls =
|
|
1536
|
+
info.op_calls = withCheck(|s| { let r = &s.op_calls; std::mem::take(&mut *r.borrow_mut()) });
|
|
1603
|
-
let mut recorded =
|
|
1537
|
+
let mut recorded = withCheck(|s| { let r = &s.record; r.borrow_mut().take() }).unwrap_or_default();
|
|
1604
|
-
let binds = RECORD_BINDS
|
|
1605
|
-
|
|
1538
|
+
let binds = withCheck(|s| { let r = &s.record_binds; r.borrow_mut().take() })
|
|
1606
1539
|
.unwrap_or_default();
|
|
1607
|
-
|
|
1540
|
+
withCheck(|s| { let r = &s.calls; r.borrow_mut().take() });
|
|
1608
|
-
|
|
1541
|
+
withCheck(|s| { let s = &s.tsubst; *s.borrow_mut() = None });
|
|
1609
|
-
|
|
1542
|
+
withCheck(|s| { let m = &s.inst_mode; m.set(false) });
|
|
1610
1543
|
// Zonk: apply the final substitution; a variable nothing ever constrained
|
|
1611
1544
|
// (an unused `None`, an empty literal, ...) is defaulted to `Unit`, since
|
|
1612
1545
|
// no value of it can ever be observed.
|
|
@@ -2347,18 +2280,18 @@ fn checkSourceInner(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
|
2347
2280
|
file,
|
|
2348
2281
|
};
|
|
2349
2282
|
drainCalls();
|
|
2350
|
-
|
|
2283
|
+
withCheck(|s| { let c = &s.calls; {
|
|
2351
2284
|
if let Some(v) = c.borrow_mut().as_mut() {
|
|
2352
2285
|
v.extend(const_calls);
|
|
2353
2286
|
}
|
|
2354
|
-
});
|
|
2287
|
+
} });
|
|
2355
2288
|
registerCalls(&mut inst, &fn_by_key, &ctx, None);
|
|
2356
2289
|
|
|
2357
2290
|
// Roots: every non-generic fn and every test. A generic fn is only
|
|
2358
2291
|
// name-resolved here (its body is type-checked per instantiation below).
|
|
2359
2292
|
for item in &source.items {
|
|
2360
2293
|
if let ast::Item::Fn(f) = item {
|
|
2361
|
-
|
|
2294
|
+
withCheck(|s| { let l = &s.cur_pos; l.set((f.line, f.column)) });
|
|
2362
2295
|
if let Some(e) = exportFnError(f, &ctx) {
|
|
2363
2296
|
errors.push(e);
|
|
2364
2297
|
continue;
|
|
@@ -2376,7 +2309,7 @@ fn checkSourceInner(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
|
2376
2309
|
|
|
2377
2310
|
for item in &source.items {
|
|
2378
2311
|
if let ast::Item::Test(t) = item {
|
|
2379
|
-
|
|
2312
|
+
withCheck(|s| { let l = &s.cur_pos; l.set((t.line, t.column)) });
|
|
2380
2313
|
let mut env = global_env.clone();
|
|
2381
2314
|
// A test may use `io` when std/Io is loaded (the test wrapper passes it).
|
|
2382
2315
|
if source.items.iter().any(|i| matches!(i, ast::Item::Enum(e) if e.name == "Io")) {
|
|
@@ -2418,14 +2351,14 @@ fn checkSourceInner(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
|
2418
2351
|
.cloned()
|
|
2419
2352
|
.zip(i.type_args.iter().cloned())
|
|
2420
2353
|
.collect();
|
|
2421
|
-
|
|
2354
|
+
withCheck(|s| { let s = &s.tsubst; *s.borrow_mut() = Some(subst) });
|
|
2422
|
-
|
|
2355
|
+
withCheck(|s| { let m = &s.inst_mode; m.set(true) });
|
|
2423
|
-
|
|
2356
|
+
withCheck(|s| { let l = &s.cur_pos; l.set((f.line, f.column)) });
|
|
2424
|
-
let saved =
|
|
2357
|
+
let saved = withCheck(|s| { let r = &s.record; r.borrow_mut().replace(HashMap::new()) });
|
|
2425
2358
|
let errs = checkFn(f, &global_env, &ctx);
|
|
2426
|
-
let own =
|
|
2359
|
+
let own = withCheck(|s| { let r = &s.record; std::mem::replace(&mut *r.borrow_mut(), saved) });
|
|
2427
|
-
|
|
2360
|
+
withCheck(|s| { let s = &s.tsubst; *s.borrow_mut() = None });
|
|
2428
|
-
|
|
2361
|
+
withCheck(|s| { let m = &s.inst_mode; m.set(false) });
|
|
2429
2362
|
for (ptr, t) in own.unwrap_or_default() {
|
|
2430
2363
|
inst.info.inst_expr_types.insert((id, ptr), t);
|
|
2431
2364
|
}
|
|
@@ -2481,21 +2414,21 @@ fn inferErrorSets(source: &ast::Source, global_env: &TypeEnv, ctx: &CheckCtx) ->
|
|
|
2481
2414
|
m.borrow_mut().insert(fnErrKey(f), BTreeSet::new());
|
|
2482
2415
|
}
|
|
2483
2416
|
});
|
|
2484
|
-
let saved_record =
|
|
2417
|
+
let saved_record = withCheck(|s| { let r = &s.record; r.borrow_mut().take() });
|
|
2485
|
-
let saved_calls =
|
|
2418
|
+
let saved_calls = withCheck(|s| { let r = &s.calls; r.borrow_mut().take() });
|
|
2486
|
-
let saved_sites =
|
|
2419
|
+
let saved_sites = withCheck(|s| { let r = &s.err_sites; r.borrow_mut().take() });
|
|
2487
|
-
|
|
2420
|
+
withCheck(|s| { let e = &s.err_edges; e.borrow_mut().clear() });
|
|
2488
2421
|
for _ in 0..64 {
|
|
2489
2422
|
let mut changed = false;
|
|
2490
2423
|
for f in &fns {
|
|
2491
2424
|
let key = fnErrKey(f);
|
|
2492
|
-
|
|
2425
|
+
withCheck(|s| { let c = &s.cur_fn; *c.borrow_mut() = Some(key.clone()) });
|
|
2493
|
-
|
|
2426
|
+
withCheck(|s| { let a = &s.err_acc; a.borrow_mut().clear() });
|
|
2494
|
-
|
|
2427
|
+
withCheck(|s| { let r = &s.record; *r.borrow_mut() = Some(HashMap::new()) });
|
|
2495
|
-
|
|
2428
|
+
withCheck(|s| { let r = &s.calls; *r.borrow_mut() = Some(Vec::new()) });
|
|
2496
|
-
|
|
2429
|
+
withCheck(|s| { let l = &s.cur_pos; l.set((f.line, f.column)) });
|
|
2497
2430
|
let _ = checkFn(f, global_env, ctx);
|
|
2498
|
-
let acc =
|
|
2431
|
+
let acc = withCheck(|s| { let a = &s.err_acc; std::mem::take(&mut *a.borrow_mut()) });
|
|
2499
2432
|
FN_ERR_SETS.with(|m| {
|
|
2500
2433
|
let mut m = m.borrow_mut();
|
|
2501
2434
|
if m.get(&key) != Some(&acc) {
|
|
@@ -2508,11 +2441,11 @@ fn inferErrorSets(source: &ast::Source, global_env: &TypeEnv, ctx: &CheckCtx) ->
|
|
|
2508
2441
|
break;
|
|
2509
2442
|
}
|
|
2510
2443
|
}
|
|
2511
|
-
|
|
2444
|
+
withCheck(|s| { let c = &s.cur_fn; *c.borrow_mut() = None });
|
|
2512
|
-
|
|
2445
|
+
withCheck(|s| { let r = &s.record; *r.borrow_mut() = saved_record });
|
|
2513
|
-
|
|
2446
|
+
withCheck(|s| { let r = &s.calls; *r.borrow_mut() = saved_calls });
|
|
2514
|
-
|
|
2447
|
+
withCheck(|s| { let r = &s.err_sites; *r.borrow_mut() = saved_sites });
|
|
2515
|
-
let edges =
|
|
2448
|
+
let edges = withCheck(|s| { let e = &s.err_edges; std::mem::take(&mut *e.borrow_mut()) });
|
|
2516
2449
|
for f in &fns {
|
|
2517
2450
|
let key = fnErrKey(f);
|
|
2518
2451
|
if f.is_extern {
|
|
@@ -2575,12 +2508,12 @@ fn isResolutionError(msg: &str) -> bool {
|
|
|
2575
2508
|
}
|
|
2576
2509
|
|
|
2577
2510
|
fn drainCalls() -> Vec<CallSite> {
|
|
2578
|
-
|
|
2511
|
+
withCheck(|s| { let c = &s.calls; {
|
|
2579
2512
|
c.borrow_mut()
|
|
2580
2513
|
.as_mut()
|
|
2581
2514
|
.map(std::mem::take)
|
|
2582
2515
|
.unwrap_or_default()
|
|
2583
|
-
})
|
|
2516
|
+
} })
|
|
2584
2517
|
}
|
|
2585
2518
|
|
|
2586
2519
|
/// Generic parameter names of `f`: its owner type's generics, then the free
|
|
@@ -2625,8 +2558,7 @@ fn registerCalls(
|
|
|
2625
2558
|
ctx: &CheckCtx,
|
|
2626
2559
|
parent: Option<usize>,
|
|
2627
2560
|
) {
|
|
2628
|
-
let binds = RECORD_BINDS
|
|
2629
|
-
|
|
2561
|
+
let binds = withCheck(|s| { let r = &s.record_binds; r.borrow().clone() })
|
|
2630
2562
|
.unwrap_or_default();
|
|
2631
2563
|
let parent_chain = parent.map(|p| st.chains[p].clone()).unwrap_or_default();
|
|
2632
2564
|
let depth = parent_chain.matches("(called at").count();
|
|
@@ -2792,7 +2724,7 @@ fn checkFn(f: &ast::Fn, global_env: &TypeEnv, ctx: &CheckCtx) -> Vec<CheckError>
|
|
|
2792
2724
|
.collect(),
|
|
2793
2725
|
)
|
|
2794
2726
|
};
|
|
2795
|
-
let self_ty = match
|
|
2727
|
+
let self_ty = match withCheck(|s| { let s = &s.tsubst; s.borrow().clone() }) {
|
|
2796
2728
|
Some(m) => types::substitute(&self_ty, &m),
|
|
2797
2729
|
None => self_ty,
|
|
2798
2730
|
};
|
|
@@ -2855,10 +2787,10 @@ fn checkFn(f: &ast::Fn, global_env: &TypeEnv, ctx: &CheckCtx) -> Vec<CheckError>
|
|
|
2855
2787
|
let n_init = init.len();
|
|
2856
2788
|
for (i, stmt) in init.iter().enumerate() {
|
|
2857
2789
|
if tail.is_none() && i + 1 == n_init && !matches!(declared_ret, PlumType::TUnit) {
|
|
2858
|
-
|
|
2790
|
+
withCheck(|s| { let c = &s.tail; *c.borrow_mut() = Some(declared_ret.clone()) });
|
|
2859
2791
|
}
|
|
2860
2792
|
if let Some(hint) = closureHintFor(&block.stmts[i..], &env, ctx) {
|
|
2861
|
-
|
|
2793
|
+
withCheck(|s| { let h = &s.closure_hint; *h.borrow_mut() = Some(hint) });
|
|
2862
2794
|
}
|
|
2863
2795
|
errors.append(&mut checkStmt(
|
|
2864
2796
|
stmt,
|
|
@@ -2868,7 +2800,7 @@ fn checkFn(f: &ast::Fn, global_env: &TypeEnv, ctx: &CheckCtx) -> Vec<CheckError>
|
|
|
2868
2800
|
(f.line, f.column),
|
|
2869
2801
|
ctx,
|
|
2870
2802
|
));
|
|
2871
|
-
|
|
2803
|
+
withCheck(|s| { let h = &s.closure_hint; *h.borrow_mut() = None });
|
|
2872
2804
|
}
|
|
2873
2805
|
if let Some((last_expr, line, column)) = tail {
|
|
2874
2806
|
match inferExprWith(last_expr, &env, ctx, Some(&declared_ret)) {
|
|
@@ -2904,16 +2836,16 @@ fn checkBlock(
|
|
|
2904
2836
|
ctx: &CheckCtx,
|
|
2905
2837
|
) -> Vec<CheckError> {
|
|
2906
2838
|
let mut errors = Vec::new();
|
|
2907
|
-
let tail =
|
|
2839
|
+
let tail = withCheck(|s| { let t = &s.tail; t.borrow_mut().take() });
|
|
2908
2840
|
for (i, stmt) in block.stmts.iter().enumerate() {
|
|
2909
2841
|
if i + 1 == block.stmts.len() {
|
|
2910
|
-
|
|
2842
|
+
withCheck(|s| { let t = &s.tail; *t.borrow_mut() = tail.clone() });
|
|
2911
2843
|
}
|
|
2912
2844
|
if let Some(hint) = closureHintFor(&block.stmts[i..], env, ctx) {
|
|
2913
|
-
|
|
2845
|
+
withCheck(|s| { let h = &s.closure_hint; *h.borrow_mut() = Some(hint) });
|
|
2914
2846
|
}
|
|
2915
2847
|
let mut stmt_errors = checkStmt(stmt, env, declared_ret, fn_name, fn_pos, ctx);
|
|
2916
|
-
|
|
2848
|
+
withCheck(|s| { let h = &s.closure_hint; *h.borrow_mut() = None });
|
|
2917
2849
|
errors.append(&mut stmt_errors);
|
|
2918
2850
|
}
|
|
2919
2851
|
errors
|
|
@@ -3092,9 +3024,9 @@ fn checkStmt(
|
|
|
3092
3024
|
fn_pos: (usize, usize),
|
|
3093
3025
|
ctx: &CheckCtx,
|
|
3094
3026
|
) -> Vec<CheckError> {
|
|
3095
|
-
let saved =
|
|
3027
|
+
let saved = withCheck(|s| { let e = &s.err_pos; e.replace(None) });
|
|
3096
3028
|
let mut errors = checkStmtInner(stmt, env, declared_ret, fn_name, fn_pos, ctx);
|
|
3097
|
-
if let Some((l, c)) =
|
|
3029
|
+
if let Some((l, c)) = withCheck(|s| { let e = &s.err_pos; e.replace(saved) }) {
|
|
3098
3030
|
for e in errors.iter_mut().filter(|e| (e.line, e.column) == fn_pos) {
|
|
3099
3031
|
e.line = l;
|
|
3100
3032
|
e.column = c;
|
|
@@ -3112,8 +3044,8 @@ fn checkStmtInner(
|
|
|
3112
3044
|
ctx: &CheckCtx,
|
|
3113
3045
|
) -> Vec<CheckError> {
|
|
3114
3046
|
let mut errors = Vec::new();
|
|
3115
|
-
let tail =
|
|
3047
|
+
let tail = withCheck(|s| { let t = &s.tail; t.borrow_mut().take() });
|
|
3116
|
-
let set_tail = |t: &Option<PlumType>|
|
|
3048
|
+
let set_tail = |t: &Option<PlumType>| withCheck(|s| { let c = &s.tail; *c.borrow_mut() = t.clone() });
|
|
3117
3049
|
match stmt {
|
|
3118
3050
|
// Lowered here, once the type of the source is known (`forms::lowerLoop`);
|
|
3119
3051
|
// the lowering is checked in place. After `forms::finish` no loops are left.
|
|
@@ -3581,7 +3513,7 @@ fn checkStmtInner(
|
|
|
3581
3513
|
)),
|
|
3582
3514
|
},
|
|
3583
3515
|
ast::Stmt::Match(m) => {
|
|
3584
|
-
|
|
3516
|
+
withCheck(|s| { let c = &s.match_tail; *c.borrow_mut() = tail.clone() });
|
|
3585
3517
|
errors.append(&mut checkMatch(m, env, declared_ret, fn_name, fn_pos, ctx));
|
|
3586
3518
|
}
|
|
3587
3519
|
ast::Stmt::Break | ast::Stmt::Continue | ast::Stmt::Todo => {}
|
|
@@ -3840,7 +3772,7 @@ fn checkMatch(
|
|
|
3840
3772
|
fn_pos: (usize, usize),
|
|
3841
3773
|
ctx: &CheckCtx,
|
|
3842
3774
|
) -> Vec<CheckError> {
|
|
3843
|
-
let match_tail =
|
|
3775
|
+
let match_tail = withCheck(|s| { let c = &s.match_tail; c.borrow_mut().take() });
|
|
3844
3776
|
let mut errors = Vec::new();
|
|
3845
3777
|
|
|
3846
3778
|
let mut subject_types: Vec<PlumType> = Vec::new();
|
|
@@ -3966,7 +3898,7 @@ fn checkMatch(
|
|
|
3966
3898
|
)),
|
|
3967
3899
|
}
|
|
3968
3900
|
}
|
|
3969
|
-
|
|
3901
|
+
withCheck(|s| { let c = &s.tail; *c.borrow_mut() = match_tail.clone() });
|
|
3970
3902
|
errors.append(&mut checkBlock(
|
|
3971
3903
|
&case.body,
|
|
3972
3904
|
&mut case_env,
|
|
@@ -4190,18 +4122,13 @@ fn liftInlineVariant(
|
|
|
4190
4122
|
));
|
|
4191
4123
|
}
|
|
4192
4124
|
}
|
|
4193
|
-
let addr =
|
|
4125
|
+
let addr = withCheck(|s| { let c = &s.cur_expr; c.get() });
|
|
4194
4126
|
if addr != 0 {
|
|
4195
4127
|
plum_core::unions::recordWrap(addr, hidden);
|
|
4196
4128
|
}
|
|
4197
4129
|
Ok(lifted)
|
|
4198
4130
|
}
|
|
4199
4131
|
|
|
4200
|
-
thread_local! {
|
|
4201
|
-
/// Address of a top-level `if X := v` pattern, where a bare inline-variant name
|
|
4202
|
-
/// narrows `v` instead of matching a value.
|
|
4203
|
-
static ALLOW_BARE_TOP: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
|
4204
|
-
}
|
|
4205
4132
|
|
|
4206
4133
|
/// A tag of the subject (enum variant, union member record, error name) that `n` equals
|
|
4207
4134
|
/// case-insensitively. A lowercase pattern name that spells a tag is a mistake, not a binder.
|
|
@@ -4249,7 +4176,7 @@ pub(crate) fn checkPattern(
|
|
|
4249
4176
|
// An inline variant with fields is never matched bare (Rust semantics): the bare
|
|
4250
4177
|
// name would narrow to the hidden member and silently never match as written.
|
|
4251
4178
|
if let ast::CasePattern::Name(n) = pat {
|
|
4252
|
-
let allowed =
|
|
4179
|
+
let allowed = withCheck(|s| { let c = &s.allow_bare_top; c.get() }) == pat as *const ast::CasePattern as usize;
|
|
4253
4180
|
if let (false, Some(owner), Some(subject)) =
|
|
4254
4181
|
(allowed, unions::inlineOwner(n), nominalName(subject_ty))
|
|
4255
4182
|
{
|
|
@@ -4592,7 +4519,7 @@ fn inferMethodCall(
|
|
|
4592
4519
|
ctx: &CheckCtx,
|
|
4593
4520
|
expected: Option<&PlumType>,
|
|
4594
4521
|
) -> Result<PlumType, String> {
|
|
4595
|
-
let call_addr =
|
|
4522
|
+
let call_addr = withCheck(|s| { let c = &s.cur_expr; c.get() });
|
|
4596
4523
|
// `e.name()` on an error value (`catch e`) is its text.
|
|
4597
4524
|
if matches!(obj_ty, PlumType::TErrName(_)) && name == "name" && args.is_empty() {
|
|
4598
4525
|
return Ok(types::strType());
|
|
@@ -4649,7 +4576,7 @@ fn inferMethodCall(
|
|
|
4649
4576
|
let Some(class_name) = methodReceiverName(obj_ty) else {
|
|
4650
4577
|
// Inside an instantiation a concrete receiver with no method table at
|
|
4651
4578
|
// all (unit, error name, fn type) has no methods.
|
|
4652
|
-
if
|
|
4579
|
+
if withCheck(|s| { let m = &s.inst_mode; m.get() })
|
|
4653
4580
|
&& !hasFreeVars(obj_ty)
|
|
4654
4581
|
&& !matches!(obj_ty, PlumType::TVar(_))
|
|
4655
4582
|
{
|
|
@@ -4703,7 +4630,7 @@ fn inferMethodCall(
|
|
|
4703
4630
|
// erased pass too, so a typo'd method is a positioned checker error
|
|
4704
4631
|
// rather than a late codegen failure.
|
|
4705
4632
|
if !hasFreeVars(obj_ty)
|
|
4706
|
-
&& (
|
|
4633
|
+
&& (withCheck(|s| { let m = &s.inst_mode; m.get() })
|
|
4707
4634
|
|| matches!(
|
|
4708
4635
|
obj_ty,
|
|
4709
4636
|
PlumType::TApp(..) | PlumType::TInt | PlumType::TBool
|
|
@@ -4844,7 +4771,7 @@ fn inferOperatorCall(
|
|
|
4844
4771
|
));
|
|
4845
4772
|
}
|
|
4846
4773
|
let addr = expr as *const ast::Expr as usize;
|
|
4847
|
-
let call =
|
|
4774
|
+
let call = withCheck(|s| { let m = &s.op_calls; {
|
|
4848
4775
|
m.borrow_mut()
|
|
4849
4776
|
.entry(addr)
|
|
4850
4777
|
.or_insert_with(|| {
|
|
@@ -4860,7 +4787,7 @@ fn inferOperatorCall(
|
|
|
4860
4787
|
})
|
|
4861
4788
|
})
|
|
4862
4789
|
.clone()
|
|
4863
|
-
});
|
|
4790
|
+
} });
|
|
4864
4791
|
inferMethodCall(lt, method, &call.args, env, ctx, expected)
|
|
4865
4792
|
}
|
|
4866
4793
|
|
|
@@ -4887,7 +4814,7 @@ pub fn inferExprWith(
|
|
|
4887
4814
|
ctx: &CheckCtx,
|
|
4888
4815
|
expected: Option<&PlumType>,
|
|
4889
4816
|
) -> Result<PlumType, String> {
|
|
4890
|
-
let prev =
|
|
4817
|
+
let prev = withCheck(|s| { let c = &s.cur_expr; c.replace(expr as *const ast::Expr as usize) });
|
|
4891
4818
|
let call_pos = match expr {
|
|
4892
4819
|
ast::Expr::FnCall(c) => Some((c.line, c.column)),
|
|
4893
4820
|
ast::Expr::Attribute(a) => match &a.attr {
|
|
@@ -4896,21 +4823,21 @@ pub fn inferExprWith(
|
|
|
4896
4823
|
},
|
|
4897
4824
|
_ => None,
|
|
4898
4825
|
};
|
|
4899
|
-
let prev_pos =
|
|
4826
|
+
let prev_pos = withCheck(|s| { let p = &s.cur_pos; p.get() });
|
|
4900
4827
|
if let Some(pos) = call_pos.filter(|p| p.0 != 0) {
|
|
4901
|
-
|
|
4828
|
+
withCheck(|s| { let p = &s.cur_pos; p.set(pos) });
|
|
4902
4829
|
}
|
|
4903
|
-
let entry_err_pos =
|
|
4830
|
+
let entry_err_pos = withCheck(|s| { let e = &s.err_pos; e.get() });
|
|
4904
4831
|
let r = inferExprInner(expr, env, ctx, expected);
|
|
4905
4832
|
if r.is_ok() {
|
|
4906
4833
|
// Any failure inside was recovered from; it must not mark the statement.
|
|
4907
|
-
|
|
4834
|
+
withCheck(|s| { let e = &s.err_pos; e.set(entry_err_pos) });
|
|
4908
4835
|
}
|
|
4909
|
-
if r.is_err() && call_pos.is_some_and(|p| p.0 != 0) &&
|
|
4836
|
+
if r.is_err() && call_pos.is_some_and(|p| p.0 != 0) && withCheck(|s| { let e = &s.err_pos; e.get() }).is_none() {
|
|
4910
|
-
|
|
4837
|
+
withCheck(|s| { let e = &s.err_pos; e.set(call_pos) });
|
|
4911
4838
|
}
|
|
4912
|
-
|
|
4839
|
+
withCheck(|s| { let p = &s.cur_pos; p.set(prev_pos) });
|
|
4913
|
-
|
|
4840
|
+
withCheck(|s| { let c = &s.cur_expr; c.set(prev) });
|
|
4914
4841
|
if let Ok(t) = &r {
|
|
4915
4842
|
recordType(expr, t);
|
|
4916
4843
|
recordErrCoercion(expr, t, expected, ctx);
|
|
@@ -4918,8 +4845,8 @@ pub fn inferExprWith(
|
|
|
4918
4845
|
// A call returning another fn's inferred `!T` is an edge of the call graph.
|
|
4919
4846
|
if let PlumType::TErr(_, ErrSet::Inferred(k)) = t {
|
|
4920
4847
|
if !k.is_empty() {
|
|
4921
|
-
if let Some(cur) =
|
|
4848
|
+
if let Some(cur) = withCheck(|s| { let c = &s.cur_fn; c.borrow().clone() }) {
|
|
4922
|
-
|
|
4849
|
+
withCheck(|s| { let e = &s.err_edges; e.borrow_mut().insert((cur, k.clone())) });
|
|
4923
4850
|
}
|
|
4924
4851
|
}
|
|
4925
4852
|
}
|
|
@@ -4933,7 +4860,7 @@ fn recordErrSite(expr: &ast::Expr, kind: ErrSiteKind, operand: &PlumType) {
|
|
|
4933
4860
|
.and_then(|(_, s)| errSetNames(&s))
|
|
4934
4861
|
.map(|n| n.into_iter().collect())
|
|
4935
4862
|
.unwrap_or_default();
|
|
4936
|
-
|
|
4863
|
+
withCheck(|s| { let m = &s.err_sites; {
|
|
4937
4864
|
if let Some(m) = m.borrow_mut().as_mut() {
|
|
4938
4865
|
m.insert(
|
|
4939
4866
|
expr as *const ast::Expr as usize,
|
|
@@ -4944,7 +4871,7 @@ fn recordErrSite(expr: &ast::Expr, kind: ErrSiteKind, operand: &PlumType) {
|
|
|
4944
4871
|
},
|
|
4945
4872
|
);
|
|
4946
4873
|
}
|
|
4947
|
-
});
|
|
4874
|
+
} });
|
|
4948
4875
|
}
|
|
4949
4876
|
|
|
4950
4877
|
/// The environment of the branch an `if`/`else if`/`while`/inline-`if`
|
|
@@ -4969,9 +4896,9 @@ fn bindCondEnv(cond: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<TypeEn
|
|
|
4969
4896
|
}
|
|
4970
4897
|
pat => {
|
|
4971
4898
|
let narrow = unions::narrowTarget(&b.value, pat, &ty, env, ctx);
|
|
4972
|
-
|
|
4899
|
+
withCheck(|s| { let c = &s.allow_bare_top; c.set(pat as *const ast::CasePattern as usize) });
|
|
4973
4900
|
let r = checkPattern(pat, &ty, &mut benv, ctx);
|
|
4974
|
-
|
|
4901
|
+
withCheck(|s| { let c = &s.allow_bare_top; c.set(0) });
|
|
4975
4902
|
r?;
|
|
4976
4903
|
unions::applyNarrow(narrow, &b.case, &mut benv, ctx);
|
|
4977
4904
|
}
|
|
@@ -4996,7 +4923,7 @@ fn checkCatchBlock(
|
|
|
4996
4923
|
.get(RETURN_KEY)
|
|
4997
4924
|
.map(|s| (*s.body).clone())
|
|
4998
4925
|
.unwrap_or_else(|| PlumType::TVar("_".to_string()));
|
|
4999
|
-
let fn_pos =
|
|
4926
|
+
let fn_pos = withCheck(|s| { let p = &s.cur_pos; p.get() });
|
|
5000
4927
|
let n = body.stmts.len();
|
|
5001
4928
|
for (i, stmt) in body.stmts.iter().enumerate() {
|
|
5002
4929
|
if i + 1 == n {
|
|
@@ -5012,7 +4939,7 @@ fn checkCatchBlock(
|
|
|
5012
4939
|
| ast::Stmt::Continue
|
|
5013
4940
|
| ast::Stmt::Todo => {}
|
|
5014
4941
|
ast::Stmt::If(_) | ast::Stmt::Match(_) => {
|
|
5015
|
-
|
|
4942
|
+
withCheck(|s| { let c = &s.tail; *c.borrow_mut() = Some(t.clone()) });
|
|
5016
4943
|
}
|
|
5017
4944
|
_ if matches!(t, PlumType::TUnit) => {}
|
|
5018
4945
|
_ => {
|
|
@@ -5241,7 +5168,7 @@ fn inferExprInner(
|
|
|
5241
5168
|
}
|
|
5242
5169
|
_ => (None, None),
|
|
5243
5170
|
};
|
|
5244
|
-
let (local_hint, strict_hint) = match
|
|
5171
|
+
let (local_hint, strict_hint) = match withCheck(|s| { let h = &s.closure_hint; h.borrow_mut().take() }) {
|
|
5245
5172
|
Some((v, strict)) => (Some(v), strict),
|
|
5246
5173
|
None => (None, false),
|
|
5247
5174
|
};
|
|
@@ -5669,11 +5596,11 @@ fn inferExprInner(
|
|
|
5669
5596
|
/// For `T.Name` the result is the concrete type's source name, for a bare `T`
|
|
5670
5597
|
/// the type itself.
|
|
5671
5598
|
fn typeParamBinding(n: &str) -> Option<PlumType> {
|
|
5672
|
-
if !
|
|
5599
|
+
if !withCheck(|s| { let m = &s.inst_mode; m.get() }) {
|
|
5673
5600
|
return None;
|
|
5674
5601
|
}
|
|
5675
5602
|
let owner = n.split_once(ast::MEMBER_SEP).map(|(o, _)| o).unwrap_or(n);
|
|
5676
|
-
|
|
5603
|
+
withCheck(|s| { let s = &s.tsubst; s.borrow().as_ref().and_then(|m| m.get(owner).cloned()) })
|
|
5677
5604
|
}
|
|
5678
5605
|
|
|
5679
5606
|
/// `Type.f()` must name a static (a `fun` without `self`) and `x.f()` a
|
plum-checker/src/state.rs
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
//! All per-check mutable state of the checker, in one value. It lives in a single
|
|
2
|
+
//! thread-local, reached only through `withCheck`; `resetCheck` returns it to its
|
|
3
|
+
//! initial value. `withCheck` borrows the state shared (all fields are cells), so
|
|
4
|
+
//! nested `withCheck` calls are safe.
|
|
5
|
+
|
|
6
|
+
use crate::types::PlumType;
|
|
7
|
+
use crate::{Binds, CallSite, ErrSite};
|
|
8
|
+
use plum_core::ast;
|
|
9
|
+
use std::cell::{Cell, RefCell};
|
|
10
|
+
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
|
11
|
+
|
|
12
|
+
#[derive(Default)]
|
|
13
|
+
pub(crate) struct CheckState {
|
|
14
|
+
/// Param types for the closure literal about to be inferred as `name := |..| ..`,
|
|
15
|
+
/// taken from the first later call of `name` (see `closureHintFor`). `Some(vec![])`
|
|
16
|
+
/// means "assigned to a local, no call information"; the flag is whether an unresolved
|
|
17
|
+
/// param is an error (the local is never used as a plain value either).
|
|
18
|
+
pub(crate) closure_hint: RefCell<Option<(Vec<PlumType>, bool)>>,
|
|
19
|
+
/// Counter for the fresh `_t<N>` inference variables minted per generic call site.
|
|
20
|
+
pub(crate) fresh: Cell<u32>,
|
|
21
|
+
/// When set, `inferExprWith` records every expression's type here (see `typedSource`).
|
|
22
|
+
pub(crate) record_binds: RefCell<Option<Binds>>,
|
|
23
|
+
pub(crate) record: RefCell<Option<HashMap<usize, PlumType>>>,
|
|
24
|
+
/// Generic-call sites seen while checking one body (see `CallSite`); `None` outside `checkSourceInner`.
|
|
25
|
+
pub(crate) calls: RefCell<Option<Vec<CallSite>>>,
|
|
26
|
+
/// While checking a generic body for one instantiation: its generic parameter -> concrete type.
|
|
27
|
+
pub(crate) tsubst: RefCell<Option<BTreeMap<String, PlumType>>>,
|
|
28
|
+
/// True while checking an instantiated generic body (method lookups on declared types are strict).
|
|
29
|
+
pub(crate) inst_mode: Cell<bool>,
|
|
30
|
+
/// Position (line, column) of the innermost call expression being inferred, or of the
|
|
31
|
+
/// declaration being checked outside any call: the "called at" site of recorded calls.
|
|
32
|
+
pub(crate) cur_pos: Cell<(usize, usize)>,
|
|
33
|
+
/// Position of the innermost call whose inference failed, so a statement's
|
|
34
|
+
/// error points at that call rather than the enclosing fn/test header.
|
|
35
|
+
pub(crate) err_pos: Cell<Option<(usize, usize)>>,
|
|
36
|
+
/// Address of the expression `inferExprWith` is currently inferring (the innermost one).
|
|
37
|
+
pub(crate) cur_expr: Cell<usize>,
|
|
38
|
+
/// While checking the last statement of a value-producing tail: the type that tail
|
|
39
|
+
/// value must have, so its generic arguments are pinned like a returned expression's.
|
|
40
|
+
pub(crate) tail: RefCell<Option<PlumType>>,
|
|
41
|
+
pub(crate) match_tail: RefCell<Option<PlumType>>,
|
|
42
|
+
/// Key of the fn whose inferred error set the pre-pass is collecting (`None` otherwise).
|
|
43
|
+
pub(crate) cur_fn: RefCell<Option<String>>,
|
|
44
|
+
/// Error names contributed to `cur_fn`'s set by the body being checked.
|
|
45
|
+
pub(crate) err_acc: RefCell<BTreeSet<String>>,
|
|
46
|
+
/// Call edges `(caller key, callee key)` seen while collecting error sets.
|
|
47
|
+
pub(crate) err_edges: RefCell<BTreeSet<(String, String)>>,
|
|
48
|
+
/// Error-union sites (`try`, `!`, `catch`, `?:`) recorded for codegen; `None` while collecting.
|
|
49
|
+
pub(crate) err_sites: RefCell<Option<HashMap<usize, ErrSite>>>,
|
|
50
|
+
/// Operator expressions (address) on non-primitive operands -> the synthetic method call
|
|
51
|
+
/// (`a + b` -> `a.add(b)`) they resolve to. Kept so the cloned arguments have stable addresses.
|
|
52
|
+
pub(crate) op_calls: RefCell<HashMap<usize, std::rc::Rc<ast::FnCall>>>,
|
|
53
|
+
/// Trait name -> declared method names (required first, then defaults); filled by `traits::register`.
|
|
54
|
+
pub(crate) traits: RefCell<BTreeMap<String, Vec<String>>>,
|
|
55
|
+
/// Trait name -> declared fields `(name, is mut)`.
|
|
56
|
+
pub(crate) trait_fields: RefCell<BTreeMap<String, Vec<(String, bool)>>>,
|
|
57
|
+
/// Set while a member field is being unified, so union conversions do not nest.
|
|
58
|
+
pub(crate) in_wrap: Cell<bool>,
|
|
59
|
+
/// Nesting depth inside type arguments / fn types: conversion applies only at depth 0.
|
|
60
|
+
pub(crate) depth: Cell<usize>,
|
|
61
|
+
/// Inline-variant record name -> the enum it is a variant of.
|
|
62
|
+
pub(crate) inline: RefCell<BTreeMap<String, String>>,
|
|
63
|
+
/// Address of a top-level pattern that narrows (it records a narrowing, not a pattern).
|
|
64
|
+
pub(crate) narrow_top: Cell<usize>,
|
|
65
|
+
/// Address of the one case pattern whose bare payload-variant form is allowed.
|
|
66
|
+
pub(crate) allow_bare_top: Cell<usize>,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
thread_local! {
|
|
70
|
+
static CHECK: RefCell<CheckState> = RefCell::new(CheckState::default());
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Run `f` with the checker state. Every field is a `Cell`/`RefCell`, so the state is
|
|
74
|
+
/// borrowed shared: a `withCheck` inside `f` (a nested call through inference) is fine;
|
|
75
|
+
/// only a second `borrow_mut` of the same field panics, as with separate thread-locals.
|
|
76
|
+
pub(crate) fn withCheck<R>(f: impl FnOnce(&CheckState) -> R) -> R {
|
|
77
|
+
CHECK.with(|c| f(&c.borrow()))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Return every field to its initial value. Must not run inside `withCheck`.
|
|
81
|
+
pub(crate) fn resetCheck() {
|
|
82
|
+
CHECK.with(|c| *c.borrow_mut() = CheckState::default());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#[cfg(test)]
|
|
86
|
+
mod tests {
|
|
87
|
+
use super::*;
|
|
88
|
+
|
|
89
|
+
#[test]
|
|
90
|
+
fn resetZeroesFresh() {
|
|
91
|
+
withCheck(|s| s.fresh.set(7));
|
|
92
|
+
resetCheck();
|
|
93
|
+
assert_eq!(withCheck(|s| s.fresh.get()), 0);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#[test]
|
|
97
|
+
fn nestedWithCheckDoesNotPanic() {
|
|
98
|
+
// a borrow held across a call that itself reaches the state (e.g. through
|
|
99
|
+
// inference) must not double-borrow the whole state
|
|
100
|
+
let n = withCheck(|outer| {
|
|
101
|
+
outer.depth.set(1);
|
|
102
|
+
withCheck(|inner| inner.depth.get() + 1)
|
|
103
|
+
});
|
|
104
|
+
assert_eq!(n, 2);
|
|
105
|
+
resetCheck();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#[test]
|
|
109
|
+
fn typedSourceIsRepeatable() {
|
|
110
|
+
let src = plum_test_support::parse(
|
|
111
|
+
"fun id(x: T) -> T = x\nfun main() -> Int =\n a := id(1)\n b := id(true)\n a\n",
|
|
112
|
+
);
|
|
113
|
+
let first = crate::typedSource(&src).expect("checks");
|
|
114
|
+
let second = crate::typedSource(&src).expect("checks");
|
|
115
|
+
// generated names (closure copies, `_t<N>` vars) must not depend on earlier checks
|
|
116
|
+
let names = |i: &crate::TypedInfo| format!("{:?}", i.lowered.as_ref().map(|l| l.items.len()));
|
|
117
|
+
assert_eq!(names(&first), names(&second));
|
|
118
|
+
assert_eq!(first.instantiations, second.instantiations);
|
|
119
|
+
let sorted = |i: &crate::TypedInfo| {
|
|
120
|
+
let mut v: Vec<_> = i.inst_index.iter().map(|(k, v)| (format!("{:?}", k), *v)).collect();
|
|
121
|
+
v.sort();
|
|
122
|
+
v
|
|
123
|
+
};
|
|
124
|
+
assert_eq!(sorted(&first), sorted(&second));
|
|
125
|
+
}
|
|
126
|
+
}
|
plum-checker/src/traits.rs
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
//! Trait rules in the checker. A trait is a union enum (`Enum.isTrait`) whose
|
|
2
2
|
//! members are the embedding records; the declared methods come from `Source.traits`.
|
|
3
|
+
use crate::state::withCheck;
|
|
3
4
|
use crate::CheckCtx;
|
|
4
5
|
use plum_core::ast;
|
|
5
|
-
use std::cell::RefCell;
|
|
6
6
|
use std::collections::BTreeMap;
|
|
7
7
|
|
|
8
|
-
thread_local! {
|
|
9
|
-
static TRAITS: RefCell<BTreeMap<String, Vec<String>>> = const { RefCell::new(BTreeMap::new()) };
|
|
10
|
-
static TRAIT_FIELDS: RefCell<BTreeMap<String, Vec<(String, bool)>>> = const { RefCell::new(BTreeMap::new()) };
|
|
11
|
-
}
|
|
12
|
-
|
|
13
8
|
/// Records the traits of `source` (called while the global tables are built).
|
|
14
9
|
pub fn register(source: &ast::Source) {
|
|
15
10
|
let mut out: BTreeMap<String, Vec<String>> = BTreeMap::new();
|
|
@@ -29,31 +24,31 @@ pub fn register(source: &ast::Source) {
|
|
|
29
24
|
.collect();
|
|
30
25
|
out.insert(t.name.clone(), names);
|
|
31
26
|
}
|
|
32
|
-
|
|
27
|
+
withCheck(|s| { let c = &s.traits; *c.borrow_mut() = out });
|
|
33
28
|
let fields = source
|
|
34
29
|
.traits
|
|
35
30
|
.iter()
|
|
36
31
|
.map(|t| (t.name.clone(), t.fields.iter().map(|f| (f.name.clone(), f.isMut)).collect()))
|
|
37
32
|
.collect();
|
|
38
|
-
|
|
33
|
+
withCheck(|s| { let c = &s.trait_fields; *c.borrow_mut() = fields });
|
|
39
34
|
}
|
|
40
35
|
|
|
41
36
|
/// `Some(false)` when `trait_` declares `field` without `mut`; `Some(true)` when `mut`.
|
|
42
37
|
pub fn traitFieldIsMut(trait_: &str, field: &str) -> Option<bool> {
|
|
43
|
-
|
|
38
|
+
withCheck(|s| { let c = &s.trait_fields; {
|
|
44
39
|
c.borrow()
|
|
45
40
|
.get(trait_)
|
|
46
41
|
.and_then(|fs| fs.iter().find(|(n, _)| n == field).map(|(_, m)| *m))
|
|
47
|
-
})
|
|
42
|
+
} })
|
|
48
43
|
}
|
|
49
44
|
|
|
50
45
|
pub fn isTrait(name: &str, _ctx: &CheckCtx) -> bool {
|
|
51
|
-
|
|
46
|
+
withCheck(|s| { let c = &s.traits; c.borrow().contains_key(name) })
|
|
52
47
|
}
|
|
53
48
|
|
|
54
49
|
/// The trait's declared methods: required first, then defaults, declaration order.
|
|
55
50
|
pub fn traitMethods(name: &str, _ctx: &CheckCtx) -> Vec<String> {
|
|
56
|
-
|
|
51
|
+
withCheck(|s| { let c = &s.traits; c.borrow().get(name).cloned().unwrap_or_default() })
|
|
57
52
|
}
|
|
58
53
|
|
|
59
54
|
/// The error for calling `method` on a value of trait type `ty`, if it is not declared.
|
plum-checker/src/unions.rs
CHANGED
|
@@ -1,25 +1,12 @@
|
|
|
1
1
|
//! Union enums in the checker: a member value converts to its enum, and
|
|
2
2
|
//! branches of different members join to their common enum.
|
|
3
|
+
use crate::state::withCheck;
|
|
3
4
|
use crate::types::{self, PlumType};
|
|
4
5
|
use crate::{unify, CheckCtx};
|
|
5
6
|
use plum_core::ast;
|
|
6
7
|
use plum_core::unions as plan;
|
|
7
|
-
use std::cell::Cell;
|
|
8
8
|
use std::collections::{BTreeMap, BTreeSet};
|
|
9
9
|
|
|
10
|
-
thread_local! {
|
|
11
|
-
/// Set while a member field is being unified, so conversions do not nest.
|
|
12
|
-
static IN_WRAP: Cell<bool> = const { Cell::new(false) };
|
|
13
|
-
/// Nesting depth inside type arguments / fn types: conversion applies only at depth 0.
|
|
14
|
-
static DEPTH: Cell<usize> = const { Cell::new(0) };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
thread_local! {
|
|
18
|
-
/// Inline-variant record name -> the enum it is a variant of.
|
|
19
|
-
static INLINE: std::cell::RefCell<BTreeMap<String, String>> =
|
|
20
|
-
const { std::cell::RefCell::new(BTreeMap::new()) };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
10
|
/// Remembers the inline-variant records of `source` (called while the global tables are built).
|
|
24
11
|
pub fn registerInline(source: &ast::Source) {
|
|
25
12
|
let mut out = BTreeMap::new();
|
|
@@ -30,19 +17,19 @@ pub fn registerInline(source: &ast::Source) {
|
|
|
30
17
|
}
|
|
31
18
|
}
|
|
32
19
|
}
|
|
33
|
-
|
|
20
|
+
withCheck(|s| { let c = &s.inline; *c.borrow_mut() = out });
|
|
34
21
|
}
|
|
35
22
|
|
|
36
23
|
/// The enum that declares `record` inline, if it is an inline-variant record.
|
|
37
24
|
pub fn inlineOwner(record: &str) -> Option<String> {
|
|
38
|
-
|
|
25
|
+
withCheck(|s| { let c = &s.inline; c.borrow().get(record).cloned() })
|
|
39
26
|
}
|
|
40
27
|
|
|
41
28
|
/// Runs `f` (a unify of a nested type position), where no member conversion applies.
|
|
42
29
|
pub fn nested<R>(f: impl FnOnce() -> R) -> R {
|
|
43
|
-
|
|
30
|
+
withCheck(|s| { let d = &s.depth; d.set(d.get() + 1) });
|
|
44
31
|
let r = f();
|
|
45
|
-
|
|
32
|
+
withCheck(|s| { let d = &s.depth; d.set(d.get() - 1) });
|
|
46
33
|
r
|
|
47
34
|
}
|
|
48
35
|
|
|
@@ -50,9 +37,9 @@ pub fn nested<R>(f: impl FnOnce() -> R) -> R {
|
|
|
50
37
|
/// member converts to a union enum there, though the error union itself is a
|
|
51
38
|
/// nested position for everything else.
|
|
52
39
|
pub fn atTop<R>(f: impl FnOnce() -> R) -> R {
|
|
53
|
-
let saved =
|
|
40
|
+
let saved = withCheck(|s| { let d = &s.depth; d.replace(0) });
|
|
54
41
|
let r = f();
|
|
55
|
-
|
|
42
|
+
withCheck(|s| { let d = &s.depth; d.set(saved) });
|
|
56
43
|
r
|
|
57
44
|
}
|
|
58
45
|
|
|
@@ -101,7 +88,7 @@ pub fn isUnion(enumName: &str, ctx: &CheckCtx) -> bool {
|
|
|
101
88
|
|
|
102
89
|
/// The hidden variant that converts `actual` into the union enum `expected`.
|
|
103
90
|
pub fn wrapVariantFor(expected: &PlumType, actual: &PlumType, ctx: &CheckCtx) -> Option<String> {
|
|
104
|
-
if
|
|
91
|
+
if withCheck(|s| { let c = &s.in_wrap; c.get() }) || withCheck(|s| { let d = &s.depth; d.get() }) > 0 {
|
|
105
92
|
return None;
|
|
106
93
|
}
|
|
107
94
|
if matches!(
|
|
@@ -118,9 +105,9 @@ pub fn wrapVariantFor(expected: &PlumType, actual: &PlumType, ctx: &CheckCtx) ->
|
|
|
118
105
|
if ms.is_empty() {
|
|
119
106
|
return None;
|
|
120
107
|
}
|
|
121
|
-
|
|
108
|
+
withCheck(|s| { let c = &s.in_wrap; c.set(true) });
|
|
122
109
|
let found = ms.into_iter().find(|(_, t)| unify(t, actual, ctx).is_ok());
|
|
123
|
-
|
|
110
|
+
withCheck(|s| { let c = &s.in_wrap; c.set(false) });
|
|
124
111
|
found.map(|(v, _)| v)
|
|
125
112
|
}
|
|
126
113
|
|
|
@@ -258,11 +245,6 @@ pub fn joinBranches(
|
|
|
258
245
|
use crate::types::{TypeEnv, TypeScheme};
|
|
259
246
|
use crate::{checkPattern, EnumVariantInfo};
|
|
260
247
|
|
|
261
|
-
thread_local! {
|
|
262
|
-
/// Address of a top-level pattern that narrows (it records a narrowing, not a pattern).
|
|
263
|
-
static NARROW_TOP: Cell<usize> = const { Cell::new(0) };
|
|
264
|
-
}
|
|
265
|
-
|
|
266
248
|
const COPY_KEY: &str = "$union-copy:";
|
|
267
249
|
|
|
268
250
|
/// The member base name of a hidden variant (`Shape__Circle` -> `Circle`).
|
|
@@ -296,7 +278,7 @@ pub fn checkMemberPattern(
|
|
|
296
278
|
match pat {
|
|
297
279
|
ast::CasePattern::Name(n) if n.chars().next().is_some_and(|c| c.is_uppercase()) => {
|
|
298
280
|
let (v, _) = memberNamed(subject, n, ctx)?;
|
|
299
|
-
if
|
|
281
|
+
if withCheck(|s| { let c = &s.narrow_top; c.get() }) != addr {
|
|
300
282
|
plan::recordPattern(addr, v);
|
|
301
283
|
}
|
|
302
284
|
Some(Ok(()))
|
|
@@ -361,7 +343,7 @@ pub fn narrowTarget(
|
|
|
361
343
|
};
|
|
362
344
|
let (variant, ty) = memberNamed(subject_ty, n, ctx)?;
|
|
363
345
|
let addr = pat as *const ast::CasePattern as usize;
|
|
364
|
-
|
|
346
|
+
withCheck(|s| { let c = &s.narrow_top; c.set(addr) });
|
|
365
347
|
Some(Narrow {
|
|
366
348
|
subject: name,
|
|
367
349
|
variant,
|
|
@@ -374,11 +356,11 @@ pub fn narrowTarget(
|
|
|
374
356
|
/// After a pattern check: records the narrowing and retypes the subject.
|
|
375
357
|
pub fn applyNarrow(n: Option<Narrow>, case: &ast::Case, env: &mut TypeEnv, ctx: &CheckCtx) {
|
|
376
358
|
let Some(n) = n else { return };
|
|
377
|
-
|
|
359
|
+
withCheck(|s| { let c = &s.narrow_top; {
|
|
378
360
|
if c.get() == n.pat {
|
|
379
361
|
c.set(0)
|
|
380
362
|
}
|
|
381
|
-
});
|
|
363
|
+
} });
|
|
382
364
|
plan::recordNarrow(
|
|
383
365
|
case as *const ast::Case as usize,
|
|
384
366
|
n.subject.clone(),
|
plum-cli/src/game.rs
CHANGED
|
@@ -227,9 +227,10 @@ pub fn cmdGame(
|
|
|
227
227
|
port: Option<u16>,
|
|
228
228
|
noOpen: bool,
|
|
229
229
|
lib_path: Option<PathBuf>,
|
|
230
|
+
fusion: plum_checker::FusionOptions,
|
|
230
231
|
) -> Result<()> {
|
|
231
232
|
let lib_path = lib_path.unwrap_or_else(repoRoot);
|
|
232
|
-
let wasm = crate::compileToWasm(&file, &lib_path)?;
|
|
233
|
+
let wasm = crate::compileToWasm(&file, &lib_path, fusion)?;
|
|
233
234
|
let assets = assetRoot(&file, assets);
|
|
234
235
|
if assets.exists() && !assets.is_dir() {
|
|
235
236
|
bail!("{} is not a directory", assets.display());
|
plum-cli/src/main.rs
CHANGED
|
@@ -137,12 +137,9 @@ fn main() {
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
/// The `--no-fusion` / `--explain-fusion` flags of
|
|
140
|
+
/// The `--no-fusion` / `--explain-fusion` flags of a command, as explicit fusion options.
|
|
141
|
-
/// and passed to every compile as explicit `CompileOptions`.
|
|
142
|
-
static FUSION: std::sync::OnceLock<plum_checker::FusionOptions> = std::sync::OnceLock::new();
|
|
143
|
-
|
|
144
|
-
fn
|
|
141
|
+
fn fusionFlags(explain: bool, no_fusion: bool) -> plum_checker::FusionOptions {
|
|
145
|
-
|
|
142
|
+
plum_checker::FusionOptions { enabled: !no_fusion, explain }
|
|
146
143
|
}
|
|
147
144
|
|
|
148
145
|
/// `compile` reports positioned type errors without a file name; name `file` in them
|
|
@@ -151,10 +148,6 @@ fn nameFileInErrors(msg: &str, file: &std::path::Path) -> String {
|
|
|
151
148
|
msg.replace("type error at line", &format!("type error in '{}' at line", file.display()))
|
|
152
149
|
}
|
|
153
150
|
|
|
154
|
-
fn compileOptions(tests: bool) -> plum_wasm_codegen::CompileOptions {
|
|
155
|
-
plum_wasm_codegen::CompileOptions { tests, fusion: FUSION.get().copied().unwrap_or_default() }
|
|
156
|
-
}
|
|
157
|
-
|
|
158
151
|
fn run() -> Result<()> {
|
|
159
152
|
let cli = Cli::parse();
|
|
160
153
|
match cli.command {
|
|
@@ -165,8 +158,7 @@ fn run() -> Result<()> {
|
|
|
165
158
|
lib_path,
|
|
166
159
|
} => cmdCompile(file, output, lib_path),
|
|
167
160
|
Command::Run { file, lib_path, explain_fusion, no_fusion } => {
|
|
168
|
-
|
|
161
|
+
cmdRun(file, lib_path, fusionFlags(explain_fusion, no_fusion))
|
|
169
|
-
cmdRun(file, lib_path)
|
|
170
162
|
}
|
|
171
163
|
Command::Build {
|
|
172
164
|
file,
|
|
@@ -175,12 +167,10 @@ fn run() -> Result<()> {
|
|
|
175
167
|
explain_fusion,
|
|
176
168
|
no_fusion,
|
|
177
169
|
} => {
|
|
178
|
-
|
|
170
|
+
cmdBuild(file, output, lib_path, fusionFlags(explain_fusion, no_fusion))
|
|
179
|
-
cmdBuild(file, output, lib_path)
|
|
180
171
|
}
|
|
181
172
|
Command::Test { paths, lib_path, explain_fusion, no_fusion } => {
|
|
182
|
-
|
|
173
|
+
cmdTest(paths, lib_path, fusionFlags(explain_fusion, no_fusion))
|
|
183
|
-
cmdTest(paths, lib_path)
|
|
184
174
|
}
|
|
185
175
|
Command::Game {
|
|
186
176
|
file,
|
|
@@ -192,8 +182,7 @@ fn run() -> Result<()> {
|
|
|
192
182
|
explain_fusion,
|
|
193
183
|
no_fusion,
|
|
194
184
|
} => {
|
|
195
|
-
setFusion(explain_fusion, no_fusion);
|
|
196
|
-
game::cmdGame(file, output, assets, port, no_open, lib_path)
|
|
185
|
+
game::cmdGame(file, output, assets, port, no_open, lib_path, fusionFlags(explain_fusion, no_fusion))
|
|
197
186
|
}
|
|
198
187
|
Command::Editor { editor, insiders } => editor::install(editor, insiders),
|
|
199
188
|
}
|
|
@@ -233,10 +222,14 @@ fn cmdFormat(file: Option<std::path::PathBuf>, check: bool, use_stdin: bool) ->
|
|
|
233
222
|
Ok(())
|
|
234
223
|
}
|
|
235
224
|
|
|
225
|
+
fn compileToWasm(
|
|
226
|
+
file: &std::path::Path,
|
|
227
|
+
lib_path: &std::path::Path,
|
|
228
|
+
fusion: plum_checker::FusionOptions,
|
|
236
|
-
|
|
229
|
+
) -> Result<Vec<u8>> {
|
|
237
230
|
let ast = plum_core::loadAndMerge(file, lib_path).map_err(|e| anyhow::anyhow!("{e}"))?;
|
|
238
231
|
|
|
239
|
-
plum_wasm_codegen::compile(&ast, &
|
|
232
|
+
plum_wasm_codegen::compile(&ast, &plum_wasm_codegen::CompileOptions { tests: false, fusion }).map(|o| o.wasm).map_err(|e| {
|
|
240
233
|
if e.starts_with("type error") {
|
|
241
234
|
eprintln!("{}", nameFileInErrors(&e, file));
|
|
242
235
|
process::exit(1);
|
|
@@ -248,6 +241,7 @@ fn compileToWasm(file: &std::path::Path, lib_path: &std::path::Path) -> Result<V
|
|
|
248
241
|
fn compileToTestWasm(
|
|
249
242
|
file: &std::path::Path,
|
|
250
243
|
lib_path: &std::path::Path,
|
|
244
|
+
fusion: plum_checker::FusionOptions,
|
|
251
245
|
) -> Result<(Vec<u8>, Vec<String>)> {
|
|
252
246
|
let ast = plum_core::loadAndMerge(file, lib_path).map_err(|e| anyhow::anyhow!("{e}"))?;
|
|
253
247
|
|
|
@@ -258,7 +252,7 @@ fn compileToTestWasm(
|
|
|
258
252
|
// continue to the rest of the directory instead of the whole run
|
|
259
253
|
// dying on the first file with a type error. Checked once, on the
|
|
260
254
|
// test-desugared source codegen then compiles.
|
|
261
|
-
let out = plum_wasm_codegen::compile(&ast, &
|
|
255
|
+
let out = plum_wasm_codegen::compile(&ast, &plum_wasm_codegen::CompileOptions { tests: true, fusion }).map_err(|e| {
|
|
262
256
|
if e.starts_with("type error") {
|
|
263
257
|
anyhow::anyhow!(nameFileInErrors(&e, file))
|
|
264
258
|
} else {
|
|
@@ -278,7 +272,7 @@ fn cmdCompile(
|
|
|
278
272
|
output: Option<std::path::PathBuf>,
|
|
279
273
|
lib_path: std::path::PathBuf,
|
|
280
274
|
) -> Result<()> {
|
|
281
|
-
let wasm_bytes = compileToWasm(&file, &lib_path)?;
|
|
275
|
+
let wasm_bytes = compileToWasm(&file, &lib_path, plum_checker::FusionOptions::default())?;
|
|
282
276
|
|
|
283
277
|
let out_path = output.unwrap_or_else(|| file.with_extension("wasm"));
|
|
284
278
|
fs::write(&out_path, &wasm_bytes)
|
|
@@ -288,8 +282,12 @@ fn cmdCompile(
|
|
|
288
282
|
Ok(())
|
|
289
283
|
}
|
|
290
284
|
|
|
285
|
+
fn cmdRun(
|
|
286
|
+
file: std::path::PathBuf,
|
|
291
|
-
|
|
287
|
+
lib_path: std::path::PathBuf,
|
|
288
|
+
fusion: plum_checker::FusionOptions,
|
|
289
|
+
) -> Result<()> {
|
|
292
|
-
let wasm_bytes = compileToWasm(&file, &lib_path)?;
|
|
290
|
+
let wasm_bytes = compileToWasm(&file, &lib_path, fusion)?;
|
|
293
291
|
|
|
294
292
|
// GC + typed function references are required to load modules containing
|
|
295
293
|
// Plum's structs/enums/strings/closures (see docs/superpowers/plans/2026-07-25-wasm-gc-migration.md).
|
|
@@ -355,7 +353,11 @@ fn collectPlumFiles(path: &std::path::Path) -> Result<Vec<std::path::PathBuf>> {
|
|
|
355
353
|
Ok(files)
|
|
356
354
|
}
|
|
357
355
|
|
|
356
|
+
fn cmdTest(
|
|
358
|
-
|
|
357
|
+
paths: Vec<std::path::PathBuf>,
|
|
358
|
+
lib_path: std::path::PathBuf,
|
|
359
|
+
fusion: plum_checker::FusionOptions,
|
|
360
|
+
) -> Result<()> {
|
|
359
361
|
// A single plain file keeps the exact original single-file UX (no
|
|
360
362
|
// `=== path ===` header, no aggregate summary line, no "N/1 files
|
|
361
363
|
// passed" — just that one file's own test tree). Anything else (a
|
|
@@ -363,7 +365,7 @@ fn cmdTest(paths: Vec<std::path::PathBuf>, lib_path: std::path::PathBuf) -> Resu
|
|
|
363
365
|
// matching `scripts/test-plum.sh`'s old find-and-loop shell logic.
|
|
364
366
|
if let [only] = paths.as_slice() {
|
|
365
367
|
if only.is_file() {
|
|
366
|
-
let (_, failed) = runTestFile(only, &lib_path)?;
|
|
368
|
+
let (_, failed) = runTestFile(only, &lib_path, fusion)?;
|
|
367
369
|
if failed > 0 {
|
|
368
370
|
process::exit(1);
|
|
369
371
|
}
|
|
@@ -380,7 +382,7 @@ fn cmdTest(paths: Vec<std::path::PathBuf>, lib_path: std::path::PathBuf) -> Resu
|
|
|
380
382
|
let mut files_failed = 0usize;
|
|
381
383
|
for file in &files {
|
|
382
384
|
println!("=== {} ===", file.display());
|
|
383
|
-
match runTestFile(file, &lib_path) {
|
|
385
|
+
match runTestFile(file, &lib_path, fusion) {
|
|
384
386
|
Ok((passed, failed)) => {
|
|
385
387
|
total_passed += passed;
|
|
386
388
|
total_failed += failed;
|
|
@@ -433,8 +435,12 @@ fn cmdTest(paths: Vec<std::path::PathBuf>, lib_path: std::path::PathBuf) -> Resu
|
|
|
433
435
|
/// Runs every `test` block in one file, printing the Spock-flavored per-test
|
|
434
436
|
/// tree as it goes, and returns `(passed, failed)` counts — shared by both
|
|
435
437
|
/// the single-file and directory-recursive paths of `cmdTest`.
|
|
438
|
+
fn runTestFile(
|
|
439
|
+
file: &std::path::Path,
|
|
440
|
+
lib_path: &std::path::Path,
|
|
441
|
+
fusion: plum_checker::FusionOptions,
|
|
436
|
-
|
|
442
|
+
) -> Result<(usize, usize)> {
|
|
437
|
-
let (wasm_bytes, names) = compileToTestWasm(file, lib_path)?;
|
|
443
|
+
let (wasm_bytes, names) = compileToTestWasm(file, lib_path, fusion)?;
|
|
438
444
|
if let Ok(path) = std::env::var("PLUM_DUMP_WASM") {
|
|
439
445
|
let _ = std::fs::write(path, &wasm_bytes);
|
|
440
446
|
}
|
|
@@ -556,8 +562,9 @@ fn cmdBuild(
|
|
|
556
562
|
file: std::path::PathBuf,
|
|
557
563
|
output: Option<std::path::PathBuf>,
|
|
558
564
|
lib_path: std::path::PathBuf,
|
|
565
|
+
fusion: plum_checker::FusionOptions,
|
|
559
566
|
) -> Result<()> {
|
|
560
|
-
let wasm_bytes = compileToWasm(&file, &lib_path)?;
|
|
567
|
+
let wasm_bytes = compileToWasm(&file, &lib_path, fusion)?;
|
|
561
568
|
|
|
562
569
|
let stem = file
|
|
563
570
|
.file_stem()
|
plum-core/src/forms.rs
CHANGED
|
@@ -54,7 +54,6 @@
|
|
|
54
54
|
use std::cell::{Cell, RefCell};
|
|
55
55
|
use std::collections::{HashMap, HashSet};
|
|
56
56
|
use std::rc::Rc;
|
|
57
|
-
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
58
57
|
|
|
59
58
|
use crate::ast::*;
|
|
60
59
|
use crate::generics::isGenericName;
|
|
@@ -111,10 +110,12 @@ impl Registry {
|
|
|
111
110
|
}
|
|
112
111
|
}
|
|
113
112
|
|
|
113
|
+
thread_local! {
|
|
114
|
-
static NEXT_LOOP_ID:
|
|
114
|
+
static NEXT_LOOP_ID: Cell<usize> = const { Cell::new(1) };
|
|
115
|
+
}
|
|
115
116
|
|
|
116
117
|
fn nextLoopId() -> usize {
|
|
117
|
-
NEXT_LOOP_ID.
|
|
118
|
+
NEXT_LOOP_ID.with(|n| { let v = n.get(); n.set(v + 1); v })
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
thread_local! {
|
|
@@ -517,6 +518,17 @@ impl Prep<'_> {
|
|
|
517
518
|
|
|
518
519
|
// ---- typed lowering -----------------------------------------------------------
|
|
519
520
|
|
|
521
|
+
/// Resets the loop-id counter and every session table (see `session::begin`).
|
|
522
|
+
pub fn resetCounters() {
|
|
523
|
+
NEXT_LOOP_ID.with(|n| n.set(1));
|
|
524
|
+
SEQ_KINDS.with(|k| k.borrow_mut().clear());
|
|
525
|
+
REG.with(|r| *r.borrow_mut() = None);
|
|
526
|
+
COUNTER.with(|c| c.set(0));
|
|
527
|
+
LOWERED.with(|l| l.borrow_mut().clear());
|
|
528
|
+
ERRORS.with(|l| l.borrow_mut().clear());
|
|
529
|
+
USES.with(|l| l.borrow_mut().clear());
|
|
530
|
+
}
|
|
531
|
+
|
|
520
532
|
/// Starts a lowering session for `source` (see [`lowerLoop`], [`finish`]).
|
|
521
533
|
pub fn begin(source: &Source) {
|
|
522
534
|
let reg = Registry::of(&source.stars);
|
plum-core/src/lib.rs
CHANGED
|
@@ -18,6 +18,7 @@ pub mod parser;
|
|
|
18
18
|
pub mod purity;
|
|
19
19
|
pub mod rename;
|
|
20
20
|
pub mod reuse;
|
|
21
|
+
pub mod session;
|
|
21
22
|
pub mod simplify;
|
|
22
23
|
pub mod tail;
|
|
23
24
|
pub mod tests_lower;
|
plum-core/src/loader.rs
CHANGED
|
@@ -517,6 +517,12 @@ thread_local! {
|
|
|
517
517
|
std::cell::RefCell::new(Vec::new());
|
|
518
518
|
}
|
|
519
519
|
|
|
520
|
+
/// Clears the per-compile loader state (see `session::begin`).
|
|
521
|
+
pub fn resetSession() {
|
|
522
|
+
MERGED_LIT_ERRORS.with(|e| e.borrow_mut().clear());
|
|
523
|
+
USER_PRIMS.with(|u| u.borrow_mut().clear());
|
|
524
|
+
}
|
|
525
|
+
|
|
520
526
|
fn mergeItems(
|
|
521
527
|
file: &Path,
|
|
522
528
|
source: &Source,
|
plum-core/src/lower.rs
CHANGED
|
@@ -19,6 +19,7 @@ pub fn perFile(source: &mut Source) -> Result<(), String> {
|
|
|
19
19
|
/// type checks, union members, then forms. The first failing stage returns its
|
|
20
20
|
/// errors joined with "\n".
|
|
21
21
|
pub fn frontEnd(source: &mut Source) -> Result<(), String> {
|
|
22
|
+
crate::session::begin();
|
|
22
23
|
let traitErrors = crate::traits::lowerTraits(source);
|
|
23
24
|
if !traitErrors.is_empty() {
|
|
24
25
|
return Err(traitErrors.join("\n"));
|
plum-core/src/session.rs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//! Per-compile counters: everything process-global the front end numbers or
|
|
2
|
+
//! accumulates, reset in one place at the start of a compile.
|
|
3
|
+
|
|
4
|
+
/// Resets the forms, simplify, unions and loader counters/registries so two
|
|
5
|
+
/// compiles in one process produce identical generated names. Called only at
|
|
6
|
+
/// the start of `lower::frontEnd`; a forms session already running (the checker
|
|
7
|
+
/// is mid-check) is left alone.
|
|
8
|
+
pub fn begin() {
|
|
9
|
+
if !crate::forms::active() {
|
|
10
|
+
crate::forms::resetCounters();
|
|
11
|
+
}
|
|
12
|
+
crate::simplify::resetFresh();
|
|
13
|
+
crate::unions::beginPlan();
|
|
14
|
+
crate::loader::resetSession();
|
|
15
|
+
}
|
plum-core/src/simplify.rs
CHANGED
|
@@ -8,14 +8,19 @@ use crate::ast::*;
|
|
|
8
8
|
use crate::rename::{renameStmts, Name};
|
|
9
9
|
use crate::visit::{argRef, isTag, walkBlockRef, walkRef, childExprs, childRefs, stmtBlocks, stmtExprs, visitBlock};
|
|
10
10
|
use std::collections::{HashMap, HashSet};
|
|
11
|
-
|
|
11
|
+
thread_local! {
|
|
12
|
-
|
|
13
|
-
static FRESH:
|
|
12
|
+
static FRESH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
|
13
|
+
}
|
|
14
14
|
const MAX_SIZE: usize = 30;
|
|
15
15
|
const MAX_DEPTH: usize = 8;
|
|
16
16
|
|
|
17
|
+
/// Restarts the fresh-name counter (see `session::begin`).
|
|
18
|
+
pub fn resetFresh() {
|
|
19
|
+
FRESH.with(|f| f.set(0));
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
fn fresh() -> usize {
|
|
18
|
-
FRESH.
|
|
23
|
+
FRESH.with(|f| { let v = f.get(); f.set(v + 1); v })
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
pub fn simplifyExpr(e: Expr, src: &Source, pure: &HashSet<String>) -> Expr {
|
plum-core/tests/session_test.rs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
use plum_core::lower::frontEnd;
|
|
3
|
+
|
|
4
|
+
const SRC: &str = "\
|
|
5
|
+
enum Shape =
|
|
6
|
+
| Circle(r: Int)
|
|
7
|
+
| Square(s: Int)
|
|
8
|
+
|
|
9
|
+
fun main() =
|
|
10
|
+
for x := range 3
|
|
11
|
+
x
|
|
12
|
+
";
|
|
13
|
+
|
|
14
|
+
fn lowered() -> String {
|
|
15
|
+
let mut s = plum_test_support::parseRawSource(SRC);
|
|
16
|
+
frontEnd(&mut s).unwrap();
|
|
17
|
+
format!("{:?}", s.items)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[test]
|
|
21
|
+
fn frontEndTwiceInOneProcessIsIdentical() {
|
|
22
|
+
let a = lowered();
|
|
23
|
+
let b = lowered();
|
|
24
|
+
assert_eq!(a, b);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#[test]
|
|
28
|
+
fn beginResetsFreshNames() {
|
|
29
|
+
plum_core::session::begin();
|
|
30
|
+
plum_core::unions::recordNarrow(1, "x".into(), "V".into());
|
|
31
|
+
let a = plum_core::unions::takePlan().narrowings[&1].2.clone();
|
|
32
|
+
plum_core::session::begin();
|
|
33
|
+
plum_core::unions::recordNarrow(1, "x".into(), "V".into());
|
|
34
|
+
let b = plum_core::unions::takePlan().narrowings[&1].2.clone();
|
|
35
|
+
assert_eq!(a, b);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#[test]
|
|
39
|
+
fn otherThreadsBeginDoesNotDisturbThisCompile() {
|
|
40
|
+
let expected = lowered();
|
|
41
|
+
let h = std::thread::spawn(|| {
|
|
42
|
+
for _ in 0..200 {
|
|
43
|
+
plum_core::session::begin();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
for _ in 0..50 {
|
|
47
|
+
assert_eq!(lowered(), expected);
|
|
48
|
+
}
|
|
49
|
+
h.join().unwrap();
|
|
50
|
+
}
|
plum-core/tests/trmc_test.rs
CHANGED
|
@@ -2,24 +2,25 @@
|
|
|
2
2
|
//! Tail recursion modulo cons (`plum_core::trmc`): which fns are rewritten, and
|
|
3
3
|
//! that the rewrite computes what the recursion would (compiled and run through
|
|
4
4
|
//! the real pipeline, which is where `trmc::rewrite` is wired in).
|
|
5
|
-
use plum_checker::
|
|
5
|
+
use plum_checker::FusionOptions;
|
|
6
|
+
use plum_wasm_codegen::CompileOptions;
|
|
6
7
|
|
|
7
|
-
/// The fusion option is thread-local: each test compiles on its own thread.
|
|
8
|
-
fn
|
|
8
|
+
fn compileWith(source: &plum_core::ast::Source, fusion: bool) -> Result<Vec<u8>, String> {
|
|
9
|
-
|
|
9
|
+
let opts = CompileOptions { fusion: FusionOptions { enabled: fusion, explain: false }, ..Default::default() };
|
|
10
|
+
plum_wasm_codegen::compile(source, &opts).map(|o| o.wasm)
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
fn compileSource(source: &plum_core::ast::Source) -> Result<Vec<u8>, String> {
|
|
13
|
-
|
|
14
|
+
compileWith(source, true)
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
/// Compiles without the fusion stage, for tests that inspect an unfused fn by name.
|
|
17
18
|
fn compileUnfused(source: &plum_core::ast::Source) -> Result<Vec<u8>, String> {
|
|
18
|
-
|
|
19
|
+
compileWith(source, false)
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
fn typedSource(source: &plum_core::ast::Source) -> plum_checker::types::CheckResult<plum_checker::TypedInfo> {
|
|
22
|
-
|
|
23
|
+
plum_checker::typedSourceWith(source, FusionOptions::default())
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
fn gcEngine() -> wasmtime::Engine {
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -551,25 +551,49 @@ fn fnKey(f: &ast::Fn) -> String {
|
|
|
551
551
|
}
|
|
552
552
|
}
|
|
553
553
|
|
|
554
|
-
thread_local! {
|
|
555
|
-
|
|
554
|
+
/// Everything one `compile` call accumulates and later reads from anywhere in codegen
|
|
555
|
+
/// (the wasm-gc type registry, top-level consts, value-enum facts, checker error
|
|
556
|
-
|
|
556
|
+
/// info). One thread-local, reset at the start of `compile`; not reentrant, so
|
|
557
|
+
/// `withGen` closures must not call back into `withGen`.
|
|
558
|
+
#[derive(Default)]
|
|
559
|
+
struct GenState {
|
|
557
|
-
///
|
|
560
|
+
/// The wasm-gc type registry; set once `buildGcTypeRegistry` has run.
|
|
558
|
-
/// every one of `plumTypeToValtype`/`astTypeToWasm`'s ~25 call sites (many several
|
|
559
|
-
|
|
561
|
+
gc_types: Option<std::rc::Rc<GcTypeRegistry>>,
|
|
560
|
-
///
|
|
562
|
+
/// Top-level `NAME = literal` const values. A bare `NAME` always lexes as a
|
|
563
|
+
/// type_identifier, so `Expr::TypeName` consults this before enum-variant handling.
|
|
564
|
+
consts: HashMap<String, ast::Expr>,
|
|
565
|
+
/// `value enum` name -> its fields `(name, type)` in declaration order. A value enum
|
|
566
|
+
/// is flattened into one wasm local / param / result per leaf field and only boxed
|
|
567
|
+
/// (into its variant's GC struct) when stored in a heap object.
|
|
568
|
+
value_enums: HashMap<String, Vec<(String, PlumType)>>,
|
|
569
|
+
/// Box struct type index of a value enum -> the multi-value block type `[] -> leaves`
|
|
570
|
+
/// used when an `if`/`match`/inline if produces that value enum.
|
|
571
|
+
value_block_types: HashMap<u32, u32>,
|
|
572
|
+
/// Helper function indices: `unbox:Vec2`, `eq:Vec2`, `proj:Vec2.x`.
|
|
573
|
+
value_helpers: HashMap<String, u32>,
|
|
561
|
-
///
|
|
574
|
+
/// Error-union facts from the checker for the program being compiled.
|
|
575
|
+
err_info: ErrInfo,
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
impl GenState {
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/// The registry, shared out so callers can read it while calling other `withGen` users
|
|
562
|
-
|
|
582
|
+
/// (e.g. `flatValtypes` -> `valueFieldsOf`) without a nested borrow.
|
|
583
|
+
fn gcTypes() -> std::rc::Rc<GcTypeRegistry> {
|
|
584
|
+
withGen(|s| {
|
|
585
|
+
s.gc_types.clone().expect(
|
|
586
|
+
"internal codegen error: GC type registry read before compileSource initialized it",
|
|
587
|
+
)
|
|
588
|
+
})
|
|
563
589
|
}
|
|
564
590
|
|
|
565
591
|
thread_local! {
|
|
566
|
-
/// Top-level `NAME = literal` const values, keyed by name — set once near the
|
|
567
|
-
/// top of `compileSource`, alongside `CURRENT_GC_TYPES` (same non-reentrancy
|
|
568
|
-
/// rationale). A bare `NAME` reference always lexes as a type_identifier (any
|
|
569
|
-
/// uppercase-leading name does, there's no separate "constant" token — see
|
|
570
|
-
/// `plum-checker`'s `inferExpr`'s matching `TypeName` comment), so `Expr::TypeName`
|
|
571
|
-
/// consults this map before falling back to enum-variant/unmodeled-type handling.
|
|
572
|
-
static
|
|
592
|
+
static GEN: RefCell<GenState> = RefCell::new(GenState::default());
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
fn withGen<R>(f: impl FnOnce(&mut GenState) -> R) -> R {
|
|
596
|
+
GEN.with(|g| f(&mut g.borrow_mut()))
|
|
573
597
|
}
|
|
574
598
|
|
|
575
599
|
/// A variant's payload field types as instantiated by `subject_ty`'s type
|
|
@@ -700,23 +724,13 @@ fn declaredType(value: &ast::Expr, types: &HashMap<usize, PlumType>) -> Option<P
|
|
|
700
724
|
.cloned()
|
|
701
725
|
}
|
|
702
726
|
|
|
703
|
-
fn withGcTypes<R>(f: impl FnOnce(&GcTypeRegistry) -> R) -> R {
|
|
704
|
-
CURRENT_GC_TYPES.with(|c| {
|
|
705
|
-
let borrow = c.borrow();
|
|
706
|
-
let registry = borrow.as_ref().expect(
|
|
707
|
-
"internal codegen error: GC type registry read before compileSource initialized it",
|
|
708
|
-
);
|
|
709
|
-
f(registry)
|
|
710
|
-
})
|
|
711
|
-
}
|
|
712
|
-
|
|
713
727
|
/// Wasm type of a pattern/`?`/re-layout scratch local: a nullable `T?` is held as
|
|
714
728
|
/// its payload reference, anything else as the GC struct `scratchTypeIdx` names.
|
|
715
729
|
fn scratchValtype(ty: &PlumType, vname: &str) -> ValType {
|
|
716
730
|
if vname.is_empty() && nullableErr(ty).is_some() {
|
|
717
731
|
return plumTypeToValtype(ty);
|
|
718
732
|
}
|
|
719
|
-
gcRef(
|
|
733
|
+
gcRef({ let r = gcTypes(); scratchTypeIdx(&r, ty, vname) })
|
|
720
734
|
}
|
|
721
735
|
|
|
722
736
|
/// GC type index of a pattern/`?` scratch local: variant `vname` of enum type `ty`,
|
|
@@ -761,21 +775,6 @@ fn pushSelfPlaceholder(vt: ValType, body: &mut Vec<u8>) {
|
|
|
761
775
|
}
|
|
762
776
|
}
|
|
763
777
|
|
|
764
|
-
thread_local! {
|
|
765
|
-
/// `value enum` name -> its fields `(name, type)` in declaration order. Set once near
|
|
766
|
-
/// the top of `compileSourceInner`. A value enum is flattened into one wasm local /
|
|
767
|
-
/// param / result per leaf field and only boxed (into its variant's GC struct, whose
|
|
768
|
-
/// fields are the same flat leaves) when stored in a heap object.
|
|
769
|
-
static VALUE_ENUMS: RefCell<HashMap<String, Vec<(String, PlumType)>>> = RefCell::new(HashMap::new());
|
|
770
|
-
/// Box struct type index of a value enum -> the multi-value block type `[] -> leaves`
|
|
771
|
-
/// used when an `if`/`match`/inline if produces that value enum.
|
|
772
|
-
static VALUE_BLOCK_TYPES: RefCell<HashMap<u32, u32>> = RefCell::new(HashMap::new());
|
|
773
|
-
/// Helper function indices: `unbox:Vec2`, `eq:Vec2`, `proj:Vec2.x`.
|
|
774
|
-
static VALUE_HELPERS: RefCell<HashMap<String, u32>> = RefCell::new(HashMap::new());
|
|
775
|
-
/// Error-union facts from the checker for the program being compiled.
|
|
776
|
-
static ERR_INFO: RefCell<ErrInfo> = RefCell::new(ErrInfo::default());
|
|
777
|
-
}
|
|
778
|
-
|
|
779
778
|
#[derive(Default)]
|
|
780
779
|
struct ErrInfo {
|
|
781
780
|
/// File of the fn being compiled (`expr!` crash messages name it).
|
|
@@ -787,13 +786,13 @@ struct ErrInfo {
|
|
|
787
786
|
/// Records `file` as the source file of the fn about to be compiled (when known).
|
|
788
787
|
fn setCurrentFile(file: &str) {
|
|
789
788
|
if !file.is_empty() {
|
|
790
|
-
|
|
789
|
+
withGen(|s| s.err_info.file = file.to_string());
|
|
791
790
|
}
|
|
792
791
|
}
|
|
793
792
|
|
|
794
793
|
fn errSite(expr: &ast::Expr) -> Option<plum_checker::ErrSite> {
|
|
795
|
-
|
|
794
|
+
withGen(|s| {
|
|
796
|
-
|
|
795
|
+
s.err_info
|
|
797
796
|
.sites
|
|
798
797
|
.get(&(expr as *const ast::Expr as usize))
|
|
799
798
|
.cloned()
|
|
@@ -801,8 +800,7 @@ fn errSite(expr: &ast::Expr) -> Option<plum_checker::ErrSite> {
|
|
|
801
800
|
}
|
|
802
801
|
|
|
803
802
|
fn errCode(name: &str) -> Result<i32, String> {
|
|
804
|
-
ERR_INFO
|
|
805
|
-
|
|
803
|
+
withGen(|s| s.err_info.codes.get(name).copied())
|
|
806
804
|
.ok_or_else(|| format!("internal codegen error: no error code for '{}'", name))
|
|
807
805
|
}
|
|
808
806
|
|
|
@@ -817,14 +815,14 @@ fn emitZeroLeaf(vt: ValType, body: &mut Vec<u8>) {
|
|
|
817
815
|
}
|
|
818
816
|
|
|
819
817
|
fn emitZeros(ty: &PlumType, body: &mut Vec<u8>) {
|
|
820
|
-
for vt in
|
|
818
|
+
for vt in flatValtypes(ty, &gcTypes()) {
|
|
821
819
|
emitZeroLeaf(vt, body);
|
|
822
820
|
}
|
|
823
821
|
}
|
|
824
822
|
|
|
825
823
|
fn valueFieldsOf(ty: &PlumType) -> Option<Vec<(String, PlumType)>> {
|
|
826
824
|
match ty {
|
|
827
|
-
PlumType::TNamed(n) =>
|
|
825
|
+
PlumType::TNamed(n) => withGen(|s| s.value_enums.get(n).cloned()),
|
|
828
826
|
// `!T` is `(T leaves..., code: i32)`; a Unit success has only the code.
|
|
829
827
|
PlumType::TErr(t, _) if nullableErr(ty).is_none() => {
|
|
830
828
|
let mut f = Vec::new();
|
|
@@ -909,14 +907,14 @@ fn emitDrops(ty: &PlumType, body: &mut Vec<u8>) {
|
|
|
909
907
|
|
|
910
908
|
/// Flat leaves on the stack -> the value's heap box (its variant struct).
|
|
911
909
|
fn emitBox(ty: &PlumType, body: &mut Vec<u8>) {
|
|
912
|
-
let idx =
|
|
910
|
+
let idx = { let r = gcTypes(); r.classIdx(ty) }
|
|
913
911
|
.expect("internal codegen error: value enum box type missing");
|
|
914
912
|
Instruction::StructNew(idx).encode(body);
|
|
915
913
|
}
|
|
916
914
|
|
|
917
915
|
fn valueHelper(key: &str) -> u32 {
|
|
918
|
-
|
|
916
|
+
withGen(|s| {
|
|
919
|
-
*
|
|
917
|
+
*s.value_helpers
|
|
920
918
|
.get(key)
|
|
921
919
|
.unwrap_or_else(|| panic!("internal codegen error: missing helper {key}"))
|
|
922
920
|
})
|
|
@@ -973,7 +971,7 @@ fn strType() -> PlumType {
|
|
|
973
971
|
}
|
|
974
972
|
|
|
975
973
|
fn plumTypeToValtype(t: &PlumType) -> ValType {
|
|
976
|
-
|
|
974
|
+
plumTypeToGcValtype(t, &gcTypes())
|
|
977
975
|
}
|
|
978
976
|
|
|
979
977
|
/// Type-section indices for every wasm-gc composite type this program's
|
|
@@ -1638,13 +1636,13 @@ fn buildGcTypeRegistry(
|
|
|
1638
1636
|
|
|
1639
1637
|
fn blockTypeFor(result_vt: Option<ValType>) -> BlockType {
|
|
1640
1638
|
// A value enum's block result is its flat leaves; its box reference type stands in
|
|
1641
|
-
// for it as the `result_vt` marker (see `
|
|
1639
|
+
// for it as the `result_vt` marker (see `GenState::value_block_types`).
|
|
1642
1640
|
if let Some(ValType::Ref(RefType {
|
|
1643
1641
|
heap_type: HeapType::Concrete(i),
|
|
1644
1642
|
..
|
|
1645
1643
|
})) = result_vt
|
|
1646
1644
|
{
|
|
1647
|
-
if let Some(t) =
|
|
1645
|
+
if let Some(t) = withGen(|s| s.value_block_types.get(&i).copied()) {
|
|
1648
1646
|
return BlockType::FunctionType(t);
|
|
1649
1647
|
}
|
|
1650
1648
|
}
|
|
@@ -1728,7 +1726,7 @@ pub fn typeCheck(source: &ast::Source) -> Result<plum_checker::TypedInfo, String
|
|
|
1728
1726
|
/// Like `typeCheck`, naming `file` in positioned errors. Positions refer to the
|
|
1729
1727
|
/// merged source, so imported-file errors are attributed to the entry file.
|
|
1730
1728
|
pub fn typeCheckIn(source: &ast::Source, file: &str) -> Result<plum_checker::TypedInfo, String> {
|
|
1731
|
-
typeCheckWith(source, file, plum_checker::
|
|
1729
|
+
typeCheckWith(source, file, plum_checker::FusionOptions::default())
|
|
1732
1730
|
}
|
|
1733
1731
|
|
|
1734
1732
|
fn typeCheckWith(
|
|
@@ -1799,12 +1797,8 @@ pub fn compile(source: &ast::Source, opts: &CompileOptions) -> Result<Output, St
|
|
|
1799
1797
|
}
|
|
1800
1798
|
}
|
|
1801
1799
|
|
|
1802
|
-
fn threadLocalOptions(tests: bool) -> CompileOptions {
|
|
1803
|
-
CompileOptions { tests, fusion: plum_checker::fusionOptions() }
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
1800
|
pub fn compileSource(source: &ast::Source) -> Result<Vec<u8>, String> {
|
|
1807
|
-
compile(source, &
|
|
1801
|
+
compile(source, &CompileOptions::default()).map(|o| o.wasm)
|
|
1808
1802
|
}
|
|
1809
1803
|
|
|
1810
1804
|
pub use plum_core::tests_lower::{TestProgram, ASSERT_REPORT_MARK};
|
|
@@ -1830,6 +1824,14 @@ mod compileEntryTests {
|
|
|
1830
1824
|
assert_eq!(out.wasm, compileSource(&src).expect("compiles"));
|
|
1831
1825
|
}
|
|
1832
1826
|
|
|
1827
|
+
#[test]
|
|
1828
|
+
fn compilingTwiceInOneProcessGivesIdenticalBytes() {
|
|
1829
|
+
let src = load();
|
|
1830
|
+
let first = compile(&src, &CompileOptions::default()).expect("compiles").wasm;
|
|
1831
|
+
let second = compile(&src, &CompileOptions::default()).expect("compiles").wasm;
|
|
1832
|
+
assert_eq!(first, second);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1833
1835
|
#[test]
|
|
1834
1836
|
fn testsOptionNamesTests() {
|
|
1835
1837
|
let src = load();
|
|
@@ -2162,11 +2164,15 @@ fn compileSourceInner(
|
|
|
2162
2164
|
error_codes,
|
|
2163
2165
|
..
|
|
2164
2166
|
} = typed;
|
|
2167
|
+
// A fresh state for this compile: nothing carries over from a previous one.
|
|
2165
|
-
|
|
2168
|
+
withGen(|s| {
|
|
2169
|
+
*s = GenState {
|
|
2166
|
-
|
|
2170
|
+
err_info: ErrInfo {
|
|
2167
|
-
|
|
2171
|
+
file: source.file.clone(),
|
|
2168
|
-
|
|
2172
|
+
sites: err_sites,
|
|
2169
|
-
|
|
2173
|
+
codes: error_codes,
|
|
2174
|
+
},
|
|
2175
|
+
..GenState::default()
|
|
2170
2176
|
}
|
|
2171
2177
|
});
|
|
2172
2178
|
// The types are keyed by the addresses of the checker's lowered program.
|
|
@@ -2281,9 +2287,8 @@ fn compileSourceInner(
|
|
|
2281
2287
|
}
|
|
2282
2288
|
|
|
2283
2289
|
// ---- collect the concrete generic types the program uses ----
|
|
2284
|
-
|
|
2290
|
+
withGen(|s| {
|
|
2285
|
-
let
|
|
2291
|
+
let v = &mut s.value_enums;
|
|
2286
|
-
v.clear();
|
|
2287
2292
|
for item in &source.items {
|
|
2288
2293
|
if let ast::Item::Enum(e) = item {
|
|
2289
2294
|
if e.isValue && e.variants.len() == 1 && e.generics.is_empty() {
|
|
@@ -2357,9 +2362,7 @@ fn compileSourceInner(
|
|
|
2357
2362
|
|
|
2358
2363
|
let mut module = WasmModule::new();
|
|
2359
2364
|
|
|
2360
|
-
|
|
2365
|
+
|
|
2361
|
-
VALUE_HELPERS.with(|m| m.borrow_mut().clear());
|
|
2362
|
-
|
|
2363
2366
|
let mut gc_types = buildGcTypeRegistry(
|
|
2364
2367
|
&mut module,
|
|
2365
2368
|
source,
|
|
@@ -2368,9 +2371,9 @@ fn compileSourceInner(
|
|
|
2368
2371
|
&tc.apps,
|
|
2369
2372
|
&tc.errs,
|
|
2370
2373
|
);
|
|
2371
|
-
|
|
2374
|
+
withGen(|s| s.gc_types = Some(std::rc::Rc::new(gc_types.clone())));
|
|
2372
2375
|
let value_names: Vec<String> = {
|
|
2373
|
-
let mut v: Vec<String> =
|
|
2376
|
+
let mut v: Vec<String> = withGen(|s| s.value_enums.keys().cloned().collect());
|
|
2374
2377
|
v.sort();
|
|
2375
2378
|
v
|
|
2376
2379
|
};
|
|
@@ -2381,7 +2384,7 @@ fn compileSourceInner(
|
|
|
2381
2384
|
.expect("value enum box type registered");
|
|
2382
2385
|
let leaves = flatValtypes(&ty, &gc_types);
|
|
2383
2386
|
let bt = module.addType(&[], &leaves);
|
|
2384
|
-
|
|
2387
|
+
withGen(|s| s.value_block_types.insert(box_idx, bt));
|
|
2385
2388
|
}
|
|
2386
2389
|
let mut err_boxes: Vec<(PlumType, u32)> = gc_types
|
|
2387
2390
|
.err_box
|
|
@@ -2396,11 +2399,11 @@ fn compileSourceInner(
|
|
|
2396
2399
|
);
|
|
2397
2400
|
let leaves = flatValtypes(&ty, &gc_types);
|
|
2398
2401
|
let bt = module.addType(&[], &leaves);
|
|
2399
|
-
|
|
2402
|
+
withGen(|s| s.value_block_types.insert(*box_idx, bt));
|
|
2400
2403
|
}
|
|
2401
2404
|
|
|
2402
|
-
|
|
2405
|
+
withGen(|s| {
|
|
2403
|
-
let
|
|
2406
|
+
let consts = &mut s.consts;
|
|
2404
2407
|
for item in &source.items {
|
|
2405
2408
|
if let ast::Item::Const(cst) = item {
|
|
2406
2409
|
consts.insert(cst.name.clone(), cst.value.clone());
|
|
@@ -2435,7 +2438,7 @@ fn compileSourceInner(
|
|
|
2435
2438
|
for (vt, idx) in variadic_elem_vts.iter().zip(indices) {
|
|
2436
2439
|
gc_types.variadic_array_type_idx.insert(*vt, idx);
|
|
2437
2440
|
}
|
|
2438
|
-
|
|
2441
|
+
withGen(|s| s.gc_types = Some(std::rc::Rc::new(gc_types.clone())));
|
|
2439
2442
|
}
|
|
2440
2443
|
|
|
2441
2444
|
// Register every `extern fun` as a genuine wasm import BEFORE any function.
|
|
@@ -2572,7 +2575,7 @@ fn compileSourceInner(
|
|
|
2572
2575
|
let t = module.addType(&[gcRef(box_idx)], &leaves);
|
|
2573
2576
|
let f = module.addFunction(t, &b);
|
|
2574
2577
|
module.nameFunction(f, &format!("unbox.{name}"));
|
|
2575
|
-
|
|
2578
|
+
withGen(|s| s.value_helpers.insert(format!("unbox:{name}"), f));
|
|
2576
2579
|
// eq
|
|
2577
2580
|
let mut b = vec![0u8];
|
|
2578
2581
|
let n = leaves.len() as u32;
|
|
@@ -2596,7 +2599,7 @@ fn compileSourceInner(
|
|
|
2596
2599
|
let t = module.addType(&both, &[ValType::I32]);
|
|
2597
2600
|
let f = module.addFunction(t, &b);
|
|
2598
2601
|
module.nameFunction(f, &format!("eq.{name}"));
|
|
2599
|
-
|
|
2602
|
+
withGen(|s| s.value_helpers.insert(format!("eq:{name}"), f));
|
|
2600
2603
|
// proj
|
|
2601
2604
|
for (fname, fty) in valueFieldsOf(&ty).unwrap_or_default() {
|
|
2602
2605
|
let (off, _) = valueFieldRange(&ty, &fname).unwrap();
|
|
@@ -2609,7 +2612,7 @@ fn compileSourceInner(
|
|
|
2609
2612
|
let t = module.addType(&leaves, &fleaves);
|
|
2610
2613
|
let f = module.addFunction(t, &b);
|
|
2611
2614
|
module.nameFunction(f, &format!("proj.{name}.{fname}"));
|
|
2612
|
-
|
|
2615
|
+
withGen(|s| s.value_helpers.insert(format!("proj:{name}.{fname}"), f));
|
|
2613
2616
|
}
|
|
2614
2617
|
}
|
|
2615
2618
|
|
|
@@ -2632,7 +2635,7 @@ fn compileSourceInner(
|
|
|
2632
2635
|
let ft = module.addType(&[gcRef(*box_idx)], &leaves);
|
|
2633
2636
|
let f = module.addFunction(ft, &b);
|
|
2634
2637
|
module.nameFunction(f, &format!("unbox.!{t}"));
|
|
2635
|
-
|
|
2638
|
+
withGen(|s| s.value_helpers.insert(unboxKey(&ty), f));
|
|
2636
2639
|
}
|
|
2637
2640
|
|
|
2638
2641
|
// Pre-allocate one instance of every payload-free variant of every enum instance.
|
|
@@ -2678,7 +2681,7 @@ fn compileSourceInner(
|
|
|
2678
2681
|
let t = module.addType(&[gcRef(gc_types.byte_array_type_idx)], &[gcRef(str_idx)]);
|
|
2679
2682
|
gc_types.mk_str_fn = Some(module.addFunction(t, &b));
|
|
2680
2683
|
}
|
|
2681
|
-
|
|
2684
|
+
withGen(|s| s.gc_types = Some(std::rc::Rc::new(gc_types.clone())));
|
|
2682
2685
|
|
|
2683
2686
|
// Closure ABI: one `call_indirect` type per Plum function signature,
|
|
2684
2687
|
// `(env: anyref, params...) -> result`, with every value in its own representation.
|
|
@@ -2800,7 +2803,7 @@ fn compileSourceInner(
|
|
|
2800
2803
|
}
|
|
2801
2804
|
let closure_type_indices = module.addGcTypes(closure_subtypes);
|
|
2802
2805
|
gc_types.closure_type_idx = closure_type_indices[0];
|
|
2803
|
-
|
|
2806
|
+
withGen(|s| s.gc_types = Some(std::rc::Rc::new(gc_types.clone())));
|
|
2804
2807
|
|
|
2805
2808
|
let mut closures: HashMap<(usize, usize), ClosureInfo> = HashMap::new();
|
|
2806
2809
|
let mut closure_asts: HashMap<(usize, usize), &ast::Closure> = HashMap::new();
|
|
@@ -3256,7 +3259,8 @@ fn compileMemIntrinsicFnBody(f: &ast::Fn) -> Option<Vec<u8>> {
|
|
|
3256
3259
|
/// through this.
|
|
3257
3260
|
fn wrapBytesAsStr(body: &mut Vec<u8>) {
|
|
3258
3261
|
// No real `Str` registered at all — a raw `array<i8>` already IS this program's "Str".
|
|
3262
|
+
let mk_str_fn = gcTypes().mk_str_fn;
|
|
3259
|
-
let Some(mk) =
|
|
3263
|
+
let Some(mk) = mk_str_fn else {
|
|
3260
3264
|
return;
|
|
3261
3265
|
};
|
|
3262
3266
|
Instruction::Call(mk).encode(body);
|
|
@@ -3276,7 +3280,7 @@ fn compileArrayIntrinsicFnBody(f: &ast::Fn, subst: &Subst) -> Option<Vec<u8>> {
|
|
|
3276
3280
|
let elem = subst.get("T")?.clone();
|
|
3277
3281
|
let array_elem = elem.clone();
|
|
3278
3282
|
let array_ty = PlumType::TApp("Array".to_string(), vec![elem]);
|
|
3279
|
-
let array_type_idx =
|
|
3283
|
+
let array_type_idx = { let r = gcTypes(); r.superIdx(&array_ty) }?;
|
|
3280
3284
|
if isSoaElem(&array_elem) {
|
|
3281
3285
|
return compileSoaArrayIntrinsic(f.name.as_str(), &array_elem, array_type_idx);
|
|
3282
3286
|
}
|
|
@@ -3510,9 +3514,9 @@ fn compileArrayIntrinsicFnBody(f: &ast::Fn, subst: &Subst) -> Option<Vec<u8>> {
|
|
|
3510
3514
|
/// is one GC struct whose field `k` is the native array of leaf `k`. `get`/`set`
|
|
3511
3515
|
/// read/write each leaf array directly (no allocation, no box).
|
|
3512
3516
|
fn compileSoaArrayIntrinsic(name: &str, elem: &PlumType, soa_idx: u32) -> Option<Vec<u8>> {
|
|
3513
|
-
let leaves =
|
|
3517
|
+
let leaves = flatValtypes(elem, &gcTypes());
|
|
3514
3518
|
let leaf_arrs: Vec<u32> =
|
|
3515
|
-
|
|
3519
|
+
{ let r = gcTypes(); leaves.iter().map(|vt| r.leaf_array_idx[vt]).collect() };
|
|
3516
3520
|
let n = leaves.len() as u32;
|
|
3517
3521
|
let mut body = Vec::new();
|
|
3518
3522
|
// Locals: declared groups, one entry per local (all single).
|
|
@@ -4662,12 +4666,12 @@ fn compileFnBodyImpl(
|
|
|
4662
4666
|
idx += leafCount(ty);
|
|
4663
4667
|
}
|
|
4664
4668
|
for (name, ty) in &collector.named {
|
|
4665
|
-
let vts =
|
|
4669
|
+
let vts = flatValtypes(ty, &gcTypes());
|
|
4666
4670
|
// `s = T(..s, ...)` on a param reassigns the param's own slot (F-T6-3): a
|
|
4667
4671
|
// fresh local would shadow the param for the whole body, so every read of
|
|
4668
4672
|
// `s` (the right-hand side included) would see the unset local.
|
|
4669
4673
|
if let Some(pty) = decl_types.get(name).filter(|_| locals.contains_key(name)) {
|
|
4670
|
-
if
|
|
4674
|
+
if flatValtypes(pty, &gcTypes()) == vts {
|
|
4671
4675
|
continue;
|
|
4672
4676
|
}
|
|
4673
4677
|
}
|
|
@@ -6849,7 +6853,7 @@ fn compileErrBang(
|
|
|
6849
6853
|
.get(CRASH_FN_KEY)
|
|
6850
6854
|
.ok_or_else(|| "internal codegen error: crash import missing".to_string())?;
|
|
6851
6855
|
let callee = calleeText(&b.value);
|
|
6852
|
-
let file =
|
|
6856
|
+
let file = withGen(|s| s.err_info.file.clone());
|
|
6853
6857
|
let file = if file.is_empty() {
|
|
6854
6858
|
"main.plum".to_string()
|
|
6855
6859
|
} else {
|
|
@@ -7100,7 +7104,7 @@ fn compileTagName(
|
|
|
7100
7104
|
// An open set (a `!T` parameter's) can be any error of the program.
|
|
7101
7105
|
let all: std::collections::BTreeSet<String>;
|
|
7102
7106
|
let names = if names.is_empty() {
|
|
7103
|
-
all =
|
|
7107
|
+
all = withGen(|s| s.err_info.codes.keys().cloned().collect());
|
|
7104
7108
|
&all
|
|
7105
7109
|
} else {
|
|
7106
7110
|
names
|
|
@@ -7528,7 +7532,7 @@ fn compileExprCore(
|
|
|
7528
7532
|
}
|
|
7529
7533
|
None => {
|
|
7530
7534
|
let n = &typeParamConstName(n, ctx);
|
|
7531
|
-
let const_value =
|
|
7535
|
+
let const_value = withGen(|s| s.consts.get(n).cloned());
|
|
7532
7536
|
match const_value {
|
|
7533
7537
|
Some(_) if ctx.func_ids.contains_key(&format!("const#{n}")) => {
|
|
7534
7538
|
Instruction::Call(ctx.func_ids[&format!("const#{n}")]).encode(body);
|
|
@@ -8381,7 +8385,7 @@ fn compileStaticString(text: &str, body: &mut Vec<u8>, state: &mut ModuleState)
|
|
|
8381
8385
|
let bytes = text.as_bytes();
|
|
8382
8386
|
let data_index = state.passive_segments.len() as u32;
|
|
8383
8387
|
state.passive_segments.push(bytes.to_vec());
|
|
8384
|
-
let byte_array_type_idx =
|
|
8388
|
+
let byte_array_type_idx = { let r = gcTypes(); r.byte_array_type_idx };
|
|
8385
8389
|
Instruction::I32Const(0).encode(body);
|
|
8386
8390
|
Instruction::I32Const(bytes.len() as i32).encode(body);
|
|
8387
8391
|
Instruction::ArrayNewData {
|
|
@@ -8443,7 +8447,7 @@ fn compileInterpolatedString(
|
|
|
8443
8447
|
Instruction::Call(func_idx).encode(body);
|
|
8444
8448
|
}
|
|
8445
8449
|
PlumType::TBool => {
|
|
8446
|
-
let str_ref =
|
|
8450
|
+
let str_ref = plumTypeToGcValtype(&strType(), &gcTypes());
|
|
8447
8451
|
compileBoolConditionAsI32(expr, body, ctx, state)?;
|
|
8448
8452
|
Instruction::If(BlockType::Result(str_ref)).encode(body);
|
|
8449
8453
|
compileStaticString("true", body, state);
|
|
@@ -8704,7 +8708,7 @@ fn compileClosureBody(
|
|
|
8704
8708
|
}
|
|
8705
8709
|
for (name, ty) in &info.free_vars {
|
|
8706
8710
|
locals.insert(name.clone(), idx);
|
|
8707
|
-
let vts =
|
|
8711
|
+
let vts = flatValtypes(ty, &gcTypes());
|
|
8708
8712
|
idx += vts.len() as u32;
|
|
8709
8713
|
groups.extend(vts);
|
|
8710
8714
|
}
|
|
@@ -8714,7 +8718,7 @@ fn compileClosureBody(
|
|
|
8714
8718
|
continue;
|
|
8715
8719
|
}
|
|
8716
8720
|
locals.insert(name.clone(), idx);
|
|
8717
|
-
let vts =
|
|
8721
|
+
let vts = flatValtypes(ty, &gcTypes());
|
|
8718
8722
|
idx += vts.len() as u32;
|
|
8719
8723
|
groups.extend(vts);
|
|
8720
8724
|
}
|
plum-wasm-codegen/tests/e2e_mem/reuse.rs
CHANGED
|
@@ -27,8 +27,21 @@ fn runInt(name: &str, src: &str, reuse: bool) -> i64 {
|
|
|
27
27
|
|
|
28
28
|
/// Number of `struct.new` instructions in the whole module.
|
|
29
29
|
fn structNewCount(name: &str, src: &str, reuse: bool) -> usize {
|
|
30
|
+
structNewCountWith(name, src, reuse, true)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// `structNewCount` with the fusion stage on or off, so reuse is measured on its own: with
|
|
34
|
+
/// fusion on, the `sum(bump(..))` pipeline is fused away entirely (fewer allocations for
|
|
35
|
+
/// another reason), and a program pre-marked by hand is not what the real pipeline sees.
|
|
36
|
+
fn structNewCountWith(name: &str, src: &str, reuse: bool, fusion: bool) -> usize {
|
|
30
37
|
let source = load(name, src, reuse).expect("load");
|
|
38
|
+
let opts = plum_wasm_codegen::CompileOptions {
|
|
39
|
+
fusion: plum_checker::FusionOptions { enabled: fusion, explain: false },
|
|
40
|
+
..Default::default()
|
|
41
|
+
};
|
|
42
|
+
let bytes = plum_wasm_codegen::compile(&source, &opts)
|
|
43
|
+
.map(|o| o.wasm)
|
|
31
|
-
|
|
44
|
+
.unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
32
45
|
wasmparser::validate(&bytes).expect("valid wasm");
|
|
33
46
|
let mut n = 0;
|
|
34
47
|
for payload in wasmparser::Parser::new(0).parse_all(&bytes) {
|
|
@@ -68,20 +81,13 @@ fun bump(xs: Link) -> Link = match xs
|
|
|
68
81
|
fun main2() -> Int = sum(bump(Cons(head: 1, tail: Cons(head: 2, tail: Nil))))
|
|
69
82
|
";
|
|
70
83
|
|
|
71
|
-
/// Runs `f` with the fusion stage off, so reuse is measured on its own: with fusion on, the
|
|
72
|
-
/// `sum(bump(..))` pipeline is fused away entirely (fewer allocations for another reason),
|
|
73
|
-
/// and a program pre-marked by hand is not what the real pipeline sees.
|
|
74
|
-
fn withoutFusion<R>(f: impl FnOnce() -> R) -> R {
|
|
75
|
-
plum_checker::withFusionOptions(plum_checker::FusionOptions { enabled: false, explain: false }, f)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
84
|
/// The recursive `inc` is rewritten by TRMC (which builds its own cell), so the
|
|
79
85
|
/// struct.new count is checked on a non-recursive rebuild; `inc` is checked for results.
|
|
80
86
|
#[test]
|
|
81
87
|
fn bump_reuses_cells() {
|
|
82
88
|
let p = format!("{PRE}{INC}{BUMP}");
|
|
83
|
-
let a =
|
|
89
|
+
let a = structNewCountWith("bump_off", &p, false, false);
|
|
84
|
-
let b =
|
|
90
|
+
let b = structNewCountWith("bump_on", &p, true, false);
|
|
85
91
|
assert_eq!(a, b + 1, "off {a} struct.new, on {b}");
|
|
86
92
|
}
|
|
87
93
|
|
|
@@ -104,7 +110,7 @@ fn pipeline_marks_bump_reuse() {
|
|
|
104
110
|
assert!(bump.contains("reuse: Some(\"xs\")"), "{bump}");
|
|
105
111
|
// The fused pipeline allocates less than the unfused one.
|
|
106
112
|
let fused = structNewCount("bump_fused", &p, false);
|
|
107
|
-
let plain =
|
|
113
|
+
let plain = structNewCountWith("bump_plain", &p, false, false);
|
|
108
114
|
assert!(fused < plain, "fused {fused} vs unfused {plain}");
|
|
109
115
|
}
|
|
110
116
|
|
plum-wasm-codegen/tests/fusion_alloc_test.rs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#![allow(non_snake_case)]
|
|
2
2
|
//! Fusion allocation checks: compile through the real pipeline (checker `typedSource`
|
|
3
3
|
//! runs the supercompiler stage) and count `struct.new` of cell types per function.
|
|
4
|
-
use plum_wasm_codegen::compileSource;
|
|
5
4
|
use plum_test_support::repoRoot;
|
|
6
5
|
use std::collections::HashMap;
|
|
7
6
|
use std::collections::HashSet;
|
|
@@ -15,11 +14,11 @@ fn load(name: &str, src: &str) -> plum_core::ast::Source {
|
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
fn compile(source: &plum_core::ast::Source, fusion: bool) -> Vec<u8> {
|
|
18
|
-
let
|
|
17
|
+
let opts = plum_wasm_codegen::CompileOptions {
|
|
19
|
-
plum_checker::FusionOptions { enabled: fusion, explain: false },
|
|
18
|
+
fusion: plum_checker::FusionOptions { enabled: fusion, explain: false },
|
|
20
|
-
|
|
19
|
+
..Default::default()
|
|
21
|
-
|
|
20
|
+
};
|
|
22
|
-
let bytes =
|
|
21
|
+
let bytes = plum_wasm_codegen::compile(source, &opts).map(|o| o.wasm).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
23
22
|
wasmparser::validate(&bytes).expect("valid wasm");
|
|
24
23
|
bytes
|
|
25
24
|
}
|
|
@@ -121,9 +120,9 @@ const PRE: &str = "import std/Str\nimport std/Int\nimport std/List\nimport std/S
|
|
|
121
120
|
/// The fusion notes for `source` (what `--explain-fusion` prints): the stage is run
|
|
122
121
|
/// by hand on the checker's lowered program, built without fusion.
|
|
123
122
|
fn notes(source: &plum_core::ast::Source) -> Vec<String> {
|
|
124
|
-
let info = plum_checker::
|
|
123
|
+
let info = plum_checker::typedSourceWith(
|
|
124
|
+
source,
|
|
125
125
|
plum_checker::FusionOptions { enabled: false, explain: false },
|
|
126
|
-
|| plum_checker::typedSource(source),
|
|
127
126
|
)
|
|
128
127
|
.unwrap_or_else(|e| panic!("check: {e:?}"));
|
|
129
128
|
let mut copy = match info.lowered.as_ref() {
|