nx_numeric.nx source
↩ module page · 191 lines · 6550 B
1// numeric.nx -- Kahan-discipline numerical primitives.
2//
3// Per W. Kahan ("How Java's Floating-Point Hurts Everyone Everywhere"
4// 1998 + "MxMulEps" matrix-mul precision paper), a numerically sound
5// language must expose substrate-level building blocks for
6// catastrophic-cancellation-resistant arithmetic. This file ships
7// the canonical three: TwoSum, TwoProduct, and Kahan compensated
8// summation.
9//
10// Compilation note: this module uses `f64` types and arithmetic
11// operators directly, which the NishiLang sovereign sibling
12// (nxc.elf) accepts but the C bootstrap (nxc2.exe) does NOT lex.
13// That is the correct division of labor: C bootstrap is one
14// Wheeler-comparator anchor for the integer subset; f64-using
15// modules Wheeler against a different peer (rustc, gcc-riscv64
16// compiling an equivalent C program, or self-host nxc.elf when
17// it ships). NishiLang is not limited by what its bootstrap
18// can lex; the bootstrap is limited by NishiLang's needs.
19//
20// Calling convention: state structs caller-allocated via
21// sys_mmap, mutated through pointers; scalar helpers return
22// single f64 values. No struct-by-value returns, no &local
23// out-params (pre-existing language constraints).
24//
25// Bibliography:
26// Dekker (1971) "A Floating-Point Technique for Extending
27// the Available Precision" -- TwoSum,
28// TwoProduct, the SPLIT_FACTOR trick
29// Møller (1965) pre-Dekker accumulating-correction summation
30// Kahan (1965) compensated summation algorithm
31// Kahan & Ivory (1995) "Roundoff Degrades an Idealized Cantilever"
32// Ogita-Rump-Oishi (2005) "Accurate Sum and Dot Product"
33// -- compensated dot product
34
35// nx_safety_envelope:
36// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
37// sil_target: SIL1
38// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
39// verdict: NOT_YET_EVALUATED
40
41import "nx_syscalls.nx"
42
43// === Kahan running-sum state ======================================
44//
45// 16 bytes: { s: f64, err: f64 }. s is the rounded running sum;
46// err is the pending compensation that captures bits IEEE rounding
47// has dropped so far. Reusable via nx_kahan_reset.
48
49struct KahanState {
50 s: f64,
51 err: f64,
52}
53
54func nx_kahan_alloc() -> *KahanState {
55 let raw: *u8 = sys_mmap(16)
56 let st: *KahanState = raw as *KahanState
57 st.s = 0.0
58 st.err = 0.0
59 return st
60}
61
62func nx_kahan_reset(st: *KahanState) -> i64 {
63 st.s = 0.0
64 st.err = 0.0
65 return 0
66}
67
68func nx_kahan_finish(st: *KahanState) -> f64 {
69 return st.s
70}
71
72// === TwoSum (Knuth 1969 / Møller 1965) ============================
73//
74// Given two f64 inputs a, b, compute the IEEE-rounded sum s = a + b
75// and the EXACT rounding error err such that a + b == s + err in
76// real arithmetic. Their pair carries a > 53-bit-precision
77// representation of the true sum.
78//
79// The "fast" 6-op TwoSum that doesn't require |a| >= |b|. Dekker's
80// 3-op variant exists but needs the magnitude precondition; we
81// prefer fewer branches over fewer ops.
82//
83// API split into nx_two_sum (returns s) and nx_two_sum_err
84// (returns err) so each is single-return. Callers needing both
85// call both -- the IEEE rounding is deterministic, so s computed
86// twice gives the same bits.
87
88func nx_two_sum(a: f64, b: f64) -> f64 {
89 return a + b
90}
91
92func nx_two_sum_err(a: f64, b: f64) -> f64 {
93 let s: f64 = a + b
94 let bp: f64 = s - a
95 let ap: f64 = s - bp
96 let db: f64 = b - bp
97 let da: f64 = a - ap
98 return da + db
99}
100
101// === Dekker split (helper for TwoProduct) =========================
102//
103// SPLIT_FACTOR = 2^27 + 1 = 134217729. Multiplying an f64 by this
104// rounds to a_hi exactly because the trailing 26 bits get carried
105// into a_hi during round-to-nearest-even; a - a_hi yields a_lo,
106// the dropped low half.
107
108const NX_DEKKER_SPLIT: f64 = 134217729.0
109
110func nx_dekker_hi(a: f64) -> f64 {
111 let c: f64 = NX_DEKKER_SPLIT * a
112 return c - (c - a)
113}
114
115func nx_dekker_lo(a: f64) -> f64 {
116 let c: f64 = NX_DEKKER_SPLIT * a
117 let hi: f64 = c - (c - a)
118 return a - hi
119}
120
121// === TwoProduct (Veltkamp / Dekker) ===============================
122//
123// Compute the IEEE-rounded product p = a * b and the exact
124// rounding error err such that a * b == p + err in real arithmetic.
125// 17 f64 ops without FMA. When OP_FMADD codegen lands the err
126// computation collapses to fma(a, b, -p) (2 ops); this version
127// uses Dekker's split for portability.
128
129func nx_two_product(a: f64, b: f64) -> f64 {
130 return a * b
131}
132
133func nx_two_product_err(a: f64, b: f64) -> f64 {
134 let p: f64 = a * b
135 let a_hi: f64 = nx_dekker_hi(a)
136 let a_lo: f64 = nx_dekker_lo(a)
137 let b_hi: f64 = nx_dekker_hi(b)
138 let b_lo: f64 = nx_dekker_lo(b)
139 let t1: f64 = a_hi * b_hi - p
140 let t2: f64 = a_hi * b_lo
141 let t3: f64 = a_lo * b_hi
142 let t4: f64 = a_lo * b_lo
143 return t1 + t2 + t3 + t4
144}
145
146// === Kahan Compensated Summation (1965) ===========================
147//
148// Add x to the running state st with O(1) extra precision: tracks
149// the per-step rounding loss in st.err and reintroduces it on the
150// next step. Backward error bound:
151// |sum - true| <= 2 * eps * sum_of_|x_i|
152// independent of n -- vs naive summation's O(n*eps) bound.
153//
154// Caller: allocate via nx_kahan_alloc, feed via nx_kahan_add,
155// retrieve via nx_kahan_finish.
156
157func nx_kahan_add(st: *KahanState, x: f64) -> i64 {
158 let y: f64 = x - st.err
159 let t: f64 = st.s + y
160 let ts: f64 = t - st.s
161 let new_err: f64 = ts - y
162 st.s = t
163 st.err = new_err
164 return 0
165}
166
167// === Compensated Dot Product (Ogita-Rump-Oishi 2005) ==============
168//
169// dot(x, y) = sum_i x_i * y_i with a parallel-error track that
170// absorbs both per-product and per-sum rounding. Backward error
171// bound matches a 2K-bit accumulator at every step. Cost: ~25
172// f64 ops per element vs naive's 2. Kahan's MxMulEps paper makes
173// this non-negotiable for trustworthy matrix multiplication.
174
175func nx_compensated_dot(x: *f64, y: *f64, n: i64) -> f64 {
176 let sum_p: *KahanState = nx_kahan_alloc()
177 let sum_e: *KahanState = nx_kahan_alloc()
178 var i: i64 = 0
179 while i < n {
180 let xi: f64 = x[i]
181 let yi: f64 = y[i]
182 let p: f64 = xi * yi
183 let e: f64 = nx_two_product_err(xi, yi)
184 nx_kahan_add(sum_p, p)
185 nx_kahan_add(sum_e, e)
186 i = i + 1
187 }
188 let final_e: f64 = sum_e.s + sum_e.err
189 nx_kahan_add(sum_p, final_e)
190 return nx_kahan_finish(sum_p)
191}