Skip to content

fix(codegen)!: wasm for-in/while loop bodies never executed (Closes #255)#257

Merged
hyperpolymath merged 1 commit into
mainfrom
fix-255-loop-codegen
May 19, 2026
Merged

fix(codegen)!: wasm for-in/while loop bodies never executed (Closes #255)#257
hyperpolymath merged 1 commit into
mainfrom
fix-255-loop-codegen

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Fix #255 — wasm for-in/while loop bodies never executed

Root cause

Layout mismatch in StmtFor lowering (lib/codegen.ml). ExprArray /
ExprIndex use the canonical array layout [length@+0][elem0@+4]…
(base pointer at the length word). All four StmtFor branches instead
read length from arr_ptr − 4 and element i from arr_ptr + i*4:

  • length read from 4 bytes before the array → garbage (≈0) → index ≥ length immediately → loop body ran zero times;
  • element addressing off by one word; tuple lists additionally trapped memory access out of bounds.

while itself was correct, but the idiomatic while i < vlen(xs) inherited the zero length (vlen uses for) and never ran.

Fix

All StmtFor branches read length from arr_ptr + 0 and element i via I32Load (2, 4) over arr_ptr + i*4, matching ExprArray/ExprIndex exactly. Tuple sub-field extraction unchanged (correct).

Why it was never caught

tests/codegen/test_for_loop.affine existed but had no .mjs, so the harness compiled it and never ran it. Pre-existing since ≥ 81a59bf (not a regression). Added asserting harnesses: test_for_loop.mjs (⇒15), while_loop.affine+test_while_loop.mjs (⇒34), for_tuple.affine+test_for_tuple.mjs (⇒21).

Verification

dune test --force 271/271; tools/run_codegen_wasm_tests.sh all pass incl. the three new. Zero regression.

Unblocks INT-08 (#183) runtime and INT-07 (#182) TEA run loop.

Closes #255. Refs #183 #182.

🤖 Generated with Claude Code

)

Root cause: a layout mismatch in `StmtFor` lowering. `ExprArray` and
`ExprIndex` use the canonical array layout `[length@+0][elem0@+4]
[elem1@+8]…` (base pointer at the length word). `StmtFor` (all four
branches: PatVar, PatWildcard, PatTuple, fallback) instead read the
length from `arr_ptr - 4` and element `i` from `arr_ptr + i*4`. So:

- length was loaded from 4 bytes *before* the array (garbage, typically
  0) ⇒ `index >= length` immediately ⇒ **loop body ran zero times**;
- element addressing was off by one word (would read the length as
  elem 0); for tuple lists this also trapped `memory access out of
  bounds`.

`while` was correct, but any `while` bounded by a `for`-derived length
(the idiomatic `vlen` helper) inherited the zero-length and never ran.

Fix: all StmtFor branches now read length from `arr_ptr + 0` and
element `i` via `I32Load (2, 4)` over `arr_ptr + i*4` (= `arr + 4 +
i*4`), matching ExprArray/ExprIndex exactly. Tuple sub-field extraction
was already correct (tuples have no length prefix) and is unchanged.

Pre-existing since at least `81a59bf`; never caught because
`tests/codegen/test_for_loop.affine` had **no** `.mjs`, so the harness
compiled but never executed it. Added asserting harnesses:
- `test_for_loop.mjs` (for-in over [Int] ⇒ 15)
- `while_loop.affine` + `test_while_loop.mjs` (while + for-count len +
  index ⇒ 34)
- `for_tuple.affine` + `test_for_tuple.mjs` (for over tuple list +
  tuple destructure ⇒ 21)

Gates: `dune test --force` 271/271; `tools/run_codegen_wasm_tests.sh`
all pass (incl. the three new). Zero regression.

Unblocks INT-08 (#183) runtime and INT-07 (#182) TEA run loop.

Closes #255. Refs #183 #182.
@hyperpolymath hyperpolymath merged commit 85488d7 into main May 19, 2026
12 of 13 checks passed
@hyperpolymath hyperpolymath deleted the fix-255-loop-codegen branch May 19, 2026 17:06
@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 44 issues detected

Severity Count
🔴 Critical 12
🟠 High 21
🟡 Medium 11

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Stray AI.a2ml in root -- use 0-AI-MANIFEST.a2ml only",
    "type": "banned",
    "file": "AI.a2ml",
    "action": "delete",
    "rule_module": "root_hygiene",
    "severity": "high"
  },
  {
    "reason": "Superseded by 0-AI-MANIFEST.a2ml",
    "type": "banned",
    "file": "AI.djot",
    "action": "delete",
    "rule_module": "root_hygiene",
    "severity": "high"
  },
  {
    "reason": "Issue in quality.yml",
    "type": "missing_workflow",
    "file": "quality.yml",
    "action": "create",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "Issue in security-policy.yml",
    "type": "missing_workflow",
    "file": "security-policy.yml",
    "action": "create",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Action hyperpolymath/standards/.github/workflows/governance-reusable.yml@main needs attention",
    "type": "unpinned_action",
    "file": "governance.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/affinescript/affinescript/affinescript-deno-test/example/smoke_driver.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/affinescript/affinescript/affinescript-deno-test/cli.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/affinescript/affinescript/affinescript-deno-test/mod.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/affinescript/affinescript/affinescript-deno-test/lib/compile.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/affinescript/affinescript/affinescript-deno-test/lib/runner.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

hyperpolymath added a commit that referenced this pull request May 28, 2026
)

## Summary

PR #257 (`fix(codegen)!: wasm for-in/while loop bodies never executed
(Closes #255)`) shipped 2026-05-19 and resolved the codegen bug that
stdlib roadmap rows #3 (`for x in xs` codegen) and #4 (`while` + `mut`
codegen) were waiting on. The status markers were stale at `◯
blocked-by-#255`.

Flips both rows to `●` with a reference to the closing PR.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request Jul 7, 2026
…xed (#678)

## What

Issue **#255** (`for-in`/`while` loop bodies never execute in compiled
wasm) was fixed in **PR #257** (commit `85488d74`, an ~8-line
array-offset correction) and **closed 2026-05-19**. Verified on current
`main`: freshly compiled loops execute their bodies (`while`→55,
`for`→15, the exact #255 combo fixture→34).

But several roadmap docs still advertised #255 as a **live runtime
blocker**, falsely gating **INT-08** (DOM reconciler #183), **INT-11**
(browser parity), and the DOM bindings row on a resolved bug.

## Fix — 7 references reconciled (docs-only)
- `docs/ECOSYSTEM.adoc` — satellite registry + INT-08/INT-11 rows
- `docs/bindings-roadmap.adoc` — DOM row #7
- `docs/TECH-DEBT.adoc` — INT-08 row

Honest reconciliation: states #255 is fixed (PR #257, closed 2026-05-19)
and marks the DOM/INT-08 **end-to-end runtime as "to be re-verified"**
rather than asserting it runs (not driven end-to-end in this change).

## Why it matters
A false "blocked" on the roadmap misdirects effort — it implies
INT-08/INT-11/DOM need a codegen fix that already shipped. Removing it
clarifies that the next step there is *runtime re-verification*, not a
bug fix.

## Verification (real toolchain — dune 3.24.0)
```
BUILD_EXIT=0
OK: doc-truthing intact — presence invariants + over-claim ratchet (DOC-04/05/08/09).
OK: soundness ledger — all 5 properties hold.
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request Jul 7, 2026
…ess (Refs #183)

The dom.affine header said "RUNTIME blocked by #255; no runtime e2e harness
is shipped until then (a harness that cannot pass would be dishonest)".
#255 was fixed in PR #257 — and the harness now CAN pass, so per the
header's own bar, it ships:

- affinescript-dom/e2e/run.sh — concatenates src/dom.affine + a driver
  `main`, compiles to core-WASM, runs under Node against an Int-handle
  host DOM, asserts the mutation log + final tree. Skips loudly (exit 0)
  when the compiler or node is absent; AFFINESCRIPT_BIN overrides the
  binary path for CI.
- affinescript-dom/e2e/driver_main.affine — mounts
  <div id=app class=old>["hello", <span>["x"]]</div>, then reconciles to
  <div id=app title=t2>["world"]</div>. Exercises every loop-bearing path
  (the #255 class): render attr+children loops, patch_attrs add+remove
  (attr_has), and the `while` child-reconcile loop (in-place text patch +
  surplus-child removal). The affine discipline is load-bearing in the
  driver itself: `mount` consumes its tree, so reconcile takes a fresh
  copy — reuse would be a use-after-move type error.
- affinescript-dom/e2e/dom_host.mjs — the host: Int-handle node map,
  mutation log, hard assertions (title added, class removed, text patched
  in place, span removed, reconcile returned the in-place handle).

Observed run (2026-07-07, node 26, dune 3.24 build):
  query(#root) … setAttr(#2,title=t2) removeAttr(#2,class)
  setText(#3,"world") remove(#2,#4)
  <#root> └ <div id="app" title="t2"> └ "world"
  ALL ASSERTIONS PASS

Docs reconciled to VERIFIED (dom.affine header; ECOSYSTEM satellite +
INT-08/INT-11 rows; TECH-DEBT INT-08; bindings-roadmap DOM row). INT-11's
remaining leg is browser-host parity (this run is Node).

Gates: dune build exit 0; doc-truthing OK; soundness ledger all-5 OK;
e2e harness exit 0 from the branch's own build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request Jul 7, 2026
…ess (#680)

## What

`dom.affine`'s header said *"RUNTIME blocked by #255; no runtime e2e
harness is shipped until then (a harness that cannot pass would be
dishonest)."* #255 was fixed in PR #257 — and the harness now **can**
pass, so per the header's own bar, it ships. Refs #183 (INT-08).

## The proof

`affinescript-dom/e2e/run.sh` concatenates `src/dom.affine` + a driver
`main`, compiles to core-WASM, runs under Node against an Int-handle
host DOM, and asserts the mutation log + final tree:

- mounts `<div id=app class=old>["hello", <span>["x"]]</div>`
- reconciles to `<div id=app title=t2>["world"]</div>`
- exercises **every loop-bearing path** (the #255 class): render
attr+children loops, `patch_attrs` add+remove (via `attr_has`), and the
`while` child-reconcile loop — in-place text patch *and* surplus-child
removal

Observed (node 26, dune 3.24 build):
```
setAttr(#2,title=t2)  removeAttr(#2,class)  setText(#3,"world")  remove(#2,#4)
<#root> └ <div id="app" title="t2"> └ "world"
ALL ASSERTIONS PASS — reconciler ran end-to-end (loops executed)
```

A nice detail: the affine discipline is load-bearing in the driver
itself — `mount` **consumes** its tree, so the reconcile call takes a
freshly built copy; reuse would be a use-after-move type error.

## Docs reconciled to VERIFIED
`dom.affine` header; `ECOSYSTEM.adoc` (satellite + INT-08/INT-11 rows);
`TECH-DEBT.adoc` (INT-08); `bindings-roadmap.adoc` (DOM row). INT-11's
remaining leg is **browser-host** parity (this run is Node).

## Gates
`dune build` exit 0 · doc-truthing OK · soundness ledger all-5 OK · e2e
harness exit 0 from this branch's own build. Harness skips loudly (exit
0) without the compiler or node; `AFFINESCRIPT_BIN` overrides the binary
path for CI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

wasm codegen: for-in/while loop bodies never execute in compiled programs (pre-existing, untested)

1 participant