plum

#treesitter#compiler#wasm

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

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


b3aeaba — Peter John 2026-11-04T09:40:55+05:30
plum: --no-fusion and --explain-fusion flags replace the env vars
README.md CHANGED
@@ -891,7 +891,7 @@ What it does:
891
891
  - **Uniqueness to in-place spreads.** `markUnique` proves that a spread source (`T(..x, f: v)`) is not used afterwards and not shared, and marks the construction `unique`: codegen then writes the fields into `x` instead of copying it. Records declared `own` are already treated this way.
892
892
  - **Reuse.** A same-shape rebuild of a value that is dead afterwards (a `match` arm that rebuilds the constructor it matched) reuses the old cell.
893
893
 
894
- Switches: `PLUM_NO_FUSION=1` skips the whole stage; `plum run|test --explain-fusion` (or `PLUM_EXPLAIN_FUSION=1`) prints each decision to stderr (`fusion: ...`, `unique spread at fn:line`, and why a call was not specialised).
894
+ Switches: `plum build|run|test|game --no-fusion` skips the whole stage; `--explain-fusion` prints each decision to stderr (`fusion: ...`, `unique spread at fn:line`, and why a call was not specialised).
895
895
 
896
896
  Limits:
897
897
 
plum-checker/src/lib.rs CHANGED
@@ -1178,6 +1178,47 @@ pub fn checkSource(source: &ast::Source) -> CheckResult<()> {
1178
1178
  /// Type-checks the original source like `checkSource` and also returns the
1179
1179
  /// instantiated type of every expression (keyed by expression address) and the
1180
1180
  /// set of generic instantiations (see `TypedInfo`) for codegen.
1181
+ /// Switches for the supercompiler stage of `typedSource` (per thread).
1182
+ #[derive(Clone, Copy, Debug)]
1183
+ pub struct FusionOptions {
1184
+ /// Run the fusion stage (`--no-fusion` clears it).
1185
+ pub enabled: bool,
1186
+ /// Print each fusion decision to stderr (`--explain-fusion`).
1187
+ pub explain: bool,
1188
+ }
1189
+
1190
+ impl Default for FusionOptions {
1191
+ fn default() -> Self {
1192
+ FusionOptions { enabled: true, explain: false }
1193
+ }
1194
+ }
1195
+
1196
+ thread_local! {
1197
+ static FUSION_OPTIONS: Cell<FusionOptions> = Cell::new(FusionOptions::default());
1198
+ }
1199
+
1200
+ pub fn setFusionOptions(o: FusionOptions) {
1201
+ FUSION_OPTIONS.with(|c| c.set(o));
1202
+ }
1203
+
1204
+ pub fn fusionOptions() -> FusionOptions {
1205
+ FUSION_OPTIONS.with(|c| c.get())
1206
+ }
1207
+
1208
+ /// Runs `f` with `o` as this thread's fusion options, restoring the previous ones after
1209
+ /// (also on panic).
1210
+ pub fn withFusionOptions<R>(o: FusionOptions, f: impl FnOnce() -> R) -> R {
1211
+ struct Restore(FusionOptions);
1212
+ impl Drop for Restore {
1213
+ fn drop(&mut self) {
1214
+ setFusionOptions(self.0);
1215
+ }
1216
+ }
1217
+ let _restore = Restore(fusionOptions());
1218
+ setFusionOptions(o);
1219
+ f()
1220
+ }
1221
+
1181
1222
  pub fn typedSource(source: &ast::Source) -> CheckResult<TypedInfo> {
1182
1223
  let (mut result, mut info) = checkSourceFull(source);
1183
1224
  // Closure specialisation: copies of callees with the closure argument inlined. Same
@@ -1202,8 +1243,8 @@ pub fn typedSource(source: &ast::Source) -> CheckResult<TypedInfo> {
1202
1243
  }
1203
1244
  }
1204
1245
  // Supercompiler stage: purity -> simplify -> drive -> uniqueness -> reuse. Same
1205
- // re-check discipline; PLUM_NO_FUSION=1 skips it.
1246
+ // re-check discipline; `--no-fusion` skips it.
1206
- if result.is_ok() && std::env::var_os("PLUM_NO_FUSION").is_none() {
1247
+ if result.is_ok() && fusionOptions().enabled {
1207
1248
  {
1208
1249
  // Programs with no loops or unions have no lowered program: start from the source.
1209
1250
  let mut copy = match info.lowered.as_ref() {
@@ -1211,7 +1252,7 @@ pub fn typedSource(source: &ast::Source) -> CheckResult<TypedInfo> {
1211
1252
  None => source.clone(),
1212
1253
  };
1213
1254
  let notes = fusionRewrite(&mut copy);
1214
- if std::env::var("PLUM_EXPLAIN_FUSION").map(|v| v == "1").unwrap_or(false) {
1255
+ if fusionOptions().explain {
1215
1256
  for n in &notes {
1216
1257
  eprintln!("fusion: {n}");
1217
1258
  }
plum-checker/tests/fusion_pipeline_test.rs CHANGED
@@ -89,7 +89,7 @@ fn runTest(fusion: bool, file: &str) -> String {
89
89
  let mut cmd = std::process::Command::new(bin);
90
90
  cmd.current_dir(&root).args(["test", file]);
91
91
  if !fusion {
92
- cmd.env("PLUM_NO_FUSION", "1");
92
+ cmd.arg("--no-fusion");
93
93
  }
94
94
  let out = cmd.output().unwrap();
95
95
  format!("{}{}", String::from_utf8_lossy(&out.stdout), out.status.success())
plum-cli/src/main.rs CHANGED
@@ -53,6 +53,9 @@ enum Command {
53
53
  /// Print the fusion/uniqueness decisions to stderr
54
54
  #[arg(long)]
55
55
  explain_fusion: bool,
56
+ /// Skip the fusion (supercompiler) stage
57
+ #[arg(long)]
58
+ no_fusion: bool,
56
59
  /// Root directory `import <path>` is resolved against (`import std/x` -> `<lib-path>/std/x.plum`)
57
60
  #[arg(long, default_value = ".")]
58
61
  lib_path: std::path::PathBuf,
@@ -65,6 +68,9 @@ enum Command {
65
68
  /// Print the fusion/uniqueness decisions to stderr
66
69
  #[arg(long)]
67
70
  explain_fusion: bool,
71
+ /// Skip the fusion (supercompiler) stage
72
+ #[arg(long)]
73
+ no_fusion: bool,
68
74
  /// Output executable path (default: input file's name, no extension)
69
75
  #[arg(short, long)]
70
76
  output: Option<std::path::PathBuf>,
@@ -81,6 +87,9 @@ enum Command {
81
87
  /// Print the fusion/uniqueness decisions to stderr
82
88
  #[arg(long)]
83
89
  explain_fusion: bool,
90
+ /// Skip the fusion (supercompiler) stage
91
+ #[arg(long)]
92
+ no_fusion: bool,
84
93
  /// Root directory `import <path>` is resolved against (`import std/x` -> `<lib-path>/std/x.plum`)
85
94
  #[arg(long, default_value = ".")]
86
95
  lib_path: std::path::PathBuf,
@@ -92,6 +101,9 @@ enum Command {
92
101
  /// Print the fusion/uniqueness decisions to stderr
93
102
  #[arg(long)]
94
103
  explain_fusion: bool,
104
+ /// Skip the fusion (supercompiler) stage
105
+ #[arg(long)]
106
+ no_fusion: bool,
95
107
  /// Write the static build (index.html, plum_bridge.js, plum.wasm, preload.txt, assets/) here and exit
96
108
  #[arg(short, long)]
97
109
  output: Option<std::path::PathBuf>,
@@ -125,11 +137,9 @@ fn main() {
125
137
  }
126
138
  }
127
139
 
128
- /// `--explain-fusion` is the env switch `plum-checker` reads.
140
+ /// `--no-fusion` / `--explain-fusion` set the checker's fusion options.
129
- fn setExplain(on: bool) {
141
+ fn setFusion(explain: bool, no_fusion: bool) {
130
- if on {
131
- std::env::set_var("PLUM_EXPLAIN_FUSION", "1");
142
+ plum_checker::setFusionOptions(plum_checker::FusionOptions { enabled: !no_fusion, explain });
132
- }
133
143
  }
134
144
 
135
145
  fn run() -> Result<()> {
@@ -141,8 +151,8 @@ fn run() -> Result<()> {
141
151
  output,
142
152
  lib_path,
143
153
  } => cmdCompile(file, output, lib_path),
144
- Command::Run { file, lib_path, explain_fusion } => {
154
+ Command::Run { file, lib_path, explain_fusion, no_fusion } => {
145
- setExplain(explain_fusion);
155
+ setFusion(explain_fusion, no_fusion);
146
156
  cmdRun(file, lib_path)
147
157
  }
148
158
  Command::Build {
@@ -150,12 +160,13 @@ fn run() -> Result<()> {
150
160
  output,
151
161
  lib_path,
152
162
  explain_fusion,
163
+ no_fusion,
153
164
  } => {
154
- setExplain(explain_fusion);
165
+ setFusion(explain_fusion, no_fusion);
155
166
  cmdBuild(file, output, lib_path)
156
167
  }
157
- Command::Test { paths, lib_path, explain_fusion } => {
168
+ Command::Test { paths, lib_path, explain_fusion, no_fusion } => {
158
- setExplain(explain_fusion);
169
+ setFusion(explain_fusion, no_fusion);
159
170
  cmdTest(paths, lib_path)
160
171
  }
161
172
  Command::Game {
@@ -166,8 +177,9 @@ fn run() -> Result<()> {
166
177
  no_open,
167
178
  lib_path,
168
179
  explain_fusion,
180
+ no_fusion,
169
181
  } => {
170
- setExplain(explain_fusion);
182
+ setFusion(explain_fusion, no_fusion);
171
183
  game::cmdGame(file, output, assets, port, no_open, lib_path)
172
184
  }
173
185
  Command::Editor { editor, insiders } => editor::install(editor, insiders),
plum-core/tests/trmc_test.rs CHANGED
@@ -2,22 +2,11 @@
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 std::sync::Mutex;
6
-
7
- /// `PLUM_NO_FUSION` is process-wide and the checker reads it on every `typedSource`:
8
- /// every check or compile in this file takes this lock.
9
- static ENV: Mutex<()> = Mutex::new(());
5
+ use plum_checker::{withFusionOptions, FusionOptions};
10
6
 
7
+ /// The fusion option is thread-local: each test compiles on its own thread.
11
8
  fn withFusion<R>(fusion: bool, f: impl FnOnce() -> R) -> R {
12
- let _g = ENV.lock().unwrap_or_else(|e| e.into_inner());
9
+ withFusionOptions(FusionOptions { enabled: fusion, explain: false }, f)
13
- if fusion {
14
- std::env::remove_var("PLUM_NO_FUSION");
15
- } else {
16
- std::env::set_var("PLUM_NO_FUSION", "1");
17
- }
18
- let r = f();
19
- std::env::remove_var("PLUM_NO_FUSION");
20
- r
21
10
  }
22
11
 
23
12
  fn compileSource(source: &plum_core::ast::Source) -> Result<Vec<u8>, String> {
plum-wasm-codegen/tests/fusion_alloc_test.rs CHANGED
@@ -3,12 +3,8 @@
3
3
  //! runs the supercompiler stage) and count `struct.new` of cell types per function.
4
4
  use plum_wasm_codegen::compileSource;
5
5
  use std::collections::HashMap;
6
- use std::sync::Mutex;
7
6
  use std::collections::HashSet;
8
7
 
9
- /// `PLUM_NO_FUSION` is process-wide: every compile takes this lock.
10
- static ENV: Mutex<()> = Mutex::new(());
11
-
12
8
  fn repoRoot() -> std::path::PathBuf {
13
9
  std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
14
10
  }
@@ -28,14 +24,10 @@ fn load(name: &str, src: &str) -> plum_core::ast::Source {
28
24
  }
29
25
 
30
26
  fn compile(source: &plum_core::ast::Source, fusion: bool) -> Vec<u8> {
31
- let _g = ENV.lock().unwrap_or_else(|e| e.into_inner());
27
+ let r = plum_checker::withFusionOptions(
32
- if fusion {
33
- std::env::remove_var("PLUM_NO_FUSION");
34
- } else {
35
- std::env::set_var("PLUM_NO_FUSION", "1");
28
+ plum_checker::FusionOptions { enabled: fusion, explain: false },
36
- }
37
- let r = compileSource(source);
29
+ || compileSource(source),
38
- std::env::remove_var("PLUM_NO_FUSION");
30
+ );
39
31
  let bytes = r.unwrap_or_else(|e| panic!("compile failed: {e}"));
40
32
  wasmparser::validate(&bytes).expect("valid wasm");
41
33
  bytes
@@ -135,13 +127,14 @@ fn say(line: String) {
135
127
 
136
128
  const PRE: &str = "import std/Str\nimport std/Int\nimport std/List\nimport std/Slice\n\n";
137
129
 
138
- /// The fusion notes for `source` (what `PLUM_EXPLAIN_FUSION=1` prints): the stage is run
130
+ /// The fusion notes for `source` (what `--explain-fusion` prints): the stage is run
139
131
  /// by hand on the checker's lowered program, built without fusion.
140
132
  fn notes(source: &plum_core::ast::Source) -> Vec<String> {
141
- let _g = ENV.lock().unwrap_or_else(|e| e.into_inner());
133
+ let info = plum_checker::withFusionOptions(
142
- std::env::set_var("PLUM_NO_FUSION", "1");
134
+ plum_checker::FusionOptions { enabled: false, explain: false },
135
+ || plum_checker::typedSource(source),
136
+ )
143
- let info = plum_checker::typedSource(source).unwrap_or_else(|e| panic!("check: {e:?}"));
137
+ .unwrap_or_else(|e| panic!("check: {e:?}"));
144
- std::env::remove_var("PLUM_NO_FUSION");
145
138
  let mut copy = match info.lowered.as_ref() {
146
139
  Some(l) => (**l).clone(),
147
140
  None => source.clone(),
@@ -222,7 +215,7 @@ fn list_pipeline_loop_has_no_cons_alloc() {
222
215
  let eng = engine(false);
223
216
  let (a, _) = runInt(&eng, &fused);
224
217
  let (b, _) = runInt(&eng, &plain);
225
- assert_eq!(a, b, "fused result differs from PLUM_NO_FUSION");
218
+ assert_eq!(a, b, "fused result differs from the unfused build");
226
219
  assert_eq!(a, expected(100));
227
220
  }
228
221
 
plum-wasm-codegen/tests/own_inplace_e2e_test.rs CHANGED
@@ -1,9 +1,6 @@
1
1
  #![allow(non_snake_case)]
2
2
  use plum_wasm_codegen::compileSource;
3
3
 
4
- /// Env vars are process-wide; serialise tests that depend on PLUM_NO_FUSION.
5
- static ENV: std::sync::Mutex<()> = std::sync::Mutex::new(());
6
-
7
4
  fn gcEngine() -> wasmtime::Engine {
8
5
  let mut config = wasmtime::Config::new();
9
6
  config.wasm_gc(true);
@@ -230,10 +227,9 @@ fun main() -> Int =
230
227
  assert_eq!(runInt("reassign_param", &own), 41000 + 207);
231
228
  let plain = prog(&body.replace("own record", "record").replace("own s", "s"));
232
229
  assert_eq!(runInt("reassign_param_plain", &plain), 41000 + 207);
233
- let _g = ENV.lock().unwrap_or_else(|e| e.into_inner());
234
230
  let p = structNewCount("reassign_param_p", &plain);
235
231
  let o = structNewCount("reassign_param_o", &own);
236
- if std::env::var_os("PLUM_NO_FUSION").is_some() {
232
+ if !plum_checker::fusionOptions().enabled {
237
233
  assert_eq!(p, o + 1);
238
234
  } else {
239
235
  // Uniqueness proves the plain reassignment unique, so it is written in place too.