nx_stubmain_lib.nx source
↩ module page · 210 lines · 9169 B
1// nx_stubmain_lib.nx -- THE STUB-MAIN RULER. One predicate, so the writer and the reader of this
2// classification cannot drift apart.
3//
4// THE DEFECT IT NAMES, MEASURED 2026-09-03. buildroot/runtime/nx_https_get.nx ends with
5// `// Compile-only smoke.` and `func main() -> i64 { return 0 }`. Nothing is reachable from that main,
6// so the compiler DEAD-STRIPS the entire TLS closure: /api/build returns action=BUILT src_stable=1 with
7// no error and stages 14,880 bytes against a LIVE 468,929-byte binary. Any seat running the ordinary
8// build-then-promote loop replaces the estate's sovereign HTTPS client with a binary that returns 0 --
9// silently, off a GREEN receipt.
10//
11// ★THE SHARP PART, AND THE REASON THIS NEEDS ITS OWN RULER: nx_catalog ALREADY detects libraries, by
12// "no top-level main()" -- nx_https_fetch reads `KIND LIB (a binary is NOT expected for this one)` and
13// is therefore safe from this whole class. nx_https_get EVADES that detector PRECISELY BECAUSE IT HAS
14// A MAIN. A stub one. So the lib detector is defeated by the very convention that lets a library
15// compile standalone, and the catalogue then reports the name as LIVE because an .elf sits beside it.
16// THREE STATES, NEVER TWO -- collapsing them is the whole bug:
17// SM_NO_MAIN a library by construction. nx_catalog already handles it. NOT this ruler's subject.
18// SM_STUB_MAIN a main that returns a constant and calls nothing. Compiles, links nothing, ships a no-op.
19// SM_REAL_MAIN a program.
20//
21// WHAT IT DELIBERATELY DOES NOT DECIDE. Being SM_STUB_MAIN is NOT by itself a defect: it is a deliberate
22// estate convention ("compile-only smoke") and a census over 12,067 sources found 114 of them, of which
23// every one checked was correctly unpromoted. The ARMED condition is the CONJUNCTION -- stub main AND a
24// promoted binary somebody calls -- and the promoted half is nx_artifactdrift's to answer, because that
25// organ already walks the serving root and already resolves sources through PS_SRCROOT_A then
26// PS_SRCROOT_B in builder order. This lib exists so that adoption is one predicate call and NOT a second
27// tree walker: the estate has one drift census and must keep having one.
28//
29// NO SIZE THRESHOLD, ON PURPOSE. The tempting rule is "refuse a candidate much smaller than live", but
30// that is a magic number (rule 11) and it fails in both directions -- a legitimate toolchain shrink of
31// 304 permil is on record, and a stub of a SMALL program is not much smaller than the real thing. The
32// structural fact (main calls nothing) is the signal; size is at most a triage hint.
33//
34// license_tier: ORIGINAL
35import "nx_syscalls.nx"
36
37const SM_NO_MAIN: i64 = 0
38const SM_STUB_MAIN: i64 = 1
39const SM_REAL_MAIN: i64 = 2
40const SM_UNREADABLE: i64 = 0 - 1
41
42const SM_NL: i64 = 10
43const SM_CR: i64 = 13
44const SM_SP: i64 = 32
45const SM_TAB: i64 = 9
46const SM_SLASH: i64 = 47
47const SM_LBRACE: i64 = 123
48const SM_RBRACE: i64 = 125
49const SM_MINUS: i64 = 45
50const SM_D0: i64 = 48
51const SM_D9: i64 = 57
52
53func sm_is_space(c: i64) -> i64 {
54 if c == SM_SP { return 1 }
55 if c == SM_TAB { return 1 }
56 return 0
57}
58
59func sm_streq_at(buf: *u8, s: i64, e: i64, lit: *u8) -> i64 {
60 var k: i64 = 0
61 while lit[k] != (0 as u8) {
62 if s + k >= e { return 0 }
63 if buf[s + k] != lit[k] { return 0 }
64 k = k + 1
65 }
66 return 1
67}
68
69func sm_find(buf: *u8, s: i64, e: i64, lit: *u8) -> i64 {
70 var ll: i64 = 0
71 while lit[ll] != (0 as u8) { ll = ll + 1 }
72 if ll == 0 { return 0 - 1 }
73 var i: i64 = s
74 while i + ll <= e {
75 if sm_streq_at(buf, i, e, lit) == 1 { return i }
76 i = i + 1
77 }
78 return 0 - 1
79}
80
81// Find `lit` ONLY where it begins a LINE (index 0, or immediately after a newline). Returns its start
82// or -1.
83//
84// ⚠THIS EXISTS BECAUSE THE FIRST DRAFT DID NOT HAVE IT AND THIS GATE'S LIVE CONTROLS CAUGHT IT.
85// A plain substring search matched `func main` inside a COMMENT -- including the comment in THIS FILE's
86// own header, which quotes `func main() -> i64 { return 0 }` while explaining the defect. The ruler then
87// classified itself, and nx_https_fetch, as STUB when both are libraries with no main at all. That is
88// the estate's recorded law arriving in person: A RULE THAT CANNOT TELL CODE FROM THE COMMENT DESCRIBING
89// IT WILL FLAG EVERY EXPLANATION OF THE BUG IT HUNTS. Line-anchoring is sufficient and cheap here
90// because NishiLang top-level declarations begin at column 0 and a comment line begins with '//', so no
91// comment can present a line-anchored `func main`.
92func sm_find_line_start(buf: *u8, n: i64, lit: *u8) -> i64 {
93 var i: i64 = 0
94 while i < n {
95 var at_line_start: i64 = 0
96 if i == 0 { at_line_start = 1 } else {
97 if ((buf[i - 1] as i64) & 255) == SM_NL { at_line_start = 1 }
98 }
99 if at_line_start == 1 {
100 if sm_streq_at(buf, i, n, lit) == 1 { return i }
101 }
102 i = i + 1
103 }
104 return 0 - 1
105}
106
107// Index just past the first '{' at or after s, or -1.
108func sm_open_brace(buf: *u8, s: i64, e: i64) -> i64 {
109 var i: i64 = s
110 while i < e {
111 if ((buf[i] as i64) & 255) == SM_LBRACE { return i + 1 }
112 i = i + 1
113 }
114 return 0 - 1
115}
116
117// Matching '}' for a body that starts at s (depth already 1). Returns its index, or -1.
118// Brace-counted rather than "first } at column 0", because a nested block would end the body early
119// and make a REAL main read as a stub -- a false ARMED is worse than none, it teaches distrust.
120func sm_close_brace(buf: *u8, s: i64, e: i64) -> i64 {
121 var depth: i64 = 1
122 var i: i64 = s
123 while i < e {
124 let c: i64 = (buf[i] as i64) & 255
125 if c == SM_LBRACE { depth = depth + 1 }
126 if c == SM_RBRACE {
127 depth = depth - 1
128 if depth == 0 { return i }
129 }
130 i = i + 1
131 }
132 return 0 - 1
133}
134
135// Classify the body [s,e) of a main. A stub body is a SINGLE statement `return <integer>` once
136// comments and blank lines are removed. Anything else -- any call, any second statement -- is real.
137func sm_body_is_stub(buf: *u8, s: i64, e: i64) -> i64 {
138 var i: i64 = s
139 var saw_return: i64 = 0
140 var saw_digit: i64 = 0
141 var other: i64 = 0
142 while i < e {
143 let c: i64 = (buf[i] as i64) & 255
144 if sm_is_space(c) == 1 { i = i + 1 } else {
145 if c == SM_NL { i = i + 1 } else {
146 if c == SM_CR { i = i + 1 } else {
147 // a comment runs to end of line and is not a statement
148 if c == SM_SLASH {
149 if i + 1 < e {
150 if ((buf[i + 1] as i64) & 255) == SM_SLASH {
151 var j: i64 = i
152 while j < e {
153 if ((buf[j] as i64) & 255) == SM_NL { i = j + 1; j = e } else { j = j + 1 }
154 }
155 if i <= s { i = e }
156 } else { other = other + 1; i = i + 1 }
157 } else { other = other + 1; i = i + 1 }
158 } else {
159 if sm_streq_at(buf, i, e, "return" as *u8) == 1 {
160 saw_return = saw_return + 1
161 i = i + 6
162 } else {
163 if c == SM_MINUS { i = i + 1 } else {
164 if c >= SM_D0 {
165 if c <= SM_D9 { saw_digit = 1; i = i + 1 } else { other = other + 1; i = i + 1 }
166 } else { other = other + 1; i = i + 1 }
167 }
168 }
169 }
170 }
171 }
172 }
173 }
174 if other > 0 { return 0 }
175 if saw_return != 1 { return 0 }
176 if saw_digit == 0 { return 0 }
177 return 1
178}
179
180// THE RULER. Classify a source buffer. Three states, never two.
181func sm_classify(buf: *u8, n: i64) -> i64 {
182 if n <= 0 { return SM_UNREADABLE }
183 let at: i64 = sm_find_line_start(buf, n, "func main" as *u8)
184 if at < 0 { return SM_NO_MAIN }
185 let bs: i64 = sm_open_brace(buf, at, n)
186 if bs < 0 { return SM_NO_MAIN }
187 let be: i64 = sm_close_brace(buf, bs, n)
188 if be < 0 { return SM_UNREADABLE }
189 if sm_body_is_stub(buf, bs, be) == 1 { return SM_STUB_MAIN }
190 return SM_REAL_MAIN
191}
192
193// Convenience: classify a source FILE. Composes sys_read_file, which sizes from the file and cannot
194// short-read -- a capped reader here would classify a truncated head and call a real main a stub.
195func sm_classify_path(path: *u8) -> i64 {
196 let lp: *i64 = sys_mmap(8) as *i64
197 lp[0] = 0
198 let buf: *u8 = sys_read_file(path, lp)
199 if (buf as i64) == 0 { return SM_UNREADABLE }
200 return sm_classify(buf, lp[0])
201}
202
203// Is this source ARMED given that a promoted binary exists for its name? The promoted half is the
204// CALLER's to establish (nx_artifactdrift already knows it); this keeps the two halves in one place so
205// no consumer invents its own conjunction.
206func sm_is_armed(cls: i64, promoted_exists: i64) -> i64 {
207 if cls != SM_STUB_MAIN { return 0 }
208 if promoted_exists != 1 { return 0 }
209 return 1
210}