code wiki / (root) / nx_jsscope.nx

nx_jsscope.nx source

↩ module page · 445 lines · 19095 B

1// nx_jsscope.nx -- STATIC SCOPE RESOLUTION over nx_js_parse's AST: does every identifier a script 2// REFERENCES have a declaration in a scope that ENCLOSES the reference? 3// 4// WHY THIS EXISTS. 2026-08-25 every /world page with a cast went black on the WebGL tier and reverted 5// to the CPU renderer, and the reason -- once the reload was made to carry it -- was 6// `ReferenceError: nxHairTick is not defined`: a function DECLARED inside loadNPC, CALLED from 7// drawNPCs at script scope. The page LEXED (nx_jsbalance passed it), the wasm PAINTED, every gate was 8// green. Nothing in the estate asks the one question a browser asks first: can this name be seen from 9// here? This organ asks it, statically, over the incumbent parser -- one AST, no second front-end. 10// 11// THE RULE, STATED ONCE. A reference resolves if its name is declared in the SAME function scope or 12// any ENCLOSING one (function declarations and `var` hoist to their function; params, class names, 13// catch params and for-targets bind where they appear). A name declared ONLY in a function that does 14// NOT enclose the reference is the exact defect above and is reported as OUT-OF-SCOPE, with the 15// declaring scope named -- because "undefined" sends a reader to grep for a typo, while "declared in 16// loadNPC" sends them to the real fix. A name declared nowhere is UNRESOLVED and is checked against a 17// globals CONF (knowledge/js_globals.conf, one name per line): browser and language globals are DATA 18// the page may lean on, never a list compiled into this organ. 19// 20// ⚠ DECLARED IMPRECISION, because a checker whose blind spots are unstated gets trusted as exact. 21// 1. `let`/`const` are BLOCK-scoped in JS; this pass binds them to their FUNCTION. A let in one block 22// referenced from a sibling block would be a real ReferenceError this organ does NOT see. That is 23// the conservative direction -- it misses an error class, it never invents one. 24// 2. Template-literal interiors (`${expr}`) are not parsed by nx_js_parse (the token is opaque), so 25// references inside them are invisible here. 26// 3. `with` statements and `eval` are not modelled; a page that used them could not be judged. 27// 4. Property names (a.b), object keys, labels and destructuring keys are NOT references and are 28// never counted, by construction of the walk. 29// license_tier: ORIGINAL No hw writes (Rule 26). 30import "nx_syscalls.nx" 31import "nx_js_lex.nx" 32import "nx_js_parse.nx" 33 34// ---- scope table ---------------------------------------------------------------------------------- 35// One row per FUNCTION scope: the node index of its FUNC_DECL (or -1 for the program), its parent scope 36// row, and a range into the declaration table. Declarations are (scope, tokidx) pairs: the token IS the 37// name, compared by bytes against the source, so no strings are ever copied. 38const SC_MAX: i64 = 4096 // function scopes a page may hold -- a REFUSAL at the cap, never a silent stop 39const DC_MAX: i64 = 65536 // declarations across all scopes 40const SC_ROW: i64 = 4 // node, parent, decl_start, decl_count 41const SC_NODE: i64 = 0 42const SC_PARENT: i64 = 1 43const SC_DSTART: i64 = 2 44const SC_DCOUNT: i64 = 3 45 46// report rows: kind, tokidx-of-reference, scope-of-reference, declaring-scope (-1 if none) 47const RP_MAX: i64 = 4096 48const RP_ROW: i64 = 4 49const RP_UNRESOLVED: i64 = 1 50const RP_OUTOFSCOPE: i64 = 2 51// a const/let/class declared twice in ONE scope is a SyntaxError that kills the whole script 52// at EVAL -- the class that blacked /world five times (NXHCH_G) while lex and scope both passed. 53const RP_REDECL: i64 = 3 54 55// state block handed through the walk 56const JS_ST_CTX: i64 = 0 57const JS_ST_SC: i64 = 1 // scope rows 58const JS_ST_NSC: i64 = 2 59const JS_ST_DC: i64 = 3 // decl pairs 60const JS_ST_NDC: i64 = 4 61const JS_ST_RP: i64 = 5 // report rows 62const JS_ST_NRP: i64 = 6 63const JS_ST_NREF: i64 = 7 // references examined -- the coverage figure 64const JS_ST_OVER: i64 = 8 // 1 if any table hit its cap (the verdict is then a BOUND) 65const JS_ST_GLOB: i64 = 9 // globals conf bytes 66const JS_ST_GLOBN: i64 = 10 67const JS_ST_N: i64 = 12 68 69func jss_tokidx(ctx: *i64, idx: i64) -> i64 { let n: *i64 = jp_nodes(ctx); return n[idx * NODE_SLOTS + 4] } 70func jss_src(ctx: *i64) -> *u8 { return (ctx[CTX_SRC]) as *u8 } 71func jss_tok_start(ctx: *i64, tokidx: i64) -> i64 { let t: *i64 = jp_toks(ctx); return t[tokidx*3 + 1] } 72func jss_tok_len(ctx: *i64, tokidx: i64) -> i64 { let t: *i64 = jp_toks(ctx); return t[tokidx*3 + 2] } 73 74// Compare two identifier tokens by their source bytes. 75func jss_tok_eq(ctx: *i64, ta: i64, tb: i64) -> i64 { 76 let la: i64 = jss_tok_len(ctx, ta) 77 if la != jss_tok_len(ctx, tb) { return 0 } 78 let s: *u8 = jss_src(ctx) 79 let a: i64 = jss_tok_start(ctx, ta) 80 let b: i64 = jss_tok_start(ctx, tb) 81 var i: i64 = 0 82 while i < la { 83 if (s[a+i] & 255) != (s[b+i] & 255) { return 0 } 84 i = i + 1 85 } 86 return 1 87} 88 89// Is `tokidx` declared in scope `s` (that scope only)? 90func jss_declared_in(st: *i64, s: i64, tokidx: i64) -> i64 { 91 let ctx: *i64 = (st[JS_ST_CTX]) as *i64 92 let dc: *i64 = (st[JS_ST_DC]) as *i64 93 let n: i64 = st[JS_ST_NDC] 94 var i: i64 = 0 95 while i < n { 96 if dc[i*2] == s { if jss_tok_eq(ctx, dc[i*2+1], tokidx) == 1 { return 1 } } 97 i = i + 1 98 } 99 return 0 100} 101 102func jss_report(st: *i64, kind: i64, tokidx: i64, scope: i64, declscope: i64) -> i64 { 103 let n: i64 = st[JS_ST_NRP] 104 if n >= RP_MAX { st[JS_ST_OVER] = 1 105 return 0 } 106 let rp: *i64 = (st[JS_ST_RP]) as *i64 107 rp[n*RP_ROW] = kind 108 rp[n*RP_ROW+1] = tokidx 109 rp[n*RP_ROW+2] = scope 110 rp[n*RP_ROW+3] = declscope 111 st[JS_ST_NRP] = n + 1 112 return 0 113} 114 115func jss_declare(st: *i64, scope: i64, tokidx: i64) -> i64 { 116 if tokidx < 0 { return 0 } 117 let n: i64 = st[JS_ST_NDC] 118 if n >= DC_MAX { st[JS_ST_OVER] = 1 119 return 0 } 120 // SCOPE 0 ONLY, BY MEASUREMENT (2026-08-26): kind-blind detection over ALL scopes flagged 129 121 // rows on the clean page -- every one a loop counter or catch param (i/v/e/loop) in a FUNCTION 122 // scope, i.e. the block-scoping this v1 does not model; and ZERO at script scope, where the 123 // NXHCH_G eval-killer class lives. Top-level-only detection is therefore armable with zero 124 // false positives on the real page; block-scope modeling is the NAMED follow-up that would 125 // extend coverage inward. 126 if scope == 0 { if jss_declared_in(st, scope, tokidx) == 1 { jss_report(st, RP_REDECL, tokidx, scope, scope) } } 127 let dc: *i64 = (st[JS_ST_DC]) as *i64 128 dc[n*2] = scope 129 dc[n*2+1] = tokidx 130 st[JS_ST_NDC] = n + 1 131 let sc: *i64 = (st[JS_ST_SC]) as *i64 132 sc[scope*SC_ROW + SC_DCOUNT] = sc[scope*SC_ROW + SC_DCOUNT] + 1 133 return 0 134} 135 136func jss_new_scope(st: *i64, node: i64, parent: i64) -> i64 { 137 let n: i64 = st[JS_ST_NSC] 138 if n >= SC_MAX { st[JS_ST_OVER] = 1 139 return parent } 140 let sc: *i64 = (st[JS_ST_SC]) as *i64 141 sc[n*SC_ROW + SC_NODE] = node 142 sc[n*SC_ROW + SC_PARENT] = parent 143 sc[n*SC_ROW + SC_DSTART] = 0 144 sc[n*SC_ROW + SC_DCOUNT] = 0 145 st[JS_ST_NSC] = n + 1 146 return n 147} 148 149 150// Walk the scope chain from `s` outward. Returns the scope that declares it, or -1. 151func jss_resolve(st: *i64, s: i64, tokidx: i64) -> i64 { 152 let sc: *i64 = (st[JS_ST_SC]) as *i64 153 var cur: i64 = s 154 while cur >= 0 { 155 if jss_declared_in(st, cur, tokidx) == 1 { return cur } 156 cur = sc[cur*SC_ROW + SC_PARENT] 157 } 158 return 0 - 1 159} 160 161// Any scope at all that declares it -- the OUT-OF-SCOPE discriminator. 162func jss_declared_anywhere(st: *i64, tokidx: i64) -> i64 { 163 let n: i64 = st[JS_ST_NSC] 164 var s: i64 = 0 165 while s < n { 166 if jss_declared_in(st, s, tokidx) == 1 { return s } 167 s = s + 1 168 } 169 return 0 - 1 170} 171 172// Is this identifier token's name present in the globals conf? Line-anchored byte compare. 173func jss_is_global(st: *i64, tokidx: i64) -> i64 { 174 let g: *u8 = (st[JS_ST_GLOB]) as *u8 175 let gn: i64 = st[JS_ST_GLOBN] 176 if gn <= 0 { return 0 } 177 let ctx: *i64 = (st[JS_ST_CTX]) as *i64 178 let s: *u8 = jss_src(ctx) 179 let a: i64 = jss_tok_start(ctx, tokidx) 180 let la: i64 = jss_tok_len(ctx, tokidx) 181 var p: i64 = 0 182 while p < gn { 183 // line start 184 var q: i64 = p 185 while q < gn && (g[q] & 255) != 10 { q = q + 1 } 186 let ll: i64 = q - p 187 if ll == la && (g[p] & 255) != 35 { 188 var i: i64 = 0 189 var eq: i64 = 1 190 while i < la { if (g[p+i] & 255) != (s[a+i] & 255) { eq = 0 191 i = la } 192 i = i + 1 } 193 if eq == 1 { return 1 } 194 } 195 p = q + 1 196 } 197 return 0 198} 199 200 201// ---- PASS 1: declarations of one function scope (does NOT enter nested functions) ---------------- 202func jss_is_list(k: i64) -> i64 { 203 if k == ND_PROGRAM { return 1 } 204 if k == ND_BLOCK { return 1 } 205 if k == ND_PARAMS { return 1 } 206 if k == ND_OBJECT { return 1 } 207 if k == ND_ARRAY { return 1 } 208 if k == ND_VAR_LIST { return 1 } 209 if k == ND_SEQ { return 1 } 210 if k == ND_ARRAY_PAT { return 1 } 211 if k == ND_OBJ_PAT { return 1 } 212 if k == ND_SWITCH { return 1 } 213 if k == ND_CASE { return 1 } 214 return 0 215} 216 217func jss_collect(st: *i64, scope: i64, idx: i64) -> i64 { 218 if idx < 0 { return 0 } 219 let ctx: *i64 = (st[JS_ST_CTX]) as *i64 220 let k: i64 = jp_nkind(ctx, idx) 221 if k == ND_FUNC_DECL { 222 // the NAME binds in the ENCLOSING scope; the body is a new scope collected when walked 223 let nm: i64 = jp_na(ctx, idx) 224 if nm >= 0 { jss_declare(st, scope, jss_tokidx(ctx, nm)) } 225 return 0 226 } 227 if k == ND_CLASS { 228 let nm: i64 = jp_na(ctx, idx) 229 if nm >= 0 { jss_declare(st, scope, jss_tokidx(ctx, nm)) } 230 return 0 231 } 232 if k == ND_VAR_DECL { 233 let nm: i64 = jp_na(ctx, idx) 234 if nm >= 0 { 235 let nk: i64 = jp_nkind(ctx, nm) 236 if nk == ND_IDENT { jss_declare(st, scope, jss_tokidx(ctx, nm)) } 237 if nk == ND_ARRAY_PAT || nk == ND_OBJ_PAT { 238 let c: i64 = jp_nb(ctx, nm) 239 var i: i64 = 0 240 while i < c { jss_declare(st, scope, jss_tokidx(ctx, jp_child_at(ctx, nm, i))); i = i + 1 } 241 } 242 } 243 jss_collect(st, scope, jp_nb(ctx, idx)) 244 return 0 245 } 246 if k == ND_TRY { 247 let cp: i64 = jp_nb(ctx, idx) 248 if cp >= 0 { jss_declare(st, scope, jss_tokidx(ctx, cp)) } 249 jss_collect(st, scope, jp_na(ctx, idx)) 250 jss_collect(st, scope, jp_nc(ctx, idx)) 251 jss_collect(st, scope, jp_nextra(ctx, idx)) 252 return 0 253 } 254 if k == ND_FOR_OF || k == ND_FOR_IN { 255 let tg: i64 = jp_na(ctx, idx) 256 if tg >= 0 { 257 if jp_nkind(ctx, tg) == ND_IDENT { jss_declare(st, scope, jss_tokidx(ctx, tg)) } 258 if jp_nkind(ctx, tg) == ND_VAR_DECL { jss_collect(st, scope, tg) } 259 } 260 jss_collect(st, scope, jp_nc(ctx, idx)) 261 return 0 262 } 263 if jss_is_list(k) == 1 { 264 let c: i64 = jp_nb(ctx, idx) 265 var i: i64 = 0 266 while i < c { jss_collect(st, scope, jp_child_at(ctx, idx, i)); i = i + 1 } 267 if k == ND_SWITCH || k == ND_CASE { jss_collect(st, scope, jp_nc(ctx, idx)) } 268 return 0 269 } 270 if k == ND_FOR { 271 jss_collect(st, scope, jp_na(ctx, idx)) 272 jss_collect(st, scope, jp_nb(ctx, idx)) 273 jss_collect(st, scope, jp_nc(ctx, idx)) 274 jss_collect(st, scope, jss_tokidx(ctx, idx)) // FOR keeps its body in the tokidx slot 275 return 0 276 } 277 if k == ND_CALL { 278 // A CALL is half a list: a=callee, b=child_start, c=count. jp_child_at reads the start from 279 // slot a -- correct for every list node, WRONG here -- so it walked the callee index as an 280 // arena offset and every argument as a garbage node. That misattributed references AND 281 // recursed without a base case (SIGSEGV at the stack boundary) on the gate first run. 282 jss_collect(st, scope, jp_na(ctx, idx)) 283 let ch: *i64 = jp_children(ctx) 284 let start: i64 = jp_nb(ctx, idx) 285 let c: i64 = jp_nc(ctx, idx) 286 var i: i64 = 0 287 while i < c { jss_collect(st, scope, ch[start + i]); i = i + 1 } 288 return 0 289 } 290 // generic: a,b,c are node references for every remaining kind 291 if k == ND_IDENT || k == ND_NUMBER || k == ND_STRING || k == ND_BOOL || k == ND_NULL { return 0 } 292 if k == ND_THIS || k == ND_SUPER || k == ND_HOLE || k == ND_TEMPLATE || k == ND_REGEX { return 0 } 293 if k == ND_BREAK || k == ND_CONTINUE { return 0 } 294 if k == ND_MEMBER { jss_collect(st, scope, jp_na(ctx, idx)) 295 return 0 } 296 if k == ND_PROP { jss_collect(st, scope, jp_na(ctx, idx)) 297 return 0 } 298 jss_collect(st, scope, jp_na(ctx, idx)) 299 jss_collect(st, scope, jp_nb(ctx, idx)) 300 jss_collect(st, scope, jp_nc(ctx, idx)) 301 return 0 302} 303 304// ---- PASS 2: references, entering nested functions as NEW scopes --------------------------------- 305func jss_ref(st: *i64, scope: i64, tokidx: i64) -> i64 { 306 st[JS_ST_NREF] = st[JS_ST_NREF] + 1 307 if jss_resolve(st, scope, tokidx) >= 0 { return 0 } 308 if jss_is_global(st, tokidx) == 1 { return 0 } 309 let any: i64 = jss_declared_anywhere(st, tokidx) 310 if any >= 0 { jss_report(st, RP_OUTOFSCOPE, tokidx, scope, any) } else { jss_report(st, RP_UNRESOLVED, tokidx, scope, 0 - 1) } 311 return 0 312} 313 314func jss_walk(st: *i64, scope: i64, idx: i64) -> i64 { 315 if idx < 0 { return 0 } 316 let ctx: *i64 = (st[JS_ST_CTX]) as *i64 317 let k: i64 = jp_nkind(ctx, idx) 318 if k == ND_IDENT { jss_ref(st, scope, jss_tokidx(ctx, idx)) 319 return 0 } 320 if k == ND_FUNC_DECL { 321 let inner: i64 = jss_new_scope(st, idx, scope) 322 // params bind inside; then hoist the body's declarations; then walk the body 323 let ps: i64 = jp_nb(ctx, idx) 324 if ps >= 0 { 325 let c: i64 = jp_nb(ctx, ps) 326 var i: i64 = 0 327 while i < c { 328 let pn: i64 = jp_child_at(ctx, ps, i) 329 if pn >= 0 { 330 let pk: i64 = jp_nkind(ctx, pn) 331 if pk == ND_IDENT { jss_declare(st, inner, jss_tokidx(ctx, pn)) } 332 if pk == ND_VAR_DECL { jss_collect(st, inner, pn) } 333 if pk == ND_ARRAY_PAT || pk == ND_OBJ_PAT { jss_collect(st, inner, pn) } 334 } 335 i = i + 1 336 } 337 } 338 // a NAMED function expression can refer to itself from inside 339 let nm: i64 = jp_na(ctx, idx) 340 if nm >= 0 { jss_declare(st, inner, jss_tokidx(ctx, nm)) } 341 jss_collect(st, inner, jp_nc(ctx, idx)) 342 jss_walk(st, inner, jp_nc(ctx, idx)) 343 return 0 344 } 345 if k == ND_CLASS { 346 jss_walk(st, scope, jp_nb(ctx, idx)) 347 jss_walk(st, scope, jp_nc(ctx, idx)) 348 return 0 349 } 350 if k == ND_MEMBER { jss_walk(st, scope, jp_na(ctx, idx)) 351 return 0 } // .b is a property name, never a reference 352 if k == ND_PROP { jss_walk(st, scope, jp_na(ctx, idx)) 353 return 0 } // key is a token, value is the expression 354 if k == ND_VAR_DECL { jss_walk(st, scope, jp_nb(ctx, idx)) 355 return 0 } // the name was declared in pass 1 356 if k == ND_ARRAY_PAT || k == ND_OBJ_PAT { return 0 } 357 if k == ND_TRY { 358 jss_walk(st, scope, jp_na(ctx, idx)) 359 jss_walk(st, scope, jp_nc(ctx, idx)) 360 jss_walk(st, scope, jp_nextra(ctx, idx)) 361 return 0 362 } 363 if k == ND_FOR_OF || k == ND_FOR_IN { 364 let tg: i64 = jp_na(ctx, idx) 365 if tg >= 0 { if jp_nkind(ctx, tg) == ND_VAR_DECL { jss_walk(st, scope, tg) } } 366 jss_walk(st, scope, jp_nb(ctx, idx)) 367 jss_walk(st, scope, jp_nc(ctx, idx)) 368 return 0 369 } 370 if jss_is_list(k) == 1 { 371 let c: i64 = jp_nb(ctx, idx) 372 var i: i64 = 0 373 while i < c { jss_walk(st, scope, jp_child_at(ctx, idx, i)); i = i + 1 } 374 if k == ND_SWITCH || k == ND_CASE { jss_walk(st, scope, jp_nc(ctx, idx)) } 375 return 0 376 } 377 if k == ND_FOR { 378 jss_walk(st, scope, jp_na(ctx, idx)) 379 jss_walk(st, scope, jp_nb(ctx, idx)) 380 jss_walk(st, scope, jp_nc(ctx, idx)) 381 jss_walk(st, scope, jss_tokidx(ctx, idx)) 382 return 0 383 } 384 if k == ND_CALL { 385 jss_walk(st, scope, jp_na(ctx, idx)) 386 let ch: *i64 = jp_children(ctx) 387 let start: i64 = jp_nb(ctx, idx) 388 let c: i64 = jp_nc(ctx, idx) 389 var i: i64 = 0 390 while i < c { jss_walk(st, scope, ch[start + i]); i = i + 1 } 391 return 0 392 } 393 if k == ND_NUMBER || k == ND_STRING || k == ND_BOOL || k == ND_NULL { return 0 } 394 if k == ND_THIS || k == ND_SUPER || k == ND_HOLE || k == ND_TEMPLATE || k == ND_REGEX { return 0 } 395 if k == ND_BREAK || k == ND_CONTINUE { return 0 } 396 jss_walk(st, scope, jp_na(ctx, idx)) 397 jss_walk(st, scope, jp_nb(ctx, idx)) 398 jss_walk(st, scope, jp_nc(ctx, idx)) 399 return 0 400} 401 402// ---- driver -------------------------------------------------------------------------------------- 403// Parse `src`, resolve every reference, fill the report. Returns the state block, or 0 if the source 404// did not parse (a scope verdict over a parse failure would be a verdict about nothing). 405func jss_check(src: *u8, srclen: i64, globals: *u8, globlen: i64) -> *i64 { 406 let ctxbox: *i64 = sys_mmap(16) as *i64 407 let prog: i64 = jp_parse_source(src, srclen, ctxbox) 408 let ctx: *i64 = (ctxbox[0]) as *i64 409 let pst: *i64 = jp_pst(ctx) 410 if pst[PST_ERR] == 1 { return 0 as *i64 } 411 let st: *i64 = sys_mmap(JS_ST_N*8) as *i64 412 var i: i64 = 0 413 while i < JS_ST_N { st[i] = 0; i = i + 1 } 414 st[JS_ST_CTX] = ctx as i64 415 st[JS_ST_SC] = sys_mmap(SC_MAX*SC_ROW*8) as i64 416 st[JS_ST_DC] = sys_mmap(DC_MAX*2*8) as i64 417 st[JS_ST_RP] = sys_mmap(RP_MAX*RP_ROW*8) as i64 418 st[JS_ST_GLOB] = globals as i64 419 st[JS_ST_GLOBN] = globlen 420 let top: i64 = jss_new_scope(st, 0 - 1, 0 - 1) 421 jss_collect(st, top, prog) 422 jss_walk(st, top, prog) 423 return st 424} 425 426// Parse diagnostics are callable so emitters can identify the exact rejected byte instead of emitting a generic grammar error. 427func jss_error_pos(src: *u8, srclen: i64) -> i64 { 428 let box: *i64 = sys_mmap(16) as *i64 429 jp_parse_source(src, srclen, box) 430 let ctx: *i64 = (box[0]) as *i64 431 let pst: *i64 = jp_pst(ctx) 432 return pst[PST_ERR_POS] 433} 434 435// The scope row's function NAME token (for the report): -1 for the program or an anonymous function. 436func jss_scope_name_tok(st: *i64, s: i64) -> i64 { 437 if s < 0 { return 0 - 1 } 438 let sc: *i64 = (st[JS_ST_SC]) as *i64 439 let node: i64 = sc[s*SC_ROW + SC_NODE] 440 if node < 0 { return 0 - 1 } 441 let ctx: *i64 = (st[JS_ST_CTX]) as *i64 442 let nm: i64 = jp_na(ctx, node) 443 if nm < 0 { return 0 - 1 } 444 return jss_tokidx(ctx, nm) 445}