code wiki / _hdl_build / nx_jsscope_gate.nx
nx_jsscope_gate.nx source
↩ module page · 277 lines · 14976 B
1// nx_jsscope_gate.nx -- EVERY NAME THE SHIPPED PAGE REFERENCES IS VISIBLE FROM WHERE IT IS USED.
2//
3// THE DEFECT THIS EXISTS TO CATCH, VERBATIM FROM THE OPERATOR'S URL BAR ON 2026-08-25:
4// /world/craft?gl=0&why=ReferenceError%3A+nxHairTick+is+not+defined
5// A function declared inside loadNPC and called from drawNPCs at script scope. The page lexed, the
6// wasm painted, the ship lane read the page back and verified its marker -- and every visitor lost
7// the WebGL tier on the first frame. This gate asks the one question a browser asks first, statically,
8// over the incumbent nx_js_parse AST: is every referenced name declared in an ENCLOSING scope?
9//
10// THE CONTROLS RUN FIRST AND THE LOAD-BEARING ONE IS THE PLANTED nxHairTick SHAPE. A resolver that
11// only reports "undefined" would send a reader to grep for a typo; this one must say OUT-OF-SCOPE and
12// NAME THE DECLARING FUNCTION, because that is the line that sends them to the fix.
13// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
14import "nx_syscalls.nx"
15import "nx_js_lex.nx"
16import "nx_js_parse.nx"
17import "nx_jsscope.nx"
18import "nx_gate_verdict.nx"
19
20const JG_PAGE: *u8 = "../sites/nishifamily/world/beach.html"
21const JG_GLOBALS: *u8 = "../knowledge/js_globals.conf"
22// A real emitted page is hundreds of KB; anything far below is an empty or truncated read and a scope
23// verdict over it would be the gate-passes-on-the-empty-set defect.
24const JG_MIN_PAGE: i64 = 100000
25// How many offending rows to PRINT. The COUNT is always complete; the list is a prefix and says so.
26const JG_PRINT_MAX: i64 = 24
27
28func jg_len(s: *u8) -> i64 { var n: i64 = 0; while (s[n] & 255) != 0 { n = n + 1 } return n }
29
30func jg_put_tok(st: *i64, tokidx: i64) -> i64 {
31 if tokidx < 0 { gv_puts("(anonymous)" as *u8); return 0 }
32 let ctx: *i64 = (st[JS_ST_CTX]) as *i64
33 let s: *u8 = jss_src(ctx)
34 sys_write(1, ((s as i64) + jss_tok_start(ctx, tokidx)) as *u8, jss_tok_len(ctx, tokidx))
35 return 0
36}
37
38// Count report rows of one kind. The whole table, never a prefix.
39func jg_count(st: *i64, kind: i64) -> i64 {
40 let rp: *i64 = (st[JS_ST_RP]) as *i64
41 var n: i64 = 0
42 var i: i64 = 0
43 while i < st[JS_ST_NRP] { if rp[i*RP_ROW] == kind { n = n + 1 }
44 i = i + 1 }
45 return n
46}
47
48func jg_print(st: *i64) -> i64 {
49 let rp: *i64 = (st[JS_ST_RP]) as *i64
50 let ctx: *i64 = (st[JS_ST_CTX]) as *i64
51 var shown: i64 = 0
52 var i: i64 = 0
53 while i < st[JS_ST_NRP] {
54 if shown < JG_PRINT_MAX {
55 if rp[i*RP_ROW] == RP_OUTOFSCOPE { gv_puts(" OUT-OF-SCOPE name=" as *u8) } else { gv_puts(" UNRESOLVED name=" as *u8) }
56 jg_put_tok(st, rp[i*RP_ROW+1])
57 gv_puts(" at_byte=" as *u8); gv_num(jss_tok_start(ctx, rp[i*RP_ROW+1]))
58 if rp[i*RP_ROW] == RP_OUTOFSCOPE {
59 gv_puts(" declared_in=" as *u8); jg_put_tok(st, jss_scope_name_tok(st, rp[i*RP_ROW+3]))
60 }
61 gv_puts("\n" as *u8)
62 shown = shown + 1
63 }
64 i = i + 1
65 }
66 if st[JS_ST_NRP] > shown { gv_puts(" ... " as *u8); gv_num(st[JS_ST_NRP] - shown); gv_puts(" more (THIS LIST IS A PREFIX OF ITS OWN COUNT)\n" as *u8) }
67 return 0
68}
69
70// Run the checker over a fixture with NO globals conf, so nothing is forgiven by the allowlist.
71func jg_fix(src: *u8) -> *i64 { return jss_check(src, jg_len(src), 0 as *u8, 0) }
72
73func main() -> i64 {
74 let ctr: *i64 = gv_ctr()
75 gv_head("nx_jsscope_gate -- every referenced name resolves to an ENCLOSING scope, on the shipped page" as *u8)
76
77 // T1 THE BITE, THE EXACT SHAPE THAT TOOK THE WORLDS DOWN: declared inside one function, called
78 // from another at script scope. Must be reported OUT-OF-SCOPE and name the declaring function.
79 let s1: *i64 = jg_fix("function loadNPC(){ if(1){ function nxHairTick(g){ return g } } }\nfunction drawNPCs(g){ nxHairTick(g); }\n" as *u8)
80 var t1: i64 = 0
81 if s1 as i64 != 0 { if jg_count(s1, RP_OUTOFSCOPE) == 1 { if jg_count(s1, RP_UNRESOLVED) == 0 { t1 = 1 } } }
82 if s1 as i64 != 0 { jg_print(s1) }
83 gv_check("T1 BITE: a function declared inside another and called from script scope is OUT-OF-SCOPE, declaring function named" as *u8, t1, ctr)
84
85 // T2 the fix shape resolves: the same function hoisted to script scope.
86 let s2: *i64 = jg_fix("function nxHairTick(g){ return g }\nfunction loadNPC(){ if(1){ } }\nfunction drawNPCs(g){ nxHairTick(g); }\n" as *u8)
87 var t2: i64 = 0
88 if s2 as i64 != 0 { if s2[JS_ST_NRP] == 0 { t2 = 1 } }
89 gv_check("T2 POSITIVE CONTROL: the same function at script scope resolves with zero findings" as *u8, t2, ctr)
90
91 // T2b THE PAGE'S OWN SHAPE: the real loadNPC is `async function`. A parser that rejects that form
92 // cannot judge the real page at all, so the gap is a NAMED tooth here rather than a silent T10 fail
93 // on the subject -- separable from the resolver, which T1/T2 prove on plain functions.
94 let s2b: *i64 = jg_fix("async function loadNPC(){ if(1){ function nxHairTick(g){ return g } } }\nfunction drawNPCs(g){ nxHairTick(g); }\n" as *u8)
95 var t2b: i64 = 0
96 if s2b as i64 != 0 { if jg_count(s2b, RP_OUTOFSCOPE) == 1 { t2b = 1 } }
97 gv_check("T2b the PAGE'S OWN SHAPE parses: an `async function` declaration is accepted and its nested declaration is still caught" as *u8, t2b, ctr)
98
99 // T3 HOISTING: a call BEFORE the declaration in the same scope is legal JS and must not be flagged.
100 // A checker that only looked backwards would flag half of every real page.
101 let s3: *i64 = jg_fix("function a(){ return b(1) }\nfunction b(x){ return x }\n" as *u8)
102 var t3: i64 = 0
103 if s3 as i64 != 0 { if s3[JS_ST_NRP] == 0 { t3 = 1 } }
104 gv_check("T3 HOISTING: a call that precedes its declaration in the same scope is NOT flagged" as *u8, t3, ctr)
105
106 // T4 enclosing scopes are visible inward; params and vars bind.
107 let s4: *i64 = jg_fix("var top=1;\nfunction outer(p){ var v=2; function inner(q){ return top+p+v+q } return inner(3) }\n" as *u8)
108 var t4: i64 = 0
109 if s4 as i64 != 0 { if s4[JS_ST_NRP] == 0 { t4 = 1 } }
110 gv_check("T4 ENCLOSING SCOPES resolve inward: outer vars, params and script-level names all bind" as *u8, t4, ctr)
111
112 // T5 property names are NEVER references -- a.b, {k:v} and a["s"] must not be looked up as names.
113 let s5: *i64 = jg_fix("var a={k:1};\nfunction f(){ return a.k + a.missing + a[\"s\"] }\n" as *u8)
114 var t5: i64 = 0
115 if s5 as i64 != 0 { if s5[JS_ST_NRP] == 0 { t5 = 1 } }
116 gv_check("T5 PROPERTY NAMES are not references: a.b and object keys are never looked up" as *u8, t5, ctr)
117
118 // neg-control: a name declared NOWHERE is UNRESOLVED, distinct from OUT-OF-SCOPE.
119 let s6: *i64 = jg_fix("function f(){ return nowhere(1) }\n" as *u8)
120 var t6: i64 = 0
121 if s6 as i64 != 0 { if jg_count(s6, RP_UNRESOLVED) == 1 { if jg_count(s6, RP_OUTOFSCOPE) == 0 { t6 = 1 } } }
122 gv_check("neg-control-a-name-declared-NOWHERE-is-UNRESOLVED-and-is-NOT-confused-with-out-of-scope" as *u8, t6, ctr)
123
124 // neg-control: the globals conf forgives ONLY what it lists. An allowlist that forgave everything
125 // would make every finding above impossible on the real page.
126 let g7: *u8 = "window\nconsole\n" as *u8
127 let s7: *i64 = jss_check("function f(){ return window.x + console.log + notglobal }\n" as *u8, 57, g7, jg_len(g7))
128 var t7: i64 = 0
129 if s7 as i64 != 0 { if jg_count(s7, RP_UNRESOLVED) == 1 { t7 = 1 } }
130 gv_check("neg-control-the-globals-conf-forgives-exactly-its-rows-window-and-console-pass-notglobal-does-not" as *u8, t7, ctr)
131
132 // neg-control: a source that does not PARSE gets NO scope verdict. A resolver run over a broken
133 // AST would report whatever the parser managed before it gave up, as if it were the program.
134 var t8: i64 = 0
135 if (jg_fix("function f({ return 1 }\n" as *u8)) as i64 == 0 { t8 = 1 }
136 gv_check("neg-control-a-source-that-does-not-parse-REFUSES-a-scope-verdict-rather-than-judging-a-partial-AST" as *u8, t8, ctr)
137
138 // ---- THE SUBJECT: the shipped page, read on the NAS where it lives ---------------------------
139 let plenp: *i64 = sys_mmap(8) as *i64
140 plenp[0] = 0
141 let page: *u8 = sys_read_file(JG_PAGE, plenp)
142 let plen: i64 = plenp[0]
143 let glenp: *i64 = sys_mmap(8) as *i64
144 glenp[0] = 0
145 let glob: *u8 = sys_read_file(JG_GLOBALS, glenp)
146 let glen: i64 = glenp[0]
147 gv_puts(" subject=" as *u8); gv_puts(JG_PAGE); gv_puts(" bytes=" as *u8); gv_num(plen)
148 gv_puts(" globals_conf_bytes=" as *u8); gv_num(glen); gv_puts("\n" as *u8)
149
150 var t9: i64 = 0
151 if plen > JG_MIN_PAGE { if glen > 0 { t9 = 1 } }
152 gv_check("T9 THE SHIPPED PAGE AND THE GLOBALS CONF WERE BOTH READ (real sizes, not empty reads)" as *u8, t9, ctr)
153
154 // extract the first <script> body -- the page's behaviour lives there
155 var sstart: i64 = 0 - 1
156 var send: i64 = plen
157 var i: i64 = 0
158 while i + 8 < plen {
159 if sstart < 0 {
160 if (page[i]&255)==60 { if (page[i+1]&255)==115 { if (page[i+2]&255)==99 { if (page[i+3]&255)==114 {
161 var j: i64 = i
162 while j < plen && (page[j]&255) != 62 { j = j + 1 }
163 sstart = j + 1
164 i = j
165 } } } }
166 } else {
167 if (page[i]&255)==60 { if (page[i+1]&255)==47 { if (page[i+2]&255)==115 { if (page[i+3]&255)==99 { send = i
168 i = plen } } } }
169 }
170 i = i + 1
171 }
172 // Parse the subject FIRST and, on failure, print the bytes around the offending token -- each run
173 // then NAMES the parser's next grammar gap instead of reporting a bare FAIL. The local probe loop
174 // hit its precision floor (a windowed transport injects newlines mid-comment), so the NAS file,
175 // read exactly where it lives, is the only honest subject for this loop.
176 var t10: i64 = 0
177 var st: *i64 = 0 as *i64
178 if t9 == 1 { if sstart >= 0 {
179 let pbox: *i64 = sys_mmap(16) as *i64
180 let pprog: i64 = jp_parse_source(((page as i64) + sstart) as *u8, send - sstart, pbox)
181 let pctx: *i64 = (pbox[0]) as *i64
182 let ppst: *i64 = jp_pst(pctx)
183 if ppst[PST_ERR] == 1 {
184 let ts: i64 = jp_tok_start(pctx)
185 gv_puts(" PARSE GAP at script byte " as *u8); gv_num(ts); gv_puts(": [" as *u8)
186 var glo: i64 = ts - 100
187 if glo < 0 { glo = 0 }
188 var ghi: i64 = ts + 100
189 if ghi > send - sstart { ghi = send - sstart }
190 sys_write(1, ((page as i64) + sstart + glo) as *u8, ghi - glo)
191 gv_puts("]\n" as *u8)
192 } else {
193 t10 = 1
194 st = jss_check(((page as i64) + sstart) as *u8, send - sstart, glob, glen)
195 }
196 } }
197 gv_check("T10 THE PAGE PARSES under nx_js_parse, so the scope verdict below is about the whole program" as *u8, t10, ctr)
198
199 var oos: i64 = 0
200 var unr: i64 = 0
201 if st as i64 != 0 {
202 oos = jg_count(st, RP_OUTOFSCOPE)
203 unr = jg_count(st, RP_UNRESOLVED)
204 gv_puts(" references_examined=" as *u8); gv_num(st[JS_ST_NREF])
205 gv_puts(" scopes=" as *u8); gv_num(st[JS_ST_NSC])
206 gv_puts(" declarations=" as *u8); gv_num(st[JS_ST_NDC])
207 gv_puts(" OUT-OF-SCOPE=" as *u8); gv_num(oos)
208 gv_puts(" UNRESOLVED=" as *u8); gv_num(unr)
209 gv_puts(" capped=" as *u8); gv_num(st[JS_ST_OVER]); gv_puts("\n" as *u8)
210 jg_print(st)
211 }
212
213 var t11: i64 = 0
214 if st as i64 != 0 { if oos == 0 { t11 = 1 } }
215 gv_check("T11 NO NAME ON THE SHIPPED PAGE IS DECLARED IN A FUNCTION THAT DOES NOT ENCLOSE ITS USE (the nxHairTick class is extinct)" as *u8, t11, ctr)
216
217 // Unresolved names are either real bugs or globals missing from the conf. Either way the fix is a
218 // named row, and this tooth stays RED until the conf is complete -- the conf is calibrated by
219 // measurement, on this line, never by guessing which globals a page might use.
220 var t12: i64 = 0
221 if st as i64 != 0 { if unr == 0 { t12 = 1 } }
222 gv_check("T12 EVERY REMAINING NAME IS A DECLARED GLOBAL: the unresolved list is empty against knowledge/js_globals.conf" as *u8, t12, ctr)
223
224 // The tables must not have hit their caps, or every count above is a floor wearing a total's name.
225 var t13: i64 = 0
226 if st as i64 != 0 { if st[JS_ST_OVER] == 0 { t13 = 1 } }
227 gv_check("T13 NO TABLE HIT ITS CAP, so the counts above are totals and not floors" as *u8, t13, ctr)
228
229 // T15 REDECLARATION BITE: a planted duplicate const MUST yield exactly one RP_REDECL row --
230 // the SyntaxError class that blacked /world five times (NXHCH_G) while lex and scope passed.
231 let rsrc: *u8 = "const a9=1;const a9=2;
232" as *u8
233 let rst: *i64 = jss_check(rsrc, jg_len(rsrc), 0 as *u8, 0)
234 var t15: i64 = 0
235 if (rst as i64) != 0 {
236 let rrp: *i64 = (rst[JS_ST_RP]) as *i64
237 var rn9: i64 = 0
238 var ri9: i64 = 0
239 while ri9 < rst[JS_ST_NRP] { if rrp[ri9*RP_ROW] == RP_REDECL { rn9 = rn9 + 1 } ri9 = ri9 + 1 }
240 if rn9 == 1 { t15 = 1 }
241 }
242 gv_check("T15 REDECLARATION BITE: a planted duplicate const yields exactly one RP_REDECL row" as *u8, t15, ctr)
243 // T16 THE REAL PAGE IS REDECL-CLEAN AT SCRIPT SCOPE: the detector reports scope-0 collisions
244 // only (129 inner-scope rows measured on the clean page were ALL unmodeled block scoping --
245 // loop counters and catch params -- so top-level-only is the zero-false-positive arming).
246 var t16: i64 = 0
247 if (st as i64) != 0 {
248 let prp: *i64 = (st[JS_ST_RP]) as *i64
249 var pn9: i64 = 0
250 var pi9: i64 = 0
251 while pi9 < st[JS_ST_NRP] { if prp[pi9*RP_ROW] == RP_REDECL { pn9 = pn9 + 1 } pi9 = pi9 + 1 }
252 gv_puts(" page RP_REDECL rows=" as *u8); gv_num(pn9); gv_puts("
253" as *u8)
254 // NAME the first offenders: scope id + the token's own text -- a count without a worklist
255 // is not actionable, and an arming decision needs to see WHAT it would refuse.
256 var sh9: i64 = 0
257 pi9 = 0
258 while pi9 < st[JS_ST_NRP] {
259 if prp[pi9*RP_ROW] == RP_REDECL { if sh9 < 6 {
260 let tk9: i64 = prp[pi9*RP_ROW+1]
261 let ctx9: *i64 = (st[JS_ST_CTX]) as *i64
262 gv_puts(" REDECL scope=" as *u8); gv_num(prp[pi9*RP_ROW+2])
263 gv_puts(" tok=[" as *u8)
264 sys_write(1, ((jss_src(ctx9) as i64) + jss_tok_start(ctx9, tk9)) as *u8, jss_tok_len(ctx9, tk9))
265 gv_puts("]
266" as *u8)
267 sh9 = sh9 + 1
268 } }
269 pi9 = pi9 + 1
270 }
271 if pn9 == 0 { t16 = 1 }
272 }
273 gv_check("T16 THE SHIPPED PAGE IS REDECL-CLEAN (armable): zero duplicate-declaration rows" as *u8, t16, ctr)
274
275 return gv_verdict("JS-SCOPE" as *u8, ctr,
276 "static scope resolution over the incumbent parser; OUT-OF-SCOPE names its declaring function; globals are conf rows" as *u8)
277}