plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
af1776b
— Peter John
2026-09-08T12:57:14+05:30
feat(lang): add postfix `?` error-propagation operator
- README.md +11 -0
- plum-checker/src/lib.rs +52 -0
- plum-checker/src/monomorphize.rs +3 -0
- plum-core/src/ast.rs +3 -0
- plum-core/src/builtin_usage.rs +1 -0
- plum-core/src/parser.rs +10 -0
- plum-examples/try_operator.plum +58 -0
- plum-tooling/tree-sitter-plum/grammar.js +16 -1
- 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/try.txt +85 -0
- plum-wasm-codegen/src/lib.rs +75 -0
README.md
CHANGED
|
@@ -312,6 +312,17 @@ $ plum run plum-examples/io.plum
|
|
|
312
312
|
hello from plum
|
|
313
313
|
```
|
|
314
314
|
|
|
315
|
+
## Error propagation
|
|
316
|
+
|
|
317
|
+
`expr?` unwraps a `Result`'s `Ok` or an `Option`'s `Some`, or exits the enclosing function early with the `Err`/`None` value as-is otherwise — Rust's `?`:
|
|
318
|
+
|
|
319
|
+
```plum
|
|
320
|
+
fun sumTwo(a: Str, b: Str) -> Result[Int, Str] =
|
|
321
|
+
x := parsePositive(a)?
|
|
322
|
+
y := parsePositive(b)?
|
|
323
|
+
return Ok(x + y)
|
|
324
|
+
```
|
|
325
|
+
|
|
315
326
|
## Standard library highlights
|
|
316
327
|
|
|
317
328
|
- **`Bool`** (`import std/Bool`) — ordinary `enum Bool = | True | False`
|
plum-checker/src/lib.rs
CHANGED
|
@@ -1104,6 +1104,39 @@ pub(crate) fn lookupFieldType(class_name: &str, field_name: &str, ctx: &CheckCtx
|
|
|
1104
1104
|
}
|
|
1105
1105
|
}
|
|
1106
1106
|
|
|
1107
|
+
/// A mangled variant name's own unmangled base (`"Ok$Int$Str"` -> `"Ok"`,
|
|
1108
|
+
/// `"None"` -> `"None"`) — `monomorphize::mangle` always joins the original
|
|
1109
|
+
/// variant name and its resolved type args with `$`, so the base is simply
|
|
1110
|
+
/// everything before the first `$`.
|
|
1111
|
+
fn variantBaseName(name: &str) -> &str {
|
|
1112
|
+
name.split('$').next().unwrap_or(name)
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/// Finds the "success" (`Ok`/`Some`) and "failure" (`Err`/`None`) variants
|
|
1116
|
+
/// belonging to the same enum as `enum_name`, for the `?` operator (`Expr::Try`)
|
|
1117
|
+
/// — used identically by the checker (`inferExpr`) and codegen (`inferLocalType`
|
|
1118
|
+
/// callers), which is why this lives here rather than inline in either. Returns
|
|
1119
|
+
/// `None` if `enum_name` isn't (a monomorphized specialization of) `Result` or
|
|
1120
|
+
/// `Option` — i.e. doesn't have both shapes of variant.
|
|
1121
|
+
pub fn tryOperatorVariants<'a>(enum_name: &str, ctx: &'a CheckCtx) -> Option<(&'a str, &'a EnumVariantInfo, &'a str, &'a EnumVariantInfo)> {
|
|
1122
|
+
let mut success: Option<(&str, &EnumVariantInfo)> = None;
|
|
1123
|
+
let mut failure: Option<(&str, &EnumVariantInfo)> = None;
|
|
1124
|
+
for (vname, info) in ctx.enum_variants.iter() {
|
|
1125
|
+
if info.enum_name != enum_name {
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
match variantBaseName(vname) {
|
|
1129
|
+
"Ok" | "Some" => success = Some((vname.as_str(), info)),
|
|
1130
|
+
"Err" | "None" => failure = Some((vname.as_str(), info)),
|
|
1131
|
+
_ => {}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
match (success, failure) {
|
|
1135
|
+
(Some((sn, si)), Some((fn_, fi))) => Some((sn, si, fn_, fi)),
|
|
1136
|
+
_ => None,
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1107
1140
|
pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<PlumType, String> {
|
|
1108
1141
|
match expr {
|
|
1109
1142
|
ast::Expr::Int(_) => Ok(PlumType::TInt),
|
|
@@ -1134,6 +1167,25 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
1134
1167
|
},
|
|
1135
1168
|
},
|
|
1136
1169
|
ast::Expr::Paren(inner) => inferExpr(inner, env, ctx),
|
|
1170
|
+
// `expr?` — deliberately does NOT check that the enclosing function's
|
|
1171
|
+
// return type actually accepts the `Err`/`None` value this can exit
|
|
1172
|
+
// with early; that's left permissive here (codegen will catch a
|
|
1173
|
+
// genuine mismatch as a wasm type error), consistent with this
|
|
1174
|
+
// checker's existing "unmodeled/unvalidated, codegen catches it"
|
|
1175
|
+
// fallbacks elsewhere (e.g. `TypeName`'s unmodeled-type-name arm
|
|
1176
|
+
// above) — checking it properly would need the enclosing function's
|
|
1177
|
+
// declared return type threaded through every `inferExpr` call site,
|
|
1178
|
+
// for a case a wrong program still fails on, just less precisely.
|
|
1179
|
+
ast::Expr::Try(inner) => {
|
|
1180
|
+
let inner_ty = inferExpr(inner, env, ctx)?;
|
|
1181
|
+
match &inner_ty {
|
|
1182
|
+
PlumType::TNamed(name) => match tryOperatorVariants(name, ctx) {
|
|
1183
|
+
Some((_, success_info, _, _)) => Ok(success_info.field_types.first().cloned().unwrap_or(PlumType::TUnit)),
|
|
1184
|
+
None => Err(format!("'?' requires a Result or Option value, found '{}'", name)),
|
|
1185
|
+
},
|
|
1186
|
+
other => Err(format!("'?' requires a Result or Option value, found {}", other)),
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1137
1189
|
ast::Expr::Closure(cl) => {
|
|
1138
1190
|
let mut closure_env = env.clone();
|
|
1139
1191
|
let param_names: std::collections::HashSet<&str> = cl.params.iter().map(|s| s.as_str()).collect();
|
plum-checker/src/monomorphize.rs
CHANGED
|
@@ -294,6 +294,7 @@ fn substituteTypesInExpr(expr: &mut ast::Expr, subst: &Substitution) {
|
|
|
294
294
|
substituteTypesInExpr(&mut b.right, subst);
|
|
295
295
|
}
|
|
296
296
|
ast::Expr::Not(inner) => substituteTypesInExpr(inner, subst),
|
|
297
|
+
ast::Expr::Try(inner) => substituteTypesInExpr(inner, subst),
|
|
297
298
|
ast::Expr::Compare(c) => {
|
|
298
299
|
substituteTypesInExpr(&mut c.left, subst);
|
|
299
300
|
substituteTypesInExpr(&mut c.right, subst);
|
|
@@ -486,6 +487,7 @@ fn renameVarInExpr(expr: &mut ast::Expr, old: &str, new: &str) {
|
|
|
486
487
|
ast::Expr::Bool(b) => { renameVarInExpr(&mut b.left, old, new); renameVarInExpr(&mut b.right, old, new); }
|
|
487
488
|
ast::Expr::Compare(c) => { renameVarInExpr(&mut c.left, old, new); renameVarInExpr(&mut c.right, old, new); }
|
|
488
489
|
ast::Expr::Not(inner) => renameVarInExpr(inner, old, new),
|
|
490
|
+
ast::Expr::Try(inner) => renameVarInExpr(inner, old, new),
|
|
489
491
|
ast::Expr::Unary(u) => renameVarInExpr(&mut u.operand, old, new),
|
|
490
492
|
ast::Expr::Paren(inner) => renameVarInExpr(inner, old, new),
|
|
491
493
|
ast::Expr::Ternary(t) => {
|
|
@@ -2211,6 +2213,7 @@ impl<'a> Monomorphizer<'a> {
|
|
|
2211
2213
|
self.resolveBareVariantAgainstExpected(&mut c.right, <);
|
|
2212
2214
|
}
|
|
2213
2215
|
ast::Expr::Not(inner) => self.rewriteExpr(inner, env)?,
|
|
2216
|
+
ast::Expr::Try(inner) => self.rewriteExpr(inner, env)?,
|
|
2214
2217
|
ast::Expr::Unary(u) => self.rewriteExpr(&mut u.operand, env)?,
|
|
2215
2218
|
ast::Expr::Paren(inner) => self.rewriteExpr(inner, env)?,
|
|
2216
2219
|
ast::Expr::Ternary(t) => {
|
plum-core/src/ast.rs
CHANGED
|
@@ -335,6 +335,9 @@ pub enum Expr {
|
|
|
335
335
|
ClassCall(ClassCall),
|
|
336
336
|
/// `expr.field` or `expr.method(…)`
|
|
337
337
|
Attribute(Box<AttributeExpr>),
|
|
338
|
+
/// `expr?` — unwraps a `Result`'s `Ok`/an `Option`'s `Some`, or returns the
|
|
339
|
+
/// `Err`/`None` value from the enclosing function as-is otherwise.
|
|
340
|
+
Try(Box<Expr>),
|
|
338
341
|
/// `{expr}` — grouped/parenthesized expression
|
|
339
342
|
Paren(Box<Expr>),
|
|
340
343
|
String(StringExpr),
|
plum-core/src/builtin_usage.rs
CHANGED
|
@@ -188,6 +188,7 @@ fn walkExpr(expr: &Expr, names: &mut HashSet<String>) {
|
|
|
188
188
|
walkExpr(&b.right, names);
|
|
189
189
|
}
|
|
190
190
|
Expr::Not(e) => walkExpr(e, names),
|
|
191
|
+
Expr::Try(e) => walkExpr(e, names),
|
|
191
192
|
Expr::Compare(c) => {
|
|
192
193
|
walkExpr(&c.left, names);
|
|
193
194
|
walkExpr(&c.right, names);
|
plum-core/src/parser.rs
CHANGED
|
@@ -670,6 +670,7 @@ impl<'a> AstParser<'a> {
|
|
|
670
670
|
"binary_operator" => self.parseBinary(node),
|
|
671
671
|
"unary_operator" => self.parseUnary(node),
|
|
672
672
|
"attribute" => self.parseAttribute(node),
|
|
673
|
+
"try_expression" => self.parseTryExpression(node),
|
|
673
674
|
"fn_call" => Expr::FnCall(self.parseFnCall(node)),
|
|
674
675
|
"class_call" => Expr::ClassCall(self.parseClassCall(node)),
|
|
675
676
|
"parenthesized_expression" => {
|
|
@@ -802,6 +803,14 @@ impl<'a> AstParser<'a> {
|
|
|
802
803
|
Expr::Attribute(Box::new(AttributeExpr { object, attr }))
|
|
803
804
|
}
|
|
804
805
|
|
|
806
|
+
fn parseTryExpression(&self, node: Node) -> Expr {
|
|
807
|
+
// try_expression: primary_expression "?" — the sole named child is the value.
|
|
808
|
+
let value = node.child_by_field_name("value")
|
|
809
|
+
.map(|n| { let u = self.unwrapExprNode(n); self.parsePrimaryExpression(u) })
|
|
810
|
+
.unwrap_or(Expr::Int(0));
|
|
811
|
+
Expr::Try(Box::new(value))
|
|
812
|
+
}
|
|
813
|
+
|
|
805
814
|
fn parseFnCall(&self, node: Node) -> FnCall {
|
|
806
815
|
// fn_call: var_identifier fn_argument_list (the callee lexes as var_identifier
|
|
807
816
|
// to avoid an identifier-token tie with all-lowercase, no-underscore names)
|
|
@@ -1052,6 +1061,7 @@ fn isExpressionKind(kind: &str) -> bool {
|
|
|
1052
1061
|
| "comparison_operator"
|
|
1053
1062
|
| "ternary_expression"
|
|
1054
1063
|
| "attribute"
|
|
1064
|
+
| "try_expression"
|
|
1055
1065
|
| "fn_call"
|
|
1056
1066
|
| "class_call"
|
|
1057
1067
|
| "parenthesized_expression"
|
plum-examples/try_operator.plum
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import std/Result
|
|
2
|
+
import std/Option
|
|
3
|
+
import std/Bool
|
|
4
|
+
import std/Number
|
|
5
|
+
import std/Str
|
|
6
|
+
import std/List
|
|
7
|
+
|
|
8
|
+
# `expr?` unwraps a `Result`'s `Ok` or an `Option`'s `Some`, or exits the
|
|
9
|
+
# enclosing function early with the `Err`/`None` value as-is otherwise — same
|
|
10
|
+
# idea as Rust's `?`. The enclosing function's own return type must accept
|
|
11
|
+
# whatever gets returned early; that isn't checked until codegen (see
|
|
12
|
+
# `plum-checker`'s `inferExpr` on `Expr::Try`), so a mismatch there still
|
|
13
|
+
# surfaces as a clear compile error, just a less precise one.
|
|
14
|
+
|
|
15
|
+
fun parsePositive(s: Str) -> Result[Int, Str] =
|
|
16
|
+
n := parseInt(s)?
|
|
17
|
+
if n < 0
|
|
18
|
+
return Err("negative")
|
|
19
|
+
return Ok(n)
|
|
20
|
+
|
|
21
|
+
fun sumTwo(a: Str, b: Str) -> Result[Int, Str] =
|
|
22
|
+
x := parsePositive(a)?
|
|
23
|
+
y := parsePositive(b)?
|
|
24
|
+
return Ok(x + y)
|
|
25
|
+
|
|
26
|
+
fun firstPositive(list: List[Int]) -> Option[Int] =
|
|
27
|
+
match list.get(0)
|
|
28
|
+
Some(v) =>
|
|
29
|
+
if v > 0
|
|
30
|
+
return Some(v)
|
|
31
|
+
return None
|
|
32
|
+
None =>
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
fun doubledFirstPositive(list: List[Int]) -> Option[Int] =
|
|
36
|
+
a := firstPositive(list)?
|
|
37
|
+
return Some(a * 2)
|
|
38
|
+
|
|
39
|
+
test "try operator unwraps Ok and propagates the value through two calls"
|
|
40
|
+
r := sumTwo("2", "3")
|
|
41
|
+
assert r.isOk()
|
|
42
|
+
assert r.unwrap() == 5
|
|
43
|
+
|
|
44
|
+
test "try operator exits early with Err, skipping the rest of the function"
|
|
45
|
+
r := sumTwo("2", "-3")
|
|
46
|
+
assert r.isErr()
|
|
47
|
+
assert r.unwrapErr() == "negative"
|
|
48
|
+
|
|
49
|
+
test "try operator unwraps Some and propagates the value"
|
|
50
|
+
l := List(5)
|
|
51
|
+
r := doubledFirstPositive(l)
|
|
52
|
+
assert r.isSome()
|
|
53
|
+
assert r.unwrap() == 10
|
|
54
|
+
|
|
55
|
+
test "try operator exits early with None, skipping the rest of the function"
|
|
56
|
+
l := List[Int]()
|
|
57
|
+
r := doubledFirstPositive(l)
|
|
58
|
+
assert r.isNone()
|
plum-tooling/tree-sitter-plum/grammar.js
CHANGED
|
@@ -71,7 +71,14 @@ module.exports = grammar({
|
|
|
71
71
|
// Conflict arises at the empty-argument-list state (e.g. `Foo()`): the parser cannot
|
|
72
72
|
// distinguish fn_argument_list from class_argument_list until it sees content or ')'.
|
|
73
73
|
// Declaring the conflict at the argument-list level (not the call level) resolves this.
|
|
74
|
+
conflicts: ($) => [
|
|
74
|
-
|
|
75
|
+
[$.fn_argument_list, $.class_argument_list],
|
|
76
|
+
// `cmp ? sum : bits` (ternary) vs `cmp?` (try-operator postfix) both start
|
|
77
|
+
// with `primary_expression "?"` — genuinely ambiguous with only 1 token of
|
|
78
|
+
// lookahead (a `:` may or may not follow much later); GLR resolves it by
|
|
79
|
+
// trying both and keeping whichever completes.
|
|
80
|
+
[$.expression, $.try_expression],
|
|
81
|
+
],
|
|
75
82
|
inline: ($) => [$.generic_type],
|
|
76
83
|
rules: {
|
|
77
84
|
source: ($) =>
|
|
@@ -394,6 +401,7 @@ module.exports = grammar({
|
|
|
394
401
|
$.float,
|
|
395
402
|
$.unary_operator,
|
|
396
403
|
$.attribute,
|
|
404
|
+
$.try_expression,
|
|
397
405
|
$.fn_call,
|
|
398
406
|
$.class_call,
|
|
399
407
|
$.parenthesized_expression,
|
|
@@ -509,6 +517,13 @@ module.exports = grammar({
|
|
|
509
517
|
),
|
|
510
518
|
),
|
|
511
519
|
|
|
520
|
+
// Rust-style error-propagation postfix: `expr?` — unwraps a `Result`'s `Ok`
|
|
521
|
+
// or an `Option`'s `Some`, or exits the enclosing function early with the
|
|
522
|
+
// `Err`/`None` value as-is otherwise. Same postfix shape/precedence as
|
|
523
|
+
// `attribute` (`.`) just above.
|
|
524
|
+
try_expression: ($) =>
|
|
525
|
+
seq(field("value", $.primary_expression), "?"),
|
|
526
|
+
|
|
512
527
|
// The callee name lexes as `var_identifier` (widened to a superset of
|
|
513
528
|
// `fn_identifier`'s charset below) rather than `fn_identifier` — using two
|
|
514
529
|
// different identifier tokens here was ambiguous for any all-lowercase,
|
plum-tooling/tree-sitter-plum/queries/plum/highlights.scm
CHANGED
|
@@ -62,6 +62,8 @@
|
|
|
62
62
|
".."
|
|
63
63
|
] @operator
|
|
64
64
|
|
|
65
|
+
(try_expression "?" @operator)
|
|
66
|
+
|
|
65
67
|
[
|
|
66
68
|
"->"
|
|
67
69
|
"=>"
|
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/try.txt
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
================================================================================
|
|
2
|
+
try operator - postfix ? unwraps or returns early
|
|
3
|
+
================================================================================
|
|
4
|
+
|
|
5
|
+
fun parsePositive(s: Str) -> Result[Int, Str] =
|
|
6
|
+
n := parseInt(s)?
|
|
7
|
+
return Ok(n)
|
|
8
|
+
|
|
9
|
+
--------------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
(source
|
|
12
|
+
(fn
|
|
13
|
+
name: (fn_identifier)
|
|
14
|
+
params: (param
|
|
15
|
+
name: (var_identifier)
|
|
16
|
+
type: (type
|
|
17
|
+
(type_identifier)))
|
|
18
|
+
returns: (type
|
|
19
|
+
(type_identifier)
|
|
20
|
+
generics: (type
|
|
21
|
+
(type_identifier))
|
|
22
|
+
generics: (type
|
|
23
|
+
(type_identifier)))
|
|
24
|
+
body: (body
|
|
25
|
+
(assign
|
|
26
|
+
(var_identifier)
|
|
27
|
+
(expression
|
|
28
|
+
(primary_expression
|
|
29
|
+
(try_expression
|
|
30
|
+
value: (primary_expression
|
|
31
|
+
(fn_call
|
|
32
|
+
function: (var_identifier)
|
|
33
|
+
arguments: (fn_argument_list
|
|
34
|
+
(expression
|
|
35
|
+
(primary_expression
|
|
36
|
+
(var_identifier))))))))))
|
|
37
|
+
(return
|
|
38
|
+
(expression
|
|
39
|
+
(primary_expression
|
|
40
|
+
(fn_call
|
|
41
|
+
function: (type_identifier)
|
|
42
|
+
arguments: (fn_argument_list
|
|
43
|
+
(expression
|
|
44
|
+
(primary_expression
|
|
45
|
+
(var_identifier)))))))))))
|
|
46
|
+
|
|
47
|
+
================================================================================
|
|
48
|
+
try operator - coexists with the unrelated ternary operator, same `?` token
|
|
49
|
+
================================================================================
|
|
50
|
+
|
|
51
|
+
fun classify(n: Int) -> Int =
|
|
52
|
+
picked = n > 0 ? n : 0
|
|
53
|
+
picked
|
|
54
|
+
|
|
55
|
+
--------------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
(source
|
|
58
|
+
(fn
|
|
59
|
+
(fn_identifier)
|
|
60
|
+
(param
|
|
61
|
+
(var_identifier)
|
|
62
|
+
(type
|
|
63
|
+
(type_identifier)))
|
|
64
|
+
(type
|
|
65
|
+
(type_identifier))
|
|
66
|
+
(body
|
|
67
|
+
(assign
|
|
68
|
+
(var_identifier)
|
|
69
|
+
(expression
|
|
70
|
+
(ternary_expression
|
|
71
|
+
(expression
|
|
72
|
+
(comparison_operator
|
|
73
|
+
(primary_expression
|
|
74
|
+
(var_identifier))
|
|
75
|
+
(primary_expression
|
|
76
|
+
(integer))))
|
|
77
|
+
(expression
|
|
78
|
+
(primary_expression
|
|
79
|
+
(var_identifier)))
|
|
80
|
+
(expression
|
|
81
|
+
(primary_expression
|
|
82
|
+
(integer))))))
|
|
83
|
+
(expression
|
|
84
|
+
(primary_expression
|
|
85
|
+
(var_identifier))))))
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -511,6 +511,12 @@ fn withGcTypes<R>(f: impl FnOnce(&GcTypeRegistry) -> R) -> R {
|
|
|
511
511
|
fn classOrVariantTypeIdx(r: &GcTypeRegistry, name: &str) -> u32 {
|
|
512
512
|
*r.variant_type_idx.get(name)
|
|
513
513
|
.or_else(|| r.class_type_idx.get(name))
|
|
514
|
+
// A scratch slot narrowed to a MULTI-variant enum's own wide supertype
|
|
515
|
+
// (not any one variant) — the `?` operator's scratch local holds the
|
|
516
|
+
// subject at its natural (possibly multi-variant, e.g. `Result`/`Option`)
|
|
517
|
+
// type, never narrower, since it ref.casts to whichever of the two
|
|
518
|
+
// variants applies only at the point of use, not up front.
|
|
519
|
+
.or_else(|| r.enum_super_type_idx.get(name))
|
|
514
520
|
.unwrap_or_else(|| panic!("internal codegen error: '{}' missing from the GC type registry", name))
|
|
515
521
|
}
|
|
516
522
|
|
|
@@ -2145,6 +2151,7 @@ impl<'a, 'c> ClosureWalker<'a, 'c> {
|
|
|
2145
2151
|
ast::Expr::Bool(b) => { self.walkExpr(&b.left, None); self.walkExpr(&b.right, None); }
|
|
2146
2152
|
ast::Expr::Compare(c) => { self.walkExpr(&c.left, None); self.walkExpr(&c.right, None); }
|
|
2147
2153
|
ast::Expr::Not(inner) => self.walkExpr(inner, None),
|
|
2154
|
+
ast::Expr::Try(inner) => self.walkExpr(inner, None),
|
|
2148
2155
|
ast::Expr::Unary(u) => self.walkExpr(&u.operand, None),
|
|
2149
2156
|
ast::Expr::Paren(inner) => self.walkExpr(inner, None),
|
|
2150
2157
|
ast::Expr::Ternary(t) => {
|
|
@@ -2657,6 +2664,7 @@ fn fvCollectRefsExpr(
|
|
|
2657
2664
|
ast::Expr::Bool(b) => { fvCollectRefsExpr(&b.left, bound, seen, free, env, fn_decls); fvCollectRefsExpr(&b.right, bound, seen, free, env, fn_decls); }
|
|
2658
2665
|
ast::Expr::Compare(c) => { fvCollectRefsExpr(&c.left, bound, seen, free, env, fn_decls); fvCollectRefsExpr(&c.right, bound, seen, free, env, fn_decls); }
|
|
2659
2666
|
ast::Expr::Not(inner) => fvCollectRefsExpr(inner, bound, seen, free, env, fn_decls),
|
|
2667
|
+
ast::Expr::Try(inner) => fvCollectRefsExpr(inner, bound, seen, free, env, fn_decls),
|
|
2660
2668
|
ast::Expr::Unary(u) => fvCollectRefsExpr(&u.operand, bound, seen, free, env, fn_decls),
|
|
2661
2669
|
ast::Expr::Paren(inner) => fvCollectRefsExpr(inner, bound, seen, free, env, fn_decls),
|
|
2662
2670
|
ast::Expr::Ternary(t) => {
|
|
@@ -2892,6 +2900,29 @@ impl<'a> Collector<'a> {
|
|
|
2892
2900
|
self.walkExpr(&c.right);
|
|
2893
2901
|
}
|
|
2894
2902
|
ast::Expr::Not(inner) => self.walkExpr(inner),
|
|
2903
|
+
// `expr?` needs a scratch local to hold `inner`'s value across the
|
|
2904
|
+
// `ref.test` that decides Ok/Some vs Err/None (which consumes it)
|
|
2905
|
+
// and the `ref.cast` that reads it again inside whichever branch —
|
|
2906
|
+
// reuses the same pointer-identity-keyed scratch-slot mechanism
|
|
2907
|
+
// constructor patterns already use (`nested_class_scratch`), keyed
|
|
2908
|
+
// by this `Expr::Try` node itself. Declared with `inner`'s own
|
|
2909
|
+
// (possibly multi-variant, e.g. `Result`/`Option`) enum type —
|
|
2910
|
+
// `classOrVariantTypeIdx` falls back to `enum_super_type_idx` for
|
|
2911
|
+
// exactly this case — since narrowing happens later via `ref.cast`
|
|
2912
|
+
// in each branch, not by declaring this local pre-narrowed.
|
|
2913
|
+
ast::Expr::Try(inner) => {
|
|
2914
|
+
let key = expr as *const ast::Expr as usize;
|
|
2915
|
+
let ty = plum_checker::inferExpr(inner, &self.env, &self.cctx).unwrap_or(PlumType::TVar("_".to_string()));
|
|
2916
|
+
let name = match &ty {
|
|
2917
|
+
PlumType::TNamed(n) => n.clone(),
|
|
2918
|
+
_ => String::new(),
|
|
2919
|
+
};
|
|
2920
|
+
let slot = self.next_nested_class_slot;
|
|
2921
|
+
self.next_nested_class_slot += 1;
|
|
2922
|
+
self.nested_class_scratch.insert(key, slot);
|
|
2923
|
+
self.nested_class_scratch_types.push(name);
|
|
2924
|
+
self.walkExpr(inner);
|
|
2925
|
+
}
|
|
2895
2926
|
ast::Expr::Unary(u) => self.walkExpr(&u.operand),
|
|
2896
2927
|
ast::Expr::Paren(inner) => self.walkExpr(inner),
|
|
2897
2928
|
ast::Expr::Ternary(t) => {
|
|
@@ -4039,6 +4070,50 @@ fn compileExpr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut
|
|
|
4039
4070
|
ast::Expr::Paren(inner) => {
|
|
4040
4071
|
compileExpr(inner, body, ctx, state)?;
|
|
4041
4072
|
}
|
|
4073
|
+
// `expr?` — `ref.test` decides Ok/Some (fall through, extract the
|
|
4074
|
+
// field) vs Err/None (return the already-constructed value AS-IS,
|
|
4075
|
+
// no reconstruction needed). The `then`/`else` branches don't need to
|
|
4076
|
+
// agree in wasm's usual sense: after `return`, the rest of that
|
|
4077
|
+
// branch is unreachable, so the validator accepts the `if`'s single
|
|
4078
|
+
// declared result type (the success field's) coming only from the
|
|
4079
|
+
// `else` branch — the same stack-polymorphism-after-`return` rule
|
|
4080
|
+
// `compileStmt`'s own `Return` arm relies on, just reached from
|
|
4081
|
+
// inside an expression here for the first time.
|
|
4082
|
+
ast::Expr::Try(inner) => {
|
|
4083
|
+
let inner_ty = inferLocalType(inner, ctx);
|
|
4084
|
+
let enum_name = match &inner_ty {
|
|
4085
|
+
PlumType::TNamed(n) => n.clone(),
|
|
4086
|
+
other => return Err(format!("codegen: '?' requires a Result or Option value, found {}", other)),
|
|
4087
|
+
};
|
|
4088
|
+
let cctx = checkCtxOf(ctx.methods, ctx.enum_variants, ctx.enum_params, ctx.min_required);
|
|
4089
|
+
let (success_name, success_info, failure_name, _) = plum_checker::tryOperatorVariants(&enum_name, &cctx)
|
|
4090
|
+
.ok_or_else(|| format!("codegen: '?' requires a Result or Option value, found '{}'", enum_name))?;
|
|
4091
|
+
let success_idx = *ctx.gc_types.variant_type_idx.get(success_name)
|
|
4092
|
+
.ok_or_else(|| format!("codegen: variant '{}' missing from the GC type registry", success_name))?;
|
|
4093
|
+
let failure_idx = *ctx.gc_types.variant_type_idx.get(failure_name)
|
|
4094
|
+
.ok_or_else(|| format!("codegen: variant '{}' missing from the GC type registry", failure_name))?;
|
|
4095
|
+
let success_vt = plumTypeToValtype(&success_info.field_types.first().cloned().unwrap_or(PlumType::TUnit));
|
|
4096
|
+
|
|
4097
|
+
let key = expr as *const ast::Expr as usize;
|
|
4098
|
+
let slot = *ctx.nested_class_scratch.get(&key)
|
|
4099
|
+
.ok_or_else(|| "internal codegen error: missing '?' scratch slot".to_string())?;
|
|
4100
|
+
let scratch_local = ctx.nested_class_scratch_base + slot;
|
|
4101
|
+
|
|
4102
|
+
compileExpr(inner, body, ctx, state)?;
|
|
4103
|
+
Instruction::LocalTee(scratch_local).encode(body);
|
|
4104
|
+
Instruction::RefTestNonNull(HeapType::Concrete(failure_idx)).encode(body);
|
|
4105
|
+
Instruction::If(BlockType::Result(success_vt)).encode(body);
|
|
4106
|
+
Instruction::LocalGet(scratch_local).encode(body);
|
|
4107
|
+
Instruction::RefCastNonNull(HeapType::Concrete(failure_idx)).encode(body);
|
|
4108
|
+
Instruction::Return.encode(body);
|
|
4109
|
+
Instruction::Else.encode(body);
|
|
4110
|
+
Instruction::LocalGet(scratch_local).encode(body);
|
|
4111
|
+
Instruction::RefCastNonNull(HeapType::Concrete(success_idx)).encode(body);
|
|
4112
|
+
if !success_info.field_types.is_empty() {
|
|
4113
|
+
Instruction::StructGet { struct_type_index: success_idx, field_index: 0 }.encode(body);
|
|
4114
|
+
}
|
|
4115
|
+
Instruction::End.encode(body);
|
|
4116
|
+
}
|
|
4042
4117
|
ast::Expr::Unary(u) => match u.op {
|
|
4043
4118
|
ast::UnOp::Neg => {
|
|
4044
4119
|
if matches!(inferLocalType(&u.operand, ctx), PlumType::TFloat) {
|