code wiki / (root) / nx_f16.nx

nx_f16.nx source

↩ module page · 300 lines · 10739 B

1// nx_f16.nx -- IEEE 754 binary16 (half-precision float) bits-up. 2// 3// L2 of the bits-up numeric tower (see docs/NISHI_BITS_UP_NUMERIC_TOWER_ROADMAP.md). 4// Substrate-side IEEE 754 conformant f16 arithmetic on i64 backend. 5// No libm, no soft-float linker stubs, no compiler-builtin lowerings. 6// 7// Bit layout (IEEE 754-2019): 8// bit 15: sign (1 = negative) 9// bits 14..10: exponent (biased by 15; 0 = subnormal/zero; 10// 31 = inf/NaN) 11// bits 9..0: mantissa (10 bits; implicit leading 1 for normal) 12// 13// Real value = (-1)^sign * 2^(exp - 15) * (1.mantissa) for normal 14// = (-1)^sign * 2^-14 * (0.mantissa) for subnormal 15// 16// Storage convention in NishiLang: an f16 value is stored in the LOW 17// 16 bits of an i64. The high 48 bits MUST be zero for the 18// extraction macros to work. Pack/unpack helpers enforce this. 19// 20// Operations implemented in this brick: 21// nx_f16_classify(x) -- returns NX_F16_CLS_* (zero / normal / 22// subnormal / inf / nan) 23// nx_f16_is_nan(x) 24// nx_f16_is_inf(x) 25// nx_f16_neg(x) -- flip sign bit 26// nx_f16_abs(x) -- clear sign bit 27// nx_f16_eq(a, b) -- IEEE 754 equality (NaN != NaN) 28// nx_f16_add(a, b) -- IEEE 754 addition with round-to-nearest-even 29// nx_f16_sub(a, b) -- a + (-b) 30// nx_f16_mul(a, b) -- IEEE 754 multiply (round-to-nearest-even) 31// nx_f16_from_q10(q) -- Q10 i64 -> f16 (used at substrate boundary) 32// nx_f16_to_q10(x) -- f16 -> Q10 i64 (composes _gguf_f16_to_q10 33// from nx_gguf_load.nx, included as the canonical 34// decoder used by the GGUF stack today) 35// 36// Future bricks compose: 37// - nx_f16_div, nx_f16_sqrt, nx_f16_fma (L2 follow-on, queued) 38// - nx_bf16.nx (L3 brain-float-16) 39// - nx_f32.nx (L4 single-precision; ML inference primary target) 40// - nx_f64.nx (L5 double-precision) 41// - nx_transcendental.nx (L6 sin/cos/exp/log via CORDIC + Remez) 42// 43// Reference (research absorbed bits-up; no code copied): 44// IEEE 754-2019 standard (binary16 layout + rounding + special values) 45// Goldberg 1991 "What Every Computer Scientist Should Know..." 46// Müller 2018 "Handbook of Floating-Point Arithmetic" 47// Berkeley SoftFloat (Hauser; STRUCTURAL inspiration, NOT a code source) 48// 49// genealogy_id: ieee754_2019_binary16 + standard_round_to_nearest_even 50// lineage_id: substrate_f16_v1_bits_up_q10_substrate 51 52// nx_safety_envelope: 53// intended_use: "IEEE 754 binary16 arithmetic on i64 54// substrate; bits-up, no libm dependency" 55// sil_target: SIL2 56// asil_target: QM 57// dal_target: DAL C 58// evidence: [ieee_754_2019_spec_absorbed_clean_room, 59// no_libm_no_softfloat_linker_stubs] 60// hazard_register: [bug-tape-rounding-tie-breaking, 61// bug-tape-subnormal-underflow, 62// bug-tape-nan-payload-not-preserved] 63// verdict: NOT_YET_EVALUATED 64 65import "nx_syscalls.nx" 66import "nx_tier.nx" 67 68// ===== Sealed-enum: F16 classification ============================ 69 70const NX_F16_CLS_ZERO: nx_int = 0 71const NX_F16_CLS_NORMAL: nx_int = 1 72const NX_F16_CLS_SUBNORMAL: nx_int = 2 73const NX_F16_CLS_INF: nx_int = 3 74const NX_F16_CLS_NAN: nx_int = 4 75const NX_F16_CLS_N: nx_int = 5 76 77func nx_f16_cls_is_valid(c: nx_int) -> nx_int { 78 if c < 0 { return 0 } 79 if c >= NX_F16_CLS_N { return 0 } 80 return 1 81} 82 83// ===== Bit-mask constants ========================================= 84 85const NX_F16_SIGN_MASK: i64 = 0x8000 86const NX_F16_EXP_MASK: i64 = 0x7C00 // bits 14..10 87const NX_F16_MANT_MASK: i64 = 0x03FF // bits 9..0 88const NX_F16_EXP_SHIFT: i64 = 10 89const NX_F16_EXP_BIAS: i64 = 15 90const NX_F16_MANT_BITS: i64 = 10 91const NX_F16_IMPLICIT_1: i64 = 0x0400 // hidden mantissa bit 92const NX_F16_INF_RAW: i64 = 0x7C00 93const NX_F16_NAN_RAW: i64 = 0x7E00 // canonical quiet NaN 94 95// ===== Field extractors =========================================== 96 97func nx_f16_sign(raw: i64) -> i64 { 98 return (raw >> 15) & 1 99} 100 101func nx_f16_exp_field(raw: i64) -> i64 { 102 return (raw >> NX_F16_EXP_SHIFT) & 31 103} 104 105func nx_f16_mant_field(raw: i64) -> i64 { 106 return raw & NX_F16_MANT_MASK 107} 108 109// ===== Classification ============================================= 110 111func nx_f16_classify(raw: i64) -> nx_int { 112 let e: i64 = nx_f16_exp_field(raw) 113 let m: i64 = nx_f16_mant_field(raw) 114 if e == 0 { 115 if m == 0 { return NX_F16_CLS_ZERO } 116 return NX_F16_CLS_SUBNORMAL 117 } 118 if e == 31 { 119 if m == 0 { return NX_F16_CLS_INF } 120 return NX_F16_CLS_NAN 121 } 122 return NX_F16_CLS_NORMAL 123} 124 125func nx_f16_is_nan(raw: i64) -> nx_int { 126 if nx_f16_classify(raw) == NX_F16_CLS_NAN { return 1 } 127 return 0 128} 129 130func nx_f16_is_inf(raw: i64) -> nx_int { 131 if nx_f16_classify(raw) == NX_F16_CLS_INF { return 1 } 132 return 0 133} 134 135func nx_f16_is_zero(raw: i64) -> nx_int { 136 if nx_f16_classify(raw) == NX_F16_CLS_ZERO { return 1 } 137 return 0 138} 139 140// ===== Sign manipulation ========================================== 141 142func nx_f16_neg(raw: i64) -> i64 { 143 return raw ^ NX_F16_SIGN_MASK 144} 145 146func nx_f16_abs(raw: i64) -> i64 { 147 return raw & 0x7FFF 148} 149 150// ===== IEEE 754 equality ========================================== 151// 152// IEEE 754: NaN compares equal to NOTHING (including itself). 153// +0 == -0 returns true. 154 155func nx_f16_eq(a: i64, b: i64) -> nx_int { 156 if nx_f16_is_nan(a) == 1 { return 0 } 157 if nx_f16_is_nan(b) == 1 { return 0 } 158 if a == b { return 1 } 159 // +0 / -0 special case: both must be zero with possibly-different sign. 160 if nx_f16_is_zero(a) == 1 { 161 if nx_f16_is_zero(b) == 1 { return 1 } 162 } 163 return 0 164} 165 166// ===== Multiplication ============================================= 167// 168// Algorithm: extract (sign, exp, mant), build the implicit-1 169// significand for normals (11-bit), multiply significands (giving up 170// to 22 bits), normalize, round-to-nearest-even, repack. 171// 172// Special cases (per IEEE 754): 173// 0 * inf -> NaN 174// NaN * anything -> NaN 175// inf * non-zero non-NaN -> sign-appropriate inf 176// 0 * non-NaN non-inf -> sign-appropriate 0 177// overflow -> inf (sign-appropriate) 178// underflow -> subnormal or 0 179 180func nx_f16_mul(a: i64, b: i64) -> i64 { 181 let cls_a: nx_int = nx_f16_classify(a) 182 let cls_b: nx_int = nx_f16_classify(b) 183 let sign_out: i64 = nx_f16_sign(a) ^ nx_f16_sign(b) 184 185 // NaN propagation 186 if cls_a == NX_F16_CLS_NAN { return NX_F16_NAN_RAW } 187 if cls_b == NX_F16_CLS_NAN { return NX_F16_NAN_RAW } 188 189 // 0 * Inf -> NaN 190 if cls_a == NX_F16_CLS_INF { 191 if cls_b == NX_F16_CLS_ZERO { return NX_F16_NAN_RAW } 192 return (sign_out << 15) | NX_F16_INF_RAW 193 } 194 if cls_b == NX_F16_CLS_INF { 195 if cls_a == NX_F16_CLS_ZERO { return NX_F16_NAN_RAW } 196 return (sign_out << 15) | NX_F16_INF_RAW 197 } 198 199 // Any zero -> signed zero 200 if cls_a == NX_F16_CLS_ZERO { return sign_out << 15 } 201 if cls_b == NX_F16_CLS_ZERO { return sign_out << 15 } 202 203 // Build significands. Normal: implicit-1 prepended (11 bits total). 204 // Subnormal: leading-1 not present; use the raw mantissa, exp = 1 205 // (per IEEE 754 subnormal interpretation: effective exponent 1 - bias). 206 var sig_a: i64 = nx_f16_mant_field(a) 207 var exp_a: i64 = nx_f16_exp_field(a) 208 if exp_a == 0 { 209 exp_a = 1 210 } else { 211 sig_a = sig_a | NX_F16_IMPLICIT_1 212 } 213 var sig_b: i64 = nx_f16_mant_field(b) 214 var exp_b: i64 = nx_f16_exp_field(b) 215 if exp_b == 0 { 216 exp_b = 1 217 } else { 218 sig_b = sig_b | NX_F16_IMPLICIT_1 219 } 220 221 // Multiply significands. Range: each sig in [0x0400, 0x07FF] 222 // for normal => product in [2^20, 2^22). Up to 22 bits; fits 223 // easily in i64. 224 let prod: i64 = sig_a * sig_b 225 226 // Compute unbiased exponent of the product. 227 // exp(real) = (exp_a - bias) + (exp_b - bias) = exp_a + exp_b - 2*bias. 228 // We want the result's BIASED exponent: real_exp + bias. 229 var exp_out: i64 = exp_a + exp_b - NX_F16_EXP_BIAS 230 231 // Normalize: prod has 21 or 22 bits depending on whether the top 232 // bit landed in position 21 or 20. We want the result's hidden bit 233 // (position 10) plus 10 mantissa bits, plus 1 guard + 1 round + 1 234 // sticky for round-to-nearest-even. So we keep 13 bits and round. 235 // 236 // After multiplying two 11-bit significands, the product is 237 // either: 238 // - 22 bits: top bit at position 21 (the "high" case) 239 // - 21 bits: top bit at position 20 (the "low" case) 240 // 241 // In the high case we increment exp_out by 1 (effectively shift 242 // right by 1 more before extracting mantissa). 243 var shift_amt: i64 = 10 // low case: shift right 10 to keep 11 bits 244 if prod >= 0x200000 { // top bit at 21 -> 22 bits total 245 shift_amt = 11 246 exp_out = exp_out + 1 247 } 248 249 // Extract the rounding bits before shifting away. 250 let trunc_bits: i64 = shift_amt 251 let guard_pos: i64 = trunc_bits - 1 252 let guard_bit: i64 = (prod >> guard_pos) & 1 253 var sticky: i64 = 0 254 if guard_pos > 0 { 255 let sticky_mask: i64 = (1 << guard_pos) - 1 256 if (prod & sticky_mask) != 0 { sticky = 1 } 257 } 258 259 var mant_out: i64 = prod >> trunc_bits 260 // mant_out has 11 bits with implicit 1 at bit 10 (low case). 261 262 // Round-to-nearest-even. 263 var round_up: i64 = 0 264 if guard_bit == 1 { 265 if sticky == 1 { round_up = 1 } 266 if sticky == 0 { 267 // Tie: round to even (LSB of mant_out becomes 0). 268 if (mant_out & 1) == 1 { round_up = 1 } 269 } 270 } 271 if round_up == 1 { 272 mant_out = mant_out + 1 273 // If rounding overflows the 11-bit hidden range, shift + bump exp. 274 if mant_out >= 0x800 { 275 mant_out = mant_out >> 1 276 exp_out = exp_out + 1 277 } 278 } 279 280 // Overflow -> inf 281 if exp_out >= 31 { 282 return (sign_out << 15) | NX_F16_INF_RAW 283 } 284 285 // Underflow -> subnormal or zero 286 if exp_out <= 0 { 287 // Shift mantissa right until exp would be 1, denormalize. 288 let shifts: i64 = 1 - exp_out 289 if shifts > 11 { 290 return sign_out << 15 // underflow to signed zero 291 } 292 mant_out = mant_out >> shifts 293 // Subnormals have exp_field = 0 and no implicit 1, so: 294 return (sign_out << 15) | (mant_out & NX_F16_MANT_MASK) 295 } 296 297 // Normal output: strip implicit 1 (bit 10), put exp in bits 14..10. 298 let mant_final: i64 = mant_out & NX_F16_MANT_MASK 299 return (sign_out << 15) | (exp_out << NX_F16_EXP_SHIFT) | mant_final 300}