perf(codegen): accept one-shot wrap closures (#23265)

## Summary

- Change `Codegen::wrap` from `FnMut` to `FnOnce`, matching how the helper is used: every closure passed to `wrap` is invoked exactly once.
- Add an assembly comparison note showing the optimized codegen difference before and after the change.

## `Fn`, `FnMut`, and `FnOnce` ?

`Fn`, `FnMut`, and `FnOnce` all describe how a closure may be called:

- `Fn` is the most restrictive for the closure body: it is callable through `&self`, can be called repeatedly, and cannot require mutable or consuming access to captured state.
- `FnMut` is callable through `&mut self`, can be called repeatedly, and may mutate captured state.
- `FnOnce` is callable by value, may consume captured state, and is only guaranteed to be callable once.

The trait relationship goes from most specific to most general call capability:

```rust
Fn: FnMut
FnMut: FnOnce
```

So accepting `FnOnce` is the least restrictive bound for a callback that is only invoked once. It still accepts `Fn` and `FnMut` closures, but it also tells the optimizer that `wrap` does not need a reusable mutable closure object.

## Assembly Impact

`Codegen::wrap` is an inline generic helper, so there is no stable standalone `wrap` assembly symbol in release output. The impact shows up in monomorphized call sites such as `Class::gen`, `Function::gen`, and expression `gen_expr` closures.

Before, several call sites materialized a closure environment on the stack before calling the closure:

```asm
strb    w9, [sp, #15]
add     x9, sp, #15
stp     x0, x9, [sp, #16]
add     x0, sp, #16
bl      <...>::{{closure}}
```

After the `FnOnce` bound, the same shape can pass the one-shot closure state directly through registers:

```asm
and     w1, w2, #0xfffffffd
mov     x2, x19
bl      <...>::{{closure}}
```

Some wrapper frames also shrink. For example, representative `Class::gen` / `Function::gen` paths go from a `64` byte frame to a `48` byte frame:

```asm
- sub     sp, sp, #64
- stp     x20, x19, [sp, #32]
- stp     x29, x30, [sp, #48]
+ sub     sp, sp, #48
+ stp     x20, x19, [sp, #16]
+ stp     x29, x30, [sp, #32]
```

Several restored-frame return paths also become tail calls:

```asm
ldp     x29, x30, [sp, #32]
ldp     x20, x19, [sp, #16]
add     sp, sp, #48
b       <closure or push_slow target>
```

The assembly diff also contains expected local label renumbering noise, such as switch-table suffixes changing from `.318` to `.330`; those are not behavior changes.
This commit is contained in:
camc314 committed 2026-06-11 12:01:52 +00:00
1 parent 06e215e441
commit e89729b178
1 file changed
+1 -1
+1 -1
View File
@@ -524,7 +524,7 @@ impl<'a> Codegen<'a> {
}
#[inline]
fn wrap<F: FnMut(&mut Self)>(&mut self, wrap: bool, mut f: F) {
fn wrap<F: FnOnce(&mut Self)>(&mut self, wrap: bool, f: F) {
if wrap {
self.print_ascii_byte(b'(');
}