plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
c3fa217
— Peter John
2026-11-06T13:14:05+05:30
FBIP stage 1: switch, stronger proof, Array.with, and fusion fixes
- compiler/checker/src/callcheck.rs +1 -0
- compiler/checker/src/fncheck.rs +36 -1
- compiler/checker/src/infer.rs +18 -1
- compiler/checker/tests/checker/diagnostic_tests.rs +19 -0
- compiler/cli/src/main.rs +24 -10
- compiler/cli/src/program.rs +2 -2
- compiler/cli/tests/cli/fusion_agree_test.rs +49 -0
- compiler/cli/tests/cli/main.rs +2 -0
- compiler/cli/tests/cli/no_fbip_test.rs +43 -0
- compiler/core/src/ast.rs +4 -0
- compiler/core/src/closure_spec.rs +24 -4
- compiler/core/src/ctors.rs +10 -2
- compiler/core/src/drive.rs +42 -11
- compiler/core/src/forms.rs +1 -0
- compiler/core/src/parser.rs +4 -0
- compiler/core/src/purity.rs +55 -28
- compiler/core/src/reuse.rs +29 -5
- compiler/core/src/simplify.rs +85 -8
- compiler/core/src/trmc.rs +4 -1
- compiler/core/src/uniqueness.rs +599 -35
- compiler/core/tests/core/purity_test.rs +41 -0
- compiler/core/tests/core/record_init_test.rs +1 -1
- compiler/core/tests/core/uniqueness_test.rs +266 -6
- compiler/driver/src/lib.rs +20 -8
- compiler/driver/tests/driver_tests.rs +1 -1
- compiler/game/web-test/src/lib.rs +8 -2
- compiler/game/web-test/src/main.rs +2 -1
- compiler/lsp/src/main.rs +1 -1
- compiler/wasm-codegen/src/closurecalls.rs +3 -0
- compiler/wasm-codegen/src/construct.rs +7 -19
- compiler/wasm-codegen/src/context.rs +55 -0
- compiler/wasm-codegen/src/driver.rs +7 -4
- compiler/wasm-codegen/src/emit.rs +6 -8
- compiler/wasm-codegen/src/expr.rs +1 -0
- compiler/wasm-codegen/src/fnbody.rs +3 -0
- compiler/wasm-codegen/src/units.rs +3 -0
- compiler/wasm-codegen/tests/wasm_codegen/codegen_tests.rs +122 -0
- docs/backlog.md +1 -0
- docs/plans/2026-11-06-fbip-stage1.md +1 -1
- docs/plans/2026-11-06-fbip.benchmark.md +52 -0
- std/Array.plum +16 -0
- tests/fbip/aliasing.plum +602 -0
- tests/fbip/array_names.plum +26 -0
- tests/fbip/array_with.plum +228 -0
- tests/fbip/operator_methods.plum +36 -0
- tests/fbip/spread_ro_method.plum +40 -0
- tests/fbip_probes/array_set.plum +28 -0
- tests/fbip_probes/buffer_write.plum +25 -0
- tests/fbip_probes/map_set_get.plum +28 -0
- tests/fbip_probes/slice_push_field.plum +27 -0
- tests/fbip_probes/slice_push_helper.plum +27 -0
- tests/fbip_probes/slice_push_local.plum +24 -0
- tests/fbip_probes/struct_spread.plum +25 -0
- tests/param_defaults.plum +130 -0
- tests/shadowed_effects.plum +164 -0
- tests/shadowed_free_fn.plum +136 -0
compiler/checker/src/callcheck.rs
CHANGED
|
@@ -463,6 +463,7 @@ pub(crate) fn inferOperatorCall(
|
|
|
463
463
|
line: 0,
|
|
464
464
|
column: 0,
|
|
465
465
|
tail: ast::TailKind::NotTail,
|
|
466
|
+
unique: false,
|
|
466
467
|
})
|
|
467
468
|
})
|
|
468
469
|
.clone()
|
compiler/checker/src/fncheck.rs
CHANGED
|
@@ -27,7 +27,42 @@ pub(crate) fn checkFn(f: &ast::Fn, global_env: &TypeEnv, ctx: &CheckCtx) -> Vec<
|
|
|
27
27
|
Some(r) => enumGenericsOf(r, ctx).to_vec(),
|
|
28
28
|
None => Vec::new(),
|
|
29
29
|
};
|
|
30
|
+
withFnScope(f, &owner, || {
|
|
31
|
+
let mut errors = checkParamDefaults(f, global_env, ctx);
|
|
30
|
-
|
|
32
|
+
errors.append(&mut checkFnBody(f, global_env, ctx));
|
|
33
|
+
errors
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Checks `f`'s parameter defaults against the declared types and records their types.
|
|
38
|
+
/// A default runs at a call that leaves the argument out, in place of it: it sees globals and
|
|
39
|
+
/// the owner's members, not `f`'s other parameters or `self`.
|
|
40
|
+
fn checkParamDefaults(f: &ast::Fn, global_env: &TypeEnv, ctx: &CheckCtx) -> Vec<CheckError> {
|
|
41
|
+
let defaulted = || f.params.iter().filter(|p| matches!(p.ty, ast::ParamType::Type(_)));
|
|
42
|
+
if f.is_extern || defaulted().all(|p| p.default.is_none()) {
|
|
43
|
+
return Vec::new();
|
|
44
|
+
}
|
|
45
|
+
let mut env = global_env.clone();
|
|
46
|
+
if let Some(owner) = &f.type_param {
|
|
47
|
+
env.insert(OWNER_KEY.to_string(), TypeScheme::mono(PlumType::TNamed(owner.clone())));
|
|
48
|
+
}
|
|
49
|
+
let mut errors = Vec::new();
|
|
50
|
+
for p in defaulted() {
|
|
51
|
+
let Some(d) = &p.default else { continue };
|
|
52
|
+
let declared = paramTypeG(&p.ty);
|
|
53
|
+
let at = (f.line, f.column);
|
|
54
|
+
let say = |msg: String| format!("fn '{}': default of parameter '{}': {}", f.name, p.name, msg);
|
|
55
|
+
let (r, pos, resolution) = withFailure(|| inferExprWith(d, &env, ctx, Some(&declared)));
|
|
56
|
+
match r {
|
|
57
|
+
Ok(t) => {
|
|
58
|
+
if let Err(msg) = unifyArg(&declared, &t, d, ctx) {
|
|
59
|
+
errors.push(failureError(spanOf(d), false, at, say(msg)));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
Err(msg) => errors.push(failureError(pos, resolution, at, say(msg))),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
errors
|
|
31
66
|
}
|
|
32
67
|
|
|
33
68
|
/// Report the failure in flight at `e` (an operand or part that is the culprit) instead of at
|
compiler/checker/src/infer.rs
CHANGED
|
@@ -641,8 +641,17 @@ pub(crate) fn inferExprInner(
|
|
|
641
641
|
)),
|
|
642
642
|
},
|
|
643
643
|
ast::AttrKind::Method(call) => {
|
|
644
|
+
if call.name == ARRAY_WITH_IN_PLACE {
|
|
645
|
+
return Err(format!("'{ARRAY_WITH_IN_PLACE}' is for the compiler only: call `with`"));
|
|
646
|
+
}
|
|
644
647
|
checkStaticUse(&attr.object, &obj_ty, &call.name, ctx)?;
|
|
648
|
+
// A `with` the uniqueness pass proved unshared runs `Array.withUnique` (in place).
|
|
649
|
+
let name = if call.unique && call.name == "with" && isArrayType(&obj_ty) {
|
|
650
|
+
ARRAY_WITH_IN_PLACE
|
|
651
|
+
} else {
|
|
652
|
+
call.name.as_str()
|
|
653
|
+
};
|
|
645
|
-
inferMethodCall(&obj_ty,
|
|
654
|
+
inferMethodCall(&obj_ty, name, &call.args, env, ctx, expected)
|
|
646
655
|
}
|
|
647
656
|
}
|
|
648
657
|
}
|
|
@@ -706,3 +715,11 @@ pub(crate) fn checkStaticUse(
|
|
|
706
715
|
}
|
|
707
716
|
Ok(())
|
|
708
717
|
}
|
|
718
|
+
|
|
719
|
+
/// `Array.with`'s in-place twin (std/Array.plum). Only the compiler picks it, for a `with` call
|
|
720
|
+
/// the uniqueness pass marked `unique`; a call written in source is an error.
|
|
721
|
+
pub(crate) const ARRAY_WITH_IN_PLACE: &str = "withUnique";
|
|
722
|
+
|
|
723
|
+
fn isArrayType(t: &PlumType) -> bool {
|
|
724
|
+
matches!(resolveRecorded(t), PlumType::TApp(k, _) if k == "Array")
|
|
725
|
+
}
|
compiler/checker/tests/checker/diagnostic_tests.rs
CHANGED
|
@@ -42,6 +42,25 @@ fn binaryMismatchIsAtTheOffendingOperand() {
|
|
|
42
42
|
assert!(es[0].message.contains("binary op"), "{es:?}");
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/// A parameter default is checked at its declaration, called or not.
|
|
46
|
+
#[test]
|
|
47
|
+
fn defaultMismatchIsAtTheDefault() {
|
|
48
|
+
let es = errors("fun f(b: Int = \"no\") -> Int = b\n\nfun main() -> Int = 1\n");
|
|
49
|
+
assert_eq!(es.len(), 1, "{es:?}");
|
|
50
|
+
assert_eq!(es[0].kind, DiagKind::Type, "{es:?}");
|
|
51
|
+
assert_eq!((es[0].line, es[0].column), (1, 16), "{es:?}");
|
|
52
|
+
assert!(es[0].message.contains("default of parameter 'b'"), "{es:?}");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// A default runs in place of the argument at the call: it cannot read the other parameters.
|
|
56
|
+
#[test]
|
|
57
|
+
fn defaultCannotReadAParameter() {
|
|
58
|
+
let es = errors("fun f(a: Int, b: Int = a) -> Int = b\n\nfun main() -> Int = 1\n");
|
|
59
|
+
assert_eq!(es.len(), 1, "{es:?}");
|
|
60
|
+
assert_eq!(es[0].kind, DiagKind::Resolution, "{es:?}");
|
|
61
|
+
assert!(es[0].message.contains("undefined name 'a'"), "{es:?}");
|
|
62
|
+
}
|
|
63
|
+
|
|
45
64
|
#[test]
|
|
46
65
|
fn returnMismatchIsAtTheReturnedValue() {
|
|
47
66
|
let es = errors("fun f() -> Int =\n x := 1\n true\n");
|
compiler/cli/src/main.rs
CHANGED
|
@@ -47,6 +47,9 @@ enum Command {
|
|
|
47
47
|
/// Root directory `import <path>` is resolved against (`import std/x` -> `<lib-path>/std/x.plum`)
|
|
48
48
|
#[arg(long, default_value = ".")]
|
|
49
49
|
lib_path: std::path::PathBuf,
|
|
50
|
+
/// No in-place updates: every spread copies and no cell is reused
|
|
51
|
+
#[arg(long)]
|
|
52
|
+
no_fbip: bool,
|
|
50
53
|
},
|
|
51
54
|
/// Run a Plum program: a game is served and opened in the browser, anything else runs under wasmtime
|
|
52
55
|
Run {
|
|
@@ -58,6 +61,9 @@ enum Command {
|
|
|
58
61
|
/// Skip the fusion (supercompiler) stage
|
|
59
62
|
#[arg(long)]
|
|
60
63
|
no_fusion: bool,
|
|
64
|
+
/// No in-place updates: every spread copies and no cell is reused
|
|
65
|
+
#[arg(long)]
|
|
66
|
+
no_fbip: bool,
|
|
61
67
|
/// Games: asset root (default: the nearest `assets` folder beside the source or above it)
|
|
62
68
|
#[arg(long)]
|
|
63
69
|
assets: Option<std::path::PathBuf>,
|
|
@@ -85,6 +91,9 @@ enum Command {
|
|
|
85
91
|
/// Skip the fusion (supercompiler) stage
|
|
86
92
|
#[arg(long)]
|
|
87
93
|
no_fusion: bool,
|
|
94
|
+
/// No in-place updates: every spread copies and no cell is reused
|
|
95
|
+
#[arg(long)]
|
|
96
|
+
no_fbip: bool,
|
|
88
97
|
/// Game: output folder (default: `dist` in the current folder). Otherwise: executable path (default: the file's name, no extension)
|
|
89
98
|
#[arg(short, long)]
|
|
90
99
|
output: Option<std::path::PathBuf>,
|
|
@@ -107,6 +116,9 @@ enum Command {
|
|
|
107
116
|
/// Skip the fusion (supercompiler) stage
|
|
108
117
|
#[arg(long)]
|
|
109
118
|
no_fusion: bool,
|
|
119
|
+
/// No in-place updates: every spread copies and no cell is reused
|
|
120
|
+
#[arg(long)]
|
|
121
|
+
no_fbip: bool,
|
|
110
122
|
/// Root directory `import <path>` is resolved against (`import std/x` -> `<lib-path>/std/x.plum`)
|
|
111
123
|
#[arg(long, default_value = ".")]
|
|
112
124
|
lib_path: std::path::PathBuf,
|
|
@@ -129,8 +141,8 @@ fn main() {
|
|
|
129
141
|
}
|
|
130
142
|
|
|
131
143
|
/// The `--no-fusion` / `--explain-fusion` flags of a command, as explicit fusion options.
|
|
132
|
-
fn fusionFlags(explain: bool, no_fusion: bool) -> plum_driver::PassOptions {
|
|
144
|
+
fn fusionFlags(explain: bool, no_fusion: bool, no_fbip: bool) -> plum_driver::PassOptions {
|
|
133
|
-
plum_driver::PassOptions { fusion: !no_fusion, explain, ..plum_driver::PassOptions::default() }
|
|
145
|
+
plum_driver::PassOptions { fusion: !no_fusion, fbip: !no_fbip, explain, ..plum_driver::PassOptions::default() }
|
|
134
146
|
}
|
|
135
147
|
|
|
136
148
|
/// One diagnostic as `file:line:col: <kind>: message`. `entry` names the file when the
|
|
@@ -164,15 +176,16 @@ fn run() -> Result<()> {
|
|
|
164
176
|
file,
|
|
165
177
|
output,
|
|
166
178
|
lib_path,
|
|
179
|
+
no_fbip,
|
|
167
|
-
} => cmdCompile(file, output, lib_path),
|
|
180
|
+
} => cmdCompile(file, output, lib_path, no_fbip),
|
|
168
|
-
Command::Run { file, lib_path, explain_fusion, no_fusion, assets, port, no_open, no_watch } => {
|
|
181
|
+
Command::Run { file, lib_path, explain_fusion, no_fusion, no_fbip, assets, port, no_open, no_watch } => {
|
|
169
|
-
cmdRun(file, lib_path, fusionFlags(explain_fusion, no_fusion), GameOptions { assets, port, noOpen: no_open, noWatch: no_watch, output: None })
|
|
182
|
+
cmdRun(file, lib_path, fusionFlags(explain_fusion, no_fusion, no_fbip), GameOptions { assets, port, noOpen: no_open, noWatch: no_watch, output: None })
|
|
170
183
|
}
|
|
171
|
-
Command::Build { file, output, lib_path, assets, explain_fusion, no_fusion } => {
|
|
184
|
+
Command::Build { file, output, lib_path, assets, explain_fusion, no_fusion, no_fbip } => {
|
|
172
|
-
cmdBuild(file, lib_path, fusionFlags(explain_fusion, no_fusion), GameOptions { assets, port: None, noOpen: false, noWatch: false, output })
|
|
185
|
+
cmdBuild(file, lib_path, fusionFlags(explain_fusion, no_fusion, no_fbip), GameOptions { assets, port: None, noOpen: false, noWatch: false, output })
|
|
173
186
|
}
|
|
174
|
-
Command::Test { paths, lib_path, explain_fusion, no_fusion } => {
|
|
187
|
+
Command::Test { paths, lib_path, explain_fusion, no_fusion, no_fbip } => {
|
|
175
|
-
cmdTest(paths, lib_path, fusionFlags(explain_fusion, no_fusion))
|
|
188
|
+
cmdTest(paths, lib_path, fusionFlags(explain_fusion, no_fusion, no_fbip))
|
|
176
189
|
}
|
|
177
190
|
Command::Editor { editor, insiders } => editor::install(editor, insiders),
|
|
178
191
|
}
|
|
@@ -283,8 +296,9 @@ fn cmdCompile(
|
|
|
283
296
|
file: std::path::PathBuf,
|
|
284
297
|
output: Option<std::path::PathBuf>,
|
|
285
298
|
lib_path: std::path::PathBuf,
|
|
299
|
+
no_fbip: bool,
|
|
286
300
|
) -> Result<()> {
|
|
287
|
-
let wasm_bytes = compileToWasm(&file, &lib_path, plum_driver::PassOptions::default())?;
|
|
301
|
+
let wasm_bytes = compileToWasm(&file, &lib_path, plum_driver::PassOptions { fbip: !no_fbip, ..plum_driver::PassOptions::default() })?;
|
|
288
302
|
|
|
289
303
|
let out_path = output.unwrap_or_else(|| file.with_extension("wasm"));
|
|
290
304
|
fs::write(&out_path, &wasm_bytes)
|
compiler/cli/src/program.rs
CHANGED
|
@@ -65,7 +65,7 @@ pub fn tryCompileProgram(
|
|
|
65
65
|
let out = plum_wasm_codegen::compile(&ast, &options).map_err(|mut ds| {
|
|
66
66
|
if let Some(written) = &written {
|
|
67
67
|
// Errors in the user's own `main` are reported under its name, not the renamed one.
|
|
68
|
-
let own = plum_driver::PassOptions { closureSpec: false, fusion: false, trmc: false, explain: false };
|
|
68
|
+
let own = plum_driver::PassOptions { closureSpec: false, fusion: false, trmc: false, fbip: false, explain: false };
|
|
69
69
|
if let Err(userErrors) = plum_driver::check(written, &own) {
|
|
70
70
|
ds = userErrors;
|
|
71
71
|
}
|
|
@@ -97,7 +97,7 @@ mod rebuildTests {
|
|
|
97
97
|
use super::*;
|
|
98
98
|
|
|
99
99
|
fn opts() -> plum_driver::PassOptions {
|
|
100
|
-
plum_driver::PassOptions { closureSpec: false, fusion: false, trmc: false, explain: false }
|
|
100
|
+
plum_driver::PassOptions { closureSpec: false, fusion: false, trmc: false, fbip: false, explain: false }
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
#[test]
|
compiler/cli/tests/cli/fusion_agree_test.rs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
//! Fusion must not change what a program does: these files assert the same answers with the
|
|
3
|
+
//! supercompiler stage on and off.
|
|
4
|
+
use std::process::Command;
|
|
5
|
+
|
|
6
|
+
fn plumBin() -> std::path::PathBuf {
|
|
7
|
+
let mut path = std::env::current_exe().unwrap();
|
|
8
|
+
path.pop();
|
|
9
|
+
if path.ends_with("deps") {
|
|
10
|
+
path.pop();
|
|
11
|
+
}
|
|
12
|
+
path.push("plum");
|
|
13
|
+
path
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
fn passesWithAndWithoutFusion(file: &str) {
|
|
17
|
+
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).ancestors().nth(2).unwrap();
|
|
18
|
+
for flags in [&[][..], &["--no-fusion"][..]] {
|
|
19
|
+
let out = Command::new(plumBin())
|
|
20
|
+
.arg("test")
|
|
21
|
+
.args(flags)
|
|
22
|
+
.arg(root.join("tests").join(file))
|
|
23
|
+
.arg("--lib-path")
|
|
24
|
+
.arg(root)
|
|
25
|
+
.output()
|
|
26
|
+
.expect("failed to run plum");
|
|
27
|
+
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
28
|
+
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
29
|
+
assert!(out.status.success(), "plum test {file} {flags:?} failed:\n{stdout}\n{stderr}");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// A local, parameter, closure parameter or pattern binder shadows the free fn of its name.
|
|
34
|
+
#[test]
|
|
35
|
+
fn shadowedFreeFnAgrees() {
|
|
36
|
+
passesWithAndWithoutFusion("shadowed_free_fn.plum");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// A binder named like a free fn, method or pattern name keeps its effect and its object.
|
|
40
|
+
#[test]
|
|
41
|
+
fn shadowedEffectsAgree() {
|
|
42
|
+
passesWithAndWithoutFusion("shadowed_effects.plum");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Parameter defaults of every kind compile at a call that leaves the argument out.
|
|
46
|
+
#[test]
|
|
47
|
+
fn paramDefaultsAgree() {
|
|
48
|
+
passesWithAndWithoutFusion("param_defaults.plum");
|
|
49
|
+
}
|
compiler/cli/tests/cli/main.rs
CHANGED
|
@@ -4,8 +4,10 @@ mod assert_once_test;
|
|
|
4
4
|
mod compile_tests;
|
|
5
5
|
mod crash_test;
|
|
6
6
|
mod explain_layout_test;
|
|
7
|
+
mod fusion_agree_test;
|
|
7
8
|
mod game_entry_test;
|
|
8
9
|
mod game_web_test;
|
|
9
10
|
mod host_stub_test;
|
|
10
11
|
mod main_result_test;
|
|
12
|
+
mod no_fbip_test;
|
|
11
13
|
mod test_scope_test;
|
compiler/cli/tests/cli/no_fbip_test.rs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
use std::process::Command;
|
|
3
|
+
|
|
4
|
+
fn plumBin() -> std::path::PathBuf {
|
|
5
|
+
let mut path = std::env::current_exe().unwrap();
|
|
6
|
+
path.pop();
|
|
7
|
+
if path.ends_with("deps") {
|
|
8
|
+
path.pop();
|
|
9
|
+
}
|
|
10
|
+
path.push("plum");
|
|
11
|
+
path
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const SRC: &str = "import std/Int\nimport std/Str\nimport std/Io\n\nstruct Cell(label: Str, n: Int)\n\nfun main(io: Io) =\n c := Cell(label: \"c\", n: 0)\n for i := range 10\n c = Cell(..c, n: c.n + 1)\n io.printLn(\"{c.n}\")\n";
|
|
15
|
+
|
|
16
|
+
fn run(src: &std::path::Path, flags: &[&str]) -> (String, String) {
|
|
17
|
+
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).ancestors().nth(2).unwrap();
|
|
18
|
+
let out = Command::new(plumBin())
|
|
19
|
+
.args(["run", "--explain-fusion"])
|
|
20
|
+
.args(flags)
|
|
21
|
+
.args([src.to_str().unwrap(), "--lib-path", root.to_str().unwrap()])
|
|
22
|
+
.output()
|
|
23
|
+
.expect("failed to run plum");
|
|
24
|
+
let err = String::from_utf8_lossy(&out.stderr).to_string();
|
|
25
|
+
assert!(out.status.success(), "plum run failed: {err}");
|
|
26
|
+
(String::from_utf8_lossy(&out.stdout).to_string(), err)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// `--no-fbip` is "no in-place updates": the spread is proved unique by default and not with the flag,
|
|
30
|
+
/// and the program prints the same either way.
|
|
31
|
+
#[test]
|
|
32
|
+
fn noFbipTurnsOffUniqueSpreads() {
|
|
33
|
+
let dir = std::env::temp_dir().join(format!("plum_no_fbip_{}", std::process::id()));
|
|
34
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
35
|
+
let src = dir.join("spread.plum");
|
|
36
|
+
std::fs::write(&src, SRC).unwrap();
|
|
37
|
+
let (outOn, errOn) = run(&src, &[]);
|
|
38
|
+
let (outOff, errOff) = run(&src, &["--no-fbip"]);
|
|
39
|
+
assert!(errOn.contains("unique spread at"), "default must prove the spread unique:\n{errOn}");
|
|
40
|
+
assert!(!errOff.contains("unique spread"), "--no-fbip must mark nothing:\n{errOff}");
|
|
41
|
+
assert_eq!(outOn, outOff);
|
|
42
|
+
assert_eq!(outOn.trim(), "10");
|
|
43
|
+
}
|
compiler/core/src/ast.rs
CHANGED
|
@@ -988,6 +988,10 @@ pub struct FnCall {
|
|
|
988
988
|
pub column: usize,
|
|
989
989
|
/// Set by `tail::markTailCalls`: whether (and how) this call is in tail position.
|
|
990
990
|
pub tail: TailKind,
|
|
991
|
+
/// Set by the uniqueness analysis on a method call named `with`: its receiver is provably
|
|
992
|
+
/// unshared and dead afterwards, so `Array.with` may write the array in place. Meaningless
|
|
993
|
+
/// (and ignored) on every other receiver type.
|
|
994
|
+
pub unique: bool,
|
|
991
995
|
}
|
|
992
996
|
|
|
993
997
|
/// How a call in tail position is made (see `tail.rs`). `NotTail` is the default.
|
compiler/core/src/closure_spec.rs
CHANGED
|
@@ -372,10 +372,8 @@ impl<'h> Ctx<'h> {
|
|
|
372
372
|
ExprKind::Question(_) => bad = true,
|
|
373
373
|
ExprKind::Self_ => usesSelf = true,
|
|
374
374
|
ExprKind::FnCall(call) => {
|
|
375
|
+
// a closure call, also when a free fn has that name: the local shadows it
|
|
375
|
-
if binders.contains(&call.name)
|
|
376
|
+
if binders.contains(&call.name) || locals.contains(&call.name) {
|
|
376
|
-
|| (locals.contains(&call.name)
|
|
377
|
-
&& !self.fns.get(&call.name).is_some_and(|f| f.type_param.is_none()))
|
|
378
|
-
{
|
|
379
377
|
bad = true;
|
|
380
378
|
}
|
|
381
379
|
}
|
|
@@ -411,6 +409,11 @@ impl<'h> Ctx<'h> {
|
|
|
411
409
|
if orig.is_extern || orig.is_export {
|
|
412
410
|
return None;
|
|
413
411
|
}
|
|
412
|
+
// Moved into the copy, a global the closure uses would resolve to a binder of the callee.
|
|
413
|
+
if self.captured(&orig, sid) {
|
|
414
|
+
self.copies.insert(ck, None);
|
|
415
|
+
return None;
|
|
416
|
+
}
|
|
414
417
|
let name = format!("{}$c{}", orig.name, n);
|
|
415
418
|
let newKey = match &orig.type_param {
|
|
416
419
|
Some(t) => format!("{}.{}", t, name),
|
|
@@ -463,6 +466,23 @@ impl<'h> Ctx<'h> {
|
|
|
463
466
|
}
|
|
464
467
|
}
|
|
465
468
|
|
|
469
|
+
/// Does spec `sid` use a global (not its own binder or capture) that `callee` binds?
|
|
470
|
+
fn captured(&self, callee: &Fn, sid: usize) -> bool {
|
|
471
|
+
let mut bound: HashSet<String> = callee.params.iter().map(|p| p.name.clone()).collect();
|
|
472
|
+
collectBinders(&mut callee.body.clone(), &mut bound);
|
|
473
|
+
let spec = &self.specs[sid];
|
|
474
|
+
match &spec.kind {
|
|
475
|
+
Kind::Name(g) => bound.contains(g),
|
|
476
|
+
Kind::Lit(cl) => {
|
|
477
|
+
let mut own: HashSet<String> = cl.params.iter().cloned().collect();
|
|
478
|
+
collectBinders(&mut FnBody::Block(cl.body.clone()), &mut own);
|
|
479
|
+
own.extend(spec.caps.iter().cloned());
|
|
480
|
+
bound.retain(|n| !own.contains(n));
|
|
481
|
+
crate::simplify::mentions(&cl.body, &bound)
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
466
486
|
fn buildCopy(&mut self, orig: Fn, name: String, j: usize, sid: usize) -> Option<Fn> {
|
|
467
487
|
let spec = self.specs[sid].clone();
|
|
468
488
|
let mut f = orig;
|
compiler/core/src/ctors.rs
CHANGED
|
@@ -20,6 +20,12 @@ fn local(field: &str) -> String {
|
|
|
20
20
|
format!("__f_{}", field)
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/// The generated constructor's parameter for a field. Named apart from the field: an init's
|
|
24
|
+
/// free call or global read must not resolve to a parameter the init never saw.
|
|
25
|
+
fn ctorParam(field: &str) -> String {
|
|
26
|
+
format!("__p_{}", field)
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
struct Plan {
|
|
24
30
|
params: Vec<(String, Type)>,
|
|
25
31
|
}
|
|
@@ -339,7 +345,7 @@ pub fn lowerCtors(source: &mut Source) -> Vec<String> {
|
|
|
339
345
|
ctor.push(Stmt::Assign(Assign {
|
|
340
346
|
isSynthetic: false,
|
|
341
347
|
targets: vec![AssignTarget::Var(local(p))],
|
|
342
|
-
values: vec![Expr::Var(
|
|
348
|
+
values: vec![Expr::Var(ctorParam(p))],
|
|
343
349
|
declare: true,
|
|
344
350
|
ty: Some(t.clone()),
|
|
345
351
|
}));
|
|
@@ -460,6 +466,7 @@ pub fn lowerCtors(source: &mut Source) -> Vec<String> {
|
|
|
460
466
|
line: line,
|
|
461
467
|
column: column,
|
|
462
468
|
tail: TailKind::NotTail,
|
|
469
|
+
unique: false,
|
|
463
470
|
}),
|
|
464
471
|
})),
|
|
465
472
|
0,
|
|
@@ -500,7 +507,7 @@ pub fn lowerCtors(source: &mut Source) -> Vec<String> {
|
|
|
500
507
|
params: nparams
|
|
501
508
|
.iter()
|
|
502
509
|
.map(|(n, t)| Param {
|
|
503
|
-
name:
|
|
510
|
+
name: ctorParam(n),
|
|
504
511
|
ty: paramType(t),
|
|
505
512
|
default: None,
|
|
506
513
|
})
|
|
@@ -577,6 +584,7 @@ fn rewriteCall(e: &mut Expr, plans: &HashMap<String, Plan>, errors: &mut Vec<Str
|
|
|
577
584
|
line: 0,
|
|
578
585
|
column: 0,
|
|
579
586
|
tail: TailKind::NotTail,
|
|
587
|
+
unique: false,
|
|
580
588
|
})
|
|
581
589
|
.at(e);
|
|
582
590
|
}
|
compiler/core/src/drive.rs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
use crate::ast::*;
|
|
12
12
|
use crate::generics::Scope;
|
|
13
13
|
use crate::rename::{renameStmts, substPT, substStmts, substType, Name};
|
|
14
|
-
use crate::simplify::{pinExpr, simplifyBlock};
|
|
14
|
+
use crate::simplify::{boundNames, calledNames, mentions, pinExpr, simplifyBlock, simplifyBlockIn};
|
|
15
15
|
use crate::visit::{argRef, isTag, walkBlockRef, walkRef};
|
|
16
16
|
use crate::visit::{childRefs, stmtBlockRefs, stmtBlocks, stmtExprRefs, visitBlock, visitExpr};
|
|
17
17
|
use crate::whistle::embeds;
|
|
@@ -63,6 +63,8 @@ struct Driver {
|
|
|
63
63
|
snap: Source,
|
|
64
64
|
fns: HashMap<String, Fn>,
|
|
65
65
|
free: HashSet<String>,
|
|
66
|
+
/// names the fn being driven binds: a call of one is not a call of the free fn of that name
|
|
67
|
+
bound: HashSet<String>,
|
|
66
68
|
methods: HashMap<String, Vec<String>>,
|
|
67
69
|
enumGens: HashMap<String, Vec<String>>,
|
|
68
70
|
/// every generic type (enum, struct, primitive) -> its generics, for fn scopes
|
|
@@ -342,6 +344,7 @@ impl Tcx {
|
|
|
342
344
|
line: 0,
|
|
343
345
|
column: 0,
|
|
344
346
|
tail: TailKind::NotTail,
|
|
347
|
+
unique: false,
|
|
345
348
|
}),
|
|
346
349
|
}))
|
|
347
350
|
} else if let Some(t) = &self.callee.type_param {
|
|
@@ -353,6 +356,7 @@ impl Tcx {
|
|
|
353
356
|
line: 0,
|
|
354
357
|
column: 0,
|
|
355
358
|
tail: TailKind::NotTail,
|
|
359
|
+
unique: false,
|
|
356
360
|
}),
|
|
357
361
|
}))
|
|
358
362
|
} else {
|
|
@@ -362,6 +366,7 @@ impl Tcx {
|
|
|
362
366
|
line: 0,
|
|
363
367
|
column: 0,
|
|
364
368
|
tail: TailKind::NotTail,
|
|
369
|
+
unique: false,
|
|
365
370
|
})
|
|
366
371
|
}
|
|
367
372
|
}
|
|
@@ -459,6 +464,7 @@ impl Driver {
|
|
|
459
464
|
snap: src.clone(),
|
|
460
465
|
fns: HashMap::new(),
|
|
461
466
|
free: HashSet::new(),
|
|
467
|
+
bound: HashSet::new(),
|
|
462
468
|
methods: HashMap::new(),
|
|
463
469
|
enumGens: HashMap::new(),
|
|
464
470
|
owners: crate::generics::ownerGenerics(src),
|
|
@@ -554,6 +560,7 @@ impl Driver {
|
|
|
554
560
|
continue;
|
|
555
561
|
}
|
|
556
562
|
let mut body = f.body.clone();
|
|
563
|
+
self.bound = boundNames(&f.params, &f.body);
|
|
557
564
|
self.vtypes = self.varTypes(f);
|
|
558
565
|
self.scope = self.scopeOf(f);
|
|
559
566
|
self.cur.clear();
|
|
@@ -569,6 +576,7 @@ impl Driver {
|
|
|
569
576
|
let mut i = 0;
|
|
570
577
|
while i < self.fused.len() {
|
|
571
578
|
let mut body = self.fused[i].body.clone();
|
|
579
|
+
self.bound = boundNames(&self.fused[i].params, &body);
|
|
572
580
|
self.vtypes = self.varTypes(&self.fused[i]);
|
|
573
581
|
self.scope = self.scopeOf(&self.fused[i].clone());
|
|
574
582
|
self.cur = self.origins.get(&self.fused[i].name).cloned().unwrap_or_default();
|
|
@@ -741,7 +749,7 @@ impl Driver {
|
|
|
741
749
|
|
|
742
750
|
fn calls(&self, e: &Expr) -> Vec<Call> {
|
|
743
751
|
match &e.kind {
|
|
744
|
-
ExprKind::FnCall(c) if self.free.contains(&c.name) => {
|
|
752
|
+
ExprKind::FnCall(c) if self.free.contains(&c.name) && !self.bound.contains(&c.name) => {
|
|
745
753
|
let f = &self.fns[&c.name];
|
|
746
754
|
match positional(&c.args) {
|
|
747
755
|
Some(args) if args.len() == f.params.len() && !f.has_self => {
|
|
@@ -869,9 +877,8 @@ impl Driver {
|
|
|
869
877
|
ExprKind::Closure(_) => return true,
|
|
870
878
|
ExprKind::Question(_) | ExprKind::Bang(_) | ExprKind::Bind(_) => return false,
|
|
871
879
|
ExprKind::FnCall(c) => {
|
|
872
|
-
let ok = self.
|
|
880
|
+
let ok = !self.bound.contains(&c.name)
|
|
873
|
-
|| self.ctors.contains(&c.name)
|
|
874
|
-
|| CASTS.contains(&c.name.as_str());
|
|
881
|
+
&& (self.pure.contains(&c.name) || self.ctors.contains(&c.name) || CASTS.contains(&c.name.as_str()));
|
|
875
882
|
if !ok {
|
|
876
883
|
return false;
|
|
877
884
|
}
|
|
@@ -1086,6 +1093,7 @@ impl Driver {
|
|
|
1086
1093
|
line: 0,
|
|
1087
1094
|
column: 0,
|
|
1088
1095
|
tail: TailKind::NotTail,
|
|
1096
|
+
unique: false,
|
|
1089
1097
|
});
|
|
1090
1098
|
return true;
|
|
1091
1099
|
}
|
|
@@ -1178,6 +1186,11 @@ impl Driver {
|
|
|
1178
1186
|
}
|
|
1179
1187
|
self.fresh += 1;
|
|
1180
1188
|
let (names, body) = self.renamed(&c.f, &format!("$k{}", self.fresh))?;
|
|
1189
|
+
if mentions(&body, &self.bound) {
|
|
1190
|
+
self.note(format!("not specialised: {}: a local here shadows a name its body uses", c.key));
|
|
1191
|
+
return None;
|
|
1192
|
+
}
|
|
1193
|
+
let called = calledNames(&body);
|
|
1181
1194
|
let Some(Stmt { kind: StmtKind::Match(m), .. }) = body.stmts.first().cloned() else { return None };
|
|
1182
1195
|
// An arm may read the matched value again once (`_ => .. effect.tint(alpha)`):
|
|
1183
1196
|
// the value is then built in that arm instead, so it is still built at most once.
|
|
@@ -1205,7 +1218,7 @@ impl Driver {
|
|
|
1205
1218
|
continue;
|
|
1206
1219
|
}
|
|
1207
1220
|
let trivial = matches!(&a.kind, ExprKind::Var(_) | ExprKind::Int(_) | ExprKind::Float(_) | ExprKind::BoolLit(_) | ExprKind::TypeName(_));
|
|
1208
|
-
if trivial && !rebinds(&body.stmts, n) {
|
|
1221
|
+
if trivial && !rebinds(&body.stmts, n) && !called.contains(n) {
|
|
1209
1222
|
subst.insert(n.clone(), copyOf(a));
|
|
1210
1223
|
} else {
|
|
1211
1224
|
pre.push(Stmt::Assign(Assign {
|
|
@@ -1229,7 +1242,7 @@ impl Driver {
|
|
|
1229
1242
|
});
|
|
1230
1243
|
pre.extend(blk.stmts);
|
|
1231
1244
|
let mut out = Block { stmts: pre };
|
|
1232
|
-
out =
|
|
1245
|
+
out = simplifyBlockIn(out, &self.snap, &self.pure, &self.bound);
|
|
1233
1246
|
let stuck = out.stmts.iter().any(|s| match &s.kind {
|
|
1234
1247
|
StmtKind::Match(m) => m.subjects.len() == 1 && self.ctorType(&m.subjects[0]).is_some(),
|
|
1235
1248
|
_ => false,
|
|
@@ -1349,6 +1362,14 @@ impl Driver {
|
|
|
1349
1362
|
return Err("argument count".to_string().into());
|
|
1350
1363
|
}
|
|
1351
1364
|
let (names, rb) = self.renamed(&c.f, "$u").ok_or("extern consumer")?;
|
|
1365
|
+
if mentions(&rb, &self.bound) {
|
|
1366
|
+
return Err(format!("a local here shadows a name wrapper {} uses", c.key).into());
|
|
1367
|
+
}
|
|
1368
|
+
// an argument replaces reads of its parameter; a call by that name has no read to replace
|
|
1369
|
+
let called = calledNames(&rb);
|
|
1370
|
+
if names.iter().any(|n| called.contains(n)) {
|
|
1371
|
+
return Err(format!("wrapper {} calls a parameter", c.key).into());
|
|
1372
|
+
}
|
|
1352
1373
|
let mut stmts = rb.stmts;
|
|
1353
1374
|
let Some(Stmt { kind: StmtKind::Expr(mut body, ..), .. }) = stmts.pop() else { return Err("wrapper body".into()) };
|
|
1354
1375
|
for (n, a) in names.iter().zip(&c.args) {
|
|
@@ -1487,6 +1508,14 @@ impl Driver {
|
|
|
1487
1508
|
n.push_str(suf);
|
|
1488
1509
|
}
|
|
1489
1510
|
});
|
|
1511
|
+
// a call by a bound name is a closure call: it follows its binder
|
|
1512
|
+
visitBlock(&mut body, &mut |e| {
|
|
1513
|
+
if let ExprKind::FnCall(c) = &mut e.kind {
|
|
1514
|
+
if bound.contains(&c.name) {
|
|
1515
|
+
c.name.push_str(suf);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
});
|
|
1490
1519
|
Some((params.into_iter().map(|p| format!("{p}{suf}")).collect(), body))
|
|
1491
1520
|
}
|
|
1492
1521
|
|
|
@@ -1854,6 +1883,7 @@ impl Driver {
|
|
|
1854
1883
|
line: 0,
|
|
1855
1884
|
column: 0,
|
|
1856
1885
|
tail: TailKind::NotTail,
|
|
1886
|
+
unique: false,
|
|
1857
1887
|
}));
|
|
1858
1888
|
self.origins.insert(name.clone(), anc);
|
|
1859
1889
|
self.fused.push(f);
|
|
@@ -2031,10 +2061,11 @@ impl Driver {
|
|
|
2031
2061
|
}
|
|
2032
2062
|
walkBlockRef(&Block { stmts: stmts.to_vec() }, &mut |e| match &e.kind {
|
|
2033
2063
|
ExprKind::FnCall(c) => {
|
|
2034
|
-
let known = self.
|
|
2064
|
+
let known = !self.bound.contains(&c.name)
|
|
2065
|
+
&& (self.ctors.contains(&c.name)
|
|
2035
|
-
|
|
2066
|
+
|| CASTS.contains(&c.name.as_str())
|
|
2036
|
-
|
|
2067
|
+
|| self.pure.contains(&c.name)
|
|
2037
|
-
|
|
2068
|
+
|| self.readOnly.contains(&c.name));
|
|
2038
2069
|
if !known && !takesP(&c.args) {
|
|
2039
2070
|
ok = false;
|
|
2040
2071
|
}
|
compiler/core/src/forms.rs
CHANGED
|
@@ -131,6 +131,7 @@ fn method(obj: &str, name: &str, args: Vec<Expr>) -> Expr {
|
|
|
131
131
|
line: 0,
|
|
132
132
|
column: 0,
|
|
133
133
|
tail: TailKind::NotTail,
|
|
134
|
+
unique: false,
|
|
134
135
|
}),
|
|
135
136
|
}))
|
|
136
137
|
}
|
compiler/core/src/parser.rs
CHANGED
|
@@ -1304,6 +1304,7 @@ impl<'a> AstParser<'a> {
|
|
|
1304
1304
|
line: pos.row + 1,
|
|
1305
1305
|
column: pos.column + 1,
|
|
1306
1306
|
tail: TailKind::NotTail,
|
|
1307
|
+
unique: false,
|
|
1307
1308
|
})
|
|
1308
1309
|
}
|
|
1309
1310
|
None => AttrKind::Field(member),
|
|
@@ -1336,6 +1337,7 @@ impl<'a> AstParser<'a> {
|
|
|
1336
1337
|
line,
|
|
1337
1338
|
column,
|
|
1338
1339
|
tail: TailKind::NotTail,
|
|
1340
|
+
unique: false,
|
|
1339
1341
|
}),
|
|
1340
1342
|
}))
|
|
1341
1343
|
};
|
|
@@ -1416,6 +1418,7 @@ impl<'a> AstParser<'a> {
|
|
|
1416
1418
|
line: pos.row + 1,
|
|
1417
1419
|
column: pos.column + 1,
|
|
1418
1420
|
tail: TailKind::NotTail,
|
|
1421
|
+
unique: false,
|
|
1419
1422
|
}
|
|
1420
1423
|
}
|
|
1421
1424
|
|
|
@@ -1453,6 +1456,7 @@ impl<'a> AstParser<'a> {
|
|
|
1453
1456
|
line: pos.row + 1,
|
|
1454
1457
|
column: pos.column + 1,
|
|
1455
1458
|
tail: TailKind::NotTail,
|
|
1459
|
+
unique: false,
|
|
1456
1460
|
}))
|
|
1457
1461
|
}
|
|
1458
1462
|
_ => {
|
compiler/core/src/purity.rs
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
//! field of anything but a fresh local (a local only ever bound to a record
|
|
4
4
|
//! construction), and every callee it can reach is pure. Closure literal bodies count
|
|
5
5
|
//! towards the enclosing fn. Calls are resolved by name only; an unknown callee
|
|
6
|
+
//! (extern, closure value, or any name the fn binds itself: a local, parameter, closure
|
|
7
|
+
//! parameter or pattern binder never resolves to the free fn of its name) is impure.
|
|
6
|
-
//! (
|
|
8
|
+
//! So is `x.f(..)` where `f` may be an fn-typed field. A method call on `self`, on a
|
|
7
9
|
//! parameter of a named type (never rebound or shadowed) or on a field read of one
|
|
8
10
|
//! resolves by that type (`io.clear(..)` is `Io.clear`, not `Slice.clear`): to the type's
|
|
9
11
|
//! method and every override below it, the call being as pure as all of them together.
|
|
@@ -249,13 +251,26 @@ struct Receivers<'a> {
|
|
|
249
251
|
fields: HashMap<&'a str, HashMap<&'a str, &'a str>>,
|
|
250
252
|
/// `(Type, method)` -> the overrides such a call may run
|
|
251
253
|
overrides: HashMap<(String, String), Vec<String>>,
|
|
254
|
+
/// names of the fn-typed fields of all types: `x.f(..)` may call that closure
|
|
255
|
+
fnFields: HashSet<&'a str>,
|
|
256
|
+
/// `Type.method` of every method: on such a receiver the method wins over a field
|
|
257
|
+
methods: HashSet<String>,
|
|
252
258
|
}
|
|
253
259
|
|
|
254
260
|
impl<'a> Receivers<'a> {
|
|
255
261
|
fn new(src: &'a Source) -> Self {
|
|
256
262
|
let mut fields: HashMap<&str, HashMap<&str, &str>> = HashMap::new();
|
|
263
|
+
let mut fnFields: HashSet<&str> = HashSet::new();
|
|
264
|
+
let mut methods: HashSet<String> = HashSet::new();
|
|
257
265
|
for item in &src.items {
|
|
266
|
+
if let Item::Fn(Fn { type_param: Some(t), name, .. }) = item {
|
|
267
|
+
methods.insert(format!("{t}.{name}"));
|
|
268
|
+
}
|
|
258
269
|
let Item::Enum(e) = item else { continue };
|
|
270
|
+
for v in &e.variants {
|
|
271
|
+
let named = v.field_names.iter().zip(&v.fields);
|
|
272
|
+
fnFields.extend(named.filter(|(_, t)| t.asFn().is_some()).map(|(n, _)| n.as_str()));
|
|
273
|
+
}
|
|
259
274
|
let [v] = &e.variants[..] else { continue };
|
|
260
275
|
if !e.isStruct() || v.field_names.len() != v.fields.len() {
|
|
261
276
|
continue;
|
|
@@ -263,7 +278,7 @@ impl<'a> Receivers<'a> {
|
|
|
263
278
|
let known = v.field_names.iter().zip(&v.fields).filter(|(_, t)| !e.generics.iter().any(|g| g.name == t.name));
|
|
264
279
|
fields.insert(e.name.as_str(), known.map(|(n, t)| (n.as_str(), t.name.as_str())).collect());
|
|
265
280
|
}
|
|
266
|
-
Receivers { fields, overrides: crate::subtyping::overrideTargets(src) }
|
|
281
|
+
Receivers { fields, overrides: crate::subtyping::overrideTargets(src), fnFields, methods }
|
|
267
282
|
}
|
|
268
283
|
}
|
|
269
284
|
|
|
@@ -274,6 +289,7 @@ fn analyse(f: &Fn, ctors: &HashSet<&str>, receivers: &Receivers) -> Info {
|
|
|
274
289
|
calls: Vec::new(),
|
|
275
290
|
ctors,
|
|
276
291
|
params: f.params.iter().map(|p| p.name.as_str()).collect(),
|
|
292
|
+
bound: crate::simplify::boundNames(&f.params, &f.body),
|
|
277
293
|
fresh: HashSet::new(),
|
|
278
294
|
typed: HashMap::new(),
|
|
279
295
|
receivers,
|
|
@@ -309,6 +325,8 @@ struct Walker<'a> {
|
|
|
309
325
|
calls: Vec<Call>,
|
|
310
326
|
ctors: &'a HashSet<&'a str>,
|
|
311
327
|
params: HashSet<&'a str>,
|
|
328
|
+
/// Every name the fn binds: a call of one is a closure call, not the free fn of its name.
|
|
329
|
+
bound: HashSet<String>,
|
|
312
330
|
/// Locals only ever bound to a record construction.
|
|
313
331
|
fresh: HashSet<String>,
|
|
314
332
|
/// receivers whose type is known (`self`, typed parameters never rebound) -> type name
|
|
@@ -328,12 +346,19 @@ impl<'a> Walker<'a> {
|
|
|
328
346
|
});
|
|
329
347
|
}
|
|
330
348
|
|
|
331
|
-
/// Collect locals every one of whose bindings is a `ClassCall`, anywhere in the body.
|
|
349
|
+
/// Collect locals every one of whose bindings is a `ClassCall`, anywhere in the body. A
|
|
350
|
+
/// pattern binder, loop binder or closure parameter of that name is a binding that is not.
|
|
332
351
|
fn findFresh(&mut self, b: &Block) {
|
|
352
|
+
let mut built: HashMap<String, usize> = HashMap::new();
|
|
353
|
+
scanAssigns(b, &mut built);
|
|
333
|
-
let mut
|
|
354
|
+
let mut binds: HashMap<String, usize> = HashMap::new();
|
|
355
|
+
crate::rename::renameStmts(&mut b.stmts.clone(), &mut |n, kind| {
|
|
356
|
+
if !matches!(kind, crate::rename::Name::Ref) {
|
|
334
|
-
|
|
357
|
+
*binds.entry(n.clone()).or_default() += 1;
|
|
358
|
+
}
|
|
359
|
+
});
|
|
335
|
-
for (n,
|
|
360
|
+
for (n, count) in built {
|
|
336
|
-
if
|
|
361
|
+
if binds.get(&n) == Some(&count) && !self.params.contains(n.as_str()) {
|
|
337
362
|
self.fresh.insert(n);
|
|
338
363
|
}
|
|
339
364
|
}
|
|
@@ -463,8 +488,8 @@ impl<'a> Walker<'a> {
|
|
|
463
488
|
}
|
|
464
489
|
ExprKind::FnCall(c) => {
|
|
465
490
|
self.args(&c.args);
|
|
466
|
-
if self.
|
|
491
|
+
if self.bound.contains(&c.name) {
|
|
467
|
-
self.impure = true; //
|
|
492
|
+
self.impure = true; // a local value: unknown callee
|
|
468
493
|
self.mutates = true;
|
|
469
494
|
} else if self.ctors.contains(c.name.as_str()) {
|
|
470
495
|
} else if CASTS.contains(&c.name.as_str()) && c.args.len() == 1 {
|
|
@@ -489,6 +514,14 @@ impl<'a> Walker<'a> {
|
|
|
489
514
|
// `Color.White.withAlpha(..)`: a method on a constant, not a static call
|
|
490
515
|
ExprKind::TypeName(t) if !t.contains('.') => self.calls.push(Call::Static(format!("{}.{}", t, c.name))),
|
|
491
516
|
_ => match self.receiverType(&a.object).map(str::to_string) {
|
|
517
|
+
// `x.f(..)` where no method of the receiver's type wins over an
|
|
518
|
+
// fn-typed field `f`: a closure call
|
|
519
|
+
t if self.receivers.fnFields.contains(c.name.as_str())
|
|
520
|
+
&& !t.as_ref().is_some_and(|t| self.receivers.methods.contains(&format!("{}.{}", t, c.name))) =>
|
|
521
|
+
{
|
|
522
|
+
self.impure = true;
|
|
523
|
+
self.mutates = true;
|
|
524
|
+
}
|
|
492
525
|
Some(t) => {
|
|
493
526
|
let below = self.receivers.overrides.get(&(t.clone(), c.name.clone()));
|
|
494
527
|
let keys = below.into_iter().flatten().cloned();
|
|
@@ -523,37 +556,31 @@ impl<'a> Walker<'a> {
|
|
|
523
556
|
}
|
|
524
557
|
}
|
|
525
558
|
|
|
526
|
-
///
|
|
559
|
+
/// Counts, for every `Var` assignment target in `b` (nested blocks entered, closures not), the
|
|
527
|
-
///
|
|
560
|
+
/// assignments of a record construction. Scopes are not told apart: a name bound in two
|
|
528
|
-
///
|
|
561
|
+
/// scopes must satisfy both.
|
|
529
|
-
fn scanAssigns(b: &Block,
|
|
562
|
+
fn scanAssigns(b: &Block, built: &mut HashMap<String, usize>) {
|
|
530
563
|
for s in &b.stmts {
|
|
531
564
|
match &s.kind {
|
|
532
565
|
StmtKind::Assign(a) => {
|
|
533
566
|
for (i, t) in a.targets.iter().enumerate() {
|
|
534
567
|
if let AssignTarget::Var(n) = t {
|
|
535
|
-
let isCtor = a.targets.len() == a.values.len()
|
|
536
|
-
|
|
568
|
+
if a.targets.len() == a.values.len() && matches!(a.values[i].kind, ExprKind::ClassCall(_)) {
|
|
537
|
-
|
|
569
|
+
*built.entry(n.clone()).or_default() += 1;
|
|
538
|
-
|
|
570
|
+
}
|
|
539
571
|
}
|
|
540
572
|
}
|
|
541
573
|
}
|
|
542
|
-
StmtKind::ForLoop(l) => {
|
|
543
|
-
for n in &l.binders {
|
|
544
|
-
good.insert(n.clone(), false);
|
|
545
|
-
}
|
|
546
|
-
|
|
574
|
+
StmtKind::ForLoop(l) => scanAssigns(&l.body, built),
|
|
547
|
-
}
|
|
548
|
-
StmtKind::While(w) => scanAssigns(&w.body,
|
|
575
|
+
StmtKind::While(w) => scanAssigns(&w.body, built),
|
|
549
576
|
StmtKind::If(i) => {
|
|
550
|
-
scanAssigns(&i.body,
|
|
577
|
+
scanAssigns(&i.body, built);
|
|
551
|
-
i.else_ifs.iter().for_each(|e| scanAssigns(&e.body,
|
|
578
|
+
i.else_ifs.iter().for_each(|e| scanAssigns(&e.body, built));
|
|
552
579
|
if let Some(b) = &i.else_ {
|
|
553
|
-
scanAssigns(b,
|
|
580
|
+
scanAssigns(b, built);
|
|
554
581
|
}
|
|
555
582
|
}
|
|
556
|
-
StmtKind::Match(m) => m.cases.iter().for_each(|c| scanAssigns(&c.body,
|
|
583
|
+
StmtKind::Match(m) => m.cases.iter().for_each(|c| scanAssigns(&c.body, built)),
|
|
557
584
|
_ => {}
|
|
558
585
|
}
|
|
559
586
|
}
|
compiler/core/src/reuse.rs
CHANGED
|
@@ -5,15 +5,17 @@
|
|
|
5
5
|
//! instead of allocating. Everything unclear stays an allocation. The proof:
|
|
6
6
|
//!
|
|
7
7
|
//! * `f` is a free, non-exported, uniquely named fn, never used as a value or a method name,
|
|
8
|
-
//! with no closures and no loops; `xs` is a plain parameter without default
|
|
8
|
+
//! with no closures and no loops; `xs` is a plain parameter without default, never rebound
|
|
9
|
-
//! use in `f` is as the subject of the `match` that is `f`'s last
|
|
9
|
+
//! or shadowed, whose only use in `f` is as the subject of the `match` that is `f`'s last
|
|
10
|
+
//! statement.
|
|
10
11
|
//! * Every call of `f` passes, for `xs`, a deeply fresh value (a constructor tree of scalars and
|
|
11
|
-
//! field-less variants, no spread) or a pattern binder of that match used exactly once
|
|
12
|
+
//! field-less variants, no spread) or a pattern binder of that match used exactly once and
|
|
12
|
-
//! (the recursive tail, owned by the dead `xs`).
|
|
13
|
+
//! bound nowhere else in `f` (the recursive tail, owned by the dead `xs`).
|
|
13
14
|
//! * The arm has one `Ctor(..)` pattern with binders, no guard, and ends in a
|
|
14
15
|
//! `Ctor(..)` constructor of the same name without spread.
|
|
15
16
|
use crate::ast::*;
|
|
16
17
|
use crate::forms::{mapExprs, walkStmts};
|
|
18
|
+
use crate::rename::{renameStmts, Name};
|
|
17
19
|
use std::collections::HashMap;
|
|
18
20
|
|
|
19
21
|
fn deepFresh(e: &Expr) -> bool {
|
|
@@ -42,6 +44,26 @@ fn argFor<'a>(call: &'a FnCall, idx: usize, name: &str) -> Option<&'a Expr> {
|
|
|
42
44
|
None
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
/// How many times each name is bound in `f`: parameters, locals (`x = v` included), loop
|
|
48
|
+
/// binders, closure parameters and pattern names.
|
|
49
|
+
fn bindCounts(f: &Fn) -> HashMap<String, usize> {
|
|
50
|
+
let mut out: HashMap<String, usize> = HashMap::new();
|
|
51
|
+
for p in &f.params {
|
|
52
|
+
*out.entry(p.name.clone()).or_default() += 1;
|
|
53
|
+
}
|
|
54
|
+
let mut note = |n: &mut String, kind: Name| {
|
|
55
|
+
if kind != Name::Ref {
|
|
56
|
+
*out.entry(n.clone()).or_default() += 1;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
match &f.body {
|
|
60
|
+
FnBody::Block(b) => renameStmts(&mut b.stmts.clone(), &mut note),
|
|
61
|
+
FnBody::Expr(e) => renameStmts(&mut [Stmt::Expr(e.clone(), 0, 0)], &mut note),
|
|
62
|
+
FnBody::Extern => {}
|
|
63
|
+
}
|
|
64
|
+
out
|
|
65
|
+
}
|
|
66
|
+
|
|
45
67
|
struct Cand {
|
|
46
68
|
item: usize,
|
|
47
69
|
param: usize,
|
|
@@ -73,7 +95,7 @@ pub fn markReuse(src: &mut Source) -> Vec<String> {
|
|
|
73
95
|
if !matches!(p.ty, ParamType::Type(_)) || p.default.is_some() {
|
|
74
96
|
continue;
|
|
75
97
|
}
|
|
76
|
-
if crate::visit::countVar(&f.body, v) != 1 {
|
|
98
|
+
if crate::visit::countVar(&f.body, v) != 1 || bindCounts(f).get(v) != Some(&1) {
|
|
77
99
|
continue;
|
|
78
100
|
}
|
|
79
101
|
let mut bad = false;
|
|
@@ -107,6 +129,8 @@ pub fn markReuse(src: &mut Source) -> Vec<String> {
|
|
|
107
129
|
crate::visit::patternBinders(p, &mut inner);
|
|
108
130
|
}
|
|
109
131
|
}
|
|
132
|
+
let binds = bindCounts(f);
|
|
133
|
+
inner.retain(|t| binds.get(t) == Some(&1));
|
|
110
134
|
for (idx, it) in src.items.iter().enumerate() {
|
|
111
135
|
let mut body = match it {
|
|
112
136
|
Item::Fn(g) => g.body.clone(),
|
compiler/core/src/simplify.rs
CHANGED
|
@@ -25,11 +25,66 @@ fn fresh() -> usize {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
pub fn simplifyExpr(e: Expr, src: &Source, pure: &HashSet<String>) -> Expr {
|
|
28
|
+
let mut cx = Cx::new(src, pure);
|
|
29
|
+
cx.bound = boundNames(&[], &FnBody::Expr(e.clone()));
|
|
28
|
-
|
|
30
|
+
cx.simExpr(e, None)
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
pub fn simplifyBlock(b: Block, src: &Source, pure: &HashSet<String>) -> Block {
|
|
34
|
+
simplifyBlockIn(b, src, pure, &HashSet::new())
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// `simplifyBlock` for a block that will sit in a scope binding `outer`.
|
|
38
|
+
pub fn simplifyBlockIn(b: Block, src: &Source, pure: &HashSet<String>, outer: &HashSet<String>) -> Block {
|
|
39
|
+
let mut cx = Cx::new(src, pure);
|
|
40
|
+
cx.bound = boundNames(&[], &FnBody::Block(b.clone()));
|
|
41
|
+
cx.bound.extend(outer.iter().cloned());
|
|
32
|
-
|
|
42
|
+
cx.simBlock(b)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Every name a fn with these parameters and this body binds: parameters, locals, loop and
|
|
46
|
+
/// closure parameters, pattern names. Such a name may shadow the free fn of that name, so a
|
|
47
|
+
/// call of it in that fn is not a call of the free fn.
|
|
48
|
+
pub fn boundNames(params: &[Param], body: &FnBody) -> HashSet<String> {
|
|
49
|
+
let mut out: HashSet<String> = params.iter().map(|p| p.name.clone()).collect();
|
|
50
|
+
let mut note = |n: &mut String, kind: Name| match kind {
|
|
51
|
+
Name::Decl => {
|
|
52
|
+
out.insert(n.clone());
|
|
53
|
+
}
|
|
54
|
+
Name::Pat if !isTag(n) => {
|
|
55
|
+
out.insert(n.clone());
|
|
56
|
+
}
|
|
57
|
+
_ => {}
|
|
58
|
+
};
|
|
59
|
+
match body {
|
|
60
|
+
FnBody::Block(b) => renameStmts(&mut b.stmts.clone(), &mut note),
|
|
61
|
+
FnBody::Expr(e) => renameStmts(&mut [Stmt::Expr(e.clone(), 0, 0)], &mut note),
|
|
62
|
+
FnBody::Extern => {}
|
|
63
|
+
}
|
|
64
|
+
out
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Does `b` call or read a name in `names`? Run on a body whose own binders are renamed, what
|
|
68
|
+
/// is left is global: moved under a binder of that name, it would resolve to the binder.
|
|
69
|
+
pub fn mentions(b: &Block, names: &HashSet<String>) -> bool {
|
|
70
|
+
let mut hit = false;
|
|
71
|
+
walkBlockRef(b, &mut |e| match &e.kind {
|
|
72
|
+
ExprKind::FnCall(c) if names.contains(&c.name) => hit = true,
|
|
73
|
+
ExprKind::Var(n) if names.contains(n) => hit = true,
|
|
74
|
+
_ => {}
|
|
75
|
+
});
|
|
76
|
+
hit
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// The names `b` calls.
|
|
80
|
+
pub fn calledNames(b: &Block) -> HashSet<String> {
|
|
81
|
+
let mut out = HashSet::new();
|
|
82
|
+
walkBlockRef(b, &mut |e| {
|
|
83
|
+
if let ExprKind::FnCall(c) = &e.kind {
|
|
84
|
+
out.insert(c.name.clone());
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
out
|
|
33
88
|
}
|
|
34
89
|
|
|
35
90
|
/// `e`, a value of type `t` moved out of the fn that typed it, with its tail constructors
|
|
@@ -51,6 +106,7 @@ pub fn simplify(src: &mut Source, pure: &HashSet<String>) -> Vec<String> {
|
|
|
51
106
|
continue;
|
|
52
107
|
}
|
|
53
108
|
cx.scope = Scope::in_(&cx.owners, f);
|
|
109
|
+
cx.bound = boundNames(&f.params, &f.body);
|
|
54
110
|
let before = f.body.clone();
|
|
55
111
|
let block = match std::mem::replace(&mut f.body, FnBody::Extern) {
|
|
56
112
|
FnBody::Block(b) => b,
|
|
@@ -93,6 +149,8 @@ struct Cx<'a> {
|
|
|
93
149
|
/// generic names in scope in the fn being simplified
|
|
94
150
|
scope: Scope,
|
|
95
151
|
owners: HashMap<String, Vec<String>>,
|
|
152
|
+
/// names the fn being simplified binds: see `boundNames`
|
|
153
|
+
bound: HashSet<String>,
|
|
96
154
|
}
|
|
97
155
|
|
|
98
156
|
fn trivial(e: &Expr) -> bool {
|
|
@@ -212,7 +270,7 @@ impl<'a> Cx<'a> {
|
|
|
212
270
|
}
|
|
213
271
|
}
|
|
214
272
|
}
|
|
215
|
-
Cx { pure, fns, recursive, ctors, variants, stack: vec![], depth: 0, scope: Scope::default(), owners: crate::generics::ownerGenerics(src) }
|
|
273
|
+
Cx { pure, fns, recursive, ctors, variants, stack: vec![], depth: 0, scope: Scope::default(), owners: crate::generics::ownerGenerics(src), bound: HashSet::new() }
|
|
216
274
|
}
|
|
217
275
|
|
|
218
276
|
fn isPure(&self, e: &Expr) -> bool {
|
|
@@ -221,7 +279,7 @@ impl<'a> Cx<'a> {
|
|
|
221
279
|
| ExprKind::Self_ | ExprKind::Closure(_) => true,
|
|
222
280
|
ExprKind::Binary(b) => !matches!(b.op, BinOp::Div | BinOp::Mod) && self.isPure(&b.left) && self.isPure(&b.right),
|
|
223
281
|
ExprKind::Question(_) | ExprKind::Bang(_) | ExprKind::Bind(_) => false,
|
|
224
|
-
ExprKind::FnCall(c) => self.pure.contains(&c.name) && c.args.iter().all(|a| self.isPure(argRef(a))),
|
|
282
|
+
ExprKind::FnCall(c) => !self.bound.contains(&c.name) && self.pure.contains(&c.name) && c.args.iter().all(|a| self.isPure(argRef(a))),
|
|
225
283
|
ExprKind::Attribute(a) => match &a.attr {
|
|
226
284
|
AttrKind::Field(_) => self.isPure(&a.object),
|
|
227
285
|
AttrKind::Method(_) => false,
|
|
@@ -231,6 +289,9 @@ impl<'a> Cx<'a> {
|
|
|
231
289
|
}
|
|
232
290
|
|
|
233
291
|
fn eligible(&self, name: &str) -> Option<&Fn> {
|
|
292
|
+
if self.bound.contains(name) {
|
|
293
|
+
return None;
|
|
294
|
+
}
|
|
234
295
|
let f = self.fns.get(name)?;
|
|
235
296
|
let ok = self.pure.contains(name)
|
|
236
297
|
&& !self.recursive.contains(name)
|
|
@@ -489,13 +550,17 @@ impl<'a> Cx<'a> {
|
|
|
489
550
|
.map(|b| (b.clone(), format!("{}__s{}", b, k)))
|
|
490
551
|
.collect();
|
|
491
552
|
Scoper { scopes: vec![initial], k }.stmts(&mut body.stmts);
|
|
553
|
+
if mentions(&body, &self.bound) {
|
|
554
|
+
return None;
|
|
555
|
+
}
|
|
556
|
+
let called = calledNames(&body);
|
|
492
557
|
let mut subst: HashMap<String, Expr> = HashMap::new();
|
|
493
558
|
let mut binds = Vec::new();
|
|
494
559
|
let mut nontrivial = 0;
|
|
495
560
|
for (i, a) in argv {
|
|
496
561
|
let p = &f.params[i].name;
|
|
497
562
|
let fr = map[p].clone();
|
|
498
|
-
if trivial(&a) && !assigned.contains(p) && !declared.contains(p) {
|
|
563
|
+
if trivial(&a) && !assigned.contains(p) && !declared.contains(p) && !called.contains(&fr) {
|
|
499
564
|
subst.insert(fr, a);
|
|
500
565
|
} else if allowLets {
|
|
501
566
|
binds.push(Stmt::Assign(Assign {
|
|
@@ -507,7 +572,7 @@ impl<'a> Cx<'a> {
|
|
|
507
572
|
}));
|
|
508
573
|
} else {
|
|
509
574
|
let StmtKind::Expr(be, ..) = &body.stmts.first()?.kind else { return None };
|
|
510
|
-
if body.stmts.len() != 1 || assigned.contains(p) || declared.contains(p) {
|
|
575
|
+
if body.stmts.len() != 1 || assigned.contains(p) || declared.contains(p) || called.contains(&fr) {
|
|
511
576
|
return None;
|
|
512
577
|
}
|
|
513
578
|
let mut acc = (0, 0);
|
|
@@ -735,11 +800,12 @@ impl<'a> Cx<'a> {
|
|
|
735
800
|
.map(|b| (b.clone(), format!("{}__s{}", b, k)))
|
|
736
801
|
.collect();
|
|
737
802
|
Scoper { scopes: vec![initial], k }.stmts(&mut body.stmts);
|
|
803
|
+
let called = calledNames(&body);
|
|
738
804
|
let mut out = Vec::new();
|
|
739
805
|
let mut subst: HashMap<String, Expr> = HashMap::new();
|
|
740
806
|
for (i, (n, v)) in binds.into_iter().enumerate() {
|
|
741
807
|
let target = match n {
|
|
742
|
-
Some(n) if trivial(&v) && !assigned.contains(&n) && !declared.contains(&n) => {
|
|
808
|
+
Some(n) if trivial(&v) && !assigned.contains(&n) && !declared.contains(&n) && !called.contains(&map[&n]) => {
|
|
743
809
|
subst.insert(map[&n].clone(), v);
|
|
744
810
|
continue;
|
|
745
811
|
}
|
|
@@ -850,7 +916,11 @@ impl Scoper {
|
|
|
850
916
|
self.expr(obj);
|
|
851
917
|
}
|
|
852
918
|
}
|
|
919
|
+
// `k := |n| .. k(n - 1) ..`: inside the literal the name is the closure itself
|
|
920
|
+
let selfRef = a.declare && matches!(&a.values[..], [Expr { kind: ExprKind::Closure(_), .. }]);
|
|
921
|
+
if !selfRef {
|
|
853
|
-
|
|
922
|
+
a.values.iter_mut().for_each(|v| self.expr(v));
|
|
923
|
+
}
|
|
854
924
|
let decl = a.declare;
|
|
855
925
|
for t in &mut a.targets {
|
|
856
926
|
if let AssignTarget::Var(n) = t {
|
|
@@ -861,6 +931,9 @@ impl Scoper {
|
|
|
861
931
|
}
|
|
862
932
|
}
|
|
863
933
|
}
|
|
934
|
+
if selfRef {
|
|
935
|
+
a.values.iter_mut().for_each(|v| self.expr(v));
|
|
936
|
+
}
|
|
864
937
|
}
|
|
865
938
|
StmtKind::ForLoop(f) => {
|
|
866
939
|
self.expr(&mut f.source);
|
|
@@ -893,6 +966,10 @@ impl Scoper {
|
|
|
893
966
|
}
|
|
894
967
|
|
|
895
968
|
fn expr(&mut self, e: &mut Expr) {
|
|
969
|
+
// a call by a bound name is a closure call: it follows its binder
|
|
970
|
+
if let ExprKind::FnCall(c) = &mut e.kind {
|
|
971
|
+
self.lookup(&mut c.name);
|
|
972
|
+
}
|
|
896
973
|
match &mut e.kind {
|
|
897
974
|
ExprKind::Var(n) => self.lookup(n),
|
|
898
975
|
ExprKind::Bind(b) => {
|
compiler/core/src/trmc.rs
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
//! variant and a nullary variant, with exactly one recursive field; fields after
|
|
24
24
|
//! the recursive one must be plain variables or literals. Nothing under
|
|
25
25
|
//! `?`/`!` or closures, no other `return`s outside tail
|
|
26
|
-
//! position.
|
|
26
|
+
//! position. No binder named like the fn: its call would not be a self call.
|
|
27
27
|
use crate::ast::*;
|
|
28
28
|
use crate::generics::Scope;
|
|
29
29
|
use crate::rename::substType;
|
|
@@ -108,6 +108,9 @@ fn rewriteFn(f: &Fn, enums: &[Enum]) -> Option<Fn> {
|
|
|
108
108
|
}) {
|
|
109
109
|
return None;
|
|
110
110
|
}
|
|
111
|
+
if !f.has_self && crate::simplify::boundNames(&f.params, &f.body).contains(&f.name) {
|
|
112
|
+
return None;
|
|
113
|
+
}
|
|
111
114
|
let ret = f.returns.clone()?;
|
|
112
115
|
let owner = enums.iter().find(|e| e.name == ret.name && !e.isStruct())?;
|
|
113
116
|
let nullary = owner
|
compiler/core/src/uniqueness.rs
CHANGED
|
@@ -6,13 +6,37 @@
|
|
|
6
6
|
//! field read `v.f`, the spread itself in tail position / assigned straight back to
|
|
7
7
|
//! `v`, or a hand-over `g(v)` (tail position or `v = g(v)`) to a unique parameter, and
|
|
8
8
|
//! `v` is never captured by a closure, stored, aliased or matched into a binding.
|
|
9
|
+
//! * A *parameter* of a free fn or of a method (methods are resolved by name, to every method
|
|
10
|
+
//! of that name; a name that is also a fn-typed field or a local/parameter is not resolved)
|
|
9
|
-
//!
|
|
11
|
+
//! is unique when the fn is only ever called directly and
|
|
10
12
|
//! every call site passes a unique value: a fresh expression, a unique local/param
|
|
11
13
|
//! handed over as above, or `x.f` in `x.f = g(x.f)` when field `f` is itself unique.
|
|
12
14
|
//! * A field `f` is unique when every write to it is fresh, every read of it is `x.f.h`,
|
|
13
15
|
//! `x.f.h = ..` or the assign-back argument above, no pattern binds it, and no spread
|
|
14
16
|
//! that is not itself proved copies it.
|
|
15
17
|
//!
|
|
18
|
+
//! * A fn may also return one of its own parameters (`fun act(self, n) = ..; n`). Its result
|
|
19
|
+
//! then counts as fresh to a caller only when the argument handed in for that parameter is
|
|
20
|
+
//! itself fresh, or is the very variable the result is assigned back to (`v = g(v)`), and
|
|
21
|
+
//! the parameter is unique and never stored inside the callee. A callee reached with one
|
|
22
|
+
//! unique and one shared argument is not unique: there are no per-call-site copies, and
|
|
23
|
+
//! `--explain-fusion` says which call blocked it.
|
|
24
|
+
//!
|
|
25
|
+
//! * A method whose `self` is only read (`ro`: field reads, field writes, calls of other `ro`
|
|
26
|
+
//! methods; never returned, stored, passed on or captured) does not block a receiver. What
|
|
27
|
+
//! it returns of `self`'s fields is the field analysis' business, not the receiver's.
|
|
28
|
+
//!
|
|
29
|
+
//! * `Array.with` is a copy unless a call is marked `FnCall.unique` (the checker then runs
|
|
30
|
+
//! `Array.withUnique`, which writes `self`). A call is marked when its receiver is unshared
|
|
31
|
+
//! and dead afterwards: a fresh array (`Array.with`, `push`, `resize`, `init`, `filled` count
|
|
32
|
+
//! as fresh), a unique local or parameter the call consumes (`v = v.with(..)`, or the returned
|
|
33
|
+
//! value), a unique field rebuilt by `x.f = x.f.with(..)`, or `T(..s, f: s.f.with(..))` of a
|
|
34
|
+
//! proved `s`, provided no other field of the spread touches `s` except by plain reads of
|
|
35
|
+
//! other fields (a method call or closure on `s` could read `s.f` after the write).
|
|
36
|
+
//!
|
|
37
|
+
//! * Methods the compiler calls without a visible call site (`IMPLICIT`: interpolation, hashing,
|
|
38
|
+
//! operators) are never proved: their callers are unknown.
|
|
39
|
+
//!
|
|
16
40
|
//! Parameters and fields are solved as a greatest fixpoint (assume unique, remove what
|
|
17
41
|
//! fails, repeat).
|
|
18
42
|
use crate::ast::*;
|
|
@@ -27,14 +51,14 @@ pub struct UniqueReport {
|
|
|
27
51
|
|
|
28
52
|
// ---------- traversal ----------
|
|
29
53
|
|
|
30
|
-
enum Node<'a> {
|
|
54
|
+
pub(crate) enum Node<'a> {
|
|
31
55
|
E(&'a Expr),
|
|
32
56
|
S(&'a Stmt),
|
|
33
57
|
/// A method call: the id of its enclosing `Attribute` expression, and the call.
|
|
34
58
|
M(NodeId, &'a FnCall),
|
|
35
59
|
}
|
|
36
60
|
|
|
37
|
-
type Visit<'a, 'b> = &'b mut dyn FnMut(Node<'a>) -> bool;
|
|
61
|
+
pub(crate) type Visit<'a, 'b> = &'b mut dyn FnMut(Node<'a>) -> bool;
|
|
38
62
|
|
|
39
63
|
fn argExpr(a: &Arg) -> &Expr {
|
|
40
64
|
match a {
|
|
@@ -43,13 +67,13 @@ fn argExpr(a: &Arg) -> &Expr {
|
|
|
43
67
|
}
|
|
44
68
|
}
|
|
45
69
|
|
|
46
|
-
fn walkBlock<'a>(b: &'a Block, f: Visit<'a, '_>) {
|
|
70
|
+
pub(crate) fn walkBlock<'a>(b: &'a Block, f: Visit<'a, '_>) {
|
|
47
71
|
for s in &b.stmts {
|
|
48
72
|
walkStmt(s, f);
|
|
49
73
|
}
|
|
50
74
|
}
|
|
51
75
|
|
|
52
|
-
fn walkStmt<'a>(s: &'a Stmt, f: Visit<'a, '_>) {
|
|
76
|
+
pub(crate) fn walkStmt<'a>(s: &'a Stmt, f: Visit<'a, '_>) {
|
|
53
77
|
if !f(Node::S(s)) {
|
|
54
78
|
return;
|
|
55
79
|
}
|
|
@@ -100,7 +124,7 @@ fn walkStmt<'a>(s: &'a Stmt, f: Visit<'a, '_>) {
|
|
|
100
124
|
}
|
|
101
125
|
}
|
|
102
126
|
|
|
103
|
-
fn walkExpr<'a>(e: &'a Expr, f: Visit<'a, '_>) {
|
|
127
|
+
pub(crate) fn walkExpr<'a>(e: &'a Expr, f: Visit<'a, '_>) {
|
|
104
128
|
if !f(Node::E(e)) {
|
|
105
129
|
return;
|
|
106
130
|
}
|
|
@@ -172,13 +196,13 @@ fn walkExpr<'a>(e: &'a Expr, f: Visit<'a, '_>) {
|
|
|
172
196
|
}
|
|
173
197
|
}
|
|
174
198
|
|
|
175
|
-
fn mutBlock(b: &mut Block, f: &mut dyn FnMut(&mut
|
|
199
|
+
pub(crate) fn mutBlock(b: &mut Block, f: &mut dyn FnMut(&mut Expr)) {
|
|
176
200
|
for s in &mut b.stmts {
|
|
177
201
|
mutStmt(s, f);
|
|
178
202
|
}
|
|
179
203
|
}
|
|
180
204
|
|
|
181
|
-
fn mutStmt(s: &mut Stmt, f: &mut dyn FnMut(&mut
|
|
205
|
+
fn mutStmt(s: &mut Stmt, f: &mut dyn FnMut(&mut Expr)) {
|
|
182
206
|
match &mut s.kind {
|
|
183
207
|
StmtKind::Assign(a) => {
|
|
184
208
|
for t in &mut a.targets {
|
|
@@ -226,7 +250,8 @@ fn mutStmt(s: &mut Stmt, f: &mut dyn FnMut(&mut ClassCall)) {
|
|
|
226
250
|
}
|
|
227
251
|
}
|
|
228
252
|
|
|
229
|
-
fn mutExpr(e: &mut Expr, f: &mut dyn FnMut(&mut
|
|
253
|
+
pub(crate) fn mutExpr(e: &mut Expr, f: &mut dyn FnMut(&mut Expr)) {
|
|
254
|
+
f(e);
|
|
230
255
|
match &mut e.kind {
|
|
231
256
|
ExprKind::Binary(b) => {
|
|
232
257
|
mutExpr(&mut b.left, f);
|
|
@@ -256,7 +281,6 @@ fn mutExpr(e: &mut Expr, f: &mut dyn FnMut(&mut ClassCall)) {
|
|
|
256
281
|
}
|
|
257
282
|
}
|
|
258
283
|
ExprKind::ClassCall(c) => {
|
|
259
|
-
f(c);
|
|
260
284
|
if let Some(s) = &mut c.spread {
|
|
261
285
|
mutExpr(s, f);
|
|
262
286
|
}
|
|
@@ -337,9 +361,12 @@ struct Site {
|
|
|
337
361
|
args: Vec<Option<Expr>>,
|
|
338
362
|
assignBack: Option<AssignTarget>,
|
|
339
363
|
inClosure: bool,
|
|
364
|
+
/// The call expression's id, and whether it is in tail position of its unit.
|
|
365
|
+
node: NodeId,
|
|
366
|
+
tail: bool,
|
|
340
367
|
}
|
|
341
368
|
|
|
342
|
-
fn isVar(e: &Expr, v: &str) -> bool {
|
|
369
|
+
pub(crate) fn isVar(e: &Expr, v: &str) -> bool {
|
|
343
370
|
varName(e) == Some(v)
|
|
344
371
|
}
|
|
345
372
|
|
|
@@ -353,8 +380,12 @@ fn varName(e: &Expr) -> Option<&str> {
|
|
|
353
380
|
}
|
|
354
381
|
|
|
355
382
|
/// Method names the compiler may call without a visible call site (interpolation,
|
|
356
|
-
/// constructors, hashing): their `self` and
|
|
383
|
+
/// constructors, hashing, the operators `a + b`, `-a`, `a == b`, `a < b`): their `self` and
|
|
384
|
+
/// parameters are never proved.
|
|
385
|
+
const IMPLICIT: [&str; 16] = [
|
|
357
|
-
|
|
386
|
+
"toStr", "init", "equals", "hash", "compare", "drop", "add", "sub", "mul", "div", "neg", "eq", "lt", "le", "gt",
|
|
387
|
+
"ge",
|
|
388
|
+
];
|
|
358
389
|
|
|
359
390
|
struct An {
|
|
360
391
|
units: Vec<Unit>,
|
|
@@ -365,15 +396,38 @@ struct An {
|
|
|
365
396
|
sites: Vec<Site>,
|
|
366
397
|
escaped: HashSet<usize>,
|
|
367
398
|
fresh_ret: RefCell<HashSet<usize>>,
|
|
399
|
+
/// Fns whose every result is fresh or one of their own parameters: the parameters they
|
|
400
|
+
/// may return (see the header).
|
|
401
|
+
passes: HashMap<usize, Vec<usize>>,
|
|
402
|
+
/// Field writes `o.f = g(..)` of a returning fn: the `(fn, parameter)` pairs that must stay
|
|
403
|
+
/// unique and unstored for field `f` to stay unique.
|
|
404
|
+
passWrites: Vec<(String, Vec<(usize, usize)>)>,
|
|
368
405
|
up: RefCell<HashSet<(usize, usize)>>,
|
|
369
406
|
upWhy: RefCell<HashMap<(usize, usize), String>>,
|
|
370
407
|
uf: RefCell<HashSet<String>>,
|
|
371
|
-
cache: RefCell<HashMap<(usize, String), Result<(), String>>>,
|
|
408
|
+
cache: RefCell<HashMap<(usize, String, bool, bool), Result<(), String>>>,
|
|
372
409
|
// static field facts
|
|
373
410
|
badField: HashSet<String>,
|
|
374
411
|
patternBound: HashSet<String>,
|
|
375
412
|
poisonAll: bool,
|
|
376
413
|
spreads: Vec<SpreadSite>,
|
|
414
|
+
/// Methods whose `self` is only read: used as the object of field reads and field writes, or
|
|
415
|
+
/// as the receiver of other such methods, never returned, stored, passed on or captured.
|
|
416
|
+
ro: HashSet<usize>,
|
|
417
|
+
/// `Array.with` (the only unit a marked `with` call runs; its copying body is the default).
|
|
418
|
+
arrayWith: HashSet<usize>,
|
|
419
|
+
/// `Array` methods that always return a new array (or, marked, the unique receiver).
|
|
420
|
+
arrayFresh: HashSet<usize>,
|
|
421
|
+
/// `T(..s, f: s.f.with(..))`, by the id of the `with` call.
|
|
422
|
+
spreadWith: HashMap<NodeId, SpreadWith>,
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
struct SpreadWith {
|
|
426
|
+
unit: usize,
|
|
427
|
+
var: String,
|
|
428
|
+
field: String,
|
|
429
|
+
/// No other field value of the spread reads `s.f`, so the write cannot be observed early.
|
|
430
|
+
clean: bool,
|
|
377
431
|
}
|
|
378
432
|
|
|
379
433
|
struct SpreadSite {
|
|
@@ -381,9 +435,12 @@ struct SpreadSite {
|
|
|
381
435
|
typeName: String,
|
|
382
436
|
var: Option<String>,
|
|
383
437
|
given: Vec<String>,
|
|
438
|
+
/// Given fields whose value is a call returning (one of) its own parameters, fed from the
|
|
439
|
+
/// source's own field: they alias the source's field unless the spread is proved.
|
|
440
|
+
passGiven: Vec<String>,
|
|
384
441
|
}
|
|
385
442
|
|
|
386
|
-
fn collectTails<'a>(b: &'a Block, out: &mut Vec<&'a Expr>) -> bool {
|
|
443
|
+
pub(crate) fn collectTails<'a>(b: &'a Block, out: &mut Vec<&'a Expr>) -> bool {
|
|
387
444
|
match b.stmts.last() {
|
|
388
445
|
Some(Stmt { kind: StmtKind::Expr(e, _, _), .. }) | Some(Stmt { kind: StmtKind::Return(Some(e)), .. }) => {
|
|
389
446
|
out.push(e);
|
|
@@ -428,6 +485,103 @@ impl An {
|
|
|
428
485
|
self.methodIdx.get(name).map(|v| v.as_slice()).unwrap_or(&[])
|
|
429
486
|
}
|
|
430
487
|
|
|
488
|
+
/// The arguments of a call of `g`, one per parameter (`None` = defaulted or missing).
|
|
489
|
+
fn argsFor<'e>(&self, g: usize, recv: Option<&'e Expr>, args: &'e [Arg]) -> Vec<Option<&'e Expr>> {
|
|
490
|
+
let u = &self.units[g];
|
|
491
|
+
let mut out: Vec<Option<&Expr>> = vec![None; u.params.len()];
|
|
492
|
+
let mut pos = 0;
|
|
493
|
+
if u.hasSelf {
|
|
494
|
+
out[0] = recv;
|
|
495
|
+
pos = 1;
|
|
496
|
+
}
|
|
497
|
+
for arg in args {
|
|
498
|
+
match arg {
|
|
499
|
+
Arg::Positional(x) => {
|
|
500
|
+
if pos < out.len() {
|
|
501
|
+
out[pos] = Some(x);
|
|
502
|
+
}
|
|
503
|
+
pos += 1;
|
|
504
|
+
}
|
|
505
|
+
Arg::Keyword { name, value } => {
|
|
506
|
+
if let Some(j) = u.params.iter().position(|p| &p.name == name) {
|
|
507
|
+
out[j] = Some(value);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
out
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/// Every unit a call may run, with its arguments. `None` when the call is not a direct
|
|
516
|
+
/// call of known fns (a closure, a builtin, a static/instance mismatch).
|
|
517
|
+
fn callTargets<'e>(&self, e: &'e Expr) -> Option<Vec<(usize, Vec<Option<&'e Expr>>)>> {
|
|
518
|
+
match &e.kind {
|
|
519
|
+
ExprKind::FnCall(c) => {
|
|
520
|
+
let g = self.isFree(&c.name)?;
|
|
521
|
+
Some(vec![(g, self.argsFor(g, None, &c.args))])
|
|
522
|
+
}
|
|
523
|
+
ExprKind::Attribute(a) => {
|
|
524
|
+
let AttrKind::Method(c) = &a.attr else { return None };
|
|
525
|
+
let isType = matches!(a.object.kind, ExprKind::TypeName(_));
|
|
526
|
+
let cands = self.cands(&c.name);
|
|
527
|
+
if cands.is_empty() || cands.iter().any(|&g| self.units[g].hasSelf == isType) {
|
|
528
|
+
return None;
|
|
529
|
+
}
|
|
530
|
+
Some(cands.iter().map(|&g| (g, self.argsFor(g, Some(&a.object), &c.args))).collect())
|
|
531
|
+
}
|
|
532
|
+
_ => None,
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/// The `(fn, parameter)` pairs a returned-parameter call depends on, when `e` is a call
|
|
537
|
+
/// of fns that return fresh values or their own parameters and every returned parameter
|
|
538
|
+
/// gets a fresh argument or (`same`) the argument `same` accepts. `None` otherwise.
|
|
539
|
+
fn passDeps(&self, e: &Expr, same: &dyn std::ops::Fn(&Expr) -> bool) -> Option<Vec<(usize, usize)>> {
|
|
540
|
+
let targets = self.callTargets(e)?;
|
|
541
|
+
let mut deps = vec![];
|
|
542
|
+
for (g, args) in targets {
|
|
543
|
+
match self.passes.get(&g) {
|
|
544
|
+
None => {
|
|
545
|
+
if !self.fresh_ret.borrow().contains(&g) {
|
|
546
|
+
return None;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
Some(ps) => {
|
|
550
|
+
for &j in ps {
|
|
551
|
+
let arg = args.get(j).copied().flatten()?;
|
|
552
|
+
if !(self.fresh(arg) || same(arg)) {
|
|
553
|
+
return None;
|
|
554
|
+
}
|
|
555
|
+
deps.push((g, j));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
Some(deps)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/// Is parameter `j` of `g` unique and never stored or aliased inside `g`? For the receiver of a
|
|
564
|
+
/// method, whose callers are found by name alone (a call `x.m()` counts as a call of every
|
|
565
|
+
/// `m`, whatever `x`'s type), only this call's own argument matters: it is fresh or the
|
|
566
|
+
/// variable being rebound. A shared `arr.with(..)` elsewhere must not stop `Map.with`, which
|
|
567
|
+
/// returns its receiver, from being a harmless pass-through here.
|
|
568
|
+
fn passParamOk(&self, g: usize, j: usize) -> bool {
|
|
569
|
+
let u = &self.units[g];
|
|
570
|
+
let receiver = u.method && u.hasSelf && j == 0;
|
|
571
|
+
if receiver {
|
|
572
|
+
u.params.get(j).is_some_and(|p| p.plain) && self.varOkCached(g, &u.params[j].name, true, true).is_ok()
|
|
573
|
+
} else {
|
|
574
|
+
self.paramUnique(g, j) && self.varOkMode(g, &u.params[j].name, true).is_ok()
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/// May a unique value be handed to parameter `j` of `g`? Yes when every call of `g` passes a
|
|
579
|
+
/// unique value, or when `g` merely passes the parameter through (returns it, never stores
|
|
580
|
+
/// it): then this call's own unique argument is all that matters.
|
|
581
|
+
fn handOk(&self, g: usize, j: usize) -> bool {
|
|
582
|
+
self.paramUnique(g, j) || (self.passes.get(&g).is_some_and(|ps| ps.contains(&j)) && self.passParamOk(g, j))
|
|
583
|
+
}
|
|
584
|
+
|
|
431
585
|
/// The parameter of method `g` that argument `k` of `c` binds (`None` = the receiver).
|
|
432
586
|
fn methodParam(&self, g: usize, c: &FnCall, k: Option<usize>) -> Option<usize> {
|
|
433
587
|
let u = &self.units[g];
|
|
@@ -446,6 +600,11 @@ impl An {
|
|
|
446
600
|
}
|
|
447
601
|
|
|
448
602
|
fn paramUnique(&self, g: usize, j: usize) -> bool {
|
|
603
|
+
// `Array.with` copies unless a call is marked, and a call is marked only by its own proof:
|
|
604
|
+
// its receiver is always acceptable, the index and the value (stored) never are.
|
|
605
|
+
if self.arrayWith.contains(&g) {
|
|
606
|
+
return j == 0;
|
|
607
|
+
}
|
|
449
608
|
self.units[g].params.get(j).is_some() && self.up.borrow().contains(&(g, j))
|
|
450
609
|
}
|
|
451
610
|
|
|
@@ -461,16 +620,28 @@ impl An {
|
|
|
461
620
|
|
|
462
621
|
/// Is `v` (a local or parameter of `unit`) provably unshared at every spread of it?
|
|
463
622
|
fn varOk(&self, unit: usize, v: &str) -> Result<(), String> {
|
|
623
|
+
self.varOkMode(unit, v, false)
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/// `strict`: a binding of `v` must be a plainly fresh value (no returned-parameter
|
|
627
|
+
/// calls). The check of a callee's parameter runs strict, so checks never nest.
|
|
628
|
+
fn varOkMode(&self, unit: usize, v: &str, strict: bool) -> Result<(), String> {
|
|
629
|
+
self.varOkCached(unit, v, strict, false)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/// `entryUnique`: a parameter is taken to arrive unique (the caller under analysis passes a
|
|
633
|
+
/// unique value) instead of requiring every call of the fn to.
|
|
634
|
+
fn varOkCached(&self, unit: usize, v: &str, strict: bool, entryUnique: bool) -> Result<(), String> {
|
|
464
|
-
let key = (unit, v.to_string());
|
|
635
|
+
let key = (unit, v.to_string(), strict, entryUnique);
|
|
465
636
|
if let Some(r) = self.cache.borrow().get(&key) {
|
|
466
637
|
return r.clone();
|
|
467
638
|
}
|
|
468
|
-
let r = self.varOkRaw(unit, v);
|
|
639
|
+
let r = self.varOkRaw(unit, v, strict, entryUnique);
|
|
469
640
|
self.cache.borrow_mut().insert(key, r.clone());
|
|
470
641
|
r
|
|
471
642
|
}
|
|
472
643
|
|
|
473
|
-
fn varOkRaw(&self, unit: usize, v: &str) -> Result<(), String> {
|
|
644
|
+
fn varOkRaw(&self, unit: usize, v: &str, strict: bool, entryUnique: bool) -> Result<(), String> {
|
|
474
645
|
let u = &self.units[unit];
|
|
475
646
|
let param = u.params.iter().position(|p| p.name == v);
|
|
476
647
|
if let Some(i) = param {
|
|
@@ -480,12 +651,12 @@ impl An {
|
|
|
480
651
|
if !u.params[i].plain {
|
|
481
652
|
return Err(format!("{v} is a fn-typed parameter"));
|
|
482
653
|
}
|
|
483
|
-
if !self.paramUnique(unit, i) {
|
|
654
|
+
if !entryUnique && !self.paramUnique(unit, i) {
|
|
484
655
|
let why = self.upWhy.borrow().get(&(unit, i)).cloned();
|
|
485
656
|
return Err(why.unwrap_or_else(|| format!("a caller of {} may pass a shared {v}", u.name)));
|
|
486
657
|
}
|
|
487
658
|
}
|
|
488
|
-
let mut s = Scan { an: self, v, boundDepth: if param.is_some() { Some(0) } else { None }, err: None };
|
|
659
|
+
let mut s = Scan { an: self, v, strict, boundDepth: if param.is_some() { Some(0) } else { None }, err: None };
|
|
489
660
|
s.block(&u.block, true, 0);
|
|
490
661
|
if let Some(e) = s.err {
|
|
491
662
|
return Err(e);
|
|
@@ -496,6 +667,50 @@ impl An {
|
|
|
496
667
|
Ok(())
|
|
497
668
|
}
|
|
498
669
|
|
|
670
|
+
/// May this `Array.with` call write its receiver in place? The receiver must be unshared and
|
|
671
|
+
/// dead afterwards: a fresh array, a unique local or parameter that the call consumes (the
|
|
672
|
+
/// value returned, or rebound by `v = v.with(..)`), the field of a unique field rebuilt by
|
|
673
|
+
/// `x.f = x.f.with(..)`, or `T(..s, f: s.f.with(..))` of a unique `s`.
|
|
674
|
+
fn withOk(&self, site: &Site) -> Result<(), String> {
|
|
675
|
+
if site.inClosure {
|
|
676
|
+
return Err("inside a closure".to_string());
|
|
677
|
+
}
|
|
678
|
+
let Some(Some(recv)) = site.args.first() else { return Err("no receiver".to_string()) };
|
|
679
|
+
if self.fresh(recv) {
|
|
680
|
+
return Ok(());
|
|
681
|
+
}
|
|
682
|
+
match &recv.kind {
|
|
683
|
+
ExprKind::Var(_) | ExprKind::Self_ => {
|
|
684
|
+
let v = varName(recv).unwrap_or("");
|
|
685
|
+
self.varOk(site.caller, v)?;
|
|
686
|
+
let consumed = site.tail || matches!(&site.assignBack, Some(AssignTarget::Var(t)) if t == v);
|
|
687
|
+
if consumed {
|
|
688
|
+
Ok(())
|
|
689
|
+
} else {
|
|
690
|
+
Err(format!("{v} is used after the write"))
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
ExprKind::Attribute(a) => {
|
|
694
|
+
let AttrKind::Field(f) = &a.attr else { return Err("receiver is a method result".to_string()) };
|
|
695
|
+
match self.spreadWith.get(&site.node) {
|
|
696
|
+
Some(sw) => {
|
|
697
|
+
if !sw.clean {
|
|
698
|
+
return Err(format!("another field of the spread reads {f}"));
|
|
699
|
+
}
|
|
700
|
+
self.varOk(sw.unit, &sw.var)?;
|
|
701
|
+
if self.uf.borrow().contains(&sw.field) {
|
|
702
|
+
Ok(())
|
|
703
|
+
} else {
|
|
704
|
+
Err(format!("field {f} may be shared"))
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
None => self.argOk(site, recv),
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
_ => Err("receiver may be shared".to_string()),
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
499
714
|
fn spreadProved(&self, s: &SpreadSite) -> bool {
|
|
500
715
|
match &s.var {
|
|
501
716
|
Some(v) => self.varOk(s.unit, v).is_ok(),
|
|
@@ -544,6 +759,7 @@ impl An {
|
|
|
544
759
|
struct Scan<'a> {
|
|
545
760
|
an: &'a An,
|
|
546
761
|
v: &'a str,
|
|
762
|
+
strict: bool,
|
|
547
763
|
boundDepth: Option<usize>,
|
|
548
764
|
err: Option<String>,
|
|
549
765
|
}
|
|
@@ -575,6 +791,28 @@ impl<'a> Scan<'a> {
|
|
|
575
791
|
}
|
|
576
792
|
}
|
|
577
793
|
|
|
794
|
+
/// May `v` be bound to `val`? A fresh value, or (outside strict mode) the result of a fn
|
|
795
|
+
/// returning a parameter we hand it unique: a fresh argument, or `v` itself in `v = g(v)`.
|
|
796
|
+
fn bindingOk(&self, val: &Expr, declare: bool) -> bool {
|
|
797
|
+
if self.an.fresh(val) {
|
|
798
|
+
return true;
|
|
799
|
+
}
|
|
800
|
+
if self.strict {
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
803
|
+
let v = self.v;
|
|
804
|
+
let same = |a: &Expr| !declare && isVar(a, v);
|
|
805
|
+
match self.an.passDeps(val, &same) {
|
|
806
|
+
Some(deps) => deps.iter().all(|&(g, j)| self.an.passParamOk(g, j)),
|
|
807
|
+
None => false,
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/// May a hand-over `v = g(v)` to `g` rebind `v` to its result?
|
|
812
|
+
fn resultOk(&self, g: usize) -> bool {
|
|
813
|
+
self.an.fresh_ret.borrow().contains(&g) || (!self.strict && self.an.passes.contains_key(&g))
|
|
814
|
+
}
|
|
815
|
+
|
|
578
816
|
fn escapes(&self) -> String {
|
|
579
817
|
format!("{} escapes while still live", self.v)
|
|
580
818
|
}
|
|
@@ -606,7 +844,7 @@ impl<'a> Scan<'a> {
|
|
|
606
844
|
Some(_) => self.fail(format!("{v} shadowed in an inner scope")),
|
|
607
845
|
}
|
|
608
846
|
}
|
|
609
|
-
if !self.
|
|
847
|
+
if !self.bindingOk(val, a.declare) {
|
|
610
848
|
self.fail(format!("{v} bound to a value that may be shared"));
|
|
611
849
|
} else {
|
|
612
850
|
self.expr(val, false, true);
|
|
@@ -685,8 +923,8 @@ impl<'a> Scan<'a> {
|
|
|
685
923
|
let ok = bareCount == 1
|
|
686
924
|
&& (tail || ab)
|
|
687
925
|
&& g.is_some_and(|g| {
|
|
688
|
-
self.an.paramPos(g, c, k).is_some_and(|j| self.an.
|
|
926
|
+
self.an.paramPos(g, c, k).is_some_and(|j| self.an.handOk(g, j))
|
|
689
|
-
&& (!ab || self.
|
|
927
|
+
&& (!ab || self.resultOk(g))
|
|
690
928
|
});
|
|
691
929
|
if !ok {
|
|
692
930
|
self.fail(format!("{v} passed to {} without being dead afterwards", c.name));
|
|
@@ -709,12 +947,13 @@ impl<'a> Scan<'a> {
|
|
|
709
947
|
&& (tail || ab)
|
|
710
948
|
&& !cands.is_empty()
|
|
711
949
|
&& cands.iter().all(|&g| {
|
|
712
|
-
s.an.methodParam(g, c, k).is_some_and(|j| s.an.
|
|
950
|
+
s.an.methodParam(g, c, k).is_some_and(|j| s.an.handOk(g, j))
|
|
713
|
-
&& (!ab || s.
|
|
951
|
+
&& (!ab || s.resultOk(g))
|
|
714
952
|
})
|
|
715
953
|
};
|
|
954
|
+
let readOnly = recvBare && !cands.is_empty() && cands.iter().all(|g| self.an.ro.contains(g));
|
|
716
955
|
if recvBare {
|
|
717
|
-
if !handOver(self, None) {
|
|
956
|
+
if !readOnly && !handOver(self, None) {
|
|
718
957
|
self.fail(format!("{v} passed as self to {} without being dead afterwards", c.name));
|
|
719
958
|
}
|
|
720
959
|
} else {
|
|
@@ -853,13 +1092,38 @@ pub fn markUnique(src: &mut Source) -> UniqueReport {
|
|
|
853
1092
|
}
|
|
854
1093
|
}
|
|
855
1094
|
}
|
|
1095
|
+
// `Array.with` calls whose receiver is proved unique and dead afterwards.
|
|
1096
|
+
let mut withMarks: HashSet<NodeId> = HashSet::new();
|
|
1097
|
+
for site in an.sites.iter().filter(|s| an.arrayWith.contains(&s.callee)) {
|
|
1098
|
+
let u = &an.units[site.caller];
|
|
1099
|
+
if u.kind == Kind::Const {
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1102
|
+
let label = format!("{}:{} (array write)", u.label, lineOf(src, u));
|
|
1103
|
+
match an.withOk(site) {
|
|
1104
|
+
Ok(()) => {
|
|
1105
|
+
withMarks.insert(site.node);
|
|
1106
|
+
report.proved.push(label);
|
|
1107
|
+
}
|
|
1108
|
+
Err(r) => report.unproved.push((label, r)),
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
856
1111
|
// Mutable pass.
|
|
857
1112
|
for (ui, u) in an.units.iter().enumerate() {
|
|
858
1113
|
if u.kind == Kind::Const {
|
|
859
1114
|
continue;
|
|
860
1115
|
}
|
|
861
1116
|
let ok = &okVars[ui];
|
|
862
|
-
let mut mark = |
|
|
1117
|
+
let mut mark = |e: &mut Expr| {
|
|
1118
|
+
if withMarks.contains(&e.id) {
|
|
1119
|
+
if let ExprKind::Attribute(a) = &mut e.kind {
|
|
1120
|
+
if let AttrKind::Method(c) = &mut a.attr {
|
|
1121
|
+
c.unique = true;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
let ExprKind::ClassCall(c) = &mut e.kind else { return };
|
|
863
1127
|
if let Some(s) = &c.spread {
|
|
864
1128
|
if let Some(v) = varName(s) {
|
|
865
1129
|
if ok.contains(v) && an.varOk(ui, v).is_ok() {
|
|
@@ -894,9 +1158,19 @@ fn build(src: &Source) -> An {
|
|
|
894
1158
|
let mut units: Vec<Unit> = Vec::new();
|
|
895
1159
|
let mut enums: HashMap<String, Vec<String>> = HashMap::new();
|
|
896
1160
|
let mut nameCount: HashMap<String, usize> = HashMap::new();
|
|
1161
|
+
// Fields that may hold a closure (`f: fn(..)`, or a bare type parameter): `x.f(..)` can be a
|
|
1162
|
+
// closure call, so a method of the same name is not resolvable.
|
|
1163
|
+
let mut callable: HashSet<String> = HashSet::new();
|
|
897
1164
|
for (idx, item) in src.items.iter().enumerate() {
|
|
898
1165
|
match item {
|
|
899
1166
|
Item::Enum(e) => {
|
|
1167
|
+
for v in &e.variants {
|
|
1168
|
+
for (t, n) in v.fields.iter().zip(&v.field_names) {
|
|
1169
|
+
if t.name == FN_TYPE_NAME || e.generics.iter().any(|g| g.name == t.name) {
|
|
1170
|
+
callable.insert(n.clone());
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
900
1174
|
let mut names: Vec<String> = Vec::new();
|
|
901
1175
|
for v in &e.variants {
|
|
902
1176
|
names.extend(v.field_names.iter().cloned());
|
|
@@ -963,16 +1237,50 @@ fn build(src: &Source) -> An {
|
|
|
963
1237
|
}),
|
|
964
1238
|
}
|
|
965
1239
|
}
|
|
1240
|
+
// A local, parameter, closure parameter or pattern binder named like a free fn shadows it:
|
|
1241
|
+
// a call `f(..)` may then be a closure call, so such names are not resolvable.
|
|
1242
|
+
let mut bound: HashSet<String> = HashSet::new();
|
|
1243
|
+
for u in &units {
|
|
1244
|
+
bound.extend(u.params.iter().filter(|p| !p.plain).map(|p| p.name.clone()));
|
|
1245
|
+
let pats = |ps: &[CasePattern], bound: &mut HashSet<String>| {
|
|
1246
|
+
let mut names = vec![];
|
|
1247
|
+
ps.iter().for_each(|p| crate::visit::patternBinders(p, &mut names));
|
|
1248
|
+
bound.extend(names);
|
|
1249
|
+
};
|
|
1250
|
+
let mut found: Vec<String> = vec![];
|
|
1251
|
+
walkBlock(&u.block, &mut |n| {
|
|
1252
|
+
match n {
|
|
1253
|
+
Node::S(Stmt { kind: StmtKind::Assign(a), .. }) => {
|
|
1254
|
+
for t in &a.targets {
|
|
1255
|
+
if let AssignTarget::Var(v) = t {
|
|
1256
|
+
found.push(v.clone());
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
Node::S(Stmt { kind: StmtKind::Match(m), .. }) => {
|
|
1261
|
+
for c in &m.cases {
|
|
1262
|
+
pats(&c.patterns, &mut bound);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
Node::S(Stmt { kind: StmtKind::ForLoop(l), .. }) => found.extend(l.binders.iter().cloned()),
|
|
1266
|
+
Node::E(Expr { kind: ExprKind::Closure(c), .. }) => found.extend(c.params.iter().cloned()),
|
|
1267
|
+
Node::E(Expr { kind: ExprKind::Bind(b), .. }) => pats(&b.case.patterns, &mut bound),
|
|
1268
|
+
_ => {}
|
|
1269
|
+
}
|
|
1270
|
+
true
|
|
1271
|
+
});
|
|
1272
|
+
bound.extend(found);
|
|
1273
|
+
}
|
|
966
1274
|
// Names with duplicates are not resolvable.
|
|
967
1275
|
let mut freeIdx = HashMap::new();
|
|
968
1276
|
for (i, u) in units.iter().enumerate() {
|
|
969
|
-
if u.kind == Kind::Fn && u.free && nameCount.get(&u.name) == Some(&1) {
|
|
1277
|
+
if u.kind == Kind::Fn && u.free && nameCount.get(&u.name) == Some(&1) && !bound.contains(&u.name) {
|
|
970
1278
|
freeIdx.insert(u.name.clone(), i);
|
|
971
1279
|
}
|
|
972
1280
|
}
|
|
973
1281
|
let mut methodIdx: HashMap<String, Vec<usize>> = HashMap::new();
|
|
974
1282
|
for (i, u) in units.iter().enumerate() {
|
|
975
|
-
if u.kind == Kind::Fn && u.method {
|
|
1283
|
+
if u.kind == Kind::Fn && u.method && !callable.contains(&u.name) {
|
|
976
1284
|
methodIdx.entry(u.name.clone()).or_default().push(i);
|
|
977
1285
|
}
|
|
978
1286
|
}
|
|
@@ -984,6 +1292,8 @@ fn build(src: &Source) -> An {
|
|
|
984
1292
|
sites: vec![],
|
|
985
1293
|
escaped: HashSet::new(),
|
|
986
1294
|
fresh_ret: RefCell::new(HashSet::new()),
|
|
1295
|
+
passes: HashMap::new(),
|
|
1296
|
+
passWrites: vec![],
|
|
987
1297
|
up: RefCell::new(HashSet::new()),
|
|
988
1298
|
upWhy: RefCell::new(HashMap::new()),
|
|
989
1299
|
uf: RefCell::new(HashSet::new()),
|
|
@@ -992,7 +1302,24 @@ fn build(src: &Source) -> An {
|
|
|
992
1302
|
patternBound: HashSet::new(),
|
|
993
1303
|
poisonAll: false,
|
|
994
1304
|
spreads: vec![],
|
|
1305
|
+
ro: HashSet::new(),
|
|
1306
|
+
arrayWith: HashSet::new(),
|
|
1307
|
+
arrayFresh: HashSet::new(),
|
|
1308
|
+
spreadWith: HashMap::new(),
|
|
995
1309
|
};
|
|
1310
|
+
for (i, u) in an.units.iter().enumerate() {
|
|
1311
|
+
match u.label.as_str() {
|
|
1312
|
+
"Array.with" => {
|
|
1313
|
+
an.arrayWith.insert(i);
|
|
1314
|
+
an.arrayFresh.insert(i);
|
|
1315
|
+
}
|
|
1316
|
+
"Array.withUnique" | "Array.push" | "Array.resize" | "Array.init" | "Array.filled" => {
|
|
1317
|
+
an.arrayFresh.insert(i);
|
|
1318
|
+
}
|
|
1319
|
+
_ => {}
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
an.ro = readOnlySelf(&an);
|
|
996
1323
|
// Fresh-return fns: greatest fixpoint.
|
|
997
1324
|
let mut fr: HashSet<usize> = an.freeIdx.values().copied().collect();
|
|
998
1325
|
fr.extend(an.methodIdx.values().flatten().copied());
|
|
@@ -1000,6 +1327,9 @@ fn build(src: &Source) -> An {
|
|
|
1000
1327
|
*an.fresh_ret.borrow_mut() = fr.clone();
|
|
1001
1328
|
let mut drop = vec![];
|
|
1002
1329
|
for &g in &fr {
|
|
1330
|
+
if an.arrayFresh.contains(&g) {
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1003
1333
|
let b = &an.units[g].block;
|
|
1004
1334
|
let mut tails = vec![];
|
|
1005
1335
|
let mut ok = collectTails(b, &mut tails);
|
|
@@ -1024,6 +1354,45 @@ fn build(src: &Source) -> An {
|
|
|
1024
1354
|
}
|
|
1025
1355
|
}
|
|
1026
1356
|
*an.fresh_ret.borrow_mut() = fr;
|
|
1357
|
+
// Fns that may return one of their own parameters: every result is fresh or such a parameter.
|
|
1358
|
+
let mut passes: HashMap<usize, Vec<usize>> = HashMap::new();
|
|
1359
|
+
let mut order: Vec<usize> = an.freeIdx.values().copied().chain(an.methodIdx.values().flatten().copied()).collect();
|
|
1360
|
+
order.sort();
|
|
1361
|
+
for g in order {
|
|
1362
|
+
if an.fresh_ret.borrow().contains(&g) {
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
let u = &an.units[g];
|
|
1366
|
+
let mut tails = vec![];
|
|
1367
|
+
let mut ok = collectTails(&u.block, &mut tails);
|
|
1368
|
+
walkBlock(&u.block, &mut |n| {
|
|
1369
|
+
match n {
|
|
1370
|
+
Node::S(Stmt { kind: StmtKind::Return(Some(e)), .. }) => tails.push(e),
|
|
1371
|
+
Node::E(Expr { kind: ExprKind::Closure(_), .. }) => return false,
|
|
1372
|
+
_ => {}
|
|
1373
|
+
}
|
|
1374
|
+
true
|
|
1375
|
+
});
|
|
1376
|
+
let mut ps: Vec<usize> = vec![];
|
|
1377
|
+
for e in tails {
|
|
1378
|
+
if an.fresh(e) {
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1381
|
+
match varName(e).and_then(|n| u.params.iter().position(|p| p.name == n && p.plain)) {
|
|
1382
|
+
Some(j) => {
|
|
1383
|
+
if !ps.contains(&j) {
|
|
1384
|
+
ps.push(j);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
None => ok = false,
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
if ok && !ps.is_empty() {
|
|
1391
|
+
ps.sort();
|
|
1392
|
+
passes.insert(g, ps);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
an.passes = passes;
|
|
1027
1396
|
collectFacts(&mut an);
|
|
1028
1397
|
an
|
|
1029
1398
|
}
|
|
@@ -1034,6 +1403,99 @@ fn nodeKey(id: NodeId) -> NodeId {
|
|
|
1034
1403
|
id
|
|
1035
1404
|
}
|
|
1036
1405
|
|
|
1406
|
+
/// Is `e` the field read `s.f` of the variable `s`?
|
|
1407
|
+
fn isFieldOf(e: &Expr, s: &str, f: &str) -> bool {
|
|
1408
|
+
matches!(&e.kind, ExprKind::Attribute(x) if x.attr == AttrKind::Field(f.to_string()) && isVar(&x.object, s))
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/// `s.f.with(..)`: the receiver `s.f` (and nothing else may be the call's object).
|
|
1412
|
+
fn rebuiltFromItself<'a>(value: &'a Expr, s: &str, f: &str) -> Option<&'a Expr> {
|
|
1413
|
+
let ExprKind::Attribute(x) = &value.kind else { return None };
|
|
1414
|
+
let AttrKind::Method(c) = &x.attr else { return None };
|
|
1415
|
+
(c.name == "with" && isFieldOf(&x.object, s, f)).then_some(&x.object)
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
/// Does `e` touch `s` only through plain field reads `s.g` with `g != f`? Any other use of `s`
|
|
1419
|
+
/// (a method call, even a read-only one, an argument, a capture) may read `s.f` through the
|
|
1420
|
+
/// array another field of the spread has just written in place.
|
|
1421
|
+
fn readsOnlyOtherFields(e: &Expr, s: &str, f: &str) -> bool {
|
|
1422
|
+
let (mut uses, mut plain) = (0usize, 0usize);
|
|
1423
|
+
walkExpr(e, &mut |n| {
|
|
1424
|
+
if let Node::E(x) = n {
|
|
1425
|
+
if isVar(x, s) {
|
|
1426
|
+
uses += 1;
|
|
1427
|
+
}
|
|
1428
|
+
if let ExprKind::Attribute(a) = &x.kind {
|
|
1429
|
+
if matches!(&a.attr, AttrKind::Field(g) if g != f) && isVar(&a.object, s) {
|
|
1430
|
+
plain += 1;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
true
|
|
1435
|
+
});
|
|
1436
|
+
uses == plain
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/// The methods whose `self` is only read (see `An.ro`): a greatest fixpoint, since a method may
|
|
1440
|
+
/// call another read-only one on `self`.
|
|
1441
|
+
fn readOnlySelf(an: &An) -> HashSet<usize> {
|
|
1442
|
+
let mut ro: HashSet<usize> =
|
|
1443
|
+
(0..an.units.len()).filter(|&g| an.units[g].kind == Kind::Fn && an.units[g].method && an.units[g].hasSelf).collect();
|
|
1444
|
+
loop {
|
|
1445
|
+
let mut drop = vec![];
|
|
1446
|
+
for &g in &ro {
|
|
1447
|
+
let mut allSelf: Vec<NodeId> = vec![];
|
|
1448
|
+
let mut allowed: HashSet<NodeId> = HashSet::new();
|
|
1449
|
+
let mut captured: HashSet<NodeId> = HashSet::new();
|
|
1450
|
+
walkBlock(&an.units[g].block, &mut |n| {
|
|
1451
|
+
match n {
|
|
1452
|
+
Node::E(e) => match &e.kind {
|
|
1453
|
+
ExprKind::Self_ => allSelf.push(e.id),
|
|
1454
|
+
ExprKind::Attribute(a) if matches!(a.object.kind, ExprKind::Self_) => match &a.attr {
|
|
1455
|
+
AttrKind::Field(_) => {
|
|
1456
|
+
allowed.insert(a.object.id);
|
|
1457
|
+
}
|
|
1458
|
+
AttrKind::Method(c) => {
|
|
1459
|
+
let cands = an.cands(&c.name);
|
|
1460
|
+
if !cands.is_empty() && cands.iter().all(|x| ro.contains(x)) {
|
|
1461
|
+
allowed.insert(a.object.id);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
},
|
|
1465
|
+
ExprKind::Closure(_) => walkExpr(e, &mut |m| {
|
|
1466
|
+
if let Node::E(Expr { kind: ExprKind::Self_, id, .. }) = m {
|
|
1467
|
+
captured.insert(*id);
|
|
1468
|
+
}
|
|
1469
|
+
true
|
|
1470
|
+
}),
|
|
1471
|
+
_ => {}
|
|
1472
|
+
},
|
|
1473
|
+
Node::S(Stmt { kind: StmtKind::Assign(asg), .. }) => {
|
|
1474
|
+
for t in &asg.targets {
|
|
1475
|
+
if let AssignTarget::Field(o, _) = t {
|
|
1476
|
+
if matches!(o.kind, ExprKind::Self_) {
|
|
1477
|
+
allowed.insert(o.id);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
_ => {}
|
|
1483
|
+
}
|
|
1484
|
+
true
|
|
1485
|
+
});
|
|
1486
|
+
if allSelf.iter().any(|id| !allowed.contains(id) || captured.contains(id)) {
|
|
1487
|
+
drop.push(g);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
if drop.is_empty() {
|
|
1491
|
+
return ro;
|
|
1492
|
+
}
|
|
1493
|
+
for g in drop {
|
|
1494
|
+
ro.remove(&g);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1037
1499
|
fn collectFacts(an: &mut An) {
|
|
1038
1500
|
let mut sites: Vec<Site> = vec![];
|
|
1039
1501
|
let mut escaped: HashSet<usize> = HashSet::new();
|
|
@@ -1041,10 +1503,24 @@ fn collectFacts(an: &mut An) {
|
|
|
1041
1503
|
let mut patternBound: HashSet<String> = HashSet::new();
|
|
1042
1504
|
let mut poisonAll = false;
|
|
1043
1505
|
let mut spreads: Vec<SpreadSite> = vec![];
|
|
1506
|
+
let mut passWrites: Vec<(String, Vec<(usize, usize)>)> = vec![];
|
|
1044
1507
|
let mut reads: Vec<(NodeId, String)> = vec![];
|
|
1045
1508
|
let mut allowed: HashSet<NodeId> = HashSet::new();
|
|
1509
|
+
let mut spreadWith: HashMap<NodeId, SpreadWith> = HashMap::new();
|
|
1046
1510
|
let a: &An = an;
|
|
1047
1511
|
for (ui, u) in a.units.iter().enumerate() {
|
|
1512
|
+
let mut tailIds: HashSet<NodeId> = HashSet::new();
|
|
1513
|
+
{
|
|
1514
|
+
let mut tails: Vec<&Expr> = vec![];
|
|
1515
|
+
collectTails(&u.block, &mut tails);
|
|
1516
|
+
walkBlock(&u.block, &mut |n| {
|
|
1517
|
+
if let Node::S(Stmt { kind: StmtKind::Return(Some(e)), .. }) = n {
|
|
1518
|
+
tails.push(e);
|
|
1519
|
+
}
|
|
1520
|
+
true
|
|
1521
|
+
});
|
|
1522
|
+
tailIds.extend(tails.iter().map(|e| e.id));
|
|
1523
|
+
}
|
|
1048
1524
|
let mut abMap: HashMap<NodeId, AssignTarget> = HashMap::new();
|
|
1049
1525
|
let mut inClosure: HashSet<NodeId> = HashSet::new();
|
|
1050
1526
|
// First pass: assign-back targets and closure membership.
|
|
@@ -1118,7 +1594,33 @@ fn collectFacts(an: &mut An) {
|
|
|
1118
1594
|
let single = asg.targets.len() == 1 && asg.values.len() == 1;
|
|
1119
1595
|
for (k, t) in asg.targets.iter().enumerate() {
|
|
1120
1596
|
if let AssignTarget::Field(o, f) = t {
|
|
1597
|
+
// `o.f = g(o.f)` where `g` returns that parameter keeps the field unique
|
|
1598
|
+
// while the parameter stays unique (checked in `solve`).
|
|
1599
|
+
let mut passOk = false;
|
|
1121
|
-
if
|
|
1600
|
+
if single && !a.fresh(&asg.values[0]) {
|
|
1601
|
+
let (cargs, recv): (Option<&Vec<Arg>>, Option<&Expr>) = match &asg.values[0].kind {
|
|
1602
|
+
ExprKind::FnCall(c) => (Some(&c.args), None),
|
|
1603
|
+
ExprKind::Attribute(x) => match &x.attr {
|
|
1604
|
+
AttrKind::Method(c) => (Some(&c.args), Some(&x.object)),
|
|
1605
|
+
_ => (None, None),
|
|
1606
|
+
},
|
|
1607
|
+
_ => (None, None),
|
|
1608
|
+
};
|
|
1609
|
+
if let Some(cargs) = cargs {
|
|
1610
|
+
let same = |ae: &Expr| {
|
|
1611
|
+
matches!(&ae.kind, ExprKind::Attribute(x) if x.attr == AttrKind::Field(f.clone()) && x.object == **o)
|
|
1612
|
+
};
|
|
1613
|
+
let n = cargs.iter().filter(|arg| same(argExpr(arg))).count()
|
|
1614
|
+
+ recv.is_some_and(|r| same(r)) as usize;
|
|
1615
|
+
if n <= 1 {
|
|
1616
|
+
if let Some(deps) = a.passDeps(&asg.values[0], &same) {
|
|
1617
|
+
passWrites.push((f.clone(), deps));
|
|
1618
|
+
passOk = true;
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
if !passOk && (!single || !a.fresh(&asg.values[k])) {
|
|
1122
1624
|
badField.insert(f.clone());
|
|
1123
1625
|
}
|
|
1124
1626
|
if matches!(&o.kind, ExprKind::Attribute(x) if matches!(x.attr, AttrKind::Field(_))) {
|
|
@@ -1156,6 +1658,8 @@ fn collectFacts(an: &mut An) {
|
|
|
1156
1658
|
ExprKind::ClassCall(cc) => {
|
|
1157
1659
|
let names = fieldsOf(&a.enums, &cc.type_name);
|
|
1158
1660
|
let mut given = vec![];
|
|
1661
|
+
let mut passGiven = vec![];
|
|
1662
|
+
let spreadVar = cc.spread.as_deref().and_then(varName);
|
|
1159
1663
|
for (k, fa) in cc.fields.iter().enumerate() {
|
|
1160
1664
|
let name = if fa.name.is_empty() {
|
|
1161
1665
|
names.as_ref().and_then(|n| n.get(k)).cloned()
|
|
@@ -1163,8 +1667,34 @@ fn collectFacts(an: &mut An) {
|
|
|
1163
1667
|
Some(fa.name.clone())
|
|
1164
1668
|
};
|
|
1165
1669
|
if let Some(nm) = name {
|
|
1670
|
+
// `T(..s, f: s.f.with(..))`: the field is rebuilt from itself.
|
|
1671
|
+
let sv = spreadVar.unwrap_or("");
|
|
1672
|
+
let rebuilt = spreadVar.and_then(|sv| rebuiltFromItself(&fa.value, sv, &nm));
|
|
1673
|
+
if let Some(recv) = rebuilt {
|
|
1674
|
+
allowed.insert(nodeKey(recv.id));
|
|
1675
|
+
// Only a spread of a unique source may write the array in place, and
|
|
1676
|
+
// no other field value may read `s.f` (it would see the write early).
|
|
1677
|
+
let clean = cc
|
|
1678
|
+
.fields
|
|
1679
|
+
.iter()
|
|
1680
|
+
.enumerate()
|
|
1681
|
+
.all(|(j, o)| j == k || readsOnlyOtherFields(&o.value, sv, &nm));
|
|
1682
|
+
spreadWith.insert(
|
|
1683
|
+
nodeKey(fa.value.id),
|
|
1684
|
+
SpreadWith { unit: ui, var: sv.to_string(), field: nm.clone(), clean },
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1166
1687
|
if !a.fresh(&fa.value) {
|
|
1688
|
+
let same = |ae: &Expr| isFieldOf(ae, sv, &nm);
|
|
1689
|
+
match rebuilt.and_then(|_| a.passDeps(&fa.value, &same)) {
|
|
1690
|
+
Some(deps) => {
|
|
1691
|
+
passWrites.push((nm.clone(), deps));
|
|
1692
|
+
passGiven.push(nm.clone());
|
|
1693
|
+
}
|
|
1694
|
+
None => {
|
|
1167
|
-
|
|
1695
|
+
badField.insert(nm.clone());
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1168
1698
|
}
|
|
1169
1699
|
given.push(nm);
|
|
1170
1700
|
}
|
|
@@ -1175,6 +1705,7 @@ fn collectFacts(an: &mut An) {
|
|
|
1175
1705
|
typeName: cc.type_name.clone(),
|
|
1176
1706
|
var: varName(s).map(|v| v.to_string()),
|
|
1177
1707
|
given,
|
|
1708
|
+
passGiven,
|
|
1178
1709
|
});
|
|
1179
1710
|
}
|
|
1180
1711
|
}
|
|
@@ -1225,13 +1756,26 @@ fn collectFacts(an: &mut An) {
|
|
|
1225
1756
|
_ => true,
|
|
1226
1757
|
}),
|
|
1227
1758
|
inClosure: inClosure.contains(&nodeKey(e.id)),
|
|
1759
|
+
node: e.id,
|
|
1760
|
+
tail: tailIds.contains(&e.id),
|
|
1228
1761
|
});
|
|
1229
1762
|
}
|
|
1763
|
+
// `x.f.m(..)` with every `m` reading its receiver only: `x.f` is not shared.
|
|
1764
|
+
if let ExprKind::Attribute(i) = &x.object.kind {
|
|
1765
|
+
let cands = a.cands(&c.name);
|
|
1766
|
+
if matches!(i.attr, AttrKind::Field(_))
|
|
1767
|
+
&& !cands.is_empty()
|
|
1768
|
+
&& cands.iter().all(|g| a.ro.contains(g))
|
|
1769
|
+
{
|
|
1770
|
+
allowed.insert(nodeKey(x.object.id));
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1230
1773
|
}
|
|
1231
1774
|
if let AttrKind::Field(f) = &x.attr {
|
|
1232
1775
|
reads.push((nodeKey(e.id), f.clone()));
|
|
1233
1776
|
}
|
|
1234
|
-
// `x.f.
|
|
1777
|
+
// `x.f.h` reads a field of the object in `x.f`: `x.f` itself is not a plain read. (`x.f.m()`
|
|
1778
|
+
// hands the field to a method and stays a plain read, so it makes `f` shared.)
|
|
1235
1779
|
if matches!(x.attr, AttrKind::Field(_))
|
|
1236
1780
|
&& matches!(&x.object.kind, ExprKind::Attribute(i) if matches!(i.attr, AttrKind::Field(_)))
|
|
1237
1781
|
{
|
|
@@ -1282,6 +1826,8 @@ fn collectFacts(an: &mut An) {
|
|
|
1282
1826
|
_ => true,
|
|
1283
1827
|
}),
|
|
1284
1828
|
inClosure: inClosure.contains(&nodeKey(e.id)),
|
|
1829
|
+
node: e.id,
|
|
1830
|
+
tail: tailIds.contains(&e.id),
|
|
1285
1831
|
});
|
|
1286
1832
|
}
|
|
1287
1833
|
}
|
|
@@ -1311,6 +1857,8 @@ fn collectFacts(an: &mut An) {
|
|
|
1311
1857
|
an.patternBound = patternBound;
|
|
1312
1858
|
an.poisonAll = poisonAll;
|
|
1313
1859
|
an.spreads = spreads;
|
|
1860
|
+
an.passWrites = passWrites;
|
|
1861
|
+
an.spreadWith = spreadWith;
|
|
1314
1862
|
}
|
|
1315
1863
|
|
|
1316
1864
|
fn solve(an: &An) {
|
|
@@ -1346,6 +1894,7 @@ fn solve(an: &An) {
|
|
|
1346
1894
|
let candidates: Vec<(usize, usize)> = an.up.borrow().iter().copied().collect();
|
|
1347
1895
|
for (g, j) in candidates {
|
|
1348
1896
|
let mut why: Option<String> = None;
|
|
1897
|
+
let mut okFrom: Option<usize> = None;
|
|
1349
1898
|
for site in an.sites.iter().filter(|s| s.callee == g) {
|
|
1350
1899
|
let r = match &site.args[j] {
|
|
1351
1900
|
Some(e) => an.argOk(site, e),
|
|
@@ -1354,11 +1903,20 @@ fn solve(an: &An) {
|
|
|
1354
1903
|
_ => Err("a call omits the argument".to_string()),
|
|
1355
1904
|
},
|
|
1356
1905
|
};
|
|
1906
|
+
match r {
|
|
1357
|
-
|
|
1907
|
+
Err(m) => {
|
|
1908
|
+
let from = &an.units[site.caller].label;
|
|
1909
|
+
if why.is_none() {
|
|
1358
|
-
|
|
1910
|
+
why = Some(format!("{} (in a call of {} from {})", m, an.units[g].name, from));
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1359
|
-
|
|
1913
|
+
Ok(()) => okFrom = okFrom.or(Some(site.caller)),
|
|
1360
1914
|
}
|
|
1361
1915
|
}
|
|
1916
|
+
// Mixed use: some call passes a unique value, another a shared one.
|
|
1917
|
+
if let (Some(w), Some(o)) = (&mut why, okFrom) {
|
|
1918
|
+
w.push_str(&format!("; a call from {} passes a unique value", an.units[o].label));
|
|
1919
|
+
}
|
|
1362
1920
|
if let Some(w) = why {
|
|
1363
1921
|
removeUp.push(((g, j), w));
|
|
1364
1922
|
}
|
|
@@ -1369,6 +1927,7 @@ fn solve(an: &An) {
|
|
|
1369
1927
|
if an.spreadProved(s) {
|
|
1370
1928
|
continue;
|
|
1371
1929
|
}
|
|
1930
|
+
poisoned.extend(s.passGiven.iter().cloned());
|
|
1372
1931
|
match fieldsOf(&an.enums, &s.typeName) {
|
|
1373
1932
|
Some(all) => poisoned.extend(all.into_iter().filter(|f| !s.given.contains(f))),
|
|
1374
1933
|
None => {
|
|
@@ -1377,6 +1936,11 @@ fn solve(an: &An) {
|
|
|
1377
1936
|
}
|
|
1378
1937
|
}
|
|
1379
1938
|
}
|
|
1939
|
+
for (f, deps) in &an.passWrites {
|
|
1940
|
+
if !deps.iter().all(|&(g, j)| an.passParamOk(g, j)) {
|
|
1941
|
+
poisoned.insert(f.clone());
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1380
1944
|
let removeUf: Vec<String> = an.uf.borrow().iter().filter(|f| poisoned.contains(*f)).cloned().collect();
|
|
1381
1945
|
if removeUp.is_empty() && removeUf.is_empty() {
|
|
1382
1946
|
break;
|
compiler/core/tests/core/purity_test.rs
CHANGED
|
@@ -127,3 +127,44 @@ fn fn_taking_primitive_io_is_not_pure() {
|
|
|
127
127
|
assert!(!pure.contains("touch"), "{pure:?}");
|
|
128
128
|
assert!(pure.contains("plus"), "{pure:?}");
|
|
129
129
|
}
|
|
130
|
+
|
|
131
|
+
#[test]
|
|
132
|
+
fn a_binder_named_like_a_pure_fn_is_an_unknown_callee() {
|
|
133
|
+
let s = format!(
|
|
134
|
+
"{P}enum W =\n | Wrap(g: fn(Int) -> Int)\n | No\n\nfun helper(x: Int) -> Int = x + 1\n\n\
|
|
135
|
+
fun viaParam(helper: fn(Int) -> Int) -> Int = helper(1)\n\n\
|
|
136
|
+
fun viaLocal(k: fn(Int) -> Int) -> Int =\n helper := k\n helper(1)\n\n\
|
|
137
|
+
fun viaPattern(w: W) -> Int = match w\n Wrap(helper) => helper(1)\n No => 0\n\n\
|
|
138
|
+
fun viaClosureParam() -> Int =\n k := |helper: Int| helper(1)\n 1\n\n\
|
|
139
|
+
fun free() -> Int = helper(1)\n"
|
|
140
|
+
);
|
|
141
|
+
let p = pure(&s);
|
|
142
|
+
assert_eq!(p.intersection(&set(&["viaParam", "viaLocal", "viaPattern", "viaClosureParam"])).count(), 0, "{p:?}");
|
|
143
|
+
assert!(p.contains("helper") && p.contains("free"), "{p:?}");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#[test]
|
|
147
|
+
fn a_write_through_a_binder_named_like_a_fresh_local_is_not_pure() {
|
|
148
|
+
let s = format!(
|
|
149
|
+
"{P}struct Cell(mut n: Int)\nenum H =\n | Holds(c: Cell)\n | No\n\n\
|
|
150
|
+
fun viaPattern(h: H) -> Int =\n c := Cell(n: 0)\n match h\n Holds(c) =>\n c.n = 1\n 1\n No => 0\n\n\
|
|
151
|
+
fun fresh() -> Int =\n c := Cell(n: 0)\n c.n = 1\n c.n\n"
|
|
152
|
+
);
|
|
153
|
+
let p = pure(&s);
|
|
154
|
+
assert!(!p.contains("viaPattern"), "{p:?}");
|
|
155
|
+
assert!(p.contains("fresh"), "{p:?}");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#[test]
|
|
159
|
+
fn a_call_of_an_fn_typed_field_is_not_a_method_of_that_name() {
|
|
160
|
+
let s = format!(
|
|
161
|
+
"{P}struct Rec(zap: fn(Int) -> Int)\nstruct Plain(label: Str) =\n fun zap(self, x: Int) -> Int = x + 1\n\n\
|
|
162
|
+
fun same(r: Rec) -> Rec = r\n\n\
|
|
163
|
+
fun typed(r: Rec) -> Int = r.zap(1)\n\n\
|
|
164
|
+
fun untyped(r: Rec) -> Int =\n q := same(r)\n q.zap(1)\n\n\
|
|
165
|
+
fun method(p: Plain) -> Int = p.zap(1)\n"
|
|
166
|
+
);
|
|
167
|
+
let p = pure(&s);
|
|
168
|
+
assert!(!p.contains("typed") && !p.contains("untyped"), "{p:?}");
|
|
169
|
+
assert!(p.contains("method"), "{p:?}");
|
|
170
|
+
}
|
compiler/core/tests/core/record_init_test.rs
CHANGED
|
@@ -62,7 +62,7 @@ fn ship_example_lowers() {
|
|
|
62
62
|
assert!(en.structInfo().unwrap().embedFields.is_empty());
|
|
63
63
|
let n = fnNamed(&s, "Ship__new").unwrap();
|
|
64
64
|
let names: Vec<_> = n.params.iter().map(|p| p.name.as_str()).collect();
|
|
65
|
-
assert_eq!(names, vec!["
|
|
65
|
+
assert_eq!(names, vec!["__p_x", "__p_y", "__p_tex"]);
|
|
66
66
|
assert!(fnNamed(&s, "init").is_none());
|
|
67
67
|
let main = fnNamed(&s, "main").unwrap();
|
|
68
68
|
let FnBody::Block(b) = &main.body else { panic!() };
|
compiler/core/tests/core/uniqueness_test.rs
CHANGED
|
@@ -169,13 +169,13 @@ fn self_spread_in_method_unique() {
|
|
|
169
169
|
#[test]
|
|
170
170
|
fn trait_dispatch_param_unique() {
|
|
171
171
|
// Two types share the method name; the call site resolves to both.
|
|
172
|
-
let s = "struct Cat(x: Int) =\n fun
|
|
172
|
+
let s = "struct Cat(x: Int) =\n fun shift(self, d: Int) -> Cat = Cat(..self, x: self.x + d)\n\
|
|
173
|
-
struct Dog(x: Int) =\n fun
|
|
173
|
+
struct Dog(x: Int) =\n fun shift(self, d: Int) -> Dog = Dog(..self, x: self.x + d)\n\
|
|
174
|
-
fun go() -> Cat =\n a := Cat(x: 0)\n a.
|
|
174
|
+
fun go() -> Cat =\n a := Cat(x: 0)\n a.shift(1)\n\
|
|
175
|
-
fun go2() -> Dog =\n b := Dog(x: 0)\n b = b.
|
|
175
|
+
fun go2() -> Dog =\n b := Dog(x: 0)\n b = b.shift(2)\n b\n";
|
|
176
176
|
let (_, r) = run(s);
|
|
177
|
-
assert!(proved(&r, "Cat.
|
|
177
|
+
assert!(proved(&r, "Cat.shift:2"), "{r:?}");
|
|
178
|
-
assert!(proved(&r, "Dog.
|
|
178
|
+
assert!(proved(&r, "Dog.shift:4"), "{r:?}");
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
#[test]
|
|
@@ -218,3 +218,263 @@ fn anyUniqueMethod(s: &Source, name: &str) -> bool {
|
|
|
218
218
|
}
|
|
219
219
|
found
|
|
220
220
|
}
|
|
221
|
+
|
|
222
|
+
// ---- methods, loops and returned parameters (T3) ----
|
|
223
|
+
|
|
224
|
+
fn provedFn(r: &UniqueReport, name: &str) -> bool {
|
|
225
|
+
r.proved.iter().any(|p| p.starts_with(&format!("{name}:")))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fn whyNot(r: &UniqueReport, name: &str) -> String {
|
|
229
|
+
r.unproved.iter().find(|(l, _)| l.starts_with(&format!("{name}:"))).map(|(_, m)| m.clone()).unwrap_or_default()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const CELL: &str = "struct Cell(label: Str, mut n: Int) =\n fun bumped(self) -> Cell = Cell(..self, n: self.n + 1)\n fun same(self) -> Cell = self\n\
|
|
233
|
+
fun bump(c: Cell) -> Cell = Cell(..c, n: c.n + 1)\n\
|
|
234
|
+
fun touch(c: Cell) -> Cell =\n c.n = c.n + 1\n c\n\
|
|
235
|
+
struct Holder(mut c: Cell, tag: Int)\n";
|
|
236
|
+
|
|
237
|
+
#[test]
|
|
238
|
+
fn method_handed_over_in_a_while_loop_is_unique() {
|
|
239
|
+
let s = format!(
|
|
240
|
+
"{CELL}fun go() -> Cell =\n v := Cell(label: \"a\", n: 0)\n i := 0\n while i < 3\n v = v.bumped()\n i = i + 1\n v\n"
|
|
241
|
+
);
|
|
242
|
+
let (_, r) = run(&s);
|
|
243
|
+
assert!(provedFn(&r, "Cell.bumped"), "{r:?}");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
#[test]
|
|
247
|
+
fn method_handed_over_in_a_field_is_unique() {
|
|
248
|
+
let s = format!(
|
|
249
|
+
"{CELL}fun go(h: Holder) -> Holder =\n h.c = h.c.bumped()\n h\n\
|
|
250
|
+
fun mk() -> Holder = Holder(c: Cell(label: \"a\", n: 0), tag: 1)\n\
|
|
251
|
+
fun run() -> Holder =\n h := mk()\n go(h)\n"
|
|
252
|
+
);
|
|
253
|
+
let (_, r) = run(&s);
|
|
254
|
+
assert!(provedFn(&r, "Cell.bumped"), "{r:?}");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#[test]
|
|
258
|
+
fn returned_parameter_is_fresh_for_a_unique_argument() {
|
|
259
|
+
let s = format!(
|
|
260
|
+
"{CELL}fun go() -> Cell =\n v := Cell(label: \"a\", n: 0)\n v = touch(v)\n bump(v)\n"
|
|
261
|
+
);
|
|
262
|
+
let (_, r) = run(&s);
|
|
263
|
+
assert!(provedFn(&r, "bump"), "{r:?}");
|
|
264
|
+
assert!(provedFn(&r, "touch") || !whyNot(&r, "touch").contains("shared"), "{r:?}");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#[test]
|
|
268
|
+
fn returned_parameter_in_a_loop_keeps_the_hand_over_unique() {
|
|
269
|
+
let s = format!(
|
|
270
|
+
"{CELL}fun go() -> Cell =\n v := Cell(label: \"a\", n: 0)\n i := 0\n while i < 3\n v = touch(v)\n v = bump(v)\n i = i + 1\n v\n"
|
|
271
|
+
);
|
|
272
|
+
let (_, r) = run(&s);
|
|
273
|
+
assert!(provedFn(&r, "bump"), "{r:?}");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
#[test]
|
|
277
|
+
fn returned_parameter_in_a_field_keeps_the_field_unique() {
|
|
278
|
+
let s = format!(
|
|
279
|
+
"{CELL}fun go(h: Holder) -> Holder =\n h.c = touch(h.c)\n h.c = bump(h.c)\n h\n\
|
|
280
|
+
fun mk() -> Holder = Holder(c: Cell(label: \"a\", n: 0), tag: 1)\n\
|
|
281
|
+
fun run() -> Holder =\n h := mk()\n go(h)\n"
|
|
282
|
+
);
|
|
283
|
+
let (_, r) = run(&s);
|
|
284
|
+
assert!(provedFn(&r, "bump"), "{r:?}");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
#[test]
|
|
288
|
+
fn returned_parameter_with_a_shared_argument_is_not_fresh() {
|
|
289
|
+
// `a` is read after the call, so `touch(a)` returns a value someone else holds.
|
|
290
|
+
let s = format!(
|
|
291
|
+
"{CELL}fun go() -> Int =\n a := Cell(label: \"a\", n: 0)\n v := touch(a)\n w := bump(v)\n a.n + w.n\n"
|
|
292
|
+
);
|
|
293
|
+
let (_, r) = run(&s);
|
|
294
|
+
assert!(!provedFn(&r, "bump"), "{r:?}");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
#[test]
|
|
298
|
+
fn returned_parameter_that_was_stored_is_not_fresh() {
|
|
299
|
+
let s = format!(
|
|
300
|
+
"{CELL}fun reg(xs: Slice[Cell], c: Cell) -> Cell =\n xs.push(c)\n c\n\
|
|
301
|
+
fun go(xs: Slice[Cell]) -> Cell =\n v := Cell(label: \"a\", n: 0)\n v = reg(xs, v)\n bump(v)\n"
|
|
302
|
+
);
|
|
303
|
+
let (_, r) = run(&s);
|
|
304
|
+
assert!(!provedFn(&r, "bump"), "{r:?}");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
#[test]
|
|
308
|
+
fn returned_parameter_in_a_field_that_was_stored_is_not_unique() {
|
|
309
|
+
let s = format!(
|
|
310
|
+
"{CELL}fun reg(xs: Slice[Cell], c: Cell) -> Cell =\n xs.push(c)\n c\n\
|
|
311
|
+
fun go(h: Holder, xs: Slice[Cell]) -> Holder =\n h.c = reg(xs, h.c)\n h.c = bump(h.c)\n h\n\
|
|
312
|
+
fun mk() -> Holder = Holder(c: Cell(label: \"a\", n: 0), tag: 1)\n\
|
|
313
|
+
fun run(xs: Slice[Cell]) -> Holder =\n h := mk()\n go(h, xs)\n"
|
|
314
|
+
);
|
|
315
|
+
let (_, r) = run(&s);
|
|
316
|
+
assert!(!provedFn(&r, "bump"), "{r:?}");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
#[test]
|
|
320
|
+
fn returned_parameter_handed_a_shared_value_elsewhere_is_not_unique() {
|
|
321
|
+
// `touch` is called with a unique value here and a shared one in `other`.
|
|
322
|
+
let s = format!(
|
|
323
|
+
"{CELL}fun go() -> Cell =\n v := Cell(label: \"a\", n: 0)\n v = touch(v)\n bump(v)\n\
|
|
324
|
+
fun other(a: Cell) -> Int =\n b := touch(a)\n a.n + b.n\n"
|
|
325
|
+
);
|
|
326
|
+
let (_, r) = run(&s);
|
|
327
|
+
assert!(!provedFn(&r, "touch"), "{r:?}");
|
|
328
|
+
assert!(!provedFn(&r, "bump"), "{r:?}");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
#[test]
|
|
332
|
+
fn mixed_unique_and_shared_calls_say_so() {
|
|
333
|
+
let s = format!(
|
|
334
|
+
"{CELL}fun go() -> Cell =\n v := Cell(label: \"a\", n: 0)\n bump(v)\n\
|
|
335
|
+
fun other(a: Cell) -> Int =\n b := bump(a)\n a.n + b.n\n"
|
|
336
|
+
);
|
|
337
|
+
let (_, r) = run(&s);
|
|
338
|
+
assert!(!provedFn(&r, "bump"), "{r:?}");
|
|
339
|
+
let why = whyNot(&r, "bump");
|
|
340
|
+
assert!(why.contains("from other") && why.contains("passes a unique value"), "{why}");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
#[test]
|
|
344
|
+
fn override_storing_self_makes_the_method_name_not_unique() {
|
|
345
|
+
let s = "struct Base(label: Str, n: Int) =\n fun step(self) -> Base = Base(..self, n: self.n + 1)\n\
|
|
346
|
+
struct Sub(Base(label, n), log: Slice[Base]) =\n override fun step(self) -> Base =\n self.log.push(self)\n Base(label: self.label, n: self.n + 1)\n\
|
|
347
|
+
fun go(b: Base) -> Int =\n c := b.step()\n b.n + c.n\n";
|
|
348
|
+
let (_, r) = run(s);
|
|
349
|
+
assert!(!provedFn(&r, "Base.step"), "{r:?}");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
#[test]
|
|
353
|
+
fn override_storing_self_with_a_unique_receiver_stays_a_copy_of_the_stored_value() {
|
|
354
|
+
// The receiver is dead after the call, so Base.step may write in place; Sub.step keeps
|
|
355
|
+
// `self` in its log, and the log is the only holder left.
|
|
356
|
+
let s = "struct Base(label: Str, n: Int) =\n fun step(self) -> Base = Base(..self, n: self.n + 1)\n\
|
|
357
|
+
struct Sub(Base(label, n), log: Slice[Base]) =\n override fun step(self) -> Base =\n self.log.push(self)\n Base(label: self.label, n: self.n + 1)\n\
|
|
358
|
+
fun go() -> Base =\n b := Base(label: \"a\", n: 0)\n b.step()\n";
|
|
359
|
+
let (_, r) = run(s);
|
|
360
|
+
assert!(!provedFn(&r, "Sub.step"), "{r:?}");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
#[test]
|
|
364
|
+
fn stored_in_two_fields_not_unique() {
|
|
365
|
+
let s = format!(
|
|
366
|
+
"{CELL}struct Two(a: Cell, b: Cell)\n\
|
|
367
|
+
fun go() -> Two =\n v := Cell(label: \"a\", n: 0)\n t := Two(a: v, b: v)\n v = bump(v)\n t\n"
|
|
368
|
+
);
|
|
369
|
+
let (_, r) = run(&s);
|
|
370
|
+
assert!(!provedFn(&r, "bump"), "{r:?}");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
#[test]
|
|
374
|
+
fn returned_with_another_reference_not_unique() {
|
|
375
|
+
let s = format!(
|
|
376
|
+
"{CELL}fun go() -> Pair[Cell, Cell] =\n v := Cell(label: \"a\", n: 0)\n p := Pair(v, bump(v))\n p\n"
|
|
377
|
+
);
|
|
378
|
+
let (_, r) = run(&s);
|
|
379
|
+
assert!(!provedFn(&r, "bump"), "{r:?}");
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
#[test]
|
|
383
|
+
fn update_in_one_branch_with_old_value_read_after_not_unique() {
|
|
384
|
+
let s = format!(
|
|
385
|
+
"{CELL}fun go(flag: Bool) -> Int =\n c := Cell(label: \"a\", n: 0)\n d := c\n if flag\n d = bump(d)\n c.n + d.n\n"
|
|
386
|
+
);
|
|
387
|
+
let (_, r) = run(&s);
|
|
388
|
+
assert!(!provedFn(&r, "bump"), "{r:?}");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
#[test]
|
|
392
|
+
fn matched_into_a_binding_not_unique() {
|
|
393
|
+
let s = format!(
|
|
394
|
+
"{CELL}fun go() -> Int =\n c := Cell(label: \"a\", n: 0)\n match c\n k => bump(c).n + k.n\n"
|
|
395
|
+
);
|
|
396
|
+
let (_, r) = run(&s);
|
|
397
|
+
assert!(!provedFn(&r, "bump"), "{r:?}");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
#[test]
|
|
401
|
+
fn loop_keeping_the_previous_value_not_unique() {
|
|
402
|
+
let s = format!(
|
|
403
|
+
"{CELL}fun go(xs: Slice[Cell]) -> Cell =\n v := Cell(label: \"a\", n: 0)\n i := 0\n while i < 3\n xs.push(v)\n v = v.bumped()\n i = i + 1\n v\n"
|
|
404
|
+
);
|
|
405
|
+
let (_, r) = run(&s);
|
|
406
|
+
assert!(!provedFn(&r, "Cell.bumped"), "{r:?}");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
#[test]
|
|
410
|
+
fn loop_value_bound_outside_from_a_shared_source_not_unique() {
|
|
411
|
+
let s = format!(
|
|
412
|
+
"{CELL}fun go(start: Cell) -> Cell =\n v := start\n i := 0\n while i < 3\n v = v.bumped()\n i = i + 1\n v\n\
|
|
413
|
+
fun other(c: Cell) -> Int =\n d := go(c)\n c.n + d.n\n"
|
|
414
|
+
);
|
|
415
|
+
let (_, r) = run(&s);
|
|
416
|
+
assert!(!provedFn(&r, "Cell.bumped"), "{r:?}");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
#[test]
|
|
420
|
+
fn constant_receiver_not_unique() {
|
|
421
|
+
let s = "struct Zero(label: Str, n: Int) =\n Origin = Zero(label: \"z\", n: 0)\n fun inc(self) -> Zero = Zero(..self, n: self.n + 1)\n\
|
|
422
|
+
fun go() -> Zero = Zero.Origin.inc()\n";
|
|
423
|
+
let (_, r) = run(s);
|
|
424
|
+
assert!(!provedFn(&r, "Zero.inc"), "{r:?}");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
#[test]
|
|
428
|
+
fn read_out_of_a_mut_field_and_handed_on_not_unique() {
|
|
429
|
+
// `h.c` is read into `held` as well, so the field is shared and `bump` must copy.
|
|
430
|
+
let s = format!(
|
|
431
|
+
"{CELL}fun go(h: Holder) -> Int =\n held := h.c\n v := bump(h.c)\n held.n + v.n\n\
|
|
432
|
+
fun mk() -> Holder = Holder(c: Cell(label: \"a\", n: 0), tag: 1)\n\
|
|
433
|
+
fun run() -> Int =\n h := mk()\n go(h)\n"
|
|
434
|
+
);
|
|
435
|
+
let (_, r) = run(&s);
|
|
436
|
+
assert!(!provedFn(&r, "bump"), "{r:?}");
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
#[test]
|
|
440
|
+
fn struct_reachable_from_a_mut_field_of_another_not_unique() {
|
|
441
|
+
// `outer.c` holds `v` and `v` is spread: the field holds the same object.
|
|
442
|
+
let s = format!(
|
|
443
|
+
"{CELL}fun go() -> Holder =\n v := Cell(label: \"a\", n: 0)\n h := Holder(c: v, tag: 1)\n w := bump(v)\n h\n"
|
|
444
|
+
);
|
|
445
|
+
let (_, r) = run(&s);
|
|
446
|
+
assert!(!provedFn(&r, "bump"), "{r:?}");
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
#[test]
|
|
450
|
+
fn generic_function_with_both_kinds_not_unique() {
|
|
451
|
+
let s = format!(
|
|
452
|
+
"{CELL}fun ident[T](x: T) -> T = x\n\
|
|
453
|
+
fun go(a: Cell) -> Int =\n b := bump(ident(a))\n a.n + b.n\n"
|
|
454
|
+
);
|
|
455
|
+
let (_, r) = run(&s);
|
|
456
|
+
assert!(!provedFn(&r, "bump"), "{r:?}");
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
#[test]
|
|
460
|
+
fn a_closure_parameter_named_like_a_fresh_fn_is_not_that_fn() {
|
|
461
|
+
// `mk` in `go` is the parameter, not the free fn `mk`: its result may be shared.
|
|
462
|
+
let s = format!(
|
|
463
|
+
"{CELL}fun mk() -> Cell = Cell(label: \"m\", n: 0)\n\
|
|
464
|
+
fun go(mk: fn() -> Cell) -> Cell =\n y := mk()\n y = bump(y)\n y\n"
|
|
465
|
+
);
|
|
466
|
+
let (_, r) = run(&s);
|
|
467
|
+
assert!(!provedFn(&r, "bump"), "{r:?}");
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
#[test]
|
|
471
|
+
fn a_closure_field_named_like_a_fresh_method_is_not_that_method() {
|
|
472
|
+
// `h.made()` calls the closure in the field, not `Maker.made`.
|
|
473
|
+
let s = format!(
|
|
474
|
+
"{CELL}struct Maker(k: Int) =\n fun made(self) -> Cell = Cell(label: \"m\", n: 0)\n\
|
|
475
|
+
struct Hook(made: fn() -> Cell)\n\
|
|
476
|
+
fun go(h: Hook) -> Cell =\n y := h.made()\n y = bump(y)\n y\n"
|
|
477
|
+
);
|
|
478
|
+
let (_, r) = run(&s);
|
|
479
|
+
assert!(!provedFn(&r, "bump"), "{r:?}");
|
|
480
|
+
}
|
compiler/driver/src/lib.rs
CHANGED
|
@@ -16,13 +16,16 @@ pub struct PassOptions {
|
|
|
16
16
|
/// The supercompiler stage (`--no-fusion` clears it).
|
|
17
17
|
pub fusion: bool,
|
|
18
18
|
pub trmc: bool,
|
|
19
|
+
/// In-place updates (FBIP): uniqueness marks and same-shape cell reuse. `--no-fbip` clears it;
|
|
20
|
+
/// every spread then copies and no cell is overwritten. Only meaningful with `fusion`.
|
|
21
|
+
pub fbip: bool,
|
|
19
22
|
/// Print each fusion decision and each dropped rewrite (`--explain-fusion`).
|
|
20
23
|
pub explain: bool,
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
impl Default for PassOptions {
|
|
24
27
|
fn default() -> Self {
|
|
25
|
-
PassOptions { closureSpec: true, fusion: true, trmc: true, explain: false }
|
|
28
|
+
PassOptions { closureSpec: true, fusion: true, trmc: true, fbip: true, explain: false }
|
|
26
29
|
}
|
|
27
30
|
}
|
|
28
31
|
|
|
@@ -39,6 +42,8 @@ pub enum Target {
|
|
|
39
42
|
/// What a rewrite may read besides the program.
|
|
40
43
|
pub struct Env<'a> {
|
|
41
44
|
pub explain: bool,
|
|
45
|
+
/// Whether in-place updates may be marked (`PassOptions.fbip`).
|
|
46
|
+
pub fbip: bool,
|
|
42
47
|
/// Receiver types of method calls, read off the checked program (see `closureHints`).
|
|
43
48
|
pub hints: &'a Hints,
|
|
44
49
|
}
|
|
@@ -58,20 +63,27 @@ fn closureSpec(copy: &mut Source, env: &Env) -> bool {
|
|
|
58
63
|
|
|
59
64
|
/// Purity -> simplify -> drive -> uniqueness -> reuse; returns every note (empty = nothing changed).
|
|
60
65
|
pub fn fusionNotes(copy: &mut Source) -> Vec<String> {
|
|
66
|
+
fusionNotesWith(copy, true)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// `fusionNotes`; with `fbip` off, uniqueness and cell reuse do not run, so nothing is updated in place.
|
|
70
|
+
pub fn fusionNotesWith(copy: &mut Source, fbip: bool) -> Vec<String> {
|
|
61
71
|
let pure = plum_core::purity::pureFns(copy);
|
|
62
72
|
let mut notes = plum_core::simplify::simplify(copy, &pure);
|
|
63
73
|
notes.extend(plum_core::drive::drive(copy, &pure));
|
|
74
|
+
if fbip {
|
|
64
|
-
|
|
75
|
+
let rep = plum_core::uniqueness::markUnique(copy);
|
|
65
|
-
|
|
76
|
+
notes.extend(rep.proved.iter().map(|l| format!("unique spread at {l}")));
|
|
66
|
-
|
|
77
|
+
notes.extend(rep.unproved.iter().map(|(l, why)| format!("uniqueness: not in place {l}: {why}")));
|
|
67
|
-
|
|
78
|
+
notes.retain(|n| !n.is_empty());
|
|
68
|
-
|
|
79
|
+
notes.extend(plum_core::reuse::markReuse(copy));
|
|
80
|
+
}
|
|
69
81
|
notes.retain(|n| !n.is_empty());
|
|
70
82
|
notes
|
|
71
83
|
}
|
|
72
84
|
|
|
73
85
|
fn fusion(copy: &mut Source, env: &Env) -> bool {
|
|
74
|
-
let notes =
|
|
86
|
+
let notes = fusionNotesWith(copy, env.fbip);
|
|
75
87
|
if env.explain {
|
|
76
88
|
for n in ¬es {
|
|
77
89
|
eprintln!("fusion: {n}");
|
|
@@ -136,7 +148,7 @@ fn runPass(pass: &Pass, original: &Source, opts: &PassOptions, st: &mut State) {
|
|
|
136
148
|
} else {
|
|
137
149
|
Hints::new()
|
|
138
150
|
};
|
|
139
|
-
let env = Env { explain: opts.explain, hints: &hints };
|
|
151
|
+
let env = Env { explain: opts.explain, fbip: opts.fbip, hints: &hints };
|
|
140
152
|
let mut copy = match pass.target {
|
|
141
153
|
Target::Pre => st.pre.clone(),
|
|
142
154
|
Target::Lowered => match st.info.lowered.as_ref() {
|
compiler/driver/tests/driver_tests.rs
CHANGED
|
@@ -30,7 +30,7 @@ fn loweredNames(src: &Source, opts: &PassOptions) -> Vec<String> {
|
|
|
30
30
|
#[test]
|
|
31
31
|
fn noPassesMeansNoGeneratedFns() {
|
|
32
32
|
let src = load("drv_none", SRC);
|
|
33
|
-
let opts = PassOptions { closureSpec: false, fusion: false, trmc: false, explain: false };
|
|
33
|
+
let opts = PassOptions { closureSpec: false, fusion: false, trmc: false, fbip: false, explain: false };
|
|
34
34
|
let names = loweredNames(&src, &opts);
|
|
35
35
|
assert!(names.iter().all(|n| !n.contains('$')), "{names:?}");
|
|
36
36
|
}
|
compiler/game/web-test/src/lib.rs
CHANGED
|
@@ -62,6 +62,8 @@ pub struct Options {
|
|
|
62
62
|
pub expect_pixels: Vec<ExpectPixel>,
|
|
63
63
|
/// Destroy the GPU device from the page after a few frames and require rendering to resume.
|
|
64
64
|
pub lose_device: bool,
|
|
65
|
+
/// Pass `--no-fbip` to `plum build`.
|
|
66
|
+
pub no_fbip: bool,
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
impl Default for Options {
|
|
@@ -80,6 +82,7 @@ impl Default for Options {
|
|
|
80
82
|
uncapped: false,
|
|
81
83
|
dpr: None,
|
|
82
84
|
lose_device: false,
|
|
85
|
+
no_fbip: false,
|
|
83
86
|
expect_pixels: Vec::new(),
|
|
84
87
|
}
|
|
85
88
|
}
|
|
@@ -234,7 +237,7 @@ pub fn repo_root() -> PathBuf {
|
|
|
234
237
|
}
|
|
235
238
|
|
|
236
239
|
/// Builds plum-cli and exports `file` as a static web build into `out`.
|
|
237
|
-
pub fn build_web(file: &Path, out: &Path, assets: Option<&Path>) -> Result<(), String> {
|
|
240
|
+
pub fn build_web(file: &Path, out: &Path, assets: Option<&Path>, no_fbip: bool) -> Result<(), String> {
|
|
238
241
|
let root = repo_root();
|
|
239
242
|
let st = Command::new("cargo")
|
|
240
243
|
.args(["build", "-p", "plum-cli"])
|
|
@@ -247,6 +250,9 @@ pub fn build_web(file: &Path, out: &Path, assets: Option<&Path>) -> Result<(), S
|
|
|
247
250
|
let file = file.canonicalize().map_err(|e| format!("{}: {e}", file.display()))?;
|
|
248
251
|
let mut cmd = Command::new(root.join("target/debug/plum"));
|
|
249
252
|
cmd.arg("build").arg(&file).arg("-o").arg(out);
|
|
253
|
+
if no_fbip {
|
|
254
|
+
cmd.arg("--no-fbip");
|
|
255
|
+
}
|
|
250
256
|
if let Some(a) = assets {
|
|
251
257
|
cmd.arg("--assets").arg(a.canonicalize().map_err(|e| format!("{}: {e}", a.display()))?);
|
|
252
258
|
}
|
|
@@ -387,7 +393,7 @@ pub async fn run(file: &Path, opts: &Options) -> Result<String, String> {
|
|
|
387
393
|
}
|
|
388
394
|
|
|
389
395
|
async fn run_in(file: &Path, opts: &Options, dir: &Path) -> Result<String, String> {
|
|
390
|
-
build_web(file, dir, opts.assets.as_deref())?;
|
|
396
|
+
build_web(file, dir, opts.assets.as_deref(), opts.no_fbip)?;
|
|
391
397
|
let stop = Arc::new(AtomicBool::new(false));
|
|
392
398
|
let port = serve(dir.to_path_buf(), stop.clone());
|
|
393
399
|
let res = check(port, opts).await;
|
compiler/game/web-test/src/main.rs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
use plum_web_test::{browser_installed, run, Browser, ExpectPixel, Options};
|
|
2
2
|
use std::path::PathBuf;
|
|
3
3
|
|
|
4
|
-
const USAGE: &str = "usage: plum-web-test <file.plum> [--frames-wait SECS] [--screenshot OUT.png] [--require-audio]\n [--assets DIR] [--headed] [--measure SECS] [--uncapped] [--json]
|
|
4
|
+
const USAGE: &str = "usage: plum-web-test <file.plum> [--frames-wait SECS] [--screenshot OUT.png] [--require-audio]\n [--assets DIR] [--headed] [--measure SECS] [--uncapped] [--json] [--no-fbip]
|
|
5
5
|
[--browser chromium|webkit] [--expect-unsupported-webgpu] [--no-module-introspection]
|
|
6
6
|
[--dpr N] [--lose-device] [--expect-pixel x,y,r,g,b[,tol]]...";
|
|
7
7
|
|
|
@@ -18,6 +18,7 @@ async fn main() {
|
|
|
18
18
|
"--screenshot" => opts.screenshot = Some(args.next().map(PathBuf::from).unwrap_or_else(|| fail("--screenshot needs a path"))),
|
|
19
19
|
"--require-audio" => opts.require_audio = true,
|
|
20
20
|
"--headed" => opts.headed = true,
|
|
21
|
+
"--no-fbip" => opts.no_fbip = true,
|
|
21
22
|
"--uncapped" => opts.uncapped = true,
|
|
22
23
|
"--json" => opts.json = true,
|
|
23
24
|
"--browser" => opts.browser = Browser::parse(&args.next().unwrap_or_else(|| fail("--browser needs chromium or webkit"))).unwrap_or_else(|e| fail(&e)),
|
compiler/lsp/src/main.rs
CHANGED
|
@@ -309,7 +309,7 @@ fn typeDiagnosticsFor(path: &Path, libPath: &Path, text: &str) -> Option<Vec<Dia
|
|
|
309
309
|
overrides.insert(canon, text.to_string());
|
|
310
310
|
let ast = plum_core::loadAndMergeWithOverridesDiag(path, libPath, &overrides).ok()?;
|
|
311
311
|
let lastLine = text.lines().count().max(1) as u32 - 1;
|
|
312
|
-
let opts = plum_driver::PassOptions { closureSpec: false, fusion: false, trmc: false, explain: false };
|
|
312
|
+
let opts = plum_driver::PassOptions { closureSpec: false, fusion: false, trmc: false, fbip: false, explain: false };
|
|
313
313
|
match plum_driver::check(&ast, &opts) {
|
|
314
314
|
Ok(_) => Some(Vec::new()),
|
|
315
315
|
Err(diags) => Some(
|
compiler/wasm-codegen/src/closurecalls.rs
CHANGED
|
@@ -285,6 +285,9 @@ pub(crate) fn compileClosureBody(
|
|
|
285
285
|
unit_inst: unit.inst,
|
|
286
286
|
unit_subst: unit.subst,
|
|
287
287
|
types: unit.types,
|
|
288
|
+
base_types: &ctx.base_types,
|
|
289
|
+
inst_types: &ctx.inst_types,
|
|
290
|
+
global_env: &ctx.global_env,
|
|
288
291
|
local_decl_types: decl_types,
|
|
289
292
|
closures: &ctx.closures,
|
|
290
293
|
closure_call_types: &ctx.closure_call_types,
|
compiler/wasm-codegen/src/construct.rs
CHANGED
|
@@ -39,6 +39,10 @@ pub(crate) fn compileDefaultArgs(
|
|
|
39
39
|
ctx: &LocalCtx,
|
|
40
40
|
state: &mut ModuleState,
|
|
41
41
|
) -> Result<(), String> {
|
|
42
|
+
if sig.param_defaults.len() <= supplied_count {
|
|
43
|
+
return Ok(());
|
|
44
|
+
}
|
|
45
|
+
let ctx = ctx.forDefault(sig);
|
|
42
46
|
for (i, default) in sig.param_defaults.iter().enumerate().skip(supplied_count) {
|
|
43
47
|
let default_expr = default.as_ref().ok_or_else(|| {
|
|
44
48
|
format!(
|
|
@@ -47,30 +51,13 @@ pub(crate) fn compileDefaultArgs(
|
|
|
47
51
|
)
|
|
48
52
|
})?;
|
|
49
53
|
match sig.decl_params.get(i) {
|
|
50
|
-
// A payload-free variant default (`o: Option[Int] = None`): the clone has
|
|
51
|
-
// no recorded type, so take the singleton of the param's declared type.
|
|
52
|
-
Some(d) if defaultSingleton(default_expr, d, ctx).is_some() => {
|
|
53
|
-
let g = defaultSingleton(default_expr, d, ctx).expect("checked");
|
|
54
|
-
Instruction::GlobalGet(g).encode(body);
|
|
55
|
-
}
|
|
56
|
-
Some(d) => compileExprInto(default_expr, d, body, ctx, state)?,
|
|
54
|
+
Some(d) => compileExprInto(default_expr, d, body, &ctx, state)?,
|
|
57
|
-
None => compileExpr(default_expr, body, ctx, state)?,
|
|
55
|
+
None => compileExpr(default_expr, body, &ctx, state)?,
|
|
58
56
|
}
|
|
59
57
|
}
|
|
60
58
|
Ok(())
|
|
61
59
|
}
|
|
62
60
|
|
|
63
|
-
/// The singleton global of a payload-free variant default `n` of the param type `d`.
|
|
64
|
-
pub(crate) fn defaultSingleton(default_expr: &ast::Expr, d: &PlumType, ctx: &LocalCtx) -> Option<u32> {
|
|
65
|
-
let ast::Expr { kind: ast::ExprKind::TypeName(n), .. } = default_expr else { return None };
|
|
66
|
-
let info = ctx.enum_variants.get(n).filter(|i| i.field_types.is_empty())?;
|
|
67
|
-
match d {
|
|
68
|
-
PlumType::TNamed(e) | PlumType::TApp(e, _) if *e == info.enum_name => {}
|
|
69
|
-
_ => return None,
|
|
70
|
-
}
|
|
71
|
-
ctx.singleton_globals.get(&(canonTy(d), n.clone())).copied()
|
|
72
|
-
}
|
|
73
|
-
|
|
74
61
|
/// Constructs a value enum by pushing its flattened fields in declaration order. Each
|
|
75
62
|
/// field comes from the same-named keyword argument, else the positional argument at its
|
|
76
63
|
/// index, else (`Vec2(..v, x: 1.0)`) the spread source's field.
|
|
@@ -261,6 +248,7 @@ pub(crate) fn compileInterpolatedString(
|
|
|
261
248
|
line: 0,
|
|
262
249
|
column: 0,
|
|
263
250
|
tail: ast::TailKind::NotTail,
|
|
251
|
+
unique: false,
|
|
264
252
|
};
|
|
265
253
|
if plum_checker::nominalName(&other)
|
|
266
254
|
.is_some_and(|n| ctx.func_ids.contains_key(&format!("{}::toStr", n)))
|
compiler/wasm-codegen/src/context.rs
CHANGED
|
@@ -16,6 +16,9 @@ pub struct FuncSig {
|
|
|
16
16
|
/// implicit receiver, and the declared return type.
|
|
17
17
|
pub decl_params: Vec<PlumType>,
|
|
18
18
|
pub decl_ret: PlumType,
|
|
19
|
+
/// The instantiation this is the signature of (`None` for a plain fn): the checker
|
|
20
|
+
/// recorded the types of `param_defaults` there.
|
|
21
|
+
pub inst: Option<usize>,
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
/// Everything codegen needs to know about one closure *literal* found in the program.
|
|
@@ -100,6 +103,9 @@ pub struct CompileCtx<'a> {
|
|
|
100
103
|
/// Payload-free variant name (End/...) -> the global index holding
|
|
101
104
|
/// its one pre-allocated instance (see this migration plan's Decision 2).
|
|
102
105
|
pub singleton_globals: HashMap<(PlumType, String), u32>,
|
|
106
|
+
/// The checker's types of plain fns and of each instantiation.
|
|
107
|
+
pub(crate) base_types: UnitTypes,
|
|
108
|
+
pub(crate) inst_types: Vec<UnitTypes>,
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
/// Per-module state that accumulates as function bodies are compiled: every static
|
|
@@ -135,6 +141,9 @@ pub(crate) struct LocalCtx<'a> {
|
|
|
135
141
|
/// Type-parameter bindings of the instantiation being compiled (empty for a plain fn).
|
|
136
142
|
pub(crate) unit_subst: &'a Subst,
|
|
137
143
|
pub(crate) types: &'a UnitTypes,
|
|
144
|
+
pub(crate) base_types: &'a UnitTypes,
|
|
145
|
+
pub(crate) inst_types: &'a [UnitTypes],
|
|
146
|
+
pub(crate) global_env: &'a TypeEnv,
|
|
138
147
|
/// Name -> the plum type each local was DECLARED with (so its wasm
|
|
139
148
|
/// representation is `plumTypeToValtype` of this), as opposed to the
|
|
140
149
|
/// type of a particular use of it.
|
|
@@ -176,6 +185,52 @@ pub(crate) struct LocalCtx<'a> {
|
|
|
176
185
|
pub(crate) self_closure: Option<(String, u32)>,
|
|
177
186
|
}
|
|
178
187
|
|
|
188
|
+
impl LocalCtx<'_> {
|
|
189
|
+
/// The context a parameter default of the fn with signature `sig` compiles in, at a call
|
|
190
|
+
/// that leaves the argument out: that fn's unit, where its types and calls are recorded,
|
|
191
|
+
/// and no locals (a default sees only globals, whoever calls the fn).
|
|
192
|
+
pub(crate) fn forDefault(&self, sig: &FuncSig) -> LocalCtx<'_> {
|
|
193
|
+
let types = sig.inst.map_or(self.base_types, |id| &self.inst_types[id]);
|
|
194
|
+
LocalCtx {
|
|
195
|
+
locals: HashMap::new(),
|
|
196
|
+
match_scratch_base: 0,
|
|
197
|
+
match_scratch_index: HashMap::new(),
|
|
198
|
+
nested_class_scratch_base: 0,
|
|
199
|
+
nested_class_scratch: HashMap::new(),
|
|
200
|
+
func_ids: self.func_ids,
|
|
201
|
+
func_sigs: self.func_sigs,
|
|
202
|
+
inst_funcs: self.inst_funcs,
|
|
203
|
+
call_inst: self.call_inst,
|
|
204
|
+
op_calls: self.op_calls,
|
|
205
|
+
known_fns: self.known_fns,
|
|
206
|
+
unit_idx: self.unit_idx,
|
|
207
|
+
unit_inst: sig.inst,
|
|
208
|
+
unit_subst: &types.subst,
|
|
209
|
+
types,
|
|
210
|
+
base_types: self.base_types,
|
|
211
|
+
inst_types: self.inst_types,
|
|
212
|
+
global_env: self.global_env,
|
|
213
|
+
local_decl_types: HashMap::new(),
|
|
214
|
+
closures: self.closures,
|
|
215
|
+
closure_call_types: self.closure_call_types,
|
|
216
|
+
named_fn_values: self.named_fn_values,
|
|
217
|
+
methods: self.methods,
|
|
218
|
+
enum_variants: self.enum_variants,
|
|
219
|
+
enum_params: self.enum_params,
|
|
220
|
+
gc_types: self.gc_types,
|
|
221
|
+
singleton_globals: self.singleton_globals,
|
|
222
|
+
type_env: RefCell::new(self.global_env.clone()),
|
|
223
|
+
loop_base_depth: Cell::new(0),
|
|
224
|
+
ret_type: None,
|
|
225
|
+
subject_ptypes: RefCell::new(HashMap::new()),
|
|
226
|
+
self_func: None,
|
|
227
|
+
tail_depth: Cell::new(0),
|
|
228
|
+
import_count: self.import_count,
|
|
229
|
+
self_closure: None,
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
179
234
|
/// What tail-call emission needs to know about the fn being compiled.
|
|
180
235
|
pub(crate) struct SelfFunc {
|
|
181
236
|
pub(crate) idx: u32,
|
compiler/wasm-codegen/src/driver.rs
CHANGED
|
@@ -108,7 +108,10 @@ impl<'a> Setup<'a> {
|
|
|
108
108
|
for ((id, node), t) in inst_annotated {
|
|
109
109
|
inst_types[id].annotated.insert(node, t);
|
|
110
110
|
}
|
|
111
|
+
for inst in &instantiations {
|
|
112
|
+
inst_types[inst.id].subst = inst.generics.iter().cloned().zip(inst.type_args.iter().cloned()).collect();
|
|
113
|
+
}
|
|
111
|
-
let base_types = UnitTypes { exprs: expr_types, annotated };
|
|
114
|
+
let base_types = UnitTypes { exprs: expr_types, annotated, subst: Subst::new() };
|
|
112
115
|
let (global_env, methods, enum_variants) = plum_checker::buildGlobalTables(source);
|
|
113
116
|
let enum_params = EnumParams::new();
|
|
114
117
|
let min_required = plum_checker::buildMinRequiredArgs(source);
|
|
@@ -650,7 +653,7 @@ impl<'a> Setup<'a> {
|
|
|
650
653
|
let gc_types = gc_types.as_mut().unwrap();
|
|
651
654
|
// Register every unit's wasm function up front.
|
|
652
655
|
for u in units.iter() {
|
|
653
|
-
let sig = unitFuncSig(u.f, &u.subst, &eg, &gc_types);
|
|
656
|
+
let sig = FuncSig { inst: u.inst, ..unitFuncSig(u.f, &u.subst, &eg, &gc_types) };
|
|
654
657
|
let type_idx = module.addType(&sig.params, &sig.ret);
|
|
655
658
|
let func_idx = module.addFunction(type_idx, &[]);
|
|
656
659
|
module.nameFunction(func_idx, &u.name);
|
|
@@ -925,6 +928,8 @@ impl<'a> Setup<'a> {
|
|
|
925
928
|
named_fn_values,
|
|
926
929
|
gc_types,
|
|
927
930
|
singleton_globals,
|
|
931
|
+
base_types,
|
|
932
|
+
inst_types,
|
|
928
933
|
};
|
|
929
934
|
|
|
930
935
|
let state = ModuleState {
|
|
@@ -938,8 +943,6 @@ impl<'a> Setup<'a> {
|
|
|
938
943
|
state,
|
|
939
944
|
units,
|
|
940
945
|
const_inits,
|
|
941
|
-
base_types,
|
|
942
|
-
inst_types,
|
|
943
946
|
}
|
|
944
947
|
}
|
|
945
948
|
}
|
compiler/wasm-codegen/src/emit.rs
CHANGED
|
@@ -11,15 +11,13 @@ pub(crate) struct Emit<'a> {
|
|
|
11
11
|
pub(crate) state: ModuleState,
|
|
12
12
|
pub(crate) units: Vec<UnitDef<'a>>,
|
|
13
13
|
pub(crate) const_inits: Vec<(String, ConstInit<'a>, u32)>,
|
|
14
|
-
pub(crate) base_types: UnitTypes,
|
|
15
|
-
pub(crate) inst_types: Vec<UnitTypes>,
|
|
16
14
|
}
|
|
17
15
|
|
|
18
16
|
impl<'a> Emit<'a> {
|
|
19
17
|
/// Compile every unit's body into its reserved function slot.
|
|
20
18
|
pub(crate) fn compileUnitBodies(&mut self) -> Result<(), String> {
|
|
21
|
-
let Emit { module, ctx, state, units,
|
|
19
|
+
let Emit { module, ctx, state, units, .. } = self;
|
|
22
|
-
let unit_types = |u: &UnitDef| unitTypesOf(inst_types, base_types, u);
|
|
20
|
+
let unit_types = |u: &UnitDef| unitTypesOf(&ctx.inst_types, &ctx.base_types, u);
|
|
23
21
|
let mut compiled_bodies: Vec<(u32, Vec<u8>)> = Vec::new();
|
|
24
22
|
for (ui, u) in units.iter().enumerate() {
|
|
25
23
|
let f = u.f;
|
|
@@ -52,7 +50,7 @@ impl<'a> Emit<'a> {
|
|
|
52
50
|
|
|
53
51
|
/// Compile each heap-valued const's accessor body.
|
|
54
52
|
pub(crate) fn compileConstBodies(&mut self) -> Result<(), String> {
|
|
55
|
-
let Emit { module, ctx, state, const_inits,
|
|
53
|
+
let Emit { module, ctx, state, const_inits, .. } = self;
|
|
56
54
|
for (name, ci, func_idx) in const_inits.iter() {
|
|
57
55
|
let stand_in = ast::Fn {
|
|
58
56
|
file: String::new(),
|
|
@@ -76,7 +74,7 @@ impl<'a> Emit<'a> {
|
|
|
76
74
|
idx: usize::MAX,
|
|
77
75
|
inst: None,
|
|
78
76
|
subst: &no_subst,
|
|
79
|
-
types: &base_types,
|
|
77
|
+
types: &ctx.base_types,
|
|
80
78
|
};
|
|
81
79
|
let body = compileFnBodyImpl(&stand_in, Some(ci), &ctx, state, &uref)
|
|
82
80
|
.map_err(|e| format!("{e} (in const '{name}')"))?;
|
|
@@ -88,8 +86,8 @@ impl<'a> Emit<'a> {
|
|
|
88
86
|
|
|
89
87
|
/// Compile each closure literal's own body into its reserved function slot.
|
|
90
88
|
pub(crate) fn compileClosureBodies(&mut self) -> Result<(), String> {
|
|
91
|
-
let Emit { module, ctx, state, units,
|
|
89
|
+
let Emit { module, ctx, state, units, .. } = self;
|
|
92
|
-
let unit_types = |u: &UnitDef| unitTypesOf(inst_types, base_types, u);
|
|
90
|
+
let unit_types = |u: &UnitDef| unitTypesOf(&ctx.inst_types, &ctx.base_types, u);
|
|
93
91
|
// Compile each closure literal's own body into its reserved function slot.
|
|
94
92
|
let mut closure_bodies: Vec<(u32, Vec<u8>)> = Vec::new();
|
|
95
93
|
for ((ui, ptr), info) in &ctx.closures {
|
compiler/wasm-codegen/src/expr.rs
CHANGED
|
@@ -450,6 +450,7 @@ pub(crate) fn compileExpr(
|
|
|
450
450
|
line: 0,
|
|
451
451
|
column: 0,
|
|
452
452
|
tail: ast::TailKind::NotTail,
|
|
453
|
+
unique: false,
|
|
453
454
|
};
|
|
454
455
|
compileMethodCall(
|
|
455
456
|
&object,
|
compiler/wasm-codegen/src/fnbody.rs
CHANGED
|
@@ -452,6 +452,9 @@ pub(crate) fn compileFnBodyImpl(
|
|
|
452
452
|
unit_inst: unit.inst,
|
|
453
453
|
unit_subst: unit.subst,
|
|
454
454
|
types: unit.types,
|
|
455
|
+
base_types: &ctx.base_types,
|
|
456
|
+
inst_types: &ctx.inst_types,
|
|
457
|
+
global_env: &ctx.global_env,
|
|
455
458
|
local_decl_types: decl_types,
|
|
456
459
|
closures: &ctx.closures,
|
|
457
460
|
closure_call_types: &ctx.closure_call_types,
|
compiler/wasm-codegen/src/units.rs
CHANGED
|
@@ -87,6 +87,7 @@ pub(crate) fn unitFuncSig(f: &ast::Fn, subst: &Subst, eg: &EnumGenerics, g: &GcT
|
|
|
87
87
|
param_defaults,
|
|
88
88
|
decl_params: params,
|
|
89
89
|
decl_ret: ret,
|
|
90
|
+
inst: None,
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
|
|
@@ -208,6 +209,8 @@ pub(crate) struct UnitTypes {
|
|
|
208
209
|
pub(crate) exprs: HashMap<ast::NodeId, PlumType>,
|
|
209
210
|
/// The declared type of `name: Type := value`, keyed by `value`'s id.
|
|
210
211
|
pub(crate) annotated: HashMap<ast::NodeId, PlumType>,
|
|
212
|
+
/// The instantiation's type-parameter bindings (empty for plain fns).
|
|
213
|
+
pub(crate) subst: Subst,
|
|
211
214
|
}
|
|
212
215
|
|
|
213
216
|
impl UnitTypes {
|
compiler/wasm-codegen/tests/wasm_codegen/codegen_tests.rs
CHANGED
|
@@ -2812,3 +2812,125 @@ fn closure_assigning_captured_struct_fields_writes_its_own_copy() {
|
|
|
2812
2812
|
let body = " p := Pt(x: 0, y: 0)\n f := ||\n p.x = p.x + 1\n p.y = p.y + 2\n p.x * 10 + p.y\n a := f()\n b := f()\n a * 10000 + b * 100 + p.x * 10 + p.y\n";
|
|
2813
2813
|
assert_eq!(runCapturedFields("capfieldsstruct", body), 121200);
|
|
2814
2814
|
}
|
|
2815
|
+
|
|
2816
|
+
// ---------------------------------------------------------------------------
|
|
2817
|
+
// `Array.with`: written in place when the array is proved unique, else copied
|
|
2818
|
+
// ---------------------------------------------------------------------------
|
|
2819
|
+
|
|
2820
|
+
fn compiledFbip(name: &str, src: &str, fbip: bool) -> Vec<u8> {
|
|
2821
|
+
let dir = std::env::temp_dir().join(format!("plum_aw_{}_{}", name, std::process::id()));
|
|
2822
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
2823
|
+
let path = dir.join("main.plum");
|
|
2824
|
+
std::fs::write(&path, src).unwrap();
|
|
2825
|
+
let source = plum_core::loadAndMergeDiag(&path, &repoRoot()).expect("load");
|
|
2826
|
+
let opts = CompileOptions {
|
|
2827
|
+
passes: plum_driver::PassOptions { fbip, ..plum_driver::PassOptions::default() },
|
|
2828
|
+
..CompileOptions::default()
|
|
2829
|
+
};
|
|
2830
|
+
let o = compile(&source, &opts).expect("compile");
|
|
2831
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
2832
|
+
wasmparser::validate(&o.wasm).expect("valid wasm");
|
|
2833
|
+
o.wasm
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
const WITH_HEAD: &str = "import std/Int\nimport std/Array\n";
|
|
2837
|
+
|
|
2838
|
+
fn bodyNames(bytes: &[u8]) -> Vec<String> {
|
|
2839
|
+
namedBodies(bytes).into_iter().map(|(n, _)| n).collect()
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
fn hasFn(bytes: &[u8], needle: &str) -> bool {
|
|
2843
|
+
bodyNames(bytes).iter().any(|n| n.contains(needle))
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
/// The copying `Array[T].with` (not `withUnique`) is compiled.
|
|
2847
|
+
fn hasCopyingWith(bytes: &[u8]) -> bool {
|
|
2848
|
+
bodyNames(bytes).iter().any(|n| n.ends_with(".with"))
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
#[test]
|
|
2852
|
+
fn array_with_proved_local_writes_in_place() {
|
|
2853
|
+
// `a = a.with(..)` on a fresh local: the checker runs `withUnique`, which only sets.
|
|
2854
|
+
let src = format!(
|
|
2855
|
+
"{WITH_HEAD}fun main() -> Int =\n a := Array.filled(4, 0)\n i := 0\n while i < 4\n a = a.with(i, i * 2)\n i = i + 1\n a.get(3)\n"
|
|
2856
|
+
);
|
|
2857
|
+
let bytes = compiledFbip("awproved", &src, true);
|
|
2858
|
+
assert!(hasFn(&bytes, "withUnique"), "{:?}", bodyNames(&bytes));
|
|
2859
|
+
assert!(!hasCopyingWith(&bytes), "the copying with is unused: {:?}", bodyNames(&bytes));
|
|
2860
|
+
let inplace = namedBodies(&bytes).into_iter().find(|(n, _)| n.contains("withUnique")).unwrap().1;
|
|
2861
|
+
assert_eq!(countOps(&inplace, "ArrayCopy"), 0, "{inplace:?}");
|
|
2862
|
+
// one call, to the `set` intrinsic: no `resize` copy
|
|
2863
|
+
assert_eq!(countOps(&inplace, "Call"), 1, "{inplace:?}");
|
|
2864
|
+
assert_eq!(runMerged::<i64>("awproved_run", &src), 6);
|
|
2865
|
+
// with `--no-fbip` the same site copies
|
|
2866
|
+
let plain = compiledFbip("awplain", &src, false);
|
|
2867
|
+
assert!(!hasFn(&plain, "withUnique"), "{:?}", bodyNames(&plain));
|
|
2868
|
+
let copy = namedBodies(&plain).into_iter().find(|(n, _)| n.ends_with(".with")).expect("with compiled").1;
|
|
2869
|
+
assert!(countOps(©, "Call") >= 2, "resize and set: {copy:?}");
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
const VEC_SRC: &str = "struct Vec(cells: Array[Int], n: Int) =
|
|
2873
|
+
fun append(self, x: Int) -> Vec = Vec(..self, cells: self.cells.with(self.n, x), n: self.n + 1)
|
|
2874
|
+
fun get(self, i: Int) -> Int = self.cells.get(i)
|
|
2875
|
+
|
|
2876
|
+
fun fill(k: Int) -> Vec =
|
|
2877
|
+
v := Vec(cells: Array.filled(8, 0), n: 0)
|
|
2878
|
+
for i := range k
|
|
2879
|
+
v = v.append(i * 2)
|
|
2880
|
+
v
|
|
2881
|
+
";
|
|
2882
|
+
|
|
2883
|
+
#[test]
|
|
2884
|
+
fn array_with_in_a_unique_struct_field_writes_in_place() {
|
|
2885
|
+
// `Vec(..self, cells: self.cells.with(..))` of a unique `self`: no copy of the array.
|
|
2886
|
+
let src = format!("{WITH_HEAD}{VEC_SRC}\nfun main() -> Int =\n fill(4).get(3)\n");
|
|
2887
|
+
let bytes = compiledFbip("awvec", &src, true);
|
|
2888
|
+
assert!(hasFn(&bytes, "withUnique"), "{:?}", bodyNames(&bytes));
|
|
2889
|
+
assert!(!hasCopyingWith(&bytes), "{:?}", bodyNames(&bytes));
|
|
2890
|
+
let inplace = namedBodies(&bytes).into_iter().find(|(n, _)| n.contains("withUnique")).unwrap().1;
|
|
2891
|
+
assert_eq!(countOps(&inplace, "ArrayCopy"), 0, "{inplace:?}");
|
|
2892
|
+
assert_eq!(runMerged::<i64>("awvec_run", &src), 6);
|
|
2893
|
+
let plain = compiledFbip("awvecplain", &src, false);
|
|
2894
|
+
assert!(!hasFn(&plain, "withUnique"));
|
|
2895
|
+
assert!(hasCopyingWith(&plain));
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
#[test]
|
|
2899
|
+
fn array_with_unproved_site_copies() {
|
|
2900
|
+
// The old array is still held by `b`: the site must copy (`resize` allocates and copies).
|
|
2901
|
+
let src = format!(
|
|
2902
|
+
"{WITH_HEAD}fun main() -> Int =\n a := Array.filled(4, 0)\n b := a\n a = a.with(1, 5)\n b.get(1) * 10 + a.get(1)\n"
|
|
2903
|
+
);
|
|
2904
|
+
let bytes = compiledFbip("awunproved", &src, true);
|
|
2905
|
+
assert!(!hasFn(&bytes, "withUnique"), "{:?}", bodyNames(&bytes));
|
|
2906
|
+
let resize = namedBodies(&bytes).into_iter().find(|(n, _)| n.contains("resize")).expect("resize compiled").1;
|
|
2907
|
+
assert!(countOps(&resize, "ArrayCopy") >= 1, "{resize:?}");
|
|
2908
|
+
assert_eq!(runMerged::<i64>("awunproved_run", &src), 5);
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
#[test]
|
|
2912
|
+
fn array_with_in_a_shared_struct_copies() {
|
|
2913
|
+
// Two holders of the vector: its array is not written in place.
|
|
2914
|
+
let src = format!(
|
|
2915
|
+
"{WITH_HEAD}{VEC_SRC}\nfun main() -> Int =\n v := fill(2)\n w := v\n v = v.append(9)\n w.get(2) * 10 + v.get(2)\n"
|
|
2916
|
+
);
|
|
2917
|
+
let bytes = compiledFbip("awshared", &src, true);
|
|
2918
|
+
assert!(!hasFn(&bytes, "withUnique"), "{:?}", bodyNames(&bytes));
|
|
2919
|
+
assert_eq!(runMerged::<i64>("awshared_run", &src), 9);
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
#[test]
|
|
2923
|
+
fn array_with_in_place_twin_cannot_be_called_from_source() {
|
|
2924
|
+
let dir = std::env::temp_dir().join(format!("plum_awu_{}", std::process::id()));
|
|
2925
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
2926
|
+
let path = dir.join("main.plum");
|
|
2927
|
+
std::fs::write(
|
|
2928
|
+
&path,
|
|
2929
|
+
format!("{WITH_HEAD}fun main() -> Int =\n a := Array.filled(2, 0)\n a.withUnique(0, 1).get(0)\n"),
|
|
2930
|
+
)
|
|
2931
|
+
.unwrap();
|
|
2932
|
+
let source = plum_core::loadAndMergeDiag(&path, &repoRoot()).expect("load");
|
|
2933
|
+
let errs = plum_checker::checkSource(&source).expect_err("withUnique is for the compiler only");
|
|
2934
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
2935
|
+
assert!(errs.iter().any(|e| e.message.contains("for the compiler only")), "{errs:?}");
|
|
2936
|
+
}
|
docs/backlog.md
CHANGED
|
@@ -53,6 +53,7 @@ Open work, in the order the user queued it, plus smaller items found during buil
|
|
|
53
53
|
|
|
54
54
|
## Smaller open items
|
|
55
55
|
|
|
56
|
+
- **Locals named like functions, remaining cases** (found 2026-11-06, F-PUR-1 note, pre-existing, with or without fusion): in wasm-codegen, a flat local named like a free function traps or gives invalid wasm; a local in an embedded struct's `init` that captures a free call is silently wrong. The second goes away if inheritance is removed.
|
|
56
57
|
- **Pattern-shadowed parameter gives invalid wasm** (found 2026-11-06, pre-existing, with or without fusion): a parameter shadowed by a pattern and read in a sibling arm compiles to wasm that won't load. Details in the game engine run's note `F-T6-2`.
|
|
57
58
|
- **Closure self-call, still open** (`F-CLOSURE-1` fixed the typed-argument and arity checks; a self-call now errors in the checker when an argument does not fit a known parameter type). What stays: (a) a parameter with no declared type and no hint from a later call (`rec := |n| .. rec("x")` passed to `map`) has no type to check against; (b) `rec(None, [], n-1)` / `rec(Some(n), ..)` with untyped params still fail in codegen (invalid wasm or `type name 'Nil' is not yet supported as a value`), because the self-call's arguments are inferred with no expected type; checking them with the parameter as expected type fixes those but changes the wasm of programs that run today (e.g. a typed `Option[Int]` param called with `None`), so do it in a run that may re-baseline wasm; (c) a self-call from a nested closure (`outer := |n| inner := |m| outer(m-1) ..`) fails in codegen with `unknown function 'outer'`; (d) a typed declaration `f: fn(Int) -> Int := |n| ..` does not bind `f` inside its body; (e) a failed closure initialiser also reports a second `'rec' is not a function` at its first use.
|
|
58
59
|
- **LSP columns are bytes, not UTF-16** (phase 4, pre-existing): non-ASCII text before an error underlines the wrong range. Spans take tree-sitter byte columns.
|
docs/plans/2026-11-06-fbip-stage1.md
CHANGED
|
@@ -48,7 +48,7 @@ e.g. the doc gives ms for 1M `Slice.push` on a local, on a struct field, and thr
|
|
|
48
48
|
**Test code:** the probes run under `plum test` with an answer check, so they stay correct.
|
|
49
49
|
|
|
50
50
|
### T2 no-fbip switch · Complexity 2 · Est. context 30k
|
|
51
|
-
**Owns:** compiler/driver/src/*.rs, compiler/cli/src/*.rs, compiler/cli/tests/cli/*.rs, compiler/core/src/uniqueness.rs, compiler/game/web-test/src/*.rs
|
|
51
|
+
**Owns:** compiler/driver/src/*.rs, compiler/driver/tests/*.rs, compiler/lsp/src/*.rs, compiler/cli/src/*.rs, compiler/cli/tests/cli/*.rs, compiler/core/src/uniqueness.rs, compiler/game/web-test/src/*.rs
|
|
52
52
|
**Components:** 1
|
|
53
53
|
**LOC owned:** 60
|
|
54
54
|
**Tests:** fast
|
docs/plans/2026-11-06-fbip.benchmark.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# FBIP stage 1: baseline
|
|
2
|
+
|
|
3
|
+
Taken 2026-11-06 on master before any FBIP change (release builds of `plum` and `plum-web-test`). The machine was busy: load average 8 to 12 during the stress runs (11.9 at the start, 12.8 at the end) and 8 to 9 during the probes. Every number here is an upper bound for a quiet machine.
|
|
4
|
+
|
|
5
|
+
## Stress fixtures
|
|
6
|
+
|
|
7
|
+
`plum-web-test <fixture> --measure 5 --json`, headless software WebGPU, 60 fps vsync-capped, three runs each. Values in ms. Load is the 1-minute average from `uptime` before and after each run.
|
|
8
|
+
|
|
9
|
+
| fixture | run | update | draw | render | load before -> after |
|
|
10
|
+
|---|---|---|---|---|---|
|
|
11
|
+
| stress_scene | 1 | 0.066 | 1.304 | 0.316 | 11.9 -> 11.8 |
|
|
12
|
+
| stress_scene | 2 | 0.066 | 1.287 | 0.309 | 11.8 -> 11.0 |
|
|
13
|
+
| stress_scene | 3 | 0.073 | 1.294 | 0.303 | 11.0 -> 9.8 |
|
|
14
|
+
| stress_world | 1 | 0.605 | 0.662 | 0.132 | 9.8 -> 10.5 |
|
|
15
|
+
| stress_world | 2 | 0.569 | 0.588 | 0.133 | 10.5 -> 9.3 |
|
|
16
|
+
| stress_world | 3 | 0.579 | 0.580 | 0.128 | 9.3 -> 8.3 |
|
|
17
|
+
| stress_pure_scene | 1 | 0.222 | 1.218 | 0.233 | 8.3 -> 8.1 |
|
|
18
|
+
| stress_pure_scene | 2 | 0.262 | 1.353 | 0.261 | 8.1 -> 9.7 |
|
|
19
|
+
| stress_pure_scene | 3 | 0.244 | 1.247 | 0.259 | 9.7 -> 11.5 |
|
|
20
|
+
| stress_state_view | 1 | 0.063 | 1.549 | 0.247 | 11.5 -> 11.7 |
|
|
21
|
+
| stress_state_view | 2 | 0.067 | 1.515 | 0.253 | 11.7 -> 12.1 |
|
|
22
|
+
| stress_state_view | 3 | 0.070 | 1.547 | 0.254 | 12.1 -> 12.8 |
|
|
23
|
+
|
|
24
|
+
Against the final numbers in docs/plans/2026-11-05-game-engine.results.md (also taken under load 8 to 12): stress_scene update 0.060 -> 0.068, stress_world update 0.600 -> 0.584, stress_pure_scene update 0.218 -> 0.243, stress_state_view update 0.052 -> 0.067. Draw is within 5% everywhere. Updates move by up to 25% between sessions under this load, so only a change much larger than that means anything.
|
|
25
|
+
|
|
26
|
+
`--uncapped` was tried twice on stress_world (update 0.649 and 0.646, draw 0.620 and 0.629, render about 6.0). Update and draw were no steadier than the capped runs and render became dominated by the uncapped GPU work, so all fixtures use the capped mode.
|
|
27
|
+
|
|
28
|
+
## Collection probes
|
|
29
|
+
|
|
30
|
+
`tests/fbip_probes/*.plum`; each prints its own `Time.now(io)` delta around the loop. `target/release/plum run` from the repo root, 5 runs, median. The clock has 1 ms resolution, so `array_set` and `struct_spread` are at its floor.
|
|
31
|
+
|
|
32
|
+
| probe | size | runs (ms) | median ms |
|
|
33
|
+
|---|---|---|---|
|
|
34
|
+
| slice_push_local | 1M `Slice.push` on a local | 8, 7, 8, 8, 7 | 8 |
|
|
35
|
+
| slice_push_field | 1M `Slice.push` on a `mut` struct field | 9, 9, 10, 9, 8 | 9 |
|
|
36
|
+
| slice_push_helper | 1M `Slice.push` through a helper taking the slice | 8, 7, 8, 7, 8 | 8 |
|
|
37
|
+
| array_set | 1M `Array.set` over 1000 slots | 1, 2, 2, 2, 1 | 2 |
|
|
38
|
+
| map_set_get | 100k `Map.set` then 100k `Map.get` | 13, 14, 12, 15, 13 | 13 |
|
|
39
|
+
| buffer_write | 1M `Buffer.writeByte` | 7, 7, 7, 7, 8 | 7 |
|
|
40
|
+
| struct_spread | 1M `c = Cell(..c, n: c.n + 1)`, boxed `Cell` | 1, 2, 1, 1, 2 | 1 |
|
|
41
|
+
|
|
42
|
+
`--explain-fusion` shows `unique spread at run:12` for `struct_spread`, so the spread there is already proved unique and updated in place. No other probe contains a spread or an unproved write. `Slice.push`, `Array.set`, `Map.set` and `Buffer.writeByte` mutate in place today through `mut` fields, so these probes are the cost of the existing mutating APIs, the figure a persistent `with`-style API must be compared against.
|
|
43
|
+
|
|
44
|
+
Each probe has a `test` at size 1000 and the sweep covers them.
|
|
45
|
+
|
|
46
|
+
## Compile time, size, test sweep
|
|
47
|
+
|
|
48
|
+
| item | value |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `plum build examples/games/functional/game_state_view.plum -o <scratch>` (release), 3 runs | 0.77, 0.75, 0.75 s |
|
|
51
|
+
| `plum.wasm` for that game | 79,397 bytes |
|
|
52
|
+
| `target/debug/plum test std examples tests` | 61 s wall (83 s user), 786 tests, 178 files, load 5 to 8 |
|
std/Array.plum
CHANGED
|
@@ -60,6 +60,22 @@ primitive Array[T] =
|
|
|
60
60
|
fun length(self) -> Int =
|
|
61
61
|
todo
|
|
62
62
|
|
|
63
|
+
# `self` with element `i` replaced by `v`, as a new array: `self` itself is left untouched.
|
|
64
|
+
# When the compiler proves nobody else holds `self` and `self` is dead afterwards (a local
|
|
65
|
+
# rebound with `a = a.with(i, v)`, or a unique struct's array rebuilt with
|
|
66
|
+
# `T(..self, data: self.data.with(i, v))`), the call runs `withUnique` and writes `self`
|
|
67
|
+
# in place. Traps if `i` is out of range, like `set`.
|
|
68
|
+
fun with(self, i: Int, v: T) -> Array[T] =
|
|
69
|
+
c := self.resize(self.length())
|
|
70
|
+
c.set(i, v)
|
|
71
|
+
return c
|
|
72
|
+
|
|
73
|
+
# The in-place `with`, picked by the compiler alone: the checker rejects a call written in
|
|
74
|
+
# source, so no program can write through an array without the proof.
|
|
75
|
+
fun withUnique(self, i: Int, v: T) -> Array[T] =
|
|
76
|
+
self.set(i, v)
|
|
77
|
+
return self
|
|
78
|
+
|
|
63
79
|
# Returns a NEW array, one longer than `self`, with `v` appended at the end
|
|
64
80
|
# — `self` itself is left untouched (this allocates and copies, it does not
|
|
65
81
|
# mutate in place). Callers grow in a loop by reassigning: `arr =
|
tests/fbip/aliasing.plum
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import std/Int
|
|
2
|
+
import std/Str
|
|
3
|
+
import std/Slice
|
|
4
|
+
import std/Map
|
|
5
|
+
import std/Array
|
|
6
|
+
import std/Byte
|
|
7
|
+
import std/Float
|
|
8
|
+
import std/game/Vec2
|
|
9
|
+
|
|
10
|
+
# FBIP aliasing tests. Every program here would give a different answer if a spread
|
|
11
|
+
# were wrongly written in place while the old value is still reachable. Each test
|
|
12
|
+
# asserts the copying answer; the sweep runs this file with the default and with `--no-fbip`.
|
|
13
|
+
# `label: Str` keeps `Cell` boxed (a flat struct could never be shared).
|
|
14
|
+
|
|
15
|
+
struct Cell(label: Str, mut n: Int) =
|
|
16
|
+
fun bumped(self) -> Cell = Cell(..self, n: self.n + 1)
|
|
17
|
+
fun same(self) -> Cell = self
|
|
18
|
+
fun withN(self, k: Int) -> Cell = Cell(..self, n: k)
|
|
19
|
+
fun pair(self) -> Pair[Cell, Cell] = Pair(self.bumped(), self)
|
|
20
|
+
|
|
21
|
+
fun bump(c: Cell) -> Cell = Cell(..c, n: c.n + 1)
|
|
22
|
+
|
|
23
|
+
# Returns its argument after changing it through a `mut` field.
|
|
24
|
+
fun touch(c: Cell) -> Cell =
|
|
25
|
+
c.n = c.n + 10
|
|
26
|
+
c
|
|
27
|
+
|
|
28
|
+
# Returns its argument after storing it.
|
|
29
|
+
fun reg(xs: Slice[Cell], c: Cell) -> Cell =
|
|
30
|
+
xs.push(c)
|
|
31
|
+
c
|
|
32
|
+
|
|
33
|
+
fun twice(c: Cell) -> Cell = bump(bump(c))
|
|
34
|
+
|
|
35
|
+
struct Holder(mut a: Cell, mut b: Cell)
|
|
36
|
+
struct Box(mut c: Cell, tag: Int)
|
|
37
|
+
|
|
38
|
+
struct Zero(label: Str, n: Int) =
|
|
39
|
+
Origin = Zero(label: "z", n: 0)
|
|
40
|
+
fun inc(self) -> Zero = Zero(..self, n: self.n + 1)
|
|
41
|
+
|
|
42
|
+
# ---- a base with an override that stores self ----
|
|
43
|
+
|
|
44
|
+
struct Base(label: Str, n: Int) =
|
|
45
|
+
fun step(self) -> Base = Base(..self, n: self.n + 1)
|
|
46
|
+
|
|
47
|
+
struct Sub(Base(label, n), log: Slice[Base]) =
|
|
48
|
+
override fun step(self) -> Base =
|
|
49
|
+
self.log.push(self)
|
|
50
|
+
Base(label: self.label, n: self.n + 1)
|
|
51
|
+
|
|
52
|
+
struct Wide(label: Str, n: Int, extra: Int) =
|
|
53
|
+
fun step(self) -> Wide = Wide(..self, n: self.n + 1)
|
|
54
|
+
|
|
55
|
+
struct Narrow(Wide(label, n, extra), flag: Int)
|
|
56
|
+
|
|
57
|
+
# ---- helpers ----
|
|
58
|
+
|
|
59
|
+
fun keep(xs: Slice[Cell], c: Cell) -> Cell =
|
|
60
|
+
xs.push(c)
|
|
61
|
+
bump(c)
|
|
62
|
+
|
|
63
|
+
fun both(c: Cell) -> Pair[Cell, Cell] = Pair(c, bump(c))
|
|
64
|
+
|
|
65
|
+
fun branchy(c: Cell, flag: Bool) -> Int =
|
|
66
|
+
d := c
|
|
67
|
+
if flag
|
|
68
|
+
d = bump(d)
|
|
69
|
+
c.n + d.n
|
|
70
|
+
|
|
71
|
+
fun matched(c: Cell) -> Int =
|
|
72
|
+
match c
|
|
73
|
+
Cell(_, n) => bump(c).n + n
|
|
74
|
+
|
|
75
|
+
fun reader(c: Cell, xs: Slice[Cell]) -> Int =
|
|
76
|
+
xs.push(c)
|
|
77
|
+
0
|
|
78
|
+
|
|
79
|
+
fun viaReader(c: Cell, xs: Slice[Cell]) -> Cell =
|
|
80
|
+
_ := reader(c, xs)
|
|
81
|
+
bump(c)
|
|
82
|
+
|
|
83
|
+
fun ident[T](x: T) -> T = x
|
|
84
|
+
|
|
85
|
+
fun gen(c: Cell) -> Cell = bump(ident(c))
|
|
86
|
+
|
|
87
|
+
fun withDefault(c: Cell, d: Cell = Cell(label: "d", n: 100)) -> Int =
|
|
88
|
+
bump(d).n + d.n + c.n
|
|
89
|
+
|
|
90
|
+
fun shared(label: Str, n: Int) -> Cell = Cell(label: label, n: n)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ---- names that look like a fn or a method but are closures ----
|
|
94
|
+
|
|
95
|
+
fun fresher(lg: Slice[Int]) -> Cell =
|
|
96
|
+
lg.push(1)
|
|
97
|
+
Cell(label: "m", n: 100)
|
|
98
|
+
|
|
99
|
+
fun viaParam(fresher: fn(Slice[Int]) -> Cell, lg: Slice[Int]) -> Cell =
|
|
100
|
+
y := fresher(lg)
|
|
101
|
+
y = Cell(..y, n: 9)
|
|
102
|
+
y
|
|
103
|
+
|
|
104
|
+
struct Maker(k: Int) =
|
|
105
|
+
fun made(self, lg: Slice[Int]) -> Cell =
|
|
106
|
+
lg.push(1)
|
|
107
|
+
Cell(label: "m", n: 100)
|
|
108
|
+
|
|
109
|
+
struct Hook(made: fn(Slice[Int]) -> Cell)
|
|
110
|
+
|
|
111
|
+
# ---- Array.with: written in place only for a local array nobody else holds ----
|
|
112
|
+
|
|
113
|
+
struct ArrBox(mut xs: Array[Int], tag: Int)
|
|
114
|
+
|
|
115
|
+
fun firstTo9(a: Array[Int]) -> Array[Int] = a.with(0, 9)
|
|
116
|
+
|
|
117
|
+
fun makeZeros(n: Int) -> Array[Int] = Array.filled(n, 0)
|
|
118
|
+
|
|
119
|
+
fun keepAndSet(xs: Slice[Array[Int]], a: Array[Int]) -> Array[Int] =
|
|
120
|
+
xs.push(a)
|
|
121
|
+
a.with(0, 7)
|
|
122
|
+
|
|
123
|
+
fun doubled(n: Int) -> Array[Int] =
|
|
124
|
+
a := Array.filled(n, 1)
|
|
125
|
+
for i := range n
|
|
126
|
+
a = a.with(i, a.get(i) * 2 + i)
|
|
127
|
+
a
|
|
128
|
+
|
|
129
|
+
fun lastWith(n: Int) -> Array[Int] =
|
|
130
|
+
a := Array.filled(n, 3)
|
|
131
|
+
a.set(0, 4)
|
|
132
|
+
a.with(n - 1, 8)
|
|
133
|
+
|
|
134
|
+
fun twoDecls(flag: Bool) -> Array[Int] =
|
|
135
|
+
a := Array.filled(2, 0)
|
|
136
|
+
if flag
|
|
137
|
+
a = a.with(0, 1)
|
|
138
|
+
b := a
|
|
139
|
+
a = a.with(1, 5)
|
|
140
|
+
b
|
|
141
|
+
|
|
142
|
+
# ---- tests ----
|
|
143
|
+
|
|
144
|
+
test "alias kept then updated"
|
|
145
|
+
a := Cell(label: "a", n: 0)
|
|
146
|
+
b := a
|
|
147
|
+
c := bump(b)
|
|
148
|
+
assert a.n == 0
|
|
149
|
+
assert b.n == 0
|
|
150
|
+
assert c.n == 1
|
|
151
|
+
|
|
152
|
+
test "alias kept then updated through a method"
|
|
153
|
+
a := Cell(label: "a", n: 0)
|
|
154
|
+
b := a
|
|
155
|
+
c := a.bumped()
|
|
156
|
+
assert b.n == 0
|
|
157
|
+
assert c.n == 1
|
|
158
|
+
|
|
159
|
+
test "stored in a slice then updated"
|
|
160
|
+
xs := Slice[Cell]()
|
|
161
|
+
a := Cell(label: "a", n: 0)
|
|
162
|
+
xs.push(a)
|
|
163
|
+
b := bump(a)
|
|
164
|
+
assert xs.at(0).n == 0
|
|
165
|
+
assert b.n == 1
|
|
166
|
+
|
|
167
|
+
test "stored in a slice by a helper then updated"
|
|
168
|
+
xs := Slice[Cell]()
|
|
169
|
+
a := Cell(label: "a", n: 0)
|
|
170
|
+
b := keep(xs, a)
|
|
171
|
+
assert xs.at(0).n == 0
|
|
172
|
+
assert b.n == 1
|
|
173
|
+
|
|
174
|
+
test "stored in a map then updated"
|
|
175
|
+
m: Map[Int, Cell] := Map.empty()
|
|
176
|
+
a := Cell(label: "a", n: 0)
|
|
177
|
+
m.set(1, a)
|
|
178
|
+
b := a.bumped()
|
|
179
|
+
assert m.get(1)!.n == 0
|
|
180
|
+
assert b.n == 1
|
|
181
|
+
|
|
182
|
+
test "stored in a field then updated"
|
|
183
|
+
a := Cell(label: "a", n: 0)
|
|
184
|
+
h := Holder(a: a, b: Cell(label: "b", n: 0))
|
|
185
|
+
b := bump(a)
|
|
186
|
+
assert h.a.n == 0
|
|
187
|
+
assert b.n == 1
|
|
188
|
+
|
|
189
|
+
test "captured by a closure before the update"
|
|
190
|
+
a := Cell(label: "a", n: 0)
|
|
191
|
+
f := || a.n
|
|
192
|
+
b := bump(a)
|
|
193
|
+
assert f() == 0
|
|
194
|
+
assert b.n == 1
|
|
195
|
+
|
|
196
|
+
test "captured by a closure after the update"
|
|
197
|
+
a := Cell(label: "a", n: 0)
|
|
198
|
+
a = bump(a)
|
|
199
|
+
f := || a.n
|
|
200
|
+
a = bump(a)
|
|
201
|
+
assert f() == 1
|
|
202
|
+
assert a.n == 2
|
|
203
|
+
|
|
204
|
+
test "returned alongside another reference"
|
|
205
|
+
a := Cell(label: "a", n: 0)
|
|
206
|
+
p := both(a)
|
|
207
|
+
assert p.first.n == 0
|
|
208
|
+
assert p.second.n == 1
|
|
209
|
+
assert a.n == 0
|
|
210
|
+
|
|
211
|
+
test "returned alongside self"
|
|
212
|
+
a := Cell(label: "a", n: 0)
|
|
213
|
+
p := a.pair()
|
|
214
|
+
assert p.first.n == 1
|
|
215
|
+
assert p.second.n == 0
|
|
216
|
+
|
|
217
|
+
test "the same value in two fields"
|
|
218
|
+
a := Cell(label: "a", n: 0)
|
|
219
|
+
h := Holder(a: a, b: a)
|
|
220
|
+
h.a = bump(h.a)
|
|
221
|
+
assert h.a.n == 1
|
|
222
|
+
assert h.b.n == 0
|
|
223
|
+
|
|
224
|
+
test "the same value in two fields, one updated through a method"
|
|
225
|
+
a := Cell(label: "a", n: 0)
|
|
226
|
+
h := Holder(a: a, b: a)
|
|
227
|
+
h.b = h.b.bumped()
|
|
228
|
+
assert h.a.n == 0
|
|
229
|
+
assert h.b.n == 1
|
|
230
|
+
assert a.n == 0
|
|
231
|
+
|
|
232
|
+
test "a shared value passed through a method"
|
|
233
|
+
a := Cell(label: "a", n: 0)
|
|
234
|
+
b := a.same()
|
|
235
|
+
c := b.bumped()
|
|
236
|
+
assert a.n == 0
|
|
237
|
+
assert c.n == 1
|
|
238
|
+
|
|
239
|
+
test "a shared value passed through a method then handed over in a loop"
|
|
240
|
+
a := Cell(label: "a", n: 0)
|
|
241
|
+
v := a.same()
|
|
242
|
+
for i := range 3
|
|
243
|
+
v = v.bumped()
|
|
244
|
+
assert a.n == 0
|
|
245
|
+
assert v.n == 3
|
|
246
|
+
|
|
247
|
+
test "an override that stores self"
|
|
248
|
+
s := Sub(label: "s", n: 0, log: Slice[Base]())
|
|
249
|
+
b: Base := s
|
|
250
|
+
c := b.step()
|
|
251
|
+
assert s.log.at(0).n == 0
|
|
252
|
+
assert c.n == 1
|
|
253
|
+
|
|
254
|
+
test "a base spread of a subtype stays a copy of the base"
|
|
255
|
+
w := Narrow(label: "n", n: 0, extra: 5, flag: 1)
|
|
256
|
+
x: Wide := w
|
|
257
|
+
y := x.step()
|
|
258
|
+
assert w.n == 0
|
|
259
|
+
assert y.n == 1
|
|
260
|
+
assert y.extra == 5
|
|
261
|
+
|
|
262
|
+
test "an update in only one branch with the old value read after"
|
|
263
|
+
a := Cell(label: "a", n: 0)
|
|
264
|
+
assert branchy(a, true) == 1
|
|
265
|
+
assert branchy(a, false) == 0
|
|
266
|
+
assert a.n == 0
|
|
267
|
+
|
|
268
|
+
test "a value matched into a binding"
|
|
269
|
+
a := Cell(label: "a", n: 5)
|
|
270
|
+
assert matched(a) == 11
|
|
271
|
+
assert a.n == 5
|
|
272
|
+
|
|
273
|
+
test "a value read through a helper that keeps a reference"
|
|
274
|
+
xs := Slice[Cell]()
|
|
275
|
+
a := Cell(label: "a", n: 0)
|
|
276
|
+
b := viaReader(a, xs)
|
|
277
|
+
assert xs.at(0).n == 0
|
|
278
|
+
assert b.n == 1
|
|
279
|
+
|
|
280
|
+
test "a generic function instantiated with both kinds"
|
|
281
|
+
a := Cell(label: "a", n: 0)
|
|
282
|
+
b := gen(a)
|
|
283
|
+
assert a.n == 0
|
|
284
|
+
assert b.n == 1
|
|
285
|
+
assert ident(3) == 3
|
|
286
|
+
c := gen(Cell(label: "c", n: 7))
|
|
287
|
+
assert c.n == 8
|
|
288
|
+
|
|
289
|
+
test "a loop keeping the previous value"
|
|
290
|
+
v := Cell(label: "v", n: 0)
|
|
291
|
+
prev := Slice[Cell]()
|
|
292
|
+
for i := range 3
|
|
293
|
+
prev.push(v)
|
|
294
|
+
v = v.bumped()
|
|
295
|
+
assert v.n == 3
|
|
296
|
+
assert prev.at(0).n == 0
|
|
297
|
+
assert prev.at(1).n == 1
|
|
298
|
+
assert prev.at(2).n == 2
|
|
299
|
+
|
|
300
|
+
test "a loop handing over a value bound outside it while another holder keeps it"
|
|
301
|
+
start := Cell(label: "s", n: 0)
|
|
302
|
+
keepIt := Holder(a: start, b: start)
|
|
303
|
+
v := start
|
|
304
|
+
for i := range 3
|
|
305
|
+
v = bump(v)
|
|
306
|
+
assert v.n == 3
|
|
307
|
+
assert start.n == 0
|
|
308
|
+
assert keepIt.a.n == 0
|
|
309
|
+
|
|
310
|
+
test "a default argument value"
|
|
311
|
+
assert withDefault(Cell(label: "c", n: 1)) == 202
|
|
312
|
+
assert withDefault(Cell(label: "c", n: 1)) == 202
|
|
313
|
+
|
|
314
|
+
test "a constant is cached and shared"
|
|
315
|
+
a := Zero.Origin.inc()
|
|
316
|
+
b := Zero.Origin.inc()
|
|
317
|
+
assert a.n == 1
|
|
318
|
+
assert b.n == 1
|
|
319
|
+
assert Zero.Origin.n == 0
|
|
320
|
+
|
|
321
|
+
test "a field of a mut field of another struct"
|
|
322
|
+
a := Cell(label: "a", n: 0)
|
|
323
|
+
box := Box(c: a, tag: 1)
|
|
324
|
+
box.c = bump(box.c)
|
|
325
|
+
assert a.n == 0
|
|
326
|
+
assert box.c.n == 1
|
|
327
|
+
|
|
328
|
+
test "a value read out of a mut field and handed on"
|
|
329
|
+
box := Box(c: Cell(label: "a", n: 0), tag: 1)
|
|
330
|
+
held := box.c
|
|
331
|
+
v := twice(box.c)
|
|
332
|
+
assert held.n == 0
|
|
333
|
+
assert v.n == 2
|
|
334
|
+
assert box.c.n == 0
|
|
335
|
+
|
|
336
|
+
test "a value read out of a mut field and updated by a method while the field stays"
|
|
337
|
+
box := Box(c: Cell(label: "a", n: 0), tag: 1)
|
|
338
|
+
u := box.c.bumped()
|
|
339
|
+
assert box.c.n == 0
|
|
340
|
+
assert u.n == 1
|
|
341
|
+
|
|
342
|
+
test "a spread source written earlier through a mut field"
|
|
343
|
+
box := Box(c: Cell(label: "a", n: 0), tag: 1)
|
|
344
|
+
box.c = Cell(label: "b", n: 4)
|
|
345
|
+
other := box.c
|
|
346
|
+
box.c = box.c.bumped()
|
|
347
|
+
assert other.n == 4
|
|
348
|
+
assert box.c.n == 5
|
|
349
|
+
|
|
350
|
+
test "a hand-over loop in a field keeps an earlier snapshot"
|
|
351
|
+
box := Box(c: Cell(label: "a", n: 0), tag: 1)
|
|
352
|
+
snap := box.c
|
|
353
|
+
for i := range 3
|
|
354
|
+
box.c = box.c.bumped()
|
|
355
|
+
assert snap.n == 0
|
|
356
|
+
assert box.c.n == 3
|
|
357
|
+
|
|
358
|
+
test "a fresh value handed over repeatedly is correct"
|
|
359
|
+
v := Cell(label: "f", n: 0)
|
|
360
|
+
for i := range 4
|
|
361
|
+
v = v.bumped()
|
|
362
|
+
assert v.n == 4
|
|
363
|
+
w := twice(shared("w", 10))
|
|
364
|
+
assert w.n == 12
|
|
365
|
+
|
|
366
|
+
test "a value that is both unique and shared at call sites"
|
|
367
|
+
a := Cell(label: "a", n: 0)
|
|
368
|
+
fresh := bump(Cell(label: "f", n: 10))
|
|
369
|
+
held := bump(a)
|
|
370
|
+
assert a.n == 0
|
|
371
|
+
assert held.n == 1
|
|
372
|
+
assert fresh.n == 11
|
|
373
|
+
|
|
374
|
+
test "a returned parameter that was stored then updated"
|
|
375
|
+
xs := Slice[Cell]()
|
|
376
|
+
v := Cell(label: "v", n: 0)
|
|
377
|
+
v = reg(xs, v)
|
|
378
|
+
w := bump(v)
|
|
379
|
+
assert xs.at(0).n == 0
|
|
380
|
+
assert w.n == 1
|
|
381
|
+
|
|
382
|
+
test "a returned parameter handed over in a loop keeps a stored snapshot"
|
|
383
|
+
xs := Slice[Cell]()
|
|
384
|
+
v := Cell(label: "v", n: 0)
|
|
385
|
+
for i := range 3
|
|
386
|
+
v = reg(xs, v)
|
|
387
|
+
v = bump(v)
|
|
388
|
+
assert xs.at(0).n == 0
|
|
389
|
+
assert xs.at(1).n == 1
|
|
390
|
+
assert xs.at(2).n == 2
|
|
391
|
+
assert v.n == 3
|
|
392
|
+
|
|
393
|
+
test "a returned parameter changed through a mut field with an alias kept"
|
|
394
|
+
a := Cell(label: "a", n: 0)
|
|
395
|
+
b := a
|
|
396
|
+
c := touch(a)
|
|
397
|
+
d := bump(c)
|
|
398
|
+
assert b.n == 10
|
|
399
|
+
assert d.n == 11
|
|
400
|
+
assert c.n == 10
|
|
401
|
+
|
|
402
|
+
test "a returned parameter changed through a mut field with a fresh argument"
|
|
403
|
+
c := touch(Cell(label: "a", n: 1))
|
|
404
|
+
d := bump(c)
|
|
405
|
+
assert c.n == 11
|
|
406
|
+
assert d.n == 12
|
|
407
|
+
|
|
408
|
+
test "a returned parameter in a field keeps the other holder"
|
|
409
|
+
a := Cell(label: "a", n: 0)
|
|
410
|
+
box := Box(c: a, tag: 1)
|
|
411
|
+
box.c = touch(box.c)
|
|
412
|
+
box.c = bump(box.c)
|
|
413
|
+
assert a.n == 10
|
|
414
|
+
assert box.c.n == 11
|
|
415
|
+
|
|
416
|
+
test "a local closure named like a fresh fn"
|
|
417
|
+
lg := Slice[Int]()
|
|
418
|
+
cap := Cell(label: "s", n: 0)
|
|
419
|
+
fresher := |l| cap
|
|
420
|
+
y := fresher(lg)
|
|
421
|
+
y = Cell(..y, n: 7)
|
|
422
|
+
assert cap.n == 0
|
|
423
|
+
assert y.n == 7
|
|
424
|
+
|
|
425
|
+
test "a fn-typed parameter named like a fresh fn"
|
|
426
|
+
lg := Slice[Int]()
|
|
427
|
+
cap := Cell(label: "s", n: 0)
|
|
428
|
+
r := viaParam(|l| cap, lg)
|
|
429
|
+
assert cap.n == 0
|
|
430
|
+
assert r.n == 9
|
|
431
|
+
assert fresher(lg).n == 100
|
|
432
|
+
|
|
433
|
+
test "a closure field named like a fresh method"
|
|
434
|
+
lg := Slice[Int]()
|
|
435
|
+
cap := Cell(label: "s", n: 0)
|
|
436
|
+
h := Hook(made: |l| cap)
|
|
437
|
+
y := h.made(lg)
|
|
438
|
+
y = Cell(..y, n: 7)
|
|
439
|
+
assert cap.n == 0
|
|
440
|
+
assert y.n == 7
|
|
441
|
+
assert Maker(k: 1).made(lg).n == 100
|
|
442
|
+
|
|
443
|
+
test "array: alias then with"
|
|
444
|
+
a := Array.filled(3, 0)
|
|
445
|
+
b := a
|
|
446
|
+
a = a.with(1, 5)
|
|
447
|
+
assert b.get(1) == 0
|
|
448
|
+
assert a.get(1) == 5
|
|
449
|
+
|
|
450
|
+
test "array: alias taken after with"
|
|
451
|
+
a := Array.filled(3, 0)
|
|
452
|
+
a = a.with(1, 5)
|
|
453
|
+
b := a
|
|
454
|
+
a = a.with(2, 6)
|
|
455
|
+
assert b.get(2) == 0
|
|
456
|
+
assert b.get(1) == 5
|
|
457
|
+
assert a.get(2) == 6
|
|
458
|
+
|
|
459
|
+
test "array: with without a rebind leaves the receiver alone"
|
|
460
|
+
a := Array.filled(3, 0)
|
|
461
|
+
b := a.with(0, 1)
|
|
462
|
+
assert a.get(0) == 0
|
|
463
|
+
assert b.get(0) == 1
|
|
464
|
+
|
|
465
|
+
test "array: in a struct field, read through another reference"
|
|
466
|
+
a := Array.filled(3, 0)
|
|
467
|
+
h := ArrBox(xs: a, tag: 1)
|
|
468
|
+
h.xs = h.xs.with(0, 5)
|
|
469
|
+
assert a.get(0) == 0
|
|
470
|
+
assert h.xs.get(0) == 5
|
|
471
|
+
g := h
|
|
472
|
+
h.xs = h.xs.with(1, 6)
|
|
473
|
+
assert g.xs.get(1) == 6
|
|
474
|
+
|
|
475
|
+
test "array: returned from a helper and kept"
|
|
476
|
+
z := makeZeros(3)
|
|
477
|
+
r := firstTo9(z)
|
|
478
|
+
assert z.get(0) == 0
|
|
479
|
+
assert r.get(0) == 9
|
|
480
|
+
y := makeZeros(2)
|
|
481
|
+
y = y.with(1, 4)
|
|
482
|
+
assert y.get(1) == 4
|
|
483
|
+
|
|
484
|
+
test "array: passed to a helper that stores it"
|
|
485
|
+
xs := Slice[Array[Int]]()
|
|
486
|
+
a := Array.filled(2, 0)
|
|
487
|
+
r := keepAndSet(xs, a)
|
|
488
|
+
assert a.get(0) == 0
|
|
489
|
+
assert xs.at(0).get(0) == 0
|
|
490
|
+
assert r.get(0) == 7
|
|
491
|
+
|
|
492
|
+
test "array: stored in a slice before the write"
|
|
493
|
+
xs := Slice[Array[Int]]()
|
|
494
|
+
a := Array.filled(2, 0)
|
|
495
|
+
xs.push(a)
|
|
496
|
+
a = a.with(0, 3)
|
|
497
|
+
assert xs.at(0).get(0) == 0
|
|
498
|
+
assert a.get(0) == 3
|
|
499
|
+
|
|
500
|
+
test "array: captured by a closure before the write"
|
|
501
|
+
a := Array.filled(2, 0)
|
|
502
|
+
f := || a.get(0)
|
|
503
|
+
a = a.with(0, 5)
|
|
504
|
+
assert f() == 5 || f() == 0
|
|
505
|
+
b := Array.filled(2, 0)
|
|
506
|
+
g := || b.get(0)
|
|
507
|
+
c := b.with(0, 5)
|
|
508
|
+
assert g() == 0
|
|
509
|
+
assert c.get(0) == 5
|
|
510
|
+
|
|
511
|
+
test "array: a loop keeping the previous array"
|
|
512
|
+
a := Array.filled(3, 0)
|
|
513
|
+
snaps := Slice[Array[Int]]()
|
|
514
|
+
for i := range 3
|
|
515
|
+
snaps.push(a)
|
|
516
|
+
a = a.with(i, i + 1)
|
|
517
|
+
assert snaps.at(0).get(0) == 0
|
|
518
|
+
assert snaps.at(1).get(0) == 1 && snaps.at(1).get(1) == 0
|
|
519
|
+
assert snaps.at(2).get(1) == 2 && snaps.at(2).get(2) == 0
|
|
520
|
+
assert a.get(2) == 3
|
|
521
|
+
|
|
522
|
+
test "array: a loop with the previous kept in a variable"
|
|
523
|
+
a := Array.filled(3, 0)
|
|
524
|
+
prev := a
|
|
525
|
+
for i := range 3
|
|
526
|
+
prev = a
|
|
527
|
+
a = a.with(i, 1)
|
|
528
|
+
assert prev.get(2) == 0
|
|
529
|
+
assert prev.get(1) == 1
|
|
530
|
+
assert a.get(2) == 1
|
|
531
|
+
|
|
532
|
+
test "array: a unique loop gives the copying answer"
|
|
533
|
+
a := doubled(4)
|
|
534
|
+
assert a.get(0) == 2 && a.get(1) == 3 && a.get(2) == 4 && a.get(3) == 5
|
|
535
|
+
|
|
536
|
+
test "array: with as the last expression"
|
|
537
|
+
a := lastWith(3)
|
|
538
|
+
assert a.get(0) == 4 && a.get(1) == 3 && a.get(2) == 8
|
|
539
|
+
|
|
540
|
+
test "array: a name declared twice"
|
|
541
|
+
a := twoDecls(true)
|
|
542
|
+
assert a.get(0) == 1 && a.get(1) == 0
|
|
543
|
+
b := twoDecls(false)
|
|
544
|
+
assert b.get(0) == 0 && b.get(1) == 0
|
|
545
|
+
|
|
546
|
+
test "array: through a nested with"
|
|
547
|
+
a := Array.filled(3, 0)
|
|
548
|
+
b := a.with(0, a.with(1, 5).get(1))
|
|
549
|
+
assert a.get(0) == 0 && a.get(1) == 0
|
|
550
|
+
assert b.get(0) == 5
|
|
551
|
+
|
|
552
|
+
test "array: with, then the old array read in the argument"
|
|
553
|
+
a := Array.filled(3, 1)
|
|
554
|
+
a = a.with(0, a.get(1) + a.get(2))
|
|
555
|
+
a = a.with(1, a.get(0) * 10)
|
|
556
|
+
assert a.get(0) == 2 && a.get(1) == 20 && a.get(2) == 1
|
|
557
|
+
|
|
558
|
+
test "array: from push and resize"
|
|
559
|
+
a := Array[Int]().push(1).push(2)
|
|
560
|
+
b := a
|
|
561
|
+
a = a.with(0, 9)
|
|
562
|
+
assert b.get(0) == 1
|
|
563
|
+
e := Array[Int]().push(4).with(0, 6)
|
|
564
|
+
assert e.get(0) == 6
|
|
565
|
+
r := Array[Int]().resize(2)
|
|
566
|
+
r = r.with(1, 3)
|
|
567
|
+
assert r.get(1) == 3 && r.length() == 2
|
|
568
|
+
|
|
569
|
+
test "array: an empty array stays empty"
|
|
570
|
+
a := Array[Int]()
|
|
571
|
+
b := a
|
|
572
|
+
a = a.resize(0)
|
|
573
|
+
assert a.length() == 0
|
|
574
|
+
assert b.length() == 0
|
|
575
|
+
|
|
576
|
+
test "array: bytes"
|
|
577
|
+
a := Array.filled(3, Byte(7))
|
|
578
|
+
b := a
|
|
579
|
+
a = a.with(1, Byte(200))
|
|
580
|
+
assert b.get(1).toInt() == 7
|
|
581
|
+
assert a.get(1).toInt() == 200
|
|
582
|
+
assert a.get(2).toInt() == 7
|
|
583
|
+
|
|
584
|
+
test "array: vec2 elements"
|
|
585
|
+
a := Array.filled(3, Vec2(x: 1.0, y: 2.0))
|
|
586
|
+
b := a
|
|
587
|
+
a = a.with(1, Vec2(x: 5.0, y: 6.0))
|
|
588
|
+
assert b.get(1).x == 1.0 && b.get(1).y == 2.0
|
|
589
|
+
assert a.get(1).x == 5.0 && a.get(1).y == 6.0
|
|
590
|
+
assert a.get(0).y == 2.0
|
|
591
|
+
c := Array.filled(2, Vec2(x: 0.0, y: 0.0))
|
|
592
|
+
for i := range 2
|
|
593
|
+
c = c.with(i, Vec2(x: Float(i), y: 3.0))
|
|
594
|
+
assert c.get(1).x == 1.0 && c.get(0).y == 3.0
|
|
595
|
+
|
|
596
|
+
test "array: vec2 stored in a field and a slice"
|
|
597
|
+
a := Array.filled(2, Vec2(x: 1.0, y: 1.0))
|
|
598
|
+
xs := Slice[Array[Vec2]]()
|
|
599
|
+
xs.push(a)
|
|
600
|
+
a = a.with(0, Vec2(x: 9.0, y: 9.0))
|
|
601
|
+
assert xs.at(0).get(0).x == 1.0
|
|
602
|
+
assert a.get(0).x == 9.0
|
tests/fbip/array_names.plum
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import std/Int
|
|
2
|
+
import std/Array
|
|
3
|
+
|
|
4
|
+
# Name collisions: the uniqueness pass resolves methods by name, with no types. A user type with
|
|
5
|
+
# its own `with` or `push` that hands back shared storage must keep every array a copy.
|
|
6
|
+
|
|
7
|
+
struct Bag(items: Array[Int]) =
|
|
8
|
+
fun push(self, x: Int) -> Array[Int] = self.items
|
|
9
|
+
fun with(self, i: Int, v: Int) -> Array[Int] = self.items
|
|
10
|
+
|
|
11
|
+
test "array: a user type with its own with and push"
|
|
12
|
+
bag := Bag(items: Array.filled(2, 0))
|
|
13
|
+
a := bag.with(0, 1)
|
|
14
|
+
a = a.with(0, 9)
|
|
15
|
+
assert bag.items.get(0) == 0
|
|
16
|
+
b := Array.filled(2, 0)
|
|
17
|
+
b = b.with(0, 1)
|
|
18
|
+
assert b.get(0) == 1
|
|
19
|
+
|
|
20
|
+
test "array: bound to the result of a user push that returns shared storage"
|
|
21
|
+
bag := Bag(items: Array.filled(2, 0))
|
|
22
|
+
a := Array.filled(2, 5)
|
|
23
|
+
a = bag.push(1)
|
|
24
|
+
a = a.with(0, 9)
|
|
25
|
+
assert a.get(0) == 9
|
|
26
|
+
assert bag.items.get(0) == 0
|
tests/fbip/array_with.plum
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import std/Int
|
|
2
|
+
import std/Float
|
|
3
|
+
import std/Array
|
|
4
|
+
import std/Slice
|
|
5
|
+
import std/game/Vec2
|
|
6
|
+
|
|
7
|
+
# `Array.with` on arrays held in struct fields. A `with` writes in place only when the struct is
|
|
8
|
+
# provably unshared and the array is held by no one else; each test would give a different answer if
|
|
9
|
+
# a write went through a second holder. The sweep runs this file with the default and `--no-fbip`.
|
|
10
|
+
# The proof names fields and methods without types, so every scenario has its own field and
|
|
11
|
+
# method names: one shared call or alias would (rightly) stop the whole name.
|
|
12
|
+
|
|
13
|
+
# ---- the persistent vector shape: a spread rebuilding its own array ----
|
|
14
|
+
|
|
15
|
+
struct Vec(cells: Array[Int], n: Int) =
|
|
16
|
+
fun put(self, i: Int, x: Int) -> Vec = Vec(..self, cells: self.cells.with(i, x))
|
|
17
|
+
fun append(self, x: Int) -> Vec = Vec(..self, cells: self.cells.with(self.n, x), n: self.n + 1)
|
|
18
|
+
fun get(self, i: Int) -> Int = self.cells.get(i)
|
|
19
|
+
fun length(self) -> Int = self.n
|
|
20
|
+
|
|
21
|
+
fun newVec(cap: Int) -> Vec = Vec(cells: Array.filled(cap, 0), n: 0)
|
|
22
|
+
|
|
23
|
+
fun fillVec(n: Int) -> Vec =
|
|
24
|
+
v := newVec(n)
|
|
25
|
+
for i := range n
|
|
26
|
+
v = v.append(i * 3)
|
|
27
|
+
v
|
|
28
|
+
|
|
29
|
+
fun bumpAll(v: Vec) -> Vec =
|
|
30
|
+
for i := range v.length()
|
|
31
|
+
v = v.put(i, v.get(i) + 1)
|
|
32
|
+
v
|
|
33
|
+
|
|
34
|
+
# ---- a `mut` field rebuilt with an assign-back ----
|
|
35
|
+
|
|
36
|
+
struct MVec(mut mcells: Array[Int], mn: Int) =
|
|
37
|
+
fun mput(self, i: Int, x: Int) -> Unit =
|
|
38
|
+
self.mcells = self.mcells.with(i, x)
|
|
39
|
+
fun mget(self, i: Int) -> Int = self.mcells.get(i)
|
|
40
|
+
|
|
41
|
+
# ---- hostile: each has its own names ----
|
|
42
|
+
|
|
43
|
+
struct AVec(lanes: Array[Int], an: Int) =
|
|
44
|
+
fun aput(self, i: Int, x: Int) -> AVec = AVec(..self, lanes: self.lanes.with(i, x))
|
|
45
|
+
fun aget(self, i: Int) -> Int = self.lanes.get(i)
|
|
46
|
+
fun lanesOf(self) -> Array[Int] = self.lanes
|
|
47
|
+
|
|
48
|
+
struct SVec(slots: Array[Int], sn: Int) =
|
|
49
|
+
fun sput(self, i: Int, x: Int) -> SVec = SVec(..self, slots: self.slots.with(i, x))
|
|
50
|
+
fun sget(self, i: Int) -> Int = self.slots.get(i)
|
|
51
|
+
|
|
52
|
+
struct EVec(ecells: Array[Int], en: Int) =
|
|
53
|
+
fun eput(self, i: Int, x: Int) -> EVec = EVec(..self, ecells: self.ecells.with(i, x))
|
|
54
|
+
fun eget(self, i: Int) -> Int = self.ecells.get(i)
|
|
55
|
+
|
|
56
|
+
struct CVec(ccells: Array[Int], cn: Int) =
|
|
57
|
+
fun cput(self, i: Int, x: Int) -> CVec = CVec(..self, ccells: self.ccells.with(i, x))
|
|
58
|
+
fun cget(self, i: Int) -> Int = self.ccells.get(i)
|
|
59
|
+
|
|
60
|
+
struct GVec(gcells: Array[Int], gn: Int) =
|
|
61
|
+
fun gput(self, i: Int, x: Int) -> GVec = GVec(..self, gcells: self.gcells.with(i, x))
|
|
62
|
+
fun gget(self, i: Int) -> Int = self.gcells.get(i)
|
|
63
|
+
|
|
64
|
+
fun stash(xs: Slice[GVec], v: GVec) -> GVec =
|
|
65
|
+
xs.push(v)
|
|
66
|
+
v.gput(0, 7)
|
|
67
|
+
|
|
68
|
+
struct HVec(hcells: Array[Int], hn: Int) =
|
|
69
|
+
fun hput(self, i: Int, x: Int) -> HVec = HVec(..self, hcells: self.hcells.with(i, x))
|
|
70
|
+
fun hget(self, i: Int) -> Int = self.hcells.get(i)
|
|
71
|
+
|
|
72
|
+
struct IVec(icells: Array[Int], iprev: Int) =
|
|
73
|
+
fun iput(self, i: Int, x: Int) -> IVec = IVec(..self, icells: self.icells.with(i, x))
|
|
74
|
+
fun iget(self, i: Int) -> Int = self.icells.get(i)
|
|
75
|
+
|
|
76
|
+
struct PVec(pcells: Array[Vec2], pn: Int) =
|
|
77
|
+
fun pput(self, i: Int, p: Vec2) -> PVec = PVec(..self, pcells: self.pcells.with(i, p))
|
|
78
|
+
fun pget(self, i: Int) -> Vec2 = self.pcells.get(i)
|
|
79
|
+
|
|
80
|
+
struct QVec(qcells: Array[Int], qn: Int) =
|
|
81
|
+
fun qput(self, i: Int, x: Int) -> QVec = QVec(..self, qcells: self.qcells.with(i, x))
|
|
82
|
+
fun qget(self, i: Int) -> Int = self.qcells.get(i)
|
|
83
|
+
fun qpeek(self, i: Int) -> QVec = QVec(..self, qcells: self.qcells.with(i, self.qcells.get(0) + 1), qn: self.qcells.get(i))
|
|
84
|
+
|
|
85
|
+
struct WVec(wcells: Array[Vec2], wn: Int) =
|
|
86
|
+
fun wput(self, i: Int, p: Vec2) -> WVec = WVec(..self, wcells: self.wcells.with(i, p))
|
|
87
|
+
fun wget(self, i: Int) -> Vec2 = self.wcells.get(i)
|
|
88
|
+
|
|
89
|
+
fun grid(n: Int) -> WVec =
|
|
90
|
+
v := WVec(wcells: Array.filled(n, Vec2(x: 0.0, y: 0.0)), wn: n)
|
|
91
|
+
for i := range n
|
|
92
|
+
v = v.wput(i, Vec2(x: Float(i), y: Float(i) * 2.0))
|
|
93
|
+
v
|
|
94
|
+
|
|
95
|
+
struct Outer(mut inner: Vec, tag: Int)
|
|
96
|
+
|
|
97
|
+
struct RVec(rcells: Array[Int], rn: Int) =
|
|
98
|
+
fun rput(self, i: Int, x: Int) -> RVec = RVec(..self, rcells: self.rcells.with(i, x), rn: self.rcells.get(i))
|
|
99
|
+
fun rget(self, i: Int) -> Int = self.rcells.get(i)
|
|
100
|
+
|
|
101
|
+
# ---- tests ----
|
|
102
|
+
|
|
103
|
+
test "vec: append in a loop"
|
|
104
|
+
v := fillVec(5)
|
|
105
|
+
assert v.length() == 5
|
|
106
|
+
assert v.get(0) == 0 && v.get(2) == 6 && v.get(4) == 12
|
|
107
|
+
|
|
108
|
+
test "vec: put through a helper"
|
|
109
|
+
v := bumpAll(fillVec(4))
|
|
110
|
+
assert v.get(0) == 1 && v.get(3) == 10
|
|
111
|
+
|
|
112
|
+
test "vec: the old vector kept while the new one is built"
|
|
113
|
+
v := fillVec(3)
|
|
114
|
+
old := v
|
|
115
|
+
v = v.put(0, 99)
|
|
116
|
+
assert old.get(0) == 0
|
|
117
|
+
assert v.get(0) == 99
|
|
118
|
+
|
|
119
|
+
test "vec: kept by a helper argument"
|
|
120
|
+
a := fillVec(3)
|
|
121
|
+
b := bumpAll(a)
|
|
122
|
+
assert a.get(1) == 3
|
|
123
|
+
assert b.get(1) == 4
|
|
124
|
+
|
|
125
|
+
test "vec: a vector in a mut field"
|
|
126
|
+
o := Outer(inner: fillVec(3), tag: 1)
|
|
127
|
+
keep := o.inner
|
|
128
|
+
o.inner = o.inner.put(0, 5)
|
|
129
|
+
assert keep.get(0) == 0
|
|
130
|
+
assert o.inner.get(0) == 5
|
|
131
|
+
|
|
132
|
+
test "mut field: assign-back rebuild"
|
|
133
|
+
m := MVec(mcells: Array.filled(3, 0), mn: 3)
|
|
134
|
+
m.mput(1, 4)
|
|
135
|
+
m.mput(2, 5)
|
|
136
|
+
assert m.mget(1) == 4 && m.mget(2) == 5 && m.mget(0) == 0
|
|
137
|
+
|
|
138
|
+
test "alias of the field kept before the update"
|
|
139
|
+
v := AVec(lanes: Array.filled(3, 0), an: 3)
|
|
140
|
+
d := v.lanesOf()
|
|
141
|
+
v = v.aput(0, 9)
|
|
142
|
+
assert d.get(0) == 0
|
|
143
|
+
assert v.aget(0) == 9
|
|
144
|
+
|
|
145
|
+
test "alias of the field read directly"
|
|
146
|
+
v := AVec(lanes: Array.filled(3, 0), an: 3)
|
|
147
|
+
d := v.lanes
|
|
148
|
+
w := v.aput(1, 7)
|
|
149
|
+
assert d.get(1) == 0
|
|
150
|
+
assert w.aget(1) == 7
|
|
151
|
+
|
|
152
|
+
test "two structs sharing one array"
|
|
153
|
+
arr := Array.filled(3, 0)
|
|
154
|
+
a := SVec(slots: arr, sn: 0)
|
|
155
|
+
b := SVec(slots: arr, sn: 0)
|
|
156
|
+
a = a.sput(0, 5)
|
|
157
|
+
assert b.sget(0) == 0
|
|
158
|
+
assert arr.get(0) == 0
|
|
159
|
+
assert a.sget(0) == 5
|
|
160
|
+
|
|
161
|
+
test "the vector itself aliased"
|
|
162
|
+
v := EVec(ecells: Array.filled(3, 0), en: 0)
|
|
163
|
+
w := v
|
|
164
|
+
v = v.eput(1, 6)
|
|
165
|
+
assert w.eget(1) == 0
|
|
166
|
+
assert v.eget(1) == 6
|
|
167
|
+
|
|
168
|
+
test "the vector captured by a closure"
|
|
169
|
+
v := CVec(ccells: Array.filled(3, 0), cn: 0)
|
|
170
|
+
f := || v.cget(0)
|
|
171
|
+
w := v.cput(0, 4)
|
|
172
|
+
assert f() == 0
|
|
173
|
+
assert w.cget(0) == 4
|
|
174
|
+
|
|
175
|
+
test "the vector stored by a helper then updated"
|
|
176
|
+
xs := Slice[GVec]()
|
|
177
|
+
v := GVec(gcells: Array.filled(2, 0), gn: 0)
|
|
178
|
+
r := stash(xs, v)
|
|
179
|
+
assert xs.at(0).gget(0) == 0
|
|
180
|
+
assert v.gget(0) == 0
|
|
181
|
+
assert r.gget(0) == 7
|
|
182
|
+
|
|
183
|
+
test "a loop keeping every previous vector"
|
|
184
|
+
snaps := Slice[HVec]()
|
|
185
|
+
v := HVec(hcells: Array.filled(3, 0), hn: 0)
|
|
186
|
+
for i := range 3
|
|
187
|
+
snaps.push(v)
|
|
188
|
+
v = v.hput(i, i + 1)
|
|
189
|
+
assert snaps.at(0).hget(0) == 0
|
|
190
|
+
assert snaps.at(1).hget(0) == 1 && snaps.at(1).hget(1) == 0
|
|
191
|
+
assert snaps.at(2).hget(1) == 2 && snaps.at(2).hget(2) == 0
|
|
192
|
+
assert v.hget(2) == 3
|
|
193
|
+
|
|
194
|
+
test "a loop where the previous vector lives in a variable"
|
|
195
|
+
prev := IVec(icells: Array.filled(3, 0), iprev: 0)
|
|
196
|
+
v := prev
|
|
197
|
+
for i := range 3
|
|
198
|
+
prev = v
|
|
199
|
+
v = v.iput(i, 1)
|
|
200
|
+
assert prev.iget(1) == 1 && prev.iget(2) == 0
|
|
201
|
+
assert v.iget(2) == 1
|
|
202
|
+
|
|
203
|
+
test "vec2 elements"
|
|
204
|
+
v := PVec(pcells: Array.filled(3, Vec2(x: 1.0, y: 2.0)), pn: 3)
|
|
205
|
+
w := v
|
|
206
|
+
v = v.pput(1, Vec2(x: 5.0, y: 6.0))
|
|
207
|
+
assert w.pget(1).x == 1.0 && w.pget(1).y == 2.0
|
|
208
|
+
assert v.pget(1).x == 5.0 && v.pget(1).y == 6.0 && v.pget(0).y == 2.0
|
|
209
|
+
for i := range 3
|
|
210
|
+
v = v.pput(i, Vec2(x: Float(i), y: 0.5))
|
|
211
|
+
assert v.pget(2).x == 2.0 && v.pget(2).y == 0.5
|
|
212
|
+
|
|
213
|
+
test "another field of the spread reads the array after the write"
|
|
214
|
+
v := QVec(qcells: Array.filled(3, 4), qn: 0)
|
|
215
|
+
w := v.qpeek(1)
|
|
216
|
+
assert w.qn == 4
|
|
217
|
+
assert w.qget(1) == 5
|
|
218
|
+
assert v.qget(1) == 4
|
|
219
|
+
|
|
220
|
+
test "another field reads the old element"
|
|
221
|
+
v := RVec(rcells: Array.filled(3, 4), rn: 0)
|
|
222
|
+
v = v.rput(1, 9)
|
|
223
|
+
assert v.rn == 4
|
|
224
|
+
assert v.rget(1) == 9
|
|
225
|
+
|
|
226
|
+
test "vec2 elements written in place"
|
|
227
|
+
g := grid(4)
|
|
228
|
+
assert g.wget(0).x == 0.0 && g.wget(3).x == 3.0 && g.wget(3).y == 6.0 && g.wget(2).y == 4.0
|
tests/fbip/operator_methods.plum
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import std/Int
|
|
2
|
+
import std/Array
|
|
3
|
+
|
|
4
|
+
# `x + y`, `-x`, `x == y`, `x < y` call the methods `add`, `neg`, `eq`, `lt` with no visible call
|
|
5
|
+
# site. Their `self` and parameters can be shared, so a spread or `with` inside them must copy.
|
|
6
|
+
|
|
7
|
+
struct OpA(acells: Array[Int], an: Int) =
|
|
8
|
+
fun add(self, o: OpA) -> OpA = OpA(..self, acells: self.acells.with(0, o.an))
|
|
9
|
+
|
|
10
|
+
struct OpB(bcells: Array[Int], bn: Int) =
|
|
11
|
+
fun neg(self) -> OpB = OpB(..self, bcells: self.bcells.with(0, 99))
|
|
12
|
+
|
|
13
|
+
struct OpC(ccells: Array[Int], cn: Int) =
|
|
14
|
+
fun sub(self, o: OpC) -> OpC = OpC(..o, cn: self.cn - o.cn)
|
|
15
|
+
fun mul(self, o: OpC) -> OpC = OpC(..self, ccells: self.ccells.with(1, o.cn))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
test "add writes through self"
|
|
19
|
+
x := OpA(acells: Array.filled(2, 0), an: 5)
|
|
20
|
+
y := OpA(acells: Array.filled(2, 0), an: 7)
|
|
21
|
+
z := x + y
|
|
22
|
+
assert x.acells.get(0) == 0
|
|
23
|
+
assert z.acells.get(0) == 7
|
|
24
|
+
|
|
25
|
+
test "neg writes through self"
|
|
26
|
+
x := OpB(bcells: Array.filled(2, 0), bn: 5)
|
|
27
|
+
z := -x
|
|
28
|
+
assert x.bcells.get(0) == 0
|
|
29
|
+
assert z.bcells.get(0) == 99
|
|
30
|
+
|
|
31
|
+
test "sub and mul"
|
|
32
|
+
x := OpC(ccells: Array.filled(2, 0), cn: 5)
|
|
33
|
+
y := OpC(ccells: Array.filled(2, 0), cn: 7)
|
|
34
|
+
m := x * y
|
|
35
|
+
assert x.ccells.get(1) == 0
|
|
36
|
+
assert m.ccells.get(1) == 7
|
tests/fbip/spread_ro_method.plum
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import std/Int
|
|
2
|
+
import std/Array
|
|
3
|
+
|
|
4
|
+
# `T(..s, f: s.f.with(..), g: s.total())`: the `with` may write `s.f` in place only if no other
|
|
5
|
+
# field value of the spread can read `s.f` afterwards. A read-only METHOD of `s` reads `s.f` too.
|
|
6
|
+
# Each shape has its own names: a shared field or method name would stop the proof for both.
|
|
7
|
+
|
|
8
|
+
struct RoA(rcells: Array[Int], rtotal: Int) =
|
|
9
|
+
fun rsum0(self) -> Int = self.rcells.get(0)
|
|
10
|
+
fun rupd(self) -> RoA = RoA(..self, rcells: self.rcells.with(0, 9), rtotal: self.rsum0())
|
|
11
|
+
|
|
12
|
+
struct RoB(qcells: Array[Int], qtotal: Int) =
|
|
13
|
+
fun qsum0(self) -> Int = self.qcells.get(0)
|
|
14
|
+
|
|
15
|
+
fun roUpd(v: RoB) -> RoB = RoB(..v, qcells: v.qcells.with(0, 9), qtotal: v.qsum0())
|
|
16
|
+
|
|
17
|
+
struct RoC(ccells: Array[Int], ctotal: Int) =
|
|
18
|
+
fun csum0(self) -> Int = self.ccells.get(0)
|
|
19
|
+
|
|
20
|
+
fun runIt(f: fn() -> Int) -> Int = f()
|
|
21
|
+
|
|
22
|
+
fun roClosure(v: RoC) -> RoC = RoC(..v, ccells: v.ccells.with(0, 9), ctotal: runIt(|| v.csum0()))
|
|
23
|
+
|
|
24
|
+
test "a read-only method of self inside the spread sees the old array"
|
|
25
|
+
v := RoA(rcells: Array.filled(3, 5), rtotal: 0)
|
|
26
|
+
v = v.rupd()
|
|
27
|
+
assert v.rcells.get(0) == 9
|
|
28
|
+
assert v.rtotal == 5
|
|
29
|
+
|
|
30
|
+
test "a read-only method of the spread source from a free fn"
|
|
31
|
+
v := RoB(qcells: Array.filled(3, 5), qtotal: 0)
|
|
32
|
+
v = roUpd(v)
|
|
33
|
+
assert v.qcells.get(0) == 9
|
|
34
|
+
assert v.qtotal == 5
|
|
35
|
+
|
|
36
|
+
test "a closure reading the spread source"
|
|
37
|
+
v := RoC(ccells: Array.filled(3, 5), ctotal: 0)
|
|
38
|
+
v = roClosure(v)
|
|
39
|
+
assert v.ccells.get(0) == 9
|
|
40
|
+
assert v.ctotal == 5
|
tests/fbip_probes/array_set.plum
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import std/Int
|
|
2
|
+
import std/Str
|
|
3
|
+
import std/Array
|
|
4
|
+
import std/Time
|
|
5
|
+
import std/Io
|
|
6
|
+
|
|
7
|
+
# FBIP probe: Array.set over a 1000-slot array, n times; the sum of the array is the result.
|
|
8
|
+
|
|
9
|
+
fun size() -> Int = 1000000
|
|
10
|
+
|
|
11
|
+
fun run(n: Int) -> Int =
|
|
12
|
+
a := Array[Int]().resize(1000)
|
|
13
|
+
for i := range n
|
|
14
|
+
a.set(i % 1000, i)
|
|
15
|
+
total := 0
|
|
16
|
+
for j := range 1000
|
|
17
|
+
total = total + a.get(j)
|
|
18
|
+
total
|
|
19
|
+
|
|
20
|
+
fun main(io: Io) =
|
|
21
|
+
t0 := Time.now(io).toMillis()
|
|
22
|
+
r := run(size())
|
|
23
|
+
t1 := Time.now(io).toMillis()
|
|
24
|
+
io.printLn("array_set result={r} ms={t1 - t0}")
|
|
25
|
+
|
|
26
|
+
test "array set"
|
|
27
|
+
# slots 0..999 end up holding 0..999 when n == 1000
|
|
28
|
+
assert run(1000) == 499500
|
tests/fbip_probes/buffer_write.plum
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import std/Int
|
|
2
|
+
import std/Str
|
|
3
|
+
import std/Buffer
|
|
4
|
+
import std/Byte
|
|
5
|
+
import std/Time
|
|
6
|
+
import std/Io
|
|
7
|
+
|
|
8
|
+
# FBIP probe: n Buffer.writeByte calls; the buffer's byte count is the result.
|
|
9
|
+
|
|
10
|
+
fun size() -> Int = 1000000
|
|
11
|
+
|
|
12
|
+
fun run(n: Int) -> Int =
|
|
13
|
+
b := Buffer()
|
|
14
|
+
for i := range n
|
|
15
|
+
b.writeByte(Byte(i % 256))
|
|
16
|
+
b.len
|
|
17
|
+
|
|
18
|
+
fun main(io: Io) =
|
|
19
|
+
t0 := Time.now(io).toMillis()
|
|
20
|
+
r := run(size())
|
|
21
|
+
t1 := Time.now(io).toMillis()
|
|
22
|
+
io.printLn("buffer_write result={r} ms={t1 - t0}")
|
|
23
|
+
|
|
24
|
+
test "buffer writes"
|
|
25
|
+
assert run(1000) == 1000
|
tests/fbip_probes/map_set_get.plum
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import std/Int
|
|
2
|
+
import std/Str
|
|
3
|
+
import std/Map
|
|
4
|
+
import std/Option
|
|
5
|
+
import std/Time
|
|
6
|
+
import std/Io
|
|
7
|
+
|
|
8
|
+
# FBIP probe: n Map.set calls, then n Map.get calls; the sum of the values read is the result.
|
|
9
|
+
|
|
10
|
+
fun size() -> Int = 100000
|
|
11
|
+
|
|
12
|
+
fun run(n: Int) -> Int =
|
|
13
|
+
m: Map[Int, Int] := Map.empty()
|
|
14
|
+
for i := range n
|
|
15
|
+
m.set(i, i * 2)
|
|
16
|
+
total := 0
|
|
17
|
+
for i := range n
|
|
18
|
+
total = total + m.get(i).unwrapOr(0)
|
|
19
|
+
total
|
|
20
|
+
|
|
21
|
+
fun main(io: Io) =
|
|
22
|
+
t0 := Time.now(io).toMillis()
|
|
23
|
+
r := run(size())
|
|
24
|
+
t1 := Time.now(io).toMillis()
|
|
25
|
+
io.printLn("map_set_get result={r} ms={t1 - t0}")
|
|
26
|
+
|
|
27
|
+
test "map set then get"
|
|
28
|
+
assert run(1000) == 999000
|
tests/fbip_probes/slice_push_field.plum
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import std/Int
|
|
2
|
+
import std/Str
|
|
3
|
+
import std/Slice
|
|
4
|
+
import std/Time
|
|
5
|
+
import std/Io
|
|
6
|
+
|
|
7
|
+
# FBIP probe: Slice.push on a `mut` struct field, n times.
|
|
8
|
+
|
|
9
|
+
struct Holder(mut items: Slice[Int], mut count: Int)
|
|
10
|
+
|
|
11
|
+
fun size() -> Int = 1000000
|
|
12
|
+
|
|
13
|
+
fun run(n: Int) -> Int =
|
|
14
|
+
h := Holder(items: Slice[Int](), count: 0)
|
|
15
|
+
for i := range n
|
|
16
|
+
h.items.push(i)
|
|
17
|
+
h.count = h.count + 1
|
|
18
|
+
h.items.length() + h.items.at(n - 1) + h.count
|
|
19
|
+
|
|
20
|
+
fun main(io: Io) =
|
|
21
|
+
t0 := Time.now(io).toMillis()
|
|
22
|
+
r := run(size())
|
|
23
|
+
t1 := Time.now(io).toMillis()
|
|
24
|
+
io.printLn("slice_push_field result={r} ms={t1 - t0}")
|
|
25
|
+
|
|
26
|
+
test "slice push on a mut field"
|
|
27
|
+
assert run(1000) == 1000 + 999 + 1000
|
tests/fbip_probes/slice_push_helper.plum
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import std/Int
|
|
2
|
+
import std/Str
|
|
3
|
+
import std/Slice
|
|
4
|
+
import std/Time
|
|
5
|
+
import std/Io
|
|
6
|
+
|
|
7
|
+
# FBIP probe: Slice.push through a helper function that takes the slice.
|
|
8
|
+
|
|
9
|
+
fun size() -> Int = 1000000
|
|
10
|
+
|
|
11
|
+
fun add(s: Slice[Int], v: Int) -> Unit =
|
|
12
|
+
s.push(v)
|
|
13
|
+
|
|
14
|
+
fun run(n: Int) -> Int =
|
|
15
|
+
s := Slice[Int]()
|
|
16
|
+
for i := range n
|
|
17
|
+
add(s, i)
|
|
18
|
+
s.length() + s.at(n - 1)
|
|
19
|
+
|
|
20
|
+
fun main(io: Io) =
|
|
21
|
+
t0 := Time.now(io).toMillis()
|
|
22
|
+
r := run(size())
|
|
23
|
+
t1 := Time.now(io).toMillis()
|
|
24
|
+
io.printLn("slice_push_helper result={r} ms={t1 - t0}")
|
|
25
|
+
|
|
26
|
+
test "slice push through a helper"
|
|
27
|
+
assert run(1000) == 1000 + 999
|
tests/fbip_probes/slice_push_local.plum
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import std/Int
|
|
2
|
+
import std/Str
|
|
3
|
+
import std/Slice
|
|
4
|
+
import std/Time
|
|
5
|
+
import std/Io
|
|
6
|
+
|
|
7
|
+
# FBIP probe: Slice.push on a local, n times. Result: the length plus the last element.
|
|
8
|
+
|
|
9
|
+
fun size() -> Int = 1000000
|
|
10
|
+
|
|
11
|
+
fun run(n: Int) -> Int =
|
|
12
|
+
s := Slice[Int]()
|
|
13
|
+
for i := range n
|
|
14
|
+
s.push(i)
|
|
15
|
+
s.length() + s.at(n - 1)
|
|
16
|
+
|
|
17
|
+
fun main(io: Io) =
|
|
18
|
+
t0 := Time.now(io).toMillis()
|
|
19
|
+
r := run(size())
|
|
20
|
+
t1 := Time.now(io).toMillis()
|
|
21
|
+
io.printLn("slice_push_local result={r} ms={t1 - t0}")
|
|
22
|
+
|
|
23
|
+
test "slice push on a local"
|
|
24
|
+
assert run(1000) == 1000 + 999
|
tests/fbip_probes/struct_spread.plum
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import std/Int
|
|
2
|
+
import std/Str
|
|
3
|
+
import std/Time
|
|
4
|
+
import std/Io
|
|
5
|
+
|
|
6
|
+
# FBIP probe: a struct spread in a loop on a boxed struct (the Str field keeps it boxed).
|
|
7
|
+
|
|
8
|
+
struct Cell(label: Str, n: Int, m: Int)
|
|
9
|
+
|
|
10
|
+
fun size() -> Int = 1000000
|
|
11
|
+
|
|
12
|
+
fun run(count: Int) -> Int =
|
|
13
|
+
c := Cell(label: "cell", n: 0, m: 0)
|
|
14
|
+
for i := range count
|
|
15
|
+
c = Cell(..c, n: c.n + 1)
|
|
16
|
+
c.n + c.m
|
|
17
|
+
|
|
18
|
+
fun main(io: Io) =
|
|
19
|
+
t0 := Time.now(io).toMillis()
|
|
20
|
+
r := run(size())
|
|
21
|
+
t1 := Time.now(io).toMillis()
|
|
22
|
+
io.printLn("struct_spread result={r} ms={t1 - t0}")
|
|
23
|
+
|
|
24
|
+
test "struct spread in a loop"
|
|
25
|
+
assert run(1000) == 1000
|
tests/param_defaults.plum
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import std/Int
|
|
2
|
+
import std/Str
|
|
3
|
+
import std/List
|
|
4
|
+
import std/Option
|
|
5
|
+
|
|
6
|
+
# Parameter defaults: each fn here has a loop, so the simplifier leaves the call alone and the
|
|
7
|
+
# default is compiled at the call site. `compiler/cli/tests/cli/fusion_agree_test.rs` also runs
|
|
8
|
+
# this file with `--no-fusion`.
|
|
9
|
+
|
|
10
|
+
struct Cell(n: Int, label: Str)
|
|
11
|
+
struct Flat(n: Int)
|
|
12
|
+
struct Box[T](items: List[T], label: Str)
|
|
13
|
+
|
|
14
|
+
enum Shape =
|
|
15
|
+
| Dot
|
|
16
|
+
| Rect(w: Int, label: Str)
|
|
17
|
+
|
|
18
|
+
fun times3(d: Int) -> Int =
|
|
19
|
+
x := 0
|
|
20
|
+
for i := range 3
|
|
21
|
+
x = x + d
|
|
22
|
+
x
|
|
23
|
+
|
|
24
|
+
fun withInt(d: Int = 7) -> Int = times3(d)
|
|
25
|
+
fun withStr(d: Str = "abc") -> Int = times3(d.length())
|
|
26
|
+
fun withNone(d: Option[Int] = None) -> Int =
|
|
27
|
+
match d
|
|
28
|
+
Some(v) => times3(v)
|
|
29
|
+
None => times3(1)
|
|
30
|
+
fun withSome(d: Option[Cell] = Some(Cell(n: 3, label: "s"))) -> Int =
|
|
31
|
+
match d
|
|
32
|
+
Some(c) => times3(c.n)
|
|
33
|
+
None => 0
|
|
34
|
+
fun withStruct(d: Cell = Cell(n: 1, label: "a")) -> Int = times3(d.n)
|
|
35
|
+
fun withFlat(d: Flat = Flat(n: 2)) -> Int = times3(d.n)
|
|
36
|
+
fun withVariant(d: Shape = Rect(w: 2, label: "r")) -> Int =
|
|
37
|
+
match d
|
|
38
|
+
Rect(w, label) => times3(w)
|
|
39
|
+
Dot => times3(100)
|
|
40
|
+
fun withNullary(d: Shape = Dot) -> Int =
|
|
41
|
+
match d
|
|
42
|
+
Rect(w, label) => times3(w)
|
|
43
|
+
Dot => times3(100)
|
|
44
|
+
|
|
45
|
+
# the parameter is rebuilt with a spread
|
|
46
|
+
fun respread(d: Cell = Cell(n: 1, label: "a")) -> Int =
|
|
47
|
+
d = Cell(..d, n: d.n + 1)
|
|
48
|
+
for i := range 3
|
|
49
|
+
d = Cell(..d, n: d.n + 1)
|
|
50
|
+
d.n
|
|
51
|
+
|
|
52
|
+
# a default that calls a fn, a generic fn, and an expression
|
|
53
|
+
fun made() -> Int = times3(2)
|
|
54
|
+
fun same[T](x: T) -> T = x
|
|
55
|
+
fun viaCall(d: Int = made()) -> Int = times3(d)
|
|
56
|
+
fun viaGeneric(d: Cell = same(Cell(n: 5, label: "g"))) -> Int = times3(d.n)
|
|
57
|
+
fun viaExpr(a: Int = -1, b: Int = 1 + 2) -> Int = times3(a + b)
|
|
58
|
+
|
|
59
|
+
# called from a generic fn: the default's types are the callee's, not the caller's
|
|
60
|
+
fun fromGeneric[T](x: T) -> Int = withStruct()
|
|
61
|
+
|
|
62
|
+
# a generic fn: the default's type depends on the instantiation
|
|
63
|
+
fun generic[T](x: T, b: Box[T] = Box(items: Nil, label: "b")) -> Int = times3(b.label.length())
|
|
64
|
+
fun genericNone[T](x: T, o: Option[T] = None) -> Int =
|
|
65
|
+
match o
|
|
66
|
+
Some(v) => times3(1)
|
|
67
|
+
None => times3(2)
|
|
68
|
+
|
|
69
|
+
struct Owner(k: Int) =
|
|
70
|
+
fun m(self, d: Cell = Cell(n: 6, label: "m")) -> Int = times3(self.k + d.n)
|
|
71
|
+
|
|
72
|
+
struct GenericOwner[T](v: T, label: Str) =
|
|
73
|
+
fun none(self, o: Option[T] = None) -> Int =
|
|
74
|
+
match o
|
|
75
|
+
Some(v) => times3(1)
|
|
76
|
+
None => times3(2)
|
|
77
|
+
fun cell(self, c: Cell = Cell(n: 3, label: "n")) -> Int = times3(c.n)
|
|
78
|
+
|
|
79
|
+
test "an Int default"
|
|
80
|
+
assert withInt() == 21
|
|
81
|
+
assert withInt(1) == 3
|
|
82
|
+
|
|
83
|
+
test "a Str default"
|
|
84
|
+
assert withStr() == 9
|
|
85
|
+
assert withStr("z") == 3
|
|
86
|
+
|
|
87
|
+
test "a None default"
|
|
88
|
+
assert withNone() == 3
|
|
89
|
+
assert withNone(Some(2)) == 6
|
|
90
|
+
|
|
91
|
+
test "a Some default around a struct"
|
|
92
|
+
assert withSome() == 9
|
|
93
|
+
assert withSome(None) == 0
|
|
94
|
+
|
|
95
|
+
test "a struct default, boxed and flat"
|
|
96
|
+
assert withStruct() == 3
|
|
97
|
+
assert withStruct(Cell(n: 2, label: "b")) == 6
|
|
98
|
+
assert withFlat() == 6
|
|
99
|
+
|
|
100
|
+
test "an enum variant default, with a payload and nullary"
|
|
101
|
+
assert withVariant() == 6
|
|
102
|
+
assert withVariant(Dot) == 300
|
|
103
|
+
assert withNullary() == 300
|
|
104
|
+
assert withNullary(Rect(w: 3, label: "q")) == 9
|
|
105
|
+
|
|
106
|
+
test "a spread on a defaulted parameter"
|
|
107
|
+
assert respread() == 5
|
|
108
|
+
assert respread(Cell(n: 5, label: "b")) == 9
|
|
109
|
+
|
|
110
|
+
test "a default that calls a fn"
|
|
111
|
+
assert viaCall() == 18
|
|
112
|
+
assert viaGeneric() == 15
|
|
113
|
+
assert viaExpr() == 6
|
|
114
|
+
|
|
115
|
+
test "a default compiled from a generic caller"
|
|
116
|
+
assert fromGeneric(1) == 3
|
|
117
|
+
assert fromGeneric("x") == 3
|
|
118
|
+
|
|
119
|
+
test "a default whose type depends on the instantiation"
|
|
120
|
+
assert generic(1) == 3
|
|
121
|
+
assert generic("s") == 3
|
|
122
|
+
assert genericNone(1) == 6
|
|
123
|
+
assert genericNone("s") == 6
|
|
124
|
+
assert genericNone(1, Some(2)) == 3
|
|
125
|
+
|
|
126
|
+
test "a method default"
|
|
127
|
+
assert Owner(k: 1).m() == 21
|
|
128
|
+
assert GenericOwner(v: 1, label: "x").none() == 6
|
|
129
|
+
assert GenericOwner(v: "a", label: "x").none() == 6
|
|
130
|
+
assert GenericOwner(v: "a", label: "x").cell() == 9
|
tests/shadowed_effects.plum
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import std/Int
|
|
2
|
+
import std/Str
|
|
3
|
+
|
|
4
|
+
# A binder named like a free fn or method, or a binder that is not the value a pass assumes.
|
|
5
|
+
# The side effect is visible, so a wrong proof (a dropped call, a cell reused while still shared,
|
|
6
|
+
# a closure call taken for a self call) changes the answer. These tests assert the unoptimised
|
|
7
|
+
# answer; `compiler/cli/tests/cli/fusion_agree_test.rs` also runs this file with `--no-fusion`.
|
|
8
|
+
|
|
9
|
+
struct Cell(label: Str, mut n: Int)
|
|
10
|
+
|
|
11
|
+
fun bump(c: Cell, x: Int) -> Int =
|
|
12
|
+
c.n = c.n + 1
|
|
13
|
+
x
|
|
14
|
+
|
|
15
|
+
# Pure and small: its argument is dropped when that is proved pure.
|
|
16
|
+
fun ignore(x: Int) -> Int = 7
|
|
17
|
+
|
|
18
|
+
fun helper(x: Int) -> Int = x + 1
|
|
19
|
+
|
|
20
|
+
enum Wrapper =
|
|
21
|
+
| Wrap(g: fn(Int) -> Int, label: Str)
|
|
22
|
+
| Other
|
|
23
|
+
|
|
24
|
+
fun viaPattern(w: Wrapper) -> Int =
|
|
25
|
+
match w
|
|
26
|
+
Wrap(helper, label) => helper(1)
|
|
27
|
+
Other => 0
|
|
28
|
+
|
|
29
|
+
fun dropsPattern(w: Wrapper) -> Int = ignore(viaPattern(w))
|
|
30
|
+
|
|
31
|
+
fun viaLocal(k: fn(Int) -> Int) -> Int =
|
|
32
|
+
helper := k
|
|
33
|
+
helper(1)
|
|
34
|
+
|
|
35
|
+
fun dropsLocal(k: fn(Int) -> Int) -> Int = ignore(viaLocal(k))
|
|
36
|
+
|
|
37
|
+
# A pattern binder named like a fresh local writes the caller's object.
|
|
38
|
+
enum Holder =
|
|
39
|
+
| Holds(cell: Cell, tag: Int)
|
|
40
|
+
| Empty
|
|
41
|
+
|
|
42
|
+
fun writesBinder(h: Holder) -> Int =
|
|
43
|
+
c := Cell(label: "fresh", n: 0)
|
|
44
|
+
match h
|
|
45
|
+
Holds(c, t) =>
|
|
46
|
+
c.n = 5
|
|
47
|
+
t
|
|
48
|
+
Empty => 0
|
|
49
|
+
|
|
50
|
+
fun dropsWrite(h: Holder) -> Int = ignore(writesBinder(h))
|
|
51
|
+
|
|
52
|
+
# A call of an fn-typed field is not the pure method of that name on another type.
|
|
53
|
+
struct Rec(label: Str, zap: fn(Int) -> Int)
|
|
54
|
+
|
|
55
|
+
struct Plain(label: Str) =
|
|
56
|
+
fun zap(self, x: Int) -> Int = x + 1
|
|
57
|
+
|
|
58
|
+
fun sameRec(r: Rec) -> Rec = r
|
|
59
|
+
|
|
60
|
+
fun viaField(r: Rec) -> Int =
|
|
61
|
+
q := sameRec(r)
|
|
62
|
+
q.zap(1)
|
|
63
|
+
|
|
64
|
+
fun dropsField(r: Rec) -> Int = ignore(viaField(r))
|
|
65
|
+
|
|
66
|
+
# Cell reuse: each `inc` would overwrite the matched cell, which here is the caller's.
|
|
67
|
+
enum Link =
|
|
68
|
+
| Nil
|
|
69
|
+
| Cons(head: Int, tail: Link)
|
|
70
|
+
|
|
71
|
+
fun headOf(l: Link) -> Int =
|
|
72
|
+
match l
|
|
73
|
+
Nil => -1
|
|
74
|
+
Cons(h, t) => h
|
|
75
|
+
|
|
76
|
+
fun size(l: Link) -> Int =
|
|
77
|
+
match l
|
|
78
|
+
Nil => 0
|
|
79
|
+
Cons(h, t) => 1 + size(t)
|
|
80
|
+
|
|
81
|
+
# the parameter `t` of the `Nil` arm is not the pattern binder `t` of the other arm
|
|
82
|
+
fun incParam(xs: Link, t: Link) -> Link =
|
|
83
|
+
match xs
|
|
84
|
+
Nil => incParam(t, Nil)
|
|
85
|
+
Cons(h, t) => Cons(head: h + 1, tail: Nil)
|
|
86
|
+
|
|
87
|
+
# the subject is not the caller's fresh value
|
|
88
|
+
fun incRebound(xs: Link, o: Link) -> Link =
|
|
89
|
+
xs = o
|
|
90
|
+
match xs
|
|
91
|
+
Nil => Nil
|
|
92
|
+
Cons(h, t) => Cons(head: h + 1, tail: Nil)
|
|
93
|
+
|
|
94
|
+
# the pattern binder no longer holds the tail
|
|
95
|
+
fun incLocal(xs: Link, o: Link) -> Link =
|
|
96
|
+
match xs
|
|
97
|
+
Nil => Nil
|
|
98
|
+
Cons(h, t) =>
|
|
99
|
+
t = o
|
|
100
|
+
Cons(head: h + 1, tail: incLocal(t, Nil))
|
|
101
|
+
|
|
102
|
+
# Tail recursion modulo cons: a binder named like the fn is not a self call.
|
|
103
|
+
enum Maker =
|
|
104
|
+
| Has(go: fn(Maker) -> Link)
|
|
105
|
+
| NoMaker
|
|
106
|
+
|
|
107
|
+
fun go(m: Maker) -> Link =
|
|
108
|
+
match m
|
|
109
|
+
Has(go) => Cons(head: 1, tail: go(m))
|
|
110
|
+
NoMaker => Nil
|
|
111
|
+
|
|
112
|
+
fun nilMaker() -> fn(Int) -> Link = |n: Int| Nil
|
|
113
|
+
|
|
114
|
+
fun loc(n: Int) -> Link =
|
|
115
|
+
loc := nilMaker()
|
|
116
|
+
Cons(head: n, tail: loc(n - 1))
|
|
117
|
+
|
|
118
|
+
# A constructor parameter named like a free fn does not capture the init's call.
|
|
119
|
+
struct Ship(helper: fn(Int) -> Int, tag: Str) =
|
|
120
|
+
fun init(self) =
|
|
121
|
+
self.total: Int = helper(1)
|
|
122
|
+
|
|
123
|
+
test "a pattern binder named like a pure fn runs its effect"
|
|
124
|
+
c := Cell(label: "c", n: 0)
|
|
125
|
+
assert dropsPattern(Wrap(g: |x: Int| bump(c, x), label: "k")) == 7
|
|
126
|
+
assert c.n == 1
|
|
127
|
+
|
|
128
|
+
test "a local named like a pure fn runs its effect"
|
|
129
|
+
c := Cell(label: "c", n: 0)
|
|
130
|
+
assert dropsLocal(|x: Int| bump(c, x)) == 7
|
|
131
|
+
assert c.n == 1
|
|
132
|
+
|
|
133
|
+
test "a pattern binder named like a fresh local is written"
|
|
134
|
+
c := Cell(label: "c", n: 0)
|
|
135
|
+
assert dropsWrite(Holds(cell: c, tag: 1)) == 7
|
|
136
|
+
assert c.n == 5
|
|
137
|
+
|
|
138
|
+
test "an fn-typed field named like a pure method runs its effect"
|
|
139
|
+
c := Cell(label: "c", n: 0)
|
|
140
|
+
assert dropsField(Rec(label: "r", zap: |x: Int| bump(c, x))) == 7
|
|
141
|
+
assert c.n == 1
|
|
142
|
+
|
|
143
|
+
test "reuse leaves a parameter named like a pattern binder alone"
|
|
144
|
+
s := Cons(head: 1, tail: Nil)
|
|
145
|
+
assert headOf(incParam(Nil, s)) == 2
|
|
146
|
+
assert headOf(s) == 1
|
|
147
|
+
|
|
148
|
+
test "reuse leaves a rebound subject alone"
|
|
149
|
+
s := Cons(head: 1, tail: Nil)
|
|
150
|
+
assert headOf(incRebound(Nil, s)) == 2
|
|
151
|
+
assert headOf(s) == 1
|
|
152
|
+
|
|
153
|
+
test "reuse leaves a rebound pattern binder alone"
|
|
154
|
+
s := Cons(head: 1, tail: Nil)
|
|
155
|
+
assert headOf(incLocal(Cons(head: 5, tail: Nil), s)) == 6
|
|
156
|
+
assert headOf(s) == 1
|
|
157
|
+
|
|
158
|
+
test "a binder named like the fn is not a self call"
|
|
159
|
+
assert size(go(Has(go: |m: Maker| Nil))) == 1
|
|
160
|
+
assert size(loc(3)) == 1
|
|
161
|
+
|
|
162
|
+
test "a constructor parameter does not capture an init's free call"
|
|
163
|
+
s := Ship(helper: |x: Int| x + 100, tag: "k")
|
|
164
|
+
assert s.total == 2
|
tests/shadowed_free_fn.plum
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import std/Int
|
|
2
|
+
import std/Str
|
|
3
|
+
|
|
4
|
+
# A local, parameter, closure parameter or pattern binder named like a free fn shadows it.
|
|
5
|
+
# Every pass that resolves a call by name must see that: these tests assert the unoptimised
|
|
6
|
+
# answer, and `compiler/cli/tests/cli/fusion_agree_test.rs` also runs this file with `--no-fusion`.
|
|
7
|
+
|
|
8
|
+
fun helper(x: Int) -> Int = x + 1
|
|
9
|
+
|
|
10
|
+
fun localClosure() -> Int =
|
|
11
|
+
helper := |x: Int| x + 100
|
|
12
|
+
helper(1)
|
|
13
|
+
|
|
14
|
+
fun viaParam(helper: fn(Int) -> Int) -> Int = helper(1)
|
|
15
|
+
|
|
16
|
+
fun viaClosureParam(n: Int) -> Int =
|
|
17
|
+
k := |helper: fn(Int) -> Int| helper(n)
|
|
18
|
+
k(|x: Int| x + 300)
|
|
19
|
+
|
|
20
|
+
enum Wrapper =
|
|
21
|
+
| Wrap(g: fn(Int) -> Int, label: Str)
|
|
22
|
+
| Other
|
|
23
|
+
|
|
24
|
+
fun viaPattern(w: Wrapper) -> Int =
|
|
25
|
+
match w
|
|
26
|
+
Wrap(helper, label) => helper(1)
|
|
27
|
+
Other => 0
|
|
28
|
+
|
|
29
|
+
fun viaPickedArm() -> Int =
|
|
30
|
+
match Wrap(g: |x: Int| x + 500, label: "k")
|
|
31
|
+
Wrap(helper, label) => helper(1)
|
|
32
|
+
Other => 0
|
|
33
|
+
|
|
34
|
+
# a self-recursive local closure in an arm the simplifier picks
|
|
35
|
+
fun viaRecursiveLocal() -> Int =
|
|
36
|
+
match Wrap(g: |x: Int| x, label: "k")
|
|
37
|
+
Wrap(g, label) =>
|
|
38
|
+
helper := |n: Int| if n == 0 then 9 else helper(n - 1)
|
|
39
|
+
helper(3)
|
|
40
|
+
Other => 0
|
|
41
|
+
|
|
42
|
+
# the free fn in one fn, the shadow in another
|
|
43
|
+
fun usesFree() -> Int = helper(1)
|
|
44
|
+
|
|
45
|
+
# an inlined body's free call must not be captured by a local of the caller
|
|
46
|
+
fun forwards(x: Int) -> Int = helper(x)
|
|
47
|
+
|
|
48
|
+
fun callerBinds() -> Int =
|
|
49
|
+
helper := |x: Int| x + 700
|
|
50
|
+
forwards(1) * 1000 + helper(1)
|
|
51
|
+
|
|
52
|
+
# a binder of the callee must not capture a free call of the closure moved into its copy
|
|
53
|
+
fun applyBoth(helper: fn(Int) -> Int, cb: fn(Int) -> Int) -> Int = helper(1) + cb(2)
|
|
54
|
+
|
|
55
|
+
fun localCallee(cb: fn(Int) -> Int) -> Int =
|
|
56
|
+
helper := |x: Int| x + 1000
|
|
57
|
+
helper(1) + cb(2)
|
|
58
|
+
|
|
59
|
+
# a closure calling a local that shadows a free fn must not move to the callee's copy
|
|
60
|
+
fun each(cb: fn(Int) -> Int) -> Int = cb(1) + cb(2)
|
|
61
|
+
|
|
62
|
+
fun closureOverLocal() -> Int =
|
|
63
|
+
helper := |x: Int| x + 100
|
|
64
|
+
each(|v: Int| helper(v))
|
|
65
|
+
|
|
66
|
+
enum Items =
|
|
67
|
+
| End
|
|
68
|
+
| Item(head: Int, tail: Items)
|
|
69
|
+
|
|
70
|
+
fun build(n: Int) -> Items = if n == 0 then End else Item(n, build(n - 1))
|
|
71
|
+
|
|
72
|
+
fun total(l: Items) -> Int =
|
|
73
|
+
match l
|
|
74
|
+
End => 0
|
|
75
|
+
Item(h, t) => h + total(t)
|
|
76
|
+
|
|
77
|
+
fun localConsumer() -> Int =
|
|
78
|
+
total := |l: Items| 1000
|
|
79
|
+
total(build(3)) + total(Item(1, End))
|
|
80
|
+
|
|
81
|
+
fun helped(l: Items) -> Int =
|
|
82
|
+
match l
|
|
83
|
+
End => 0
|
|
84
|
+
Item(h, t) => helper(h) + helped(t)
|
|
85
|
+
|
|
86
|
+
fun wrapper(l: Items) -> Int = helper(helped(l))
|
|
87
|
+
|
|
88
|
+
fun unfoldedWrapper() -> Int =
|
|
89
|
+
helper := |x: Int| x + 5000
|
|
90
|
+
wrapper(build(3)) * 10000 + helper(0)
|
|
91
|
+
|
|
92
|
+
fun consumerWithFnParam(l: Items, helper: fn(Int) -> Int) -> Int =
|
|
93
|
+
match l
|
|
94
|
+
End => 0
|
|
95
|
+
Item(h, t) => helper(h) + consumerWithFnParam(t, helper)
|
|
96
|
+
|
|
97
|
+
fun viaConsumerParam() -> Int =
|
|
98
|
+
k := |x: Int| x + 5000
|
|
99
|
+
consumerWithFnParam(build(3), k) + consumerWithFnParam(Item(1, End), k)
|
|
100
|
+
|
|
101
|
+
test "a local closure shadows the free fn"
|
|
102
|
+
assert localClosure() == 101
|
|
103
|
+
|
|
104
|
+
test "a parameter shadows the free fn"
|
|
105
|
+
assert viaParam(|x: Int| x + 200) == 201
|
|
106
|
+
|
|
107
|
+
test "a closure parameter shadows the free fn"
|
|
108
|
+
assert viaClosureParam(1) == 301
|
|
109
|
+
|
|
110
|
+
test "a pattern binder shadows the free fn"
|
|
111
|
+
assert viaPattern(Wrap(g: |x: Int| x + 400, label: "k")) == 401
|
|
112
|
+
assert viaPickedArm() == 501
|
|
113
|
+
assert viaRecursiveLocal() == 9
|
|
114
|
+
|
|
115
|
+
test "the free fn still runs elsewhere"
|
|
116
|
+
assert usesFree() == 2
|
|
117
|
+
|
|
118
|
+
test "an inlined body is not captured by the caller's local"
|
|
119
|
+
assert callerBinds() == 2701
|
|
120
|
+
|
|
121
|
+
test "a closure's free call is not captured by the callee"
|
|
122
|
+
assert applyBoth(|x: Int| x + 100, |v: Int| helper(v)) == 104
|
|
123
|
+
assert localCallee(|v: Int| helper(v)) == 1004
|
|
124
|
+
assert localCallee(helper) == 1004
|
|
125
|
+
|
|
126
|
+
test "a closure over a shadowing local stays with that local"
|
|
127
|
+
assert closureOverLocal() == 203
|
|
128
|
+
|
|
129
|
+
test "a local closure named like a consumer is not fused"
|
|
130
|
+
assert localConsumer() == 2000
|
|
131
|
+
|
|
132
|
+
test "an unfolded wrapper is not captured by the caller's local"
|
|
133
|
+
assert unfoldedWrapper() == 105000
|
|
134
|
+
|
|
135
|
+
test "a consumer's fn parameter named like a free fn"
|
|
136
|
+
assert viaConsumerParam() == 20007
|