Skip to content

Commit 1457400

Browse files
committed
compiler: string-wall slice 2 — string_from_char_code (write-side ABI)
Phase F slice 2 of the variable-string backend wall (proposals/MIGRATION-PLAN.adoc §"The two walls"). Add the wasm-backend lowering for string_from_char_code, the first heap-allocating string op and the write side of the [len: i32 LE][utf8] ABI whose read side landed in slice 1. string_from_char_code(n) bump-allocates a one-byte string [len=1][byte] on the heap (via the existing gen_heap_alloc, the same allocator closures and enum variants use), where the byte is the low 8 bits of n — matching the interp oracle String.make 1 (Char.chr (n land 0xff)). I32Store8 writes only the low 8 bits, so it performs the land 0xff mask itself, including the correct result for negative n (-1 stores 0xFF, read back as 255 via slice 1's I32Load8U). No new memory machinery is introduced. Tests: * test/test_e2e.ml group "E2E String-wall slice 2 (string_from_char_code)": seven interp-oracle cases (round-trip, NUL byte, high byte, mask-overflow 256->0, mask-negative -1->255, length==1, OOB-after-construction), the interp consumer coverage mandated by .claude/CLAUDE.md §"Test-fixture hygiene". Runs under dune runtest. * tests/codegen/string_from_char_code.{affine,...mjs}: executable wasm parity via tools/run_codegen_wasm_tests.sh (CI). Both string harnesses (slice 1 + slice 2) green; no sibling regressions. Parity 8/8 (interp vs wasm) over masking, NUL, 255, 256, -1, 320, length, and index-past-end. Read+write byte primitives now both present; runtime-length ops (string_sub/slice/concat) remain for slice 3. Evidence: proposals/EVIDENCE-stringwall-slice2.adoc https://claude.ai/code/session_01WoKhFQePiRsAj7aqnxbG8s
1 parent f510c1f commit 1457400

6 files changed

Lines changed: 303 additions & 7 deletions

File tree

lib/codegen.ml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,36 @@ let rec gen_expr (ctx : context) (expr : expr) : (context * instr list) result =
951951
let* (ctx_with_arg, arg_code) = gen_expr ctx (List.hd args) in
952952
Ok (ctx_with_arg, arg_code)
953953

954+
| ExprVar id when id.name = "string_from_char_code"
955+
&& List.length args = 1 ->
956+
(* PHASE-F string-wall slice 2: the write-side of the
957+
`[len: i32 LE][utf8 bytes...]` ABI. `string_from_char_code(n)`
958+
builds a one-byte string from the low 8 bits of `n` — matching
959+
the interp oracle (lib/interp.ml: `String.make 1 (Char.chr
960+
(n land 0xff))`). This is the first heap-allocating string op:
961+
bump-allocate 5 bytes `[len=1][byte]`, store the length word and
962+
the byte, and leave the base pointer as the result.
963+
964+
`I32Store8` writes only the low 8 bits, so it performs the
965+
`land 0xff` masking itself (incl. the correct result for negative
966+
`n`: e.g. -1 stores 0xFF, read back as 255 via the slice-1
967+
`I32Load8U`), so no explicit mask instruction is needed. *)
968+
let* (ctx_n, n_code) = gen_expr ctx (List.hd args) in
969+
let (ctx_a, alloc_code) = gen_heap_alloc ctx_n 5 in
970+
let (ctx_p, ptr_local) = alloc_local ctx_a "__sfcc_ptr" in
971+
let (ctx_v, val_local) = alloc_local ctx_p "__sfcc_val" in
972+
let code =
973+
n_code @ [LocalSet val_local] @ (* val_local = n *)
974+
alloc_code @ [LocalSet ptr_local] @ (* ptr_local = base addr *)
975+
(* [ptr + 0] = length 1 *)
976+
[ LocalGet ptr_local; I32Const 1l; I32Store (2, 0) ] @
977+
(* [ptr + 4] = low byte of n (I32Store8 truncates) *)
978+
[ LocalGet ptr_local; LocalGet val_local; I32Store8 (0, 4) ] @
979+
(* result: the string pointer *)
980+
[ LocalGet ptr_local ]
981+
in
982+
Ok (ctx_v, code)
983+
954984
| ExprVar id when (id.name = "env_at" || id.name = "arg_at")
955985
&& List.length args = 1 ->
956986
(* ADR-015 S5 (#180): env_at(i) / arg_at(i) — fetch the i-th
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// SPDX-FileCopyrightText: 2025-2026 hyperpolymath
3+
= Phase F — string-wall slice 2: string_from_char_code (evidence)
4+
:toc: macro
5+
6+
[IMPORTANT]
7+
====
8+
*Slice landed: the write-side of the string ABI.* `string_from_char_code(n)`
9+
— already wired in resolve/typecheck/interp — gained a wasm-backend lowering
10+
that *bump-allocates* a one-byte string `[len: i32 LE = 1][byte]` on the heap,
11+
where the byte is the low 8 bits of `n`. This is the first heap-allocating
12+
string op, establishing the write side of the `[len][utf8]` ABI whose read
13+
side landed in slice 1.
14+
15+
Second slice of the *variable-string backend* wall
16+
(`proposals/MIGRATION-PLAN.adoc` §"The two walls", Phase F). Remaining
17+
write-side ops (`string_sub`/`slice`/concat/case-fold) need *runtime-length*
18+
allocation — deferred to slice 3, which will extract the shared
19+
runtime-length helper once two real callers fix its signature.
20+
====
21+
22+
toc::[]
23+
24+
== What was missing
25+
26+
`string_from_char_code` was wired end-to-end except the wasm backend:
27+
28+
[cols="2,1,1,1,1",options="header"]
29+
|===
30+
| Builtin | resolve.ml | typecheck.ml | interp.ml | codegen.ml (wasm)
31+
| `string_char_code_at` | ✓ | ✓ | ✓ | ✓ (slice 1)
32+
| `char_to_int` | ✓ | ✓ | ✓ | ✓ (slice 1)
33+
| `string_from_char_code` | ✓ | ✓ | ✓ | *was missing -> added*
34+
|===
35+
36+
Before this slice it failed at codegen:
37+
38+
----
39+
Code generation error: (Codegen.UnboundVariable
40+
"Function or variable not found: string_from_char_code")
41+
----
42+
43+
== The lowering (lib/codegen.ml)
44+
45+
`string_from_char_code(n)` — interp oracle (lib/interp.ml):
46+
`String.make 1 (Char.chr (n land 0xff))`.
47+
48+
----
49+
n_code; LocalSet val ;; val = n
50+
gen_heap_alloc 5; LocalSet ptr ;; ptr = bump-allocated base (5 bytes)
51+
LocalGet ptr; I32Const 1; I32Store (offset=0) ;; [ptr+0] = length 1
52+
LocalGet ptr; LocalGet val; I32Store8 (offset=4) ;; [ptr+4] = low byte of n
53+
LocalGet ptr ;; result = the string pointer
54+
----
55+
56+
`I32Store8` writes only the low 8 bits, so it performs the `land 0xff`
57+
masking itself — including the correct result for negative `n` (`-1` stores
58+
`0xFF`, read back as `255` via slice 1's `I32Load8U`). No explicit mask
59+
instruction is needed.
60+
61+
The allocation reuses the existing bump allocator (`gen_heap_alloc`, the same
62+
one closures and enum variants use), so no new memory machinery is
63+
introduced.
64+
65+
== Gate evidence
66+
67+
=== Gate 1 — builds
68+
69+
`dune build bin/main.exe` exit 0. The previously-failing probe now compiles
70+
and round-trips with slice 1's reader:
71+
72+
----
73+
$ affinescript compile sfcc.affine -o sfcc.wasm # string_char_code_at(string_from_char_code(66), 0)
74+
Compiled sfcc.affine -> sfcc.wasm (WASM)
75+
$ node ... main() => 66
76+
----
77+
78+
=== Gate 2 — parity (wasm vs interpreter oracle)
79+
80+
Same inputs through both backends agree across masking, NUL, negatives,
81+
overflow, length, and out-of-bounds-after-construction. Interp oracle
82+
confirmed against the real library API; wasm executed under Node.
83+
84+
[cols="4,1,1",options="header"]
85+
|===
86+
| Expression | interp | wasm
87+
| `scca(string_from_char_code(66), 0)` | 66 | 66
88+
| `scca(string_from_char_code(0), 0)` (NUL) | 0 | 0
89+
| `scca(string_from_char_code(255), 0)` | 255 | 255
90+
| `scca(string_from_char_code(256), 0)` (mask) | 0 | 0
91+
| `scca(string_from_char_code(-1), 0)` (low byte) | 255 | 255
92+
| `scca(string_from_char_code(320), 0)` (mask) | 64 | 64
93+
| `string_length(string_from_char_code(65))` | 1 | 1
94+
| `scca(string_from_char_code(65), 1)` (OOB) | -1 | -1
95+
|===
96+
97+
(`scca` = `string_char_code_at`, the slice-1 reader.)
98+
99+
The packed fixture `tests/codegen/string_from_char_code.affine` returns
100+
`4283202` (positional pack of three round-tripped bytes + the boundary
101+
trio); both backends produce it.
102+
103+
== Tests added
104+
105+
* `test/test_e2e.ml` — group *"E2E String-wall slice 2 (string_from_char_code)"*:
106+
seven interp-oracle cases (round-trip, NUL byte, high byte, mask-overflow,
107+
mask-negative, length, OOB-after-construction). Runs under `dune runtest`.
108+
The interp consumer coverage mandated by `.claude/CLAUDE.md`
109+
§"Test-fixture hygiene".
110+
* `tests/codegen/string_from_char_code.affine` +
111+
`tests/codegen/test_string_from_char_code.mjs` — executable wasm parity,
112+
run by `tools/run_codegen_wasm_tests.sh` (CI).
113+
114+
Full `tools/run_codegen_wasm_tests.sh` run: *all* codegen WASM tests pass
115+
(both slice-1 and slice-2 string harnesses), no sibling regressions.
116+
117+
== Corpus impact
118+
119+
This slice adds string *construction from a code point* — the building block
120+
for any kernel that emits text one byte at a time (manual int->string
121+
ladders, single-character separators, byte-builders). Combined with slice 1's
122+
indexing, the read+write byte primitives are now both present.
123+
124+
It does *not* yet unblock ops that copy *ranges* of bytes (`string_sub`,
125+
`slice`) or *concatenate* (`++`, `string_concat`) — those need a
126+
runtime-length allocation, the subject of slice 3.
127+
128+
== Next slices (variable-string backend wall)
129+
130+
. *Runtime-length allocation ABI* — a bump-alloc variant taking a runtime
131+
byte count, plus a copy-loop primitive. First consumers `string_sub(s,
132+
start, len)` and `slice(s, lo, hi)` (bounded byte copies), which between
133+
them fix the shared helper's signature.
134+
. *`startsWith` / `string_find`* — prefix/substring scans (read-side; build on
135+
slice-1 indexing, no allocation).
136+
. *Concat + case-folding* — `++` on strings, `to_lowercase` / `to_uppercase`.
137+
138+
Each slice: add the codegen arm, an interp-parity e2e group, and a
139+
`tests/codegen/*.mjs` executable check, then re-run the census and drop the
140+
gated count.

proposals/MIGRATION-PLAN.adoc

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,14 @@ toc::[]
3838

3939
== The two walls (what ultimately gates "CLEAR")
4040

41-
. *Variable-string backend* — `String.length` + read-side *indexing*
42-
(`string_char_code_at` / `char_to_int`) now lower to wasm (Phase F slice 1,
43-
see Ledger); `startsWith` / `fromCharCode` / `string_sub` / concat / case-fold
44-
do not yet. Gates every STRING-GATED kernel (e.g. `Kernel_IO`, `DeviceType`,
45-
i18n) whose work is building/transforming strings rather than scanning them.
46-
Lives in *this* repo's compiler, so it is attackable here.
41+
. *Variable-string backend* — `String.length`, read-side *indexing*
42+
(`string_char_code_at` / `char_to_int`, slice 1) and single-byte
43+
*construction* (`string_from_char_code`, slice 2) now lower to wasm (Phase F,
44+
see Ledger); `startsWith` / `string_sub` / `slice` / concat / case-fold
45+
(the *runtime-length* ops) do not yet. Gates every STRING-GATED kernel (e.g.
46+
`Kernel_IO`, `DeviceType`, i18n) whose work is building/transforming strings
47+
rather than scanning them. Lives in *this* repo's compiler, so it is
48+
attackable here.
4749
. *Effect codegen* — module-level mutable state / `Date.now` / `Console.log`
4850
cannot lower ("references an unbound binding"). Gates every EFFECT-GATED
4951
kernel (e.g. `Kernel_Quantum`). Also a compiler task.
@@ -203,7 +205,8 @@ Heuristic:
203205
| C10 | DONE | Utils/tools/companions/narrative/proven (2026-06-05) — *classified, NO_NEW_BRAINS.* All 21 files are host-side senses. *9 bridge files* wrap already-live wasm brains: `moletairecoprocessors.wasm` (Moletaire behaviour FSM), `moletairehunger.wasm` (hunger ordinals), `missionbriefing.wasm` (mission briefing state machine), `safefloat.wasm` (safe-float arithmetic), `passwordcracker.wasm` (password-crack score), `portscanner.wasm` (port-scan classifier), `colorpalette.wasm` (color lookup), `fontscale.wasm` (font-scale arithmetic), `gamei18n.wasm` (plural-form + language-cycle). Float-wall: `MoletaireHunger.res` (f64 gravity/hunger physics — integer enum functions already in `moletairehunger.wasm`), `SafeAngle.res` (Math.atan2/cos/sin/mod_float, all f64). String-wall: `ProvenError.res` (all constructors carry string fields), `MoletairePersistence.res` (equipmentToString/FromString), `Locales.res` (5-language translation dicts, ~650 string pairs), `PolyglotI18n.res` (String.replaceAll, async fetch of locale JSON). Effect-gated: `Announcer.res` (AccessibilitySettings + DomA11y), `DomA11y.res` (%raw DOM), `ColorPalette.res` (AccessibilitySettings mutable refs; color tables already in wasm), `KeyboardNav.res` (%raw window events), `PanicHandler.res` (Console.error), `UserSettings.res` (Storage/Audio/DesktopIntegration; stance enum string-serialized). No 4-gate run. Evidence: `migrated/EVIDENCE-C10.adoc`. NEXT: C12.
204206
| C12 | DONE | Render-glue screens/PixiJS bindings (2026-06-05) — *classified, NO_NEW_BRAINS.* All 43 files are host-side senses. *12 bridge files* wrap already-live wasm brains (companionsrenderlogic, devicesrenderlogic, enemiesrenderlogic, toolspickupsrenderlogic, playerrenderlogic, narrativepopupsrenderlogic, balanceanalyser, locationdata, screenglitchfx, screensrunlooplogic, uirenderlogic, verisimprovenrenderlogic). *23 screen/popup files* are effect-gated PixiJS rendering orchestrators (GameI18n string calls, Motion.animate, mutable refs, Navigation APIs). *4 binding files* (Motion, Pixi, PixiSound, PixiUI) are pure `external` FFI declarations wrapping host-side JS libraries. *Phase Ω cluster sequence (C11→C9→C10→C12) complete.* Genuine compiler gates remain: string wall (71 corpus files — `[len:i32][utf8]` layout already in `codegen.ml:375`, only ops missing), effect wall (111 corpus files). Evidence: `migrated/EVIDENCE-C12.adoc`.
205207
| F (string slice 1) | DONE | String-wall slice 1 — *read-side indexing landed in the wasm backend* (2026-06-12, Opus). `string_char_code_at(s, i)` + `char_to_int(c)` (already wired in resolve/typecheck/interp) gained wasm lowerings on the `[len: i32 LE][utf8]` ABI: byte `i` at `base + 4 + i`, bounds-guarded `If` load, `-1` OOB sentinel shared with the interp oracle; `char_to_int` is the i32 identity. A previously STRING-GATED shape (byte indexing into a literal) now *compiles to wasm and parity-greens* vs the interpreter. *Gates:* G1 builds; G2 parity 7/7 (interp oracle vs wasm: 65/66/67/-1/-1/-1/90); type safety preserved (`char_to_int(65)` still a Char/Int error). Tests: `test/test_e2e.ml` group "E2E String-wall slice 1 (indexing)" (interp, `dune runtest`) + `tests/codegen/string_char_code_at.{affine,…mjs}` (executable wasm parity, full `run_codegen_wasm_tests.sh` green, no regressions). Evidence: `proposals/EVIDENCE-stringwall-slice1.adoc`. Unblocks the *indexing/scan* family; allocation-returning string ops (`string_from_char_code`/`string_sub`/`slice`/concat/case-fold) are later slices. NEXT: allocation-returning ABI (`gen_string_alloc` + `string_from_char_code`).
206-
| F+ | TODO | Remaining string-wall slices (allocation-returning ABI -> `string_from_char_code` -> `string_sub`/`slice` -> `startsWith`/`string_find` -> concat/case-fold), then the effect-codegen wall (module-state / `Date.now` / `Console.log`). *Cluster migration complete; string slice 1 landed.*
208+
| F (string slice 2) | DONE | String-wall slice 2 — *write-side ABI landed in the wasm backend* (2026-06-12, Opus). `string_from_char_code(n)` (already wired in resolve/typecheck/interp) gained a wasm lowering: bump-allocate `[len: i32 LE = 1][byte]` where the byte is the low 8 bits of `n` (`I32Store8` does the `land 0xff` mask itself, incl. negatives). First heap-allocating string op; reuses the existing `gen_heap_alloc` bump allocator (no new memory machinery). *Gates:* G1 builds; G2 parity 8/8 (interp vs wasm over masking/NUL/255/256/-1/320/length/OOB-after-construction); round-trips with slice-1's reader. Tests: `test/test_e2e.ml` group "E2E String-wall slice 2 (string_from_char_code)" (interp, `dune runtest`) + `tests/codegen/string_from_char_code.{affine,…mjs}` (executable wasm parity; full `run_codegen_wasm_tests.sh` green, no regressions). Evidence: `proposals/EVIDENCE-stringwall-slice2.adoc`. Read+write byte primitives now both present. NEXT: runtime-length allocation ABI for `string_sub`/`slice` (slice 3).
209+
| F+ | TODO | Remaining string-wall slices (runtime-length alloc ABI -> `string_sub`/`slice` -> `startsWith`/`string_find` -> concat/case-fold), then the effect-codegen wall (module-state / `Date.now` / `Console.log`). *Cluster migration complete; string slices 1+2 landed (read+write byte primitives).*
207210
| Ω | TODO (access-gated) | Cutover + ReScript extinction.
208211
|===
209212

test/test_e2e.ml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3739,6 +3739,53 @@ let stringwall_index_tests = [
37393739
Alcotest.test_case "char_to_int('Z') == 90" `Quick test_stringwall_char_to_int;
37403740
]
37413741

3742+
(* ---- PHASE-F string-wall slice 2: string_from_char_code ----
3743+
3744+
`string_from_char_code(n)` gained a wasm-backend lowering (the write-side
3745+
of the [len: i32 LE][utf8] ABI): bump-allocate [len=1][byte], where the
3746+
byte is the low 8 bits of n. The interp binding already existed
3747+
(lib/interp.ml: String.make 1 (Char.chr (n land 0xff))); these pin the
3748+
interp oracle the wasm lowering must reproduce. Observable via the
3749+
slice-1 reader string_char_code_at and via string_length. *)
3750+
3751+
let test_stringwall_sfcc_roundtrip () =
3752+
Alcotest.(check int) "scca(sfcc(66), 0) == 66" 66
3753+
(eval_int_fn "fn f() -> Int { string_char_code_at(string_from_char_code(66), 0) }")
3754+
3755+
let test_stringwall_sfcc_nul () =
3756+
Alcotest.(check int) "code 0 (NUL byte) survives" 0
3757+
(eval_int_fn "fn f() -> Int { string_char_code_at(string_from_char_code(0), 0) }")
3758+
3759+
let test_stringwall_sfcc_high_byte () =
3760+
Alcotest.(check int) "code 255 survives (unsigned byte)" 255
3761+
(eval_int_fn "fn f() -> Int { string_char_code_at(string_from_char_code(255), 0) }")
3762+
3763+
let test_stringwall_sfcc_mask_overflow () =
3764+
Alcotest.(check int) "256 masked to low byte == 0" 0
3765+
(eval_int_fn "fn f() -> Int { string_char_code_at(string_from_char_code(256), 0) }")
3766+
3767+
let test_stringwall_sfcc_mask_negative () =
3768+
Alcotest.(check int) "low byte of -1 == 255" 255
3769+
(eval_int_fn "fn f() -> Int { string_char_code_at(string_from_char_code(-1), 0) }")
3770+
3771+
let test_stringwall_sfcc_length () =
3772+
Alcotest.(check int) "string_length(sfcc(n)) == 1" 1
3773+
(eval_int_fn "fn f() -> Int { string_length(string_from_char_code(65)) }")
3774+
3775+
let test_stringwall_sfcc_oob () =
3776+
Alcotest.(check int) "index 1 into a 1-byte string == -1" (-1)
3777+
(eval_int_fn "fn f() -> Int { string_char_code_at(string_from_char_code(65), 1) }")
3778+
3779+
let stringwall_alloc_tests = [
3780+
Alcotest.test_case "scca(sfcc(66),0) == 66" `Quick test_stringwall_sfcc_roundtrip;
3781+
Alcotest.test_case "sfcc(0) NUL byte == 0" `Quick test_stringwall_sfcc_nul;
3782+
Alcotest.test_case "sfcc(255) == 255" `Quick test_stringwall_sfcc_high_byte;
3783+
Alcotest.test_case "sfcc(256) masked == 0" `Quick test_stringwall_sfcc_mask_overflow;
3784+
Alcotest.test_case "sfcc(-1) low byte == 255" `Quick test_stringwall_sfcc_mask_negative;
3785+
Alcotest.test_case "string_length(sfcc) == 1" `Quick test_stringwall_sfcc_length;
3786+
Alcotest.test_case "index past 1-byte string == -1" `Quick test_stringwall_sfcc_oob;
3787+
]
3788+
37423789
(* ---- STDLIB-04b: Throws extern `error<T>` (Refs #329) ----
37433790
37443791
`error<T>(msg: String) -> T / Throws` was declared in
@@ -4847,6 +4894,7 @@ let tests =
48474894
("E2E STDLIB-04d IO #331", stdlib_04d_io_tests);
48484895
("E2E STDLIB-04e Pure #332", stdlib_04e_pure_tests);
48494896
("E2E String-wall slice 1 (indexing)", stringwall_index_tests);
4897+
("E2E String-wall slice 2 (string_from_char_code)", stringwall_alloc_tests);
48504898
("E2E STDLIB-04b error #329", stdlib_04b_error_tests);
48514899
("E2E Vscode Bindings", vscode_bindings_tests);
48524900
("E2E Array Type Sugar", array_type_tests);
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// SPDX-FileCopyrightText: 2025-2026 hyperpolymath
3+
//
4+
// PHASE-F string-wall slice 2: string_from_char_code — the write-side of
5+
// the [len: i32 LE][utf8 bytes...] ABI (proposals/MIGRATION-PLAN.adoc
6+
// §"The two walls"). string_from_char_code(n) bump-allocates a one-byte
7+
// string [len=1][byte] whose byte is the low 8 bits of n. Read back via
8+
// the slice-1 reader string_char_code_at and via string_length.
9+
//
10+
// The companion harness test_string_from_char_code.mjs asserts main()
11+
// against the value derived from the interp oracle (lib/interp.ml:
12+
// String.make 1 (Char.chr (n land 0xff))); the same constants are pinned
13+
// interp-side in test/test_e2e.ml (E2E String-wall slice 2).
14+
15+
fn main() -> Int {
16+
// build one-byte strings and read the byte back (slice-1 reader)
17+
let a = string_char_code_at(string_from_char_code(65), 0); // 'A' = 65
18+
let z = string_char_code_at(string_from_char_code(90), 0); // 'Z' = 90
19+
let masked = string_char_code_at(string_from_char_code(321), 0); // 321 & 0xff = 65
20+
let neg = string_char_code_at(string_from_char_code(-1), 0); // low byte of -1 = 255
21+
let len = string_length(string_from_char_code(0)); // NUL byte; length still 1
22+
let oob = string_char_code_at(string_from_char_code(65), 1); // 1 byte only -> -1
23+
24+
// Positional pack of the three round-tripped bytes (each 0..255, so the
25+
// 24-bit pack stays well within i32), plus the boundary trio as an
26+
// additive checksum:
27+
// packed = 65 + 90*256 + 65*65536 = 4282945
28+
// + neg(255) + len(1) - oob(-1) = +257
29+
// total = 4283202
30+
let packed = a + z * 256 + masked * 65536;
31+
packed + neg + len - oob
32+
}

0 commit comments

Comments
 (0)