Skip to content

Commit 910bfe5

Browse files
fix(codegen): implement ++ list concatenation (was placeholder I32Add) (Closes #264) (#265)
`gen_binop` had `OpConcat -> I32Add (* Placeholder *)`: `a ++ b` summed the two list *pointers*, so every concatenation produced a garbage/zero-length list (iteration over the result saw 0 elements). Pre-existing and untested — `++` is used in stdlib but AOT only checks compile, and no asserted fixture ever exercised it (same class as #255). Surfaced by #225 PR3d (`Http.readResponse` builds `headers` via `headers ++ [pair]`). Real implementation in the `ExprBinary` handler, in the canonical list layout fixed by #255 (`[len@+0][elem i @ +4 + i*4]`): evaluate both operands, allocate `len(a)+len(b)` (+ length word), store the combined length, then two index-loop element copies (a then b). Reuses the same addressing convention as `StmtFor` so it stays consistent with #255. Added asserting `tests/codegen/list_concat.{affine,mjs}`: literal++literal + loop-append + loop-prepend ⇒ 166 (the suite had no `++` coverage at all). Gate: `dune test --force` 278/278; `tools/run_codegen_wasm_tests.sh` all pass incl. the new regression. Zero regression. Closes #264. Refs #225 #255.
1 parent 96ae8ac commit 910bfe5

3 files changed

Lines changed: 109 additions & 0 deletions

File tree

lib/codegen.ml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,60 @@ let rec gen_expr (ctx : context) (expr : expr) : (context * instr list) result =
466466
end
467467
end
468468

469+
| ExprBinary (left, OpConcat, right) ->
470+
(* List concatenation `a ++ b`. `OpConcat` was a placeholder `I32Add`
471+
(it just summed the two list pointers), so every `++` produced a
472+
garbage/zero-length list. Real implementation, in the canonical
473+
list layout fixed by #255: `[len@+0][elem i @ +4 + i*4]`.
474+
Allocate len(a)+len(b), copy a's then b's elements. *)
475+
let* (ctx1, left_code) = gen_expr ctx left in
476+
let* (ctx2, right_code) = gen_expr ctx1 right in
477+
let (ctx3, heap_idx) = ensure_heap_ptr ctx2 in
478+
let (ctx4, a) = alloc_local ctx3 "__cat_a" in
479+
let (ctx5, b) = alloc_local ctx4 "__cat_b" in
480+
let (ctx6, la) = alloc_local ctx5 "__cat_la" in
481+
let (ctx7, lb) = alloc_local ctx6 "__cat_lb" in
482+
let (ctx8, dst) = alloc_local ctx7 "__cat_dst" in
483+
let (ctx9, k) = alloc_local ctx8 "__cat_k" in
484+
let copy_loop src_ptr count dst_base_off =
485+
(* for k in 0..count: dst[dst_base_off + k] = src[k]
486+
element addr = ptr + idx*4, value/store via static +4 offset
487+
(skips the length word) — exactly the #255 convention. *)
488+
[ I32Const 0l; LocalSet k;
489+
Block (BtEmpty, [ Loop (BtEmpty, [
490+
LocalGet k; LocalGet count; I32GeS; BrIf 1;
491+
(* dst slot: dst + (dst_base_off + k)*4 *)
492+
LocalGet dst;
493+
LocalGet dst_base_off; LocalGet k; I32Add;
494+
I32Const 4l; I32Mul; I32Add;
495+
(* value: src[k] = *(src + k*4 + 4) *)
496+
LocalGet src_ptr; LocalGet k; I32Const 4l; I32Mul; I32Add;
497+
I32Load (2, 4);
498+
I32Store (2, 4);
499+
LocalGet k; I32Const 1l; I32Add; LocalSet k;
500+
Br 0 ]) ]) ]
501+
in
502+
let (ctxA, zero) = alloc_local ctx9 "__cat_zero" in
503+
let code =
504+
left_code @ [LocalSet a] @ right_code @ [LocalSet b] @
505+
[ LocalGet a; I32Load (2, 0); LocalSet la;
506+
LocalGet b; I32Load (2, 0); LocalSet lb;
507+
I32Const 0l; LocalSet zero;
508+
(* dst = heap; heap += 4 + (la+lb)*4 *)
509+
GlobalGet heap_idx; LocalSet dst;
510+
GlobalGet heap_idx;
511+
I32Const 4l;
512+
LocalGet la; LocalGet lb; I32Add; I32Const 4l; I32Mul;
513+
I32Add; I32Add;
514+
GlobalSet heap_idx;
515+
(* dst length = la + lb *)
516+
LocalGet dst; LocalGet la; LocalGet lb; I32Add; I32Store (2, 0) ]
517+
@ copy_loop a la zero (* dst[0..la) := a *)
518+
@ copy_loop b lb la (* dst[la..) := b *)
519+
@ [ LocalGet dst ]
520+
in
521+
Ok (ctxA, code)
522+
469523
| ExprBinary (left, op, right) ->
470524
let* (ctx', left_code) = gen_expr ctx left in
471525
let* (ctx'', right_code) = gen_expr ctx' right in

tests/codegen/list_concat.affine

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// SPDX-FileCopyrightText: 2026 hyperpolymath
3+
//
4+
// Regression for the `++` list-concat codegen defect. `OpConcat` was a
5+
// placeholder `I32Add` (it summed the two list *pointers*), so every
6+
// `a ++ b` produced a garbage/zero-length list. Like #255's loop bug
7+
// this had ZERO coverage — no asserted fixture exercised `++`. Covers
8+
// the three real shapes: literal++literal, mut-accumulate append in a
9+
// loop, and prepend in a loop.
10+
11+
fn sum(xs: [Int]) -> Int {
12+
let mut s = 0;
13+
for x in xs {
14+
s = s + x;
15+
}
16+
s
17+
}
18+
19+
fn main() -> Int {
20+
let a = [10, 20] ++ [30, 40, 50]; // -> [10,20,30,40,50], sum 150
21+
22+
let mut b = [];
23+
let mut i = 1;
24+
while i <= 4 {
25+
b = b ++ [i]; // append: [1,2,3,4], sum 10
26+
i = i + 1;
27+
}
28+
29+
let mut c = [];
30+
let mut j = 1;
31+
while j <= 3 {
32+
c = [j] ++ c; // prepend: [3,2,1], sum 6
33+
j = j + 1;
34+
}
35+
36+
sum(a) + sum(b) + sum(c) // 150 + 10 + 6 = 166
37+
}

tests/codegen/test_list_concat.mjs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// SPDX-License-Identifier: MIT OR AGPL-3.0-or-later
2+
// SPDX-FileCopyrightText: 2026 hyperpolymath
3+
//
4+
// Regression: `++` list concatenation must actually concatenate.
5+
// Pre-fix this returned 0 (OpConcat was a placeholder I32Add).
6+
7+
import { readFile } from 'fs/promises';
8+
9+
const wasmBuffer = await readFile('./tests/codegen/list_concat.wasm');
10+
const { instance } = await WebAssembly.instantiate(wasmBuffer, {
11+
wasi_snapshot_preview1: { fd_write: () => 0 },
12+
});
13+
const result = instance.exports.main();
14+
15+
console.log(`Result: ${result}`);
16+
console.log('Expected: 166');
17+
console.log(`Test ${result === 166 ? 'PASSED ✓' : 'FAILED ✗'}`);
18+
process.exit(result === 166 ? 0 : 1);

0 commit comments

Comments
 (0)