|
Important
|
Slice landed: read-side string indexing in the wasm backend. The
This is the first slice of the variable-string backend wall
( |
The string-builtin surface was wired end-to-end except the wasm backend:
| Builtin | resolve.ml | typecheck.ml | interp.ml | codegen.ml (wasm) |
|---|---|---|---|---|
|
✓ |
✓ |
✓ |
✓ (pre-existing, |
|
✓ |
✓ |
✓ |
was missing → added |
|
✓ |
✓ |
✓ |
was missing → added |
Before this slice, compiling string_char_code_at(s, i) failed at codegen:
Code generation error: (Codegen.UnboundVariable "Function or variable not found: string_char_code_at")
The builtin fell through the gen_call dispatch to the generic
named-function path, which only knows user functions + the already-handled
builtins — hence "unbound". The fix is a dispatch arm that emits the load
sequence, exactly as string_length already does for length.
String layout is [len: i32 LE] at the base pointer, with utf8 bytes
starting at offset 4. Byte i therefore lives at base + 4 + i.
string_char_code_at(s, i) — total function, -1 on out-of-bounds (the
shared absent-byte sentinel; the interp oracle returns the same):
s_code; LocalSet base i_code; LocalSet idx ;; condition: (idx >= 0) && (idx < len) LocalGet idx; I32Const 0; I32GeS LocalGet idx; LocalGet base; I32Load; I32LtS I32And If (result i32) ;; in-bounds: zero-extend byte at base + idx + 4 then: LocalGet base; LocalGet idx; I32Add; I32Load8U (offset=4) ;; out-of-bounds sentinel else: I32Const -1
The bounds test guards the load (If, not Select): an out-of-bounds
I32Load8U could trap or read foreign linear memory, so the load must not
execute unless the index is in range. Select would evaluate both arms.
char_to_int(c) — identity on the wasm value lattice. A Char is already
an i32 byte code (LitChar lowers to I32Const (Char.code c)), so the arm
just evaluates the argument and leaves it; no conversion instruction.
dune build bin/main.exe exit 0. The previously-failing probe now compiles:
$ affinescript compile idx.affine -o idx.wasm # fn main = string_char_code_at("ABC",1)
Compiled idx.affine -> idx.wasm (WASM) # exit 0
Same inputs through both backends agree. Interp oracle confirmed against the
real library API (Parse_driver.parse_string → Interp.eval_program →
Value.lookup_env "f" → apply_function); wasm executed under Node.
| Expression | interp | wasm |
|---|---|---|
|
65 |
65 |
|
66 |
66 |
|
67 |
67 |
|
-1 |
-1 |
|
-1 |
-1 |
|
-1 |
-1 |
|
90 |
90 |
The packed fixture tests/codegen/string_char_code_at.affine returns
4276896 (positional base-256 pack of the three in-bounds bytes, minus the
three -1 sentinels, plus char_to_int('Z')); both backends produce it.
-
test/test_e2e.ml— group "E2E String-wall slice 1 (indexing)": seven interp-oracle cases (in-bounds bytes, both OOB directions, empty string,char_to_int). Runs underdune runtest. This is the interp consumer coverage mandated by.claude/CLAUDE.md§"Test-fixture hygiene". -
tests/codegen/string_char_code_at.affine
tests/codegen/test_string_char_code_at.mjs— executable wasm parity, run bytools/run_codegen_wasm_tests.sh(CI). Assertsmain()against the oracle constant; the same constants are pinned interp-side above.
Full tools/run_codegen_wasm_tests.sh run: all codegen WASM tests pass,
including the new harness, no sibling regressions.
This slice unblocks the indexing/scan shape — code that reads bytes out of
a string and compares/branches on them (first-byte dispatch, manual scanning,
char-class checks). It does not yet unblock string-gated kernels whose work
is dominated by building new strings (*ToString/*FromString
serialisers, String.replaceAll interpolation, translation-dict lookups
keyed by string), which need the allocation-returning ops in later slices.
-
Allocation-returning ABI — a shared
gen_string_allochelper writing the[len][bytes]shape into the heap, thenstring_from_char_code(n)as its first consumer (one-byte string[1][n & 0xff]). -
string_sub/slice— bounded copy loops over the byte range. -
startsWith/string_find— prefix/substring scans (read-side; can build on this slice’s indexing without allocation). -
Concat + case-folding —
to_lowercase/to_uppercase/++on strings.
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.