nx_sketch_comparator.nx source
↩ module page · 220 lines · 8159 B
1// sketch_comparator.nx -- sealed-verdict comparator for head-to-head stomp claims.
2//
3// THE VALIDATION PRIMITIVE. Every claim of the form "our primitive
4// beats incumbent X on axis Y" must pass through this. Sealed verdicts:
5//
6// NX_CMP_VERDICT_BEATS our primitive measurably better
7// NX_CMP_VERDICT_EQUIVALENT within tolerance band; can't distinguish
8// NX_CMP_VERDICT_LOSES incumbent measurably better
9// NX_CMP_VERDICT_INCONCLUSIVE missing data or degenerate inputs
10//
11// THREE INDEPENDENT AXES:
12// ACCURACY -- whoever's closer to ground truth wins
13// MEMORY -- smaller bytes wins
14// TIME -- faster wall-time wins
15//
16// COMPOSITE verdict aggregates 3 axis verdicts via majority vote with
17// inconclusive-axis discounting. A 2/3 majority with one INCONCLUSIVE
18// is enough to call. A 1/1/1 tie returns EQUIVALENT.
19//
20// All inputs are i64; caller supplies measurements from external runs.
21// This primitive does NOT itself run benchmarks -- it normalizes the
22// VERDICT against typed tolerance, so claims become falsifiable.
23//
24// LOSSLESS-LANGUAGE DISCIPLINE: result struct carries verdict + signed
25// delta in PPM (positive = our wins) + ppb confidence.
26
27// nx_safety_envelope:
28// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
29// sil_target: SIL1
30// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
31// verdict: NOT_YET_EVALUATED
32
33import "nx_syscalls.nx"
34import "nx_sketch_types.nx"
35const NX_MAGIC_1000000000: i64 = 1000000000
36const NX_MAGIC_500000000: i64 = 500000000
37const NX_MAGIC_1000000: i64 = 1000000
38
39const NX_CMP_VERDICT_BEATS: i64 = 0
40const NX_CMP_VERDICT_EQUIVALENT: i64 = 1
41const NX_CMP_VERDICT_LOSES: i64 = 2
42const NX_CMP_VERDICT_INCONCLUSIVE: i64 = 3
43
44const NX_CMP_AXIS_ACCURACY: i64 = 0
45const NX_CMP_AXIS_MEMORY: i64 = 1
46const NX_CMP_AXIS_TIME: i64 = 2
47
48struct ComparisonResult {
49 verdict: i64, // sealed enum
50 delta_ppm: i64, // signed: positive = ours wins (by this amount in PPM)
51 conf_ppb: i64, // confidence (degenerate inputs lower this)
52 axis: i64, // which axis was compared
53}
54
55// === abs ==========================================================
56
57func nx_cmp_iabs(x: i64) -> i64 {
58 if x < 0 { return -x }
59 return x
60}
61
62// === ACCURACY axis ================================================
63//
64// Inputs: our_value, their_value, ground_truth, tolerance_ppm.
65// Closer to truth wins; tie if both within tolerance of equal distance.
66//
67// delta_ppm = (their_error - our_error) * 1_000_000 / |truth|
68// positive = our is closer (ours wins)
69// negative = their is closer (we lose)
70
71func nx_cmp_accuracy(our: i64, theirs: i64, truth: i64, tolerance_ppm: i64) -> *ComparisonResult {
72 let raw: *u8 = sys_mmap(40)
73 let r: *ComparisonResult = raw as *ComparisonResult
74 r.axis = NX_CMP_AXIS_ACCURACY
75 r.conf_ppb = NX_MAGIC_1000000000
76 if truth == 0 {
77 // Cannot normalize relative error.
78 r.verdict = NX_CMP_VERDICT_INCONCLUSIVE
79 r.delta_ppm = 0
80 r.conf_ppb = NX_MAGIC_500000000 // half-confidence (degenerate)
81 return r
82 }
83 let our_err: i64 = nx_cmp_iabs(our - truth)
84 let their_err: i64 = nx_cmp_iabs(theirs - truth)
85 let truth_abs: i64 = nx_cmp_iabs(truth)
86 // delta = (their_err - our_err) / truth, in PPM
87 let delta_ppm: i64 = ((their_err - our_err) * NX_MAGIC_1000000) / truth_abs
88 r.delta_ppm = delta_ppm
89 if delta_ppm > tolerance_ppm {
90 r.verdict = NX_CMP_VERDICT_BEATS
91 }
92 if delta_ppm < -tolerance_ppm {
93 r.verdict = NX_CMP_VERDICT_LOSES
94 }
95 if delta_ppm <= tolerance_ppm {
96 if delta_ppm >= -tolerance_ppm {
97 r.verdict = NX_CMP_VERDICT_EQUIVALENT
98 }
99 }
100 return r
101}
102
103// === MEMORY axis ==================================================
104//
105// Inputs: our_bytes, their_bytes, tolerance_ppm.
106// Smaller wins. delta_ppm = (their - our) * 1_000_000 / their.
107
108func nx_cmp_memory(our_bytes: i64, their_bytes: i64, tolerance_ppm: i64) -> *ComparisonResult {
109 let raw: *u8 = sys_mmap(40)
110 let r: *ComparisonResult = raw as *ComparisonResult
111 r.axis = NX_CMP_AXIS_MEMORY
112 r.conf_ppb = NX_MAGIC_1000000000
113 if their_bytes <= 0 {
114 r.verdict = NX_CMP_VERDICT_INCONCLUSIVE
115 r.delta_ppm = 0
116 r.conf_ppb = NX_MAGIC_500000000
117 return r
118 }
119 let delta_ppm: i64 = ((their_bytes - our_bytes) * NX_MAGIC_1000000) / their_bytes
120 r.delta_ppm = delta_ppm
121 if delta_ppm > tolerance_ppm {
122 r.verdict = NX_CMP_VERDICT_BEATS
123 }
124 if delta_ppm < -tolerance_ppm {
125 r.verdict = NX_CMP_VERDICT_LOSES
126 }
127 if delta_ppm <= tolerance_ppm {
128 if delta_ppm >= -tolerance_ppm {
129 r.verdict = NX_CMP_VERDICT_EQUIVALENT
130 }
131 }
132 return r
133}
134
135// === TIME axis ====================================================
136//
137// Inputs: our_us, their_us, tolerance_ppm. Faster wins.
138// delta_ppm = (their - our) * 1_000_000 / their.
139
140func nx_cmp_time(our_us: i64, their_us: i64, tolerance_ppm: i64) -> *ComparisonResult {
141 let raw: *u8 = sys_mmap(40)
142 let r: *ComparisonResult = raw as *ComparisonResult
143 r.axis = NX_CMP_AXIS_TIME
144 r.conf_ppb = NX_MAGIC_1000000000
145 if their_us <= 0 {
146 r.verdict = NX_CMP_VERDICT_INCONCLUSIVE
147 r.delta_ppm = 0
148 r.conf_ppb = NX_MAGIC_500000000
149 return r
150 }
151 let delta_ppm: i64 = ((their_us - our_us) * NX_MAGIC_1000000) / their_us
152 r.delta_ppm = delta_ppm
153 if delta_ppm > tolerance_ppm {
154 r.verdict = NX_CMP_VERDICT_BEATS
155 }
156 if delta_ppm < -tolerance_ppm {
157 r.verdict = NX_CMP_VERDICT_LOSES
158 }
159 if delta_ppm <= tolerance_ppm {
160 if delta_ppm >= -tolerance_ppm {
161 r.verdict = NX_CMP_VERDICT_EQUIVALENT
162 }
163 }
164 return r
165}
166
167// === COMPOSITE verdict ============================================
168//
169// Aggregate three axis verdicts. Rules (sealed):
170// - All three BEATS -> COMPOSITE BEATS (high confidence)
171// - 2 BEATS + 1 EQUIVALENT -> BEATS
172// - 2 BEATS + 1 LOSES -> BEATS but lower confidence (mixed)
173// - 1 BEATS + 2 EQUIVALENT -> BEATS (one clear win)
174// - All EQUIVALENT -> EQUIVALENT
175// - 1+ LOSES outweighs equal-count BEATS -> LOSES (loss-averse)
176// - Any 2+ INCONCLUSIVE -> INCONCLUSIVE
177//
178// This is a deliberate LOSS-AVERSE policy: tied verdicts default to
179// EQUIVALENT, not BEATS. We don't claim wins unless they're clear.
180
181func nx_cmp_composite(acc: *ComparisonResult, mem: *ComparisonResult,
182 tim: *ComparisonResult) -> i64 {
183 // Count INCONCLUSIVE results -- if 2+, refuse.
184 var n_inc: i64 = 0
185 if acc.verdict == NX_CMP_VERDICT_INCONCLUSIVE { n_inc = n_inc + 1 }
186 if mem.verdict == NX_CMP_VERDICT_INCONCLUSIVE { n_inc = n_inc + 1 }
187 if tim.verdict == NX_CMP_VERDICT_INCONCLUSIVE { n_inc = n_inc + 1 }
188 if n_inc >= 2 { return NX_CMP_VERDICT_INCONCLUSIVE }
189
190 var n_beats: i64 = 0
191 var n_loses: i64 = 0
192 if acc.verdict == NX_CMP_VERDICT_BEATS { n_beats = n_beats + 1 }
193 if mem.verdict == NX_CMP_VERDICT_BEATS { n_beats = n_beats + 1 }
194 if tim.verdict == NX_CMP_VERDICT_BEATS { n_beats = n_beats + 1 }
195 if acc.verdict == NX_CMP_VERDICT_LOSES { n_loses = n_loses + 1 }
196 if mem.verdict == NX_CMP_VERDICT_LOSES { n_loses = n_loses + 1 }
197 if tim.verdict == NX_CMP_VERDICT_LOSES { n_loses = n_loses + 1 }
198
199 // Loss-averse: any loss beats equal beats.
200 if n_loses > n_beats { return NX_CMP_VERDICT_LOSES }
201 if n_loses == n_beats {
202 if n_beats == 0 { return NX_CMP_VERDICT_EQUIVALENT }
203 return NX_CMP_VERDICT_EQUIVALENT // tie -> equivalent
204 }
205 // n_beats > n_loses
206 return NX_CMP_VERDICT_BEATS
207}
208
209// === query (typed envelope) =======================================
210
211func nx_cmp_query(r: *ComparisonResult) -> *ApproxI64 {
212 return nx_approx_new(r.verdict, NX_ENV_ABS, 0,
213 r.conf_ppb,
214 NX_MATURITY_PRODUCTION,
215 NX_ADV_HONEST)
216}
217
218func nx_cmp_memory_bytes(r: *ComparisonResult) -> i64 {
219 return 40
220}