numeric.nx source
↩ module page · 185 lines · 6481 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
35import "syscalls.nx"
36
37// === Kahan running-sum state ======================================
38//
39// 16 bytes: { s: f64, err: f64 }. s is the rounded running sum;
40// err is the pending compensation that captures bits IEEE rounding
41// has dropped so far. Reusable via nx_kahan_reset.
42
43struct KahanState {
44 s: f64,
45 err: f64,
46}
47
48func nx_kahan_alloc() -> *KahanState {
49 let raw: *u8 = sys_mmap(16)
50 let st: *KahanState = raw as *KahanState
51 st.s = 0.0
52 st.err = 0.0
53 return st
54}
55
56func nx_kahan_reset(st: *KahanState) -> i64 {
57 st.s = 0.0
58 st.err = 0.0
59 return 0
60}
61
62func nx_kahan_finish(st: *KahanState) -> f64 {
63 return st.s
64}
65
66// === TwoSum (Knuth 1969 / Møller 1965) ============================
67//
68// Given two f64 inputs a, b, compute the IEEE-rounded sum s = a + b
69// and the EXACT rounding error err such that a + b == s + err in
70// real arithmetic. Their pair carries a > 53-bit-precision
71// representation of the true sum.
72//
73// The "fast" 6-op TwoSum that doesn't require |a| >= |b|. Dekker's
74// 3-op variant exists but needs the magnitude precondition; we
75// prefer fewer branches over fewer ops.
76//
77// API split into nx_two_sum (returns s) and nx_two_sum_err
78// (returns err) so each is single-return. Callers needing both
79// call both -- the IEEE rounding is deterministic, so s computed
80// twice gives the same bits.
81
82func nx_two_sum(a: f64, b: f64) -> f64 {
83 return a + b
84}
85
86func nx_two_sum_err(a: f64, b: f64) -> f64 {
87 let s: f64 = a + b
88 let bp: f64 = s - a
89 let ap: f64 = s - bp
90 let db: f64 = b - bp
91 let da: f64 = a - ap
92 return da + db
93}
94
95// === Dekker split (helper for TwoProduct) =========================
96//
97// SPLIT_FACTOR = 2^27 + 1 = 134217729. Multiplying an f64 by this
98// rounds to a_hi exactly because the trailing 26 bits get carried
99// into a_hi during round-to-nearest-even; a - a_hi yields a_lo,
100// the dropped low half.
101
102const NX_DEKKER_SPLIT: f64 = 134217729.0
103
104func nx_dekker_hi(a: f64) -> f64 {
105 let c: f64 = NX_DEKKER_SPLIT * a
106 return c - (c - a)
107}
108
109func nx_dekker_lo(a: f64) -> f64 {
110 let c: f64 = NX_DEKKER_SPLIT * a
111 let hi: f64 = c - (c - a)
112 return a - hi
113}
114
115// === TwoProduct (Veltkamp / Dekker) ===============================
116//
117// Compute the IEEE-rounded product p = a * b and the exact
118// rounding error err such that a * b == p + err in real arithmetic.
119// 17 f64 ops without FMA. When OP_FMADD codegen lands the err
120// computation collapses to fma(a, b, -p) (2 ops); this version
121// uses Dekker's split for portability.
122
123func nx_two_product(a: f64, b: f64) -> f64 {
124 return a * b
125}
126
127func nx_two_product_err(a: f64, b: f64) -> f64 {
128 let p: f64 = a * b
129 let a_hi: f64 = nx_dekker_hi(a)
130 let a_lo: f64 = nx_dekker_lo(a)
131 let b_hi: f64 = nx_dekker_hi(b)
132 let b_lo: f64 = nx_dekker_lo(b)
133 let t1: f64 = a_hi * b_hi - p
134 let t2: f64 = a_hi * b_lo
135 let t3: f64 = a_lo * b_hi
136 let t4: f64 = a_lo * b_lo
137 return t1 + t2 + t3 + t4
138}
139
140// === Kahan Compensated Summation (1965) ===========================
141//
142// Add x to the running state st with O(1) extra precision: tracks
143// the per-step rounding loss in st.err and reintroduces it on the
144// next step. Backward error bound:
145// |sum - true| <= 2 * eps * sum_of_|x_i|
146// independent of n -- vs naive summation's O(n*eps) bound.
147//
148// Caller: allocate via nx_kahan_alloc, feed via nx_kahan_add,
149// retrieve via nx_kahan_finish.
150
151func nx_kahan_add(st: *KahanState, x: f64) -> i64 {
152 let y: f64 = x - st.err
153 let t: f64 = st.s + y
154 let ts: f64 = t - st.s
155 let new_err: f64 = ts - y
156 st.s = t
157 st.err = new_err
158 return 0
159}
160
161// === Compensated Dot Product (Ogita-Rump-Oishi 2005) ==============
162//
163// dot(x, y) = sum_i x_i * y_i with a parallel-error track that
164// absorbs both per-product and per-sum rounding. Backward error
165// bound matches a 2K-bit accumulator at every step. Cost: ~25
166// f64 ops per element vs naive's 2. Kahan's MxMulEps paper makes
167// this non-negotiable for trustworthy matrix multiplication.
168
169func nx_compensated_dot(x: *f64, y: *f64, n: i64) -> f64 {
170 let sum_p: *KahanState = nx_kahan_alloc()
171 let sum_e: *KahanState = nx_kahan_alloc()
172 var i: i64 = 0
173 while i < n {
174 let xi: f64 = x[i]
175 let yi: f64 = y[i]
176 let p: f64 = xi * yi
177 let e: f64 = nx_two_product_err(xi, yi)
178 nx_kahan_add(sum_p, p)
179 nx_kahan_add(sum_e, e)
180 i = i + 1
181 }
182 let final_e: f64 = sum_e.s + sum_e.err
183 nx_kahan_add(sum_p, final_e)
184 return nx_kahan_finish(sum_p)
185}