Escape analysis
Nyra proves which values stay local at compile time, then promotes them to the stack, inlines sequential channels without mutexes, and honors #[no_escape] on references.
What is escape analysis?
After typecheck and borrow checking, Nyra builds an AST-level escape graph for every function. Each binding is classified as NoEscape (stays in the function), ArgEscape (passed as &T but not returned or spawned), or GlobalEscape (returned, sent on a channel, or captured by spawn).
The plan flows into LLVM codegen — you do not annotate ordinary locals. Optimizations apply on every build; use --verbose to see the report.
parse → typecheck → borrow → escape graph → codegen (EscapePlan)
Escape states
| State | Meaning | Codegen effect |
|---|---|---|
| NoEscape | Created and consumed in the same function | Stack promotion, SROA, skip redundant clone/free |
| ArgEscape | Passed by reference to a callee | Stays on caller stack |
| GlobalEscape | return, spawn, or channel payload | Heap / runtime channel as usual |
Stack promotion & SROA
For NoEscape values, codegen avoids unnecessary heap work:
- Struct literals — static string fields skip
str_clone. - Composite structs — NoEscape bindings skip field-wise
freewhen stack-safe. - SROA — all-copy scalar structs (e.g.
Point { x: i32 y: i32 }) decompose into separate LLVM values instead of a structalloca.
Struct literals with spread (..p) are excluded from SROA — the compiler keeps a single struct binding.
LocalChannel
When a Channel<T> is NoEscape and never captured by spawn, Nyra emits a stack LocalChannel: a fixed-capacity ring buffer (16 slots) with inline send/recv — no locking and no runtime channel calls.
If the channel handle or its messages escape to another thread, the plan falls back to the normal runtime channel.
See also: Concurrency · Memory & ownership.
#[no_escape] on parameters
Advanced callers can promise that a reference parameter never escapes the callee:
fn process(#[no_escape] data) {
print(data)
}
fn bad(#[no_escape] data){
return data // compile error E0602
}fn process(#[no_escape] data: i32) {
print(data)
}
fn bad(#[no_escape] data: i32){
return data // compile error E0602
}- E0601 —
#[no_escape]is only allowed on&Tparameters. - E0602 — the parameter would escape (return,
spawn, or channel send).
When valid, codegen treats the parameter as NoEscape for stack optimizations in the caller’s frame.
Verbose report
nyra build --verbose file.ny
nyra build -v .
Example output:
Checking escape analysis
escape: main::user → NoEscape
escape: main::chan → NoEscape
local channel: main::chan → LocalChannel (stack ring buffer)
no_escape param: process::data (must not return/spawn/send)
Example
fn main() {
let p = { x: 10, y: 20 }
print(p.x + p.y)
}fn main() -> void {
let p = { x: 10, y: 20 }
print(p.x + p.y)
}With --verbose: p is NoEscape and the struct literal is eligible for stack promotion (SROA). For LocalChannel, see the section above and Concurrency → Channels.
Limitations
- SROA applies to all-copy scalar structs without spread — not arbitrary aggregates.
- LocalChannel is sequential only, fixed capacity 16, no
spawnsharing. - Dynamic heap strings still allocate when they GlobalEscape.
- FFI boundary — values passed to or returned from
extern fn/export fnare treated as escaping; stack promotion and LocalChannel do not apply across the C ABI. See C Bindgen and FFI & ABI. - Optimizations are silent unless you pass
--verbose.
Related
- Performance toolchain —
--release, LTO, PGO - Memory & ownership — move, drop, auto-free
- Diagnostics — borrow errors and error codes
- Design notes:
Escape_Analysis.mdin the repository root