plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
77038a2
— Peter John
2026-11-01T20:41:40+05:30
T7: pipeline wiring + e2e
- plum-checker/src/lib.rs +73 -6
- plum-examples/unions.plum +118 -0
- plum-wasm-codegen/tests/unions_e2e_test.rs +189 -0
plum-checker/src/lib.rs
CHANGED
|
@@ -732,7 +732,7 @@ fn unifyErr(t1: &PlumType, t2: &PlumType, ctx: &CheckCtx) -> Result<(), String>
|
|
|
732
732
|
}
|
|
733
733
|
Ok(())
|
|
734
734
|
}
|
|
735
|
-
(Some((et, _)), None) => unify(&et, t2, ctx)
|
|
735
|
+
(Some((et, _)), None) => unions::atTop(|| unify(&et, t2, ctx))
|
|
736
736
|
.map_err(|_| format!("type mismatch: expected {}, found {}", t1, t2)),
|
|
737
737
|
_ => Err(format!(
|
|
738
738
|
"type mismatch: expected {}, found {}: {}",
|
|
@@ -1146,7 +1146,7 @@ pub fn typedSource(source: &ast::Source) -> CheckResult<TypedInfo> {
|
|
|
1146
1146
|
|
|
1147
1147
|
fn checkSourceFull(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
1148
1148
|
if !source.formLoops {
|
|
1149
|
-
return
|
|
1149
|
+
return checkUnions(source, |s| checkSourceOnce(s));
|
|
1150
1150
|
}
|
|
1151
1151
|
// `for` loops are lowered by the checker, because how a loop iterates depends
|
|
1152
1152
|
// on the type of its source. Pass 1 checks the program and lowers each loop as
|
|
@@ -1155,12 +1155,63 @@ fn checkSourceFull(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
|
1155
1155
|
// address) and codegen compiles (`TypedInfo::lowered`).
|
|
1156
1156
|
plum_core::forms::begin(source);
|
|
1157
1157
|
let _ = checkSourceOnce(source);
|
|
1158
|
-
let lowered =
|
|
1158
|
+
let lowered = plum_core::forms::finish(source);
|
|
1159
|
-
let (result, mut info) =
|
|
1159
|
+
let (result, mut info) = checkUnions(&lowered, |s| checkSourceOnce(s));
|
|
1160
|
+
if info.lowered.is_none() {
|
|
1161
|
+
// No union rewrite happened: `lowered` itself was checked.
|
|
1160
|
-
|
|
1162
|
+
info.lowered = Some(std::rc::Rc::new(lowered));
|
|
1163
|
+
}
|
|
1161
1164
|
(result, info)
|
|
1162
1165
|
}
|
|
1163
1166
|
|
|
1167
|
+
/// Whether the program declares a union enum (a hidden `Enum__Member` variant).
|
|
1168
|
+
fn hasUnions(source: &ast::Source) -> bool {
|
|
1169
|
+
source.items.iter().any(|i| match i {
|
|
1170
|
+
ast::Item::Enum(e) => e.variants.iter().any(|v| v.name.starts_with(&format!("{}__", e.name))),
|
|
1171
|
+
_ => false,
|
|
1172
|
+
})
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/// Checks `source` and, when the check recorded union uses, rewrites an owned
|
|
1176
|
+
/// copy of exactly the checked program (`plum_core::union_lower::applyPlan`),
|
|
1177
|
+
/// checks that again and returns it as `TypedInfo::lowered`. A program with no
|
|
1178
|
+
/// union enum is checked as is.
|
|
1179
|
+
fn checkUnions(
|
|
1180
|
+
source: &ast::Source,
|
|
1181
|
+
check: impl Fn(&ast::Source) -> (CheckResult<()>, TypedInfo),
|
|
1182
|
+
) -> (CheckResult<()>, TypedInfo) {
|
|
1183
|
+
if !hasUnions(source) {
|
|
1184
|
+
return check(source);
|
|
1185
|
+
}
|
|
1186
|
+
// Addresses are recorded against the program that is checked, so check a
|
|
1187
|
+
// clone and mutate that same clone.
|
|
1188
|
+
let mut copy = source.clone();
|
|
1189
|
+
plum_core::unions::beginPlan();
|
|
1190
|
+
let (result, info) = check(©);
|
|
1191
|
+
let plan = plum_core::unions::takePlan();
|
|
1192
|
+
let empty = plan.wraps.is_empty()
|
|
1193
|
+
&& plan.patterns.is_empty()
|
|
1194
|
+
&& plan.narrowings.is_empty()
|
|
1195
|
+
&& plan.dispatches.is_empty();
|
|
1196
|
+
let out = if result.is_err() {
|
|
1197
|
+
// Report errors against the original, whose addresses codegen never sees.
|
|
1198
|
+
(result, info)
|
|
1199
|
+
} else if empty {
|
|
1200
|
+
check(source)
|
|
1201
|
+
} else {
|
|
1202
|
+
plum_core::union_lower::applyPlan(&mut copy, &plan);
|
|
1203
|
+
plum_core::unions::beginPlan();
|
|
1204
|
+
let (result, mut info) = check(©);
|
|
1205
|
+
plum_core::unions::takePlan();
|
|
1206
|
+
info.lowered = Some(std::rc::Rc::new(copy));
|
|
1207
|
+
(result, info)
|
|
1208
|
+
};
|
|
1209
|
+
// Leave the plan of the checked program in the recorder, so a caller that
|
|
1210
|
+
// began one can inspect what the check found.
|
|
1211
|
+
unions::restorePlan(&plan);
|
|
1212
|
+
out
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1164
1215
|
fn checkSourceOnce(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
1165
1216
|
RECORD.with(|r| *r.borrow_mut() = Some(HashMap::new()));
|
|
1166
1217
|
RECORD_BINDS.with(|r| *r.borrow_mut() = Some(Binds::new()));
|
|
@@ -2460,6 +2511,8 @@ struct CoverCtor {
|
|
|
2460
2511
|
fields: Vec<PlumType>,
|
|
2461
2512
|
/// A union member: covered by `Name` or `Name(..)` with irrefutable sub-patterns.
|
|
2462
2513
|
member: bool,
|
|
2514
|
+
/// The hidden variant's full name (`Shape__Circle`), which lowered patterns use.
|
|
2515
|
+
full: String,
|
|
2463
2516
|
}
|
|
2464
2517
|
|
|
2465
2518
|
static COVER_WILDCARD: ast::CasePattern = ast::CasePattern::Wildcard;
|
|
@@ -2496,6 +2549,7 @@ fn coverCtors(ty: &PlumType, ctx: &CheckCtx) -> Option<(String, Vec<CoverCtor>)>
|
|
|
2496
2549
|
name: n.to_string(),
|
|
2497
2550
|
fields: Vec::new(),
|
|
2498
2551
|
member: false,
|
|
2552
|
+
full: n.to_string(),
|
|
2499
2553
|
})
|
|
2500
2554
|
.collect(),
|
|
2501
2555
|
)),
|
|
@@ -2507,6 +2561,7 @@ fn coverCtors(ty: &PlumType, ctx: &CheckCtx) -> Option<(String, Vec<CoverCtor>)>
|
|
|
2507
2561
|
name: n.clone(),
|
|
2508
2562
|
fields: Vec::new(),
|
|
2509
2563
|
member: false,
|
|
2564
|
+
full: n.clone(),
|
|
2510
2565
|
})
|
|
2511
2566
|
.collect(),
|
|
2512
2567
|
)),
|
|
@@ -2524,9 +2579,10 @@ fn coverCtors(ty: &PlumType, ctx: &CheckCtx) -> Option<(String, Vec<CoverCtor>)>
|
|
|
2524
2579
|
enum_name.to_string(),
|
|
2525
2580
|
vs.into_iter()
|
|
2526
2581
|
.map(|(k, i)| match unions::memberBase(k, i) {
|
|
2527
|
-
Some(base) => CoverCtor { name: base, fields:
|
|
2582
|
+
Some(base) => CoverCtor { name: base, fields: variantFields(i, ty), member: true, full: k.to_string() },
|
|
2528
2583
|
None => CoverCtor {
|
|
2529
2584
|
member: false,
|
|
2585
|
+
full: k.to_string(),
|
|
2530
2586
|
name: k.to_string(),
|
|
2531
2587
|
// A discriminant enum's field types are its shared
|
|
2532
2588
|
// params, which patterns never destructure.
|
|
@@ -2584,6 +2640,16 @@ fn coverRows<'a>(
|
|
|
2584
2640
|
if patCatchAll(r[0]) {
|
|
2585
2641
|
row.extend(std::iter::repeat(&COVER_WILDCARD).take(c.fields.len()));
|
|
2586
2642
|
} else if let Some((n, fs)) = patCtor(r[0]) {
|
|
2643
|
+
if c.member && n == c.full {
|
|
2644
|
+
// A lowered member pattern `Shape__Circle(inner)`.
|
|
2645
|
+
if fs.len() != 1 {
|
|
2646
|
+
continue;
|
|
2647
|
+
}
|
|
2648
|
+
row.extend(fs.iter());
|
|
2649
|
+
row.extend(r[1..].iter().copied());
|
|
2650
|
+
sub.push(row);
|
|
2651
|
+
continue;
|
|
2652
|
+
}
|
|
2587
2653
|
if n != c.name {
|
|
2588
2654
|
continue;
|
|
2589
2655
|
}
|
|
@@ -2591,6 +2657,7 @@ fn coverRows<'a>(
|
|
|
2591
2657
|
if !fs.iter().all(patCatchAll) {
|
|
2592
2658
|
continue;
|
|
2593
2659
|
}
|
|
2660
|
+
row.extend(std::iter::repeat(&COVER_WILDCARD).take(c.fields.len()));
|
|
2594
2661
|
} else if let ast::CasePattern::Name(_) = r[0] {
|
|
2595
2662
|
// Bare `Circle` against a payload variant: tag check only.
|
|
2596
2663
|
row.extend(std::iter::repeat(&COVER_WILDCARD).take(c.fields.len()));
|
plum-examples/unions.plum
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Union enums: `enum Shape = Circle | Rectangle` lists existing types as members.
|
|
2
|
+
# A member converts to the enum where one is expected, a bare member name in a
|
|
3
|
+
# pattern narrows the subject, and a method every member defines is callable on
|
|
4
|
+
# the enum (auto-dispatch). Members can be structs, records, primitives
|
|
5
|
+
# (`Int | Float | Str`), generic instances (`List[Jv]`) or bare tags.
|
|
6
|
+
module unions
|
|
7
|
+
import std/Str
|
|
8
|
+
import std/Int
|
|
9
|
+
import std/Float
|
|
10
|
+
import std/List
|
|
11
|
+
|
|
12
|
+
struct Circle(r: Float) =
|
|
13
|
+
fun area(self) -> Float = self.r * self.r * 3.0
|
|
14
|
+
fun toStr(self) -> Str = "circle"
|
|
15
|
+
|
|
16
|
+
record Rectangle(w: Float, h: Float) =
|
|
17
|
+
fun area(self) -> Float = self.w * self.h
|
|
18
|
+
fun toStr(self) -> Str = "rect"
|
|
19
|
+
|
|
20
|
+
enum Shape = Circle | Rectangle
|
|
21
|
+
|
|
22
|
+
enum Round = Circle | Oval
|
|
23
|
+
|
|
24
|
+
record Oval(a: Float)
|
|
25
|
+
|
|
26
|
+
enum Tree[T] = Leaf | Node[T]
|
|
27
|
+
|
|
28
|
+
record Leaf(v: Int)
|
|
29
|
+
|
|
30
|
+
record Node[T](left: Tree[T], right: Tree[T])
|
|
31
|
+
|
|
32
|
+
enum Jv = JvNull | Int | Float | Str | List[Jv]
|
|
33
|
+
|
|
34
|
+
fun total(t: Tree[Int]) -> Int =
|
|
35
|
+
match t
|
|
36
|
+
Leaf(v) => v
|
|
37
|
+
Node(l, r) => total(l) + total(r)
|
|
38
|
+
|
|
39
|
+
fun leaf(v: Int) -> Tree[Int] = Leaf(v: v)
|
|
40
|
+
|
|
41
|
+
fun node(l: Tree[Int], r: Tree[Int]) -> Tree[Int] = Node[Int](left: l, right: r)
|
|
42
|
+
|
|
43
|
+
fun describe(j: Jv) -> Str =
|
|
44
|
+
match j
|
|
45
|
+
JvNull => "null"
|
|
46
|
+
Int => "int"
|
|
47
|
+
Float => "float"
|
|
48
|
+
Str => "str {j}"
|
|
49
|
+
List => "list"
|
|
50
|
+
|
|
51
|
+
fun areaOf(s: Shape) -> Float = s.area()
|
|
52
|
+
|
|
53
|
+
fun roundName(r: Round) -> Str =
|
|
54
|
+
match r
|
|
55
|
+
Circle => "circle"
|
|
56
|
+
Oval => "oval"
|
|
57
|
+
|
|
58
|
+
fun make(big: Bool) -> Shape =
|
|
59
|
+
if big
|
|
60
|
+
Rectangle(w: 10.0, h: 10.0)
|
|
61
|
+
else
|
|
62
|
+
Circle(r: 1.0)
|
|
63
|
+
|
|
64
|
+
test "struct_member_copy"
|
|
65
|
+
s := make(false)
|
|
66
|
+
if Circle := s
|
|
67
|
+
c := s
|
|
68
|
+
c.r = 5.0
|
|
69
|
+
assert c.r == 5.0
|
|
70
|
+
assert s.area() == 3.0
|
|
71
|
+
else
|
|
72
|
+
assert false
|
|
73
|
+
|
|
74
|
+
test "list_of_shapes"
|
|
75
|
+
xs := List[Shape]()
|
|
76
|
+
xs.push(Circle(r: 1.0))
|
|
77
|
+
xs.push(Rectangle(w: 2.0, h: 3.0))
|
|
78
|
+
sum := 0.0
|
|
79
|
+
for x := range xs
|
|
80
|
+
sum = sum + x.area()
|
|
81
|
+
assert sum == 9.0
|
|
82
|
+
|
|
83
|
+
test "push_converts"
|
|
84
|
+
xs := List[Shape]()
|
|
85
|
+
xs.push(Circle(r: 2.0))
|
|
86
|
+
xs.push(Rectangle(w: 1.0, h: 1.0))
|
|
87
|
+
assert xs.length() == 2
|
|
88
|
+
|
|
89
|
+
test "tree_generic"
|
|
90
|
+
assert total(node(leaf(1), node(leaf(2), leaf(3)))) == 6
|
|
91
|
+
|
|
92
|
+
test "json_like_primitives"
|
|
93
|
+
assert describe(JvNull) == "null"
|
|
94
|
+
assert describe(7) == "int"
|
|
95
|
+
assert describe(1.5) == "float"
|
|
96
|
+
assert describe("hi") == "str hi"
|
|
97
|
+
js := List[Jv]()
|
|
98
|
+
js.push(1.0)
|
|
99
|
+
js.push(7)
|
|
100
|
+
assert describe(js) == "list"
|
|
101
|
+
|
|
102
|
+
test "two_enums_one_type"
|
|
103
|
+
c := Circle(r: 2.0)
|
|
104
|
+
assert areaOf(c) == 12.0
|
|
105
|
+
assert roundName(c) == "circle"
|
|
106
|
+
assert roundName(Oval(a: 1.0)) == "oval"
|
|
107
|
+
|
|
108
|
+
test "if_narrow"
|
|
109
|
+
s := make(true)
|
|
110
|
+
if Circle := s
|
|
111
|
+
assert false
|
|
112
|
+
else if Rectangle := s
|
|
113
|
+
assert s.w == 10.0
|
|
114
|
+
|
|
115
|
+
test "auto_dispatch"
|
|
116
|
+
assert make(true).area() == 100.0
|
|
117
|
+
assert make(false).area() == 3.0
|
|
118
|
+
assert "{make(false)}" == "circle"
|
plum-wasm-codegen/tests/unions_e2e_test.rs
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
use plum_wasm_codegen::compileSource;
|
|
3
|
+
|
|
4
|
+
fn gcEngine() -> wasmtime::Engine {
|
|
5
|
+
let mut config = wasmtime::Config::new();
|
|
6
|
+
config.wasm_gc(true);
|
|
7
|
+
config.wasm_function_references(true);
|
|
8
|
+
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
fn repoRoot() -> std::path::PathBuf {
|
|
12
|
+
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fn load(name: &str, src: &str) -> Result<plum_core::ast::Source, String> {
|
|
16
|
+
let dir = std::env::temp_dir().join(format!("plum_unions_{}_{}", name, std::process::id()));
|
|
17
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
18
|
+
let path = dir.join("main.plum");
|
|
19
|
+
std::fs::write(&path, src).unwrap();
|
|
20
|
+
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
21
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
22
|
+
r.map_err(|e| format!("{e}"))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Compiles `src` through the real pipeline and runs its `main() -> Int`.
|
|
26
|
+
fn runInt(name: &str, src: &str) -> i64 {
|
|
27
|
+
let source = load(name, src).expect("load");
|
|
28
|
+
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
29
|
+
wasmparser::validate(&bytes).expect("valid wasm");
|
|
30
|
+
let engine = gcEngine();
|
|
31
|
+
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
32
|
+
let mut store = wasmtime::Store::new(&engine, ());
|
|
33
|
+
let mut linker = wasmtime::Linker::new(&engine);
|
|
34
|
+
linker.define_unknown_imports_as_default_values(&mut store, &module).unwrap();
|
|
35
|
+
let instance = linker.instantiate(&mut store, &module).unwrap();
|
|
36
|
+
let main = instance.get_typed_func::<(), i64>(&mut store, "main").unwrap();
|
|
37
|
+
main.call(&mut store, ()).unwrap()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fn checkErrors(name: &str, src: &str) -> Vec<String> {
|
|
41
|
+
let source = load(name, src).expect("load");
|
|
42
|
+
match plum_checker::checkSource(&source) {
|
|
43
|
+
Ok(()) => Vec::new(),
|
|
44
|
+
Err(e) => e.into_iter().map(|x| x.message).collect(),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const DEFS: &str = "\
|
|
49
|
+
import std/Str
|
|
50
|
+
import std/Int
|
|
51
|
+
import std/Float
|
|
52
|
+
import std/List
|
|
53
|
+
|
|
54
|
+
struct Circle(r: Float) =
|
|
55
|
+
fun area(self) -> Float = self.r * self.r * 3.0
|
|
56
|
+
|
|
57
|
+
record Rectangle(w: Float, h: Float) =
|
|
58
|
+
fun area(self) -> Float = self.w * self.h
|
|
59
|
+
|
|
60
|
+
enum Shape = Circle | Rectangle
|
|
61
|
+
|
|
62
|
+
";
|
|
63
|
+
|
|
64
|
+
fn withDefs(body: &str) -> String {
|
|
65
|
+
format!("{DEFS}{body}")
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[test]
|
|
69
|
+
fn list_of_shapes() {
|
|
70
|
+
let src = withDefs(
|
|
71
|
+
"\
|
|
72
|
+
fun main() -> Int =
|
|
73
|
+
xs := List[Shape]()
|
|
74
|
+
xs.push(Circle(r: 1.0))
|
|
75
|
+
xs.push(Rectangle(w: 2.0, h: 3.0))
|
|
76
|
+
sum := 0.0
|
|
77
|
+
for x := range xs
|
|
78
|
+
sum = sum + x.area()
|
|
79
|
+
Int(sum)
|
|
80
|
+
",
|
|
81
|
+
);
|
|
82
|
+
assert_eq!(runInt("list", &src), 9);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#[test]
|
|
86
|
+
fn struct_member_copy() {
|
|
87
|
+
let src = withDefs(
|
|
88
|
+
"\
|
|
89
|
+
fun make() -> Shape = Circle(r: 1.0)
|
|
90
|
+
|
|
91
|
+
fun main() -> Int =
|
|
92
|
+
s := make()
|
|
93
|
+
r := 0.0
|
|
94
|
+
if Circle := s
|
|
95
|
+
c := s
|
|
96
|
+
c.r = 5.0
|
|
97
|
+
r = c.r + s.r
|
|
98
|
+
Int(r)
|
|
99
|
+
",
|
|
100
|
+
);
|
|
101
|
+
assert_eq!(runInt("copy", &src), 6);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#[test]
|
|
105
|
+
fn narrow_and_dispatch() {
|
|
106
|
+
let src = withDefs(
|
|
107
|
+
"\
|
|
108
|
+
fun pick(big: Bool) -> Shape =
|
|
109
|
+
if big
|
|
110
|
+
Rectangle(w: 10.0, h: 10.0)
|
|
111
|
+
else
|
|
112
|
+
Circle(r: 1.0)
|
|
113
|
+
|
|
114
|
+
fun wide(s: Shape) -> Float =
|
|
115
|
+
match s
|
|
116
|
+
Circle => 0.0
|
|
117
|
+
Rectangle => s.w
|
|
118
|
+
|
|
119
|
+
fun main() -> Int =
|
|
120
|
+
a := pick(true)
|
|
121
|
+
b := pick(false)
|
|
122
|
+
Int(a.area() + b.area() + wide(a))
|
|
123
|
+
",
|
|
124
|
+
);
|
|
125
|
+
assert_eq!(runInt("narrow", &src), 113);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#[test]
|
|
129
|
+
fn member_into_optional_and_error_unions() {
|
|
130
|
+
let src = withDefs(
|
|
131
|
+
"\
|
|
132
|
+
fun opt() -> Shape? =
|
|
133
|
+
Circle(r: 2.0)
|
|
134
|
+
|
|
135
|
+
fun bang() -> !Shape =
|
|
136
|
+
Rectangle(w: 2.0, h: 2.0)
|
|
137
|
+
|
|
138
|
+
fun typed() -> ParseError!Shape =
|
|
139
|
+
Circle(r: 1.0)
|
|
140
|
+
|
|
141
|
+
fun main() -> Int =
|
|
142
|
+
a := opt() ?: Rectangle(w: 0.0, h: 0.0)
|
|
143
|
+
b := bang() catch Rectangle(w: 0.0, h: 0.0)
|
|
144
|
+
c := typed() catch Rectangle(w: 0.0, h: 0.0)
|
|
145
|
+
Int(a.area() + b.area() + c.area())
|
|
146
|
+
",
|
|
147
|
+
);
|
|
148
|
+
let src = src.replace("import std/List\n", "import std/List\n\nerror ParseError = Bad | Worse\n");
|
|
149
|
+
assert_eq!(runInt("optional", &src), 19);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[test]
|
|
153
|
+
fn invariant_list_rejected() {
|
|
154
|
+
let src = withDefs(
|
|
155
|
+
"\
|
|
156
|
+
fun take(xs: List[Shape]) -> Int =
|
|
157
|
+
1
|
|
158
|
+
|
|
159
|
+
fun go(cs: List[Circle]) -> Int =
|
|
160
|
+
take(cs)
|
|
161
|
+
",
|
|
162
|
+
);
|
|
163
|
+
assert!(!checkErrors("invariant", &src).is_empty());
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
#[test]
|
|
167
|
+
fn variadic_dispatch_is_an_error() {
|
|
168
|
+
let src = "\
|
|
169
|
+
import std/Str
|
|
170
|
+
import std/Int
|
|
171
|
+
import std/Float
|
|
172
|
+
|
|
173
|
+
record Pa(v: Int) =
|
|
174
|
+
fun sum(self, xs: ...Int) -> Int = self.v
|
|
175
|
+
|
|
176
|
+
record Pb(v: Int) =
|
|
177
|
+
fun sum(self, xs: ...Int) -> Int = self.v
|
|
178
|
+
|
|
179
|
+
enum Pair = Pa | Pb
|
|
180
|
+
|
|
181
|
+
fun go(p: Pair) -> Int =
|
|
182
|
+
p.sum(1, 2)
|
|
183
|
+
";
|
|
184
|
+
let errs = checkErrors("variadic", src);
|
|
185
|
+
assert!(
|
|
186
|
+
errs.iter().any(|e| e.contains("Pair.sum: auto-dispatch can't forward a variadic parameter; define sum on Pair")),
|
|
187
|
+
"{errs:?}"
|
|
188
|
+
);
|
|
189
|
+
}
|