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}