plum
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 +1 -1
- plum-checker/src/lib.rs +44 -3
- plum-checker/tests/fusion_pipeline_test.rs +1 -1
- plum-cli/src/main.rs +23 -11
- plum-core/tests/trmc_test.rs +3 -14
- plum-wasm-codegen/tests/fusion_alloc_test.rs +11 -18
- plum-wasm-codegen/tests/own_inplace_e2e_test.rs +1 -5
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: `
|
|
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;
|
|
1246
|
+
// re-check discipline; `--no-fusion` skips it.
|
|
1206
|
-
if result.is_ok() &&
|
|
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
|
|
1255
|
+
if fusionOptions().explain {
|
|
1215
1256
|
for n in ¬es {
|
|
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.
|
|
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`
|
|
140
|
+
/// `--no-fusion` / `--explain-fusion` set the checker's fusion options.
|
|
129
|
-
fn
|
|
141
|
+
fn setFusion(explain: bool, no_fusion: bool) {
|
|
130
|
-
if on {
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
27
|
+
let r = plum_checker::withFusionOptions(
|
|
32
|
-
if fusion {
|
|
33
|
-
std::env::remove_var("PLUM_NO_FUSION");
|
|
34
|
-
} else {
|
|
35
|
-
|
|
28
|
+
plum_checker::FusionOptions { enabled: fusion, explain: false },
|
|
36
|
-
}
|
|
37
|
-
|
|
29
|
+
|| compileSource(source),
|
|
38
|
-
|
|
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 `
|
|
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
|
|
133
|
+
let info = plum_checker::withFusionOptions(
|
|
142
|
-
|
|
134
|
+
plum_checker::FusionOptions { enabled: false, explain: false },
|
|
135
|
+
|| plum_checker::typedSource(source),
|
|
136
|
+
)
|
|
143
|
-
|
|
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
|
|
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
|
|
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.
|