nx_essentials.nx source
↩ module page · 408 lines · 11219 B
1// nx_essentials.nx -- the gajillion tiny reused functions.
2//
3// User cardinal 2026-05-15: "I'm sure there's a gajillion basic or
4// tiny functions that could live closer to the bits faster and
5// better, that are reused over and over again that means they really
6// should probably just be primitives."
7//
8// This file is the substrate's batch of MICRO-PRIMITIVES every
9// program in the substrate reuses. Built closer to the bits so
10// callers stop reimplementing them per-file. ~25 functions across
11// 7 families:
12//
13// 1. Comparison / clamp min / max / clamp / signed-clamp
14// 2. Sign / abs / swap abs / sign / swap_via_pointer
15// 3. Saturating arithmetic sat_add / sat_sub / sat_mul
16// 4. Q10 fixed-point helpers q10_mul / q10_div / lerp / pct
17// 5. Integer number theory gcd / lcm / pow_int / mod_floor
18// 6. Bit-level popcount / clz / ctz / log2_floor /
19// log2_ceil / bit_test / set / clear /
20// rotate_left / rotate_right
21// 7. Byte / char essentials memcpy / memset / memcmp / char_is_*
22// / to_lower / to_upper
23//
24// Every function: pure, deterministic, nx_int / *u8. No allocations.
25// No fidelity reporting (these are too small to warrant dual-reading;
26// they ARE the bits-up substrate that the dual-reading primitives use).
27//
28// genealogy_id: warren_2013_hackers_delight + knuth_taocp_v2_seminum +
29// c_standard_iso_9899 + python_stdlib_2025
30// lineage_id: essentials_micro_primitives_q10
31
32// nx_safety_envelope:
33// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
34// sil_target: SIL1
35// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
36// verdict: NOT_YET_EVALUATED
37
38import "nx_syscalls.nx"
39import "nx_tier.nx"
40import "nx_bits.nx"
41
42const NX_ESS_Q: nx_int = 1024
43const NX_ESS_I64_MAX: nx_int = 9223372036854775807
44const NX_ESS_I64_MIN: nx_int = -9223372036854775807
45
46// ===== Family 1: comparison / clamp ==================================
47
48func nx_min2(a: nx_int, b: nx_int) -> nx_int {
49 if a < b { return a }
50 return b
51}
52
53func nx_max2(a: nx_int, b: nx_int) -> nx_int {
54 if a > b { return a }
55 return b
56}
57
58func nx_min3(a: nx_int, b: nx_int, c: nx_int) -> nx_int {
59 var m: nx_int = a
60 if b < m { m = b }
61 if c < m { m = c }
62 return m
63}
64
65func nx_max3(a: nx_int, b: nx_int, c: nx_int) -> nx_int {
66 var m: nx_int = a
67 if b > m { m = b }
68 if c > m { m = c }
69 return m
70}
71
72// Clamp v into [lo, hi].
73func nx_clamp(v: nx_int, lo: nx_int, hi: nx_int) -> nx_int {
74 if v < lo { return lo }
75 if v > hi { return hi }
76 return v
77}
78
79// Q10-specific clamps -- the most common case.
80func nx_clamp_q10(v: nx_int) -> nx_int {
81 if v < 0 { return 0 }
82 if v > NX_ESS_Q { return NX_ESS_Q }
83 return v
84}
85
86func nx_clamp_signed_q10(v: nx_int) -> nx_int {
87 if v < -NX_ESS_Q { return -NX_ESS_Q }
88 if v > NX_ESS_Q { return NX_ESS_Q }
89 return v
90}
91
92// ===== Family 2: sign / abs / swap ===================================
93
94func nx_abs(v: nx_int) -> nx_int {
95 if v < 0 { return -v }
96 return v
97}
98
99// -1 / 0 / +1.
100func nx_sign(v: nx_int) -> nx_int {
101 if v > 0 { return 1 }
102 if v < 0 { return -1 }
103 return 0
104}
105
106// In-place swap via pointer. Substrate doesn't have references so
107// caller passes pointers to the two cells.
108func nx_swap_via_ptr(a: *nx_int, b: *nx_int) -> nx_int {
109 let tmp: nx_int = a[0]
110 a[0] = b[0]
111 b[0] = tmp
112 return 0
113}
114
115// ===== Family 3: saturating arithmetic ===============================
116//
117// Returns I64_MAX on positive overflow, I64_MIN on negative overflow.
118// Useful for substrate code that must never wrap around (Q10 math,
119// counters, sentinels).
120
121func nx_sat_add(a: nx_int, b: nx_int) -> nx_int {
122 if b > 0 {
123 if a > NX_ESS_I64_MAX - b { return NX_ESS_I64_MAX }
124 }
125 if b < 0 {
126 if a < NX_ESS_I64_MIN - b { return NX_ESS_I64_MIN }
127 }
128 return a + b
129}
130
131func nx_sat_sub(a: nx_int, b: nx_int) -> nx_int {
132 if b < 0 {
133 if a > NX_ESS_I64_MAX + b { return NX_ESS_I64_MAX }
134 }
135 if b > 0 {
136 if a < NX_ESS_I64_MIN + b { return NX_ESS_I64_MIN }
137 }
138 return a - b
139}
140
141func nx_sat_mul(a: nx_int, b: nx_int) -> nx_int {
142 if a == 0 { return 0 }
143 if b == 0 { return 0 }
144 let absA: nx_int = nx_abs(a)
145 let absB: nx_int = nx_abs(b)
146 // Check overflow: |a|*|b| > I64_MAX iff |a| > I64_MAX / |b|.
147 if absA > NX_ESS_I64_MAX / absB {
148 // Sign-preserving saturation.
149 if nx_sign(a) == nx_sign(b) { return NX_ESS_I64_MAX }
150 return NX_ESS_I64_MIN
151 }
152 return a * b
153}
154
155// ===== Family 4: Q10 fixed-point helpers =============================
156
157// a * b in Q10: (a * b) / Q with overflow-aware multiply.
158func nx_q10_mul(a: nx_int, b: nx_int) -> nx_int {
159 return (a * b) / NX_ESS_Q
160}
161
162// a / b in Q10: (a * Q) / b.
163func nx_q10_div(a: nx_int, b: nx_int) -> nx_int {
164 if b == 0 { return 0 }
165 return (a * NX_ESS_Q) / b
166}
167
168// Linear interpolation in Q10: a + (b - a) * t / Q.
169func nx_lerp_q10(a: nx_int, b: nx_int, t_q10: nx_int) -> nx_int {
170 return a + ((b - a) * t_q10) / NX_ESS_Q
171}
172
173// Convert raw integer 0..100 to Q10.
174func nx_pct_to_q10(pct: nx_int) -> nx_int {
175 if pct <= 0 { return 0 }
176 if pct >= 100 { return NX_ESS_Q }
177 return (pct * NX_ESS_Q) / 100
178}
179
180// Convert Q10 to percent (rounds toward zero).
181func nx_q10_to_pct(q10: nx_int) -> nx_int {
182 return (q10 * 100) / NX_ESS_Q
183}
184
185// ===== Family 5: integer number theory ===============================
186
187// Euclidean GCD; handles negative inputs by abs-ing them first.
188func nx_gcd(a: nx_int, b: nx_int) -> nx_int {
189 var x: nx_int = nx_abs(a)
190 var y: nx_int = nx_abs(b)
191 while y != 0 {
192 let r: nx_int = x - (x / y) * y
193 x = y
194 y = r
195 }
196 return x
197}
198
199// LCM via standard identity.
200func nx_lcm(a: nx_int, b: nx_int) -> nx_int {
201 if a == 0 { return 0 }
202 if b == 0 { return 0 }
203 let g: nx_int = nx_gcd(a, b)
204 return nx_abs(a / g * b)
205}
206
207// Integer power: base^exp. exp must be >= 0; exp == 0 returns 1.
208func nx_pow_int(base: nx_int, exp: nx_int) -> nx_int {
209 if exp < 0 { return 0 }
210 if exp == 0 { return 1 }
211 var result: nx_int = 1
212 var b: nx_int = base
213 var e: nx_int = exp
214 while e > 0 {
215 let bit: nx_int = e - (e / 2) * 2
216 if bit == 1 { result = result * b }
217 b = b * b
218 e = e / 2
219 }
220 return result
221}
222
223// Modulo with FLOOR semantics (handles negative dividend correctly).
224// nx_mod_floor(-5, 3) = 1 (not -2 like trunc-toward-zero)
225func nx_mod_floor(a: nx_int, m: nx_int) -> nx_int {
226 if m == 0 { return 0 }
227 let r: nx_int = a - (a / m) * m
228 if r < 0 {
229 if m > 0 { return r + m }
230 return r - m
231 }
232 return r
233}
234
235// ===== Family 6: bit-level ============================================
236
237// Hamming weight: count of set bits in a 64-bit word.
238// Branchless via SWAR (parallel summation tree). Uses standard
239// magic constants (Knuth/Warren).
240// Delegated to nx_bits_popcount64 for substrate-wide consolidation.
241// The intrinsic dispatch gives a measured 2x speedup over this SWAR
242// pattern. Original division-based phrasing preserved in the test
243// oracle for cross-target equivalence.
244func nx_popcount(x: nx_int) -> nx_int {
245 return nx_bits_popcount64(x)
246}
247
248// Delegated to nx_bits_clz64 (composes two intrinsic clz32 calls;
249// ~3 ops vs the legacy O(64) division loop). clz(0) = 64.
250func nx_clz(x: nx_int) -> nx_int {
251 return nx_bits_clz64(x)
252}
253
254// Delegated to nx_bits_ctz64 (composes two intrinsic ctz32 calls).
255// ctz(0) = 64.
256func nx_ctz(x: nx_int) -> nx_int {
257 return nx_bits_ctz64(x)
258}
259
260// floor(log2(n)) for n > 0; -1 if n <= 0.
261func nx_log2_floor(n: nx_int) -> nx_int {
262 if n <= 0 { return -1 }
263 var k: nx_int = 0
264 var v: nx_int = n
265 while v > 1 {
266 v = v / 2
267 k = k + 1
268 }
269 return k
270}
271
272// ceil(log2(n)) for n > 0; -1 if n <= 0.
273func nx_log2_ceil(n: nx_int) -> nx_int {
274 if n <= 0 { return -1 }
275 if n == 1 { return 0 }
276 let f: nx_int = nx_log2_floor(n)
277 let pow: nx_int = nx_pow_int(2, f)
278 if pow == n { return f }
279 return f + 1
280}
281
282// Test bit at position p (0-indexed from LSB). Returns 1 if set, 0 if clear.
283func nx_bit_test(x: nx_int, p: nx_int) -> nx_int {
284 if p < 0 { return 0 }
285 if p >= 64 { return 0 }
286 let mask: nx_int = nx_pow_int(2, p)
287 let v: nx_int = (x / mask) - ((x / mask) / 2) * 2
288 return v
289}
290
291// Set bit at position p; returns the modified word.
292func nx_bit_set(x: nx_int, p: nx_int) -> nx_int {
293 if p < 0 { return x }
294 if p >= 64 { return x }
295 let mask: nx_int = nx_pow_int(2, p)
296 return x | mask
297}
298
299// Clear bit at position p; returns the modified word.
300func nx_bit_clear(x: nx_int, p: nx_int) -> nx_int {
301 if p < 0 { return x }
302 if p >= 64 { return x }
303 let mask: nx_int = nx_pow_int(2, p)
304 return x & (~mask)
305}
306
307// ===== Family 7: byte / char essentials ===============================
308
309// memcpy: copy n bytes from src to dst.
310func nx_memcpy(dst: *u8, src: *u8, n: nx_int) -> nx_int {
311 var i: nx_int = 0
312 while i < n {
313 dst[i] = src[i]
314 i = i + 1
315 }
316 return 0
317}
318
319// memset: fill n bytes of buf with value v.
320func nx_memset(buf: *u8, v: u8, n: nx_int) -> nx_int {
321 var i: nx_int = 0
322 while i < n {
323 buf[i] = v
324 i = i + 1
325 }
326 return 0
327}
328
329// memcmp: returns 0 if equal, -1 if a < b lex order, +1 if a > b.
330func nx_memcmp(a: *u8, b: *u8, n: nx_int) -> nx_int {
331 var i: nx_int = 0
332 while i < n {
333 if a[i] != b[i] {
334 if a[i] < b[i] { return -1 }
335 return 1
336 }
337 i = i + 1
338 }
339 return 0
340}
341
342// Character classification (ASCII).
343func nx_char_is_digit(c: u8) -> nx_int {
344 if c >= 48 {
345 if c <= 57 { return 1 }
346 }
347 return 0
348}
349
350func nx_char_is_alpha(c: u8) -> nx_int {
351 if c >= 65 {
352 if c <= 90 { return 1 }
353 }
354 if c >= 97 {
355 if c <= 122 { return 1 }
356 }
357 return 0
358}
359
360func nx_char_is_alnum(c: u8) -> nx_int {
361 if nx_char_is_digit(c) == 1 { return 1 }
362 if nx_char_is_alpha(c) == 1 { return 1 }
363 return 0
364}
365
366func nx_char_is_whitespace(c: u8) -> nx_int {
367 if c == 32 { return 1 } // space
368 if c == 9 { return 1 } // tab
369 if c == 10 { return 1 } // newline
370 if c == 13 { return 1 } // carriage return
371 if c == 11 { return 1 } // vertical tab
372 if c == 12 { return 1 } // form feed
373 return 0
374}
375
376// to_lower / to_upper (ASCII only).
377func nx_to_lower(c: u8) -> u8 {
378 if c >= 65 {
379 if c <= 90 { return c + 32 }
380 }
381 return c
382}
383
384func nx_to_upper(c: u8) -> u8 {
385 if c >= 97 {
386 if c <= 122 { return c - 32 }
387 }
388 return c
389}
390
391// Hex-nibble helpers (commonly needed for serialization).
392func nx_hex_to_nibble(c: u8) -> nx_int {
393 if c >= 48 {
394 if c <= 57 { return c - 48 }
395 }
396 if c >= 97 {
397 if c <= 102 { return c - 87 }
398 }
399 if c >= 65 {
400 if c <= 70 { return c - 55 }
401 }
402 return -1
403}
404
405func nx_nibble_to_hex(n: nx_int) -> u8 {
406 if n < 10 { return 48 + n }
407 return 87 + n // 'a' + (n - 10)
408}