|
Important
|
Slice landed: the runtime-length copy op. Third slice of the variable-string backend wall
( |
| Builtin | resolve.ml | typecheck.ml | interp.ml | codegen.ml (wasm) |
|---|---|---|---|---|
|
✓ |
✓ |
✓ |
✓ (slice 1) |
|
✓ |
✓ |
✓ |
✓ (slice 2) |
|
✓ |
✓ |
✓ |
was missing → added |
Before this slice it failed at codegen:
Code generation error: (Codegen.UnboundVariable "Function or variable not found: string_sub")
Interp oracle (lib/interp.ml):
slen = String.length s start' = max 0 (min start slen) length' = max 0 (min length (slen - start')) String.sub s start' length'
The wasm lowering, on the [len: i32 LE][utf8] ABI:
-
Evaluate
s(→ base pointer),start,length; loadslenfrom[src+0]. -
Clamp:
start' = max(0, min(start, slen)), thenlength' = max(0, min(length, slen - start')).min/maxuseSelectover the operand pair (the operands are locals/constants — no side effects, so evaluating both arms is safe, unlike the bounds-guarded load in slice 1). -
Runtime-sized allocation:
dst = heap; heap = 4 + length'` — the bump allocator (`ensure_heap_ptr`) with a runtime byte count, exactly as list `+allocates4 + (la+lb)*4. -
Store
length'at[dst+0]. -
Copy loop (
Block [ Loop [ k>=length' ? break; dst[4+k] = src[4+start'+k]; k; continue ] ]`), byte-wise via `I32Load8U` / `I32Store8`. Same `Block`/`Loop`/`BrIf 1`/`Br 0` idiom as the list-`element copy, with a 1-byte stride instead of 4. -
Return
dst.
All clamps are non-negative (max 0), so no negative addresses are formed.
length' = 0 exits the loop immediately and yields the empty string.
dune build bin/main.exe exit 0. The previously-failing op now compiles and
round-trips with the slice-1 reader.
Same inputs through both backends agree across normal extraction, both clamps, negative start, and zero length. Interp oracle confirmed against the real library API; wasm executed under Node.
| Expression | interp | wasm |
|---|---|---|
|
101 |
101 |
|
108 |
108 |
|
3 |
3 |
|
5 |
5 |
|
3 |
3 |
|
0 |
0 |
|
104 |
104 |
|
0 |
0 |
(scca = string_char_code_at, the slice-1 reader.)
The packed fixture tests/codegen/string_sub.affine returns 6843501
(positional pack of three round-tripped bytes + four length probes); both
backends produce it.
-
test/test_e2e.ml— group "E2E String-wall slice 3 (string_sub)": eight interp-oracle cases (extraction, length, length-clamp, start-clamp, negative-start, zero-length). Runs underdune runtest. The interp consumer coverage mandated by.claude/CLAUDE.md§"Test-fixture hygiene". -
tests/codegen/string_sub.affine+tests/codegen/test_string_sub.mjs— executable wasm parity, run bytools/run_codegen_wasm_tests.sh(CI).
Full tools/run_codegen_wasm_tests.sh run: all codegen WASM tests pass
(slices 1, 2, and 3 string harnesses), no sibling regressions.
The runtime-length allocation + byte-copy loop are now established string primitives. This unblocks substring extraction directly, and gives the next slices their building blocks:
-
Concatenation (
` on strings, `string_concat`) — allocate `4 + la + lb`, copy both byte ranges (two copy loops, like list `). -
Case-folding (
to_lowercase/to_uppercase) — allocate4 + slen, copy with a per-byte transform in the loop body. -
Scans (
startsWith,string_find) — comparison loops over slice-1 indexing; no allocation.
-
Concatenation —
string_concat/++on strings (two copy loops into a4 + la + lballocation). -
Scans —
string_find(substring search returning an index), the read-side loop with no allocation. -
Case-folding —
to_lowercase/to_uppercase(copy-with-transform). -
slice— the polymorphic (array|string) JS-semantics variant with negative-index normalisation; needs compile-time array-vs-string dispatch.
Each slice: add the codegen arm, an interp-parity e2e group, and a
tests/codegen/*.mjs executable check, then re-run the census and drop the
gated count.