Repro
mod.pyaff:
# face: python
def score(n: Int) -> Int:
let mut sum = 0
let mut i = 1
while i <= n:
sum = sum + i
i = i + 1
sum
affinescript compile mod.pyaff -o mod.wasm # parse error
affinescript preview-python mod.pyaff # shows the bad canonical:
while i <= n {
sum = sum + i;
i = i + 1 <-- NO trailing ';'
}
sum
The last statement of the while body (i = i + 1) is emitted without a ;, so it is parsed as the block's tail value expression — but a loop body is not value-returning, and i = i + 1 is a statement.
Cause
lib/python_face.ml tail-position detection (comment at ~line 229: "a regular statement … in the last position of a block … drops the ;"). Dropping the ; on a block's tail line is correct for a def body or an if/else value-arm (the tail IS the return value — this is why recursive face programs like fac compile + run fine), but wrong for a while/for body, where the tail line is a statement that still needs its ;.
Fix direction
The face must know the enclosing block-opener kind when deciding whether the tail line drops its ;: drop it for def/if/else/match-arm value bodies, keep it for while/for bodies (and arguably any block whose value is unused). is_block_opener / the block-opener set already exist (lib/python_face.ml:185-196); thread the opener kind into the tail-position decision.
Impact / severity
Blocks any face-authored program with a loop — directly relevant to the mod/plugin wedge (mods have update loops). Recursive face programs are unaffected (proven: fac(5)=120 compiles through the face and runs). Workaround until fixed: write loops in the canonical .affine surface, or use recursion in the face.
Discovered
2026-07-07, proving a Python-face program end-to-end to wasm.
Repro
mod.pyaff:The last statement of the
whilebody (i = i + 1) is emitted without a;, so it is parsed as the block's tail value expression — but a loop body is not value-returning, andi = i + 1is a statement.Cause
lib/python_face.mltail-position detection (comment at ~line 229: "a regular statement … in the last position of a block … drops the ;"). Dropping the;on a block's tail line is correct for adefbody or anif/elsevalue-arm (the tail IS the return value — this is why recursive face programs likefaccompile + run fine), but wrong for awhile/forbody, where the tail line is a statement that still needs its;.Fix direction
The face must know the enclosing block-opener kind when deciding whether the tail line drops its
;: drop it fordef/if/else/match-arm value bodies, keep it forwhile/forbodies (and arguably any block whose value is unused).is_block_opener/ the block-opener set already exist (lib/python_face.ml:185-196); thread the opener kind into the tail-position decision.Impact / severity
Blocks any face-authored program with a loop — directly relevant to the mod/plugin wedge (mods have update loops). Recursive face programs are unaffected (proven:
fac(5)=120compiles through the face and runs). Workaround until fixed: write loops in the canonical.affinesurface, or use recursion in the face.Discovered
2026-07-07, proving a Python-face program end-to-end to wasm.