plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
d61ff9f
— Peter John
2026-11-06T13:36:25+05:30
fip fun: a compile-time guarantee of in-place updates
- compiler/checker/tests/checker/fip_test.rs +176 -0
- compiler/checker/tests/checker/fusion_pipeline_test.rs +1 -1
- compiler/checker/tests/checker/main.rs +1 -0
- compiler/core/src/ast.rs +2 -0
- compiler/core/src/ctors.rs +2 -0
- compiler/core/src/drive.rs +3 -1
- compiler/core/src/fip.rs +98 -0
- compiler/core/src/lib.rs +1 -0
- compiler/core/src/parser.rs +2 -0
- compiler/core/src/simplify.rs +1 -0
- compiler/core/src/tests_lower.rs +1 -0
- compiler/core/src/uniqueness.rs +75 -42
- compiler/core/tests/core/parser_test.rs +16 -0
- compiler/core/tests/core/uniqueness_test.rs +1 -1
- compiler/driver/src/lib.rs +33 -5
- compiler/lsp/src/completion.rs +1 -0
- compiler/tooling/tree-sitter-plum/grammar.js +2 -0
- compiler/tooling/tree-sitter-plum/queries/plum/format.scm +2 -0
- compiler/tooling/tree-sitter-plum/queries/plum/highlights.scm +1 -0
- compiler/tooling/tree-sitter-plum/src/grammar.json +0 -0
- compiler/tooling/tree-sitter-plum/src/node-types.json +0 -0
- compiler/tooling/tree-sitter-plum/src/parser.c +0 -0
- compiler/tooling/tree-sitter-plum/test/corpus/fip.txt +81 -0
- compiler/tooling/vscode-plum/syntaxes/plum.tmLanguage.json +1 -1
- compiler/wasm-codegen/src/emit.rs +1 -0
- docs/plans/2026-11-06-fbip-stage1.md +1 -1
- tests/fbip/fip.plum +75 -0
- tests/fbip/fuzz/fuzz.sh +24 -0
- tests/fbip/fuzz/gen.pl +118 -0
compiler/checker/tests/checker/fip_test.rs
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
//! `fip fun`: every spread and `Array.with` written in the body must be proved in place; a
|
|
3
|
+
//! violation is a compile error at the exact span, from the driver, after the proof ran.
|
|
4
|
+
use plum_core::Diagnostic;
|
|
5
|
+
use plum_driver::PassOptions;
|
|
6
|
+
use plum_test_support::load;
|
|
7
|
+
|
|
8
|
+
const HEAD: &str = "import std/Int\nimport std/Array\n\nstruct Pt(px: Int, py: Int)\n\n";
|
|
9
|
+
|
|
10
|
+
fn run(name: &str, body: &str, opts: &PassOptions) -> Result<(), Vec<Diagnostic>> {
|
|
11
|
+
let source = load(name, &format!("{HEAD}{body}")).expect("loads");
|
|
12
|
+
plum_driver::check(&source, opts).map(|_| ())
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/// 1-based line and column of the first `needle` in `HEAD + body`, and its end (exclusive).
|
|
16
|
+
fn span(body: &str, needle: &str) -> (usize, usize, usize, usize) {
|
|
17
|
+
let all = format!("{HEAD}{body}");
|
|
18
|
+
let at = all.find(needle).unwrap_or_else(|| panic!("no {needle:?}"));
|
|
19
|
+
let line = all[..at].matches('\n').count() + 1;
|
|
20
|
+
let col = at - all[..at].rfind('\n').map_or(0, |n| n + 1) + 1;
|
|
21
|
+
(line, col, line, col + needle.len())
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fn errs(name: &str, body: &str) -> Vec<Diagnostic> {
|
|
25
|
+
run(name, body, &PassOptions::default()).expect_err("a fip violation")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// One error, at exactly `needle`, whose message is `not in place: ` + `reason`.
|
|
29
|
+
fn expectAt(name: &str, body: &str, needle: &str, reason: &str) {
|
|
30
|
+
let es = errs(name, body);
|
|
31
|
+
assert_eq!(es.len(), 1, "{es:?}");
|
|
32
|
+
let e = &es[0];
|
|
33
|
+
assert_eq!((e.line, e.column, e.end_line, e.end_column), span(body, needle), "{e:?}");
|
|
34
|
+
assert_eq!(e.message, format!("not in place: {reason}"), "{e:?}");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#[test]
|
|
38
|
+
fn passing_forms_compile() {
|
|
39
|
+
let body = "\
|
|
40
|
+
fip fun plain(a: Int) -> Int = a + 1
|
|
41
|
+
fip fun fresh() -> Array[Int] = Array.filled(2, 0).with(0, 1)
|
|
42
|
+
fip fun mv(p: Pt) -> Pt = Pt(..p, px: p.px + 1)
|
|
43
|
+
fun useMv() -> Pt = mv(Pt(px: 1, py: 2))
|
|
44
|
+
";
|
|
45
|
+
run("fip_ok", body, &PassOptions::default()).expect("compiles");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#[test]
|
|
49
|
+
fn alias_kept() {
|
|
50
|
+
let body = "fip fun upd(a: Array[Int]) -> Array[Int] =\n b := a\n a.with(0, 1)\n";
|
|
51
|
+
expectAt("fip_alias", body, "a.with(0, 1)", "`a` is also held by `b`");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#[test]
|
|
55
|
+
fn spread_alias_kept() {
|
|
56
|
+
let body = "fip fun upd(s: Pt) -> Pt =\n k := s\n Pt(..s, px: 1)\n";
|
|
57
|
+
expectAt("fip_spread_alias", body, "Pt(..s, px: 1)", "`s` is also held by `k`");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#[test]
|
|
61
|
+
fn stored_in_a_list() {
|
|
62
|
+
let body = "fip fun upd(a: Array[Int]) -> Array[Int] =\n xs := [a]\n a.with(0, 1)\n";
|
|
63
|
+
expectAt("fip_list", body, "a.with(0, 1)", "`a` is stored in field `head`");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#[test]
|
|
67
|
+
fn captured_by_a_closure() {
|
|
68
|
+
let body = "fip fun upd(a: Array[Int]) -> Array[Int] =\n g := || a.get(0)\n a.with(0, 1)\n";
|
|
69
|
+
expectAt("fip_capture", body, "a.with(0, 1)", "`a` is captured by a closure");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[test]
|
|
73
|
+
fn read_after_the_update() {
|
|
74
|
+
let body = "fip fun upd(a: Array[Int]) -> Int =\n b := a.with(0, 1)\n a.get(0)\n";
|
|
75
|
+
expectAt("fip_after", body, "a.with(0, 1)", "`a` is read after the update");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#[test]
|
|
79
|
+
fn shared_by_another_caller() {
|
|
80
|
+
let body = "fip fun upd(a: Array[Int]) -> Array[Int] = a.with(0, 1)\nfun keep(x: Array[Int]) -> Int =\n y := upd(x)\n x.get(0)\n";
|
|
81
|
+
expectAt(
|
|
82
|
+
"fip_caller",
|
|
83
|
+
body,
|
|
84
|
+
"a.with(0, 1)",
|
|
85
|
+
"`a` may be shared by a caller: `x` is passed to `upd`, which may keep it or the caller reads it afterwards (call of `upd` from keep)",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#[test]
|
|
90
|
+
fn spread_shared_by_another_caller() {
|
|
91
|
+
let body = "fip fun upd(p: Pt) -> Pt = Pt(..p, px: 1)\nfun keep(x: Pt) -> Int =\n y := upd(x)\n y.px + x.px\n";
|
|
92
|
+
expectAt(
|
|
93
|
+
"fip_spread_caller",
|
|
94
|
+
body,
|
|
95
|
+
"Pt(..p, px: 1)",
|
|
96
|
+
"`p` may be shared by a caller: `x` is passed to `upd`, which may keep it or the caller reads it afterwards (call of `upd` from keep)",
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[test]
|
|
101
|
+
fn a_caller_that_hands_over_keeps_it_in_place() {
|
|
102
|
+
let body = "fip fun upd(p: Pt) -> Pt = Pt(..p, px: 1)\nfun keep(x: Pt) -> Pt = upd(x)\n";
|
|
103
|
+
run("fip_handover", body, &PassOptions::default()).expect("compiles");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#[test]
|
|
107
|
+
fn update_inside_a_closure() {
|
|
108
|
+
let body = "fip fun upd(a: Array[Int]) -> Array[Int] =\n g := || a.with(0, 1)\n g()\n";
|
|
109
|
+
expectAt("fip_in_closure", body, "a.with(0, 1)", "the update is inside a closure");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#[test]
|
|
113
|
+
fn spread_inside_a_closure_counts() {
|
|
114
|
+
let body = "fip fun upd(p: Pt) -> Pt =\n g := |n: Int| Pt(..p, px: n)\n g(1)\n";
|
|
115
|
+
expectAt("fip_spread_closure", body, "Pt(..p, px: n)", "`p` is captured by a closure");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#[test]
|
|
119
|
+
fn a_fip_method() {
|
|
120
|
+
let body = "\
|
|
121
|
+
struct Bx(items: Array[Int], n: Int) =
|
|
122
|
+
fip fun put(self, x: Int) -> Bx = Bx(..self, items: self.items.with(0, x))
|
|
123
|
+
fun peek(self) -> Int = self.items.get(0)
|
|
124
|
+
fun twice(b: Bx) -> Int =
|
|
125
|
+
c := b.put(1)
|
|
126
|
+
c.peek() + b.peek()
|
|
127
|
+
";
|
|
128
|
+
let es = errs("fip_method", body);
|
|
129
|
+
let why = "`self` may be shared by a caller: `b` is the receiver of `put`, which may keep it or the caller reads it afterwards (call of `put` from twice)";
|
|
130
|
+
let want = [span(body, "Bx(..self, items: self.items.with(0, x))"), span(body, "self.items.with(0, x)")];
|
|
131
|
+
let got: Vec<_> = es.iter().map(|e| (e.line, e.column, e.end_line, e.end_column)).collect();
|
|
132
|
+
assert_eq!(got, want, "{es:?}");
|
|
133
|
+
assert!(es.iter().all(|e| e.message == format!("not in place: {why}")), "{es:?}");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#[test]
|
|
137
|
+
fn a_fip_method_handed_over_compiles() {
|
|
138
|
+
let body = "\
|
|
139
|
+
struct Bx(items: Array[Int], n: Int) =
|
|
140
|
+
fip fun put(self, x: Int) -> Bx = Bx(..self, items: self.items.with(0, x))
|
|
141
|
+
fun once(n: Int) -> Bx =
|
|
142
|
+
b := Bx(items: Array.filled(n, 0), n: n)
|
|
143
|
+
b = b.put(1)
|
|
144
|
+
b
|
|
145
|
+
";
|
|
146
|
+
run("fip_method_ok", body, &PassOptions::default()).expect("compiles");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#[test]
|
|
150
|
+
fn an_inlined_callee_is_reported_at_the_fip_fun() {
|
|
151
|
+
// `helper` is small, so fusion builds its spread into `upd`; the spot is no longer the user's.
|
|
152
|
+
let body = "fun helper(s: Pt) -> Pt = Pt(..s, px: s.px + 1)\nfip fun upd(p: Pt) -> Int =\n q := helper(p)\n q.px + p.px\n";
|
|
153
|
+
let es = errs("fip_inlined", body);
|
|
154
|
+
assert_eq!(es.len(), 1, "{es:?}");
|
|
155
|
+
let (line, col, _, _) = span(body, "fip fun upd");
|
|
156
|
+
assert_eq!((es[0].line, es[0].column), (line, col), "{es:?}");
|
|
157
|
+
assert_eq!(
|
|
158
|
+
es[0].message,
|
|
159
|
+
"not in place: `p` is read after the spread (a spread that fusion built into `fip fun upd`; the exact spot is not kept)"
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#[test]
|
|
164
|
+
fn a_fip_fun_is_not_inlined_away() {
|
|
165
|
+
// Were `upd` inlined into `keep` and left without callers, its own spread would look unshared.
|
|
166
|
+
let body = "fip fun upd(p: Pt) -> Pt = Pt(..p, px: 1)\nfun keep(x: Pt) -> Int =\n y := upd(x)\n y.px + x.px\n";
|
|
167
|
+
assert_eq!(errs("fip_not_inlined", body).len(), 1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#[test]
|
|
171
|
+
fn not_enforced_without_the_proof() {
|
|
172
|
+
let body = "fip fun upd(a: Array[Int]) -> Array[Int] =\n b := a\n a.with(0, 1)\n";
|
|
173
|
+
run("fip_nofbip", body, &PassOptions { fbip: false, ..PassOptions::default() }).expect("--no-fbip: not enforced");
|
|
174
|
+
run("fip_nofusion", body, &PassOptions { fusion: false, ..PassOptions::default() }).expect("--no-fusion: not enforced");
|
|
175
|
+
assert_eq!(errs("fip_on", body).len(), 1);
|
|
176
|
+
}
|
compiler/checker/tests/checker/fusion_pipeline_test.rs
CHANGED
|
@@ -124,7 +124,7 @@ fn explain_lists_unproved_uniqueness() {
|
|
|
124
124
|
let mut copy = parse(src);
|
|
125
125
|
let notes = fusionNotes(&mut copy);
|
|
126
126
|
let n = notes.iter().find(|n| n.contains("not in place")).unwrap_or_else(|| panic!("{notes:?}"));
|
|
127
|
-
assert!(n.contains("
|
|
127
|
+
assert!(n.contains("also held"), "{n}");
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
#[test]
|
compiler/checker/tests/checker/main.rs
CHANGED
|
@@ -6,6 +6,7 @@ mod diagnostic_tests;
|
|
|
6
6
|
mod io_primitive_tests;
|
|
7
7
|
mod explicit_generics_tests;
|
|
8
8
|
mod features;
|
|
9
|
+
mod fip_test;
|
|
9
10
|
mod fusion_pipeline_test;
|
|
10
11
|
mod inline_variants_tests;
|
|
11
12
|
mod question_tests;
|
compiler/core/src/ast.rs
CHANGED
|
@@ -543,6 +543,8 @@ pub struct Fn {
|
|
|
543
543
|
/// `abstract fun m(self) -> T` (no body): an abstract struct method that every
|
|
544
544
|
/// subtype must override.
|
|
545
545
|
pub is_abstract: bool,
|
|
546
|
+
/// `fip fun`: every spread and `Array.with` in the body must be proved in place (driver check).
|
|
547
|
+
pub is_fip: bool,
|
|
546
548
|
/// The fn declared `self` as its first parameter (an instance method). A
|
|
547
549
|
/// fn nested in a type or `primitive` body without it is a static.
|
|
548
550
|
pub has_self: bool,
|
compiler/core/src/ctors.rs
CHANGED
|
@@ -484,6 +484,7 @@ pub fn lowerCtors(source: &mut Source) -> Vec<String> {
|
|
|
484
484
|
is_test: false,
|
|
485
485
|
is_override: false,
|
|
486
486
|
is_abstract: false,
|
|
487
|
+
is_fip: false,
|
|
487
488
|
has_self: true,
|
|
488
489
|
params: vec![],
|
|
489
490
|
returns: None,
|
|
@@ -503,6 +504,7 @@ pub fn lowerCtors(source: &mut Source) -> Vec<String> {
|
|
|
503
504
|
is_test: false,
|
|
504
505
|
is_override: false,
|
|
505
506
|
is_abstract: false,
|
|
507
|
+
is_fip: false,
|
|
506
508
|
has_self: false,
|
|
507
509
|
params: nparams
|
|
508
510
|
.iter()
|
compiler/core/src/drive.rs
CHANGED
|
@@ -513,7 +513,8 @@ impl Driver {
|
|
|
513
513
|
continue;
|
|
514
514
|
}
|
|
515
515
|
}
|
|
516
|
+
// A `fip fun` stays a call, so the uniqueness proof sees every caller of it.
|
|
516
|
-
if f.is_extern || f.is_export {
|
|
517
|
+
if f.is_extern || f.is_export || f.is_fip {
|
|
517
518
|
continue;
|
|
518
519
|
}
|
|
519
520
|
let k = keyOf(f);
|
|
@@ -1849,6 +1850,7 @@ impl Driver {
|
|
|
1849
1850
|
is_test: false,
|
|
1850
1851
|
is_override: false,
|
|
1851
1852
|
is_abstract: false,
|
|
1853
|
+
is_fip: false,
|
|
1852
1854
|
has_self: false,
|
|
1853
1855
|
params,
|
|
1854
1856
|
returns: cret.as_ref().map(|r| substType(r, &map)),
|
compiler/core/src/fip.rs
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
//! `fip fun`: a compile-time guarantee that the function's own updates run in place.
|
|
2
|
+
//!
|
|
3
|
+
//! Every spread `T(..x, ..)` and every `Array.with` call written in the body of a `fip fun`
|
|
4
|
+
//! (closures inside it included) must be proved in place by `uniqueness::markUnique`. Calls to
|
|
5
|
+
//! other functions are not checked: the guarantee is local to the body.
|
|
6
|
+
//!
|
|
7
|
+
//! The check runs on the program after fusion, because only there is the proof complete. Fusion
|
|
8
|
+
//! never inlines or fuses a `fip fun` (simplify and drive skip it), so every call of one stays
|
|
9
|
+
//! visible to the proof and the function is never removed; the flag lives on the `Fn`, so the
|
|
10
|
+
//! check cannot be skipped. A site whose span is not one the function had before fusion (a
|
|
11
|
+
//! callee inlined into its body) is reported at the `fip` function's own line instead, with a
|
|
12
|
+
//! note saying so.
|
|
13
|
+
use crate::ast::*;
|
|
14
|
+
use crate::diag::{DiagKind, Diagnostic};
|
|
15
|
+
use crate::uniqueness::UniqueReport;
|
|
16
|
+
use crate::visit::walkBlockRef;
|
|
17
|
+
use std::collections::{HashMap, HashSet};
|
|
18
|
+
|
|
19
|
+
type SpanKey = (usize, usize, usize, usize);
|
|
20
|
+
|
|
21
|
+
fn key(s: &Span) -> SpanKey {
|
|
22
|
+
(s.startLine, s.startColumn, s.endLine, s.endColumn)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// The spans of the spreads and `.with` calls each `fip fun` had before fusion, by the fn's
|
|
26
|
+
/// own position (the rewrites keep a fn's line and column).
|
|
27
|
+
#[derive(Default)]
|
|
28
|
+
pub struct FipOrigins(HashMap<(String, usize, usize), HashSet<SpanKey>>);
|
|
29
|
+
|
|
30
|
+
impl FipOrigins {
|
|
31
|
+
pub fn isEmpty(&self) -> bool {
|
|
32
|
+
self.0.is_empty()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fn isCheckedSite(e: &Expr) -> bool {
|
|
37
|
+
match &e.kind {
|
|
38
|
+
ExprKind::ClassCall(c) => c.spread.is_some(),
|
|
39
|
+
ExprKind::Attribute(a) => matches!(&a.attr, AttrKind::Method(c) if c.name == "with"),
|
|
40
|
+
_ => false,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Records the `fip fun`s of `src` and the spans of their checked sites.
|
|
45
|
+
pub fn origins(src: &Source) -> FipOrigins {
|
|
46
|
+
let mut out = FipOrigins::default();
|
|
47
|
+
for item in &src.items {
|
|
48
|
+
let Item::Fn(f) = item else { continue };
|
|
49
|
+
if !f.is_fip {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
let mut spans = HashSet::new();
|
|
53
|
+
let mut note = |e: &Expr| {
|
|
54
|
+
if isCheckedSite(e) {
|
|
55
|
+
spans.insert(key(&e.span));
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
match &f.body {
|
|
59
|
+
FnBody::Expr(e) => crate::visit::walkRef(e, &mut note),
|
|
60
|
+
FnBody::Block(b) => walkBlockRef(b, &mut note),
|
|
61
|
+
FnBody::Extern => {}
|
|
62
|
+
}
|
|
63
|
+
out.0.insert((f.file.clone(), f.line, f.column), spans);
|
|
64
|
+
}
|
|
65
|
+
out
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// One error per spread or `Array.with` of a `fip fun` that the proof left a copy.
|
|
69
|
+
pub fn diagnostics(src: &Source, report: &UniqueReport, origins: &FipOrigins) -> Vec<Diagnostic> {
|
|
70
|
+
let mut out: Vec<Diagnostic> = Vec::new();
|
|
71
|
+
for v in &report.fipSites {
|
|
72
|
+
let Err(why) = &v.result else { continue };
|
|
73
|
+
let Some(Item::Fn(f)) = src.items.get(v.itemIdx) else { continue };
|
|
74
|
+
let known = origins.0.get(&(f.file.clone(), f.line, f.column));
|
|
75
|
+
let d = match known {
|
|
76
|
+
Some(spans) if spans.contains(&key(&v.span)) => Diagnostic::new(
|
|
77
|
+
DiagKind::Type,
|
|
78
|
+
f.file.clone(),
|
|
79
|
+
v.span.startLine,
|
|
80
|
+
v.span.startColumn,
|
|
81
|
+
format!("not in place: {why}"),
|
|
82
|
+
)
|
|
83
|
+
.with_end(v.span.endLine, v.span.endColumn),
|
|
84
|
+
_ => Diagnostic::new(
|
|
85
|
+
DiagKind::Type,
|
|
86
|
+
f.file.clone(),
|
|
87
|
+
f.line,
|
|
88
|
+
f.column,
|
|
89
|
+
format!("not in place: {why} (a {} that fusion built into `fip fun {}`; the exact spot is not kept)", v.what, f.name),
|
|
90
|
+
),
|
|
91
|
+
};
|
|
92
|
+
if !out.iter().any(|o| o.file == d.file && o.line == d.line && o.column == d.column && o.message == d.message) {
|
|
93
|
+
out.push(d);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
out.sort_by(|a, b| (&a.file, a.line, a.column).cmp(&(&b.file, b.line, b.column)));
|
|
97
|
+
out
|
|
98
|
+
}
|
compiler/core/src/lib.rs
CHANGED
|
@@ -12,6 +12,7 @@ pub mod drive;
|
|
|
12
12
|
pub mod embeds;
|
|
13
13
|
pub mod enum_checks;
|
|
14
14
|
pub mod formatter;
|
|
15
|
+
pub mod fip;
|
|
15
16
|
pub mod forms;
|
|
16
17
|
pub mod generics;
|
|
17
18
|
pub mod layout;
|
compiler/core/src/parser.rs
CHANGED
|
@@ -481,6 +481,7 @@ impl<'a> AstParser<'a> {
|
|
|
481
481
|
let is_export = node.child_by_field_name("exportKw").is_some();
|
|
482
482
|
let is_override = node.child_by_field_name("overrideKw").is_some();
|
|
483
483
|
let is_abstract = node.child_by_field_name("abstractKw").is_some();
|
|
484
|
+
let is_fip = node.child_by_field_name("fipKw").is_some();
|
|
484
485
|
|
|
485
486
|
let generics: Vec<String> = node
|
|
486
487
|
.child_by_field_name("generics")
|
|
@@ -505,6 +506,7 @@ impl<'a> AstParser<'a> {
|
|
|
505
506
|
is_test: false,
|
|
506
507
|
is_override,
|
|
507
508
|
is_abstract,
|
|
509
|
+
is_fip,
|
|
508
510
|
has_self: !self.childrenOfKind(node, "self").is_empty(),
|
|
509
511
|
params,
|
|
510
512
|
returns,
|
compiler/core/src/simplify.rs
CHANGED
|
@@ -297,6 +297,7 @@ impl<'a> Cx<'a> {
|
|
|
297
297
|
&& !self.recursive.contains(name)
|
|
298
298
|
&& !self.stack.iter().any(|s| s == name)
|
|
299
299
|
&& !f.is_extern
|
|
300
|
+
&& !f.is_fip
|
|
300
301
|
&& !f.is_export
|
|
301
302
|
&& !f.has_self
|
|
302
303
|
&& f.type_param.is_none()
|
compiler/core/src/tests_lower.rs
CHANGED
|
@@ -116,6 +116,7 @@ fn desugarTestsToFns(source: &crate::ast::Source) -> (crate::ast::Source, Vec<(S
|
|
|
116
116
|
is_test: true,
|
|
117
117
|
is_override: false,
|
|
118
118
|
is_abstract: false,
|
|
119
|
+
is_fip: false,
|
|
119
120
|
has_self: true,
|
|
120
121
|
params: ioParam.clone(),
|
|
121
122
|
returns: Some(crate::ast::Type {
|
compiler/core/src/uniqueness.rs
CHANGED
|
@@ -47,6 +47,20 @@ use std::collections::{HashMap, HashSet};
|
|
|
47
47
|
pub struct UniqueReport {
|
|
48
48
|
pub proved: Vec<String>,
|
|
49
49
|
pub unproved: Vec<(String, String)>,
|
|
50
|
+
/// A verdict per spread and per `Array.with` inside a `fip fun` (empty when there is none).
|
|
51
|
+
pub fipSites: Vec<SiteVerdict>,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// One spread or `Array.with` call of a `fip fun`, and whether it was proved in place.
|
|
55
|
+
#[derive(Debug, Clone)]
|
|
56
|
+
pub struct SiteVerdict {
|
|
57
|
+
/// Index of the fn in `Source.items`.
|
|
58
|
+
pub itemIdx: usize,
|
|
59
|
+
pub span: Span,
|
|
60
|
+
/// `"spread"` or `"Array.with"`.
|
|
61
|
+
pub what: &'static str,
|
|
62
|
+
/// `Err` carries the reason in plain words.
|
|
63
|
+
pub result: Result<(), String>,
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
// ---------- traversal ----------
|
|
@@ -432,6 +446,7 @@ struct SpreadWith {
|
|
|
432
446
|
|
|
433
447
|
struct SpreadSite {
|
|
434
448
|
unit: usize,
|
|
449
|
+
node: NodeId,
|
|
435
450
|
typeName: String,
|
|
436
451
|
var: Option<String>,
|
|
437
452
|
given: Vec<String>,
|
|
@@ -646,14 +661,14 @@ impl An {
|
|
|
646
661
|
let param = u.params.iter().position(|p| p.name == v);
|
|
647
662
|
if let Some(i) = param {
|
|
648
663
|
if !u.free && !u.method {
|
|
649
|
-
return Err(format!("{v} is a parameter of an exported or iterator fn; callers are not tracked"));
|
|
664
|
+
return Err(format!("`{v}` is a parameter of an exported or iterator fn; its callers are not tracked"));
|
|
650
665
|
}
|
|
651
666
|
if !u.params[i].plain {
|
|
652
|
-
return Err(format!("{v} is a fn-typed parameter"));
|
|
667
|
+
return Err(format!("`{v}` is a fn-typed parameter"));
|
|
653
668
|
}
|
|
654
669
|
if !entryUnique && !self.paramUnique(unit, i) {
|
|
655
670
|
let why = self.upWhy.borrow().get(&(unit, i)).cloned();
|
|
656
|
-
return Err(why.unwrap_or_else(|| format!("a caller of {} may pass a shared {v}", u.name)));
|
|
671
|
+
return Err(why.unwrap_or_else(|| format!("a caller of `{}` may pass a shared `{v}`", u.name)));
|
|
657
672
|
}
|
|
658
673
|
}
|
|
659
674
|
let mut s = Scan { an: self, v, strict, boundDepth: if param.is_some() { Some(0) } else { None }, err: None };
|
|
@@ -662,7 +677,7 @@ impl An {
|
|
|
662
677
|
return Err(e);
|
|
663
678
|
}
|
|
664
679
|
if s.boundDepth.is_none() {
|
|
665
|
-
return Err(format!("{v} is not a local or parameter (global or field)"));
|
|
680
|
+
return Err(format!("`{v}` is not a local or parameter (a global or a field)"));
|
|
666
681
|
}
|
|
667
682
|
Ok(())
|
|
668
683
|
}
|
|
@@ -673,7 +688,7 @@ impl An {
|
|
|
673
688
|
/// `x.f = x.f.with(..)`, or `T(..s, f: s.f.with(..))` of a unique `s`.
|
|
674
689
|
fn withOk(&self, site: &Site) -> Result<(), String> {
|
|
675
690
|
if site.inClosure {
|
|
676
|
-
return Err("inside a closure".to_string());
|
|
691
|
+
return Err("the update is inside a closure".to_string());
|
|
677
692
|
}
|
|
678
693
|
let Some(Some(recv)) = site.args.first() else { return Err("no receiver".to_string()) };
|
|
679
694
|
if self.fresh(recv) {
|
|
@@ -687,27 +702,27 @@ impl An {
|
|
|
687
702
|
if consumed {
|
|
688
703
|
Ok(())
|
|
689
704
|
} else {
|
|
690
|
-
Err(format!("{v} is
|
|
705
|
+
Err(format!("`{v}` is read after the update"))
|
|
691
706
|
}
|
|
692
707
|
}
|
|
693
708
|
ExprKind::Attribute(a) => {
|
|
694
|
-
let AttrKind::Field(f) = &a.attr else { return Err("receiver is a method result".to_string()) };
|
|
709
|
+
let AttrKind::Field(f) = &a.attr else { return Err("the receiver is a method result".to_string()) };
|
|
695
710
|
match self.spreadWith.get(&site.node) {
|
|
696
711
|
Some(sw) => {
|
|
697
712
|
if !sw.clean {
|
|
698
|
-
return Err(format!("another field of the spread reads {f}"));
|
|
713
|
+
return Err(format!("another field of the spread reads `{f}`"));
|
|
699
714
|
}
|
|
700
715
|
self.varOk(sw.unit, &sw.var)?;
|
|
701
716
|
if self.uf.borrow().contains(&sw.field) {
|
|
702
717
|
Ok(())
|
|
703
718
|
} else {
|
|
704
|
-
Err(format!("field {f} may be shared"))
|
|
719
|
+
Err(format!("field `{f}` may be shared"))
|
|
705
720
|
}
|
|
706
721
|
}
|
|
707
722
|
None => self.argOk(site, recv),
|
|
708
723
|
}
|
|
709
724
|
}
|
|
710
|
-
_ => Err("
|
|
725
|
+
_ => Err("the array may be held elsewhere".to_string()),
|
|
711
726
|
}
|
|
712
727
|
}
|
|
713
728
|
|
|
@@ -728,28 +743,28 @@ impl An {
|
|
|
728
743
|
return Ok(());
|
|
729
744
|
}
|
|
730
745
|
if site.inClosure {
|
|
731
|
-
return Err("argument inside a closure".to_string());
|
|
746
|
+
return Err("an argument is passed from inside a closure".to_string());
|
|
732
747
|
}
|
|
733
748
|
match &e.kind {
|
|
734
749
|
ExprKind::Var(_) | ExprKind::Self_ => self.varOk(site.caller, varName(e).unwrap()),
|
|
735
750
|
ExprKind::Attribute(a) => {
|
|
736
751
|
let AttrKind::Field(f) = &a.attr else {
|
|
737
|
-
return Err("argument is a method call result that may be shared".to_string());
|
|
752
|
+
return Err("an argument is a method call result that may be shared".to_string());
|
|
738
753
|
};
|
|
739
754
|
let ok = match &site.assignBack {
|
|
740
755
|
Some(AssignTarget::Field(tobj, tf)) => tf == f && **tobj == a.object,
|
|
741
756
|
_ => false,
|
|
742
757
|
};
|
|
743
758
|
if !ok {
|
|
744
|
-
return Err(format!("field {f} passed without being written back"));
|
|
759
|
+
return Err(format!("field `{f}` is passed on without being written back"));
|
|
745
760
|
}
|
|
746
761
|
if self.uf.borrow().contains(f) {
|
|
747
762
|
Ok(())
|
|
748
763
|
} else {
|
|
749
|
-
Err(format!("field {f} may be shared"))
|
|
764
|
+
Err(format!("field `{f}` may be shared"))
|
|
750
765
|
}
|
|
751
766
|
}
|
|
752
|
-
_ => Err("argument may be shared".to_string()),
|
|
767
|
+
_ => Err("an argument may be shared".to_string()),
|
|
753
768
|
}
|
|
754
769
|
}
|
|
755
770
|
}
|
|
@@ -814,14 +829,14 @@ impl<'a> Scan<'a> {
|
|
|
814
829
|
}
|
|
815
830
|
|
|
816
831
|
fn escapes(&self) -> String {
|
|
817
|
-
format!("{}
|
|
832
|
+
format!("`{}` is still used after the update", self.v)
|
|
818
833
|
}
|
|
819
834
|
|
|
820
835
|
fn checkPatterns(&mut self, ps: &[CasePattern]) {
|
|
821
836
|
let mut names = vec![];
|
|
822
837
|
ps.iter().for_each(|p| crate::visit::patternBinders(p, &mut names));
|
|
823
838
|
if names.iter().any(|n| n == self.v) {
|
|
824
|
-
self.fail(format!("{} rebound by a pattern", self.v));
|
|
839
|
+
self.fail(format!("`{}` is rebound by a pattern", self.v));
|
|
825
840
|
}
|
|
826
841
|
}
|
|
827
842
|
|
|
@@ -841,27 +856,27 @@ impl<'a> Scan<'a> {
|
|
|
841
856
|
match self.boundDepth {
|
|
842
857
|
None => self.boundDepth = Some(depth),
|
|
843
858
|
Some(d) if d == depth => {}
|
|
844
|
-
Some(_) => self.fail(format!("{v} shadowed in an inner scope")),
|
|
859
|
+
Some(_) => self.fail(format!("`{v}` is shadowed in an inner scope")),
|
|
845
860
|
}
|
|
846
861
|
}
|
|
847
862
|
if !self.bindingOk(val, a.declare) {
|
|
848
|
-
self.fail(format!("{v} bound to a value that may be shared"));
|
|
863
|
+
self.fail(format!("`{v}` is bound to a value that may be shared"));
|
|
849
864
|
} else {
|
|
850
865
|
self.expr(val, false, true);
|
|
851
866
|
}
|
|
852
867
|
}
|
|
853
|
-
AssignTarget::Var(n) => self.operand(val, format!("{v}
|
|
868
|
+
AssignTarget::Var(n) => self.operand(val, format!("`{v}` is also held by `{n}`")),
|
|
854
869
|
AssignTarget::Field(o, f) => {
|
|
855
870
|
if !self.bare(o) {
|
|
856
871
|
self.expr(o, false, false);
|
|
857
872
|
}
|
|
858
|
-
self.operand(val, format!("{v} stored in field {f}"));
|
|
873
|
+
self.operand(val, format!("`{v}` is stored in field `{f}`"));
|
|
859
874
|
}
|
|
860
875
|
}
|
|
861
876
|
} else {
|
|
862
877
|
for t in &a.targets {
|
|
863
878
|
match t {
|
|
864
|
-
AssignTarget::Var(n) if *n == v => self.fail(format!("{v} rebound in a multi-assign")),
|
|
879
|
+
AssignTarget::Var(n) if *n == v => self.fail(format!("`{v}` is rebound in a multi-assign")),
|
|
865
880
|
AssignTarget::Var(_) => {}
|
|
866
881
|
AssignTarget::Field(o, _) => {
|
|
867
882
|
if !self.bare(o) {
|
|
@@ -871,7 +886,7 @@ impl<'a> Scan<'a> {
|
|
|
871
886
|
}
|
|
872
887
|
}
|
|
873
888
|
for val in &a.values {
|
|
874
|
-
self.operand(val, format!("{v}
|
|
889
|
+
self.operand(val, format!("`{v}` is also held through a multi-assign"));
|
|
875
890
|
}
|
|
876
891
|
}
|
|
877
892
|
}
|
|
@@ -894,7 +909,7 @@ impl<'a> Scan<'a> {
|
|
|
894
909
|
for (k, sub) in m.subjects.iter().enumerate() {
|
|
895
910
|
if self.bare(sub) {
|
|
896
911
|
if m.cases.iter().any(|c| matches!(c.patterns.get(k), Some(CasePattern::Name(_)))) {
|
|
897
|
-
self.fail(format!("{v}
|
|
912
|
+
self.fail(format!("`{v}` is also held by a pattern binding"));
|
|
898
913
|
}
|
|
899
914
|
} else {
|
|
900
915
|
self.expr(sub, false, false);
|
|
@@ -927,7 +942,7 @@ impl<'a> Scan<'a> {
|
|
|
927
942
|
&& (!ab || self.resultOk(g))
|
|
928
943
|
});
|
|
929
944
|
if !ok {
|
|
930
|
-
self.fail(format!("{v} passed to {}
|
|
945
|
+
self.fail(format!("`{v}` is passed to `{}`, which may keep it or the caller reads it afterwards", c.name));
|
|
931
946
|
}
|
|
932
947
|
} else {
|
|
933
948
|
self.expr(e, false, false);
|
|
@@ -954,7 +969,7 @@ impl<'a> Scan<'a> {
|
|
|
954
969
|
let readOnly = recvBare && !cands.is_empty() && cands.iter().all(|g| self.an.ro.contains(g));
|
|
955
970
|
if recvBare {
|
|
956
971
|
if !readOnly && !handOver(self, None) {
|
|
957
|
-
self.fail(format!("{v}
|
|
972
|
+
self.fail(format!("`{v}` is the receiver of `{}`, which may keep it or the caller reads it afterwards", c.name));
|
|
958
973
|
}
|
|
959
974
|
} else {
|
|
960
975
|
self.expr(&a.object, false, false);
|
|
@@ -963,7 +978,7 @@ impl<'a> Scan<'a> {
|
|
|
963
978
|
let e = argExpr(x);
|
|
964
979
|
if self.bare(e) {
|
|
965
980
|
if !handOver(self, Some(k)) {
|
|
966
|
-
self.fail(format!("{v} passed to method {}
|
|
981
|
+
self.fail(format!("`{v}` is passed to method `{}`, which may keep it or the caller reads it afterwards", c.name));
|
|
967
982
|
}
|
|
968
983
|
} else {
|
|
969
984
|
self.expr(e, false, false);
|
|
@@ -996,14 +1011,14 @@ impl<'a> Scan<'a> {
|
|
|
996
1011
|
match &cc.spread {
|
|
997
1012
|
Some(s) if self.bare(s) => {
|
|
998
1013
|
if !(tail || ab) {
|
|
999
|
-
self.fail(format!("{v}
|
|
1014
|
+
self.fail(format!("`{v}` is read after the spread"));
|
|
1000
1015
|
}
|
|
1001
1016
|
}
|
|
1002
1017
|
Some(s) => self.expr(s, false, false),
|
|
1003
1018
|
None => {}
|
|
1004
1019
|
}
|
|
1005
1020
|
for fa in &cc.fields {
|
|
1006
|
-
self.operand(&fa.value, format!("{v} stored in field {}", fa.name));
|
|
1021
|
+
self.operand(&fa.value, format!("`{v}` is stored in field `{}`", fa.name));
|
|
1007
1022
|
}
|
|
1008
1023
|
}
|
|
1009
1024
|
ExprKind::Attribute(a) => match &a.attr {
|
|
@@ -1025,7 +1040,7 @@ impl<'a> Scan<'a> {
|
|
|
1025
1040
|
true
|
|
1026
1041
|
});
|
|
1027
1042
|
if found {
|
|
1028
|
-
self.fail(format!("{v} captured by a closure"));
|
|
1043
|
+
self.fail(format!("`{v}` is captured by a closure"));
|
|
1029
1044
|
}
|
|
1030
1045
|
}
|
|
1031
1046
|
ExprKind::Binary(b) => {
|
|
@@ -1076,19 +1091,26 @@ pub fn markUnique(src: &mut Source) -> UniqueReport {
|
|
|
1076
1091
|
solve(&an);
|
|
1077
1092
|
// Report and mark.
|
|
1078
1093
|
let mut report = UniqueReport::default();
|
|
1094
|
+
let isFip = |ui: usize| matches!(&src.items[an.units[ui].itemIdx], Item::Fn(f) if f.is_fip);
|
|
1095
|
+
let mut verdicts: Vec<(usize, NodeId, &'static str, Result<(), String>)> = Vec::new();
|
|
1079
1096
|
let mut okVars: Vec<HashSet<String>> = vec![HashSet::new(); an.units.len()];
|
|
1080
1097
|
for (ui, u) in an.units.iter().enumerate() {
|
|
1081
1098
|
let label = format!("{}:{}", u.label, lineOf(src, u));
|
|
1082
1099
|
for s in an.spreads.iter().filter(|s| s.unit == ui) {
|
|
1083
|
-
match &s.var {
|
|
1100
|
+
let verdict = match &s.var {
|
|
1084
|
-
None =>
|
|
1101
|
+
None => Err("the spread source is not a plain variable".to_string()),
|
|
1085
|
-
Some(v) =>
|
|
1102
|
+
Some(v) => an.varOk(ui, v),
|
|
1103
|
+
};
|
|
1104
|
+
match (&s.var, &verdict) {
|
|
1086
|
-
|
|
1105
|
+
(Some(v), Ok(())) => {
|
|
1087
|
-
|
|
1106
|
+
report.proved.push(label.clone());
|
|
1088
|
-
|
|
1107
|
+
okVars[ui].insert(v.clone());
|
|
1089
|
-
|
|
1108
|
+
}
|
|
1090
|
-
|
|
1109
|
+
(_, Err(r)) => report.unproved.push((label.clone(), r.clone())),
|
|
1110
|
+
_ => {}
|
|
1091
|
-
|
|
1111
|
+
}
|
|
1112
|
+
if isFip(ui) {
|
|
1113
|
+
verdicts.push((ui, s.node, "spread", verdict));
|
|
1092
1114
|
}
|
|
1093
1115
|
}
|
|
1094
1116
|
}
|
|
@@ -1100,12 +1122,16 @@ pub fn markUnique(src: &mut Source) -> UniqueReport {
|
|
|
1100
1122
|
continue;
|
|
1101
1123
|
}
|
|
1102
1124
|
let label = format!("{}:{} (array write)", u.label, lineOf(src, u));
|
|
1103
|
-
|
|
1125
|
+
let verdict = an.withOk(site);
|
|
1126
|
+
match &verdict {
|
|
1104
1127
|
Ok(()) => {
|
|
1105
1128
|
withMarks.insert(site.node);
|
|
1106
1129
|
report.proved.push(label);
|
|
1107
1130
|
}
|
|
1108
|
-
Err(r) => report.unproved.push((label, r)),
|
|
1131
|
+
Err(r) => report.unproved.push((label, r.clone())),
|
|
1132
|
+
}
|
|
1133
|
+
if isFip(site.caller) {
|
|
1134
|
+
verdicts.push((site.caller, site.node, "Array.with", verdict));
|
|
1109
1135
|
}
|
|
1110
1136
|
}
|
|
1111
1137
|
// Mutable pass.
|
|
@@ -1114,7 +1140,9 @@ pub fn markUnique(src: &mut Source) -> UniqueReport {
|
|
|
1114
1140
|
continue;
|
|
1115
1141
|
}
|
|
1116
1142
|
let ok = &okVars[ui];
|
|
1143
|
+
let mut spans: HashMap<NodeId, Span> = HashMap::new();
|
|
1117
1144
|
let mut mark = |e: &mut Expr| {
|
|
1145
|
+
spans.insert(e.id, e.span);
|
|
1118
1146
|
if withMarks.contains(&e.id) {
|
|
1119
1147
|
if let ExprKind::Attribute(a) = &mut e.kind {
|
|
1120
1148
|
if let AttrKind::Method(c) = &mut a.attr {
|
|
@@ -1141,6 +1169,10 @@ pub fn markUnique(src: &mut Source) -> UniqueReport {
|
|
|
1141
1169
|
Item::Test(t) => mutBlock(&mut t.body, &mut mark),
|
|
1142
1170
|
_ => {}
|
|
1143
1171
|
}
|
|
1172
|
+
for (_, node, what, result) in verdicts.iter().filter(|v| v.0 == ui) {
|
|
1173
|
+
let span = spans.get(node).copied().unwrap_or_default();
|
|
1174
|
+
report.fipSites.push(SiteVerdict { itemIdx: u.itemIdx, span, what, result: result.clone() });
|
|
1175
|
+
}
|
|
1144
1176
|
}
|
|
1145
1177
|
report
|
|
1146
1178
|
}
|
|
@@ -1702,6 +1734,7 @@ fn collectFacts(an: &mut An) {
|
|
|
1702
1734
|
if let Some(s) = &cc.spread {
|
|
1703
1735
|
spreads.push(SpreadSite {
|
|
1704
1736
|
unit: ui,
|
|
1737
|
+
node: e.id,
|
|
1705
1738
|
typeName: cc.type_name.clone(),
|
|
1706
1739
|
var: varName(s).map(|v| v.to_string()),
|
|
1707
1740
|
given,
|
|
@@ -1907,7 +1940,7 @@ fn solve(an: &An) {
|
|
|
1907
1940
|
Err(m) => {
|
|
1908
1941
|
let from = &an.units[site.caller].label;
|
|
1909
1942
|
if why.is_none() {
|
|
1910
|
-
why = Some(format!("{}
|
|
1943
|
+
why = Some(format!("`{}` may be shared by a caller: {} (call of `{}` from {})", an.units[g].params[j].name, m, an.units[g].name, from));
|
|
1911
1944
|
}
|
|
1912
1945
|
}
|
|
1913
1946
|
Ok(()) => okFrom = okFrom.or(Some(site.caller)),
|
compiler/core/tests/core/parser_test.rs
CHANGED
|
@@ -825,6 +825,22 @@ fn abstractFunSetsFlag() {
|
|
|
825
825
|
assert!(fns.iter().any(|f| f.name == "other" && !f.is_abstract));
|
|
826
826
|
}
|
|
827
827
|
|
|
828
|
+
#[test]
|
|
829
|
+
fn fipFunSetsFlag() {
|
|
830
|
+
let s = parseRaw("fip fun a1() -> Int = 1\nfun b1() -> Int = 2\nstruct Sq(x: Int) =\n override fip fun d(self) -> Int = self.x\n");
|
|
831
|
+
let fns: Vec<&Fn> = s.items.iter().filter_map(|i| if let Item::Fn(f) = i { Some(f) } else { None }).collect();
|
|
832
|
+
assert!(fns.iter().any(|f| f.name == "a1" && f.is_fip && !f.is_override));
|
|
833
|
+
assert!(fns.iter().any(|f| f.name == "b1" && !f.is_fip));
|
|
834
|
+
assert!(fns.iter().any(|f| f.name == "d" && f.is_fip && f.is_override));
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
#[test]
|
|
838
|
+
fn fipStaysAnOrdinaryName() {
|
|
839
|
+
// a variable, a field and a field read called `fip` still parse
|
|
840
|
+
let s = parseRaw("struct Hold(fip: Int) =\n fun get(self) -> Int = self.fip\nfun main() -> Int =\n fip := 3\n fip + 1\n");
|
|
841
|
+
assert!(s.items.iter().all(|i| !matches!(i, Item::Fn(f) if f.is_fip)));
|
|
842
|
+
}
|
|
843
|
+
|
|
828
844
|
// `try g()`, `g() catch 0`, `null` and `error.Bad` are not listed: they are now ordinary names, so they read as separate
|
|
829
845
|
// expressions or a field access and fail in the checker (undefined name), not the parser.
|
|
830
846
|
#[test]
|
compiler/core/tests/core/uniqueness_test.rs
CHANGED
|
@@ -85,7 +85,7 @@ fn aliased_by_local_not_unique() {
|
|
|
85
85
|
let s = format!("{PRE}fun bad(s: Pt) -> Pt =\n k := s\n Pt(..s, x: 1)\n");
|
|
86
86
|
let (src, r) = run(&s);
|
|
87
87
|
assert!(!proved(&r, "bad:3"));
|
|
88
|
-
assert_eq!(reason(&r, "bad:3"), "s
|
|
88
|
+
assert_eq!(reason(&r, "bad:3"), "`s` is also held by `k`");
|
|
89
89
|
assert!(!anyUnique(&src, "bad"));
|
|
90
90
|
}
|
|
91
91
|
|
compiler/driver/src/lib.rs
CHANGED
|
@@ -7,6 +7,7 @@ use plum_checker::{checkOnce, checkProgram, TypedInfo};
|
|
|
7
7
|
use plum_core::ast::Source;
|
|
8
8
|
use plum_core::closure_spec::Hints;
|
|
9
9
|
use plum_core::diag::Diagnostic;
|
|
10
|
+
use std::cell::RefCell;
|
|
10
11
|
use std::rc::Rc;
|
|
11
12
|
|
|
12
13
|
/// Which passes run, and whether dropped or applied rewrites are explained on stderr.
|
|
@@ -46,6 +47,8 @@ pub struct Env<'a> {
|
|
|
46
47
|
pub fbip: bool,
|
|
47
48
|
/// Receiver types of method calls, read off the checked program (see `closureHints`).
|
|
48
49
|
pub hints: &'a Hints,
|
|
50
|
+
/// Errors a pass found in the program it rewrote (`fip fun` sites that stayed copies).
|
|
51
|
+
pub errors: &'a RefCell<Vec<Diagnostic>>,
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
/// One rewrite pass: changes the program in place, returns whether anything changed.
|
|
@@ -68,6 +71,14 @@ pub fn fusionNotes(copy: &mut Source) -> Vec<String> {
|
|
|
68
71
|
|
|
69
72
|
/// `fusionNotes`; with `fbip` off, uniqueness and cell reuse do not run, so nothing is updated in place.
|
|
70
73
|
pub fn fusionNotesWith(copy: &mut Source, fbip: bool) -> Vec<String> {
|
|
74
|
+
fusionRun(copy, fbip).0
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// `fusionNotesWith`, and the errors of the `fip fun`s whose spreads or `Array.with` calls the
|
|
78
|
+
/// proof left as copies (see `plum_core::fip`). Needs `fbip`; without it nothing is enforced.
|
|
79
|
+
pub fn fusionRun(copy: &mut Source, fbip: bool) -> (Vec<String>, Vec<Diagnostic>) {
|
|
80
|
+
let origins = plum_core::fip::origins(copy);
|
|
81
|
+
let mut errors = Vec::new();
|
|
71
82
|
let pure = plum_core::purity::pureFns(copy);
|
|
72
83
|
let mut notes = plum_core::simplify::simplify(copy, &pure);
|
|
73
84
|
notes.extend(plum_core::drive::drive(copy, &pure));
|
|
@@ -75,15 +86,17 @@ pub fn fusionNotesWith(copy: &mut Source, fbip: bool) -> Vec<String> {
|
|
|
75
86
|
let rep = plum_core::uniqueness::markUnique(copy);
|
|
76
87
|
notes.extend(rep.proved.iter().map(|l| format!("unique spread at {l}")));
|
|
77
88
|
notes.extend(rep.unproved.iter().map(|(l, why)| format!("uniqueness: not in place {l}: {why}")));
|
|
89
|
+
errors = plum_core::fip::diagnostics(copy, &rep, &origins);
|
|
78
90
|
notes.retain(|n| !n.is_empty());
|
|
79
91
|
notes.extend(plum_core::reuse::markReuse(copy));
|
|
80
92
|
}
|
|
81
93
|
notes.retain(|n| !n.is_empty());
|
|
82
|
-
notes
|
|
94
|
+
(notes, errors)
|
|
83
95
|
}
|
|
84
96
|
|
|
85
97
|
fn fusion(copy: &mut Source, env: &Env) -> bool {
|
|
86
|
-
let notes =
|
|
98
|
+
let (notes, errors) = fusionRun(copy, env.fbip);
|
|
99
|
+
env.errors.borrow_mut().extend(errors);
|
|
87
100
|
if env.explain {
|
|
88
101
|
for n in ¬es {
|
|
89
102
|
eprintln!("fusion: {n}");
|
|
@@ -110,6 +123,8 @@ struct State {
|
|
|
110
123
|
pre: Source,
|
|
111
124
|
result: Result<(), Vec<Diagnostic>>,
|
|
112
125
|
info: TypedInfo,
|
|
126
|
+
/// `fip fun` violations, kept even when the rewrite that found them is dropped.
|
|
127
|
+
fip: Vec<Diagnostic>,
|
|
113
128
|
}
|
|
114
129
|
|
|
115
130
|
/// Checks `source`, then runs the enabled passes of `PASSES` in order, each adopted only if
|
|
@@ -122,8 +137,11 @@ pub fn check(source: &Source, opts: &PassOptions) -> Result<TypedInfo, Vec<Diagn
|
|
|
122
137
|
/// `check` with an explicit pass list.
|
|
123
138
|
pub fn check_with(source: &Source, opts: &PassOptions, passes: &[Pass]) -> Result<TypedInfo, Vec<Diagnostic>> {
|
|
124
139
|
let (result, info) = checkProgram(source);
|
|
125
|
-
let mut st = State { pre: preUnionSource(source), result, info };
|
|
140
|
+
let mut st = State { pre: preUnionSource(source), result, info, fip: Vec::new() };
|
|
126
141
|
if st.result.is_ok() {
|
|
142
|
+
if opts.explain && !(opts.fusion && opts.fbip) && !plum_core::fip::origins(&st.pre).isEmpty() {
|
|
143
|
+
eprintln!("fusion: fip fun is not enforced: it needs the uniqueness proof, which --no-fusion and --no-fbip switch off");
|
|
144
|
+
}
|
|
127
145
|
for pass in passes.iter().filter(|p| (p.enabled)(opts)) {
|
|
128
146
|
runPass(pass, source, opts, &mut st);
|
|
129
147
|
}
|
|
@@ -132,6 +150,9 @@ pub fn check_with(source: &Source, opts: &PassOptions, passes: &[Pass]) -> Resul
|
|
|
132
150
|
if let Some(lowered) = st.info.lowered.as_mut().and_then(Rc::get_mut) {
|
|
133
151
|
plum_core::tail::markTailCalls(lowered);
|
|
134
152
|
}
|
|
153
|
+
if st.result.is_ok() && !st.fip.is_empty() {
|
|
154
|
+
return Err(st.fip);
|
|
155
|
+
}
|
|
135
156
|
st.result.map(|()| st.info)
|
|
136
157
|
}
|
|
137
158
|
|
|
@@ -148,7 +169,8 @@ fn runPass(pass: &Pass, original: &Source, opts: &PassOptions, st: &mut State) {
|
|
|
148
169
|
} else {
|
|
149
170
|
Hints::new()
|
|
150
171
|
};
|
|
172
|
+
let errors = RefCell::new(Vec::new());
|
|
151
|
-
let env = Env { explain: opts.explain, fbip: opts.fbip, hints: &hints };
|
|
173
|
+
let env = Env { explain: opts.explain, fbip: opts.fbip, hints: &hints, errors: &errors };
|
|
152
174
|
let mut copy = match pass.target {
|
|
153
175
|
Target::Pre => st.pre.clone(),
|
|
154
176
|
Target::Lowered => match st.info.lowered.as_ref() {
|
|
@@ -156,7 +178,13 @@ fn runPass(pass: &Pass, original: &Source, opts: &PassOptions, st: &mut State) {
|
|
|
156
178
|
None => return,
|
|
157
179
|
},
|
|
158
180
|
};
|
|
159
|
-
|
|
181
|
+
let changed = (pass.rewrite)(&mut copy, &env);
|
|
182
|
+
for d in errors.into_inner() {
|
|
183
|
+
if !st.fip.contains(&d) {
|
|
184
|
+
st.fip.push(d);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if !changed {
|
|
160
188
|
return;
|
|
161
189
|
}
|
|
162
190
|
// A pass builds nodes with no id and copies code; number the new ones, and in a debug
|
compiler/lsp/src/completion.rs
CHANGED
|
@@ -647,6 +647,7 @@ mod tests {
|
|
|
647
647
|
is_test: false,
|
|
648
648
|
is_override: false,
|
|
649
649
|
is_abstract: false,
|
|
650
|
+
is_fip: false,
|
|
650
651
|
has_self: false,
|
|
651
652
|
params: vec![],
|
|
652
653
|
returns: None,
|
compiler/tooling/tree-sitter-plum/grammar.js
CHANGED
|
@@ -249,6 +249,8 @@ module.exports = grammar({
|
|
|
249
249
|
// `export fun foo(...) = ...` is a normal function that is also exported from
|
|
250
250
|
// the wasm module under its own name, with the `extern` boundary ABI.
|
|
251
251
|
optional(choice(field("externKw", "extern"), field("exportKw", "export"), field("overrideKw", "override"), field("abstractKw", "abstract"))),
|
|
252
|
+
// `fip fun`: a compile-time guarantee that the body's updates run in place.
|
|
253
|
+
optional(field("fipKw", "fip")),
|
|
252
254
|
field("kw", "fun"),
|
|
253
255
|
field("name", $.fn_identifier),
|
|
254
256
|
// Declared type parameters: `fun map[B](..)`, `fun id[Elem](..)`.
|
compiler/tooling/tree-sitter-plum/queries/plum/format.scm
CHANGED
|
@@ -132,6 +132,8 @@
|
|
|
132
132
|
|
|
133
133
|
(fn "abstract" @append_space)
|
|
134
134
|
|
|
135
|
+
(fn "fip" @append_space)
|
|
136
|
+
|
|
135
137
|
(fn "fun" @append_space)
|
|
136
138
|
|
|
137
139
|
(if "if" @append_space)
|
compiler/tooling/tree-sitter-plum/queries/plum/highlights.scm
CHANGED
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
"export"
|
|
96
96
|
"override"
|
|
97
97
|
"abstract"
|
|
98
|
+
"fip"
|
|
98
99
|
"fn"
|
|
99
100
|
"test"
|
|
100
101
|
] @keyword
|
compiler/tooling/tree-sitter-plum/src/grammar.json
CHANGED
|
Binary file
|
compiler/tooling/tree-sitter-plum/src/node-types.json
CHANGED
|
Binary file
|
compiler/tooling/tree-sitter-plum/src/parser.c
CHANGED
|
Binary file
|
compiler/tooling/tree-sitter-plum/test/corpus/fip.txt
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
================================================================================
|
|
2
|
+
fip fun
|
|
3
|
+
================================================================================
|
|
4
|
+
|
|
5
|
+
fip fun step(n: Int) -> Int = n
|
|
6
|
+
|
|
7
|
+
--------------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
(source
|
|
10
|
+
(fn
|
|
11
|
+
(fn_identifier)
|
|
12
|
+
(param
|
|
13
|
+
(var_identifier)
|
|
14
|
+
(type
|
|
15
|
+
(type_identifier)))
|
|
16
|
+
(type
|
|
17
|
+
(type_identifier))
|
|
18
|
+
(expression
|
|
19
|
+
(primary_expression
|
|
20
|
+
(var_identifier)))))
|
|
21
|
+
|
|
22
|
+
================================================================================
|
|
23
|
+
override fip fun in a struct body
|
|
24
|
+
================================================================================
|
|
25
|
+
|
|
26
|
+
struct P(x: Int, fip: Int) =
|
|
27
|
+
override fip fun size(self) -> Int = self.fip
|
|
28
|
+
|
|
29
|
+
--------------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
(source
|
|
32
|
+
(struct
|
|
33
|
+
(type_identifier)
|
|
34
|
+
(enum_named_field
|
|
35
|
+
(var_identifier)
|
|
36
|
+
(type
|
|
37
|
+
(type_identifier)))
|
|
38
|
+
(enum_named_field
|
|
39
|
+
(var_identifier)
|
|
40
|
+
(type
|
|
41
|
+
(type_identifier)))
|
|
42
|
+
(fn
|
|
43
|
+
(fn_identifier)
|
|
44
|
+
(self)
|
|
45
|
+
(type
|
|
46
|
+
(type_identifier))
|
|
47
|
+
(expression
|
|
48
|
+
(primary_expression
|
|
49
|
+
(attribute
|
|
50
|
+
(primary_expression
|
|
51
|
+
(self))
|
|
52
|
+
(fn_identifier)))))))
|
|
53
|
+
|
|
54
|
+
================================================================================
|
|
55
|
+
fip as a variable name
|
|
56
|
+
================================================================================
|
|
57
|
+
|
|
58
|
+
fun main() -> Int =
|
|
59
|
+
fip := 3
|
|
60
|
+
fip + 1
|
|
61
|
+
|
|
62
|
+
--------------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
(source
|
|
65
|
+
(fn
|
|
66
|
+
(fn_identifier)
|
|
67
|
+
(type
|
|
68
|
+
(type_identifier))
|
|
69
|
+
(body
|
|
70
|
+
(assign
|
|
71
|
+
(var_identifier)
|
|
72
|
+
(expression
|
|
73
|
+
(primary_expression
|
|
74
|
+
(integer))))
|
|
75
|
+
(expression
|
|
76
|
+
(primary_expression
|
|
77
|
+
(binary_operator
|
|
78
|
+
(primary_expression
|
|
79
|
+
(var_identifier))
|
|
80
|
+
(primary_expression
|
|
81
|
+
(integer))))))))
|
compiler/tooling/vscode-plum/syntaxes/plum.tmLanguage.json
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"keywords": {
|
|
40
40
|
"patterns": [
|
|
41
41
|
{
|
|
42
|
-
"match": "\\b(module|struct|enum|trait|primitive|fun|fn|extern|export|self|test)(?![A-Za-z0-9_])",
|
|
42
|
+
"match": "\\b(module|struct|enum|trait|primitive|fun|fn|extern|export|override|abstract|fip|self|test)(?![A-Za-z0-9_])",
|
|
43
43
|
"name": "keyword.const.plum"
|
|
44
44
|
},
|
|
45
45
|
{
|
compiler/wasm-codegen/src/emit.rs
CHANGED
|
@@ -64,6 +64,7 @@ impl<'a> Emit<'a> {
|
|
|
64
64
|
is_test: false,
|
|
65
65
|
is_override: false,
|
|
66
66
|
is_abstract: false,
|
|
67
|
+
is_fip: false,
|
|
67
68
|
has_self: true,
|
|
68
69
|
params: Vec::new(),
|
|
69
70
|
returns: None,
|
docs/plans/2026-11-06-fbip-stage1.md
CHANGED
|
@@ -70,7 +70,7 @@ e.g. `v = v.bumped()` in a `for` loop, with `bumped(self) = Cell(..self, n: self
|
|
|
70
70
|
**Test code:** `tests/fbip/aliasing.plum`: keep an alias then update; store in a list then update; capture in a closure; return both; a value in two fields; a shared value through a method; an override keeping `self`; update in only one branch. Each gives the copying answer, and matches `--no-fbip`. Plus positive tests in `uniqueness_test.rs`.
|
|
71
71
|
|
|
72
72
|
### T4 Array.with · Complexity 5 · Est. context 55k
|
|
73
|
-
**Owns:** std/Array.plum, compiler/core/src/*.rs, compiler/core/tests/core/*.rs, compiler/wasm-codegen/src/*.rs, compiler/wasm-codegen/tests/wasm_codegen/**/*.rs, compiler/checker/src/*.rs, tests/fbip/*.plum
|
|
73
|
+
**Owns:** std/Array.plum, compiler/core/src/*.rs, compiler/core/tests/core/*.rs, compiler/wasm-codegen/src/*.rs, compiler/wasm-codegen/tests/wasm_codegen/**/*.rs, compiler/checker/src/*.rs, tests/fbip/*.plum, tests/fbip/fuzz/*
|
|
74
74
|
**Components:** 2
|
|
75
75
|
**LOC owned:** 180
|
|
76
76
|
**Tests:** fast
|
tests/fbip/fip.plum
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import std/Int
|
|
2
|
+
import std/Array
|
|
3
|
+
|
|
4
|
+
# `fip fun` forms that must compile: every spread and `Array.with` written in the body is
|
|
5
|
+
# proved in place. The sweep also runs this file with `--no-fbip` and `--no-fusion`, where `fip`
|
|
6
|
+
# is not enforced and the programs still give the same answers. Names are unique per scenario
|
|
7
|
+
# because the proof resolves methods and fields by name.
|
|
8
|
+
|
|
9
|
+
# a fip fun with no update at all is trivially true
|
|
10
|
+
fip fun plainSum(a: Int, b: Int) -> Int = a + b
|
|
11
|
+
|
|
12
|
+
# a fresh array written once
|
|
13
|
+
fip fun freshWrite() -> Array[Int] = Array.filled(3, 0).with(1, 7)
|
|
14
|
+
|
|
15
|
+
# a local array written in a loop
|
|
16
|
+
fip fun squares(n: Int) -> Array[Int] =
|
|
17
|
+
a := Array.filled(n, 0)
|
|
18
|
+
for i := range n
|
|
19
|
+
a = a.with(i, i * i)
|
|
20
|
+
a
|
|
21
|
+
|
|
22
|
+
# a spread of a parameter every caller builds fresh
|
|
23
|
+
struct Pt(px: Int, py: Int)
|
|
24
|
+
|
|
25
|
+
fip fun movePt(p: Pt, dx: Int) -> Pt = Pt(..p, px: p.px + dx)
|
|
26
|
+
|
|
27
|
+
fun walkPt(n: Int) -> Pt =
|
|
28
|
+
p := Pt(px: 0, py: 5)
|
|
29
|
+
for i := range n
|
|
30
|
+
p = movePt(p, 2)
|
|
31
|
+
p
|
|
32
|
+
|
|
33
|
+
# a fip method: self handed back by `v = v.push(..)`
|
|
34
|
+
struct Bag(items: Array[Int], count: Int) =
|
|
35
|
+
fip fun bagAdd(self, i: Int, x: Int) -> Bag = Bag(..self, items: self.items.with(i, x), count: self.count + 1)
|
|
36
|
+
fun bagAt(self, i: Int) -> Int = self.items.get(i)
|
|
37
|
+
|
|
38
|
+
fun fillBag(n: Int) -> Bag =
|
|
39
|
+
b := Bag(items: Array.filled(n, 0), count: 0)
|
|
40
|
+
for i := range n
|
|
41
|
+
b = b.bagAdd(i, i + 10)
|
|
42
|
+
b
|
|
43
|
+
|
|
44
|
+
# a fip fun whose update sits in a closure-free helper chain
|
|
45
|
+
fip fun bumpAt(xs: Array[Int], i: Int) -> Array[Int] = xs.with(i, xs.get(i) + 1)
|
|
46
|
+
|
|
47
|
+
# a generic fip fun
|
|
48
|
+
fip fun setFirst[Elem](xs: Array[Elem], x: Elem) -> Array[Elem] = xs.with(0, x)
|
|
49
|
+
|
|
50
|
+
test "fip: plain"
|
|
51
|
+
assert plainSum(2, 3) == 5
|
|
52
|
+
|
|
53
|
+
test "fip: fresh array write"
|
|
54
|
+
a := freshWrite()
|
|
55
|
+
assert a.get(0) == 0 && a.get(1) == 7
|
|
56
|
+
|
|
57
|
+
test "fip: loop of writes"
|
|
58
|
+
a := squares(5)
|
|
59
|
+
assert a.get(3) == 9 && a.get(4) == 16
|
|
60
|
+
|
|
61
|
+
test "fip: spread of a fresh parameter"
|
|
62
|
+
p := walkPt(4)
|
|
63
|
+
assert p.px == 8 && p.py == 5
|
|
64
|
+
|
|
65
|
+
test "fip: method"
|
|
66
|
+
b := fillBag(4)
|
|
67
|
+
assert b.bagAt(0) == 10 && b.bagAt(3) == 13 && b.count == 4
|
|
68
|
+
|
|
69
|
+
test "fip: read then write"
|
|
70
|
+
a := bumpAt(Array.filled(2, 4), 1)
|
|
71
|
+
assert a.get(1) == 5 && a.get(0) == 4
|
|
72
|
+
|
|
73
|
+
test "fip: generic"
|
|
74
|
+
a := setFirst(Array.filled(2, 1), 9)
|
|
75
|
+
assert a.get(0) == 9 && a.get(1) == 1
|
tests/fbip/fuzz/fuzz.sh
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Differential fuzzer for the FBIP uniqueness proof.
|
|
3
|
+
# A wrong proof writes through a value another holder can see, so a generated
|
|
4
|
+
# program must give the same result with and without --no-fbip. Any DIFF is a
|
|
5
|
+
# real miscompile. Usage (after a build): tests/fbip/fuzz/fuzz.sh FROM TO (seeds)
|
|
6
|
+
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
root="$(cd "$here/../../.." && pwd)"
|
|
8
|
+
cd "$root" || exit 1
|
|
9
|
+
out=$(mktemp -d "${TMPDIR:-/tmp}/plum-fuzz.XXXXXX")
|
|
10
|
+
ok=0; diff=0; err=0
|
|
11
|
+
for i in $(seq "$1" "$2"); do
|
|
12
|
+
f="$out/p$i.plum"
|
|
13
|
+
perl "$here/gen.pl" "$i" "$f"
|
|
14
|
+
a=$(target/debug/plum test --lib-path . "$f" 2>&1)
|
|
15
|
+
if echo "$a" | grep -q "^error"; then
|
|
16
|
+
err=$((err+1)); echo "ERR $i: $(echo "$a" | head -1 | cut -c1-200)"; continue
|
|
17
|
+
fi
|
|
18
|
+
b=$(target/debug/plum test --no-fbip --lib-path . "$f" 2>&1)
|
|
19
|
+
if [ "$a" == "$b" ]; then ok=$((ok+1)); rm -f "$f"
|
|
20
|
+
else diff=$((diff+1)); echo "DIFF $i: $f"; fi
|
|
21
|
+
done
|
|
22
|
+
echo "done ok=$ok diff=$diff err=$err"
|
|
23
|
+
echo "$out"
|
|
24
|
+
[ "$diff" -eq 0 ]
|
tests/fbip/fuzz/gen.pl
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/perl
|
|
2
|
+
use strict; use warnings;
|
|
3
|
+
my ($seed, $out) = @ARGV;
|
|
4
|
+
srand($seed);
|
|
5
|
+
sub r { return int(rand($_[0])); }
|
|
6
|
+
sub pick { return $_[r(scalar @_)]; }
|
|
7
|
+
my @arr = ('a','b','c','d','e');
|
|
8
|
+
my @st = ('s','t','u');
|
|
9
|
+
sub aexpr { # Array[Int] expression (reads)
|
|
10
|
+
return pick(@arr);
|
|
11
|
+
}
|
|
12
|
+
sub iexpr {
|
|
13
|
+
my $k = r(9);
|
|
14
|
+
return r(5) if $k == 0;
|
|
15
|
+
return pick(@arr).".get(".r(3).")" if $k <= 2;
|
|
16
|
+
return pick(@st).".f.get(".r(3).")" if $k == 3;
|
|
17
|
+
return pick(@st).".g.get(".r(3).")" if $k == 4;
|
|
18
|
+
return pick(@st).".n" if $k == 5;
|
|
19
|
+
return pick(@st).".total()" if $k == 6;
|
|
20
|
+
return "m.f.get(".r(3).")" if $k == 7;
|
|
21
|
+
return pick(@arr).".length()";
|
|
22
|
+
}
|
|
23
|
+
sub idx { return r(3); }
|
|
24
|
+
my @stmts;
|
|
25
|
+
for my $n (1..(6+r(10))) {
|
|
26
|
+
my $A = pick(@arr); my $B = pick(@arr); my $S = pick(@st); my $T = pick(@st);
|
|
27
|
+
my $k = r(34);
|
|
28
|
+
my %alias = map { $_ => 1 } (2,8,9,10,17,18,19,20,21,22,25,26,4,15,12,1,30);
|
|
29
|
+
while ($alias{$k} && r(100) >= 12) { $k = r(34); }
|
|
30
|
+
my $s;
|
|
31
|
+
if ($k==0) { $s = "$A = $A.with(".idx().", ".iexpr().")"; }
|
|
32
|
+
elsif ($k==1) { $s = "$A = $B.with(".idx().", ".iexpr().")"; }
|
|
33
|
+
elsif ($k==2) { $s = "$A = $B"; }
|
|
34
|
+
elsif ($k==3) { $s = "$A = h1($A)"; }
|
|
35
|
+
elsif ($k==4) { $s = "$A = h1($B)"; }
|
|
36
|
+
elsif ($k==5) { $s = "$S = S(..$S, f: $S.f.with(".idx().", ".iexpr()."))"; }
|
|
37
|
+
elsif ($k==6) { $s = "$S = S(..$S, f: $S.f.with(".idx().", ".iexpr()."), n: ".iexpr().")"; }
|
|
38
|
+
elsif ($k==7) { $s = "$S = S(..$S, g: $S.g.with(".idx().", $S.f.get(".idx()."))), f: $S.f.with(".idx().", 1))"; $s = "$S = S(..$S, g: $S.g.with(".idx().", $S.f.get(".idx().")), f: $S.f.with(".idx().", 1))"; }
|
|
39
|
+
elsif ($k==8) { $s = "$S = $T"; }
|
|
40
|
+
elsif ($k==9) { $s = "$A = $S.f"; }
|
|
41
|
+
elsif ($k==10) { $s = "$S = S(f: $A, g: $B, n: ".r(4).")"; }
|
|
42
|
+
elsif ($k==11) { $s = "$S = S(f: $A, g: Array.filled(3, 0), n: ".r(4).")"; }
|
|
43
|
+
elsif ($k==12) { $s = "$S = $T.put(".idx().", ".iexpr().")"; }
|
|
44
|
+
elsif ($k==13) { $s = "$S = $S.put(".idx().", ".iexpr().")"; }
|
|
45
|
+
elsif ($k==14) { $s = "$S = upd($S, ".idx().")"; }
|
|
46
|
+
elsif ($k==15) { $s = "$S = upd($T, ".idx().")"; }
|
|
47
|
+
elsif ($k==16) { $s = "m.f = m.f.with(".idx().", ".iexpr().")"; }
|
|
48
|
+
elsif ($k==17) { $s = "m.f = $A"; }
|
|
49
|
+
elsif ($k==18) { $s = "$A = m.f"; }
|
|
50
|
+
elsif ($k==19) { $s = "xs.push($A)"; }
|
|
51
|
+
elsif ($k==20) { $s = "ys.push($S)"; }
|
|
52
|
+
elsif ($k==21) { $s = "$A = xs.at(".r(2).")"; }
|
|
53
|
+
elsif ($k==22) { $s = "$S = ys.at(".r(2).")"; }
|
|
54
|
+
elsif ($k==23) { $s = "$A = Array.filled(3, ".r(5).")"; }
|
|
55
|
+
elsif ($k==24) { $s = "$A = $A.resize(3)"; }
|
|
56
|
+
elsif ($k==25) { $s = "$S = $S.view().put(".idx().", ".iexpr().")"; }
|
|
57
|
+
elsif ($k==26) { $s = "$A = $S.fOf()"; }
|
|
58
|
+
elsif ($k==27) { $s = "$S = $S.bump()"; }
|
|
59
|
+
elsif ($k==28) { $s = "$S = S(..$S, f: $S.f.with(".idx().", $S.total()))"; }
|
|
60
|
+
elsif ($k==29) { $s = r(2) ? "$S = S(..$S, n: $S.total(), f: $S.f.with(".idx().", 7))" : "$S = S(..$S, f: $S.f.with(".idx().", 7), n: $S.total())"; }
|
|
61
|
+
elsif ($k==30) { $s = "$S = $S + $T"; }
|
|
62
|
+
elsif ($k==31) { $s = "$S = -$S"; }
|
|
63
|
+
elsif ($k==32) { $s = "$A = pass($A, $B)"; }
|
|
64
|
+
else { $s = "if ".iexpr()." > 1\n $A = $A.with(".idx().", 9)\n else\n $B = $A"; }
|
|
65
|
+
push @stmts, $s;
|
|
66
|
+
}
|
|
67
|
+
my $body = join("\n ", @stmts);
|
|
68
|
+
my $dump = " out := \"\"\n";
|
|
69
|
+
for my $v (@arr) { $dump .= " out = out + show3(\"$v\", $v.get(0), $v.get(1), $v.get(2))\n"; }
|
|
70
|
+
for my $v (@st) { $dump .= " out = out + show3(\"$v.f\", $v.f.get(0), $v.f.get(1), $v.f.get(2)) + show3(\"$v.g\", $v.g.get(0), $v.g.get(1), $v.g.get(2)) + \"$v.n=\" + $v.n.toStr() + \";\"\n"; }
|
|
71
|
+
$dump .= " out = out + show3(\"m.f\", m.f.get(0), m.f.get(1), m.f.get(2))" . join("", map { my $e=$_; " + show3(\"$e\", $e.get(0), $e.get(1), $e.get(2))" } ("xs.at(0)","xs.at(1)","ys.at(0).f","ys.at(1).f","ys.at(0).g")) . "\n assert out == \"\"\n";
|
|
72
|
+
open(my $fh, '>', $out) or die;
|
|
73
|
+
print $fh <<"EOP";
|
|
74
|
+
import std/Int
|
|
75
|
+
import std/Str
|
|
76
|
+
import std/Array
|
|
77
|
+
import std/Slice
|
|
78
|
+
|
|
79
|
+
struct S(f: Array[Int], g: Array[Int], n: Int) =
|
|
80
|
+
fun put(self, i: Int, x: Int) -> S = S(..self, f: self.f.with(i, x))
|
|
81
|
+
fun total(self) -> Int = self.f.get(0) + self.g.get(1) + self.n
|
|
82
|
+
fun view(self) -> S = self
|
|
83
|
+
fun fOf(self) -> Array[Int] = self.f
|
|
84
|
+
fun bump(self) -> S = S(..self, g: self.g.with(2, self.total()), n: self.n + 1)
|
|
85
|
+
fun add(self, o: S) -> S = S(..self, f: self.f.with(0, o.n))
|
|
86
|
+
fun neg(self) -> S = S(..self, g: self.g.with(0, 5))
|
|
87
|
+
|
|
88
|
+
struct M(mut f: Array[Int])
|
|
89
|
+
|
|
90
|
+
fun h1(x: Array[Int]) -> Array[Int] = x.with(0, x.get(1) + 1)
|
|
91
|
+
|
|
92
|
+
fun pass(x: Array[Int], y: Array[Int]) -> Array[Int] = if x.get(0) > 2 then x else y.with(1, 3)
|
|
93
|
+
|
|
94
|
+
fun upd(s: S, i: Int) -> S = S(..s, g: s.g.with(i, s.f.get(i)))
|
|
95
|
+
|
|
96
|
+
fun show3(name: Str, x: Int, y: Int, z: Int) -> Str =
|
|
97
|
+
name + "=" + x.toStr() + "," + y.toStr() + "," + z.toStr() + ";"
|
|
98
|
+
|
|
99
|
+
test "fuzz"
|
|
100
|
+
a := Array.filled(3, 1)
|
|
101
|
+
b := Array.filled(3, 2)
|
|
102
|
+
c := Array.filled(3, 3)
|
|
103
|
+
d := Array.filled(3, 30)
|
|
104
|
+
e := Array.filled(3, 31)
|
|
105
|
+
s := S(f: Array.filled(3, 4), g: Array.filled(3, 5), n: 1)
|
|
106
|
+
t := S(f: Array.filled(3, 6), g: Array.filled(3, 7), n: 2)
|
|
107
|
+
u := S(f: Array.filled(3, 16), g: Array.filled(3, 17), n: 5)
|
|
108
|
+
m := M(f: Array.filled(3, 8))
|
|
109
|
+
xs := Slice[Array[Int]]()
|
|
110
|
+
xs.push(Array.filled(3, 10))
|
|
111
|
+
xs.push(Array.filled(3, 11))
|
|
112
|
+
ys := Slice[S]()
|
|
113
|
+
ys.push(S(f: Array.filled(3, 12), g: Array.filled(3, 13), n: 3))
|
|
114
|
+
ys.push(S(f: Array.filled(3, 14), g: Array.filled(3, 15), n: 4))
|
|
115
|
+
$body
|
|
116
|
+
$dump
|
|
117
|
+
EOP
|
|
118
|
+
close $fh;
|