plum

#treesitter#compiler#wasm

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 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(CUR_EXPR.with(|e| e.get()), false, recv, name, args);
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) = CUR_POS.with(|p| p.get());
48
+ let (line, column) = withCheck(|s| { let p = &s.cur_pos; p.get() });
90
- CALLS.with(|c| {
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
- FRESH.with(|c| {
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) = TSUBST.with(|s| s.borrow().as_ref().and_then(|m| m.get(&ty.name).cloned()))
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
- RECORD_BINDS.with(|r| match r.borrow().as_ref() {
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
- RECORD_BINDS.with(|r| {
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
- RECORD.with(|r| {
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
- RECORD.with(|r| {
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
- RECORD.with(|r| {
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() && CUR_FN.with(|c| c.borrow().as_deref() == Some(k.as_str())) {
724
+ if !k.is_empty() && withCheck(|s| { let c = &s.cur_fn; c.borrow().as_deref() == Some(k.as_str()) }) {
766
- ERR_ACC.with(|a| a.borrow_mut().extend(names.iter().cloned()));
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` (per thread).
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, fusionOptions())
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
- RECORD.with(|r| *r.borrow_mut() = Some(HashMap::new()));
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
- OP_CALLS.with(|r| r.borrow_mut().clear());
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
- .with(|r| r.borrow_mut().take())
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 = OP_CALLS.with(|r| std::mem::take(&mut *r.borrow_mut()));
1536
+ info.op_calls = withCheck(|s| { let r = &s.op_calls; std::mem::take(&mut *r.borrow_mut()) });
1603
- let mut recorded = RECORD.with(|r| r.borrow_mut().take()).unwrap_or_default();
1537
+ let mut recorded = withCheck(|s| { let r = &s.record; r.borrow_mut().take() }).unwrap_or_default();
1604
- let binds = RECORD_BINDS
1605
- .with(|r| r.borrow_mut().take())
1538
+ let binds = withCheck(|s| { let r = &s.record_binds; r.borrow_mut().take() })
1606
1539
  .unwrap_or_default();
1607
- CALLS.with(|r| r.borrow_mut().take());
1540
+ withCheck(|s| { let r = &s.calls; r.borrow_mut().take() });
1608
- TSUBST.with(|s| *s.borrow_mut() = None);
1541
+ withCheck(|s| { let s = &s.tsubst; *s.borrow_mut() = None });
1609
- INST_MODE.with(|m| m.set(false));
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
- CALLS.with(|c| {
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
- CUR_POS.with(|l| l.set((f.line, f.column)));
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
- CUR_POS.with(|l| l.set((t.line, t.column)));
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
- TSUBST.with(|s| *s.borrow_mut() = Some(subst));
2354
+ withCheck(|s| { let s = &s.tsubst; *s.borrow_mut() = Some(subst) });
2422
- INST_MODE.with(|m| m.set(true));
2355
+ withCheck(|s| { let m = &s.inst_mode; m.set(true) });
2423
- CUR_POS.with(|l| l.set((f.line, f.column)));
2356
+ withCheck(|s| { let l = &s.cur_pos; l.set((f.line, f.column)) });
2424
- let saved = RECORD.with(|r| r.borrow_mut().replace(HashMap::new()));
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 = RECORD.with(|r| std::mem::replace(&mut *r.borrow_mut(), saved));
2359
+ let own = withCheck(|s| { let r = &s.record; std::mem::replace(&mut *r.borrow_mut(), saved) });
2427
- TSUBST.with(|s| *s.borrow_mut() = None);
2360
+ withCheck(|s| { let s = &s.tsubst; *s.borrow_mut() = None });
2428
- INST_MODE.with(|m| m.set(false));
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 = RECORD.with(|r| r.borrow_mut().take());
2417
+ let saved_record = withCheck(|s| { let r = &s.record; r.borrow_mut().take() });
2485
- let saved_calls = CALLS.with(|r| r.borrow_mut().take());
2418
+ let saved_calls = withCheck(|s| { let r = &s.calls; r.borrow_mut().take() });
2486
- let saved_sites = ERR_SITES.with(|r| r.borrow_mut().take());
2419
+ let saved_sites = withCheck(|s| { let r = &s.err_sites; r.borrow_mut().take() });
2487
- ERR_EDGES.with(|e| e.borrow_mut().clear());
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
- CUR_FN.with(|c| *c.borrow_mut() = Some(key.clone()));
2425
+ withCheck(|s| { let c = &s.cur_fn; *c.borrow_mut() = Some(key.clone()) });
2493
- ERR_ACC.with(|a| a.borrow_mut().clear());
2426
+ withCheck(|s| { let a = &s.err_acc; a.borrow_mut().clear() });
2494
- RECORD.with(|r| *r.borrow_mut() = Some(HashMap::new()));
2427
+ withCheck(|s| { let r = &s.record; *r.borrow_mut() = Some(HashMap::new()) });
2495
- CALLS.with(|r| *r.borrow_mut() = Some(Vec::new()));
2428
+ withCheck(|s| { let r = &s.calls; *r.borrow_mut() = Some(Vec::new()) });
2496
- CUR_POS.with(|l| l.set((f.line, f.column)));
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 = ERR_ACC.with(|a| std::mem::take(&mut *a.borrow_mut()));
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
- CUR_FN.with(|c| *c.borrow_mut() = None);
2444
+ withCheck(|s| { let c = &s.cur_fn; *c.borrow_mut() = None });
2512
- RECORD.with(|r| *r.borrow_mut() = saved_record);
2445
+ withCheck(|s| { let r = &s.record; *r.borrow_mut() = saved_record });
2513
- CALLS.with(|r| *r.borrow_mut() = saved_calls);
2446
+ withCheck(|s| { let r = &s.calls; *r.borrow_mut() = saved_calls });
2514
- ERR_SITES.with(|r| *r.borrow_mut() = saved_sites);
2447
+ withCheck(|s| { let r = &s.err_sites; *r.borrow_mut() = saved_sites });
2515
- let edges = ERR_EDGES.with(|e| std::mem::take(&mut *e.borrow_mut()));
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
- CALLS.with(|c| {
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
- .with(|r| r.borrow().clone())
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 TSUBST.with(|s| s.borrow().clone()) {
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
- TAIL.with(|c| *c.borrow_mut() = Some(declared_ret.clone()));
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
- CLOSURE_HINT.with(|h| *h.borrow_mut() = Some(hint));
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
- CLOSURE_HINT.with(|h| *h.borrow_mut() = None);
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 = TAIL.with(|t| t.borrow_mut().take());
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
- TAIL.with(|t| *t.borrow_mut() = tail.clone());
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
- CLOSURE_HINT.with(|h| *h.borrow_mut() = Some(hint));
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
- CLOSURE_HINT.with(|h| *h.borrow_mut() = None);
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 = ERR_POS.with(|e| e.replace(None));
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)) = ERR_POS.with(|e| e.replace(saved)) {
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 = TAIL.with(|t| t.borrow_mut().take());
3047
+ let tail = withCheck(|s| { let t = &s.tail; t.borrow_mut().take() });
3116
- let set_tail = |t: &Option<PlumType>| TAIL.with(|c| *c.borrow_mut() = t.clone());
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
- MATCH_TAIL.with(|c| *c.borrow_mut() = tail.clone());
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 = MATCH_TAIL.with(|c| c.borrow_mut().take());
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
- TAIL.with(|c| *c.borrow_mut() = match_tail.clone());
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 = CUR_EXPR.with(|c| c.get());
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 = ALLOW_BARE_TOP.with(|c| c.get()) == pat as *const ast::CasePattern as usize;
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 = CUR_EXPR.with(|c| c.get());
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 INST_MODE.with(|m| m.get())
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
- && (INST_MODE.with(|m| m.get())
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 = OP_CALLS.with(|m| {
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 = CUR_EXPR.with(|c| c.replace(expr as *const ast::Expr as usize));
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 = CUR_POS.with(|p| p.get());
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
- CUR_POS.with(|p| p.set(pos));
4828
+ withCheck(|s| { let p = &s.cur_pos; p.set(pos) });
4902
4829
  }
4903
- let entry_err_pos = ERR_POS.with(|e| e.get());
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
- ERR_POS.with(|e| e.set(entry_err_pos));
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) && ERR_POS.with(|e| e.get()).is_none() {
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
- ERR_POS.with(|e| e.set(call_pos));
4837
+ withCheck(|s| { let e = &s.err_pos; e.set(call_pos) });
4911
4838
  }
4912
- CUR_POS.with(|p| p.set(prev_pos));
4839
+ withCheck(|s| { let p = &s.cur_pos; p.set(prev_pos) });
4913
- CUR_EXPR.with(|c| c.set(prev));
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) = CUR_FN.with(|c| c.borrow().clone()) {
4848
+ if let Some(cur) = withCheck(|s| { let c = &s.cur_fn; c.borrow().clone() }) {
4922
- ERR_EDGES.with(|e| e.borrow_mut().insert((cur, k.clone())));
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
- ERR_SITES.with(|m| {
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
- ALLOW_BARE_TOP.with(|c| c.set(pat as *const ast::CasePattern as usize));
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
- ALLOW_BARE_TOP.with(|c| c.set(0));
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 = CUR_POS.with(|p| p.get());
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
- TAIL.with(|c| *c.borrow_mut() = Some(t.clone()));
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 CLOSURE_HINT.with(|h| h.borrow_mut().take()) {
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 !INST_MODE.with(|m| m.get()) {
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
- TSUBST.with(|s| s.borrow().as_ref().and_then(|m| m.get(owner).cloned()))
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
- TRAITS.with(|c| *c.borrow_mut() = out);
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
- TRAIT_FIELDS.with(|c| *c.borrow_mut() = fields);
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
- TRAIT_FIELDS.with(|c| {
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
- TRAITS.with(|c| c.borrow().contains_key(name))
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
- TRAITS.with(|c| c.borrow().get(name).cloned().unwrap_or_default())
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
- INLINE.with(|c| *c.borrow_mut() = out);
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
- INLINE.with(|c| c.borrow().get(record).cloned())
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
- DEPTH.with(|d| d.set(d.get() + 1));
30
+ withCheck(|s| { let d = &s.depth; d.set(d.get() + 1) });
44
31
  let r = f();
45
- DEPTH.with(|d| d.set(d.get() - 1));
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 = DEPTH.with(|d| d.replace(0));
40
+ let saved = withCheck(|s| { let d = &s.depth; d.replace(0) });
54
41
  let r = f();
55
- DEPTH.with(|d| d.set(saved));
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 IN_WRAP.with(|c| c.get()) || DEPTH.with(|d| d.get()) > 0 {
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
- IN_WRAP.with(|c| c.set(true));
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
- IN_WRAP.with(|c| c.set(false));
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 NARROW_TOP.with(|c| c.get()) != addr {
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
- NARROW_TOP.with(|c| c.set(addr));
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
- NARROW_TOP.with(|c| {
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 the running command, set once in `run`
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 setFusion(explain: bool, no_fusion: bool) {
141
+ fn fusionFlags(explain: bool, no_fusion: bool) -> plum_checker::FusionOptions {
145
- let _ = FUSION.set(plum_checker::FusionOptions { enabled: !no_fusion, explain });
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
- setFusion(explain_fusion, no_fusion);
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
- setFusion(explain_fusion, no_fusion);
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
- setFusion(explain_fusion, no_fusion);
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
- fn compileToWasm(file: &std::path::Path, lib_path: &std::path::Path) -> Result<Vec<u8>> {
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, &compileOptions(false)).map(|o| o.wasm).map_err(|e| {
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, &compileOptions(true)).map_err(|e| {
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
- fn cmdRun(file: std::path::PathBuf, lib_path: std::path::PathBuf) -> Result<()> {
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
- fn cmdTest(paths: Vec<std::path::PathBuf>, lib_path: std::path::PathBuf) -> Result<()> {
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
- fn runTestFile(file: &std::path::Path, lib_path: &std::path::Path) -> Result<(usize, usize)> {
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: AtomicUsize = AtomicUsize::new(1);
114
+ static NEXT_LOOP_ID: Cell<usize> = const { Cell::new(1) };
115
+ }
115
116
 
116
117
  fn nextLoopId() -> usize {
117
- NEXT_LOOP_ID.fetch_add(1, Ordering::Relaxed)
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
- use std::sync::atomic::{AtomicUsize, Ordering};
11
+ thread_local! {
12
-
13
- static FRESH: AtomicUsize = AtomicUsize::new(0);
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.fetch_add(1, Ordering::Relaxed)
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::{withFusionOptions, FusionOptions};
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 withFusion<R>(fusion: bool, f: impl FnOnce() -> R) -> R {
8
+ fn compileWith(source: &plum_core::ast::Source, fusion: bool) -> Result<Vec<u8>, String> {
9
- withFusionOptions(FusionOptions { enabled: fusion, explain: false }, f)
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
- withFusion(true, || plum_wasm_codegen::compileSource(source))
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
- withFusion(false, || plum_wasm_codegen::compileSource(source))
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
- withFusion(true, || plum_checker::typedSource(source))
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
- /// The current `compileSource` call's wasm-gc type registry. `compileSource` is
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
- /// the sole entry point and is never re-entrant/concurrent within one thread, so
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
- /// a thread-local avoids threading an explicit `&GcTypeRegistry` parameter through
560
+ /// The wasm-gc type registry; set once `buildGcTypeRegistry` has run.
558
- /// every one of `plumTypeToValtype`/`astTypeToWasm`'s ~25 call sites (many several
559
- /// functions removed from anywhere a `CompileCtx`/`LocalCtx` is in scope, e.g.
561
+ gc_types: Option<std::rc::Rc<GcTypeRegistry>>,
560
- /// function-signature registration that runs before any `LocalCtx` exists). Set
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
- /// once near the top of `compileSource`, before anything below reads it.
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
- static CURRENT_GC_TYPES: RefCell<Option<GcTypeRegistry>> = const { RefCell::new(None) };
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 CURRENT_CONSTS: RefCell<HashMap<String, ast::Expr>> = RefCell::new(HashMap::new());
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(withGcTypes(|r| scratchTypeIdx(r, ty, vname)))
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
- ERR_INFO.with(|e| e.borrow_mut().file = file.to_string());
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
- ERR_INFO.with(|e| {
794
+ withGen(|s| {
796
- e.borrow()
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
- .with(|e| e.borrow().codes.get(name).copied())
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 withGcTypes(|g| flatValtypes(ty, g)) {
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) => VALUE_ENUMS.with(|v| v.borrow().get(n).cloned()),
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 = withGcTypes(|r| r.classIdx(ty))
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
- VALUE_HELPERS.with(|h| {
916
+ withGen(|s| {
919
- *h.borrow()
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
- withGcTypes(|r| plumTypeToGcValtype(t, r))
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 `VALUE_BLOCK_TYPES`).
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) = VALUE_BLOCK_TYPES.with(|m| m.borrow().get(&i).copied()) {
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::fusionOptions())
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, &threadLocalOptions(false)).map(|o| o.wasm)
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
- ERR_INFO.with(|e| {
2168
+ withGen(|s| {
2169
+ *s = GenState {
2166
- *e.borrow_mut() = ErrInfo {
2170
+ err_info: ErrInfo {
2167
- file: source.file.clone(),
2171
+ file: source.file.clone(),
2168
- sites: err_sites,
2172
+ sites: err_sites,
2169
- codes: error_codes,
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
- VALUE_ENUMS.with(|v| {
2290
+ withGen(|s| {
2285
- let mut v = v.borrow_mut();
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
- VALUE_BLOCK_TYPES.with(|m| m.borrow_mut().clear());
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
- CURRENT_GC_TYPES.with(|c| *c.borrow_mut() = Some(gc_types.clone()));
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> = VALUE_ENUMS.with(|v| v.borrow().keys().cloned().collect());
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
- VALUE_BLOCK_TYPES.with(|m| m.borrow_mut().insert(box_idx, bt));
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
- VALUE_BLOCK_TYPES.with(|m| m.borrow_mut().insert(*box_idx, bt));
2402
+ withGen(|s| s.value_block_types.insert(*box_idx, bt));
2400
2403
  }
2401
2404
 
2402
- CURRENT_CONSTS.with(|c| {
2405
+ withGen(|s| {
2403
- let mut consts = c.borrow_mut();
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
- CURRENT_GC_TYPES.with(|c| *c.borrow_mut() = Some(gc_types.clone()));
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
- VALUE_HELPERS.with(|h| h.borrow_mut().insert(format!("unbox:{name}"), f));
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
- VALUE_HELPERS.with(|h| h.borrow_mut().insert(format!("eq:{name}"), f));
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
- VALUE_HELPERS.with(|h| h.borrow_mut().insert(format!("proj:{name}.{fname}"), f));
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
- VALUE_HELPERS.with(|h| h.borrow_mut().insert(unboxKey(&ty), f));
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
- CURRENT_GC_TYPES.with(|c| *c.borrow_mut() = Some(gc_types.clone()));
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
- CURRENT_GC_TYPES.with(|c| *c.borrow_mut() = Some(gc_types.clone()));
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) = withGcTypes(|r| r.mk_str_fn) else {
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 = withGcTypes(|r| r.superIdx(&array_ty))?;
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 = withGcTypes(|r| flatValtypes(elem, r));
3517
+ let leaves = flatValtypes(elem, &gcTypes());
3514
3518
  let leaf_arrs: Vec<u32> =
3515
- withGcTypes(|r| leaves.iter().map(|vt| r.leaf_array_idx[vt]).collect());
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 = withGcTypes(|g| flatValtypes(ty, g));
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 withGcTypes(|g| flatValtypes(pty, g)) == vts {
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 = ERR_INFO.with(|e| e.borrow().file.clone());
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 = ERR_INFO.with(|e| e.borrow().codes.keys().cloned().collect());
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 = CURRENT_CONSTS.with(|c| c.borrow().get(n).cloned());
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 = withGcTypes(|r| r.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 = withGcTypes(|r| plumTypeToGcValtype(&strType(), r));
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 = withGcTypes(|g| flatValtypes(ty, g));
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 = withGcTypes(|g| flatValtypes(ty, g));
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
- let bytes = plum_wasm_codegen::compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
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 = withoutFusion(|| structNewCount("bump_off", &p, false));
89
+ let a = structNewCountWith("bump_off", &p, false, false);
84
- let b = withoutFusion(|| structNewCount("bump_on", &p, true));
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 = withoutFusion(|| structNewCount("bump_plain", &p, false));
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 r = plum_checker::withFusionOptions(
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
- || compileSource(source),
19
+ ..Default::default()
21
- );
20
+ };
22
- let bytes = r.unwrap_or_else(|e| panic!("compile failed: {e}"));
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::withFusionOptions(
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() {