mirror of
https://github.com/horizon-rust/horizon-mixins.git
synced 2026-11-07 10:05:58 -10:00
feat(plan): let return hooks move plain closures inside exit values
This commit is contained in:
5 files changed
+49
-13
No files matched your search
@@ -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");
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user