nandi/rustnimpublic Fork
⑂ 11fe65c
Commits
⬇ Clone ▾
git clone https://git.rickub.com/nandi/rustnim.git
git clone ssh://git@rickub.com/nandi/rustnim.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

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>
nandithebull committed 2026-09-19T19:09:06-07:00 Browse files
11fe65c parent: 8204755
modified DESIGN.md +5 -2
@@ -1144,8 +1144,11 @@ Cases carry directives in leading `//@` comments:
11441144 | `//@ extern: <crate>` | the **oracle** links this crate; rustnim does not get it |
11451145
11461146 `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
11491152 the `i128` rejection and was deleted when `i128` became implementable — a
11501153 rejection test is only worth keeping while the rejection is still true.)
11511154
@@ -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 merely1146 `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` pinned1148+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 — a1152 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 {
56665666 targs: &[Nim],
56675667 c: &syn::ExprClosure,
56685668 ) -> 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+
56745672 if c.inputs.len() != 1 {
56755673 return Err(format!("`.{what}()` takes a one-argument closure"));
56765674 }
@@ -5757,11 +5755,35 @@ impl Lowerer {
57575755 /// A `move` closure captures by value, which is a different thing, so it
57585756 /// is rejected rather than lowered to the same construct.
57595757 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+ };
57655787 let want: Option<&Vec<Nim>> = match expect {
57665788 Some(Nim::Proc(a, _)) => Some(a),
57675789 _ => None,
@@ -5808,10 +5830,19 @@ impl Lowerer {
58085830 let ret = ret_ann
58095831 .or_else(|| v.ty.clone())
58105832 .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)))))
58155846 }
58165847
58175848 /// 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 it5755 /// 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_ann5830 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 opening5848 /// 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-}