code wiki / _hdl_build / nx_lang_lint_ext.nx
nx_lang_lint_ext.nx source
↩ module page · 317 lines · 13242 B
1// nx_lang_lint_ext.nx -- TWO landmine classes nx_lang_lint does not yet see, both paid for in build cycles
2// on 2026-07-25/26. This EXTENDS the existing linter's vocabulary; it does not replace it, and it
3// deliberately re-implements none of L1-L4.
4//
5// L5 EMPTYESC -- an EMPTY LITERAL IN DISGUISE. L2 catches a literal "" by looking for two adjacent quote
6// bytes. It does not catch "\x00", which is four source characters and still a zero-length string, so it
7// hits the SAME nx_cc defect (a zero-length literal returns a pointer to the NEXT literal in the pool).
8// That is precisely the shape that segfaulted a gate: a sentinel spelled "\x00" silently became some
9// other string. A detector that catches only the obvious spelling of a defect leaves the subtle spelling
10// free to keep landing.
11//
12// L6 UNDEFCONST -- an ALL-CAPS identifier that is USED but never DECLARED anywhere in the file or in what
13// it imports. nx_cc compiles these silently (seq1012, sev 9) and resolves them to whatever is to hand:
14// first a wild pointer, then -- after unrelated code moved the frame -- the caller's output buffer, so
15// the program ran and wrote WRONG DATA. A typo that does not fail the build is worse than one that does.
16//
17// HONEST SCOPE, stated rather than implied: L6 covers CONSTANT-STYLE names only ([A-Z][A-Z0-9_]+). A
18// mistyped lowercase function or variable is NOT covered yet. That is a real limit, and it is written here
19// so nobody reads a clean run as more assurance than it is. Constant-style is where the failure actually
20// happened, and it is the subset that can be detected without resolving the whole language.
21//
22// Declarations are gathered from the target AND its transitive imports, because an identifier arriving from
23// an import is not undefined -- a checker that cannot see through imports reports noise, and a checker that
24// reports noise gets switched off.
25// license_tier: ORIGINAL expect_exit: 0
26import "nx_syscalls.nx"
27
28const LX_SRC: i64 = 2097152
29const LX_MAXDECL: i64 = 4096
30const LX_MAXIMP: i64 = 24
31const LX_NAME: i64 = 64
32const LX_ROOTS: i64 = 3
33const LX_PATH: i64 = 512
34
35func lx_w(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
36func lx_n(v: i64) -> i64 {
37 let bb: *u8=sys_mmap(28); var m: i64=v
38 if m<0 { m=0-m; sys_write(1,"-" as *u8,1) }
39 let t: *u8=sys_mmap(28); var k: i64=0
40 if m==0 { t[0]=48 as u8; k=1 }
41 while m>0 { t[k]=(48+(m%10)) as u8; m=m/10; k=k+1 }
42 var i: i64=0
43 while i<k { bb[i]=t[k-1-i]; i=i+1 }
44 sys_write(1,bb,k); return 0
45}
46func lx_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
47func lx_eq(a: *u8, b: *u8) -> i64 {
48 var i: i64=0
49 while 1==1 {
50 if a[i]!=b[i] { return 0 }
51 if a[i]==(0 as u8) { return 1 }
52 i=i+1
53 }
54 return 0
55}
56func lx_root(i: i64) -> *u8 {
57 if i==0 { return "./runtime/_hdl_build/" as *u8 }
58 if i==1 { return "./runtime/" as *u8 }
59 return "./" as *u8
60}
61func lx_read(path: *u8, buf: *u8, cap: i64) -> i64 {
62 let fd: i64 = sys_openat_rd(path)
63 if fd<0 { return 0 }
64 var total: i64=0
65 while total<cap {
66 let r: i64 = sys_read(fd, (((buf as i64)+total) as *u8), cap-total)
67 if r<=0 { break }
68 total=total+r
69 }
70 sys_close(fd)
71 return total
72}
73// read a bare filename from whichever known source root holds it
74func lx_read_any(name: *u8, buf: *u8, cap: i64, scratch: *u8) -> i64 {
75 var r: i64=0
76 while r<LX_ROOTS {
77 var o: i64=0
78 let pre: *u8 = lx_root(r)
79 var k: i64=0
80 while pre[k]!=(0 as u8) { scratch[o]=pre[k]; o=o+1; k=k+1 }
81 k=0
82 while name[k]!=(0 as u8) { scratch[o]=name[k]; o=o+1; k=k+1 }
83 scratch[o]=0 as u8
84 let got: i64 = lx_read(scratch, buf, cap)
85 if got>0 { return got }
86 r=r+1
87 }
88 return 0
89}
90func lx_is_upper(c: i64) -> i64 { if c>=65 { if c<=90 { return 1 } } return 0 }
91func lx_is_idchar(c: i64) -> i64 {
92 if c>=65 { if c<=90 { return 1 } }
93 if c>=97 { if c<=122 { return 1 } }
94 if c>=48 { if c<=57 { return 1 } }
95 if c==95 { return 1 }
96 return 0
97}
98// does buf match `word` at position i, with a non-identifier char before it?
99func lx_kw_at(b: *u8, n: i64, i: i64, word: *u8) -> i64 {
100 let wl: i64 = lx_slen(word)
101 if i+wl > n { return 0 }
102 if i>0 { if lx_is_idchar(b[i-1] as i64)==1 { return 0 } }
103 var k: i64=0
104 while k<wl { if b[i+k]!=word[k] { return 0 } k=k+1 }
105 return 1
106}
107// record a declared name (deduped)
108func lx_add_decl(names: *u8, cnt: *i64, nm: *u8) -> i64 {
109 var j: i64=0
110 while j<cnt[0] {
111 if lx_eq((((names as i64)+j*LX_NAME) as *u8), nm)==1 { return 0 }
112 j=j+1
113 }
114 if cnt[0] >= LX_MAXDECL { return 0-1 }
115 let dst: *u8 = (((names as i64)+cnt[0]*LX_NAME) as *u8)
116 var k: i64=0
117 while nm[k]!=(0 as u8) { if k<LX_NAME-1 { dst[k]=nm[k] } k=k+1 }
118 if k>LX_NAME-1 { k=LX_NAME-1 }
119 dst[k]=0 as u8
120 cnt[0]=cnt[0]+1
121 return 1
122}
123// copy the identifier starting at i into out; returns its length
124func lx_take_ident(b: *u8, n: i64, i: i64, out: *u8) -> i64 {
125 var k: i64=0
126 while i+k<n {
127 if lx_is_idchar(b[i+k] as i64)==0 { break }
128 if k<LX_NAME-1 { out[k]=b[i+k] }
129 k=k+1
130 }
131 var t: i64=k
132 if t>LX_NAME-1 { t=LX_NAME-1 }
133 out[t]=0 as u8
134 return k
135}
136// harvest every declaration in one buffer: const/func/let/var names + func parameter names
137func lx_harvest(b: *u8, n: i64, names: *u8, cnt: *i64) -> i64 {
138 let nm: *u8 = sys_mmap(LX_NAME)
139 var i: i64=0
140 while i<n {
141 var adv: i64=0
142 if lx_kw_at(b,n,i,"const " as *u8)==1 { adv=6 }
143 if lx_kw_at(b,n,i,"func " as *u8)==1 { adv=5 }
144 if lx_kw_at(b,n,i,"let " as *u8)==1 { adv=4 }
145 if lx_kw_at(b,n,i,"var " as *u8)==1 { adv=4 }
146 if adv>0 {
147 var j: i64=i+adv
148 while j<n { if b[j]!=(32 as u8) { break } j=j+1 }
149 let l: i64 = lx_take_ident(b,n,j,nm)
150 if l>0 { lx_add_decl(names,cnt,nm) }
151 // a func header also DECLARES its parameters: walk to '(' and take each name before ':'
152 if adv==5 {
153 var p: i64=j+l
154 while p<n { if b[p]==(40 as u8) { break } if b[p]==(10 as u8) { break } p=p+1 }
155 if p<n { if b[p]==(40 as u8) {
156 var depth: i64=1
157 p=p+1
158 var want: i64=1
159 while p<n {
160 if depth<=0 { break }
161 let c: i64 = b[p] as i64
162 if c==40 { depth=depth+1 }
163 if c==41 { depth=depth-1 }
164 if c==44 { want=1 }
165 if want==1 { if lx_is_idchar(c)==1 {
166 let pl: i64 = lx_take_ident(b,n,p,nm)
167 if pl>0 { lx_add_decl(names,cnt,nm); p=p+pl-1 }
168 want=0
169 } }
170 p=p+1
171 }
172 } }
173 }
174 i=i+adv
175 } else { i=i+1 }
176 }
177 return cnt[0]
178}
179// pull each `import "name.nx"` into the import list
180func lx_imports(b: *u8, n: i64, imps: *u8, icnt: *i64) -> i64 {
181 var i: i64=0
182 while i<n {
183 if lx_kw_at(b,n,i,"import " as *u8)==1 {
184 var j: i64=i+7
185 while j<n { if b[j]==(34 as u8) { break } j=j+1 }
186 if j<n {
187 j=j+1
188 var k: i64=0
189 let dst: *u8 = (((imps as i64)+icnt[0]*LX_NAME) as *u8)
190 while j+k<n {
191 if b[j+k]==(34 as u8) { break }
192 if k<LX_NAME-1 { dst[k]=b[j+k] }
193 k=k+1
194 }
195 var t: i64=k
196 if t>LX_NAME-1 { t=LX_NAME-1 }
197 dst[t]=0 as u8
198 if icnt[0] < LX_MAXIMP-1 { icnt[0]=icnt[0]+1 }
199 }
200 i=i+7
201 } else { i=i+1 }
202 }
203 return icnt[0]
204}
205
206func main(argc: i64, argv: *i64) -> i64 {
207 if argc<2 { lx_w("usage: nx_lang_lint_ext <source.nx>\n" as *u8); sys_exit(2); return 2 }
208 let path: *u8 = argv[1] as *u8
209 lx_w("=== nx_lang_lint_ext -- L5 disguised-empty-literal + L6 undeclared constant ===\n" as *u8)
210
211 let b: *u8 = sys_mmap(LX_SRC)
212 let n: i64 = lx_read(path, b, LX_SRC-16)
213 if n<=0 { lx_w(" source unreadable\n" as *u8); sys_exit(2); return 2 }
214
215 let names: *u8 = sys_mmap(LX_NAME*LX_MAXDECL)
216 let cnt: *i64 = sys_mmap(16) as *i64
217 cnt[0]=0
218 lx_harvest(b, n, names, cnt)
219
220 // transitive-ish: the target's imports, and THEIR imports, one level deeper. Bounded and stated.
221 let imps: *u8 = sys_mmap(LX_NAME*LX_MAXIMP)
222 let icnt: *i64 = sys_mmap(16) as *i64
223 icnt[0]=0
224 lx_imports(b, n, imps, icnt)
225 let scratch: *u8 = sys_mmap(LX_PATH)
226 let ib: *u8 = sys_mmap(LX_SRC)
227 var resolved: i64=0
228 var q: i64=0
229 while q<icnt[0] {
230 let nm: *u8 = (((imps as i64)+q*LX_NAME) as *u8)
231 let got: i64 = lx_read_any(nm, ib, LX_SRC-16, scratch)
232 if got>0 {
233 resolved=resolved+1
234 lx_harvest(ib, got, names, cnt)
235 lx_imports(ib, got, imps, icnt) // grows the queue in place: one more level, still bounded
236 }
237 q=q+1
238 }
239
240 lx_w(" declarations in scope=" as *u8); lx_n(cnt[0])
241 lx_w(" imports seen=" as *u8); lx_n(icnt[0])
242 lx_w(" imports resolved=" as *u8); lx_n(resolved); lx_w("\n" as *u8)
243
244 // ---- scan the target for the two classes ----
245 let nm: *u8 = sys_mmap(LX_NAME)
246 var hits5: i64=0
247 var hits6: i64=0
248 var line: i64=1
249 var i: i64=0
250 var in_str: i64=0
251 var in_com: i64=0
252 while i<n {
253 let c: i64 = b[i] as i64
254 if c==10 { line=line+1; in_com=0 }
255 // COMMENTS ARE PROSE, NOT CODE. The first run flagged 35 "undeclared constants" that were words
256 // like MUTATES and WITH inside my own comments -- exactly the noise this file's header warns makes
257 // a checker get switched off. Skip from // to end of line.
258 if in_str==0 { if c==47 { if i+1<n { if b[i+1]==(47 as u8) { in_com=1 } } } }
259 // L5: a quote, then \x00, then a quote -> a zero-length literal wearing an escape
260 if c==34 { if in_com==0 {
261 if i+5<n {
262 if b[i+1]==(92 as u8) { if b[i+2]==(120 as u8) { if b[i+3]==(48 as u8) { if b[i+4]==(48 as u8) { if b[i+5]==(34 as u8) {
263 lx_w(" L5 EMPTYESC line=" as *u8); lx_n(line)
264 lx_w(" \x22\\x00\x22 is a ZERO-LENGTH literal in disguise -- same defect as \x22\x22 (seq907)\n" as *u8)
265 hits5=hits5+1
266 } } } } }
267 }
268 in_str = 1 - in_str
269 } }
270 // L6: an ALL-CAPS identifier used outside a string AND outside a comment
271 if in_str==0 { if in_com==0 {
272 if lx_is_upper(c)==1 {
273 var prev_ok: i64=1
274 if i>0 { if lx_is_idchar(b[i-1] as i64)==1 { prev_ok=0 } }
275 if prev_ok==1 {
276 let l: i64 = lx_take_ident(b,n,i,nm)
277 if l>=3 {
278 var all_const: i64=1
279 var k: i64=0
280 while k<l {
281 let cc: i64 = b[i+k] as i64
282 if lx_is_upper(cc)==0 { if cc!=95 { if cc<48 { all_const=0 } else { if cc>57 { all_const=0 } } } }
283 k=k+1
284 }
285 if all_const==1 {
286 var found: i64=0
287 var j: i64=0
288 while j<cnt[0] {
289 if lx_eq((((names as i64)+j*LX_NAME) as *u8), nm)==1 { found=1; j=cnt[0] } else { j=j+1 }
290 }
291 if found==0 {
292 lx_w(" L6 UNDEFCONST line=" as *u8); lx_n(line)
293 lx_w(" " as *u8); lx_w(nm)
294 lx_w(" is USED but DECLARED NOWHERE -- nx_cc will compile this silently (seq1012)\n" as *u8)
295 hits6=hits6+1
296 }
297 }
298 i=i+l-1
299 }
300 }
301 }
302 } }
303 i=i+1
304 }
305
306 lx_w("\n L5 disguised-empty-literal hits=" as *u8); lx_n(hits5)
307 lx_w(" L6 undeclared-constant hits=" as *u8); lx_n(hits6); lx_w("\n" as *u8)
308 lx_w(" SCOPE (stated, not implied): L6 sees CONSTANT-STYLE names only. A mistyped lowercase function\n" as *u8)
309 lx_w(" or variable is NOT covered -- a clean run here is not a clean bill of health.\n" as *u8)
310 if hits5+hits6 == 0 {
311 lx_w("VERDICT: verdict=GREEN (neither landmine class present)\n" as *u8)
312 sys_exit(0)
313 }
314 lx_w("VERDICT: LANDMINES\n" as *u8)
315 sys_exit(1)
316 return 1
317}