Lower `move` closures by snapshotting their captures
A `move` closure captures by value; Nim's closures capture by reference. That
was grounds for refusing them, but the difference is bridgeable: rebind each
captured variable in a scope of its own and let the closure capture *that*.
(block:
var n = n
proc (): int = (n += 1; n))
The snapshot is taken where the closure is written, the closure's own
mutations persist across calls, and a later assignment to the original is
invisible to it. That is what `move` means, checked against rustc rather than
assumed. `var` and not `let`, because an `FnMut` mutates its captures.
`thread::spawn` already did this the long way round, with an explicit state
object; that path is untouched, since a thread's captures have to outlive the
scope in a way a closure's do not.
In `map_closure` the closure is inlined at the call site, so it cannot outlive
the expression and by-value capture cannot be told from by-reference there.
tests/cases/904 pinned the rejection and is deleted -- the second such case to
go this session, after the `i128` one. A rejection test is worth keeping only
while its rejection is true.
tests/cases/059.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>11fe65c parent: 8204755 modified
DESIGN.md +5 -2 | @@ -1144,8 +1144,11 @@ Cases carry directives in leading `//@` comments: | ||
| 1144 | 1144 | | `//@ extern: <crate>` | the **oracle** links this crate; rustnim does not get it | |
| 1145 | 1145 | |
| 1146 | 1146 | `reject` cases are how the "fail loudly" rule is tested rather than merely |
| 1147 | -stated: `901`–`904` pin the rejections of an unmapped standard-library method, | |
| 1148 | -a float→int cast, an unimplemented format spec, and a closure. (`900` pinned | |
| 1147 | +stated: `901`–`903` pin the rejections of an unmapped standard-library method, | |
| 1148 | +a float→int cast and an unimplemented format spec. (`900` pinned the `i128` | |
| 1149 | +rejection and `904` the `move`-closure one; both were deleted when those | |
| 1150 | +stopped being true. A rejection test is worth keeping only while its rejection | |
| 1151 | +is.) (`900` pinned | |
| 1149 | 1152 | the `i128` rejection and was deleted when `i128` became implementable — a |
| 1150 | 1153 | rejection test is only worth keeping while the rejection is still true.) |
| 1151 | 1154 | |
| @@ -1144,8 +1144,11 @@ Cases carry directives in leading `//@` comments: | |||
| 1144 | | `//@ extern: <crate>` | the **oracle** links this crate; rustnim does not get it | | 1144 | | `//@ extern: <crate>` | the **oracle** links this crate; rustnim does not get it | |
| 1145 | 1145 | ||
| 1146 | `reject` cases are how the "fail loudly" rule is tested rather than merely | 1146 | `reject` cases are how the "fail loudly" rule is tested rather than merely |
| 1147 | -stated: `901`–`904` pin the rejections of an unmapped standard-library method, | 1147 | +stated: `901`–`903` pin the rejections of an unmapped standard-library method, |
| 1148 | -a float→int cast, an unimplemented format spec, and a closure. (`900` pinned | 1148 | +a float→int cast and an unimplemented format spec. (`900` pinned the `i128` |
| 1149 | +rejection and `904` the `move`-closure one; both were deleted when those | ||
| 1150 | +stopped being true. A rejection test is worth keeping only while its rejection | ||
| 1151 | +is.) (`900` pinned | ||
| 1149 | the `i128` rejection and was deleted when `i128` became implementable — a | 1152 | the `i128` rejection and was deleted when `i128` became implementable — a |
| 1150 | rejection test is only worth keeping while the rejection is still true.) | 1153 | rejection test is only worth keeping while the rejection is still true.) |
| 1151 | 1154 | ||
modified
src/lower.rs +45 -14 | @@ -5666,11 +5666,9 @@ impl Lowerer { | ||
| 5666 | 5666 | targs: &[Nim], |
| 5667 | 5667 | c: &syn::ExprClosure, |
| 5668 | 5668 | ) -> Result<Val, String> { |
| 5669 | - if c.capture.is_some() { | |
| 5670 | - return Err("a `move` closure captures by value; Nim's closures \ | |
| 5671 | - capture by reference, and the two are not the same" | |
| 5672 | - .into()); | |
| 5673 | - } | |
| 5669 | + // Inlined at the call site, so the closure does not outlive the | |
| 5670 | + // expression and by-value capture cannot be told from by-reference. | |
| 5671 | + | |
| 5674 | 5672 | if c.inputs.len() != 1 { |
| 5675 | 5673 | return Err(format!("`.{what}()` takes a one-argument closure")); |
| 5676 | 5674 | } |
| @@ -5757,11 +5755,35 @@ impl Lowerer { | ||
| 5757 | 5755 | /// A `move` closure captures by value, which is a different thing, so it |
| 5758 | 5756 | /// is rejected rather than lowered to the same construct. |
| 5759 | 5757 | fn closure(&mut self, c: &syn::ExprClosure, expect: Option<&Nim>) -> Result<Val, String> { |
| 5760 | - if c.capture.is_some() { | |
| 5761 | - return Err("a `move` closure captures by value; Nim's closures \ | |
| 5762 | - capture by reference, and the two are not the same" | |
| 5763 | - .into()); | |
| 5764 | - } | |
| 5758 | + // A `move` closure captures by value. Nim's closures capture by | |
| 5759 | + // reference, so the copy is made explicitly: each captured variable is | |
| 5760 | + // rebound in a scope of its own, and it is that rebinding the closure | |
| 5761 | + // captures. The snapshot is taken where the closure is written, the | |
| 5762 | + // closure's own mutations persist across calls, and a later assignment | |
| 5763 | + // to the original is invisible to it -- which is what `move` means. | |
| 5764 | + // | |
| 5765 | + // `var`, not `let`: an `FnMut` mutates its captures. | |
| 5766 | + let moved: Vec<(String, Nim)> = if c.capture.is_some() { | |
| 5767 | + let mut names = Vec::new(); | |
| 5768 | + collect_idents(&c.body, &mut names); | |
| 5769 | + let mut caps: Vec<(String, Nim)> = Vec::new(); | |
| 5770 | + for n in names { | |
| 5771 | + if caps.iter().any(|(m, _)| *m == n) { | |
| 5772 | + continue; | |
| 5773 | + } | |
| 5774 | + // Only what is bound out here: a parameter of the closure, or | |
| 5775 | + // anything it declares itself, is not a capture. | |
| 5776 | + if c.inputs.iter().any(|p| matches!(p, Pat::Ident(i) if i.ident == n)) { | |
| 5777 | + continue; | |
| 5778 | + } | |
| 5779 | + if let Some(t) = self.lookup(&n) { | |
| 5780 | + caps.push((n, t)); | |
| 5781 | + } | |
| 5782 | + } | |
| 5783 | + caps | |
| 5784 | + } else { | |
| 5785 | + Vec::new() | |
| 5786 | + }; | |
| 5765 | 5787 | let want: Option<&Vec<Nim>> = match expect { |
| 5766 | 5788 | Some(Nim::Proc(a, _)) => Some(a), |
| 5767 | 5789 | _ => None, |
| @@ -5808,10 +5830,19 @@ impl Lowerer { | ||
| 5808 | 5830 | let ret = ret_ann |
| 5809 | 5831 | .or_else(|| v.ty.clone()) |
| 5810 | 5832 | .ok_or("cannot infer a closure's return type; annotate it")?; |
| 5811 | - Ok(Val::new( | |
| 5812 | - format!("(proc ({}): {} = {})", parts.join(", "), ret.render(), v.code), | |
| 5813 | - Some(Nim::Proc(ptys, Box::new(ret))), | |
| 5814 | - )) | |
| 5833 | + let proc_code = format!("(proc ({}): {} = {})", parts.join(", "), ret.render(), v.code); | |
| 5834 | + let code = if moved.is_empty() { | |
| 5835 | + proc_code | |
| 5836 | + } else { | |
| 5837 | + let copies: Vec<String> = moved | |
| 5838 | + .iter() | |
| 5839 | + .map(|(n, _)| format!("var {0} = {0}", ident(n))) | |
| 5840 | + .collect(); | |
| 5841 | + format!("(block:\n{}\n {})", | |
| 5842 | + copies.iter().map(|c| format!(" {c}")).collect::<Vec<_>>().join("\n"), | |
| 5843 | + proc_code) | |
| 5844 | + }; | |
| 5845 | + Ok(Val::new(code, Some(Nim::Proc(ptys, Box::new(ret))))) | |
| 5815 | 5846 | } |
| 5816 | 5847 | |
| 5817 | 5848 | /// Lower a block's statements at the current indentation, without opening |
| @@ -5666,11 +5666,9 @@ impl Lowerer { | |||
| 5666 | targs: &[Nim], | 5666 | targs: &[Nim], |
| 5667 | c: &syn::ExprClosure, | 5667 | c: &syn::ExprClosure, |
| 5668 | ) -> Result<Val, String> { | 5668 | ) -> Result<Val, String> { |
| 5669 | - if c.capture.is_some() { | 5669 | + // Inlined at the call site, so the closure does not outlive the |
| 5670 | - return Err("a `move` closure captures by value; Nim's closures \ | 5670 | + // expression and by-value capture cannot be told from by-reference. |
| 5671 | - capture by reference, and the two are not the same" | 5671 | + |
| 5672 | - .into()); | ||
| 5673 | - } | ||
| 5674 | if c.inputs.len() != 1 { | 5672 | if c.inputs.len() != 1 { |
| 5675 | return Err(format!("`.{what}()` takes a one-argument closure")); | 5673 | return Err(format!("`.{what}()` takes a one-argument closure")); |
| 5676 | } | 5674 | } |
| @@ -5757,11 +5755,35 @@ impl Lowerer { | |||
| 5757 | /// A `move` closure captures by value, which is a different thing, so it | 5755 | /// A `move` closure captures by value, which is a different thing, so it |
| 5758 | /// is rejected rather than lowered to the same construct. | 5756 | /// is rejected rather than lowered to the same construct. |
| 5759 | fn closure(&mut self, c: &syn::ExprClosure, expect: Option<&Nim>) -> Result<Val, String> { | 5757 | fn closure(&mut self, c: &syn::ExprClosure, expect: Option<&Nim>) -> Result<Val, String> { |
| 5760 | - if c.capture.is_some() { | 5758 | + // A `move` closure captures by value. Nim's closures capture by |
| 5761 | - return Err("a `move` closure captures by value; Nim's closures \ | 5759 | + // reference, so the copy is made explicitly: each captured variable is |
| 5762 | - capture by reference, and the two are not the same" | 5760 | + // rebound in a scope of its own, and it is that rebinding the closure |
| 5763 | - .into()); | 5761 | + // captures. The snapshot is taken where the closure is written, the |
| 5764 | - } | 5762 | + // closure's own mutations persist across calls, and a later assignment |
| 5763 | + // to the original is invisible to it -- which is what `move` means. | ||
| 5764 | + // | ||
| 5765 | + // `var`, not `let`: an `FnMut` mutates its captures. | ||
| 5766 | + let moved: Vec<(String, Nim)> = if c.capture.is_some() { | ||
| 5767 | + let mut names = Vec::new(); | ||
| 5768 | + collect_idents(&c.body, &mut names); | ||
| 5769 | + let mut caps: Vec<(String, Nim)> = Vec::new(); | ||
| 5770 | + for n in names { | ||
| 5771 | + if caps.iter().any(|(m, _)| *m == n) { | ||
| 5772 | + continue; | ||
| 5773 | + } | ||
| 5774 | + // Only what is bound out here: a parameter of the closure, or | ||
| 5775 | + // anything it declares itself, is not a capture. | ||
| 5776 | + if c.inputs.iter().any(|p| matches!(p, Pat::Ident(i) if i.ident == n)) { | ||
| 5777 | + continue; | ||
| 5778 | + } | ||
| 5779 | + if let Some(t) = self.lookup(&n) { | ||
| 5780 | + caps.push((n, t)); | ||
| 5781 | + } | ||
| 5782 | + } | ||
| 5783 | + caps | ||
| 5784 | + } else { | ||
| 5785 | + Vec::new() | ||
| 5786 | + }; | ||
| 5765 | let want: Option<&Vec<Nim>> = match expect { | 5787 | let want: Option<&Vec<Nim>> = match expect { |
| 5766 | Some(Nim::Proc(a, _)) => Some(a), | 5788 | Some(Nim::Proc(a, _)) => Some(a), |
| 5767 | _ => None, | 5789 | _ => None, |
| @@ -5808,10 +5830,19 @@ impl Lowerer { | |||
| 5808 | let ret = ret_ann | 5830 | let ret = ret_ann |
| 5809 | .or_else(|| v.ty.clone()) | 5831 | .or_else(|| v.ty.clone()) |
| 5810 | .ok_or("cannot infer a closure's return type; annotate it")?; | 5832 | .ok_or("cannot infer a closure's return type; annotate it")?; |
| 5811 | - Ok(Val::new( | 5833 | + let proc_code = format!("(proc ({}): {} = {})", parts.join(", "), ret.render(), v.code); |
| 5812 | - format!("(proc ({}): {} = {})", parts.join(", "), ret.render(), v.code), | 5834 | + let code = if moved.is_empty() { |
| 5813 | - Some(Nim::Proc(ptys, Box::new(ret))), | 5835 | + proc_code |
| 5814 | - )) | 5836 | + } else { |
| 5837 | + let copies: Vec<String> = moved | ||
| 5838 | + .iter() | ||
| 5839 | + .map(|(n, _)| format!("var {0} = {0}", ident(n))) | ||
| 5840 | + .collect(); | ||
| 5841 | + format!("(block:\n{}\n {})", | ||
| 5842 | + copies.iter().map(|c| format!(" {c}")).collect::<Vec<_>>().join("\n"), | ||
| 5843 | + proc_code) | ||
| 5844 | + }; | ||
| 5845 | + Ok(Val::new(code, Some(Nim::Proc(ptys, Box::new(ret))))) | ||
| 5815 | } | 5846 | } |
| 5816 | 5847 | ||
| 5817 | /// Lower a block's statements at the current indentation, without opening | 5848 | /// Lower a block's statements at the current indentation, without opening |
added
tests/cases/059-move-closure.rs +44 -0 | new file mode 100644 | ||
| @@ -0,0 +1,44 @@ | ||
| 1 | +// A `move` closure captures by value. Nim's closures capture by reference, so | |
| 2 | +// the copy is made explicitly and it is the copy the closure captures: the | |
| 3 | +// snapshot is taken where the closure is written, and a later assignment to | |
| 4 | +// the original is invisible to it. | |
| 5 | +// | |
| 6 | +// (Closure bodies with statements are a separate gap, so these are all single | |
| 7 | +// expressions.) | |
| 8 | +fn adder(base: i32) -> impl Fn(i32) -> i32 { | |
| 9 | + move |x| x + base | |
| 10 | +} | |
| 11 | + | |
| 12 | +fn scaled(factor: i32) -> impl Fn(i32) -> i32 { | |
| 13 | + let doubled = factor * 2; | |
| 14 | + move |x| x * doubled | |
| 15 | +} | |
| 16 | + | |
| 17 | +fn apply(f: impl Fn(i32) -> i32, v: i32) -> i32 { | |
| 18 | + f(v) | |
| 19 | +} | |
| 20 | + | |
| 21 | +// What `904-reject-move-closure` used to assert was impossible. | |
| 22 | +fn borrowed_len() -> impl Fn() -> usize { | |
| 23 | + let v: Vec<i32> = vec![1, 2, 3]; | |
| 24 | + move || v.len() | |
| 25 | +} | |
| 26 | + | |
| 27 | +fn main() { | |
| 28 | + let bl = borrowed_len(); | |
| 29 | + println!("{}", bl()); | |
| 30 | + | |
| 31 | + let add7 = adder(7); | |
| 32 | + println!("{} {}", add7(1), add7(35)); | |
| 33 | + | |
| 34 | + let s = scaled(3); | |
| 35 | + println!("{} {}", s(10), s(4)); | |
| 36 | + | |
| 37 | + // The snapshot is taken where the closure is written. | |
| 38 | + let mut base = 1; | |
| 39 | + let snap = move |x: i32| x + base; | |
| 40 | + base = 1000; | |
| 41 | + println!("{} {}", snap(5), base); | |
| 42 | + | |
| 43 | + println!("{}", apply(adder(2), 40)); | |
| 44 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,44 @@ | |||
| 1 | +// A `move` closure captures by value. Nim's closures capture by reference, so | ||
| 2 | +// the copy is made explicitly and it is the copy the closure captures: the | ||
| 3 | +// snapshot is taken where the closure is written, and a later assignment to | ||
| 4 | +// the original is invisible to it. | ||
| 5 | +// | ||
| 6 | +// (Closure bodies with statements are a separate gap, so these are all single | ||
| 7 | +// expressions.) | ||
| 8 | +fn adder(base: i32) -> impl Fn(i32) -> i32 { | ||
| 9 | + move |x| x + base | ||
| 10 | +} | ||
| 11 | + | ||
| 12 | +fn scaled(factor: i32) -> impl Fn(i32) -> i32 { | ||
| 13 | + let doubled = factor * 2; | ||
| 14 | + move |x| x * doubled | ||
| 15 | +} | ||
| 16 | + | ||
| 17 | +fn apply(f: impl Fn(i32) -> i32, v: i32) -> i32 { | ||
| 18 | + f(v) | ||
| 19 | +} | ||
| 20 | + | ||
| 21 | +// What `904-reject-move-closure` used to assert was impossible. | ||
| 22 | +fn borrowed_len() -> impl Fn() -> usize { | ||
| 23 | + let v: Vec<i32> = vec![1, 2, 3]; | ||
| 24 | + move || v.len() | ||
| 25 | +} | ||
| 26 | + | ||
| 27 | +fn main() { | ||
| 28 | + let bl = borrowed_len(); | ||
| 29 | + println!("{}", bl()); | ||
| 30 | + | ||
| 31 | + let add7 = adder(7); | ||
| 32 | + println!("{} {}", add7(1), add7(35)); | ||
| 33 | + | ||
| 34 | + let s = scaled(3); | ||
| 35 | + println!("{} {}", s(10), s(4)); | ||
| 36 | + | ||
| 37 | + // The snapshot is taken where the closure is written. | ||
| 38 | + let mut base = 1; | ||
| 39 | + let snap = move |x: i32| x + base; | ||
| 40 | + base = 1000; | ||
| 41 | + println!("{} {}", snap(5), base); | ||
| 42 | + | ||
| 43 | + println!("{}", apply(adder(2), 40)); | ||
| 44 | +} | ||
deleted
tests/cases/904-reject-move-closure.rs +0 -9 | deleted file mode 100644 | ||
| @@ -1,9 +0,0 @@ | ||
| 1 | -//@ reject: captures by value | |
| 2 | -// Nim's closures capture by reference, as Rust's non-`move` closures do. A | |
| 3 | -// `move` closure is a different thing, so it is not lowered to the same | |
| 4 | -// construct. | |
| 5 | -fn main() { | |
| 6 | - let v: Vec<i32> = vec![1, 2, 3]; | |
| 7 | - let f = move || v.len(); | |
| 8 | - println!("{}", f()); | |
| 9 | -} | |
| deleted file mode 100644 | |||
| @@ -1,9 +0,0 @@ | |||
| 1 | -//@ reject: captures by value | ||
| 2 | -// Nim's closures capture by reference, as Rust's non-`move` closures do. A | ||
| 3 | -// `move` closure is a different thing, so it is not lowered to the same | ||
| 4 | -// construct. | ||
| 5 | -fn main() { | ||
| 6 | - let v: Vec<i32> = vec![1, 2, 3]; | ||
| 7 | - let f = move || v.len(); | ||
| 8 | - println!("{}", f()); | ||
| 9 | -} | ||