From f78edc07d2a4a305b883d4af5b493f95e053f168 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 19 May 2026 18:05:53 +0100 Subject: [PATCH] fix(codegen)!: wasm for-in/while loop bodies never executed (Closes #255) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- lib/codegen.ml | 27 ++++++++++++++------------- tests/codegen/for_tuple.affine | 17 +++++++++++++++++ tests/codegen/test_for_loop.mjs | 20 ++++++++++++++++++++ tests/codegen/test_for_tuple.mjs | 17 +++++++++++++++++ tests/codegen/test_while_loop.mjs | 17 +++++++++++++++++ tests/codegen/while_loop.affine | 26 ++++++++++++++++++++++++++ 6 files changed, 111 insertions(+), 13 deletions(-) create mode 100644 tests/codegen/for_tuple.affine create mode 100644 tests/codegen/test_for_loop.mjs create mode 100644 tests/codegen/test_for_tuple.mjs create mode 100644 tests/codegen/test_while_loop.mjs create mode 100644 tests/codegen/while_loop.affine diff --git a/lib/codegen.ml b/lib/codegen.ml index 7d815a87..2af2552b 100644 --- a/lib/codegen.ml +++ b/lib/codegen.ml @@ -1713,9 +1713,9 @@ and gen_stmt (ctx : context) (stmt : stmt) : (context * instr list) result = Ok (ctx_final, iter_code @ [ LocalSet arr_ptr; (* Save array pointer *) LocalGet arr_ptr; - I32Const (-4l); - I32Add; (* arr - 4 points to length *) - I32Load (2, 0); (* Load length *) + I32Load (2, 0); (* Load length (canonical layout: + length @ arr+0, elems @ arr+4+i*4 — + matches ExprArray / ExprIndex) *) LocalSet len_var; (* Save length *) I32Const 0l; LocalSet idx_var; (* index = 0 *) @@ -1727,13 +1727,14 @@ and gen_stmt (ctx : context) (stmt : stmt) : (context * instr list) result = I32GeS; (* index >= length? *) BrIf 1; (* Exit loop if true *) - (* Load array[index] into item variable *) + (* Load array[index] into item variable. + elem i @ arr + 4 + i*4 (offset 4 skips the length word). *) LocalGet arr_ptr; LocalGet idx_var; I32Const 4l; I32Mul; (* index * 4 *) I32Add; (* arr + index*4 *) - I32Load (2, 0); (* Load array[index] *) + I32Load (2, 4); (* Load array[index] (+4 = skip len) *) LocalSet item_var; (* item = array[index] *) ] @ body_code @ [ (* Increment index *) @@ -1750,8 +1751,8 @@ and gen_stmt (ctx : context) (stmt : stmt) : (context * instr list) result = let* (ctx_final, body_code) = gen_block ctx_with_idx body in Ok (ctx_final, iter_code @ [ LocalSet arr_ptr; - LocalGet arr_ptr; I32Const (-4l); I32Add; - I32Load (2, 0); LocalSet len_var; + LocalGet arr_ptr; + I32Load (2, 0); LocalSet len_var; (* length @ arr+0 (canonical) *) I32Const 0l; LocalSet idx_var; Block (BtEmpty, [ Loop (BtEmpty, [ @@ -1784,15 +1785,15 @@ and gen_stmt (ctx : context) (stmt : stmt) : (context * instr list) result = let* (ctx_final, body_code) = gen_block ctx_with_binds body in Ok (ctx_final, iter_code @ [ LocalSet arr_ptr; - LocalGet arr_ptr; I32Const (-4l); I32Add; - I32Load (2, 0); LocalSet len_var; + LocalGet arr_ptr; + I32Load (2, 0); LocalSet len_var; (* length @ arr+0 (canonical) *) I32Const 0l; LocalSet idx_var; Block (BtEmpty, [ Loop (BtEmpty, [ LocalGet idx_var; LocalGet len_var; I32GeS; BrIf 1; LocalGet arr_ptr; LocalGet idx_var; I32Const 4l; I32Mul; I32Add; - I32Load (2, 0); LocalSet elem_var; + I32Load (2, 4); LocalSet elem_var; (* +4 skips length word *) ] @ bind_codes @ body_code @ [ LocalGet idx_var; I32Const 1l; I32Add; LocalSet idx_var; Br 0 @@ -1805,15 +1806,15 @@ and gen_stmt (ctx : context) (stmt : stmt) : (context * instr list) result = let* (ctx_final, body_code) = gen_block ctx_with_tmp body in Ok (ctx_final, iter_code @ [ LocalSet arr_ptr; - LocalGet arr_ptr; I32Const (-4l); I32Add; - I32Load (2, 0); LocalSet len_var; + LocalGet arr_ptr; + I32Load (2, 0); LocalSet len_var; (* length @ arr+0 (canonical) *) I32Const 0l; LocalSet idx_var; Block (BtEmpty, [ Loop (BtEmpty, [ LocalGet idx_var; LocalGet len_var; I32GeS; BrIf 1; LocalGet arr_ptr; LocalGet idx_var; I32Const 4l; I32Mul; I32Add; - I32Load (2, 0); LocalSet tmp_var; + I32Load (2, 4); LocalSet tmp_var; (* +4 skips length word *) ] @ body_code @ [ LocalGet idx_var; I32Const 1l; I32Add; LocalSet idx_var; Br 0 diff --git a/tests/codegen/for_tuple.affine b/tests/codegen/for_tuple.affine new file mode 100644 index 00000000..2d784802 --- /dev/null +++ b/tests/codegen/for_tuple.affine @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: PMPL-1.0-or-later +// SPDX-FileCopyrightText: 2026 hyperpolymath +// +// #255 regression: `for` over a list of tuples with tuple-pattern +// destructuring (StmtFor PatTuple branch). Before the fix this trapped +// `memory access out of bounds` (same off-by-one length/elem layout). + +fn main() -> Int { + let pairs = [(1, 2), (3, 4), (5, 6)]; + let mut s = 0; + for p in pairs { + match p { + (a, b) => { s = s + a + b; } + } + } + s // (1+2)+(3+4)+(5+6) = 21 +} diff --git a/tests/codegen/test_for_loop.mjs b/tests/codegen/test_for_loop.mjs new file mode 100644 index 00000000..3c068116 --- /dev/null +++ b/tests/codegen/test_for_loop.mjs @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT OR AGPL-3.0-or-later +// SPDX-FileCopyrightText: 2026 hyperpolymath +// +// Regression for #255: `for-in` loop bodies must execute. Before the +// fix, StmtFor read length from arr-4 and elem i from arr+i*4 (the +// canonical layout is length@arr+0, elem i@arr+4+i*4 — ExprArray / +// ExprIndex), so the loop ran zero times and main() returned 0. +// test_for_loop.affine existed but had NO harness, so nothing caught it. + +import { readFile } from 'fs/promises'; + +const wasmBuffer = await readFile('./tests/codegen/test_for_loop.wasm'); +const imports = { wasi_snapshot_preview1: { fd_write: () => 0 } }; +const { instance } = await WebAssembly.instantiate(wasmBuffer, imports); +const result = instance.exports.main(); + +console.log(`Result: ${result}`); +console.log('Expected: 15'); +console.log(`Test ${result === 15 ? 'PASSED ✓' : 'FAILED ✗'}`); +process.exit(result === 15 ? 0 : 1); diff --git a/tests/codegen/test_for_tuple.mjs b/tests/codegen/test_for_tuple.mjs new file mode 100644 index 00000000..acedd92a --- /dev/null +++ b/tests/codegen/test_for_tuple.mjs @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT OR AGPL-3.0-or-later +// SPDX-FileCopyrightText: 2026 hyperpolymath +// +// #255 regression: for over a tuple list + tuple-pattern destructure. + +import { readFile } from 'fs/promises'; + +const wasmBuffer = await readFile('./tests/codegen/for_tuple.wasm'); +const { instance } = await WebAssembly.instantiate(wasmBuffer, { + wasi_snapshot_preview1: { fd_write: () => 0 }, +}); +const result = instance.exports.main(); + +console.log(`Result: ${result}`); +console.log('Expected: 21'); +console.log(`Test ${result === 21 ? 'PASSED ✓' : 'FAILED ✗'}`); +process.exit(result === 21 ? 0 : 1); diff --git a/tests/codegen/test_while_loop.mjs b/tests/codegen/test_while_loop.mjs new file mode 100644 index 00000000..8cd0f714 --- /dev/null +++ b/tests/codegen/test_while_loop.mjs @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT OR AGPL-3.0-or-later +// SPDX-FileCopyrightText: 2026 hyperpolymath +// +// #255 regression: while + for-count len + index. + +import { readFile } from 'fs/promises'; + +const wasmBuffer = await readFile('./tests/codegen/while_loop.wasm'); +const { instance } = await WebAssembly.instantiate(wasmBuffer, { + wasi_snapshot_preview1: { fd_write: () => 0 }, +}); +const result = instance.exports.main(); + +console.log(`Result: ${result}`); +console.log('Expected: 34'); +console.log(`Test ${result === 34 ? 'PASSED ✓' : 'FAILED ✗'}`); +process.exit(result === 34 ? 0 : 1); diff --git a/tests/codegen/while_loop.affine b/tests/codegen/while_loop.affine new file mode 100644 index 00000000..0213e6e4 --- /dev/null +++ b/tests/codegen/while_loop.affine @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: PMPL-1.0-or-later +// SPDX-FileCopyrightText: 2026 hyperpolymath +// +// #255 regression: `while` + a `for`-count length helper + array index. +// (The reconciler in affinescript-dom/#183 depends on exactly this +// shape; before the fix it iterated zero times.) + +fn vlen(xs: [Int]) -> Int { + let mut c = 0; + for x in xs { + c = c + 1; + } + c +} + +fn main() -> Int { + let xs = [3, 7, 11, 13]; + let n = vlen(xs); + let mut i = 0; + let mut acc = 0; + while i < n { + acc = acc + xs[i]; + i = i + 1; + } + acc // 3 + 7 + 11 + 13 = 34 +}