code wiki / (root) / nx_fnptr_arity_probe.nx

nx_fnptr_arity_probe.nx source

↩ module page · 92 lines · 5208 B

1// nx_fnptr_arity_probe.nx -- ARITY PROBE for struct-field function pointers. 2// 3// The struct fn-ptr postfix-call fix (2026-05-19) was smoke-proven at arity 0/1/2/3 only. An OOP 4// vtable with a real signature needs more: a describe method is (self, gray, rgb, w, h, out) = SIX 5// arguments, which is exactly where the System V AMD64 integer argument registers run out 6// (rdi, rsi, rdx, rcx, r8, r9). An INDIRECT call must also materialise the callee address in a 7// register, so if the code generator picks one of those six the highest argument is clobbered -- 8// a failure mode a direct call can never exhibit, and one that produces WRONG VALUES rather than a 9// crash or a compile error. This probe walks arity 3..8 through a struct field and prints the 10// arithmetic each callee performs on its arguments, so a clobbered slot is visible as a wrong sum 11// rather than inferred. 12// license_tier: ORIGINAL 13import "syscalls.nx" 14const PROBE_MAGIC_10000: i64 = 10000 15const PROBE_MAGIC_100000: i64 = 100000 16const PROBE_MAGIC_1241: i64 = 1241 17const PROBE_MAGIC_12352: i64 = 12352 18const PROBE_MAGIC_123463: i64 = 123463 19 20func p_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 21func p_num(v: i64) -> i64 { let bb: *u8 = sys_mmap(28); var m: i64 = v; if m < 0 { m = 0 - m; sys_write(1, "-" as *u8, 1) }; let t: *u8 = sys_mmap(28); var k: i64 = 0; if m == 0 { t[0] = (48 as u8); k = 1 }; while m > 0 { t[k] = ((48 + (m % 10)) as u8); m = m / 10; k = k + 1 }; var i: i64 = 0; while i < k { bb[i] = t[k - 1 - i]; i = i + 1 }; sys_write(1, bb, k); return 0 } 22 23struct Probe { 24 tag: i64, 25 f3: func(*Probe, i64, i64) -> i64, 26 f4: func(*Probe, i64, i64, i64) -> i64, 27 f5: func(*Probe, i64, i64, i64, i64) -> i64, 28 f6: func(*Probe, i64, i64, i64, i64, i64) -> i64, 29 f7: func(*Probe, i64, i64, i64, i64, i64, i64) -> i64, 30} 31const PROBE_BYTES: i64 = 48 32 33func imp3(p: *Probe, a: i64, b: i64) -> i64 { return p.tag + a * 10 + b } 34func imp4(p: *Probe, a: i64, b: i64, c: i64) -> i64 { return p.tag + a * 100 + b * 10 + c } 35func imp5(p: *Probe, a: i64, b: i64, c: i64, d: i64) -> i64 { return p.tag + a * 1000 + b * 100 + c * 10 + d } 36func imp6(p: *Probe, a: i64, b: i64, c: i64, d: i64, e: i64) -> i64 { return p.tag + a * PROBE_MAGIC_10000 + b * 1000 + c * 100 + d * 10 + e } 37func imp7(p: *Probe, a: i64, b: i64, c: i64, d: i64, e: i64, f: i64) -> i64 { return p.tag + a * PROBE_MAGIC_100000 + b * PROBE_MAGIC_10000 + c * 1000 + d * 100 + e * 10 + f } 38 39func main() -> i64 { 40 let p: *Probe = sys_mmap(PROBE_BYTES) as *Probe 41 p.tag = 7 42 p.f3 = imp3 43 p.f4 = imp4 44 p.f5 = imp5 45 p.f6 = imp6 46 p.f7 = imp7 47 48 var bad: i64 = 0 49 50 // arity 3 (self + 2): expect 7 + 10 + 2 = 19 51 let r3: i64 = p.f3(p, 1, 2) 52 p_puts("arity3 (self+2) got=" as *u8); p_num(r3); p_puts(" want=19 " as *u8) 53 if r3 == 19 { p_puts("OK\n" as *u8) } else { p_puts("WRONG\n" as *u8); bad = bad + 1 } 54 55 // arity 4 (self + 3): expect 7 + 100 + 20 + 3 = 130 56 let r4: i64 = p.f4(p, 1, 2, 3) 57 p_puts("arity4 (self+3) got=" as *u8); p_num(r4); p_puts(" want=130 " as *u8) 58 if r4 == 130 { p_puts("OK\n" as *u8) } else { p_puts("WRONG\n" as *u8); bad = bad + 1 } 59 60 // arity 5 (self + 4): expect 7 + 1000 + 200 + 30 + 4 = 1241 61 let r5: i64 = p.f5(p, 1, 2, 3, 4) 62 p_puts("arity5 (self+4) got=" as *u8); p_num(r5); p_puts(" want=1241 " as *u8) 63 if r5 == PROBE_MAGIC_1241 { p_puts("OK\n" as *u8) } else { p_puts("WRONG\n" as *u8); bad = bad + 1 } 64 65 // arity 6 (self + 5) -- the SysV boundary: expect 7 + 10000 + 2000 + 300 + 40 + 5 = 12352 66 let r6: i64 = p.f6(p, 1, 2, 3, 4, 5) 67 p_puts("arity6 (self+5) got=" as *u8); p_num(r6); p_puts(" want=12352 " as *u8) 68 if r6 == PROBE_MAGIC_12352 { p_puts("OK\n" as *u8) } else { p_puts("WRONG\n" as *u8); bad = bad + 1 } 69 70 // arity 7 (self + 6) -- first stack-passed argument: expect 7 + 100000 + 20000 + 3000 + 400 + 50 + 6 = 123463 71 let r7: i64 = p.f7(p, 1, 2, 3, 4, 5, 6) 72 p_puts("arity7 (self+6) got=" as *u8); p_num(r7); p_puts(" want=123463 " as *u8) 73 if r7 == PROBE_MAGIC_123463 { p_puts("OK\n" as *u8) } else { p_puts("WRONG\n" as *u8); bad = bad + 1 } 74 75 // control: the SAME callee invoked DIRECTLY, to separate "fn-ptr dispatch is broken" from 76 // "6-argument calls are broken in general". 77 let d6: i64 = imp6(p, 1, 2, 3, 4, 5) 78 p_puts("direct6 got=" as *u8); p_num(d6); p_puts(" want=12352 " as *u8) 79 if d6 == PROBE_MAGIC_12352 { p_puts("OK\n" as *u8) } else { p_puts("WRONG\n" as *u8); bad = bad + 1 } 80 81 // control: LET-COPY idiom (the documented fallback) at the same arity. 82 let fp: func(*Probe, i64, i64, i64, i64, i64) -> i64 = p.f6 83 let l6: i64 = fp(p, 1, 2, 3, 4, 5) 84 p_puts("letcopy6 got=" as *u8); p_num(l6); p_puts(" want=12352 " as *u8) 85 if l6 == PROBE_MAGIC_12352 { p_puts("OK\n" as *u8) } else { p_puts("WRONG\n" as *u8); bad = bad + 1 } 86 87 p_puts("FNPTR-ARITY-PROBE wrong=" as *u8); p_num(bad); p_puts("\n" as *u8) 88 if bad == 0 { p_puts("FNPTR-ARITY-PROBE verdict=GREEN\n" as *u8); sys_exit(0); return 0 } 89 p_puts("FNPTR-ARITY-PROBE verdict=RED\n" as *u8) 90 sys_exit(1) 91 return 1 92}