feat(plan): let return hooks move plain closures inside exit values

This commit is contained in:
reservedbytes committed 2026-10-31 08:44:58 -01:00
1 parent 4342a0bfbe
commit adc34e5be0
5 files changed
+49 -13

No files matched your search

+1 -1
View File
@@ -80,7 +80,7 @@ zero-based `ordinal`. It supports a complete expression statement, plain `let`
initializer, or block/function tail. Calls nested in larger expressions are
rejected because wrapping them can change temporary drop timing. For BEFORE_CALL, macro
sites, unresolved cfg, async/closures/`?`, generic/unsafe/const targets, and
ambiguous source identities are rejected (RETURN allows closures and references to
ambiguous source identities are rejected (RETURN allows plain closures, and references to
calls outside the values it returns, and MODIFY_VARIABLE closures, `?` and references
to calls outside the value it modifies). Rust checks privacy, types and borrowing in
`check`; successful planning alone does not guarantee compilation.
@@ -85,8 +85,6 @@ fn return_hooks_still_refuse_references_to_calls_and_closures_in_the_values_they
"if early { return *&sink(1); } sink(2)",
"if early { return sink(1); } *&sink(2)",
"if early { return sink(1); } { let x = &Guard::new(\"tail\", 2); size(x) }",
"if early { return sink(1); } (|| sink(2))()",
"if early { return (|| sink(1))(); } sink(2)",
] {
let f = fixture(body);
let e = build_plan(&f.root, &f.mods).unwrap_err();
@@ -95,10 +93,12 @@ fn return_hooks_still_refuse_references_to_calls_and_closures_in_the_values_they
assert!(e.message.contains("beneath a reference"), "{body}: {e}");
}
}
// What a closure holds is still checked: `?` and async stay refused.
// What a closure holds is still checked, also in an exit value: `?` and async stay refused.
for body in [
"let f = || -> Option<i32> { Some(1)?; None }; sink(f().unwrap_or(2))",
"let f = async || 1; drop(f); sink(2)",
"if early { return (|| -> Option<i32> { Some(1)?; None })().unwrap_or(1); } sink(2)",
"if early { return sink(1); } { let f = async || 1; drop(f); sink(2) }",
] {
let f = fixture(body);
let e = build_plan(&f.root, &f.mods).unwrap_err();
@@ -168,3 +168,35 @@ fn the_outer_local_is_captured() {
.unwrap();
f.run("check");
}
#[test]
fn return_hooks_move_closures_in_exit_values_with_the_value() {
// A closure in a `return` operand and in the tail moves into the helper's argument with the
// value. The first one's own `return` leaves the closure and is not an exit of `run`.
let f = fixture(
r#"
let base = sink(2);
if early { return [1, 2].iter().map(|n| { if *n > 1 { return n * 100; } n + base }).sum(); }
mark("body");
[3, 4].map(|n| n * base).iter().copied().sum::<i32>() + (|| sink(1))()
"#,
);
fs::write(
f.root.join("driver/src/lib.rs"),
r#"
#[test]
fn the_helper_sees_each_exit_value_once() {
use fixture_game::{run, LOG};
// 1 + 2, then the closure's own `return` 200: 203, plus the helper's 1.
assert_eq!(run(true), 204);
assert_eq!(*LOG.lock().unwrap(), ["helper"]);
LOG.lock().unwrap().clear();
// 6 + 8 + 1, plus the helper's 1.
assert_eq!(run(false), 16);
assert_eq!(*LOG.lock().unwrap(), ["body", "helper"]);
}
"#,
)
.unwrap();
f.run("check");
}
+4 -4
View File
@@ -972,9 +972,10 @@ const CLOSURES_ETC: (&str, &str) = (
"closures, async, try/?, unsafe and const blocks are unsupported injection contexts",
);
/// What RETURN refuses in the values it moves into a helper argument: each `return` operand
/// and the body's tail. A call beneath a reference there could change temporary lifetimes, and
/// a closure there is refused as before. Closures elsewhere are left alone: a `return` inside
/// one leaves the closure, not the target, and RETURN does not rewrite it.
/// and the body's tail. A call beneath a reference there could change temporary lifetimes. A
/// plain closure there moves into the argument with the value, and what it holds is not part of
/// that value: a `return` inside it leaves the closure, and RETURN does not rewrite it. The
/// whole-body check still refuses async closures and `?` or unsafe inside any closure.
fn exit_value_error(block: &syn::Block) -> Option<(&'static str, &'static str)> {
struct Find {
exits: usize,
@@ -983,7 +984,6 @@ fn exit_value_error(block: &syn::Block) -> Option<(&'static str, &'static str)>
impl<'ast> Visit<'ast> for Find {
fn visit_expr(&mut self, e: &'ast Expr) {
match e {
Expr::Closure(_) if self.exits > 0 => self.error = Some(CLOSURES_ETC),
Expr::Closure(_) => return,
Expr::Return(r) => {
self.exits += 1;
+6 -2
View File
@@ -33,7 +33,8 @@ macros elsewhere are preserved (see the macro section).
A RETURN modifier moves only the exit values (each `return` operand and the
function tail) into its helper's argument; the rest of the body is untouched.
So, elsewhere in the body, RETURN allows:
So, elsewhere in the body, RETURN allows the first of these, and anywhere,
the second:
- a call beneath a reference, such as `decode(Some(&Tag::Compound(all)))` in a
statement or `let x = &make();` whose temporary is extended: it keeps its
@@ -44,7 +45,10 @@ So, elsewhere in the body, RETURN allows:
refused. Locals declared inside a closure do not count toward capture ordinals:
`$local_ref:later:0` is the first `later` of the function itself.
Inside an exit value both stay `E_UNSUPPORTED_CONTROL_FLOW`. A target that also
A plain closure inside an exit value, such as the tail
`xs.iter().map(|x| x * 2).sum()`, moves into the helper's argument with the
value; its own `return` is still not an exit. A call beneath a reference inside
an exit value stays `E_UNSUPPORTED_CONTROL_FLOW`. A target that also
has an interior hook (BEFORE_CALL, AFTER_CALL, value and variable modifiers)
keeps the check over the whole body for that hook, since it rewrites there.
+3 -3
View File
@@ -89,9 +89,9 @@ BEFORE_CALL supports complete expression statements, plain identifier `let`
initializers and block/function tails. Other placements fail with
`E_UNSUPPORTED_SITE`. Functions containing opaque macros, unresolved cfg,
closures, async, await, `?`/try, unsafe/const blocks, nested items or calls under
references are outside the proven MVP subset. RETURN is narrower: it refuses
closures and calls under references only inside the values it moves (each
`return` operand and the function tail); see
references are outside the proven MVP subset. RETURN is narrower: it allows
plain closures, and refuses calls under references only inside the values it
moves (each `return` operand and the function tail); see
[advanced injections](advanced-injections.md). MODIFY_VARIABLE refuses closures, `?`
and calls under references only inside the value it modifies; see
[scopes and bridges](scopes-and-bridges.md). Generic, const, unsafe and