code wiki / (root) / nx_u4096_mul_test.nx

nx_u4096_mul_test.nx source

↩ module page · 204 lines · 7191 B

1// nx_u4096_mul_test.nx -- KAT for the 4096x4096 -> 8192 wide-multiply. 2// 3// Mirrors nx_u256_mul_test cases, scaled to 256-limb wide buffer. 4// All five algebraic-identity cases that prove a schoolbook 5// multiplier is correct: 6// - 0 * 0 = 0 7// - 1 * 1 = 1 8// - 3 * 5 = 15 (small * small) 9// - 0xFFFFFFFF * 0xFFFFFFFF = 0xFFFFFFFE00000001 (well-known 10// limb-saturation square, exercises carry from limb 0 to limb 1) 11// - 2^32 * 2^32 = 2^64 (single-bit cross-limb) 12// - 2^4096 - 1 squared (exercises full 256-limb wide range; high 13// limb 255 must be non-zero, exercising last carry-out) 14// - shl1 round-trip on wide buffer 15// 16// expect_exit: 0 17// license_tier: ORIGINAL 18 19import "nx_syscalls.nx" 20import "nx_u4096.nx" 21import "nx_u4096_mul.nx" 22 23func main() -> i64 { 24 let a: *i64 = u4096_alloc() 25 let b: *i64 = u4096_alloc() 26 let out: *i64 = u4096_wide_alloc() 27 28 // ---- Test A: 0 * 0 = 0 ---- 29 u4096_zero(a); u4096_zero(b) 30 u4096_mul_wide(out, a, b) 31 var i: i64 = 0 32 while i < NX_U4096_WIDE_LIMBS { 33 if out[i] != 0 { return 1 } 34 i = i + 1 35 } 36 37 // ---- Test B: 1 * 1 = 1 ---- 38 u4096_one(a); u4096_one(b) 39 u4096_mul_wide(out, a, b) 40 if out[0] != 1 { return 2 } 41 i = 1 42 while i < NX_U4096_WIDE_LIMBS { 43 if out[i] != 0 { return 3 } 44 i = i + 1 45 } 46 47 // ---- Test C: 3 * 5 = 15 ---- 48 u4096_zero(a); a[0] = 3 49 u4096_zero(b); b[0] = 5 50 u4096_mul_wide(out, a, b) 51 if out[0] != 15 { return 4 } 52 i = 1 53 while i < NX_U4096_WIDE_LIMBS { 54 if out[i] != 0 { return 5 } 55 i = i + 1 56 } 57 58 // ---- Test D: 0xFFFFFFFF * 0xFFFFFFFF = 0xFFFFFFFE00000001 ---- 59 u4096_zero(a); a[0] = 0xFFFFFFFF 60 u4096_zero(b); b[0] = 0xFFFFFFFF 61 u4096_mul_wide(out, a, b) 62 if out[0] != 0x00000001 { return 6 } 63 if out[1] != 0xFFFFFFFE { return 7 } 64 i = 2 65 while i < NX_U4096_WIDE_LIMBS { 66 if out[i] != 0 { return 8 } 67 i = i + 1 68 } 69 70 // ---- Test E: 2^32 * 2^32 = 2^64 (single-bit cross-limb) ---- 71 u4096_zero(a); a[1] = 1 72 u4096_zero(b); b[1] = 1 73 u4096_mul_wide(out, a, b) 74 if out[0] != 0 { return 10 } 75 if out[1] != 0 { return 11 } 76 if out[2] != 1 { return 12 } 77 i = 3 78 while i < NX_U4096_WIDE_LIMBS { 79 if out[i] != 0 { return 13 } 80 i = i + 1 81 } 82 83 // ---- Test F: a * 1 = a (identity on full-width operand) ---- 84 // Set a = some pattern across all limbs, b = 1, verify out_low == a 85 // and out_high all zero. 86 var k: i64 = 0 87 while k < NX_U4096_LIMBS { 88 a[k] = (k * 7 + 1) & NX_U4096_LIMB_MASK 89 k = k + 1 90 } 91 u4096_zero(b); b[0] = 1 92 u4096_mul_wide(out, a, b) 93 k = 0 94 while k < NX_U4096_LIMBS { 95 let expect: i64 = (k * 7 + 1) & NX_U4096_LIMB_MASK 96 if (out[k] & NX_U4096_LIMB_MASK) != expect { return 20 } 97 k = k + 1 98 } 99 k = NX_U4096_LIMBS 100 while k < NX_U4096_WIDE_LIMBS { 101 if out[k] != 0 { return 21 } 102 k = k + 1 103 } 104 105 // ---- Test G: (2^4096 - 1)^2 exercises the high-limb carry-out ---- 106 // (2^4096 - 1)^2 = 2^8192 - 2^4097 + 1 107 // low limb 0 = 1 108 // limbs 1..127 = 0 109 // limb 128 (bit 4096) = 0 (since -2^4097 only flips bit 4097) 110 // limb 128 high bit (bit 4097) = ... wait let me re-derive. 111 // 112 // (2^n - 1)^2 = 2^(2n) - 2*2^n + 1 = 2^(2n) - 2^(n+1) + 1. 113 // For n = 4096: result = 2^8192 - 2^4097 + 1. 114 // 2^8192 -> limb 256 bit 0 (overflow; we have 256 limbs = 8192 bits; 115 // so 2^8192 wraps to "0" within our wide buffer) 116 // -2^4097 + 1 -> we have a 8192-bit container; -2^4097 + 1 in two's-complement 117 // in our space is 2^8192 - 2^4097 + 1. 118 // Combined: 2^8192 + (2^8192 - 2^4097 + 1) = 2 * 2^8192 - 2^4097 + 1. 119 // Modulo 2^8192 that's -2^4097 + 1 = 2^8192 - 2^4097 + 1. 120 // 121 // So expected low limbs: 122 // limb 0 = 1 123 // limbs 1..127 = 0 124 // limb 128: bit 0 = 0 (the -2^4097 + 1 in this range subtracts bit 4097 125 // from the implicit 2^8192; the borrow propagates down filling 126 // with 0xFFFFFFFF from limb 128 (bit 4097's limb) downward). 127 // 128 // Actually it's easier to just compute and check observable invariants: 129 // out[0] must equal 1 (the +1 term). 130 // The schoolbook produces (2^n-1)^2 directly: limb 0 = 1, limbs 1..127 131 // form the "-2^4097 + 1 - 1" pattern which is borrow-propagated, and 132 // limbs 128..255 form the "2^8192 - 2^4097" pattern. 133 // 134 // Concretely, computing (2^4096 - 1)^2 with the standard formula: 135 // Each a[i] = 0xFFFFFFFF for i in 0..127. Product of any two is 136 // (2^32-1)^2 = 2^64 - 2^33 + 1. Sum over all 128*128 partial products 137 // produces the well-known: low limb 0 = 1; limb 127 = 0xFFFFFFFE; 138 // limb 128 = 0xFFFFFFFE; high limb 255 = 0xFFFFFFFE. 139 // 140 // Rather than encode the full expected pattern (too error-prone), 141 // assert the load-bearing endpoints: out[0] == 1 and out[255] != 0. 142 k = 0 143 while k < NX_U4096_LIMBS { 144 a[k] = NX_U4096_LIMB_MASK 145 b[k] = NX_U4096_LIMB_MASK 146 k = k + 1 147 } 148 u4096_mul_wide(out, a, b) 149 // (2^4096-1)^2 = 2^8192 - 2^4097 + 1 150 // = bit 0 set + bits 4097..8191 set (other bits zero) 151 // => out[0] = 0x00000001 152 // out[1..127] = 0 153 // out[128] = 0xFFFFFFFE (bit 0 of limb 128 = bit 4096 = zero; 154 // bits 1..31 of limb 128 = bits 4097..4127 = set) 155 // out[129..255] = 0xFFFFFFFF (bits 4128..8191 all set) 156 if out[0] != 1 { return 30 } 157 if (out[128] & NX_U4096_LIMB_MASK) != 0xFFFFFFFE { return 31 } 158 if (out[255] & NX_U4096_LIMB_MASK) != 0xFFFFFFFF { return 32 } 159 if (out[129] & NX_U4096_LIMB_MASK) != 0xFFFFFFFF { return 33 } 160 if out[127] != 0 { return 34 } 161 162 // ---- Test H: wide_shl1 round-trip ---- 163 let w: *i64 = u4096_wide_alloc() 164 u4096_zero(w as *i64) 165 w[0] = 1 166 u4096_wide_shl1(w) 167 if w[0] != 2 { return 40 } 168 u4096_wide_shl1(w) 169 if w[0] != 4 { return 41 } 170 171 // ---- Test I: wide_get_bit ---- 172 var z: i64 = 0 173 while z < NX_U4096_WIDE_LIMBS { w[z] = 0; z = z + 1 } 174 w[4] = 0x80000000 // bit 32*4+31 = 159 set 175 if u4096_wide_get_bit(w, 159) != 1 { return 50 } 176 if u4096_wide_get_bit(w, 158) != 0 { return 51 } 177 if u4096_wide_get_bit(w, 160) != 0 { return 52 } 178 179 // ---- Test J: wide_sub_low + wide_low_cmp ---- 180 let modn: *i64 = u4096_alloc() 181 u4096_zero(modn); modn[0] = 5 182 var z2: i64 = 0 183 while z2 < NX_U4096_WIDE_LIMBS { w[z2] = 0; z2 = z2 + 1 } 184 w[0] = 12 185 let cv: i64 = u4096_wide_low_cmp(w, modn) 186 if cv != 1 { return 60 } // 12 > 5 187 let bv: i64 = u4096_wide_sub_low(w, modn) 188 if bv != 0 { return 61 } 189 if w[0] != 7 { return 62 } 190 let cv2: i64 = u4096_wide_low_cmp(w, modn) 191 if cv2 != 1 { return 63 } // 7 > 5 192 193 // ---- Test K: wide_copy_low ---- 194 let low: *i64 = u4096_alloc() 195 u4096_zero(low) 196 u4096_wide_copy_low(low, w) 197 if low[0] != 7 { return 70 } 198 199 // "PASS\n" 200 let ok: *u8 = sys_mmap(8) 201 ok[0]=80; ok[1]=65; ok[2]=83; ok[3]=83; ok[4]=10 202 sys_write(1, ok, 5) 203 return 0 204}