Skip to content

loop capture per iteration - #11596

Merged
Thomas Ball (thomasjball) merged 2 commits into
masterfrom
dev/jwunderl/loop-capture-semantics
Sep 21, 2026
Merged

Thomas Ball (thomasjball) merged 2 commits into
masterfrom
dev/jwunderl/loop-capture-semantics

Conversation

@jwunderl

Copy link
Copy Markdown
Member

close #11563

build here https://arcade.makecode.com/app/48bf6ee7932fd01859f48cc3cadc2211a3ba2253-baa5c5f82b#pub:S11575-26979-25273-96005; should see rainbow diagonal of numbers, vs https://arcade.makecode.com/#pub:S11575-26979-25273-96005 being set at 14 / black so it shows nothing on screen

@jwunderl
Joey Wunderlich (jwunderl) requested a review from a team September 9, 2026 22:47
@jwunderl

Copy link
Copy Markdown
Member Author

(Eric Anderson (@humanapp) in case you want to take a look, had some notable gpt assistance but took a stab at both this and #11597 based off your last two issues, still validating / need to test hardware but so far think got it)

@jwunderl

Copy link
Copy Markdown
Member Author
image tested on office gamego

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Positionless synthetic captures can bypass loop-block hoisting and cause compilation or capture failures.

Get a fresh assessment by requesting another Copilot review.

Review tier: Balanced
Findings: 1 Medium severity

New issues introduced by this change (1)
Severity Finding
Medium severity pxtcompiler/​emitter/​emitter.ts — Handle positionless captures in loop-block hoisting View comment
What changed in this PR

Fixes per-iteration capture semantics for let bindings in for and for...of loops.

Changes:

  • Creates fresh captured bindings at correct loop boundaries.
  • Corrects loop-local hoisted function placement and labeled-statement emission.
  • Adds semantic, native assembly, and hardware regression coverage.
File Description
pxtcompiler/​emitter/​emitter.ts Implements per-iteration bindings and loop-local function hoisting.
tests/​compile-test/​lang-test0/​58loopcapture.ts Adds comprehensive semantic tests.
tests/​compile-test/​lang-test0/​README-codegen.md Documents loop-capture coverage.
tests/​thumb-test/​cases/​loopcapture.ts Adds native compiler probe.
tests/​thumb-test/​asmchecks.ts Verifies generated assembly structure.
tests/​hw-ab/​gen-project.js Adds hardware A/B coverage.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pxtcompiler/emitter/emitter.ts Outdated
Comment on lines +3235 to +3237
info.capturedVars.sort((a, b) => b.pos - a.pos)
const vinfo = getVarInfo(info.capturedVars[0])
if (!vinfo.functionsToDefine)
vinfo.functionsToDefine = []
vinfo.functionsToDefine.push(node)
const scope = node.parent;
if (scope.kind === SK.Block && inLoop(scope) && info.capturedVars[0].pos < scope.pos) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

narrow but looks like real edge case / right, build after fix https://arcade.makecode.com/app/7537abbebd27997ef4e4312ef1c218991a9add97-28ed852db5

@humanapp

Copy link
Copy Markdown
Contributor

Small nit: This PR and #11597 both add a test file with numeric prefix 58. It doesn't break anything, but one of them should probably be 59.

@humanapp

Copy link
Copy Markdown
Contributor

Fix looks good! The additional work of boxing/unboxing the captured values is unfortunate, but unavoidable and is the expected cost here.

function emitLabeledStatement(node: LabeledStatement) {
let l = getLabels(node.statement)
emit(node.statement)
proc.emitLblDirect(l.brk)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting. Did you find a different compiler bug? Were labeled loops broken?

@jwunderl Joey Wunderlich (jwunderl) Sep 14, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, this was a small side case of this emitting the same label a second time while going through it. Probably a case 0 people will ever hit but might as well fix, repro i came up with from description:

let count: number = 0;

outer: for (let i = 0; i < 3; i++) {
    count++;
    if (i === 1) break outer;
}

game.splash("Count: " + count);

on live hw compilation breaks, fixed in branch

Image

(and re: test file, i'll swap 11597 to be 59 before merging yeah / after incorporating your suggested change on that one)

@thomasjball
Thomas Ball (thomasjball) added this pull request to the merge queue Sep 21, 2026
Merged via the queue into master with commit bf24ac0 Sep 21, 2026
20 checks passed
@thomasjball
Thomas Ball (thomasjball) deleted the dev/jwunderl/loop-capture-semantics branch September 21, 2026 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[compiler] Closures capture let loop variables by final value, not per iteration

4 participants