1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 17:06:04 +00:00
nescript/examples/sha256/sha_core.ne
Claude 726faef538
sha256: specialize rotr_wk and shr_wk per rotation amount
The generic `rotr_wk(dst, n)` does its byte/bit decomposition
with two runtime `while` loops — necessary in the abstract, but
wasteful in SHA-256 where every rotation amount is one of ten
fixed compile-time constants (2, 6, 7, 11, 13, 17, 18, 19, 22,
25). The loop overhead alone is ~80 cycles of bookkeeping per
call, on top of the actual rotation work.

Replace each `rotr_wk(SIG, 6)` call inside the sigma helpers
with a dedicated `rotr_wk_6(SIG)` (and similarly for the other
amounts and for `shr_wk_3` / `shr_wk_10`). Each new helper just
chains the right number of `byte_rotr_wk` and `rotr1_wk` calls
inline — no `rem` variable, no `>= 8` / `> 0` checks.

The original `rotr_wk` / `shr_wk` wrappers stay defined for
runtime-amount callers; nothing else in the example uses them
today, but keeping them documents the general-purpose form.

Per SHA-256 block: ~45K cycles saved across 384 sigma rotations.
Combined with commit 0b5470b's leaf-function fix, the per-block
compression cost drops by ~3.5 frames at NTSC.

The hash output is unchanged (still
AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D
for "NES"), no other examples are affected, and the emulator
harness stays at 34/34.

https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 16:49:35 +00:00

760 lines
18 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 ──────────────────────────────────
//
// 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.
// wk[dst..dst+4] = wk[src..src+4]
fun cp_wk(dst: u8, src: u8) {
asm {
LDX {dst}
LDY {src}
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 {
LDX {dst}
LDY {src}
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 {
LDX {dst}
LDY {src}
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 {
LDX {dst}
LDY {src}
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 {
LDX {dst}
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 {
LDX {dst}
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 {
LDX {dst}
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.
//
// The SHA-256 sigmas only need a fixed set of rotation amounts
// (2, 6, 7, 11, 13, 17, 18, 19, 22, 25), so the per-amount
// helpers below skip this loop's runtime byte/bit decomposition
// and unroll the right number of `byte_rotr_wk` / `rotr1_wk`
// calls. This `rotr_wk` wrapper stays available for the rare
// caller that needs a runtime amount.
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
}
}
// ── Per-amount rotate helpers ───────────────────────────────
//
// Each `rotr_wk_<N>` rotates wk[dst..dst+4] right by exactly N
// bits with no loop overhead. Calling these directly from the
// sigma helpers replaces ~80 cycles of `rem >= 8` / `rem > 0`
// loop bookkeeping with the bare sequence of byte + bit
// rotations the analyzer-level constant rotation always reduces
// to. Per SHA-256 block, ~45K cycles saved across 384 sigma
// rotations.
fun rotr_wk_2(dst: u8) {
rotr1_wk(dst)
rotr1_wk(dst)
}
fun rotr_wk_6(dst: u8) {
rotr1_wk(dst)
rotr1_wk(dst)
rotr1_wk(dst)
rotr1_wk(dst)
rotr1_wk(dst)
rotr1_wk(dst)
}
fun rotr_wk_7(dst: u8) {
rotr1_wk(dst)
rotr1_wk(dst)
rotr1_wk(dst)
rotr1_wk(dst)
rotr1_wk(dst)
rotr1_wk(dst)
rotr1_wk(dst)
}
// 11 = 1 byte + 3 bits
fun rotr_wk_11(dst: u8) {
byte_rotr_wk(dst)
rotr1_wk(dst)
rotr1_wk(dst)
rotr1_wk(dst)
}
// 13 = 1 byte + 5 bits
fun rotr_wk_13(dst: u8) {
byte_rotr_wk(dst)
rotr1_wk(dst)
rotr1_wk(dst)
rotr1_wk(dst)
rotr1_wk(dst)
rotr1_wk(dst)
}
// 17 = 2 bytes + 1 bit
fun rotr_wk_17(dst: u8) {
byte_rotr_wk(dst)
byte_rotr_wk(dst)
rotr1_wk(dst)
}
// 18 = 2 bytes + 2 bits
fun rotr_wk_18(dst: u8) {
byte_rotr_wk(dst)
byte_rotr_wk(dst)
rotr1_wk(dst)
rotr1_wk(dst)
}
// 19 = 2 bytes + 3 bits
fun rotr_wk_19(dst: u8) {
byte_rotr_wk(dst)
byte_rotr_wk(dst)
rotr1_wk(dst)
rotr1_wk(dst)
rotr1_wk(dst)
}
// 22 = 2 bytes + 6 bits
fun rotr_wk_22(dst: u8) {
byte_rotr_wk(dst)
byte_rotr_wk(dst)
rotr1_wk(dst)
rotr1_wk(dst)
rotr1_wk(dst)
rotr1_wk(dst)
rotr1_wk(dst)
rotr1_wk(dst)
}
// 25 = 3 bytes + 1 bit
fun rotr_wk_25(dst: u8) {
byte_rotr_wk(dst)
byte_rotr_wk(dst)
byte_rotr_wk(dst)
rotr1_wk(dst)
}
// Shift wk[dst..dst+4] right by 1 bit (logical — top bit
// becomes 0).
fun shr1_wk(dst: u8) {
asm {
LDX {dst}
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 {
LDX {dst}
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). Generic
// runtime-amount form; the SHA-256 sigmas use the per-amount
// helpers below instead.
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
}
}
// 3 bits — used by σ0(x) = ... ^ (x >> 3)
fun shr_wk_3(dst: u8) {
shr1_wk(dst)
shr1_wk(dst)
shr1_wk(dst)
}
// 10 bits = 1 byte + 2 bits — used by σ1(x) = ... ^ (x >> 10)
fun shr_wk_10(dst: u8) {
byte_shr_wk(dst)
shr1_wk(dst)
shr1_wk(dst)
}
// ── Cross-array primitives ──────────────────────────────────
// wk[dst..dst+4] = w[w_ofs..w_ofs+4]
fun cp_w_to_wk(dst: u8, w_ofs: u8) {
asm {
LDX {dst}
LDY {w_ofs}
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 {
LDX {dst}
LDY {w_ofs}
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 {
LDX {src}
LDY {w_ofs}
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 {
LDX {h_ofs}
LDY {src}
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 {
LDX {dst}
LDY {k_ofs}
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_2(OFS_SIG)
cp_wk(OFS_TMP, src)
rotr_wk_13(OFS_TMP)
xor_wk(OFS_SIG, OFS_TMP)
cp_wk(OFS_TMP, src)
rotr_wk_22(OFS_TMP)
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_6(OFS_SIG)
cp_wk(OFS_TMP, src)
rotr_wk_11(OFS_TMP)
xor_wk(OFS_SIG, OFS_TMP)
cp_wk(OFS_TMP, src)
rotr_wk_25(OFS_TMP)
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_7(OFS_SIG)
cp_wk(OFS_TMP, src)
rotr_wk_18(OFS_TMP)
xor_wk(OFS_SIG, OFS_TMP)
cp_wk(OFS_TMP, src)
shr_wk_3(OFS_TMP)
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_17(OFS_SIG)
cp_wk(OFS_TMP, src)
rotr_wk_19(OFS_TMP)
xor_wk(OFS_SIG, OFS_TMP)
cp_wk(OFS_TMP, src)
shr_wk_10(OFS_TMP)
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)
}