Problem
The cell unboxing of #239 lowers a Ref/STRef operation only in run position — where the application carries the magic-do run marker, or is the body of a literal loop lambda the native-loop lowering runs per iteration. An operation used as a first-class value keeps the whole cell boxed. The most visible casualty is the while condition: map (_ > 0) (STRef.read value) is an ST action value built once and forced per iteration, so the cell it reads stays a {value = …} table even when everything else about it would unbox. From test/ps/output/Golden.NativeLoopsST.Test/golden.lua after #239 — steps (run positions only) unboxes while value (read inside the condition value) stays boxed:
local steps = 0
local value = Control_Monad_ST_Internal_new(start)()
do
local _S_cond0 = Control_Monad_ST_Internal_map_(function(v)
return v >= 0 and v ~= 0
end)(Control_Monad_ST_Internal_read(value))
while _S_cond0() do
Approach
Lower a recognised operation in value position to a nullary Lua closure over the unboxed local — function() return g(value) end for the condition above — which is semantically what the boxed foreign chain builds (the closure shares the local as an upvalue, so each forcing sees the current contents). The subtlety is evaluation timing: the boxed form evaluates the operation's non-reference arguments when the thunk is built, not when it is forced, so a non-atomic argument must be pre-bound outside the closure (or the analysis must require atomic arguments in value positions). The while-condition case can go further: when the condition's compiled run is a pure expression, the native-loop lowering can inline it as the while predicate directly instead of building the thunk at all.
Relations
Extends #239; the value-position restriction and this exact countDown shape are pinned by Golden.NativeLoopsST.Test. Independent of the Effect-side dictionary gap (filed separately).
Verification
countDown's value cell in Golden.NativeLoopsST.Test compiles with no {value = …} table and the loop condition reads the local; eval golden unchanged. A stored-action shape (let action = STRef.write 5 r re-run twice) unboxes with a fresh golden pinning it.
Problem
The cell unboxing of #239 lowers a
Ref/STRefoperation only in run position — where the application carries the magic-do run marker, or is the body of a literal loop lambda the native-loop lowering runs per iteration. An operation used as a first-class value keeps the whole cell boxed. The most visible casualty is thewhilecondition:map (_ > 0) (STRef.read value)is an ST action value built once and forced per iteration, so the cell it reads stays a{value = …}table even when everything else about it would unbox. Fromtest/ps/output/Golden.NativeLoopsST.Test/golden.luaafter #239 —steps(run positions only) unboxes whilevalue(read inside the condition value) stays boxed:Approach
Lower a recognised operation in value position to a nullary Lua closure over the unboxed local —
function() return g(value) endfor the condition above — which is semantically what the boxed foreign chain builds (the closure shares the local as an upvalue, so each forcing sees the current contents). The subtlety is evaluation timing: the boxed form evaluates the operation's non-reference arguments when the thunk is built, not when it is forced, so a non-atomic argument must be pre-bound outside the closure (or the analysis must require atomic arguments in value positions). The while-condition case can go further: when the condition's compiled run is a pure expression, the native-loop lowering can inline it as thewhilepredicate directly instead of building the thunk at all.Relations
Extends #239; the value-position restriction and this exact
countDownshape are pinned byGolden.NativeLoopsST.Test. Independent of the Effect-side dictionary gap (filed separately).Verification
countDown'svaluecell inGolden.NativeLoopsST.Testcompiles with no{value = …}table and the loop condition reads the local; eval golden unchanged. A stored-action shape (let action = STRef.write 5 rre-run twice) unboxes with a fresh golden pinning it.