plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
9c5f64a
— Peter John
2026-10-25T16:00:23+05:30
feat(checker): structural tag-union types and Roc-style ? checking
- plum-checker/src/lib.rs +310 -26
- plum-checker/src/monomorphize.rs +4 -1
- plum-checker/src/types.rs +14 -0
- plum-checker/tests/checker_tests.rs +328 -1
- plum-wasm-codegen/src/lib.rs +2 -2
plum-checker/src/lib.rs
CHANGED
|
@@ -21,10 +21,125 @@ pub fn plumTypeFromName(name: &str) -> PlumType {
|
|
|
21
21
|
"Byte" => PlumType::TByte,
|
|
22
22
|
"[]Byte" => PlumType::TByteSlice,
|
|
23
23
|
"Unit" => PlumType::TUnit,
|
|
24
|
+
other if other.starts_with('[') && !other.starts_with("[]") && other.ends_with(']') => {
|
|
25
|
+
parseTagUnion(other).unwrap_or_else(|| PlumType::TNamed(other.to_string()))
|
|
26
|
+
}
|
|
24
27
|
other => PlumType::TNamed(other.to_string()),
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
/// Splits `s` on `sep` occurring at bracket/paren depth 0 (so a nested tag
|
|
32
|
+
/// union or payload never splits its parent). `[]` (slice prefix) is neutral.
|
|
33
|
+
fn splitTopLevel(s: &str, sep: char) -> Vec<&str> {
|
|
34
|
+
let mut parts = Vec::new();
|
|
35
|
+
let mut depth = 0i32;
|
|
36
|
+
let mut start = 0;
|
|
37
|
+
let bytes: Vec<(usize, char)> = s.char_indices().collect();
|
|
38
|
+
let mut i = 0;
|
|
39
|
+
while i < bytes.len() {
|
|
40
|
+
let (idx, c) = bytes[i];
|
|
41
|
+
match c {
|
|
42
|
+
'[' if bytes.get(i + 1).map(|b| b.1) == Some(']') => i += 1,
|
|
43
|
+
'[' | '(' => depth += 1,
|
|
44
|
+
']' | ')' => depth -= 1,
|
|
45
|
+
c if c == sep && depth == 0 => {
|
|
46
|
+
parts.push(&s[start..idx]);
|
|
47
|
+
start = idx + c.len_utf8();
|
|
48
|
+
}
|
|
49
|
+
_ => {}
|
|
50
|
+
}
|
|
51
|
+
i += 1;
|
|
52
|
+
}
|
|
53
|
+
parts.push(&s[start..]);
|
|
54
|
+
parts
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Parses a canonical tag-union string (`[A,B(Str)]`) into `PlumType::TTags`.
|
|
58
|
+
fn parseTagUnion(s: &str) -> Option<PlumType> {
|
|
59
|
+
let inner = s.strip_prefix('[')?.strip_suffix(']')?;
|
|
60
|
+
let mut tags = BTreeMap::new();
|
|
61
|
+
if inner.is_empty() {
|
|
62
|
+
return Some(PlumType::TTags(tags));
|
|
63
|
+
}
|
|
64
|
+
for seg in splitTopLevel(inner, ',') {
|
|
65
|
+
let seg = seg.trim();
|
|
66
|
+
match seg.find('(') {
|
|
67
|
+
Some(p) => {
|
|
68
|
+
let name = seg[..p].trim().to_string();
|
|
69
|
+
let payload = seg[p + 1..].strip_suffix(')')?;
|
|
70
|
+
tags.insert(name, Some(Box::new(plumTypeFromName(payload.trim()))));
|
|
71
|
+
}
|
|
72
|
+
None => {
|
|
73
|
+
tags.insert(seg.to_string(), None);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
Some(PlumType::TTags(tags))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Replaces every tag-union segment (`[...]`, but not the `[]Byte` slice
|
|
81
|
+
/// spelling) of a mangled name with `Tags`, so specializations that differ
|
|
82
|
+
/// only in their tag unions share one runtime type:
|
|
83
|
+
/// `erasedEnumName("Try$Int$[A,B(Str)]") == "Try$Int$Tags"`.
|
|
84
|
+
pub fn erasedEnumName(name: &str) -> String {
|
|
85
|
+
let chars: Vec<char> = name.chars().collect();
|
|
86
|
+
let mut out = String::new();
|
|
87
|
+
let mut i = 0;
|
|
88
|
+
while i < chars.len() {
|
|
89
|
+
if chars[i] == '[' && chars.get(i + 1) != Some(&']') {
|
|
90
|
+
let mut depth = 0;
|
|
91
|
+
while i < chars.len() {
|
|
92
|
+
match chars[i] {
|
|
93
|
+
'[' => depth += 1,
|
|
94
|
+
']' => depth -= 1,
|
|
95
|
+
_ => {}
|
|
96
|
+
}
|
|
97
|
+
i += 1;
|
|
98
|
+
if depth == 0 {
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
out.push_str("Tags");
|
|
103
|
+
} else {
|
|
104
|
+
out.push(chars[i]);
|
|
105
|
+
i += 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
out
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// True when two mangled specialization names (`Try$Int$[A,B]`) are identical
|
|
112
|
+
/// except for their tag-union segments and every `actual` union is a subset of
|
|
113
|
+
/// the matching `expected` one (E is covariant).
|
|
114
|
+
pub fn tagUnionSegmentsCompatible(expected: &str, actual: &str, ctx: &CheckCtx) -> bool {
|
|
115
|
+
let e_segs = splitTopLevel(expected, '$');
|
|
116
|
+
let a_segs = splitTopLevel(actual, '$');
|
|
117
|
+
if e_segs.len() != a_segs.len() || e_segs.len() < 2 {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
let mut saw_union = false;
|
|
121
|
+
for (e, a) in e_segs.iter().zip(a_segs.iter()) {
|
|
122
|
+
if e == a {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
let both_unions = e.starts_with('[')
|
|
126
|
+
&& !e.starts_with("[]")
|
|
127
|
+
&& a.starts_with('[')
|
|
128
|
+
&& !a.starts_with("[]");
|
|
129
|
+
if !both_unions {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
saw_union = true;
|
|
133
|
+
if unify(&plumTypeFromName(e), &plumTypeFromName(a), ctx).is_err() {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
saw_union
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Reserved `TypeEnv` key holding the enclosing function's declared return type.
|
|
141
|
+
const RETURN_KEY: &str = "$return";
|
|
142
|
+
|
|
28
143
|
pub fn plumTypeFromAst(ty: &ast::Type) -> PlumType {
|
|
29
144
|
plumTypeFromName(&ty.name)
|
|
30
145
|
}
|
|
@@ -82,6 +197,35 @@ pub fn unify(t1: &PlumType, t2: &PlumType, ctx: &CheckCtx) -> Result<(), String>
|
|
|
82
197
|
(PlumType::TByteSlice, PlumType::TByteSlice) => Ok(()),
|
|
83
198
|
(PlumType::TUnit, PlumType::TUnit) => Ok(()),
|
|
84
199
|
(PlumType::TNamed(a), PlumType::TNamed(b)) if a == b => Ok(()),
|
|
200
|
+
// Tag-union subtyping: every tag of `actual` must exist in `expected`
|
|
201
|
+
// with a unifying payload.
|
|
202
|
+
(PlumType::TTags(exp), PlumType::TTags(act)) => {
|
|
203
|
+
for (tag, act_payload) in act {
|
|
204
|
+
match (exp.get(tag), act_payload) {
|
|
205
|
+
(None, _) => {
|
|
206
|
+
return Err(format!(
|
|
207
|
+
"type mismatch: expected {}, found {} (tag '{}' is not allowed)",
|
|
208
|
+
t1, t2, tag
|
|
209
|
+
))
|
|
210
|
+
}
|
|
211
|
+
(Some(None), None) => {}
|
|
212
|
+
(Some(Some(ep)), Some(ap)) => unify(ep, ap, ctx)?,
|
|
213
|
+
_ => {
|
|
214
|
+
return Err(format!(
|
|
215
|
+
"type mismatch: expected {}, found {} (tag '{}' payload differs)",
|
|
216
|
+
t1, t2, tag
|
|
217
|
+
))
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
Ok(())
|
|
222
|
+
}
|
|
223
|
+
// Two specializations differing only in their tag-union segments.
|
|
224
|
+
(PlumType::TNamed(a), PlumType::TNamed(b))
|
|
225
|
+
if a != b && tagUnionSegmentsCompatible(a, b, ctx) =>
|
|
226
|
+
{
|
|
227
|
+
Ok(())
|
|
228
|
+
}
|
|
85
229
|
// A bare enum variant name (`Red`) can be used directly as a type — e.g.
|
|
86
230
|
// a fn param `v: Red` — meaning "a `Color` value that is specifically
|
|
87
231
|
// the `Red` variant". A variant's OWN constructions/references always
|
|
@@ -705,6 +849,8 @@ fn checkFn(f: &ast::Fn, global_env: &TypeEnv, ctx: &CheckCtx) -> Vec<CheckError>
|
|
|
705
849
|
.as_ref()
|
|
706
850
|
.map(plumTypeFromAst)
|
|
707
851
|
.unwrap_or(PlumType::TUnit);
|
|
852
|
+
// Reserved env key read by `?` (see `inferExpr`'s `Try` arm); closures remove it.
|
|
853
|
+
env.insert(RETURN_KEY.to_string(), TypeScheme::mono(declared_ret.clone()));
|
|
708
854
|
|
|
709
855
|
match &f.body {
|
|
710
856
|
ast::FnBody::Expr(e) => match inferExpr(e, &env, ctx) {
|
|
@@ -1054,8 +1200,22 @@ fn checkStmt(
|
|
|
1054
1200
|
errors.append(&mut checkMatch(m, env, declared_ret, fn_name, fn_pos, ctx));
|
|
1055
1201
|
}
|
|
1056
1202
|
ast::Stmt::Break | ast::Stmt::Continue | ast::Stmt::Todo => {}
|
|
1057
|
-
// Real semantics (Str check) land in T2.
|
|
1058
|
-
ast::Stmt::Crash(
|
|
1203
|
+
ast::Stmt::Crash(e) => match inferExpr(e, env, ctx) {
|
|
1204
|
+
Ok(t) => {
|
|
1205
|
+
if let Err(msg) = unify(&PlumType::TStr, &t, ctx) {
|
|
1206
|
+
errors.push(CheckError::at(
|
|
1207
|
+
fn_pos.0,
|
|
1208
|
+
fn_pos.1,
|
|
1209
|
+
format!("fn '{}': crash message must be Str: {}", fn_name, msg),
|
|
1210
|
+
));
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
Err(msg) => errors.push(CheckError::at(
|
|
1214
|
+
fn_pos.0,
|
|
1215
|
+
fn_pos.1,
|
|
1216
|
+
format!("fn '{}': crash: {}", fn_name, msg),
|
|
1217
|
+
)),
|
|
1218
|
+
},
|
|
1059
1219
|
}
|
|
1060
1220
|
errors
|
|
1061
1221
|
}
|
|
@@ -1085,6 +1245,50 @@ fn checkMatch(
|
|
|
1085
1245
|
return errors; // a subject failed to type — cases can't be checked meaningfully
|
|
1086
1246
|
}
|
|
1087
1247
|
|
|
1248
|
+
// A match over a tag union without a catch-all must cover every tag.
|
|
1249
|
+
if let [PlumType::TTags(tags)] = subject_types.as_slice() {
|
|
1250
|
+
let mut covered: std::collections::BTreeSet<&str> = Default::default();
|
|
1251
|
+
let mut catch_all = false;
|
|
1252
|
+
for case in &m.cases {
|
|
1253
|
+
if case.guard.is_some() {
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
match case.patterns.first() {
|
|
1257
|
+
Some(ast::CasePattern::Wildcard) => catch_all = true,
|
|
1258
|
+
Some(ast::CasePattern::Name(n)) => {
|
|
1259
|
+
if tags.contains_key(n) {
|
|
1260
|
+
covered.insert(n.as_str());
|
|
1261
|
+
} else {
|
|
1262
|
+
catch_all = true; // a binding
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
Some(ast::CasePattern::Class { name, .. }) => {
|
|
1266
|
+
covered.insert(name.as_str());
|
|
1267
|
+
}
|
|
1268
|
+
_ => {}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
if !catch_all {
|
|
1272
|
+
let missing: Vec<&str> = tags
|
|
1273
|
+
.keys()
|
|
1274
|
+
.map(|k| k.as_str())
|
|
1275
|
+
.filter(|k| !covered.contains(k))
|
|
1276
|
+
.collect();
|
|
1277
|
+
if !missing.is_empty() {
|
|
1278
|
+
errors.push(CheckError::at(
|
|
1279
|
+
fn_pos.0,
|
|
1280
|
+
fn_pos.1,
|
|
1281
|
+
format!(
|
|
1282
|
+
"fn '{}': non-exhaustive match on {}: missing {}",
|
|
1283
|
+
fn_name,
|
|
1284
|
+
PlumType::TTags(tags.clone()),
|
|
1285
|
+
missing.join(", ")
|
|
1286
|
+
),
|
|
1287
|
+
));
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1088
1292
|
for case in &m.cases {
|
|
1089
1293
|
let mut case_env = env.clone();
|
|
1090
1294
|
if case.patterns.len() == subject_types.len() {
|
|
@@ -1281,6 +1485,29 @@ fn checkPattern(
|
|
|
1281
1485
|
env: &mut TypeEnv,
|
|
1282
1486
|
ctx: &CheckCtx,
|
|
1283
1487
|
) -> Result<(), String> {
|
|
1488
|
+
if let PlumType::TTags(tags) = subject_ty {
|
|
1489
|
+
let is_upper = |n: &str| n.chars().next().is_some_and(|c| c.is_uppercase());
|
|
1490
|
+
match pat {
|
|
1491
|
+
ast::CasePattern::Name(n) if is_upper(n) => {
|
|
1492
|
+
return match tags.get(n) {
|
|
1493
|
+
Some(None) => Ok(()),
|
|
1494
|
+
Some(Some(_)) => Err(format!("tag '{}' carries a payload; write '{}(..)'", n, n)),
|
|
1495
|
+
None => Err(format!("tag '{}' is not part of {}", n, subject_ty)),
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
ast::CasePattern::Class { name, fields } if is_upper(name) => {
|
|
1499
|
+
return match tags.get(name) {
|
|
1500
|
+
Some(Some(payload)) if fields.len() == 1 => {
|
|
1501
|
+
checkPattern(&fields[0], payload, env, ctx)
|
|
1502
|
+
}
|
|
1503
|
+
Some(Some(_)) => Err(format!("tag '{}' has exactly one payload", name)),
|
|
1504
|
+
Some(None) => Err(format!("tag '{}' has no payload", name)),
|
|
1505
|
+
None => Err(format!("tag '{}' is not part of {}", name, subject_ty)),
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
_ => {}
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1284
1511
|
match pat {
|
|
1285
1512
|
ast::CasePattern::Wildcard => Ok(()),
|
|
1286
1513
|
ast::CasePattern::Int(_) => unify(subject_ty, &PlumType::TInt, ctx),
|
|
@@ -1496,7 +1723,7 @@ fn variantBaseName(name: &str) -> &str {
|
|
|
1496
1723
|
name.split('$').next().unwrap_or(name)
|
|
1497
1724
|
}
|
|
1498
1725
|
|
|
1499
|
-
/// Finds the "success" (`Ok`
|
|
1726
|
+
/// Finds the "success" (`Ok`) and "failure" (`Err`) variants
|
|
1500
1727
|
/// belonging to the same enum as `enum_name`, for the `?` operator (`Expr::Try`)
|
|
1501
1728
|
/// — used identically by the checker (`inferExpr`) and codegen (`inferLocalType`
|
|
1502
1729
|
/// callers), which is why this lives here rather than inline in either. Returns
|
|
@@ -1513,8 +1740,8 @@ pub fn tryOperatorVariants<'a>(
|
|
|
1513
1740
|
continue;
|
|
1514
1741
|
}
|
|
1515
1742
|
match variantBaseName(vname) {
|
|
1516
|
-
"Ok"
|
|
1743
|
+
"Ok" => success = Some((vname.as_str(), info)),
|
|
1517
|
-
"Err"
|
|
1744
|
+
"Err" => failure = Some((vname.as_str(), info)),
|
|
1518
1745
|
_ => {}
|
|
1519
1746
|
}
|
|
1520
1747
|
}
|
|
@@ -1524,6 +1751,19 @@ pub fn tryOperatorVariants<'a>(
|
|
|
1524
1751
|
}
|
|
1525
1752
|
}
|
|
1526
1753
|
|
|
1754
|
+
/// True if `n` is an uppercase name that is not a declared type, variant, method
|
|
1755
|
+
/// receiver, builtin type or const — i.e. a structural tag literal.
|
|
1756
|
+
fn isTagLiteralName(n: &str, ctx: &CheckCtx) -> bool {
|
|
1757
|
+
n.chars().next().is_some_and(|c| c.is_uppercase())
|
|
1758
|
+
&& !matches!(
|
|
1759
|
+
n,
|
|
1760
|
+
"Int" | "Float" | "Bool" | "Str" | "Byte" | "Unit" | "Self"
|
|
1761
|
+
)
|
|
1762
|
+
&& !ctx.enum_variants.contains_key(n)
|
|
1763
|
+
&& !ctx.enum_variants.values().any(|i| i.enum_name == n)
|
|
1764
|
+
&& !ctx.methods.keys().any(|(recv, _)| recv == n)
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1527
1767
|
pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<PlumType, String> {
|
|
1528
1768
|
match expr {
|
|
1529
1769
|
ast::Expr::Int(_) => Ok(PlumType::TInt),
|
|
@@ -1549,6 +1789,9 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
1549
1789
|
// keeps that working without patching every such site.
|
|
1550
1790
|
Some(info) if info.enum_name == "Bool" => Ok(PlumType::TBool),
|
|
1551
1791
|
Some(info) => Ok(PlumType::TNamed(info.enum_name.clone())),
|
|
1792
|
+
None if isTagLiteralName(n, ctx) => {
|
|
1793
|
+
Ok(PlumType::TTags(BTreeMap::from([(n.clone(), None)])))
|
|
1794
|
+
}
|
|
1552
1795
|
// Unmodeled/builtin type name: allow, codegen will catch.
|
|
1553
1796
|
None => Ok(PlumType::TNamed(n.to_string())),
|
|
1554
1797
|
},
|
|
@@ -1565,26 +1808,41 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
1565
1808
|
// for a case a wrong program still fails on, just less precisely.
|
|
1566
1809
|
ast::Expr::Try(inner) => {
|
|
1567
1810
|
let inner_ty = inferExpr(inner, env, ctx)?;
|
|
1568
|
-
match &inner_ty {
|
|
1811
|
+
let name = match &inner_ty {
|
|
1812
|
+
PlumType::TNamed(name) => name,
|
|
1813
|
+
other => return Err(format!("'?' requires a Try value, found {}", other)),
|
|
1814
|
+
};
|
|
1815
|
+
let Some((_, success_info, _, failure_info)) = tryOperatorVariants(name, ctx) else {
|
|
1816
|
+
return Err(format!("'?' requires a Try value, found '{}'", name));
|
|
1817
|
+
};
|
|
1818
|
+
// The failure branch returns early from the enclosing fn, so the
|
|
1819
|
+
// inner error type must fit the fn's declared error type. Closures
|
|
1820
|
+
// (and test blocks) have no `$return` and aren't checked.
|
|
1821
|
+
if let Some(PlumType::TNamed(outer)) = env.get(RETURN_KEY).map(|s| (*s.body).clone()) {
|
|
1569
|
-
|
|
1822
|
+
if let Some((_, _, _, outer_failure)) = tryOperatorVariants(&outer, ctx) {
|
|
1570
|
-
Some(
|
|
1823
|
+
if let (Some(inner_err), Some(outer_err)) = (
|
|
1571
|
-
.field_types
|
|
1824
|
+
failure_info.field_types.first(),
|
|
1572
|
-
.first()
|
|
1573
|
-
.cloned()
|
|
1574
|
-
.
|
|
1825
|
+
outer_failure.field_types.first(),
|
|
1826
|
+
) {
|
|
1827
|
+
if unify(outer_err, inner_err, ctx).is_err() {
|
|
1575
|
-
|
|
1828
|
+
return Err(format!(
|
|
1576
|
-
|
|
1829
|
+
"'?' error type {} is not part of the function's error type {}",
|
|
1577
|
-
|
|
1830
|
+
inner_err, outer_err
|
|
1578
|
-
|
|
1831
|
+
));
|
|
1579
|
-
|
|
1832
|
+
}
|
|
1580
|
-
other => Err(format!(
|
|
1581
|
-
"'?' requires a Result or Option value, found {}",
|
|
1582
|
-
other
|
|
1583
|
-
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1584
1835
|
}
|
|
1836
|
+
Ok(success_info
|
|
1837
|
+
.field_types
|
|
1838
|
+
.first()
|
|
1839
|
+
.cloned()
|
|
1840
|
+
.unwrap_or(PlumType::TUnit))
|
|
1585
1841
|
}
|
|
1586
1842
|
ast::Expr::Closure(cl) => {
|
|
1587
1843
|
let mut closure_env = env.clone();
|
|
1844
|
+
// A `?` inside a closure returns from the closure, not the outer fn.
|
|
1845
|
+
closure_env.remove(RETURN_KEY);
|
|
1588
1846
|
let param_names: std::collections::HashSet<&str> =
|
|
1589
1847
|
cl.params.iter().map(|s| s.as_str()).collect();
|
|
1590
1848
|
let mut resolved_from_usage: BTreeMap<String, PlumType> = BTreeMap::new();
|
|
@@ -1657,6 +1915,16 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
1657
1915
|
ast::Expr::Compare(c) => {
|
|
1658
1916
|
let lt = inferExpr(&c.left, env, ctx)?;
|
|
1659
1917
|
let rt = inferExpr(&c.right, env, ctx)?;
|
|
1918
|
+
if let (PlumType::TTags(l), PlumType::TTags(r)) = (<, &rt) {
|
|
1919
|
+
// Tag equality compares ids only, so payload tags can't be compared.
|
|
1920
|
+
if l.values().chain(r.values()).any(|p| p.is_some()) {
|
|
1921
|
+
return Err(format!(
|
|
1922
|
+
"compare op: cannot compare tag unions with payload tags ({} vs {})",
|
|
1923
|
+
lt, rt
|
|
1924
|
+
));
|
|
1925
|
+
}
|
|
1926
|
+
return Ok(PlumType::TBool);
|
|
1927
|
+
}
|
|
1660
1928
|
unify(<, &rt, ctx).map_err(|e| format!("compare op: {}", e))?;
|
|
1661
1929
|
Ok(PlumType::TBool)
|
|
1662
1930
|
}
|
|
@@ -1668,9 +1936,9 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
1668
1936
|
unify(&tt, &et, ctx).map_err(|e| format!("ternary branches: {}", e))?;
|
|
1669
1937
|
Ok(tt)
|
|
1670
1938
|
}
|
|
1671
|
-
// `left ?
|
|
1939
|
+
// `left ?? right` — unlike `?`, a plain expression (no early return),
|
|
1672
1940
|
// so (unlike `Try` above) it's fully validated here: `right` must
|
|
1673
|
-
// unify with `left`'s success (`Ok`
|
|
1941
|
+
// unify with `left`'s success (`Ok`) field type.
|
|
1674
1942
|
ast::Expr::Elvis(e) => {
|
|
1675
1943
|
let left_ty = inferExpr(&e.left, env, ctx)?;
|
|
1676
1944
|
let success_ty = match &left_ty {
|
|
@@ -1682,20 +1950,20 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
1682
1950
|
.unwrap_or(PlumType::TUnit),
|
|
1683
1951
|
None => {
|
|
1684
1952
|
return Err(format!(
|
|
1685
|
-
"'?
|
|
1953
|
+
"'??' requires a Try value, found '{}'",
|
|
1686
1954
|
name
|
|
1687
1955
|
))
|
|
1688
1956
|
}
|
|
1689
1957
|
},
|
|
1690
1958
|
other => {
|
|
1691
1959
|
return Err(format!(
|
|
1692
|
-
"'?
|
|
1960
|
+
"'??' requires a Try value, found {}",
|
|
1693
1961
|
other
|
|
1694
1962
|
))
|
|
1695
1963
|
}
|
|
1696
1964
|
};
|
|
1697
1965
|
let right_ty = inferExpr(&e.right, env, ctx)?;
|
|
1698
|
-
unify(&success_ty, &right_ty, ctx).map_err(|e| format!("'?
|
|
1966
|
+
unify(&success_ty, &right_ty, ctx).map_err(|e| format!("'??' branches: {}", e))?;
|
|
1699
1967
|
Ok(success_ty)
|
|
1700
1968
|
}
|
|
1701
1969
|
ast::Expr::FnCall(call) => {
|
|
@@ -1888,6 +2156,22 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
1888
2156
|
}
|
|
1889
2157
|
}
|
|
1890
2158
|
Ok(_) => Err(format!("'{}' is not a function", call.name)),
|
|
2159
|
+
Err(_) if isTagLiteralName(&call.name, ctx) && call.args.len() <= 1 => {
|
|
2160
|
+
// `Bad(x)` — an undeclared uppercase name applied to one arg is
|
|
2161
|
+
// a tag literal with a payload.
|
|
2162
|
+
let payload = match call.args.first() {
|
|
2163
|
+
Some(arg) => {
|
|
2164
|
+
let arg_expr = match arg {
|
|
2165
|
+
ast::Arg::Positional(e) => e,
|
|
2166
|
+
ast::Arg::Keyword { value, .. } => value,
|
|
2167
|
+
ast::Arg::Pair { value, .. } => value,
|
|
2168
|
+
};
|
|
2169
|
+
Some(Box::new(inferExpr(arg_expr, env, ctx)?))
|
|
2170
|
+
}
|
|
2171
|
+
None => None,
|
|
2172
|
+
};
|
|
2173
|
+
Ok(PlumType::TTags(BTreeMap::from([(call.name.clone(), payload)])))
|
|
2174
|
+
}
|
|
1891
2175
|
Err(_) => Ok(PlumType::TVar("_".to_string())), // unknown fn: allow, codegen will catch
|
|
1892
2176
|
}
|
|
1893
2177
|
}
|
plum-checker/src/monomorphize.rs
CHANGED
|
@@ -151,7 +151,10 @@ fn plumTypeToAstType(t: &PlumType) -> ast::Type {
|
|
|
151
151
|
PlumType::TByteSlice => "[]Byte".to_string(),
|
|
152
152
|
PlumType::TUnit => "Unit".to_string(),
|
|
153
153
|
PlumType::TNamed(n) => n.clone(),
|
|
154
|
+
PlumType::TVar(_)
|
|
155
|
+
| PlumType::TFun(_, _)
|
|
156
|
+
| PlumType::TVariadic(_)
|
|
154
|
-
|
|
157
|
+
| PlumType::TTags(_) => t.to_string(),
|
|
155
158
|
};
|
|
156
159
|
ast::Type {
|
|
157
160
|
name,
|
plum-checker/src/types.rs
CHANGED
|
@@ -22,6 +22,10 @@ pub enum PlumType {
|
|
|
22
22
|
/// a body is as a `for` loop's iterable — no other `unify`/`inferExpr` arm
|
|
23
23
|
/// handles it, so any other use is a type error by construction.
|
|
24
24
|
TVariadic(Box<PlumType>),
|
|
25
|
+
/// A structural tag-union type `[A, B(Str)]`: tag name -> optional payload type.
|
|
26
|
+
/// `Display` is the canonical string (sorted, no spaces) the parser also
|
|
27
|
+
/// produces for the type annotation, so mangled names round-trip.
|
|
28
|
+
TTags(BTreeMap<String, Option<Box<PlumType>>>),
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
impl std::fmt::Display for PlumType {
|
|
@@ -41,6 +45,16 @@ impl std::fmt::Display for PlumType {
|
|
|
41
45
|
}
|
|
42
46
|
PlumType::TNamed(n) => write!(f, "{}", n),
|
|
43
47
|
PlumType::TVariadic(inner) => write!(f, "...{}", inner),
|
|
48
|
+
PlumType::TTags(tags) => {
|
|
49
|
+
let parts: Vec<String> = tags
|
|
50
|
+
.iter()
|
|
51
|
+
.map(|(n, p)| match p {
|
|
52
|
+
Some(t) => format!("{}({})", n, t),
|
|
53
|
+
None => n.clone(),
|
|
54
|
+
})
|
|
55
|
+
.collect();
|
|
56
|
+
write!(f, "[{}]", parts.join(","))
|
|
57
|
+
}
|
|
44
58
|
}
|
|
45
59
|
}
|
|
46
60
|
}
|
plum-checker/tests/checker_tests.rs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#![allow(non_snake_case)]
|
|
2
2
|
use plum_checker::types::*;
|
|
3
|
+
use plum_checker::{
|
|
3
|
-
|
|
4
|
+
checkSource, erasedEnumName, plumTypeFromAst, plumTypeFromName, tagUnionSegmentsCompatible,
|
|
5
|
+
unify, CheckCtx,
|
|
6
|
+
};
|
|
4
7
|
use plum_core::ast::Type as AstType;
|
|
5
8
|
use plum_core::{ast::*, AstParser};
|
|
6
9
|
|
|
@@ -1299,3 +1302,327 @@ fun bad() -> Int =
|
|
|
1299
1302
|
errs
|
|
1300
1303
|
);
|
|
1301
1304
|
}
|
|
1305
|
+
|
|
1306
|
+
// ---------------------------------------------------------------------------
|
|
1307
|
+
// Roc-style tag unions + `Try` (T2). Sources define a local `Try` inline.
|
|
1308
|
+
// ---------------------------------------------------------------------------
|
|
1309
|
+
|
|
1310
|
+
/// Like `parse`, but asserts the parse produced no error nodes.
|
|
1311
|
+
fn parseClean(src: &str) -> Source {
|
|
1312
|
+
let mut parser = tree_sitter::Parser::new();
|
|
1313
|
+
parser
|
|
1314
|
+
.set_language(&tree_sitter_plum::LANGUAGE.into())
|
|
1315
|
+
.unwrap();
|
|
1316
|
+
let tree = parser.parse(src, None).unwrap();
|
|
1317
|
+
assert!(
|
|
1318
|
+
!tree.root_node().has_error(),
|
|
1319
|
+
"embedded test source has parse errors:\n{}",
|
|
1320
|
+
src
|
|
1321
|
+
);
|
|
1322
|
+
AstParser::new(src).parseSource(tree.root_node())
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
const TRY_DEF: &str = "\
|
|
1326
|
+
enum Try[T, E] =
|
|
1327
|
+
| Ok(T)
|
|
1328
|
+
| Err(E)
|
|
1329
|
+
|
|
1330
|
+
fun mapErr(self, cb: fn(E) -> F) -> Try[T, F] =
|
|
1331
|
+
match self
|
|
1332
|
+
Ok(v) =>
|
|
1333
|
+
return Ok(v)
|
|
1334
|
+
Err(e) =>
|
|
1335
|
+
return Err(cb(e))
|
|
1336
|
+
|
|
1337
|
+
";
|
|
1338
|
+
|
|
1339
|
+
fn checkTry(body: &str) -> Result<(), Vec<CheckError>> {
|
|
1340
|
+
checkSource(&parseClean(&format!("{}{}", TRY_DEF, body)))
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
fn errText(r: Result<(), Vec<CheckError>>) -> String {
|
|
1344
|
+
r.unwrap_err()
|
|
1345
|
+
.iter()
|
|
1346
|
+
.map(|e| e.message.clone())
|
|
1347
|
+
.collect::<Vec<_>>()
|
|
1348
|
+
.join(" | ")
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
#[test]
|
|
1352
|
+
fn tagsTypeDisplayAndRoundTrip() {
|
|
1353
|
+
let t = plumTypeFromName("[A,B(Str)]");
|
|
1354
|
+
match &t {
|
|
1355
|
+
PlumType::TTags(m) => {
|
|
1356
|
+
assert_eq!(m.len(), 2);
|
|
1357
|
+
assert!(m["A"].is_none());
|
|
1358
|
+
assert_eq!(m["B"].as_deref(), Some(&PlumType::TStr));
|
|
1359
|
+
}
|
|
1360
|
+
other => panic!("expected TTags, got {:?}", other),
|
|
1361
|
+
}
|
|
1362
|
+
assert_eq!(t.to_string(), "[A,B(Str)]");
|
|
1363
|
+
assert_eq!(plumTypeFromName("[]Byte"), PlumType::TByteSlice);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
#[test]
|
|
1367
|
+
fn tagsSubsetUnify() {
|
|
1368
|
+
let ctx = emptyCtx();
|
|
1369
|
+
let small = plumTypeFromName("[A]");
|
|
1370
|
+
let big = plumTypeFromName("[A,B(Str)]");
|
|
1371
|
+
assert!(unify(&big, &small, &ctx).is_ok());
|
|
1372
|
+
assert!(unify(&small, &big, &ctx).is_err());
|
|
1373
|
+
assert!(unify(&big, &plumTypeFromName("[B(Int)]"), &ctx).is_err());
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
#[test]
|
|
1377
|
+
fn nested_union_in_payload() {
|
|
1378
|
+
let ctx = emptyCtx();
|
|
1379
|
+
// Brackets inside a payload must not split the outer segment list.
|
|
1380
|
+
assert!(tagUnionSegmentsCompatible(
|
|
1381
|
+
"Try$Int$[A(Try$Int$[X,Y])]",
|
|
1382
|
+
"Try$Int$[A(Try$Int$[X])]",
|
|
1383
|
+
&ctx
|
|
1384
|
+
));
|
|
1385
|
+
assert!(!tagUnionSegmentsCompatible(
|
|
1386
|
+
"Try$Int$[A(Try$Int$[X])]",
|
|
1387
|
+
"Try$Int$[A(Try$Int$[Z])]",
|
|
1388
|
+
&ctx
|
|
1389
|
+
));
|
|
1390
|
+
assert!(tagUnionSegmentsCompatible("Try$Int$[A,B]", "Try$Int$[A]", &ctx));
|
|
1391
|
+
assert!(!tagUnionSegmentsCompatible("Try$Int$[A]", "Try$Int$[A,B]", &ctx));
|
|
1392
|
+
// Non-tag segments must match exactly; arity must match.
|
|
1393
|
+
assert!(!tagUnionSegmentsCompatible("Try$Int$[A]", "Try$Str$[A]", &ctx));
|
|
1394
|
+
assert!(!tagUnionSegmentsCompatible("Try$Int$[A]", "Try$Int", &ctx));
|
|
1395
|
+
assert_eq!(
|
|
1396
|
+
erasedEnumName("Try$Int$[A,B(Try$Int$[X])]"),
|
|
1397
|
+
"Try$Int$Tags"
|
|
1398
|
+
);
|
|
1399
|
+
assert_eq!(erasedEnumName("Try$[A]$[B,C]"), "Try$Tags$Tags");
|
|
1400
|
+
assert_eq!(erasedEnumName("List$[]Byte"), "List$[]Byte");
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
#[test]
|
|
1404
|
+
fn tryOperatorVariantsOnlyOkErr() {
|
|
1405
|
+
let src = "\
|
|
1406
|
+
enum Opt =
|
|
1407
|
+
| Some(Int)
|
|
1408
|
+
| None
|
|
1409
|
+
";
|
|
1410
|
+
let source = parseClean(src);
|
|
1411
|
+
let (_, methods, variants, params) = plum_checker::buildGlobalTables(&source);
|
|
1412
|
+
let min = plum_checker::buildMinRequiredArgs(&source);
|
|
1413
|
+
let ctx = CheckCtx {
|
|
1414
|
+
methods: &methods,
|
|
1415
|
+
enum_variants: &variants,
|
|
1416
|
+
enum_params: ¶ms,
|
|
1417
|
+
min_required: &min,
|
|
1418
|
+
};
|
|
1419
|
+
assert!(plum_checker::tryOperatorVariants("Opt", &ctx).is_none());
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
#[test]
|
|
1423
|
+
fn question_mark_rejects_unlisted_tag() {
|
|
1424
|
+
let r = checkTry(
|
|
1425
|
+
"\
|
|
1426
|
+
fun g() -> Try[Int, [B]] =
|
|
1427
|
+
Err(B)
|
|
1428
|
+
|
|
1429
|
+
fun f() -> Try[Int, [A]] =
|
|
1430
|
+
x := g()?
|
|
1431
|
+
Ok(x)
|
|
1432
|
+
",
|
|
1433
|
+
);
|
|
1434
|
+
let msg = errText(r);
|
|
1435
|
+
assert!(msg.contains("[B]"), "got: {}", msg);
|
|
1436
|
+
assert!(msg.contains("[A]"), "got: {}", msg);
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
#[test]
|
|
1440
|
+
fn question_mark_widens() {
|
|
1441
|
+
let r = checkTry(
|
|
1442
|
+
"\
|
|
1443
|
+
fun g() -> Try[Int, [A]] =
|
|
1444
|
+
Err(A)
|
|
1445
|
+
|
|
1446
|
+
fun f() -> Try[Int, [A, B]] =
|
|
1447
|
+
x := g()?
|
|
1448
|
+
Ok(x)
|
|
1449
|
+
",
|
|
1450
|
+
);
|
|
1451
|
+
assert!(r.is_ok(), "got {:?}", r.err());
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
#[test]
|
|
1455
|
+
fn question_mark_different_ok_types_check() {
|
|
1456
|
+
let r = checkTry(
|
|
1457
|
+
"\
|
|
1458
|
+
fun g() -> Try[Int, [A]] =
|
|
1459
|
+
Err(A)
|
|
1460
|
+
|
|
1461
|
+
fun f() -> Try[Str, [A, B]] =
|
|
1462
|
+
x := g()?
|
|
1463
|
+
Ok(\"v\")
|
|
1464
|
+
",
|
|
1465
|
+
);
|
|
1466
|
+
assert!(r.is_ok(), "got {:?}", r.err());
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
#[test]
|
|
1470
|
+
fn closure_question_mark_no_return_check() {
|
|
1471
|
+
// The closure's own `?` must not be checked against f's `[A]` return union.
|
|
1472
|
+
let r = checkTry(
|
|
1473
|
+
"\
|
|
1474
|
+
fun g() -> Try[Int, [B]] =
|
|
1475
|
+
Err(B)
|
|
1476
|
+
|
|
1477
|
+
fun f() -> Try[Int, [A]] =
|
|
1478
|
+
h := || g()?
|
|
1479
|
+
Ok(1)
|
|
1480
|
+
",
|
|
1481
|
+
);
|
|
1482
|
+
assert!(r.is_ok(), "got {:?}", r.err());
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
#[test]
|
|
1486
|
+
fn function_return_widens_try_value() {
|
|
1487
|
+
let r = checkTry(
|
|
1488
|
+
"\
|
|
1489
|
+
fun g() -> Try[Int, [A]] =
|
|
1490
|
+
Err(A)
|
|
1491
|
+
|
|
1492
|
+
fun f() -> Try[Int, [A, B(Str)]] =
|
|
1493
|
+
g()
|
|
1494
|
+
",
|
|
1495
|
+
);
|
|
1496
|
+
assert!(r.is_ok(), "got {:?}", r.err());
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
#[test]
|
|
1500
|
+
fn tag_literal_with_payload_and_wrong_payload() {
|
|
1501
|
+
assert!(checkTry("fun f() -> Try[Int, [Bad(Str)]] =\n Err(Bad(\"x\"))\n").is_ok());
|
|
1502
|
+
assert!(checkTry("fun f() -> Try[Int, [Bad(Str)]] =\n Err(Bad(5))\n").is_err());
|
|
1503
|
+
// Typos are caught by the declared union.
|
|
1504
|
+
assert!(checkTry("fun f() -> Try[Int, [Bad]] =\n Err(Bda)\n").is_err());
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
#[test]
|
|
1508
|
+
fn match_exhaustive_tags() {
|
|
1509
|
+
let ok = checkTry(
|
|
1510
|
+
"\
|
|
1511
|
+
fun f(t: [A, B(Str)]) -> Int =
|
|
1512
|
+
match t
|
|
1513
|
+
A => 1
|
|
1514
|
+
B(s) => 2
|
|
1515
|
+
",
|
|
1516
|
+
);
|
|
1517
|
+
assert!(ok.is_ok(), "got {:?}", ok.err());
|
|
1518
|
+
let missing = errText(checkTry(
|
|
1519
|
+
"\
|
|
1520
|
+
fun f(t: [A, B(Str)]) -> Int =
|
|
1521
|
+
match t
|
|
1522
|
+
A => 1
|
|
1523
|
+
",
|
|
1524
|
+
));
|
|
1525
|
+
assert!(
|
|
1526
|
+
missing.contains("non-exhaustive match on [A,B(Str)]: missing B"),
|
|
1527
|
+
"got: {}",
|
|
1528
|
+
missing
|
|
1529
|
+
);
|
|
1530
|
+
let wild = checkTry(
|
|
1531
|
+
"\
|
|
1532
|
+
fun f(t: [A, B(Str)]) -> Int =
|
|
1533
|
+
match t
|
|
1534
|
+
A => 1
|
|
1535
|
+
_ => 2
|
|
1536
|
+
",
|
|
1537
|
+
);
|
|
1538
|
+
assert!(wild.is_ok(), "got {:?}", wild.err());
|
|
1539
|
+
let unknown = checkTry(
|
|
1540
|
+
"\
|
|
1541
|
+
fun f(t: [A, B]) -> Int =
|
|
1542
|
+
match t
|
|
1543
|
+
A => 1
|
|
1544
|
+
C => 3
|
|
1545
|
+
_ => 2
|
|
1546
|
+
",
|
|
1547
|
+
);
|
|
1548
|
+
assert!(unknown.is_err());
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
#[test]
|
|
1552
|
+
fn nested_tag_pattern_in_err() {
|
|
1553
|
+
let r = checkTry(
|
|
1554
|
+
"\
|
|
1555
|
+
fun f(r: Try[Int, [NotFound, Bad(Int)]]) -> Int =
|
|
1556
|
+
match r
|
|
1557
|
+
Ok(v) => v
|
|
1558
|
+
Err(NotFound) => 0
|
|
1559
|
+
Err(Bad(n)) => n
|
|
1560
|
+
",
|
|
1561
|
+
);
|
|
1562
|
+
assert!(r.is_ok(), "got {:?}", r.err());
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
#[test]
|
|
1566
|
+
fn eq_rejects_payload_tags() {
|
|
1567
|
+
let ok = checkTry("fun f(a: [A, B], b: [A, B]) -> Bool =\n a == b\n");
|
|
1568
|
+
assert!(ok.is_ok(), "got {:?}", ok.err());
|
|
1569
|
+
let lit = checkTry("fun f() -> Bool =\n A == B\n");
|
|
1570
|
+
assert!(lit.is_ok(), "got {:?}", lit.err());
|
|
1571
|
+
let bad = errText(checkTry(
|
|
1572
|
+
"fun f(a: [A, B(Str)], b: [A, B(Str)]) -> Bool =\n a == b\n",
|
|
1573
|
+
));
|
|
1574
|
+
assert!(bad.contains("payload"), "got: {}", bad);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
#[test]
|
|
1578
|
+
fn unknown_lowercase_still_error() {
|
|
1579
|
+
let r = checkTry("fun f() -> Int =\n nope\n");
|
|
1580
|
+
assert!(r.is_err());
|
|
1581
|
+
assert!(errText(r).contains("undefined name 'nope'"));
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
#[test]
|
|
1585
|
+
fn crash_requires_str() {
|
|
1586
|
+
assert!(checkTry("fun f() -> Int =\n crash \"boom\"\n").is_ok());
|
|
1587
|
+
let bad = errText(checkTry("fun f() -> Int =\n crash 5\n"));
|
|
1588
|
+
assert!(bad.contains("Str"), "got: {}", bad);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
#[test]
|
|
1592
|
+
fn elvis_defaults_try_value() {
|
|
1593
|
+
let ok = checkTry(
|
|
1594
|
+
"\
|
|
1595
|
+
fun g() -> Try[Int, [A]] =
|
|
1596
|
+
Err(A)
|
|
1597
|
+
|
|
1598
|
+
fun f() -> Int =
|
|
1599
|
+
g() ?? 7
|
|
1600
|
+
",
|
|
1601
|
+
);
|
|
1602
|
+
assert!(ok.is_ok(), "got {:?}", ok.err());
|
|
1603
|
+
let bad = checkTry(
|
|
1604
|
+
"\
|
|
1605
|
+
fun g() -> Try[Int, [A]] =
|
|
1606
|
+
Err(A)
|
|
1607
|
+
|
|
1608
|
+
fun f() -> Int =
|
|
1609
|
+
g() ?? \"x\"
|
|
1610
|
+
",
|
|
1611
|
+
);
|
|
1612
|
+
assert!(bad.is_err());
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
#[test]
|
|
1616
|
+
fn try_closure_maps_error() {
|
|
1617
|
+
let r = checkTry(
|
|
1618
|
+
"\
|
|
1619
|
+
fun g() -> Try[Int, [B]] =
|
|
1620
|
+
Err(B)
|
|
1621
|
+
|
|
1622
|
+
fun f() -> Try[Int, [Mapped]] =
|
|
1623
|
+
x := g() ? |e| Mapped
|
|
1624
|
+
Ok(x)
|
|
1625
|
+
",
|
|
1626
|
+
);
|
|
1627
|
+
assert!(r.is_ok(), "got {:?}", r.err());
|
|
1628
|
+
}
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -649,7 +649,7 @@ fn plumTypeToValtype(t: &PlumType) -> ValType {
|
|
|
649
649
|
// where a closure's exact struct type isn't being constructed/destructured
|
|
650
650
|
// directly (e.g. deciding a local's storage class), matching `TVariadic`'s
|
|
651
651
|
// existing "not modeled as a concrete shape here" treatment.
|
|
652
|
-
PlumType::TFun(_, _) | PlumType::TVariadic(_) => ValType::Ref(RefType::ANYREF),
|
|
652
|
+
PlumType::TFun(_, _) | PlumType::TVariadic(_) | PlumType::TTags(_) => ValType::Ref(RefType::ANYREF),
|
|
653
653
|
PlumType::TVar(_) | PlumType::TUnit => ValType::I64,
|
|
654
654
|
}
|
|
655
655
|
}
|
|
@@ -755,7 +755,7 @@ fn plumTypeToGcValtype(t: &PlumType, registry: &GcTypeRegistry) -> ValType {
|
|
|
755
755
|
// TFun (closures) and TVariadic get their own concrete representation once
|
|
756
756
|
// Task 2 (closures/variadic calls) lands — `anyref` is a safe, valid-but-not-
|
|
757
757
|
// yet-meaningful placeholder in the meantime, since nothing consumes it yet.
|
|
758
|
-
PlumType::TFun(_, _) | PlumType::TVariadic(_) => ValType::Ref(RefType::ANYREF),
|
|
758
|
+
PlumType::TFun(_, _) | PlumType::TVariadic(_) | PlumType::TTags(_) => ValType::Ref(RefType::ANYREF),
|
|
759
759
|
PlumType::TVar(_) | PlumType::TUnit => ValType::I64,
|
|
760
760
|
}
|
|
761
761
|
}
|