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

1 participant