Skip to content

Commit ebfb803

Browse files
Haofeiclaude
andcommitted
feat(lang): ? early-return on Option (tinygrad port item)
`?` now short-circuits on `Option` (keeps `Some(x)`, early-returns `None`) inside an `Option`-returning function, mirroring `?` on `Result` (keeps `Ok`, returns `Err`). Closes the "early return on none" gap from the tinygrad port feedback. - checker: allow `?` in `Option`-returning functions, and accept an `Option` operand (RS0013 now names both `Result` and `Option`); the Result error-type check is unaffected (it only fires for `Result`-returning functions). - reg VM: `TryResult` keeps `Some(x)` and short-circuits `None` (mirrors the `Err` path) in addition to the existing `Ok`/`Err`. - compiled backend: Rust's `?` already handles `Option`, so no lowering change. Works on owned operands (the common pattern — a call result), at exact parity with `Result`. `?` on a *borrowed* `read` param is a pre-existing limitation for both `Result` and `Option` (the borrowed `&T` isn't `Try`); out of scope here. Tests: pass fixture + VM/compiled parity (`parity_try_on_option_short_circuits_on_none`). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent cd3b63f commit ebfb803

5 files changed

Lines changed: 112 additions & 24 deletions

File tree

crates/rsscript/src/analyzer.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3459,10 +3459,12 @@ impl Analyzer<'_> {
34593459
let Item::Function(function) = item else {
34603460
continue;
34613461
};
3462+
// `?` short-circuits on the failure variant of the function's return
3463+
// type: `Err` for `Result`, `None` for `Option`. Both are permitted.
34623464
if function
34633465
.return_ty
34643466
.as_ref()
3465-
.is_some_and(|return_ty| return_ty.name == "Result")
3467+
.is_some_and(|return_ty| matches!(return_ty.name.as_str(), "Result" | "Option"))
34663468
{
34673469
continue;
34683470
}

crates/rsscript/src/checks/body.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4245,23 +4245,25 @@ fn check_try_value_is_result(analyzer: &mut Analyzer<'_>, value: &HirExpr, span:
42454245
let Some(type_name) = hir_expr_type_name(value) else {
42464246
return;
42474247
};
4248-
if is_result_type(type_name) {
4248+
// `?` applies to either failure-carrying type: `Result` (short-circuits `Err`)
4249+
// or `Option` (short-circuits `None`).
4250+
if is_result_type(type_name) || is_option_type(type_name) {
42494251
return;
42504252
}
42514253

42524254
analyzer.diagnostics.push(
42534255
Diagnostic::error(
42544256
code::INVALID_TRY_OPERATOR,
4255-
"`?` can only be applied to a Result value.",
4257+
"`?` can only be applied to a `Result` or `Option` value.",
42564258
span.clone(),
42574259
"invalid try operator",
42584260
)
42594261
.with_cause(format!(
4260-
"The expression before `?` has type `{type_name}`, not `Result<T, E>`."
4262+
"The expression before `?` has type `{type_name}`, not `Result<T, E>` or `Option<T>`."
42614263
))
42624264
.with_fix(
42634265
"remove_try_or_return_result",
4264-
"Remove `?`, or call an API that returns `Result<T, E>`.",
4266+
"Remove `?`, or call an API that returns `Result<T, E>` or `Option<T>`.",
42654267
"manual",
42664268
),
42674269
);
@@ -4482,6 +4484,10 @@ fn is_result_type(type_name: &str) -> bool {
44824484
type_name == "Result" || type_name.starts_with("Result<")
44834485
}
44844486

4487+
fn is_option_type(type_name: &str) -> bool {
4488+
type_name == "Option" || type_name.starts_with("Option<")
4489+
}
4490+
44854491
fn result_error_type_ref_name(return_ty: &TypeRef) -> Option<String> {
44864492
if return_ty.name != "Result" || return_ty.args.len() != 2 {
44874493
return None;

crates/rsscript/src/reg_vm/mod.rs

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7993,27 +7993,40 @@ impl RegVm {
79937993
self.set_reg(base + *dst, value);
79947994
}
79957995
RegInstr::TryResult { dst, src, cleanup } => {
7996-
let value = self.reg(base + *src);
7997-
let result = result_variant_payload(value)?;
7998-
match result {
7999-
Ok(value) => self.set_reg(base + *dst, value),
8000-
Err(error) => {
8001-
for resource in cleanup {
8002-
let value = self.reg(base + *resource).clone();
8003-
self.run_resource_drop(unit, value, next_base)?;
8004-
}
8005-
// `?` short-circuit: return the `Err` from the *current*
8006-
// frame only (pop one frame like `Return`), not out of the
8007-
// whole stackless driver.
8008-
let err_value = value_err(error);
8009-
let frame = self.frames.pop().expect("active frame");
8010-
self.apply_mut_writeback(&frame);
8011-
if self.frames.len() == floor {
8012-
return Ok(Outcome::Completed(err_value));
7996+
let value = self.reg(base + *src).clone();
7997+
// `?` keeps the success payload (`Ok(x)`/`Some(x)`) and
7998+
// short-circuits on failure (`Err(e)`/`None`), returning
7999+
// that failure from the current frame. Option support
8000+
// mirrors Result so `?` works in `Option`-returning fns.
8001+
let short_circuit = match &value {
8002+
VmValue::OptionSome(inner) => {
8003+
self.set_reg(base + *dst, (**inner).clone());
8004+
None
8005+
}
8006+
VmValue::OptionNone => Some(VmValue::OptionNone),
8007+
_ => match result_variant_payload(&value)? {
8008+
Ok(payload) => {
8009+
self.set_reg(base + *dst, payload);
8010+
None
80138011
}
8014-
self.set_reg(frame.ret_dst, err_value);
8015-
continue 'frames;
8012+
Err(error) => Some(value_err(error)),
8013+
},
8014+
};
8015+
if let Some(return_value) = short_circuit {
8016+
for resource in cleanup {
8017+
let resource_value = self.reg(base + *resource).clone();
8018+
self.run_resource_drop(unit, resource_value, next_base)?;
8019+
}
8020+
// Short-circuit: return the failure from the *current*
8021+
// frame only (pop one frame like `Return`), not out of
8022+
// the whole stackless driver.
8023+
let frame = self.frames.pop().expect("active frame");
8024+
self.apply_mut_writeback(&frame);
8025+
if self.frames.len() == floor {
8026+
return Ok(Outcome::Completed(return_value));
80168027
}
8028+
self.set_reg(frame.ret_dst, return_value);
8029+
continue 'frames;
80178030
}
80188031
}
80198032
// `Return` and the rest of the pure subset are handled above by
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// `?` on an `Option` short-circuits on `None` (early-return) inside an
2+
// `Option`-returning function, mirroring `?` on `Result`.
3+
fn make(n: Int) -> Option<Int> {
4+
if n > 0 {
5+
return Some(n)
6+
}
7+
return None
8+
}
9+
10+
fn checked_double(n: Int) -> Option<Int> {
11+
let v = make(n: n)?
12+
return Some(v + v)
13+
}
14+
15+
fn main() -> Unit {
16+
match checked_double(n: 21) {
17+
Some(x) => { Log.write(message: read String.from_int(value: x)) }
18+
None => { Log.write(message: read "none") }
19+
}
20+
match checked_double(n: 0) {
21+
Some(x) => { Log.write(message: read String.from_int(value: x)) }
22+
None => { Log.write(message: read "none") }
23+
}
24+
return Unit
25+
}

crates/rsscript/tests/vm_eval_parity/misc.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,48 @@ fn main() -> Unit {
11011101
);
11021102
}
11031103

1104+
#[test]
1105+
fn parity_try_on_option_short_circuits_on_none() {
1106+
// `?` on an `Option` keeps `Some(x)` and early-returns `None`, matching `?`
1107+
// on `Result`. Must behave identically on the VM and the compiled backend.
1108+
let source = r#"
1109+
fn make(n: Int) -> Option<Int> {
1110+
if n > 0 {
1111+
return Some(n)
1112+
}
1113+
return None
1114+
}
1115+
1116+
fn doubled(n: Int) -> Option<Int> {
1117+
let v = make(n: n)?
1118+
return Some(v + v)
1119+
}
1120+
1121+
fn show(label: read String, value: read Option<Int>) -> Unit {
1122+
match value {
1123+
Some(x) => {
1124+
Log.write(message: read String.concat(left: read label, right: read String.from_int(value: read x)))
1125+
}
1126+
None => {
1127+
Log.write(message: read String.concat(left: read label, right: read "none"))
1128+
}
1129+
}
1130+
return Unit
1131+
}
1132+
1133+
fn main() -> Unit {
1134+
show(label: read "pos: ", value: read doubled(n: 21))
1135+
show(label: read "zero: ", value: read doubled(n: 0))
1136+
return Unit
1137+
}
1138+
"#;
1139+
common::assert_vm_eval_matches_backend(
1140+
"parity-try-option.rss",
1141+
"rsscript_parity_try_option",
1142+
source,
1143+
);
1144+
}
1145+
11041146
#[test]
11051147
fn parity_boolean_operators_short_circuit() {
11061148
let source = r#"

0 commit comments

Comments
 (0)