Exact affine scopes#
Ordinary affine locals are destroyed automatically at their lexical boundary, but they may also be moved, returned, or dropped early. with gives an affine value a stricter contract: one exact extent whose visible name is a borrow.
local soa = nupp.mem.soa
local struct Position
x: float
velocity: float
end
local positions = soa.allocate(ffi.typeof<Position>(), 128)
with rows = positions:write() do
for index = 1, #rows do
rows[index].x += rows[index].velocity
end
endThe hidden owner is dropped when the body falls through, returns, raises, or leaves through loop control or an outward goto. For writable SoA and span views, dropping only ends the exclusive borrow: stores already changed the original columns, so the scope performs no flush or copy.
Syntax#
One acquisition is the common form:
with file = openFile(path) do
print(file:read("*a"))
endSeveral resources may share an extent. Acquisitions run left to right and terminals run in reverse order:
with
input = openFile(source, "rb"),
output = openFile(target, "wb")
do
output:write(input:read("*a"))
endA binding may have an underlying representation type annotation:
with file: FileHandle = openFile(path) do
use(file)
endwith is contextual. It introduces a scope only at statement position when a name and = or : follow it, so local with = function(...) ... end remains ordinary Lua-compatible code. See Nupp syntax for the other contextual words.
Visible names are borrows#
The acquisition must produce a non-optional affine(T, terminal) value. The compiler moves that owner into an inaccessible slot and binds the authored name as a scoped borrow of T. Code in the body can read and mutate through that borrow, but cannot:
- move it into a
takesparameter; - return it or store it in a longer-lived value;
- capture it in an escaping closure;
- assign a replacement to it; or
- invoke
dropon it directly.
Those rules make cleanup unconditional without an "already closed" flag. If a value needs to move or end early, use an ordinary affine local and drop:
local rows = positions:write()
update(rows)
drop rowsDive deeper
This construct survives beside automatic destruction because it guarantees something automatic destruction cannot: that the value is inaccessible and cannot escape, over one exact extent. It was in fact removed once the general case was covered, and then restored. A general feature subsumes a specific one only when it provides the specific one's guarantee, not merely its common use case.
Failure and control flow#
Each successful acquisition is registered before the next begins. If a later acquisition fails, earlier owners still drop. Every terminal is attempted even if another terminal raises.
When both the body and cleanup fail, the body error stays primary and cleanup errors are attached as suppressed failures. When only cleanup fails, its first failure is primary. A single failure is rethrown unchanged.
return, break, continue, and an outward goto cross the cleanup region and resume only after its terminals run. A goto cannot enter a with body and bypass acquisition.
Runtime cost#
Affine wrappers and with bindings are erased; the acquired value itself is not copied or boxed. The generated code uses the same cleanup-region machinery as automatic lexical destruction. When the body has ordinary fallthrough, every reachable operation is proven non-raising, and every terminal has a noRaise guarantee, the compiler emits an ordinary block followed by direct terminal calls. No pcall, xpcall, or closure is present in that form.
Uncertain or raising extents use a protected call, and a non-capturing body is shared rather than rebuilt on each execution. Outward return, loop control, goto, multiple acquisition, and nested cleanup keep this general lowering. This is a control-flow cost, not a storage-layout or copy-back cost.
For data-oriented code, put with around the hot loop rather than inside it. The loop then continues to access SoA columns directly, and nupp bc --check reports whether anything in it blocks a LuaJIT trace.
FAQ#
When is with better than an ordinary affine local?#
When the value must not escape and must not end early. An ordinary local can be moved, returned, or dropped anywhere in its scope, while a with binding is a borrow whose owner no code can name. See Visible names are borrows for the operations it refuses.
Does leaving the body early skip cleanup?#
No. return, break, continue, an outward goto, and a raise all cross the cleanup region, and the terminals run before control resumes outside it. See Failure and control flow for what happens when a terminal itself fails.
Does a writable view copy back at the end of the scope?#
No. Stores through a writable SoA or span view already changed the original columns, so dropping the hidden owner only ends the exclusive borrow. See Structure-of-arrays storage for how those views are acquired.