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}