plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
ec5336f
— Peter John
2026-09-08T13:13:12+05:30
feat(lang): add elvis `?:` operator
- README.md +9 -0
- plum-checker/src/lib.rs +17 -0
- plum-checker/src/monomorphize.rs +12 -0
- plum-core/src/ast.rs +10 -0
- plum-core/src/builtin_usage.rs +4 -0
- plum-core/src/parser.rs +14 -0
- plum-examples/{try_operator.plum → error_propagation.plum} +25 -0
- plum-tooling/tree-sitter-plum/grammar.js +13 -0
- plum-tooling/tree-sitter-plum/queries/plum/format.scm +2 -0
- plum-tooling/tree-sitter-plum/queries/plum/highlights.scm +2 -0
- plum-tooling/tree-sitter-plum/src/grammar.json +0 -0
- plum-tooling/tree-sitter-plum/src/node-types.json +0 -0
- plum-tooling/tree-sitter-plum/src/parser.c +0 -0
- plum-tooling/tree-sitter-plum/test/corpus/elvis.txt +107 -0
- plum-wasm-codegen/src/lib.rs +68 -0
README.md
CHANGED
|
@@ -323,6 +323,15 @@ fun sumTwo(a: Str, b: Str) -> Result[Int, Str] =
|
|
|
323
323
|
return Ok(x + y)
|
|
324
324
|
```
|
|
325
325
|
|
|
326
|
+
`left ?: right` (elvis) is the plain-expression counterpart — no early return, just a fallback value if `left` is `Err`/`None`:
|
|
327
|
+
|
|
328
|
+
```plum
|
|
329
|
+
fun sumOrZero(a: Str, b: Str) -> Int =
|
|
330
|
+
x := parsePositive(a) ?: 0
|
|
331
|
+
y := parsePositive(b) ?: 0
|
|
332
|
+
x + y
|
|
333
|
+
```
|
|
334
|
+
|
|
326
335
|
## Standard library highlights
|
|
327
336
|
|
|
328
337
|
- **`Bool`** (`import std/Bool`) — ordinary `enum Bool = | True | False`
|
plum-checker/src/lib.rs
CHANGED
|
@@ -1038,6 +1038,7 @@ fn resolveClosureParamFromFieldUsage(
|
|
|
1038
1038
|
ast::Expr::Not(inner) | ast::Expr::Paren(inner) => scanExpr(inner, params, ctx, resolved),
|
|
1039
1039
|
ast::Expr::Unary(u) => scanExpr(&u.operand, params, ctx, resolved),
|
|
1040
1040
|
ast::Expr::Ternary(t) => { scanExpr(&t.condition, params, ctx, resolved); scanExpr(&t.then, params, ctx, resolved); scanExpr(&t.else_, params, ctx, resolved); }
|
|
1041
|
+
ast::Expr::Elvis(e) => { scanExpr(&e.left, params, ctx, resolved); scanExpr(&e.right, params, ctx, resolved); }
|
|
1041
1042
|
_ => {}
|
|
1042
1043
|
}
|
|
1043
1044
|
}
|
|
@@ -1259,6 +1260,22 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
1259
1260
|
unify(&tt, &et, ctx).map_err(|e| format!("ternary branches: {}", e))?;
|
|
1260
1261
|
Ok(tt)
|
|
1261
1262
|
}
|
|
1263
|
+
// `left ?: right` — unlike `?`, a plain expression (no early return),
|
|
1264
|
+
// so (unlike `Try` above) it's fully validated here: `right` must
|
|
1265
|
+
// unify with `left`'s success (`Ok`/`Some`) field type.
|
|
1266
|
+
ast::Expr::Elvis(e) => {
|
|
1267
|
+
let left_ty = inferExpr(&e.left, env, ctx)?;
|
|
1268
|
+
let success_ty = match &left_ty {
|
|
1269
|
+
PlumType::TNamed(name) => match tryOperatorVariants(name, ctx) {
|
|
1270
|
+
Some((_, success_info, _, _)) => success_info.field_types.first().cloned().unwrap_or(PlumType::TUnit),
|
|
1271
|
+
None => return Err(format!("'?:' requires a Result or Option value, found '{}'", name)),
|
|
1272
|
+
},
|
|
1273
|
+
other => return Err(format!("'?:' requires a Result or Option value, found {}", other)),
|
|
1274
|
+
};
|
|
1275
|
+
let right_ty = inferExpr(&e.right, env, ctx)?;
|
|
1276
|
+
unify(&success_ty, &right_ty, ctx).map_err(|e| format!("'?:' branches: {}", e))?;
|
|
1277
|
+
Ok(success_ty)
|
|
1278
|
+
}
|
|
1262
1279
|
ast::Expr::FnCall(call) => {
|
|
1263
1280
|
// `Int(x)`/`Float(x)`/`Byte(x)` are builtin numeric conversions, not
|
|
1264
1281
|
// ordinary calls — handled here so `y = Float(x)` unifies against
|
plum-checker/src/monomorphize.rs
CHANGED
|
@@ -295,6 +295,10 @@ fn substituteTypesInExpr(expr: &mut ast::Expr, subst: &Substitution) {
|
|
|
295
295
|
}
|
|
296
296
|
ast::Expr::Not(inner) => substituteTypesInExpr(inner, subst),
|
|
297
297
|
ast::Expr::Try(inner) => substituteTypesInExpr(inner, subst),
|
|
298
|
+
ast::Expr::Elvis(e) => {
|
|
299
|
+
substituteTypesInExpr(&mut e.left, subst);
|
|
300
|
+
substituteTypesInExpr(&mut e.right, subst);
|
|
301
|
+
}
|
|
298
302
|
ast::Expr::Compare(c) => {
|
|
299
303
|
substituteTypesInExpr(&mut c.left, subst);
|
|
300
304
|
substituteTypesInExpr(&mut c.right, subst);
|
|
@@ -488,6 +492,10 @@ fn renameVarInExpr(expr: &mut ast::Expr, old: &str, new: &str) {
|
|
|
488
492
|
ast::Expr::Compare(c) => { renameVarInExpr(&mut c.left, old, new); renameVarInExpr(&mut c.right, old, new); }
|
|
489
493
|
ast::Expr::Not(inner) => renameVarInExpr(inner, old, new),
|
|
490
494
|
ast::Expr::Try(inner) => renameVarInExpr(inner, old, new),
|
|
495
|
+
ast::Expr::Elvis(e) => {
|
|
496
|
+
renameVarInExpr(&mut e.left, old, new);
|
|
497
|
+
renameVarInExpr(&mut e.right, old, new);
|
|
498
|
+
}
|
|
491
499
|
ast::Expr::Unary(u) => renameVarInExpr(&mut u.operand, old, new),
|
|
492
500
|
ast::Expr::Paren(inner) => renameVarInExpr(inner, old, new),
|
|
493
501
|
ast::Expr::Ternary(t) => {
|
|
@@ -2221,6 +2229,10 @@ impl<'a> Monomorphizer<'a> {
|
|
|
2221
2229
|
self.rewriteExpr(&mut t.then, env)?;
|
|
2222
2230
|
self.rewriteExpr(&mut t.else_, env)?;
|
|
2223
2231
|
}
|
|
2232
|
+
ast::Expr::Elvis(e) => {
|
|
2233
|
+
self.rewriteExpr(&mut e.left, env)?;
|
|
2234
|
+
self.rewriteExpr(&mut e.right, env)?;
|
|
2235
|
+
}
|
|
2224
2236
|
// String interpolation can embed arbitrary expressions (including generic
|
|
2225
2237
|
// call sites), so recurse into its interpolated parts.
|
|
2226
2238
|
ast::Expr::String(s) => {
|
plum-core/src/ast.rs
CHANGED
|
@@ -338,6 +338,10 @@ pub enum Expr {
|
|
|
338
338
|
/// `expr?` — unwraps a `Result`'s `Ok`/an `Option`'s `Some`, or returns the
|
|
339
339
|
/// `Err`/`None` value from the enclosing function as-is otherwise.
|
|
340
340
|
Try(Box<Expr>),
|
|
341
|
+
/// `left ?: right` — if `left` is `Ok(v)`/`Some(v)`, the value is `v`; if
|
|
342
|
+
/// it's `Err(_)`/`None`, the value is `right`. Unlike `Try`, a plain
|
|
343
|
+
/// expression with no early return.
|
|
344
|
+
Elvis(Box<ElvisExpr>),
|
|
341
345
|
/// `{expr}` — grouped/parenthesized expression
|
|
342
346
|
Paren(Box<Expr>),
|
|
343
347
|
String(StringExpr),
|
|
@@ -421,6 +425,12 @@ pub struct TernaryExpr {
|
|
|
421
425
|
pub else_: Expr,
|
|
422
426
|
}
|
|
423
427
|
|
|
428
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
429
|
+
pub struct ElvisExpr {
|
|
430
|
+
pub left: Expr,
|
|
431
|
+
pub right: Expr,
|
|
432
|
+
}
|
|
433
|
+
|
|
424
434
|
#[derive(Debug, Clone, PartialEq)]
|
|
425
435
|
pub struct FnCall {
|
|
426
436
|
pub name: String,
|
plum-core/src/builtin_usage.rs
CHANGED
|
@@ -198,6 +198,10 @@ fn walkExpr(expr: &Expr, names: &mut HashSet<String>) {
|
|
|
198
198
|
walkExpr(&t.then, names);
|
|
199
199
|
walkExpr(&t.else_, names);
|
|
200
200
|
}
|
|
201
|
+
Expr::Elvis(e) => {
|
|
202
|
+
walkExpr(&e.left, names);
|
|
203
|
+
walkExpr(&e.right, names);
|
|
204
|
+
}
|
|
201
205
|
Expr::FnCall(call) => {
|
|
202
206
|
if isTracked(&call.name) {
|
|
203
207
|
names.insert(call.name.clone());
|
plum-core/src/parser.rs
CHANGED
|
@@ -637,6 +637,7 @@ impl<'a> AstParser<'a> {
|
|
|
637
637
|
}
|
|
638
638
|
"boolean_operator" => self.parseBoolOp(node),
|
|
639
639
|
"ternary_expression" => self.parseTernary(node),
|
|
640
|
+
"elvis_expression" => self.parseElvis(node),
|
|
640
641
|
"closure" => Expr::Closure(Box::new(self.parseClosure(node))),
|
|
641
642
|
_ => self.parsePrimaryExpression(node),
|
|
642
643
|
}
|
|
@@ -781,6 +782,18 @@ impl<'a> AstParser<'a> {
|
|
|
781
782
|
Expr::Ternary(Box::new(TernaryExpr { condition, then, else_ }))
|
|
782
783
|
}
|
|
783
784
|
|
|
785
|
+
fn parseElvis(&self, node: Node) -> Expr {
|
|
786
|
+
// elvis_expression: expression "?:" expression
|
|
787
|
+
let named: Vec<Node> = self.namedChildren(node);
|
|
788
|
+
let left = named.first()
|
|
789
|
+
.map(|n| { let u = self.unwrapExprNode(*n); self.parseExpression(u) })
|
|
790
|
+
.unwrap_or(Expr::Int(0));
|
|
791
|
+
let right = named.get(1)
|
|
792
|
+
.map(|n| { let u = self.unwrapExprNode(*n); self.parseExpression(u) })
|
|
793
|
+
.unwrap_or(Expr::Int(0));
|
|
794
|
+
Expr::Elvis(Box::new(ElvisExpr { left, right }))
|
|
795
|
+
}
|
|
796
|
+
|
|
784
797
|
fn parseAttribute(&self, node: Node) -> Expr {
|
|
785
798
|
// attribute: primary_expression "." fn_identifier fn_argument_list?
|
|
786
799
|
// The member name is always fn_identifier (a superset of var_identifier); an
|
|
@@ -1060,6 +1073,7 @@ fn isExpressionKind(kind: &str) -> bool {
|
|
|
1060
1073
|
| "not_operator"
|
|
1061
1074
|
| "comparison_operator"
|
|
1062
1075
|
| "ternary_expression"
|
|
1076
|
+
| "elvis_expression"
|
|
1063
1077
|
| "attribute"
|
|
1064
1078
|
| "try_expression"
|
|
1065
1079
|
| "fn_call"
|
plum-examples/{try_operator.plum → error_propagation.plum}
RENAMED
|
@@ -11,6 +11,11 @@ import std/List
|
|
|
11
11
|
# whatever gets returned early; that isn't checked until codegen (see
|
|
12
12
|
# `plum-checker`'s `inferExpr` on `Expr::Try`), so a mismatch there still
|
|
13
13
|
# surfaces as a clear compile error, just a less precise one.
|
|
14
|
+
#
|
|
15
|
+
# `left ?: right` (elvis) is the plain-expression counterpart: same Ok/Some
|
|
16
|
+
# vs Err/None narrowing, but no early return — `right` is a fallback value,
|
|
17
|
+
# fully checked (unlike `?`) since there's no enclosing-return-type
|
|
18
|
+
# uncertainty to be permissive about.
|
|
14
19
|
|
|
15
20
|
fun parsePositive(s: Str) -> Result[Int, Str] =
|
|
16
21
|
n := parseInt(s)?
|
|
@@ -36,6 +41,18 @@ fun doubledFirstPositive(list: List[Int]) -> Option[Int] =
|
|
|
36
41
|
a := firstPositive(list)?
|
|
37
42
|
return Some(a * 2)
|
|
38
43
|
|
|
44
|
+
# `left ?: right` — same Ok/Some-vs-Err/None narrowing as `?`, but a plain
|
|
45
|
+
# expression (no early return): `right` is the fallback value, unified against
|
|
46
|
+
# the success field's type by the checker.
|
|
47
|
+
|
|
48
|
+
fun sumOrZero(a: Str, b: Str) -> Int =
|
|
49
|
+
x := parsePositive(a) ?: 0
|
|
50
|
+
y := parsePositive(b) ?: 0
|
|
51
|
+
x + y
|
|
52
|
+
|
|
53
|
+
fun firstOrDefault(list: List[Int], default: Int) -> Int =
|
|
54
|
+
list.get(0) ?: default
|
|
55
|
+
|
|
39
56
|
test "try operator unwraps Ok and propagates the value through two calls"
|
|
40
57
|
r := sumTwo("2", "3")
|
|
41
58
|
assert r.isOk()
|
|
@@ -56,3 +73,11 @@ test "try operator exits early with None, skipping the rest of the function"
|
|
|
56
73
|
l := List[Int]()
|
|
57
74
|
r := doubledFirstPositive(l)
|
|
58
75
|
assert r.isNone()
|
|
76
|
+
|
|
77
|
+
test "elvis operator unwraps Ok and falls back to 0 on Err"
|
|
78
|
+
assert sumOrZero("2", "3") == 5
|
|
79
|
+
assert sumOrZero("2", "-3") == 2
|
|
80
|
+
|
|
81
|
+
test "elvis operator unwraps Some and falls back to the given default on None"
|
|
82
|
+
assert firstOrDefault(List(5), 99) == 5
|
|
83
|
+
assert firstOrDefault(List[Int](), 99) == 99
|
plum-tooling/tree-sitter-plum/grammar.js
CHANGED
|
@@ -387,6 +387,7 @@ module.exports = grammar({
|
|
|
387
387
|
$.closure,
|
|
388
388
|
$.primary_expression,
|
|
389
389
|
$.ternary_expression,
|
|
390
|
+
$.elvis_expression,
|
|
390
391
|
),
|
|
391
392
|
|
|
392
393
|
primary_expression: ($) =>
|
|
@@ -594,6 +595,18 @@ module.exports = grammar({
|
|
|
594
595
|
seq($.expression, "?", $.expression, ":", $.expression),
|
|
595
596
|
),
|
|
596
597
|
|
|
598
|
+
// Elvis / null-coalescing: `opt ?: default` — if `left` is `Ok(v)`/`Some(v)`,
|
|
599
|
+
// the value is `v`; if it's `Err(_)`/`None`, the value is `right`. A single
|
|
600
|
+
// atomic `"?:"` token (maximal munch out-munches the bare `"?"` ternary/
|
|
601
|
+
// try-operator tokens at the same position, so — unlike `try_expression`,
|
|
602
|
+
// which genuinely shares ternary's leading `?` and needs the `conflicts`
|
|
603
|
+
// entry above — this needs no special conflict handling).
|
|
604
|
+
elvis_expression: ($) =>
|
|
605
|
+
prec.right(
|
|
606
|
+
PREC.conditional,
|
|
607
|
+
seq($.expression, "?:", $.expression),
|
|
608
|
+
),
|
|
609
|
+
|
|
597
610
|
// ==========
|
|
598
611
|
// Literals
|
|
599
612
|
// ==========
|
plum-tooling/tree-sitter-plum/queries/plum/format.scm
CHANGED
|
@@ -82,6 +82,8 @@
|
|
|
82
82
|
":"
|
|
83
83
|
] @prepend_space @append_space)
|
|
84
84
|
|
|
85
|
+
(elvis_expression "?:" @prepend_space @append_space)
|
|
86
|
+
|
|
85
87
|
; ============================================================
|
|
86
88
|
; Keywords — space after (Topiary inserts NO whitespace at all
|
|
87
89
|
; between adjacent leaves unless a query says to; every keyword
|
plum-tooling/tree-sitter-plum/queries/plum/highlights.scm
CHANGED
|
@@ -64,6 +64,8 @@
|
|
|
64
64
|
|
|
65
65
|
(try_expression "?" @operator)
|
|
66
66
|
|
|
67
|
+
(elvis_expression "?:" @operator)
|
|
68
|
+
|
|
67
69
|
[
|
|
68
70
|
"->"
|
|
69
71
|
"=>"
|
plum-tooling/tree-sitter-plum/src/grammar.json
CHANGED
|
Binary file
|
plum-tooling/tree-sitter-plum/src/node-types.json
CHANGED
|
Binary file
|
plum-tooling/tree-sitter-plum/src/parser.c
CHANGED
|
Binary file
|
plum-tooling/tree-sitter-plum/test/corpus/elvis.txt
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
================================================================================
|
|
2
|
+
elvis operator - unwraps Ok/Some or falls back to the right-hand value
|
|
3
|
+
================================================================================
|
|
4
|
+
|
|
5
|
+
fun firstOrDefault(list: List[Int], default: Int) -> Int =
|
|
6
|
+
list.get(0) ?: default
|
|
7
|
+
|
|
8
|
+
--------------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
(source
|
|
11
|
+
(fn
|
|
12
|
+
(fn_identifier)
|
|
13
|
+
(param
|
|
14
|
+
(var_identifier)
|
|
15
|
+
(type
|
|
16
|
+
(type_identifier)
|
|
17
|
+
(type
|
|
18
|
+
(type_identifier))))
|
|
19
|
+
(param
|
|
20
|
+
(var_identifier)
|
|
21
|
+
(type
|
|
22
|
+
(type_identifier)))
|
|
23
|
+
(type
|
|
24
|
+
(type_identifier))
|
|
25
|
+
(body
|
|
26
|
+
(expression
|
|
27
|
+
(elvis_expression
|
|
28
|
+
(expression
|
|
29
|
+
(primary_expression
|
|
30
|
+
(attribute
|
|
31
|
+
(primary_expression
|
|
32
|
+
(var_identifier))
|
|
33
|
+
(fn_identifier)
|
|
34
|
+
(fn_argument_list
|
|
35
|
+
(expression
|
|
36
|
+
(primary_expression
|
|
37
|
+
(integer)))))))
|
|
38
|
+
(expression
|
|
39
|
+
(primary_expression
|
|
40
|
+
(var_identifier))))))))
|
|
41
|
+
|
|
42
|
+
================================================================================
|
|
43
|
+
elvis operator - coexists with ternary and try, all sharing `?`-prefixed tokens
|
|
44
|
+
================================================================================
|
|
45
|
+
|
|
46
|
+
fun classify(n: Int) -> Int =
|
|
47
|
+
a = n > 0 ? n : 0
|
|
48
|
+
b = n ?: 0
|
|
49
|
+
c = n?
|
|
50
|
+
a + b + c
|
|
51
|
+
|
|
52
|
+
--------------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
(source
|
|
55
|
+
(fn
|
|
56
|
+
name: (fn_identifier)
|
|
57
|
+
params: (param
|
|
58
|
+
name: (var_identifier)
|
|
59
|
+
type: (type
|
|
60
|
+
(type_identifier)))
|
|
61
|
+
returns: (type
|
|
62
|
+
(type_identifier))
|
|
63
|
+
body: (body
|
|
64
|
+
(assign
|
|
65
|
+
(var_identifier)
|
|
66
|
+
(expression
|
|
67
|
+
(ternary_expression
|
|
68
|
+
(expression
|
|
69
|
+
(comparison_operator
|
|
70
|
+
(primary_expression
|
|
71
|
+
(var_identifier))
|
|
72
|
+
(primary_expression
|
|
73
|
+
(integer))))
|
|
74
|
+
(expression
|
|
75
|
+
(primary_expression
|
|
76
|
+
(var_identifier)))
|
|
77
|
+
(expression
|
|
78
|
+
(primary_expression
|
|
79
|
+
(integer))))))
|
|
80
|
+
(assign
|
|
81
|
+
(var_identifier)
|
|
82
|
+
(expression
|
|
83
|
+
(elvis_expression
|
|
84
|
+
(expression
|
|
85
|
+
(primary_expression
|
|
86
|
+
(var_identifier)))
|
|
87
|
+
(expression
|
|
88
|
+
(primary_expression
|
|
89
|
+
(integer))))))
|
|
90
|
+
(assign
|
|
91
|
+
(var_identifier)
|
|
92
|
+
(expression
|
|
93
|
+
(primary_expression
|
|
94
|
+
(try_expression
|
|
95
|
+
value: (primary_expression
|
|
96
|
+
(var_identifier))))))
|
|
97
|
+
(expression
|
|
98
|
+
(primary_expression
|
|
99
|
+
(binary_operator
|
|
100
|
+
left: (primary_expression
|
|
101
|
+
(binary_operator
|
|
102
|
+
left: (primary_expression
|
|
103
|
+
(var_identifier))
|
|
104
|
+
right: (primary_expression
|
|
105
|
+
(var_identifier))))
|
|
106
|
+
right: (primary_expression
|
|
107
|
+
(var_identifier))))))))
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -2159,6 +2159,10 @@ impl<'a, 'c> ClosureWalker<'a, 'c> {
|
|
|
2159
2159
|
self.walkExpr(&t.then, None);
|
|
2160
2160
|
self.walkExpr(&t.else_, None);
|
|
2161
2161
|
}
|
|
2162
|
+
ast::Expr::Elvis(e) => {
|
|
2163
|
+
self.walkExpr(&e.left, None);
|
|
2164
|
+
self.walkExpr(&e.right, None);
|
|
2165
|
+
}
|
|
2162
2166
|
ast::Expr::FnCall(call) => {
|
|
2163
2167
|
let callee = self.fn_decls.get(&call.name).copied();
|
|
2164
2168
|
for (i, arg) in call.args.iter().enumerate() {
|
|
@@ -2434,6 +2438,10 @@ fn scanExprForParamTypes(
|
|
|
2434
2438
|
scanExprForParamTypes(&t.then, params, env, cctx, resolved);
|
|
2435
2439
|
scanExprForParamTypes(&t.else_, params, env, cctx, resolved);
|
|
2436
2440
|
}
|
|
2441
|
+
ast::Expr::Elvis(e) => {
|
|
2442
|
+
scanExprForParamTypes(&e.left, params, env, cctx, resolved);
|
|
2443
|
+
scanExprForParamTypes(&e.right, params, env, cctx, resolved);
|
|
2444
|
+
}
|
|
2437
2445
|
ast::Expr::FnCall(call) => {
|
|
2438
2446
|
if let Ok(PlumType::TFun(param_types, _)) = plum_checker::lookup(env, &call.name) {
|
|
2439
2447
|
for (arg, expected) in call.args.iter().zip(param_types.iter()) {
|
|
@@ -2672,6 +2680,10 @@ fn fvCollectRefsExpr(
|
|
|
2672
2680
|
fvCollectRefsExpr(&t.then, bound, seen, free, env, fn_decls);
|
|
2673
2681
|
fvCollectRefsExpr(&t.else_, bound, seen, free, env, fn_decls);
|
|
2674
2682
|
}
|
|
2683
|
+
ast::Expr::Elvis(e) => {
|
|
2684
|
+
fvCollectRefsExpr(&e.left, bound, seen, free, env, fn_decls);
|
|
2685
|
+
fvCollectRefsExpr(&e.right, bound, seen, free, env, fn_decls);
|
|
2686
|
+
}
|
|
2675
2687
|
ast::Expr::FnCall(call) => {
|
|
2676
2688
|
for arg in &call.args {
|
|
2677
2689
|
fvCollectRefsExpr(argExprOf(arg), bound, seen, free, env, fn_decls);
|
|
@@ -2930,6 +2942,24 @@ impl<'a> Collector<'a> {
|
|
|
2930
2942
|
self.walkExpr(&t.then);
|
|
2931
2943
|
self.walkExpr(&t.else_);
|
|
2932
2944
|
}
|
|
2945
|
+
// Same scratch-slot need as `Try` above — `left` is narrowed via
|
|
2946
|
+
// `ref.test`/`ref.cast` the same way, just with `right` (an
|
|
2947
|
+
// ordinary expression, no narrowing) as the fallback instead of a
|
|
2948
|
+
// `return`.
|
|
2949
|
+
ast::Expr::Elvis(e) => {
|
|
2950
|
+
let key = expr as *const ast::Expr as usize;
|
|
2951
|
+
let ty = plum_checker::inferExpr(&e.left, &self.env, &self.cctx).unwrap_or(PlumType::TVar("_".to_string()));
|
|
2952
|
+
let name = match &ty {
|
|
2953
|
+
PlumType::TNamed(n) => n.clone(),
|
|
2954
|
+
_ => String::new(),
|
|
2955
|
+
};
|
|
2956
|
+
let slot = self.next_nested_class_slot;
|
|
2957
|
+
self.next_nested_class_slot += 1;
|
|
2958
|
+
self.nested_class_scratch.insert(key, slot);
|
|
2959
|
+
self.nested_class_scratch_types.push(name);
|
|
2960
|
+
self.walkExpr(&e.left);
|
|
2961
|
+
self.walkExpr(&e.right);
|
|
2962
|
+
}
|
|
2933
2963
|
ast::Expr::FnCall(call) => {
|
|
2934
2964
|
for arg in &call.args {
|
|
2935
2965
|
self.walkArg(arg);
|
|
@@ -4114,6 +4144,44 @@ fn compileExpr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut
|
|
|
4114
4144
|
}
|
|
4115
4145
|
Instruction::End.encode(body);
|
|
4116
4146
|
}
|
|
4147
|
+
// `left ?: right` — same `ref.test`/scratch-local narrowing as `Try`
|
|
4148
|
+
// above, but both branches produce an ordinary value (no `return`):
|
|
4149
|
+
// the failure branch compiles `right` (already unified against the
|
|
4150
|
+
// success field's type by the checker), the success branch extracts
|
|
4151
|
+
// the field, same as `Try`'s own success branch.
|
|
4152
|
+
ast::Expr::Elvis(e) => {
|
|
4153
|
+
let left_ty = inferLocalType(&e.left, ctx);
|
|
4154
|
+
let enum_name = match &left_ty {
|
|
4155
|
+
PlumType::TNamed(n) => n.clone(),
|
|
4156
|
+
other => return Err(format!("codegen: '?:' requires a Result or Option value, found {}", other)),
|
|
4157
|
+
};
|
|
4158
|
+
let cctx = checkCtxOf(ctx.methods, ctx.enum_variants, ctx.enum_params, ctx.min_required);
|
|
4159
|
+
let (success_name, success_info, failure_name, _) = plum_checker::tryOperatorVariants(&enum_name, &cctx)
|
|
4160
|
+
.ok_or_else(|| format!("codegen: '?:' requires a Result or Option value, found '{}'", enum_name))?;
|
|
4161
|
+
let success_idx = *ctx.gc_types.variant_type_idx.get(success_name)
|
|
4162
|
+
.ok_or_else(|| format!("codegen: variant '{}' missing from the GC type registry", success_name))?;
|
|
4163
|
+
let failure_idx = *ctx.gc_types.variant_type_idx.get(failure_name)
|
|
4164
|
+
.ok_or_else(|| format!("codegen: variant '{}' missing from the GC type registry", failure_name))?;
|
|
4165
|
+
let success_vt = plumTypeToValtype(&success_info.field_types.first().cloned().unwrap_or(PlumType::TUnit));
|
|
4166
|
+
|
|
4167
|
+
let key = expr as *const ast::Expr as usize;
|
|
4168
|
+
let slot = *ctx.nested_class_scratch.get(&key)
|
|
4169
|
+
.ok_or_else(|| "internal codegen error: missing '?:' scratch slot".to_string())?;
|
|
4170
|
+
let scratch_local = ctx.nested_class_scratch_base + slot;
|
|
4171
|
+
|
|
4172
|
+
compileExpr(&e.left, body, ctx, state)?;
|
|
4173
|
+
Instruction::LocalTee(scratch_local).encode(body);
|
|
4174
|
+
Instruction::RefTestNonNull(HeapType::Concrete(failure_idx)).encode(body);
|
|
4175
|
+
Instruction::If(BlockType::Result(success_vt)).encode(body);
|
|
4176
|
+
compileExpr(&e.right, body, ctx, state)?;
|
|
4177
|
+
Instruction::Else.encode(body);
|
|
4178
|
+
Instruction::LocalGet(scratch_local).encode(body);
|
|
4179
|
+
Instruction::RefCastNonNull(HeapType::Concrete(success_idx)).encode(body);
|
|
4180
|
+
if !success_info.field_types.is_empty() {
|
|
4181
|
+
Instruction::StructGet { struct_type_index: success_idx, field_index: 0 }.encode(body);
|
|
4182
|
+
}
|
|
4183
|
+
Instruction::End.encode(body);
|
|
4184
|
+
}
|
|
4117
4185
|
ast::Expr::Unary(u) => match u.op {
|
|
4118
4186
|
ast::UnOp::Neg => {
|
|
4119
4187
|
if matches!(inferLocalType(&u.operand, ctx), PlumType::TFloat) {
|