1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 08:55:38 +00:00
nescript/examples/sha256/sha_core.ne

642 lines
16 KiB
Text
Raw Normal View History

examples/sha256: interactive SHA-256 hasher with on-screen keyboard An end-to-end FIPS 180-4 SHA-256 hasher running entirely on the NES. The player types up to 16 ASCII characters on a 5x8 on-screen keyboard, presses Enter, and the program computes and displays the 64-character hex digest. Layout (`examples/sha256/*.ne`): constants.ne layout + K[64] / H_INIT[8] tables (declared as `var` with init_array because the v0.1 compiler treats `const u8[N] = [...]` as a no-op — noted in the file) assets.ne 44-tile Tileset (A..Z, 0..9, punctuation, special keys, cursor) shared between BG and sprite layers background.ne static nametable (title, labels, keyboard grid) painted at reset state.ne globals sha_core.ne 32-bit byte primitives (copy, xor, and, add, not, rotr, shr) in inline asm + sigma/Sigma mixers + schedule/round steps + fold render.ne OAM helpers for cursor, input buffer, and 64-nibble digest keyboard.ne key dispatch table entering_state.ne cursor navigation + typing + auto-demo computing_state.ne phased driver (48 schedule steps + 64 rounds + fold across ~30 frames at 4 iterations each) showing_state.ne renders the 256-bit digest as 8 rows of 8 sprite glyphs Implementation notes: - All 32-bit words live as 4 little-endian bytes in `wk[64]`, `w[256]`, `h_state[32]` so every primitive walks four bytes with `LDA {arr},X`/`STA {arr},X` chains and, for adds, a carry chain. - Every primitive reads its parameters straight out of the transport slots `$04`/`$05` rather than `{dst}`/`{src}` substitutions: the inline-asm resolver looks parameters up in the analyzer's allocation table but the codegen spills them to a different per-function RAM slot, so `{dst}` would resolve to a ZP slot nothing ever writes to. Bypassing the substitution entirely sidesteps the issue without a compiler change. - Rotate-right by any amount is a byte-rotate loop plus a bit- rotate loop so the 10 SHA amounts (2, 6, 7, 11, 13, 17, 18, 19, 22, 25) all compile to a handful of chained `ROR`s. - The headless jsnes golden auto-types "NES" after 1 s of idle and captures its SHA-256 digest AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D — byte-identical to `shasum` / `hashlib.sha256(b"NES")`. Build: `cargo run --release -- build examples/sha256.ne` https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 14:02:58 +00:00
// sha256/sha_core.ne — SHA-256 block compression in NEScript.
//
// FIPS 180-4 §6.2 specifies a 256-bit hash computed over one or
// more 512-bit (= 64-byte) message blocks. The compression of a
// single block is the hot path; since our on-screen keyboard
// restricts the input to 16 characters, every message fits in
// one block after padding, so the driver below only needs to
// process one block per Enter press.
//
// Representation: every 32-bit word is held as four consecutive
// bytes, little-endian (LSB first). This choice lets the 6502
// do 32-bit arithmetic by chaining its native `ADC`, `EOR`,
// `AND`, `ROR`, and `LSR` instructions — each of which walks
// one byte per cycle group and pushes the carry into the next.
//
// Every primitive operates on byte offsets into one of three
// globally-visible byte arrays:
// wk[64] — scratch: a..h and T1/T2/Σ/tmp (see constants.ne)
// w[256] — 64 u32 message-schedule words, packed contig.
// h_state[32] — 8 u32 persistent hash words
//
// K[i] and H_INIT[i] live in RAM as `var` arrays loaded from
// the init_array initialiser at reset time (see constants.ne).
// ── 32-bit byte primitives ──────────────────────────────────
codegen: reuse analyzer's local allocations so inline asm `{param}` works Fixes compiler-bugs.md #1 — the inline-asm `{name}` resolver looks parameters up in the analyzer's `VarAllocation` table (because that's the only address map it has), but `IrCodeGen::new` was minting a parallel `$0300+` range for every function-local and ignoring what the analyzer had picked. The spill prologue wrote the param to the codegen's private address, the inline asm read from the analyzer's zero-page address, and nothing ever bridged the two — `LDA {param}` would silently load whatever the RAM clear left at the stale slot (always `0`). Fix: drop the `local_ram_next` loop and just look each local up in `allocations` by the analyzer's qualified name (`__local__{scope}__{local}`). The scope string that `gen_function` already computed for `substitute_asm_vars` is now shared with the new address-seeding loop via a `scope_prefix_for_fn(&str)` helper, so the two call sites can't drift. The analyzer's layout already satisfies the "no overlapping live locals" invariant the codegen was relying on — it scopes every local under `__local__<scope>__<name>` so two functions with a parameter named `x` land in different slots. Updated `gen_function_prologue_spills_params_to_local_ram`: the regression test for the War-era param clobbering bug was asserting the spill's destination specifically had to be an absolute address at `$0300+`. That's no longer the mechanism — the spill lands in whatever slot the analyzer assigned, which is zero page when there's room. The test now asserts the destination is *any* address outside `$04-$07`, which is the actual invariant. Reverted the `LDX $04` / `LDY $05` workaround in `examples/sha256/sha_core.ne` — every primitive there now uses `{dst}` / `{src}` / `{w_ofs}` / `{h_ofs}` / `{k_ofs}` substitution as originally intended. The "Parameter convention" comment that documented the workaround is gone. Regenerated `tests/emulator/goldens/inline_asm_demo.png`: that example's `times_four(input)` was previously returning `input` verbatim because the inline asm's `LDA {result}` / `ASL A` / `ASL A` / `STA {result}` operated on a zero-page byte that was disconnected from the NEScript-level `result` variable. With the fix, `times_four` correctly returns `input * 4`, so the smiley-tracker's frame-180 position shifts by the expected `(frame_count * 4) mod 256` delta. The other 33 ROMs remain byte-identical. Verified: - `cargo clippy --all-targets -- -D warnings` clean on both rustc 1.94.1 and 1.95.0. - `cargo test --all-targets`: 616 + 3 + 75 tests pass. - `cargo fmt --check` clean. - Full emulator harness: 34/34 ROMs match goldens. - SHA-256 of "NES" still computes to `AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D`. - `--memory-map` output now reflects what the generated code actually reads and writes (previously the codegen's $0300+ override was invisible to the dump). https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 16:03:10 +00:00
//
// Every primitive reads its destination and source offsets
// via `{dst}` / `{src}` / `{w_ofs}` / … substitutions, which
// resolve to the analyzer's per-function local slots. The
// codegen's function prologue spills the `$04`/`$05` transport
// slots into those same addresses on entry, so the values are
// already live by the time the asm block runs.
examples/sha256: interactive SHA-256 hasher with on-screen keyboard An end-to-end FIPS 180-4 SHA-256 hasher running entirely on the NES. The player types up to 16 ASCII characters on a 5x8 on-screen keyboard, presses Enter, and the program computes and displays the 64-character hex digest. Layout (`examples/sha256/*.ne`): constants.ne layout + K[64] / H_INIT[8] tables (declared as `var` with init_array because the v0.1 compiler treats `const u8[N] = [...]` as a no-op — noted in the file) assets.ne 44-tile Tileset (A..Z, 0..9, punctuation, special keys, cursor) shared between BG and sprite layers background.ne static nametable (title, labels, keyboard grid) painted at reset state.ne globals sha_core.ne 32-bit byte primitives (copy, xor, and, add, not, rotr, shr) in inline asm + sigma/Sigma mixers + schedule/round steps + fold render.ne OAM helpers for cursor, input buffer, and 64-nibble digest keyboard.ne key dispatch table entering_state.ne cursor navigation + typing + auto-demo computing_state.ne phased driver (48 schedule steps + 64 rounds + fold across ~30 frames at 4 iterations each) showing_state.ne renders the 256-bit digest as 8 rows of 8 sprite glyphs Implementation notes: - All 32-bit words live as 4 little-endian bytes in `wk[64]`, `w[256]`, `h_state[32]` so every primitive walks four bytes with `LDA {arr},X`/`STA {arr},X` chains and, for adds, a carry chain. - Every primitive reads its parameters straight out of the transport slots `$04`/`$05` rather than `{dst}`/`{src}` substitutions: the inline-asm resolver looks parameters up in the analyzer's allocation table but the codegen spills them to a different per-function RAM slot, so `{dst}` would resolve to a ZP slot nothing ever writes to. Bypassing the substitution entirely sidesteps the issue without a compiler change. - Rotate-right by any amount is a byte-rotate loop plus a bit- rotate loop so the 10 SHA amounts (2, 6, 7, 11, 13, 17, 18, 19, 22, 25) all compile to a handful of chained `ROR`s. - The headless jsnes golden auto-types "NES" after 1 s of idle and captures its SHA-256 digest AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D — byte-identical to `shasum` / `hashlib.sha256(b"NES")`. Build: `cargo run --release -- build examples/sha256.ne` https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 14:02:58 +00:00
// wk[dst..dst+4] = wk[src..src+4]
fun cp_wk(dst: u8, src: u8) {
asm {
codegen: reuse analyzer's local allocations so inline asm `{param}` works Fixes compiler-bugs.md #1 — the inline-asm `{name}` resolver looks parameters up in the analyzer's `VarAllocation` table (because that's the only address map it has), but `IrCodeGen::new` was minting a parallel `$0300+` range for every function-local and ignoring what the analyzer had picked. The spill prologue wrote the param to the codegen's private address, the inline asm read from the analyzer's zero-page address, and nothing ever bridged the two — `LDA {param}` would silently load whatever the RAM clear left at the stale slot (always `0`). Fix: drop the `local_ram_next` loop and just look each local up in `allocations` by the analyzer's qualified name (`__local__{scope}__{local}`). The scope string that `gen_function` already computed for `substitute_asm_vars` is now shared with the new address-seeding loop via a `scope_prefix_for_fn(&str)` helper, so the two call sites can't drift. The analyzer's layout already satisfies the "no overlapping live locals" invariant the codegen was relying on — it scopes every local under `__local__<scope>__<name>` so two functions with a parameter named `x` land in different slots. Updated `gen_function_prologue_spills_params_to_local_ram`: the regression test for the War-era param clobbering bug was asserting the spill's destination specifically had to be an absolute address at `$0300+`. That's no longer the mechanism — the spill lands in whatever slot the analyzer assigned, which is zero page when there's room. The test now asserts the destination is *any* address outside `$04-$07`, which is the actual invariant. Reverted the `LDX $04` / `LDY $05` workaround in `examples/sha256/sha_core.ne` — every primitive there now uses `{dst}` / `{src}` / `{w_ofs}` / `{h_ofs}` / `{k_ofs}` substitution as originally intended. The "Parameter convention" comment that documented the workaround is gone. Regenerated `tests/emulator/goldens/inline_asm_demo.png`: that example's `times_four(input)` was previously returning `input` verbatim because the inline asm's `LDA {result}` / `ASL A` / `ASL A` / `STA {result}` operated on a zero-page byte that was disconnected from the NEScript-level `result` variable. With the fix, `times_four` correctly returns `input * 4`, so the smiley-tracker's frame-180 position shifts by the expected `(frame_count * 4) mod 256` delta. The other 33 ROMs remain byte-identical. Verified: - `cargo clippy --all-targets -- -D warnings` clean on both rustc 1.94.1 and 1.95.0. - `cargo test --all-targets`: 616 + 3 + 75 tests pass. - `cargo fmt --check` clean. - Full emulator harness: 34/34 ROMs match goldens. - SHA-256 of "NES" still computes to `AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D`. - `--memory-map` output now reflects what the generated code actually reads and writes (previously the codegen's $0300+ override was invisible to the dump). https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 16:03:10 +00:00
LDX {dst}
LDY {src}
examples/sha256: interactive SHA-256 hasher with on-screen keyboard An end-to-end FIPS 180-4 SHA-256 hasher running entirely on the NES. The player types up to 16 ASCII characters on a 5x8 on-screen keyboard, presses Enter, and the program computes and displays the 64-character hex digest. Layout (`examples/sha256/*.ne`): constants.ne layout + K[64] / H_INIT[8] tables (declared as `var` with init_array because the v0.1 compiler treats `const u8[N] = [...]` as a no-op — noted in the file) assets.ne 44-tile Tileset (A..Z, 0..9, punctuation, special keys, cursor) shared between BG and sprite layers background.ne static nametable (title, labels, keyboard grid) painted at reset state.ne globals sha_core.ne 32-bit byte primitives (copy, xor, and, add, not, rotr, shr) in inline asm + sigma/Sigma mixers + schedule/round steps + fold render.ne OAM helpers for cursor, input buffer, and 64-nibble digest keyboard.ne key dispatch table entering_state.ne cursor navigation + typing + auto-demo computing_state.ne phased driver (48 schedule steps + 64 rounds + fold across ~30 frames at 4 iterations each) showing_state.ne renders the 256-bit digest as 8 rows of 8 sprite glyphs Implementation notes: - All 32-bit words live as 4 little-endian bytes in `wk[64]`, `w[256]`, `h_state[32]` so every primitive walks four bytes with `LDA {arr},X`/`STA {arr},X` chains and, for adds, a carry chain. - Every primitive reads its parameters straight out of the transport slots `$04`/`$05` rather than `{dst}`/`{src}` substitutions: the inline-asm resolver looks parameters up in the analyzer's allocation table but the codegen spills them to a different per-function RAM slot, so `{dst}` would resolve to a ZP slot nothing ever writes to. Bypassing the substitution entirely sidesteps the issue without a compiler change. - Rotate-right by any amount is a byte-rotate loop plus a bit- rotate loop so the 10 SHA amounts (2, 6, 7, 11, 13, 17, 18, 19, 22, 25) all compile to a handful of chained `ROR`s. - The headless jsnes golden auto-types "NES" after 1 s of idle and captures its SHA-256 digest AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D — byte-identical to `shasum` / `hashlib.sha256(b"NES")`. Build: `cargo run --release -- build examples/sha256.ne` https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 14:02:58 +00:00
LDA {wk},Y
STA {wk},X
INX
INY
LDA {wk},Y
STA {wk},X
INX
INY
LDA {wk},Y
STA {wk},X
INX
INY
LDA {wk},Y
STA {wk},X
}
}
// wk[dst..dst+4] ^= wk[src..src+4]
fun xor_wk(dst: u8, src: u8) {
asm {
codegen: reuse analyzer's local allocations so inline asm `{param}` works Fixes compiler-bugs.md #1 — the inline-asm `{name}` resolver looks parameters up in the analyzer's `VarAllocation` table (because that's the only address map it has), but `IrCodeGen::new` was minting a parallel `$0300+` range for every function-local and ignoring what the analyzer had picked. The spill prologue wrote the param to the codegen's private address, the inline asm read from the analyzer's zero-page address, and nothing ever bridged the two — `LDA {param}` would silently load whatever the RAM clear left at the stale slot (always `0`). Fix: drop the `local_ram_next` loop and just look each local up in `allocations` by the analyzer's qualified name (`__local__{scope}__{local}`). The scope string that `gen_function` already computed for `substitute_asm_vars` is now shared with the new address-seeding loop via a `scope_prefix_for_fn(&str)` helper, so the two call sites can't drift. The analyzer's layout already satisfies the "no overlapping live locals" invariant the codegen was relying on — it scopes every local under `__local__<scope>__<name>` so two functions with a parameter named `x` land in different slots. Updated `gen_function_prologue_spills_params_to_local_ram`: the regression test for the War-era param clobbering bug was asserting the spill's destination specifically had to be an absolute address at `$0300+`. That's no longer the mechanism — the spill lands in whatever slot the analyzer assigned, which is zero page when there's room. The test now asserts the destination is *any* address outside `$04-$07`, which is the actual invariant. Reverted the `LDX $04` / `LDY $05` workaround in `examples/sha256/sha_core.ne` — every primitive there now uses `{dst}` / `{src}` / `{w_ofs}` / `{h_ofs}` / `{k_ofs}` substitution as originally intended. The "Parameter convention" comment that documented the workaround is gone. Regenerated `tests/emulator/goldens/inline_asm_demo.png`: that example's `times_four(input)` was previously returning `input` verbatim because the inline asm's `LDA {result}` / `ASL A` / `ASL A` / `STA {result}` operated on a zero-page byte that was disconnected from the NEScript-level `result` variable. With the fix, `times_four` correctly returns `input * 4`, so the smiley-tracker's frame-180 position shifts by the expected `(frame_count * 4) mod 256` delta. The other 33 ROMs remain byte-identical. Verified: - `cargo clippy --all-targets -- -D warnings` clean on both rustc 1.94.1 and 1.95.0. - `cargo test --all-targets`: 616 + 3 + 75 tests pass. - `cargo fmt --check` clean. - Full emulator harness: 34/34 ROMs match goldens. - SHA-256 of "NES" still computes to `AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D`. - `--memory-map` output now reflects what the generated code actually reads and writes (previously the codegen's $0300+ override was invisible to the dump). https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 16:03:10 +00:00
LDX {dst}
LDY {src}
examples/sha256: interactive SHA-256 hasher with on-screen keyboard An end-to-end FIPS 180-4 SHA-256 hasher running entirely on the NES. The player types up to 16 ASCII characters on a 5x8 on-screen keyboard, presses Enter, and the program computes and displays the 64-character hex digest. Layout (`examples/sha256/*.ne`): constants.ne layout + K[64] / H_INIT[8] tables (declared as `var` with init_array because the v0.1 compiler treats `const u8[N] = [...]` as a no-op — noted in the file) assets.ne 44-tile Tileset (A..Z, 0..9, punctuation, special keys, cursor) shared between BG and sprite layers background.ne static nametable (title, labels, keyboard grid) painted at reset state.ne globals sha_core.ne 32-bit byte primitives (copy, xor, and, add, not, rotr, shr) in inline asm + sigma/Sigma mixers + schedule/round steps + fold render.ne OAM helpers for cursor, input buffer, and 64-nibble digest keyboard.ne key dispatch table entering_state.ne cursor navigation + typing + auto-demo computing_state.ne phased driver (48 schedule steps + 64 rounds + fold across ~30 frames at 4 iterations each) showing_state.ne renders the 256-bit digest as 8 rows of 8 sprite glyphs Implementation notes: - All 32-bit words live as 4 little-endian bytes in `wk[64]`, `w[256]`, `h_state[32]` so every primitive walks four bytes with `LDA {arr},X`/`STA {arr},X` chains and, for adds, a carry chain. - Every primitive reads its parameters straight out of the transport slots `$04`/`$05` rather than `{dst}`/`{src}` substitutions: the inline-asm resolver looks parameters up in the analyzer's allocation table but the codegen spills them to a different per-function RAM slot, so `{dst}` would resolve to a ZP slot nothing ever writes to. Bypassing the substitution entirely sidesteps the issue without a compiler change. - Rotate-right by any amount is a byte-rotate loop plus a bit- rotate loop so the 10 SHA amounts (2, 6, 7, 11, 13, 17, 18, 19, 22, 25) all compile to a handful of chained `ROR`s. - The headless jsnes golden auto-types "NES" after 1 s of idle and captures its SHA-256 digest AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D — byte-identical to `shasum` / `hashlib.sha256(b"NES")`. Build: `cargo run --release -- build examples/sha256.ne` https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 14:02:58 +00:00
LDA {wk},X
EOR {wk},Y
STA {wk},X
INX
INY
LDA {wk},X
EOR {wk},Y
STA {wk},X
INX
INY
LDA {wk},X
EOR {wk},Y
STA {wk},X
INX
INY
LDA {wk},X
EOR {wk},Y
STA {wk},X
}
}
// wk[dst..dst+4] &= wk[src..src+4]
fun and_wk(dst: u8, src: u8) {
asm {
codegen: reuse analyzer's local allocations so inline asm `{param}` works Fixes compiler-bugs.md #1 — the inline-asm `{name}` resolver looks parameters up in the analyzer's `VarAllocation` table (because that's the only address map it has), but `IrCodeGen::new` was minting a parallel `$0300+` range for every function-local and ignoring what the analyzer had picked. The spill prologue wrote the param to the codegen's private address, the inline asm read from the analyzer's zero-page address, and nothing ever bridged the two — `LDA {param}` would silently load whatever the RAM clear left at the stale slot (always `0`). Fix: drop the `local_ram_next` loop and just look each local up in `allocations` by the analyzer's qualified name (`__local__{scope}__{local}`). The scope string that `gen_function` already computed for `substitute_asm_vars` is now shared with the new address-seeding loop via a `scope_prefix_for_fn(&str)` helper, so the two call sites can't drift. The analyzer's layout already satisfies the "no overlapping live locals" invariant the codegen was relying on — it scopes every local under `__local__<scope>__<name>` so two functions with a parameter named `x` land in different slots. Updated `gen_function_prologue_spills_params_to_local_ram`: the regression test for the War-era param clobbering bug was asserting the spill's destination specifically had to be an absolute address at `$0300+`. That's no longer the mechanism — the spill lands in whatever slot the analyzer assigned, which is zero page when there's room. The test now asserts the destination is *any* address outside `$04-$07`, which is the actual invariant. Reverted the `LDX $04` / `LDY $05` workaround in `examples/sha256/sha_core.ne` — every primitive there now uses `{dst}` / `{src}` / `{w_ofs}` / `{h_ofs}` / `{k_ofs}` substitution as originally intended. The "Parameter convention" comment that documented the workaround is gone. Regenerated `tests/emulator/goldens/inline_asm_demo.png`: that example's `times_four(input)` was previously returning `input` verbatim because the inline asm's `LDA {result}` / `ASL A` / `ASL A` / `STA {result}` operated on a zero-page byte that was disconnected from the NEScript-level `result` variable. With the fix, `times_four` correctly returns `input * 4`, so the smiley-tracker's frame-180 position shifts by the expected `(frame_count * 4) mod 256` delta. The other 33 ROMs remain byte-identical. Verified: - `cargo clippy --all-targets -- -D warnings` clean on both rustc 1.94.1 and 1.95.0. - `cargo test --all-targets`: 616 + 3 + 75 tests pass. - `cargo fmt --check` clean. - Full emulator harness: 34/34 ROMs match goldens. - SHA-256 of "NES" still computes to `AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D`. - `--memory-map` output now reflects what the generated code actually reads and writes (previously the codegen's $0300+ override was invisible to the dump). https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 16:03:10 +00:00
LDX {dst}
LDY {src}
examples/sha256: interactive SHA-256 hasher with on-screen keyboard An end-to-end FIPS 180-4 SHA-256 hasher running entirely on the NES. The player types up to 16 ASCII characters on a 5x8 on-screen keyboard, presses Enter, and the program computes and displays the 64-character hex digest. Layout (`examples/sha256/*.ne`): constants.ne layout + K[64] / H_INIT[8] tables (declared as `var` with init_array because the v0.1 compiler treats `const u8[N] = [...]` as a no-op — noted in the file) assets.ne 44-tile Tileset (A..Z, 0..9, punctuation, special keys, cursor) shared between BG and sprite layers background.ne static nametable (title, labels, keyboard grid) painted at reset state.ne globals sha_core.ne 32-bit byte primitives (copy, xor, and, add, not, rotr, shr) in inline asm + sigma/Sigma mixers + schedule/round steps + fold render.ne OAM helpers for cursor, input buffer, and 64-nibble digest keyboard.ne key dispatch table entering_state.ne cursor navigation + typing + auto-demo computing_state.ne phased driver (48 schedule steps + 64 rounds + fold across ~30 frames at 4 iterations each) showing_state.ne renders the 256-bit digest as 8 rows of 8 sprite glyphs Implementation notes: - All 32-bit words live as 4 little-endian bytes in `wk[64]`, `w[256]`, `h_state[32]` so every primitive walks four bytes with `LDA {arr},X`/`STA {arr},X` chains and, for adds, a carry chain. - Every primitive reads its parameters straight out of the transport slots `$04`/`$05` rather than `{dst}`/`{src}` substitutions: the inline-asm resolver looks parameters up in the analyzer's allocation table but the codegen spills them to a different per-function RAM slot, so `{dst}` would resolve to a ZP slot nothing ever writes to. Bypassing the substitution entirely sidesteps the issue without a compiler change. - Rotate-right by any amount is a byte-rotate loop plus a bit- rotate loop so the 10 SHA amounts (2, 6, 7, 11, 13, 17, 18, 19, 22, 25) all compile to a handful of chained `ROR`s. - The headless jsnes golden auto-types "NES" after 1 s of idle and captures its SHA-256 digest AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D — byte-identical to `shasum` / `hashlib.sha256(b"NES")`. Build: `cargo run --release -- build examples/sha256.ne` https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 14:02:58 +00:00
LDA {wk},X
AND {wk},Y
STA {wk},X
INX
INY
LDA {wk},X
AND {wk},Y
STA {wk},X
INX
INY
LDA {wk},X
AND {wk},Y
STA {wk},X
INX
INY
LDA {wk},X
AND {wk},Y
STA {wk},X
}
}
// wk[dst..dst+4] += wk[src..src+4] (chained ADC for carry)
fun add_wk(dst: u8, src: u8) {
asm {
codegen: reuse analyzer's local allocations so inline asm `{param}` works Fixes compiler-bugs.md #1 — the inline-asm `{name}` resolver looks parameters up in the analyzer's `VarAllocation` table (because that's the only address map it has), but `IrCodeGen::new` was minting a parallel `$0300+` range for every function-local and ignoring what the analyzer had picked. The spill prologue wrote the param to the codegen's private address, the inline asm read from the analyzer's zero-page address, and nothing ever bridged the two — `LDA {param}` would silently load whatever the RAM clear left at the stale slot (always `0`). Fix: drop the `local_ram_next` loop and just look each local up in `allocations` by the analyzer's qualified name (`__local__{scope}__{local}`). The scope string that `gen_function` already computed for `substitute_asm_vars` is now shared with the new address-seeding loop via a `scope_prefix_for_fn(&str)` helper, so the two call sites can't drift. The analyzer's layout already satisfies the "no overlapping live locals" invariant the codegen was relying on — it scopes every local under `__local__<scope>__<name>` so two functions with a parameter named `x` land in different slots. Updated `gen_function_prologue_spills_params_to_local_ram`: the regression test for the War-era param clobbering bug was asserting the spill's destination specifically had to be an absolute address at `$0300+`. That's no longer the mechanism — the spill lands in whatever slot the analyzer assigned, which is zero page when there's room. The test now asserts the destination is *any* address outside `$04-$07`, which is the actual invariant. Reverted the `LDX $04` / `LDY $05` workaround in `examples/sha256/sha_core.ne` — every primitive there now uses `{dst}` / `{src}` / `{w_ofs}` / `{h_ofs}` / `{k_ofs}` substitution as originally intended. The "Parameter convention" comment that documented the workaround is gone. Regenerated `tests/emulator/goldens/inline_asm_demo.png`: that example's `times_four(input)` was previously returning `input` verbatim because the inline asm's `LDA {result}` / `ASL A` / `ASL A` / `STA {result}` operated on a zero-page byte that was disconnected from the NEScript-level `result` variable. With the fix, `times_four` correctly returns `input * 4`, so the smiley-tracker's frame-180 position shifts by the expected `(frame_count * 4) mod 256` delta. The other 33 ROMs remain byte-identical. Verified: - `cargo clippy --all-targets -- -D warnings` clean on both rustc 1.94.1 and 1.95.0. - `cargo test --all-targets`: 616 + 3 + 75 tests pass. - `cargo fmt --check` clean. - Full emulator harness: 34/34 ROMs match goldens. - SHA-256 of "NES" still computes to `AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D`. - `--memory-map` output now reflects what the generated code actually reads and writes (previously the codegen's $0300+ override was invisible to the dump). https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 16:03:10 +00:00
LDX {dst}
LDY {src}
examples/sha256: interactive SHA-256 hasher with on-screen keyboard An end-to-end FIPS 180-4 SHA-256 hasher running entirely on the NES. The player types up to 16 ASCII characters on a 5x8 on-screen keyboard, presses Enter, and the program computes and displays the 64-character hex digest. Layout (`examples/sha256/*.ne`): constants.ne layout + K[64] / H_INIT[8] tables (declared as `var` with init_array because the v0.1 compiler treats `const u8[N] = [...]` as a no-op — noted in the file) assets.ne 44-tile Tileset (A..Z, 0..9, punctuation, special keys, cursor) shared between BG and sprite layers background.ne static nametable (title, labels, keyboard grid) painted at reset state.ne globals sha_core.ne 32-bit byte primitives (copy, xor, and, add, not, rotr, shr) in inline asm + sigma/Sigma mixers + schedule/round steps + fold render.ne OAM helpers for cursor, input buffer, and 64-nibble digest keyboard.ne key dispatch table entering_state.ne cursor navigation + typing + auto-demo computing_state.ne phased driver (48 schedule steps + 64 rounds + fold across ~30 frames at 4 iterations each) showing_state.ne renders the 256-bit digest as 8 rows of 8 sprite glyphs Implementation notes: - All 32-bit words live as 4 little-endian bytes in `wk[64]`, `w[256]`, `h_state[32]` so every primitive walks four bytes with `LDA {arr},X`/`STA {arr},X` chains and, for adds, a carry chain. - Every primitive reads its parameters straight out of the transport slots `$04`/`$05` rather than `{dst}`/`{src}` substitutions: the inline-asm resolver looks parameters up in the analyzer's allocation table but the codegen spills them to a different per-function RAM slot, so `{dst}` would resolve to a ZP slot nothing ever writes to. Bypassing the substitution entirely sidesteps the issue without a compiler change. - Rotate-right by any amount is a byte-rotate loop plus a bit- rotate loop so the 10 SHA amounts (2, 6, 7, 11, 13, 17, 18, 19, 22, 25) all compile to a handful of chained `ROR`s. - The headless jsnes golden auto-types "NES" after 1 s of idle and captures its SHA-256 digest AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D — byte-identical to `shasum` / `hashlib.sha256(b"NES")`. Build: `cargo run --release -- build examples/sha256.ne` https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 14:02:58 +00:00
CLC
LDA {wk},X
ADC {wk},Y
STA {wk},X
INX
INY
LDA {wk},X
ADC {wk},Y
STA {wk},X
INX
INY
LDA {wk},X
ADC {wk},Y
STA {wk},X
INX
INY
LDA {wk},X
ADC {wk},Y
STA {wk},X
}
}
// wk[dst..dst+4] = ~wk[dst..dst+4] (bitwise NOT, in place)
fun not_wk(dst: u8) {
asm {
codegen: reuse analyzer's local allocations so inline asm `{param}` works Fixes compiler-bugs.md #1 — the inline-asm `{name}` resolver looks parameters up in the analyzer's `VarAllocation` table (because that's the only address map it has), but `IrCodeGen::new` was minting a parallel `$0300+` range for every function-local and ignoring what the analyzer had picked. The spill prologue wrote the param to the codegen's private address, the inline asm read from the analyzer's zero-page address, and nothing ever bridged the two — `LDA {param}` would silently load whatever the RAM clear left at the stale slot (always `0`). Fix: drop the `local_ram_next` loop and just look each local up in `allocations` by the analyzer's qualified name (`__local__{scope}__{local}`). The scope string that `gen_function` already computed for `substitute_asm_vars` is now shared with the new address-seeding loop via a `scope_prefix_for_fn(&str)` helper, so the two call sites can't drift. The analyzer's layout already satisfies the "no overlapping live locals" invariant the codegen was relying on — it scopes every local under `__local__<scope>__<name>` so two functions with a parameter named `x` land in different slots. Updated `gen_function_prologue_spills_params_to_local_ram`: the regression test for the War-era param clobbering bug was asserting the spill's destination specifically had to be an absolute address at `$0300+`. That's no longer the mechanism — the spill lands in whatever slot the analyzer assigned, which is zero page when there's room. The test now asserts the destination is *any* address outside `$04-$07`, which is the actual invariant. Reverted the `LDX $04` / `LDY $05` workaround in `examples/sha256/sha_core.ne` — every primitive there now uses `{dst}` / `{src}` / `{w_ofs}` / `{h_ofs}` / `{k_ofs}` substitution as originally intended. The "Parameter convention" comment that documented the workaround is gone. Regenerated `tests/emulator/goldens/inline_asm_demo.png`: that example's `times_four(input)` was previously returning `input` verbatim because the inline asm's `LDA {result}` / `ASL A` / `ASL A` / `STA {result}` operated on a zero-page byte that was disconnected from the NEScript-level `result` variable. With the fix, `times_four` correctly returns `input * 4`, so the smiley-tracker's frame-180 position shifts by the expected `(frame_count * 4) mod 256` delta. The other 33 ROMs remain byte-identical. Verified: - `cargo clippy --all-targets -- -D warnings` clean on both rustc 1.94.1 and 1.95.0. - `cargo test --all-targets`: 616 + 3 + 75 tests pass. - `cargo fmt --check` clean. - Full emulator harness: 34/34 ROMs match goldens. - SHA-256 of "NES" still computes to `AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D`. - `--memory-map` output now reflects what the generated code actually reads and writes (previously the codegen's $0300+ override was invisible to the dump). https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 16:03:10 +00:00
LDX {dst}
examples/sha256: interactive SHA-256 hasher with on-screen keyboard An end-to-end FIPS 180-4 SHA-256 hasher running entirely on the NES. The player types up to 16 ASCII characters on a 5x8 on-screen keyboard, presses Enter, and the program computes and displays the 64-character hex digest. Layout (`examples/sha256/*.ne`): constants.ne layout + K[64] / H_INIT[8] tables (declared as `var` with init_array because the v0.1 compiler treats `const u8[N] = [...]` as a no-op — noted in the file) assets.ne 44-tile Tileset (A..Z, 0..9, punctuation, special keys, cursor) shared between BG and sprite layers background.ne static nametable (title, labels, keyboard grid) painted at reset state.ne globals sha_core.ne 32-bit byte primitives (copy, xor, and, add, not, rotr, shr) in inline asm + sigma/Sigma mixers + schedule/round steps + fold render.ne OAM helpers for cursor, input buffer, and 64-nibble digest keyboard.ne key dispatch table entering_state.ne cursor navigation + typing + auto-demo computing_state.ne phased driver (48 schedule steps + 64 rounds + fold across ~30 frames at 4 iterations each) showing_state.ne renders the 256-bit digest as 8 rows of 8 sprite glyphs Implementation notes: - All 32-bit words live as 4 little-endian bytes in `wk[64]`, `w[256]`, `h_state[32]` so every primitive walks four bytes with `LDA {arr},X`/`STA {arr},X` chains and, for adds, a carry chain. - Every primitive reads its parameters straight out of the transport slots `$04`/`$05` rather than `{dst}`/`{src}` substitutions: the inline-asm resolver looks parameters up in the analyzer's allocation table but the codegen spills them to a different per-function RAM slot, so `{dst}` would resolve to a ZP slot nothing ever writes to. Bypassing the substitution entirely sidesteps the issue without a compiler change. - Rotate-right by any amount is a byte-rotate loop plus a bit- rotate loop so the 10 SHA amounts (2, 6, 7, 11, 13, 17, 18, 19, 22, 25) all compile to a handful of chained `ROR`s. - The headless jsnes golden auto-types "NES" after 1 s of idle and captures its SHA-256 digest AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D — byte-identical to `shasum` / `hashlib.sha256(b"NES")`. Build: `cargo run --release -- build examples/sha256.ne` https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 14:02:58 +00:00
LDA {wk},X
EOR #$FF
STA {wk},X
INX
LDA {wk},X
EOR #$FF
STA {wk},X
INX
LDA {wk},X
EOR #$FF
STA {wk},X
INX
LDA {wk},X
EOR #$FF
STA {wk},X
}
}
// Rotate wk[dst..dst+4] right by 1 bit, in place. Treat the
// 4-byte little-endian value as one 32-bit integer. A right-
// rotation pulls bit 0 of the LSB into bit 31 of the MSB. The
// ROR chain below first captures bit 0 of the LSB into the
// carry (via LSR A on a non-destructive copy), then runs ROR
// MSB, byte 2, byte 1, LSB in that order — each ROR pulls the
// previous byte's bit 0 into the next byte's bit 7.
fun rotr1_wk(dst: u8) {
asm {
codegen: reuse analyzer's local allocations so inline asm `{param}` works Fixes compiler-bugs.md #1 — the inline-asm `{name}` resolver looks parameters up in the analyzer's `VarAllocation` table (because that's the only address map it has), but `IrCodeGen::new` was minting a parallel `$0300+` range for every function-local and ignoring what the analyzer had picked. The spill prologue wrote the param to the codegen's private address, the inline asm read from the analyzer's zero-page address, and nothing ever bridged the two — `LDA {param}` would silently load whatever the RAM clear left at the stale slot (always `0`). Fix: drop the `local_ram_next` loop and just look each local up in `allocations` by the analyzer's qualified name (`__local__{scope}__{local}`). The scope string that `gen_function` already computed for `substitute_asm_vars` is now shared with the new address-seeding loop via a `scope_prefix_for_fn(&str)` helper, so the two call sites can't drift. The analyzer's layout already satisfies the "no overlapping live locals" invariant the codegen was relying on — it scopes every local under `__local__<scope>__<name>` so two functions with a parameter named `x` land in different slots. Updated `gen_function_prologue_spills_params_to_local_ram`: the regression test for the War-era param clobbering bug was asserting the spill's destination specifically had to be an absolute address at `$0300+`. That's no longer the mechanism — the spill lands in whatever slot the analyzer assigned, which is zero page when there's room. The test now asserts the destination is *any* address outside `$04-$07`, which is the actual invariant. Reverted the `LDX $04` / `LDY $05` workaround in `examples/sha256/sha_core.ne` — every primitive there now uses `{dst}` / `{src}` / `{w_ofs}` / `{h_ofs}` / `{k_ofs}` substitution as originally intended. The "Parameter convention" comment that documented the workaround is gone. Regenerated `tests/emulator/goldens/inline_asm_demo.png`: that example's `times_four(input)` was previously returning `input` verbatim because the inline asm's `LDA {result}` / `ASL A` / `ASL A` / `STA {result}` operated on a zero-page byte that was disconnected from the NEScript-level `result` variable. With the fix, `times_four` correctly returns `input * 4`, so the smiley-tracker's frame-180 position shifts by the expected `(frame_count * 4) mod 256` delta. The other 33 ROMs remain byte-identical. Verified: - `cargo clippy --all-targets -- -D warnings` clean on both rustc 1.94.1 and 1.95.0. - `cargo test --all-targets`: 616 + 3 + 75 tests pass. - `cargo fmt --check` clean. - Full emulator harness: 34/34 ROMs match goldens. - SHA-256 of "NES" still computes to `AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D`. - `--memory-map` output now reflects what the generated code actually reads and writes (previously the codegen's $0300+ override was invisible to the dump). https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 16:03:10 +00:00
LDX {dst}
examples/sha256: interactive SHA-256 hasher with on-screen keyboard An end-to-end FIPS 180-4 SHA-256 hasher running entirely on the NES. The player types up to 16 ASCII characters on a 5x8 on-screen keyboard, presses Enter, and the program computes and displays the 64-character hex digest. Layout (`examples/sha256/*.ne`): constants.ne layout + K[64] / H_INIT[8] tables (declared as `var` with init_array because the v0.1 compiler treats `const u8[N] = [...]` as a no-op — noted in the file) assets.ne 44-tile Tileset (A..Z, 0..9, punctuation, special keys, cursor) shared between BG and sprite layers background.ne static nametable (title, labels, keyboard grid) painted at reset state.ne globals sha_core.ne 32-bit byte primitives (copy, xor, and, add, not, rotr, shr) in inline asm + sigma/Sigma mixers + schedule/round steps + fold render.ne OAM helpers for cursor, input buffer, and 64-nibble digest keyboard.ne key dispatch table entering_state.ne cursor navigation + typing + auto-demo computing_state.ne phased driver (48 schedule steps + 64 rounds + fold across ~30 frames at 4 iterations each) showing_state.ne renders the 256-bit digest as 8 rows of 8 sprite glyphs Implementation notes: - All 32-bit words live as 4 little-endian bytes in `wk[64]`, `w[256]`, `h_state[32]` so every primitive walks four bytes with `LDA {arr},X`/`STA {arr},X` chains and, for adds, a carry chain. - Every primitive reads its parameters straight out of the transport slots `$04`/`$05` rather than `{dst}`/`{src}` substitutions: the inline-asm resolver looks parameters up in the analyzer's allocation table but the codegen spills them to a different per-function RAM slot, so `{dst}` would resolve to a ZP slot nothing ever writes to. Bypassing the substitution entirely sidesteps the issue without a compiler change. - Rotate-right by any amount is a byte-rotate loop plus a bit- rotate loop so the 10 SHA amounts (2, 6, 7, 11, 13, 17, 18, 19, 22, 25) all compile to a handful of chained `ROR`s. - The headless jsnes golden auto-types "NES" after 1 s of idle and captures its SHA-256 digest AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D — byte-identical to `shasum` / `hashlib.sha256(b"NES")`. Build: `cargo run --release -- build examples/sha256.ne` https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 14:02:58 +00:00
LDA {wk},X
LSR A
INX
INX
INX
ROR {wk},X
DEX
ROR {wk},X
DEX
ROR {wk},X
DEX
ROR {wk},X
}
}
// Rotate wk[dst..dst+4] right by 1 byte, in place.
// new[0] = old[1], new[1] = old[2],
// new[2] = old[3], new[3] = old[0]
fun byte_rotr_wk(dst: u8) {
asm {
codegen: reuse analyzer's local allocations so inline asm `{param}` works Fixes compiler-bugs.md #1 — the inline-asm `{name}` resolver looks parameters up in the analyzer's `VarAllocation` table (because that's the only address map it has), but `IrCodeGen::new` was minting a parallel `$0300+` range for every function-local and ignoring what the analyzer had picked. The spill prologue wrote the param to the codegen's private address, the inline asm read from the analyzer's zero-page address, and nothing ever bridged the two — `LDA {param}` would silently load whatever the RAM clear left at the stale slot (always `0`). Fix: drop the `local_ram_next` loop and just look each local up in `allocations` by the analyzer's qualified name (`__local__{scope}__{local}`). The scope string that `gen_function` already computed for `substitute_asm_vars` is now shared with the new address-seeding loop via a `scope_prefix_for_fn(&str)` helper, so the two call sites can't drift. The analyzer's layout already satisfies the "no overlapping live locals" invariant the codegen was relying on — it scopes every local under `__local__<scope>__<name>` so two functions with a parameter named `x` land in different slots. Updated `gen_function_prologue_spills_params_to_local_ram`: the regression test for the War-era param clobbering bug was asserting the spill's destination specifically had to be an absolute address at `$0300+`. That's no longer the mechanism — the spill lands in whatever slot the analyzer assigned, which is zero page when there's room. The test now asserts the destination is *any* address outside `$04-$07`, which is the actual invariant. Reverted the `LDX $04` / `LDY $05` workaround in `examples/sha256/sha_core.ne` — every primitive there now uses `{dst}` / `{src}` / `{w_ofs}` / `{h_ofs}` / `{k_ofs}` substitution as originally intended. The "Parameter convention" comment that documented the workaround is gone. Regenerated `tests/emulator/goldens/inline_asm_demo.png`: that example's `times_four(input)` was previously returning `input` verbatim because the inline asm's `LDA {result}` / `ASL A` / `ASL A` / `STA {result}` operated on a zero-page byte that was disconnected from the NEScript-level `result` variable. With the fix, `times_four` correctly returns `input * 4`, so the smiley-tracker's frame-180 position shifts by the expected `(frame_count * 4) mod 256` delta. The other 33 ROMs remain byte-identical. Verified: - `cargo clippy --all-targets -- -D warnings` clean on both rustc 1.94.1 and 1.95.0. - `cargo test --all-targets`: 616 + 3 + 75 tests pass. - `cargo fmt --check` clean. - Full emulator harness: 34/34 ROMs match goldens. - SHA-256 of "NES" still computes to `AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D`. - `--memory-map` output now reflects what the generated code actually reads and writes (previously the codegen's $0300+ override was invisible to the dump). https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 16:03:10 +00:00
LDX {dst}
examples/sha256: interactive SHA-256 hasher with on-screen keyboard An end-to-end FIPS 180-4 SHA-256 hasher running entirely on the NES. The player types up to 16 ASCII characters on a 5x8 on-screen keyboard, presses Enter, and the program computes and displays the 64-character hex digest. Layout (`examples/sha256/*.ne`): constants.ne layout + K[64] / H_INIT[8] tables (declared as `var` with init_array because the v0.1 compiler treats `const u8[N] = [...]` as a no-op — noted in the file) assets.ne 44-tile Tileset (A..Z, 0..9, punctuation, special keys, cursor) shared between BG and sprite layers background.ne static nametable (title, labels, keyboard grid) painted at reset state.ne globals sha_core.ne 32-bit byte primitives (copy, xor, and, add, not, rotr, shr) in inline asm + sigma/Sigma mixers + schedule/round steps + fold render.ne OAM helpers for cursor, input buffer, and 64-nibble digest keyboard.ne key dispatch table entering_state.ne cursor navigation + typing + auto-demo computing_state.ne phased driver (48 schedule steps + 64 rounds + fold across ~30 frames at 4 iterations each) showing_state.ne renders the 256-bit digest as 8 rows of 8 sprite glyphs Implementation notes: - All 32-bit words live as 4 little-endian bytes in `wk[64]`, `w[256]`, `h_state[32]` so every primitive walks four bytes with `LDA {arr},X`/`STA {arr},X` chains and, for adds, a carry chain. - Every primitive reads its parameters straight out of the transport slots `$04`/`$05` rather than `{dst}`/`{src}` substitutions: the inline-asm resolver looks parameters up in the analyzer's allocation table but the codegen spills them to a different per-function RAM slot, so `{dst}` would resolve to a ZP slot nothing ever writes to. Bypassing the substitution entirely sidesteps the issue without a compiler change. - Rotate-right by any amount is a byte-rotate loop plus a bit- rotate loop so the 10 SHA amounts (2, 6, 7, 11, 13, 17, 18, 19, 22, 25) all compile to a handful of chained `ROR`s. - The headless jsnes golden auto-types "NES" after 1 s of idle and captures its SHA-256 digest AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D — byte-identical to `shasum` / `hashlib.sha256(b"NES")`. Build: `cargo run --release -- build examples/sha256.ne` https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 14:02:58 +00:00
LDY {wk},X
INX
LDA {wk},X
DEX
STA {wk},X
INX
INX
LDA {wk},X
DEX
STA {wk},X
INX
INX
LDA {wk},X
DEX
STA {wk},X
INX
TYA
STA {wk},X
}
}
// Rotate wk[dst..dst+4] right by `n` bits. Handles any n in
// 0..31 by first rotating whole bytes (each call is cheaper
// than 8 ROR chains) and then finishing with up to 7 single-
// bit ROR chains.
fun rotr_wk(dst: u8, n: u8) {
var rem: u8 = n
while rem >= 8 {
byte_rotr_wk(dst)
rem -= 8
}
while rem > 0 {
rotr1_wk(dst)
rem -= 1
}
}
// Shift wk[dst..dst+4] right by 1 bit (logical — top bit
// becomes 0).
fun shr1_wk(dst: u8) {
asm {
codegen: reuse analyzer's local allocations so inline asm `{param}` works Fixes compiler-bugs.md #1 — the inline-asm `{name}` resolver looks parameters up in the analyzer's `VarAllocation` table (because that's the only address map it has), but `IrCodeGen::new` was minting a parallel `$0300+` range for every function-local and ignoring what the analyzer had picked. The spill prologue wrote the param to the codegen's private address, the inline asm read from the analyzer's zero-page address, and nothing ever bridged the two — `LDA {param}` would silently load whatever the RAM clear left at the stale slot (always `0`). Fix: drop the `local_ram_next` loop and just look each local up in `allocations` by the analyzer's qualified name (`__local__{scope}__{local}`). The scope string that `gen_function` already computed for `substitute_asm_vars` is now shared with the new address-seeding loop via a `scope_prefix_for_fn(&str)` helper, so the two call sites can't drift. The analyzer's layout already satisfies the "no overlapping live locals" invariant the codegen was relying on — it scopes every local under `__local__<scope>__<name>` so two functions with a parameter named `x` land in different slots. Updated `gen_function_prologue_spills_params_to_local_ram`: the regression test for the War-era param clobbering bug was asserting the spill's destination specifically had to be an absolute address at `$0300+`. That's no longer the mechanism — the spill lands in whatever slot the analyzer assigned, which is zero page when there's room. The test now asserts the destination is *any* address outside `$04-$07`, which is the actual invariant. Reverted the `LDX $04` / `LDY $05` workaround in `examples/sha256/sha_core.ne` — every primitive there now uses `{dst}` / `{src}` / `{w_ofs}` / `{h_ofs}` / `{k_ofs}` substitution as originally intended. The "Parameter convention" comment that documented the workaround is gone. Regenerated `tests/emulator/goldens/inline_asm_demo.png`: that example's `times_four(input)` was previously returning `input` verbatim because the inline asm's `LDA {result}` / `ASL A` / `ASL A` / `STA {result}` operated on a zero-page byte that was disconnected from the NEScript-level `result` variable. With the fix, `times_four` correctly returns `input * 4`, so the smiley-tracker's frame-180 position shifts by the expected `(frame_count * 4) mod 256` delta. The other 33 ROMs remain byte-identical. Verified: - `cargo clippy --all-targets -- -D warnings` clean on both rustc 1.94.1 and 1.95.0. - `cargo test --all-targets`: 616 + 3 + 75 tests pass. - `cargo fmt --check` clean. - Full emulator harness: 34/34 ROMs match goldens. - SHA-256 of "NES" still computes to `AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D`. - `--memory-map` output now reflects what the generated code actually reads and writes (previously the codegen's $0300+ override was invisible to the dump). https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 16:03:10 +00:00
LDX {dst}
examples/sha256: interactive SHA-256 hasher with on-screen keyboard An end-to-end FIPS 180-4 SHA-256 hasher running entirely on the NES. The player types up to 16 ASCII characters on a 5x8 on-screen keyboard, presses Enter, and the program computes and displays the 64-character hex digest. Layout (`examples/sha256/*.ne`): constants.ne layout + K[64] / H_INIT[8] tables (declared as `var` with init_array because the v0.1 compiler treats `const u8[N] = [...]` as a no-op — noted in the file) assets.ne 44-tile Tileset (A..Z, 0..9, punctuation, special keys, cursor) shared between BG and sprite layers background.ne static nametable (title, labels, keyboard grid) painted at reset state.ne globals sha_core.ne 32-bit byte primitives (copy, xor, and, add, not, rotr, shr) in inline asm + sigma/Sigma mixers + schedule/round steps + fold render.ne OAM helpers for cursor, input buffer, and 64-nibble digest keyboard.ne key dispatch table entering_state.ne cursor navigation + typing + auto-demo computing_state.ne phased driver (48 schedule steps + 64 rounds + fold across ~30 frames at 4 iterations each) showing_state.ne renders the 256-bit digest as 8 rows of 8 sprite glyphs Implementation notes: - All 32-bit words live as 4 little-endian bytes in `wk[64]`, `w[256]`, `h_state[32]` so every primitive walks four bytes with `LDA {arr},X`/`STA {arr},X` chains and, for adds, a carry chain. - Every primitive reads its parameters straight out of the transport slots `$04`/`$05` rather than `{dst}`/`{src}` substitutions: the inline-asm resolver looks parameters up in the analyzer's allocation table but the codegen spills them to a different per-function RAM slot, so `{dst}` would resolve to a ZP slot nothing ever writes to. Bypassing the substitution entirely sidesteps the issue without a compiler change. - Rotate-right by any amount is a byte-rotate loop plus a bit- rotate loop so the 10 SHA amounts (2, 6, 7, 11, 13, 17, 18, 19, 22, 25) all compile to a handful of chained `ROR`s. - The headless jsnes golden auto-types "NES" after 1 s of idle and captures its SHA-256 digest AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D — byte-identical to `shasum` / `hashlib.sha256(b"NES")`. Build: `cargo run --release -- build examples/sha256.ne` https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 14:02:58 +00:00
INX
INX
INX
LSR {wk},X
DEX
ROR {wk},X
DEX
ROR {wk},X
DEX
ROR {wk},X
}
}
// Shift wk[dst..dst+4] right by 1 byte, in place. The top
// byte becomes 0.
fun byte_shr_wk(dst: u8) {
asm {
codegen: reuse analyzer's local allocations so inline asm `{param}` works Fixes compiler-bugs.md #1 — the inline-asm `{name}` resolver looks parameters up in the analyzer's `VarAllocation` table (because that's the only address map it has), but `IrCodeGen::new` was minting a parallel `$0300+` range for every function-local and ignoring what the analyzer had picked. The spill prologue wrote the param to the codegen's private address, the inline asm read from the analyzer's zero-page address, and nothing ever bridged the two — `LDA {param}` would silently load whatever the RAM clear left at the stale slot (always `0`). Fix: drop the `local_ram_next` loop and just look each local up in `allocations` by the analyzer's qualified name (`__local__{scope}__{local}`). The scope string that `gen_function` already computed for `substitute_asm_vars` is now shared with the new address-seeding loop via a `scope_prefix_for_fn(&str)` helper, so the two call sites can't drift. The analyzer's layout already satisfies the "no overlapping live locals" invariant the codegen was relying on — it scopes every local under `__local__<scope>__<name>` so two functions with a parameter named `x` land in different slots. Updated `gen_function_prologue_spills_params_to_local_ram`: the regression test for the War-era param clobbering bug was asserting the spill's destination specifically had to be an absolute address at `$0300+`. That's no longer the mechanism — the spill lands in whatever slot the analyzer assigned, which is zero page when there's room. The test now asserts the destination is *any* address outside `$04-$07`, which is the actual invariant. Reverted the `LDX $04` / `LDY $05` workaround in `examples/sha256/sha_core.ne` — every primitive there now uses `{dst}` / `{src}` / `{w_ofs}` / `{h_ofs}` / `{k_ofs}` substitution as originally intended. The "Parameter convention" comment that documented the workaround is gone. Regenerated `tests/emulator/goldens/inline_asm_demo.png`: that example's `times_four(input)` was previously returning `input` verbatim because the inline asm's `LDA {result}` / `ASL A` / `ASL A` / `STA {result}` operated on a zero-page byte that was disconnected from the NEScript-level `result` variable. With the fix, `times_four` correctly returns `input * 4`, so the smiley-tracker's frame-180 position shifts by the expected `(frame_count * 4) mod 256` delta. The other 33 ROMs remain byte-identical. Verified: - `cargo clippy --all-targets -- -D warnings` clean on both rustc 1.94.1 and 1.95.0. - `cargo test --all-targets`: 616 + 3 + 75 tests pass. - `cargo fmt --check` clean. - Full emulator harness: 34/34 ROMs match goldens. - SHA-256 of "NES" still computes to `AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D`. - `--memory-map` output now reflects what the generated code actually reads and writes (previously the codegen's $0300+ override was invisible to the dump). https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 16:03:10 +00:00
LDX {dst}
examples/sha256: interactive SHA-256 hasher with on-screen keyboard An end-to-end FIPS 180-4 SHA-256 hasher running entirely on the NES. The player types up to 16 ASCII characters on a 5x8 on-screen keyboard, presses Enter, and the program computes and displays the 64-character hex digest. Layout (`examples/sha256/*.ne`): constants.ne layout + K[64] / H_INIT[8] tables (declared as `var` with init_array because the v0.1 compiler treats `const u8[N] = [...]` as a no-op — noted in the file) assets.ne 44-tile Tileset (A..Z, 0..9, punctuation, special keys, cursor) shared between BG and sprite layers background.ne static nametable (title, labels, keyboard grid) painted at reset state.ne globals sha_core.ne 32-bit byte primitives (copy, xor, and, add, not, rotr, shr) in inline asm + sigma/Sigma mixers + schedule/round steps + fold render.ne OAM helpers for cursor, input buffer, and 64-nibble digest keyboard.ne key dispatch table entering_state.ne cursor navigation + typing + auto-demo computing_state.ne phased driver (48 schedule steps + 64 rounds + fold across ~30 frames at 4 iterations each) showing_state.ne renders the 256-bit digest as 8 rows of 8 sprite glyphs Implementation notes: - All 32-bit words live as 4 little-endian bytes in `wk[64]`, `w[256]`, `h_state[32]` so every primitive walks four bytes with `LDA {arr},X`/`STA {arr},X` chains and, for adds, a carry chain. - Every primitive reads its parameters straight out of the transport slots `$04`/`$05` rather than `{dst}`/`{src}` substitutions: the inline-asm resolver looks parameters up in the analyzer's allocation table but the codegen spills them to a different per-function RAM slot, so `{dst}` would resolve to a ZP slot nothing ever writes to. Bypassing the substitution entirely sidesteps the issue without a compiler change. - Rotate-right by any amount is a byte-rotate loop plus a bit- rotate loop so the 10 SHA amounts (2, 6, 7, 11, 13, 17, 18, 19, 22, 25) all compile to a handful of chained `ROR`s. - The headless jsnes golden auto-types "NES" after 1 s of idle and captures its SHA-256 digest AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D — byte-identical to `shasum` / `hashlib.sha256(b"NES")`. Build: `cargo run --release -- build examples/sha256.ne` https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 14:02:58 +00:00
INX
LDA {wk},X
DEX
STA {wk},X
INX
INX
LDA {wk},X
DEX
STA {wk},X
INX
INX
LDA {wk},X
DEX
STA {wk},X
INX
LDA #0
STA {wk},X
}
}
// Shift wk[dst..dst+4] right by `n` bits (logical).
fun shr_wk(dst: u8, n: u8) {
var rem: u8 = n
while rem >= 8 {
byte_shr_wk(dst)
rem -= 8
}
while rem > 0 {
shr1_wk(dst)
rem -= 1
}
}
// ── Cross-array primitives ──────────────────────────────────
// wk[dst..dst+4] = w[w_ofs..w_ofs+4]
fun cp_w_to_wk(dst: u8, w_ofs: u8) {
asm {
codegen: reuse analyzer's local allocations so inline asm `{param}` works Fixes compiler-bugs.md #1 — the inline-asm `{name}` resolver looks parameters up in the analyzer's `VarAllocation` table (because that's the only address map it has), but `IrCodeGen::new` was minting a parallel `$0300+` range for every function-local and ignoring what the analyzer had picked. The spill prologue wrote the param to the codegen's private address, the inline asm read from the analyzer's zero-page address, and nothing ever bridged the two — `LDA {param}` would silently load whatever the RAM clear left at the stale slot (always `0`). Fix: drop the `local_ram_next` loop and just look each local up in `allocations` by the analyzer's qualified name (`__local__{scope}__{local}`). The scope string that `gen_function` already computed for `substitute_asm_vars` is now shared with the new address-seeding loop via a `scope_prefix_for_fn(&str)` helper, so the two call sites can't drift. The analyzer's layout already satisfies the "no overlapping live locals" invariant the codegen was relying on — it scopes every local under `__local__<scope>__<name>` so two functions with a parameter named `x` land in different slots. Updated `gen_function_prologue_spills_params_to_local_ram`: the regression test for the War-era param clobbering bug was asserting the spill's destination specifically had to be an absolute address at `$0300+`. That's no longer the mechanism — the spill lands in whatever slot the analyzer assigned, which is zero page when there's room. The test now asserts the destination is *any* address outside `$04-$07`, which is the actual invariant. Reverted the `LDX $04` / `LDY $05` workaround in `examples/sha256/sha_core.ne` — every primitive there now uses `{dst}` / `{src}` / `{w_ofs}` / `{h_ofs}` / `{k_ofs}` substitution as originally intended. The "Parameter convention" comment that documented the workaround is gone. Regenerated `tests/emulator/goldens/inline_asm_demo.png`: that example's `times_four(input)` was previously returning `input` verbatim because the inline asm's `LDA {result}` / `ASL A` / `ASL A` / `STA {result}` operated on a zero-page byte that was disconnected from the NEScript-level `result` variable. With the fix, `times_four` correctly returns `input * 4`, so the smiley-tracker's frame-180 position shifts by the expected `(frame_count * 4) mod 256` delta. The other 33 ROMs remain byte-identical. Verified: - `cargo clippy --all-targets -- -D warnings` clean on both rustc 1.94.1 and 1.95.0. - `cargo test --all-targets`: 616 + 3 + 75 tests pass. - `cargo fmt --check` clean. - Full emulator harness: 34/34 ROMs match goldens. - SHA-256 of "NES" still computes to `AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D`. - `--memory-map` output now reflects what the generated code actually reads and writes (previously the codegen's $0300+ override was invisible to the dump). https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 16:03:10 +00:00
LDX {dst}
LDY {w_ofs}
examples/sha256: interactive SHA-256 hasher with on-screen keyboard An end-to-end FIPS 180-4 SHA-256 hasher running entirely on the NES. The player types up to 16 ASCII characters on a 5x8 on-screen keyboard, presses Enter, and the program computes and displays the 64-character hex digest. Layout (`examples/sha256/*.ne`): constants.ne layout + K[64] / H_INIT[8] tables (declared as `var` with init_array because the v0.1 compiler treats `const u8[N] = [...]` as a no-op — noted in the file) assets.ne 44-tile Tileset (A..Z, 0..9, punctuation, special keys, cursor) shared between BG and sprite layers background.ne static nametable (title, labels, keyboard grid) painted at reset state.ne globals sha_core.ne 32-bit byte primitives (copy, xor, and, add, not, rotr, shr) in inline asm + sigma/Sigma mixers + schedule/round steps + fold render.ne OAM helpers for cursor, input buffer, and 64-nibble digest keyboard.ne key dispatch table entering_state.ne cursor navigation + typing + auto-demo computing_state.ne phased driver (48 schedule steps + 64 rounds + fold across ~30 frames at 4 iterations each) showing_state.ne renders the 256-bit digest as 8 rows of 8 sprite glyphs Implementation notes: - All 32-bit words live as 4 little-endian bytes in `wk[64]`, `w[256]`, `h_state[32]` so every primitive walks four bytes with `LDA {arr},X`/`STA {arr},X` chains and, for adds, a carry chain. - Every primitive reads its parameters straight out of the transport slots `$04`/`$05` rather than `{dst}`/`{src}` substitutions: the inline-asm resolver looks parameters up in the analyzer's allocation table but the codegen spills them to a different per-function RAM slot, so `{dst}` would resolve to a ZP slot nothing ever writes to. Bypassing the substitution entirely sidesteps the issue without a compiler change. - Rotate-right by any amount is a byte-rotate loop plus a bit- rotate loop so the 10 SHA amounts (2, 6, 7, 11, 13, 17, 18, 19, 22, 25) all compile to a handful of chained `ROR`s. - The headless jsnes golden auto-types "NES" after 1 s of idle and captures its SHA-256 digest AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D — byte-identical to `shasum` / `hashlib.sha256(b"NES")`. Build: `cargo run --release -- build examples/sha256.ne` https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 14:02:58 +00:00
LDA {w},Y
STA {wk},X
INX
INY
LDA {w},Y
STA {wk},X
INX
INY
LDA {w},Y
STA {wk},X
INX
INY
LDA {w},Y
STA {wk},X
}
}
// wk[dst..dst+4] += w[w_ofs..w_ofs+4]
fun add_w_to_wk(dst: u8, w_ofs: u8) {
asm {
codegen: reuse analyzer's local allocations so inline asm `{param}` works Fixes compiler-bugs.md #1 — the inline-asm `{name}` resolver looks parameters up in the analyzer's `VarAllocation` table (because that's the only address map it has), but `IrCodeGen::new` was minting a parallel `$0300+` range for every function-local and ignoring what the analyzer had picked. The spill prologue wrote the param to the codegen's private address, the inline asm read from the analyzer's zero-page address, and nothing ever bridged the two — `LDA {param}` would silently load whatever the RAM clear left at the stale slot (always `0`). Fix: drop the `local_ram_next` loop and just look each local up in `allocations` by the analyzer's qualified name (`__local__{scope}__{local}`). The scope string that `gen_function` already computed for `substitute_asm_vars` is now shared with the new address-seeding loop via a `scope_prefix_for_fn(&str)` helper, so the two call sites can't drift. The analyzer's layout already satisfies the "no overlapping live locals" invariant the codegen was relying on — it scopes every local under `__local__<scope>__<name>` so two functions with a parameter named `x` land in different slots. Updated `gen_function_prologue_spills_params_to_local_ram`: the regression test for the War-era param clobbering bug was asserting the spill's destination specifically had to be an absolute address at `$0300+`. That's no longer the mechanism — the spill lands in whatever slot the analyzer assigned, which is zero page when there's room. The test now asserts the destination is *any* address outside `$04-$07`, which is the actual invariant. Reverted the `LDX $04` / `LDY $05` workaround in `examples/sha256/sha_core.ne` — every primitive there now uses `{dst}` / `{src}` / `{w_ofs}` / `{h_ofs}` / `{k_ofs}` substitution as originally intended. The "Parameter convention" comment that documented the workaround is gone. Regenerated `tests/emulator/goldens/inline_asm_demo.png`: that example's `times_four(input)` was previously returning `input` verbatim because the inline asm's `LDA {result}` / `ASL A` / `ASL A` / `STA {result}` operated on a zero-page byte that was disconnected from the NEScript-level `result` variable. With the fix, `times_four` correctly returns `input * 4`, so the smiley-tracker's frame-180 position shifts by the expected `(frame_count * 4) mod 256` delta. The other 33 ROMs remain byte-identical. Verified: - `cargo clippy --all-targets -- -D warnings` clean on both rustc 1.94.1 and 1.95.0. - `cargo test --all-targets`: 616 + 3 + 75 tests pass. - `cargo fmt --check` clean. - Full emulator harness: 34/34 ROMs match goldens. - SHA-256 of "NES" still computes to `AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D`. - `--memory-map` output now reflects what the generated code actually reads and writes (previously the codegen's $0300+ override was invisible to the dump). https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 16:03:10 +00:00
LDX {dst}
LDY {w_ofs}
examples/sha256: interactive SHA-256 hasher with on-screen keyboard An end-to-end FIPS 180-4 SHA-256 hasher running entirely on the NES. The player types up to 16 ASCII characters on a 5x8 on-screen keyboard, presses Enter, and the program computes and displays the 64-character hex digest. Layout (`examples/sha256/*.ne`): constants.ne layout + K[64] / H_INIT[8] tables (declared as `var` with init_array because the v0.1 compiler treats `const u8[N] = [...]` as a no-op — noted in the file) assets.ne 44-tile Tileset (A..Z, 0..9, punctuation, special keys, cursor) shared between BG and sprite layers background.ne static nametable (title, labels, keyboard grid) painted at reset state.ne globals sha_core.ne 32-bit byte primitives (copy, xor, and, add, not, rotr, shr) in inline asm + sigma/Sigma mixers + schedule/round steps + fold render.ne OAM helpers for cursor, input buffer, and 64-nibble digest keyboard.ne key dispatch table entering_state.ne cursor navigation + typing + auto-demo computing_state.ne phased driver (48 schedule steps + 64 rounds + fold across ~30 frames at 4 iterations each) showing_state.ne renders the 256-bit digest as 8 rows of 8 sprite glyphs Implementation notes: - All 32-bit words live as 4 little-endian bytes in `wk[64]`, `w[256]`, `h_state[32]` so every primitive walks four bytes with `LDA {arr},X`/`STA {arr},X` chains and, for adds, a carry chain. - Every primitive reads its parameters straight out of the transport slots `$04`/`$05` rather than `{dst}`/`{src}` substitutions: the inline-asm resolver looks parameters up in the analyzer's allocation table but the codegen spills them to a different per-function RAM slot, so `{dst}` would resolve to a ZP slot nothing ever writes to. Bypassing the substitution entirely sidesteps the issue without a compiler change. - Rotate-right by any amount is a byte-rotate loop plus a bit- rotate loop so the 10 SHA amounts (2, 6, 7, 11, 13, 17, 18, 19, 22, 25) all compile to a handful of chained `ROR`s. - The headless jsnes golden auto-types "NES" after 1 s of idle and captures its SHA-256 digest AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D — byte-identical to `shasum` / `hashlib.sha256(b"NES")`. Build: `cargo run --release -- build examples/sha256.ne` https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 14:02:58 +00:00
CLC
LDA {wk},X
ADC {w},Y
STA {wk},X
INX
INY
LDA {wk},X
ADC {w},Y
STA {wk},X
INX
INY
LDA {wk},X
ADC {w},Y
STA {wk},X
INX
INY
LDA {wk},X
ADC {w},Y
STA {wk},X
}
}
// w[w_ofs..w_ofs+4] = wk[src..src+4]
fun cp_wk_to_w(w_ofs: u8, src: u8) {
asm {
codegen: reuse analyzer's local allocations so inline asm `{param}` works Fixes compiler-bugs.md #1 — the inline-asm `{name}` resolver looks parameters up in the analyzer's `VarAllocation` table (because that's the only address map it has), but `IrCodeGen::new` was minting a parallel `$0300+` range for every function-local and ignoring what the analyzer had picked. The spill prologue wrote the param to the codegen's private address, the inline asm read from the analyzer's zero-page address, and nothing ever bridged the two — `LDA {param}` would silently load whatever the RAM clear left at the stale slot (always `0`). Fix: drop the `local_ram_next` loop and just look each local up in `allocations` by the analyzer's qualified name (`__local__{scope}__{local}`). The scope string that `gen_function` already computed for `substitute_asm_vars` is now shared with the new address-seeding loop via a `scope_prefix_for_fn(&str)` helper, so the two call sites can't drift. The analyzer's layout already satisfies the "no overlapping live locals" invariant the codegen was relying on — it scopes every local under `__local__<scope>__<name>` so two functions with a parameter named `x` land in different slots. Updated `gen_function_prologue_spills_params_to_local_ram`: the regression test for the War-era param clobbering bug was asserting the spill's destination specifically had to be an absolute address at `$0300+`. That's no longer the mechanism — the spill lands in whatever slot the analyzer assigned, which is zero page when there's room. The test now asserts the destination is *any* address outside `$04-$07`, which is the actual invariant. Reverted the `LDX $04` / `LDY $05` workaround in `examples/sha256/sha_core.ne` — every primitive there now uses `{dst}` / `{src}` / `{w_ofs}` / `{h_ofs}` / `{k_ofs}` substitution as originally intended. The "Parameter convention" comment that documented the workaround is gone. Regenerated `tests/emulator/goldens/inline_asm_demo.png`: that example's `times_four(input)` was previously returning `input` verbatim because the inline asm's `LDA {result}` / `ASL A` / `ASL A` / `STA {result}` operated on a zero-page byte that was disconnected from the NEScript-level `result` variable. With the fix, `times_four` correctly returns `input * 4`, so the smiley-tracker's frame-180 position shifts by the expected `(frame_count * 4) mod 256` delta. The other 33 ROMs remain byte-identical. Verified: - `cargo clippy --all-targets -- -D warnings` clean on both rustc 1.94.1 and 1.95.0. - `cargo test --all-targets`: 616 + 3 + 75 tests pass. - `cargo fmt --check` clean. - Full emulator harness: 34/34 ROMs match goldens. - SHA-256 of "NES" still computes to `AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D`. - `--memory-map` output now reflects what the generated code actually reads and writes (previously the codegen's $0300+ override was invisible to the dump). https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 16:03:10 +00:00
LDX {src}
LDY {w_ofs}
examples/sha256: interactive SHA-256 hasher with on-screen keyboard An end-to-end FIPS 180-4 SHA-256 hasher running entirely on the NES. The player types up to 16 ASCII characters on a 5x8 on-screen keyboard, presses Enter, and the program computes and displays the 64-character hex digest. Layout (`examples/sha256/*.ne`): constants.ne layout + K[64] / H_INIT[8] tables (declared as `var` with init_array because the v0.1 compiler treats `const u8[N] = [...]` as a no-op — noted in the file) assets.ne 44-tile Tileset (A..Z, 0..9, punctuation, special keys, cursor) shared between BG and sprite layers background.ne static nametable (title, labels, keyboard grid) painted at reset state.ne globals sha_core.ne 32-bit byte primitives (copy, xor, and, add, not, rotr, shr) in inline asm + sigma/Sigma mixers + schedule/round steps + fold render.ne OAM helpers for cursor, input buffer, and 64-nibble digest keyboard.ne key dispatch table entering_state.ne cursor navigation + typing + auto-demo computing_state.ne phased driver (48 schedule steps + 64 rounds + fold across ~30 frames at 4 iterations each) showing_state.ne renders the 256-bit digest as 8 rows of 8 sprite glyphs Implementation notes: - All 32-bit words live as 4 little-endian bytes in `wk[64]`, `w[256]`, `h_state[32]` so every primitive walks four bytes with `LDA {arr},X`/`STA {arr},X` chains and, for adds, a carry chain. - Every primitive reads its parameters straight out of the transport slots `$04`/`$05` rather than `{dst}`/`{src}` substitutions: the inline-asm resolver looks parameters up in the analyzer's allocation table but the codegen spills them to a different per-function RAM slot, so `{dst}` would resolve to a ZP slot nothing ever writes to. Bypassing the substitution entirely sidesteps the issue without a compiler change. - Rotate-right by any amount is a byte-rotate loop plus a bit- rotate loop so the 10 SHA amounts (2, 6, 7, 11, 13, 17, 18, 19, 22, 25) all compile to a handful of chained `ROR`s. - The headless jsnes golden auto-types "NES" after 1 s of idle and captures its SHA-256 digest AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D — byte-identical to `shasum` / `hashlib.sha256(b"NES")`. Build: `cargo run --release -- build examples/sha256.ne` https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 14:02:58 +00:00
LDA {wk},X
STA {w},Y
INX
INY
LDA {wk},X
STA {w},Y
INX
INY
LDA {wk},X
STA {w},Y
INX
INY
LDA {wk},X
STA {w},Y
}
}
// h_state[h_ofs..h_ofs+4] += wk[src..src+4]
fun add_wk_to_h(h_ofs: u8, src: u8) {
asm {
codegen: reuse analyzer's local allocations so inline asm `{param}` works Fixes compiler-bugs.md #1 — the inline-asm `{name}` resolver looks parameters up in the analyzer's `VarAllocation` table (because that's the only address map it has), but `IrCodeGen::new` was minting a parallel `$0300+` range for every function-local and ignoring what the analyzer had picked. The spill prologue wrote the param to the codegen's private address, the inline asm read from the analyzer's zero-page address, and nothing ever bridged the two — `LDA {param}` would silently load whatever the RAM clear left at the stale slot (always `0`). Fix: drop the `local_ram_next` loop and just look each local up in `allocations` by the analyzer's qualified name (`__local__{scope}__{local}`). The scope string that `gen_function` already computed for `substitute_asm_vars` is now shared with the new address-seeding loop via a `scope_prefix_for_fn(&str)` helper, so the two call sites can't drift. The analyzer's layout already satisfies the "no overlapping live locals" invariant the codegen was relying on — it scopes every local under `__local__<scope>__<name>` so two functions with a parameter named `x` land in different slots. Updated `gen_function_prologue_spills_params_to_local_ram`: the regression test for the War-era param clobbering bug was asserting the spill's destination specifically had to be an absolute address at `$0300+`. That's no longer the mechanism — the spill lands in whatever slot the analyzer assigned, which is zero page when there's room. The test now asserts the destination is *any* address outside `$04-$07`, which is the actual invariant. Reverted the `LDX $04` / `LDY $05` workaround in `examples/sha256/sha_core.ne` — every primitive there now uses `{dst}` / `{src}` / `{w_ofs}` / `{h_ofs}` / `{k_ofs}` substitution as originally intended. The "Parameter convention" comment that documented the workaround is gone. Regenerated `tests/emulator/goldens/inline_asm_demo.png`: that example's `times_four(input)` was previously returning `input` verbatim because the inline asm's `LDA {result}` / `ASL A` / `ASL A` / `STA {result}` operated on a zero-page byte that was disconnected from the NEScript-level `result` variable. With the fix, `times_four` correctly returns `input * 4`, so the smiley-tracker's frame-180 position shifts by the expected `(frame_count * 4) mod 256` delta. The other 33 ROMs remain byte-identical. Verified: - `cargo clippy --all-targets -- -D warnings` clean on both rustc 1.94.1 and 1.95.0. - `cargo test --all-targets`: 616 + 3 + 75 tests pass. - `cargo fmt --check` clean. - Full emulator harness: 34/34 ROMs match goldens. - SHA-256 of "NES" still computes to `AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D`. - `--memory-map` output now reflects what the generated code actually reads and writes (previously the codegen's $0300+ override was invisible to the dump). https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 16:03:10 +00:00
LDX {h_ofs}
LDY {src}
examples/sha256: interactive SHA-256 hasher with on-screen keyboard An end-to-end FIPS 180-4 SHA-256 hasher running entirely on the NES. The player types up to 16 ASCII characters on a 5x8 on-screen keyboard, presses Enter, and the program computes and displays the 64-character hex digest. Layout (`examples/sha256/*.ne`): constants.ne layout + K[64] / H_INIT[8] tables (declared as `var` with init_array because the v0.1 compiler treats `const u8[N] = [...]` as a no-op — noted in the file) assets.ne 44-tile Tileset (A..Z, 0..9, punctuation, special keys, cursor) shared between BG and sprite layers background.ne static nametable (title, labels, keyboard grid) painted at reset state.ne globals sha_core.ne 32-bit byte primitives (copy, xor, and, add, not, rotr, shr) in inline asm + sigma/Sigma mixers + schedule/round steps + fold render.ne OAM helpers for cursor, input buffer, and 64-nibble digest keyboard.ne key dispatch table entering_state.ne cursor navigation + typing + auto-demo computing_state.ne phased driver (48 schedule steps + 64 rounds + fold across ~30 frames at 4 iterations each) showing_state.ne renders the 256-bit digest as 8 rows of 8 sprite glyphs Implementation notes: - All 32-bit words live as 4 little-endian bytes in `wk[64]`, `w[256]`, `h_state[32]` so every primitive walks four bytes with `LDA {arr},X`/`STA {arr},X` chains and, for adds, a carry chain. - Every primitive reads its parameters straight out of the transport slots `$04`/`$05` rather than `{dst}`/`{src}` substitutions: the inline-asm resolver looks parameters up in the analyzer's allocation table but the codegen spills them to a different per-function RAM slot, so `{dst}` would resolve to a ZP slot nothing ever writes to. Bypassing the substitution entirely sidesteps the issue without a compiler change. - Rotate-right by any amount is a byte-rotate loop plus a bit- rotate loop so the 10 SHA amounts (2, 6, 7, 11, 13, 17, 18, 19, 22, 25) all compile to a handful of chained `ROR`s. - The headless jsnes golden auto-types "NES" after 1 s of idle and captures its SHA-256 digest AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D — byte-identical to `shasum` / `hashlib.sha256(b"NES")`. Build: `cargo run --release -- build examples/sha256.ne` https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 14:02:58 +00:00
CLC
LDA {h_state},X
ADC {wk},Y
STA {h_state},X
INX
INY
LDA {h_state},X
ADC {wk},Y
STA {h_state},X
INX
INY
LDA {h_state},X
ADC {wk},Y
STA {h_state},X
INX
INY
LDA {h_state},X
ADC {wk},Y
STA {h_state},X
}
}
// wk[dst..dst+4] += _K_BYTES[k_ofs..k_ofs+4]
fun add_k_to_wk(dst: u8, k_ofs: u8) {
asm {
codegen: reuse analyzer's local allocations so inline asm `{param}` works Fixes compiler-bugs.md #1 — the inline-asm `{name}` resolver looks parameters up in the analyzer's `VarAllocation` table (because that's the only address map it has), but `IrCodeGen::new` was minting a parallel `$0300+` range for every function-local and ignoring what the analyzer had picked. The spill prologue wrote the param to the codegen's private address, the inline asm read from the analyzer's zero-page address, and nothing ever bridged the two — `LDA {param}` would silently load whatever the RAM clear left at the stale slot (always `0`). Fix: drop the `local_ram_next` loop and just look each local up in `allocations` by the analyzer's qualified name (`__local__{scope}__{local}`). The scope string that `gen_function` already computed for `substitute_asm_vars` is now shared with the new address-seeding loop via a `scope_prefix_for_fn(&str)` helper, so the two call sites can't drift. The analyzer's layout already satisfies the "no overlapping live locals" invariant the codegen was relying on — it scopes every local under `__local__<scope>__<name>` so two functions with a parameter named `x` land in different slots. Updated `gen_function_prologue_spills_params_to_local_ram`: the regression test for the War-era param clobbering bug was asserting the spill's destination specifically had to be an absolute address at `$0300+`. That's no longer the mechanism — the spill lands in whatever slot the analyzer assigned, which is zero page when there's room. The test now asserts the destination is *any* address outside `$04-$07`, which is the actual invariant. Reverted the `LDX $04` / `LDY $05` workaround in `examples/sha256/sha_core.ne` — every primitive there now uses `{dst}` / `{src}` / `{w_ofs}` / `{h_ofs}` / `{k_ofs}` substitution as originally intended. The "Parameter convention" comment that documented the workaround is gone. Regenerated `tests/emulator/goldens/inline_asm_demo.png`: that example's `times_four(input)` was previously returning `input` verbatim because the inline asm's `LDA {result}` / `ASL A` / `ASL A` / `STA {result}` operated on a zero-page byte that was disconnected from the NEScript-level `result` variable. With the fix, `times_four` correctly returns `input * 4`, so the smiley-tracker's frame-180 position shifts by the expected `(frame_count * 4) mod 256` delta. The other 33 ROMs remain byte-identical. Verified: - `cargo clippy --all-targets -- -D warnings` clean on both rustc 1.94.1 and 1.95.0. - `cargo test --all-targets`: 616 + 3 + 75 tests pass. - `cargo fmt --check` clean. - Full emulator harness: 34/34 ROMs match goldens. - SHA-256 of "NES" still computes to `AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D`. - `--memory-map` output now reflects what the generated code actually reads and writes (previously the codegen's $0300+ override was invisible to the dump). https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 16:03:10 +00:00
LDX {dst}
LDY {k_ofs}
examples/sha256: interactive SHA-256 hasher with on-screen keyboard An end-to-end FIPS 180-4 SHA-256 hasher running entirely on the NES. The player types up to 16 ASCII characters on a 5x8 on-screen keyboard, presses Enter, and the program computes and displays the 64-character hex digest. Layout (`examples/sha256/*.ne`): constants.ne layout + K[64] / H_INIT[8] tables (declared as `var` with init_array because the v0.1 compiler treats `const u8[N] = [...]` as a no-op — noted in the file) assets.ne 44-tile Tileset (A..Z, 0..9, punctuation, special keys, cursor) shared between BG and sprite layers background.ne static nametable (title, labels, keyboard grid) painted at reset state.ne globals sha_core.ne 32-bit byte primitives (copy, xor, and, add, not, rotr, shr) in inline asm + sigma/Sigma mixers + schedule/round steps + fold render.ne OAM helpers for cursor, input buffer, and 64-nibble digest keyboard.ne key dispatch table entering_state.ne cursor navigation + typing + auto-demo computing_state.ne phased driver (48 schedule steps + 64 rounds + fold across ~30 frames at 4 iterations each) showing_state.ne renders the 256-bit digest as 8 rows of 8 sprite glyphs Implementation notes: - All 32-bit words live as 4 little-endian bytes in `wk[64]`, `w[256]`, `h_state[32]` so every primitive walks four bytes with `LDA {arr},X`/`STA {arr},X` chains and, for adds, a carry chain. - Every primitive reads its parameters straight out of the transport slots `$04`/`$05` rather than `{dst}`/`{src}` substitutions: the inline-asm resolver looks parameters up in the analyzer's allocation table but the codegen spills them to a different per-function RAM slot, so `{dst}` would resolve to a ZP slot nothing ever writes to. Bypassing the substitution entirely sidesteps the issue without a compiler change. - Rotate-right by any amount is a byte-rotate loop plus a bit- rotate loop so the 10 SHA amounts (2, 6, 7, 11, 13, 17, 18, 19, 22, 25) all compile to a handful of chained `ROR`s. - The headless jsnes golden auto-types "NES" after 1 s of idle and captures its SHA-256 digest AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D — byte-identical to `shasum` / `hashlib.sha256(b"NES")`. Build: `cargo run --release -- build examples/sha256.ne` https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 14:02:58 +00:00
CLC
LDA {wk},X
ADC {_K_BYTES},Y
STA {wk},X
INX
INY
LDA {wk},X
ADC {_K_BYTES},Y
STA {wk},X
INX
INY
LDA {wk},X
ADC {_K_BYTES},Y
STA {wk},X
INX
INY
LDA {wk},X
ADC {_K_BYTES},Y
STA {wk},X
}
}
// ── σ and Σ helpers ─────────────────────────────────────────
//
// Each Σ/σ function writes its 32-bit result at wk[OFS_SIG].
// OFS_TMP is used internally as scratch. Callers must not
// pass `src` == OFS_SIG / OFS_TMP.
// Σ0(src) = rotr(src, 2) ^ rotr(src, 13) ^ rotr(src, 22)
fun big_sigma0(src: u8) {
cp_wk(OFS_SIG, src)
rotr_wk(OFS_SIG, 2)
cp_wk(OFS_TMP, src)
rotr_wk(OFS_TMP, 13)
xor_wk(OFS_SIG, OFS_TMP)
cp_wk(OFS_TMP, src)
rotr_wk(OFS_TMP, 22)
xor_wk(OFS_SIG, OFS_TMP)
}
// Σ1(src) = rotr(src, 6) ^ rotr(src, 11) ^ rotr(src, 25)
fun big_sigma1(src: u8) {
cp_wk(OFS_SIG, src)
rotr_wk(OFS_SIG, 6)
cp_wk(OFS_TMP, src)
rotr_wk(OFS_TMP, 11)
xor_wk(OFS_SIG, OFS_TMP)
cp_wk(OFS_TMP, src)
rotr_wk(OFS_TMP, 25)
xor_wk(OFS_SIG, OFS_TMP)
}
// σ0(src) = rotr(src, 7) ^ rotr(src, 18) ^ (src >> 3)
fun small_sigma0(src: u8) {
cp_wk(OFS_SIG, src)
rotr_wk(OFS_SIG, 7)
cp_wk(OFS_TMP, src)
rotr_wk(OFS_TMP, 18)
xor_wk(OFS_SIG, OFS_TMP)
cp_wk(OFS_TMP, src)
shr_wk(OFS_TMP, 3)
xor_wk(OFS_SIG, OFS_TMP)
}
// σ1(src) = rotr(src, 17) ^ rotr(src, 19) ^ (src >> 10)
fun small_sigma1(src: u8) {
cp_wk(OFS_SIG, src)
rotr_wk(OFS_SIG, 17)
cp_wk(OFS_TMP, src)
rotr_wk(OFS_TMP, 19)
xor_wk(OFS_SIG, OFS_TMP)
cp_wk(OFS_TMP, src)
shr_wk(OFS_TMP, 10)
xor_wk(OFS_SIG, OFS_TMP)
}
// ── Block-level helpers ─────────────────────────────────────
// Copy H_INIT[0..32] into h_state[0..32]. Used at the start of
// every hash so the driver can be re-run on a new message
// after the user clears the input.
fun reset_hash_state() {
var i: u8 = 0
while i < 32 {
h_state[i] = H_INIT[i]
i += 1
}
}
// Build the 64-byte padded message block directly into
// w[0..63]. `msg[0..msg_len]` is the ASCII input; padding
// follows FIPS 180-4 §5.1.1:
//
// pad[0..msg_len] = msg[0..msg_len]
// pad[msg_len] = 0x80
// pad[msg_len+1..56] = 0
// pad[56..62] = 0 (high 48 bits of length)
// pad[62..64] = message length in bits, big-endian
//
// Since msg_len ≤ 16 the bit length fits in 8 bits (max 128),
// so only the very last byte of the block is nonzero for the
// length field. The loader also byte-swaps each 4-byte word so
// our little-endian internal layout matches SHA-256's big-
// endian word order.
fun build_padded_block() {
// Step 1: zero the whole block.
var i: u8 = 0
while i < 64 {
w[i] = 0
i += 1
}
// Step 2: copy the ASCII message bytes into the block,
// reversing byte order within each 4-byte group so the
// "big-endian word" becomes our "little-endian word". The
// byte index inside each word flips: 0↔3, 1↔2, 2↔1, 3↔0.
i = 0
while i < msg_len {
var word_idx: u8 = i & 0xFC // i rounded down to 4
var byte_idx: u8 = i & 0x03 // 0..3
var w_ofs: u8 = word_idx + (3 - byte_idx) // byte-swap within word
w[w_ofs] = msg[i]
i += 1
}
// Step 3: append the 0x80 end-of-message marker at the
// byte-swapped position for `msg_len`.
var pad_word: u8 = msg_len & 0xFC
var pad_byte: u8 = msg_len & 0x03
var pad_ofs: u8 = pad_word + (3 - pad_byte)
w[pad_ofs] = 0x80
// Step 4: write the 64-bit big-endian length into bytes
// 56..63 of the block. The SHA-256 view puts the MSB at
// b_56 and the LSB at b_63; since `msg_len` ≤ 16, the bit
// length is ≤ 128 and fits in a single byte. That byte is
// b_63, which under our byte-swap-within-word convention
// lands at w[60] (= word 15 byte 0 = u32 LSB).
w[60] = msg_len << 3
}
// ── Schedule and round steps ────────────────────────────────
//
// `schedule_one` computes w[i] from the earlier four entries;
// `round_one` runs one SHA-256 iteration against the current
// a..h at wk[0..31]. Both are written as plain NEScript so the
// compression driver can loop over them one step at a time
// between `wait_frame`s.
// Compute w[i] = σ1(w[i-2]) + w[i-7] + σ0(w[i-15]) + w[i-16].
// `w_byte` is the byte offset of w[i] inside the w[] array,
// i.e. `4 * i`.
fun schedule_one(w_byte: u8) {
// Temp accumulator lives at OFS_T1. Seed with w[i-16].
cp_w_to_wk(OFS_T1, w_byte - 64) // w[i-16]
add_w_to_wk(OFS_T1, w_byte - 28) // + w[i-7]
// Load w[i-15] into OFS_T2, then apply σ0 into OFS_SIG.
cp_w_to_wk(OFS_T2, w_byte - 60)
small_sigma0(OFS_T2) // SIG = σ0(T2)
add_wk(OFS_T1, OFS_SIG)
// Load w[i-2] into OFS_T2, then apply σ1 into OFS_SIG.
cp_w_to_wk(OFS_T2, w_byte - 8)
small_sigma1(OFS_T2) // SIG = σ1(T2)
add_wk(OFS_T1, OFS_SIG)
// Store T1 back into w[i].
cp_wk_to_w(w_byte, OFS_T1)
}
// Ch(e, f, g) = (e & f) ^ (~e & g). Writes to wk[OFS_SIG].
// Uses wk[OFS_TMP] as scratch (clobbered).
fun ch_into_sig() {
cp_wk(OFS_SIG, OFS_E)
and_wk(OFS_SIG, OFS_F) // SIG = e & f
cp_wk(OFS_TMP, OFS_E)
not_wk(OFS_TMP) // TMP = ~e
and_wk(OFS_TMP, OFS_G) // TMP = ~e & g
xor_wk(OFS_SIG, OFS_TMP) // SIG = ch
}
// Maj(a, b, c) = (a & b) ^ (a & c) ^ (b & c). Writes to
// wk[OFS_SIG]. Uses wk[OFS_TMP] as scratch (clobbered).
fun maj_into_sig() {
cp_wk(OFS_SIG, OFS_A)
and_wk(OFS_SIG, OFS_B) // SIG = a & b
cp_wk(OFS_TMP, OFS_A)
and_wk(OFS_TMP, OFS_C) // TMP = a & c
xor_wk(OFS_SIG, OFS_TMP) // SIG = (a&b) ^ (a&c)
cp_wk(OFS_TMP, OFS_B)
and_wk(OFS_TMP, OFS_C) // TMP = b & c
xor_wk(OFS_SIG, OFS_TMP) // SIG = maj
}
// Run one SHA-256 compression round. `kw_byte` is the byte
// offset shared by K[i] and w[i] (both tables hold 32-bit
// words at 4 bytes each, so their i-th entries sit at byte
// 4*i).
fun round_one(kw_byte: u8) {
// T1 = h + Σ1(e) + Ch(e,f,g) + K[i] + W[i]
cp_wk(OFS_T1, OFS_H)
big_sigma1(OFS_E) // SIG = Σ1(e)
add_wk(OFS_T1, OFS_SIG)
ch_into_sig() // SIG = ch
add_wk(OFS_T1, OFS_SIG)
add_k_to_wk(OFS_T1, kw_byte) // T1 += K[i]
add_w_to_wk(OFS_T1, kw_byte) // T1 += W[i]
// T2 = Σ0(a) + Maj(a,b,c). Compute Σ0(a) into SIG, stash
// in T2, then replace SIG with Maj and add into T2.
big_sigma0(OFS_A) // SIG = Σ0(a)
cp_wk(OFS_T2, OFS_SIG)
maj_into_sig() // SIG = maj
add_wk(OFS_T2, OFS_SIG)
// Shift registers: h=g, g=f, f=e, e=d+T1, d=c, c=b, b=a,
// a=T1+T2. Done in an order that avoids stomping live
// data (always write the later slot before reading the
// earlier).
cp_wk(OFS_H, OFS_G)
cp_wk(OFS_G, OFS_F)
cp_wk(OFS_F, OFS_E)
cp_wk(OFS_E, OFS_D)
add_wk(OFS_E, OFS_T1)
cp_wk(OFS_D, OFS_C)
cp_wk(OFS_C, OFS_B)
cp_wk(OFS_B, OFS_A)
cp_wk(OFS_A, OFS_T1)
add_wk(OFS_A, OFS_T2)
}
// Initialise a..h from h_state. Called once before the 64
// rounds start (inside Computing's on_enter).
fun init_abcdefgh() {
var i: u8 = 0
while i < 32 {
wk[i] = h_state[i]
i += 1
}
}
// Fold wk[A..H] back into h_state with eight 32-bit adds —
// the "H_i' = H_i + a_i" step at the end of block compression.
fun fold_abcdefgh() {
add_wk_to_h(0, OFS_A)
add_wk_to_h(4, OFS_B)
add_wk_to_h(8, OFS_C)
add_wk_to_h(12, OFS_D)
add_wk_to_h(16, OFS_E)
add_wk_to_h(20, OFS_F)
add_wk_to_h(24, OFS_G)
add_wk_to_h(28, OFS_H)
}