nx_js_parse.nx source
↩ module page · 2531 lines · 127084 B
1// nx_js_parse.nx -- R-JS-PARSE (WB-JS-001 rung 1): the ECMAScript PARSER, riding
2// the rung-0 tokenizer (nx_js_lex.nx) per the no-floating law -- the lexer is built
3// + gated BEFORE this parser composes it. Shape = RECURSIVE-DESCENT over the flat
4// token stream, producing an AST in a FLAT i64 ARENA (parallel slots per node, NO
5// struct complexity). Founds R-JS-EVAL above it (the interpreter rides the AST).
6//
7// Consumes nx_js_lex's token stream (3 i64/token: kind, start-offset, byte-length)
8// and js_lexeme_eq for keyword/punct matching. Produces nodes in a node arena
9// (NODE_SLOTS i64 each) + a child-index arena for variable-arity lists (PROGRAM
10// statement list, BLOCK body, CALL arguments, function params).
11//
12// SCOPE (rung 1) -- every sub-feature below is now GATED by a KAT that asserts its
13// node (council BLOCKER fix: the prior 'ALL gated' header listed paths with ZERO
14// coverage -- string/null/bool/typeof/unary +-/ the '/ %' '< <= > >=' '!= === !=='
15// operator families were code-present-but-unasserted; KAT9/10 now assert them):
16// - primary: identifier, number, STRING, true/false (BOOL), null, ( expr )
17// - postfix chains (left-assoc): member a.b, computed index a[b], call f(args...)
18// - unary prefix: ! - + typeof
19// - binary with CORRECT precedence + associativity (highest -> lowest):
20// (* / %) > (+ -) > (< <= > >=) > (== != === !==) > (&&) > (||)
21// all left-associative; assignment `=` is RIGHT-assoc and LOWEST.
22// - statements: var/let/const decl (optional initializer), expression statement,
23// return [expr] ;, block { ... }, if (cond) stmt [else stmt], while (cond) stmt,
24// function decl function name(params){body}
25// - PROGRAM = list of statements
26//
27// ERROR CONTRACT (honest -- what the parser DOES and does NOT enforce):
28// - Semicolons are OPTIONAL via ASI-lite: a statement may end without ';' ONLY at
29// a line terminator, before '}', or at EOF. Two statements on ONE line with no
30// separator (`a b`, `1 2`) are REJECTED (ERROR node + err flag) -- real JS
31// SyntaxError. (Council BLOCKER fix: previously `;` was unconditionally optional
32// so adjacent same-line statements were silently accepted.)
33// - Assignment requires a valid lvalue target (IDENT / MEMBER / INDEX). '1 = 2'
34// and '(a+b) = c' are REJECTED (real JS: Invalid left-hand side). (BLOCKER fix.)
35// - Unbalanced parens, missing operands, declarations with no name, dangling
36// operators, unterminated blocks -> ERROR node + err flag.
37// - NOT enforced (honest OPEN, NOT claimed): full ASI restricted-production rules
38// (e.g. `return`-newline-expr nuance), strict-mode lvalue refinements, label
39// scoping. These are rung-1b; the gate does NOT assert them.
40//
41// RUNG-3 ADDITION (R-JS-OBJ, now GATED here + in eval): object literals { k: v, ... }
42// (ND_OBJECT with ND_PROP children, key = IDENT or STRING token) and array literals
43// [a, b, ...] (ND_ARRAY with element-expr children, trailing commas + empty []
44// allowed). They appear in EXPRESSION/primary position; member/index/call chains
45// apply on top ({a:1}.a, [1,2][0]). Statements beginning with '{' are still BLOCKS.
46//
47// HONEST OPEN (rung 1b -- NAMED, NOT faked/stubbed-as-done):
48// - computed object keys { [k]: v }, shorthand { x } / method { f(){} } props,
49// spread/rest ... in literals -> later rungs (only `key: expr` props are built).
50// - `({}).x` leading-brace-in-expression-statement nicety (a statement starting with
51// '{' parses as a BLOCK; wrap in parens to force object-literal context) -- OPEN.
52// - template literals `...${}`, classes,
53// destructuring patterns, for-in / for-of, switch/case, labeled break/continue,
54// try/catch/finally, throw, spread/rest ..., new, comma operator,
55// bitwise & | ^ << >> >>>, ?? and ?. , bitwise/shift compound assigns &= **= etc.
56// These are deliberately deferred; the gate does NOT assert them as working.
57//
58// RUNG-6 ADDITION (R-JS-CLOSURE, gated in eval): FUNCTION EXPRESSIONS (`function (p){b}`
59// and named `function f(p){b}` in expression/primary position -- jp_parse_func_expr, same
60// ND_FUNC_DECL shape with name=-1 when anonymous; so `var f=function(){}` and the IIFE
61// `(function(){})()` parse) and ARROW FUNCTIONS (`x=>e`, `(a,b)=>e`, `()=>e`, block-body
62// `(x)=>{stmts}` -- jp_parse_arrow_*; desugared to ND_FUNC_DECL: params + body BLOCK, an
63// expr body wrapped as `{ return e; }`). Arrows fork at the TOP of jp_parse_assign (lowest
64// precedence, right-assoc). The `( ... )` cover-grammar ambiguity (param list vs paren-expr)
65// is resolved by jp_arrow_paren_ahead, which scans to the matching ')' and checks for a
66// following '=>'. Idents-only params; default/rest/destructured params + deeply ambiguous
67// covers stay HONEST OPEN. `this`-in-arrow is an eval-side OPEN (no `this` yet).
68//
69// RUNG-4 ADDITION (R-JS-CTRL, now GATED in eval): C-style `for(init;cond;update)body`
70// -> ND_FOR (a=init, b=cond, c=update, tokidx-slot=body; any of init/cond/update may be
71// empty/-1), `do body while(cond);` -> ND_DOWHILE (a=cond, b=body), ternary `cond?a:b`
72// -> ND_TERNARY (a=cond, b=then, c=else; precedence just above assignment, right-assoc),
73// compound assign `+= -= *= /= %=` -> ND_ASSIGN with the BINARY op-code in the `extra`
74// slot (0 = plain '='), and `break;`/`continue;` -> ND_BREAK/ND_CONTINUE statements.
75// for-in/for-of (need iterator protocol) and labeled break/continue stay HONEST OPEN.
76//
77// GATE (main): KATs parse known programs and ASSERT the AST structure (node kinds +
78// nesting + that precedence/associativity parsed correctly), incl. a TAMPER KAT
79// (malformed input MUST yield ERROR, never a fabricated valid AST). Self-validating;
80// exit 0 iff all pass; appends knowledge/status/js_engine.log.
81//
82// license_tier: ORIGINAL (tutor-bootstrap scaffold; team re-authors from the
83// R-JS-PARSE data spec via author=organ -- (B)-debt, mirror the lexer note.)
84import "nx_js_lex.nx"
85import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
86const ND_MAGIC_1024: i64 = 1024
87const ND_MAGIC_2048: i64 = 2048
88const ND_MAGIC_4096: i64 = 4096
89const ND_MAGIC_65536: i64 = 65536
90const ND_MAGIC_131072: i64 = 131072
91
92// ===================== node-kind constants =====================
93const ND_ERROR: i64 = 0
94const ND_PROGRAM: i64 = 1
95const ND_NUMBER: i64 = 2
96const ND_STRING: i64 = 3
97const ND_IDENT: i64 = 4
98const ND_BOOL: i64 = 5
99const ND_NULL: i64 = 6
100const ND_BINARY: i64 = 7
101const ND_UNARY: i64 = 8
102const ND_ASSIGN: i64 = 9
103const ND_CALL: i64 = 10
104const ND_MEMBER: i64 = 11 // a.b (b is an IDENT node)
105const ND_INDEX: i64 = 12 // a[b] (b is an expr node)
106const ND_VAR_DECL: i64 = 13 // a=name IDENT node, b=init expr node (-1 if none)
107const ND_EXPR_STMT: i64 = 14
108const ND_RETURN: i64 = 15 // a=expr node (-1 if bare `return;`)
109const ND_BLOCK: i64 = 16 // list-node: a=child_start, b=child_count
110const ND_IF: i64 = 17 // a=cond, b=then-stmt, c=else-stmt (-1 if none)
111const ND_WHILE: i64 = 18 // a=cond, b=body
112const ND_FUNC_DECL: i64 = 19 // a=name IDENT, b=params list-node, c=body BLOCK
113const ND_PARAMS: i64 = 20 // list-node: a=child_start, b=child_count
114const ND_OBJECT: i64 = 21 // object literal {k:v,...}: list-node a=child_start, b=prop_count (children are ND_PROP)
115const ND_ARRAY: i64 = 22 // array literal [e0,e1,...]: list-node a=child_start, b=elem_count
116const ND_PROP: i64 = 23 // object property: tokidx=key token (IDENT or STRING), a=value-expr node
117// ---- R-JS-CTRL (rung 4): control-flow + operator completion ----
118const ND_FOR: i64 = 24 // for(init;cond;update) body: a=init, b=cond, c=update, tokidx-slot=body (all may be -1 except body)
119const ND_TERNARY: i64 = 25 // cond ? a : b -> a=cond, b=then-expr, c=else-expr
120const ND_BREAK: i64 = 26 // break; (statement, no children)
121const ND_CONTINUE: i64 = 27 // continue; (statement, no children)
122const ND_DOWHILE: i64 = 28 // do body while(cond): a=cond, b=body
123// ---- R-JS-SYNTAX2 (rung 8): for-of / for-in / switch-case ----
124const ND_FOR_OF: i64 = 29 // for(var x of iter) body: a=var-target(VAR_DECL|IDENT), b=iterable-expr, c=body
125const ND_FOR_IN: i64 = 30 // for(var k in obj) body: a=var-target(VAR_DECL|IDENT), b=object-expr, c=body
126const ND_SWITCH: i64 = 31 // switch(disc){...}: LIST-node a=case_list_child_start, b=case_count, c=disc-expr (children=ND_CASE; a/b match jp_child_at)
127const ND_CASE: i64 = 32 // one case/default clause: LIST-node a=stmt_list_child_start, b=stmt_count, c=case-expr(-1 for default)
128const ND_SPREAD: i64 = 33 // ...expr (spread element in an array literal; a=inner expr)
129const ND_ARRAY_PAT: i64 = 34 // [a,b] destructuring pattern (var-decl target); children = name IDENTs
130const ND_OBJ_PAT: i64 = 35 // {a,b} destructuring pattern (shorthand); children = name IDENTs
131const ND_TEMPLATE: i64 = 36 // template literal (tokidx = whole template token; interior decoded at eval)
132const ND_THIS: i64 = 37 // the `this` keyword (eval resolves via the call frame's this binding)
133const ND_NEW: i64 = 38 // new C(args): extra=0 -> a=ND_CALL node; extra=1 -> a=callee (bare `new C`)
134const ND_UPDATE: i64 = 39 // ++x/--x/x++/x-- : a=lvalue (IDENT/MEMBER/INDEX); extra: 1=++pre 2=--pre 3=++post 4=--post
135const ND_TRY: i64 = 40 // try{a}catch(b){c}finally{slot4}: a=try BLOCK, b=catch-param IDENT(-1), c=catch BLOCK(-1), tokidx-slot=finally BLOCK(-1)
136const ND_THROW: i64 = 41 // throw expr: a=expr
137const ND_VAR_LIST: i64 = 42 // multi-declarator `var a, b=c, d;` -- LIST-node a=child_start b=count (children=ND_VAR_DECL)
138const ND_CLASS: i64 = 43 // class Name{constructor(){} m(){}...}: a=name IDENT, b=ctor ND_FUNC_DECL, c=methods ND_BLOCK (each child = a method ND_FUNC_DECL whose name IDENT is the method name); prototype methods -> instances via func_prototype+obj_set
139const ND_REGEX: i64 = 44 // regex literal: tokidx = whole /pattern/flags token; pattern+flags extracted + compiled (nx_rxfull) at eval
140const ND_SUPER: i64 = 45 // the `super` keyword -- only valid as a call callee (super(...)=parent ctor) or member object (super.m(...)=parent method); eval routes both in js_eval_call
141const ND_SEQ: i64 = 46 // comma/sequence expr `a, b, c` -- LIST node a=child_start b=count; eval each, value = last
142const ND_HOLE: i64 = 47 // ARRAY ELISION `[0,4,,5]` -- an omitted element. Evaluates to undefined
143const ND_YIELD: i64 = 48 // `yield [expr]` / `yield* expr` (generators, JS-SOTA phase 1 2026-07-29):
144 // a=operand node or -1. PARSE-COMPLETE, execution-STUBBED: eval -> undefined
145 // (unblocks the netflix parse frontier at 860,389 = `function*`; true
146 // iteration semantics = a later rung, honestly scoped)
147 // and still OCCUPIES a slot, so length stays 4 (real JS holes are
148 // sparse; we model them as a present-but-undefined cell, which is
149 // indistinguishable for read/length -- `in` / hasOwnProperty differ).
150 // MEASURED 2026-07-27: TypeScript's downlevel async helper emits
151 // `t.trys.push([0,4,,5])`, so this blocked every TS-compiled bundle.
152
153// operator codes (stored in node slot `extra` for BINARY/UNARY/ASSIGN/VAR_DECL kw)
154const OP_ADD: i64 = 1 // +
155const OP_SUB: i64 = 2 // -
156const OP_MUL: i64 = 3 // *
157const OP_DIV: i64 = 4 // /
158const OP_MOD: i64 = 5 // %
159const OP_LT: i64 = 6 // <
160const OP_LE: i64 = 7 // <=
161const OP_GT: i64 = 8 // >
162const OP_GE: i64 = 9 // >=
163const OP_EQ: i64 = 10 // ==
164const OP_NE: i64 = 11 // !=
165const OP_SEQ: i64 = 12 // ===
166const OP_SNE: i64 = 13 // !==
167const OP_AND: i64 = 14 // &&
168const OP_OR: i64 = 15 // ||
169const OP_NOT: i64 = 16 // ! (unary)
170const OP_NEG: i64 = 17 // - (unary)
171const OP_POS: i64 = 18 // + (unary)
172const OP_TYPEOF: i64 = 19 // typeof (unary)
173const OP_NULLISH: i64 = 20 // ?? (nullish coalescing, short-circuit)
174const OP_BAND: i64 = 21 // & (bitwise AND)
175const OP_BOR: i64 = 22 // | (bitwise OR)
176const OP_BXOR: i64 = 23 // ^ (bitwise XOR)
177const OP_SHL: i64 = 24 // << (left shift)
178const OP_SHR: i64 = 25 // >> (signed right shift)
179const OP_USHR: i64 = 26 // >>> (unsigned right shift)
180const OP_BNOT: i64 = 27 // ~ (unary bitwise NOT)
181const OP_INSTANCEOF: i64 = 28 // `a instanceof B` -> true if B.prototype is on a's [[Prototype]] chain
182const OP_IN: i64 = 29 // `k in obj` -> true if obj has property k (own or inherited)
183const OP_DELETE: i64 = 30 // `delete obj[k]` / `delete obj.k` -> remove property, return true (unary, ref-based)
184const OP_VOID: i64 = 31 // `void x` -> evaluate x (side effects) then return undefined (minified `void 0`=undefined)
185
186// ===================== arena layout =====================
187// Node arena: NODE_SLOTS i64 per node.
188// [0]=nkind [1]=a [2]=b [3]=c [4]=tokidx [5]=extra(op or kw-code)
189const NODE_SLOTS: i64 = 6
190
191// Parser state box (pst, *i64):
192// [0]=cur token index
193// [1]=error flag (0=ok, 1=error seen)
194// [2]=node count
195// [3]=child-list count (next free slot in child arena)
196const PST_CUR: i64 = 0
197const PST_ERR: i64 = 1
198const PST_NNODE: i64 = 2
199const PST_NCHILD: i64 = 3
200const PST_ERR_POS: i64 = 4 // source byte offset of the FIRST error token (-1 = none); for parser-gap location
201
202// ---- A parse context bundles the pointers the recursive functions need.
203// Passed explicitly (NishiLang has no closures); kept in a small i64 box
204// array `ctx` so we thread ONE pointer rather than six. ----
205// ctx (*i64):
206// [0]=src (*u8 as i64) [1]=toks (*i64 as i64) [2]=ntok
207// [3]=nodes (*i64 as i64) [4]=children (*i64 as i64)
208// [5]=pst (*i64 as i64) [6]=maxnodes [7]=maxchildren
209const CTX_SRC: i64 = 0
210const CTX_TOKS: i64 = 1
211const CTX_NTOK: i64 = 2
212const CTX_NODES: i64 = 3
213const CTX_CHILDREN: i64 = 4
214const CTX_PST: i64 = 5
215const CTX_MAXNODE: i64 = 6
216const CTX_MAXCHILD: i64 = 7
217
218// ---- accessors (keep call sites readable) ----
219func jp_pst(ctx: *i64) -> *i64 { return (ctx[CTX_PST]) as *i64 }
220func jp_toks(ctx: *i64) -> *i64 { return (ctx[CTX_TOKS]) as *i64 }
221func jp_nodes(ctx: *i64) -> *i64 { return (ctx[CTX_NODES]) as *i64 }
222func jp_children(ctx: *i64) -> *i64 { return (ctx[CTX_CHILDREN]) as *i64 }
223func jp_src(ctx: *i64) -> *u8 { return (ctx[CTX_SRC]) as *u8 }
224
225func jp_cur(ctx: *i64) -> i64 { let p: *i64 = jp_pst(ctx); return p[PST_CUR] }
226func jp_set_cur(ctx: *i64, v: i64) -> i64 { let p: *i64 = jp_pst(ctx); p[PST_CUR] = v; return 0 }
227func jp_err(ctx: *i64) -> i64 { let p: *i64 = jp_pst(ctx); return p[PST_ERR] }
228func jp_err_pos(ctx: *i64) -> i64 { let p: *i64 = jp_pst(ctx); return p[PST_ERR_POS] }
229func jp_set_err(ctx: *i64) -> i64 { let p: *i64 = jp_pst(ctx); if p[PST_ERR] == 0 { p[PST_ERR_POS] = jp_tok_start(ctx) } p[PST_ERR] = 1; return 0 }
230
231// ---- token peeking. Returns the token KIND at the cursor (or EOF if past end). ----
232func jp_tok_kind(ctx: *i64) -> i64 {
233 let p: *i64 = jp_pst(ctx)
234 let i: i64 = p[PST_CUR]
235 if i >= ctx[CTX_NTOK] { return JS_TOK_EOF }
236 let toks: *i64 = jp_toks(ctx)
237 return toks[i * 3 + 0]
238}
239func jp_tok_start(ctx: *i64) -> i64 {
240 let p: *i64 = jp_pst(ctx)
241 let i: i64 = p[PST_CUR]
242 if i >= ctx[CTX_NTOK] { return 0 }
243 let toks: *i64 = jp_toks(ctx)
244 return toks[i * 3 + 1]
245}
246func jp_tok_len(ctx: *i64) -> i64 {
247 let p: *i64 = jp_pst(ctx)
248 let i: i64 = p[PST_CUR]
249 if i >= ctx[CTX_NTOK] { return 0 }
250 let toks: *i64 = jp_toks(ctx)
251 return toks[i * 3 + 2]
252}
253func jp_advance(ctx: *i64) -> i64 {
254 let p: *i64 = jp_pst(ctx)
255 p[PST_CUR] = p[PST_CUR] + 1
256 return 0
257}
258
259// true (1) iff current token is PUNCT/KEYWORD with exact lexeme == lit (NUL-term).
260func jp_is_lex(ctx: *i64, kind: i64, lit: *u8) -> i64 {
261 if jp_tok_kind(ctx) != kind { return 0 }
262 return js_lexeme_eq(jp_src(ctx), jp_tok_start(ctx), jp_tok_len(ctx), lit)
263}
264func jp_is_punct(ctx: *i64, lit: *u8) -> i64 { return jp_is_lex(ctx, JS_TOK_PUNCT, lit) }
265func jp_is_kw(ctx: *i64, lit: *u8) -> i64 { return jp_is_lex(ctx, JS_TOK_KEYWORD, lit) }
266
267// ---- absolute-index token peeking (for arrow-function lookahead) ----
268// Read kind/start/len of the token at ABSOLUTE index `i` (EOF/0 past the end). These
269// power the cover-grammar disambiguation `( ... ) =>` (arrow param list) vs `( ... )`
270// (a parenthesized expression): the parser must look PAST the matching ')' for `=>`.
271func jp_kind_at(ctx: *i64, i: i64) -> i64 {
272 if i < 0 { return JS_TOK_EOF }
273 if i >= ctx[CTX_NTOK] { return JS_TOK_EOF }
274 let toks: *i64 = jp_toks(ctx)
275 return toks[i * 3 + 0]
276}
277// 1 iff the token at ABSOLUTE index `i` is a PUNCT whose lexeme == lit (NUL-term).
278func jp_punct_at(ctx: *i64, i: i64, lit: *u8) -> i64 {
279 if i < 0 { return 0 }
280 if i >= ctx[CTX_NTOK] { return 0 }
281 let toks: *i64 = jp_toks(ctx)
282 if toks[i * 3 + 0] != JS_TOK_PUNCT { return 0 }
283 return js_lexeme_eq(jp_src(ctx), toks[i * 3 + 1], toks[i * 3 + 2], lit)
284}
285// ARROW lookahead for the paren form: the cursor sits on '('. Scan forward tracking
286// paren NESTING depth to the MATCHING ')', then return 1 iff the very next token is
287// '=>'. Pure scan (no node creation, no cursor mutation) so a non-arrow '(' falls
288// through untouched to the normal parenthesized-expression path. Returns 0 if the
289// parens are unbalanced before EOF (a real syntax error handled downstream).
290func jp_arrow_paren_ahead(ctx: *i64) -> i64 {
291 var i: i64 = jp_cur(ctx)
292 var depth: i64 = 0
293 var go: i64 = 1
294 var found: i64 = 0
295 while go == 1 {
296 if i >= ctx[CTX_NTOK] { go = 0 }
297 else {
298 if jp_punct_at(ctx, i, "(\x00" as *u8) == 1 { depth = depth + 1 }
299 else { if jp_punct_at(ctx, i, ")\x00" as *u8) == 1 {
300 depth = depth - 1
301 if depth == 0 { found = i; go = 0 }
302 } }
303 i = i + 1
304 }
305 }
306 if found == 0 { if depth != 0 { return 0 } }
307 if depth != 0 { return 0 }
308 // token AFTER the matching ')' (at index found+1) must be '=>'.
309 return jp_punct_at(ctx, found + 1, "=>\x00" as *u8)
310}
311
312// Consume the current token IF it matches the punct lexeme; return 1 on match (and
313// advance), else 0. Does NOT error -- callers decide if absence is fatal.
314func jp_eat_punct(ctx: *i64, lit: *u8) -> i64 {
315 if jp_is_punct(ctx, lit) == 1 { jp_advance(ctx); return 1 }
316 return 0
317}
318
319// ---- ASI (Automatic Semicolon Insertion) support, real-JS-lite ----
320// The lexer DROPS whitespace/line-terminators (they live in the GAP between the
321// previous token's end offset and the current token's start offset). To honor ASI
322// we scan that gap for a line terminator. A statement may terminate WITHOUT an
323// explicit ';' ONLY at: a line break, before '}', or at EOF -- NEVER between two
324// tokens on the same line (real JS makes `a b` / `1 2` a SyntaxError).
325
326// end offset (start+length) of the token JUST BEFORE the cursor; -1 if at start.
327func jp_prev_end(ctx: *i64) -> i64 {
328 let i: i64 = jp_cur(ctx) - 1
329 if i < 0 { return -1 }
330 if i >= ctx[CTX_NTOK] { return -1 }
331 let toks: *i64 = jp_toks(ctx)
332 return toks[i * 3 + 1] + toks[i * 3 + 2]
333}
334
335// 1 iff a line terminator (\n=10 or \r=13) appears in src between the previous
336// token's end and the current token's start (i.e. the inter-token trivia gap).
337// If there is no previous token, or no current token (EOF), treat as a break (1).
338func jp_newline_before(ctx: *i64) -> i64 {
339 let pe: i64 = jp_prev_end(ctx)
340 if pe < 0 { return 1 }
341 if jp_cur(ctx) >= ctx[CTX_NTOK] { return 1 } // EOF after last token = break
342 let cs: i64 = jp_tok_start(ctx)
343 let src: *u8 = jp_src(ctx)
344 var i: i64 = pe
345 while i < cs {
346 let ch: i64 = src[i] & 0xff
347 if ch == 10 { return 1 }
348 if ch == 13 { return 1 }
349 i = i + 1
350 }
351 return 0
352}
353
354// 1 iff a statement may legally terminate at the cursor per ASI-lite:
355// - current token is EOF, or
356// - current token is '}' (block close), or
357// - a line terminator precedes the current token.
358func jp_can_asi(ctx: *i64) -> i64 {
359 if jp_tok_kind(ctx) == JS_TOK_EOF { return 1 }
360 if jp_cur(ctx) >= ctx[CTX_NTOK] { return 1 }
361 if jp_is_punct(ctx, "}\x00" as *u8) == 1 { return 1 }
362 return jp_newline_before(ctx)
363}
364
365// 1 iff the parsed node kind is a valid assignment TARGET (lvalue):
366// identifier, member access a.b, or computed index a[b]. Real JS rejects
367// assigning to anything else ('1 = 2' -> SyntaxError: Invalid left-hand side).
368func jp_is_lvalue(ctx: *i64, idx: i64) -> i64 {
369 let k: i64 = jp_nkind(ctx, idx)
370 if k == ND_IDENT { return 1 }
371 if k == ND_MEMBER { return 1 }
372 if k == ND_INDEX { return 1 }
373 return 0
374}
375
376// ---- node allocation. Returns the new node's INDEX (>=0), or -1 if arena full. ----
377func jp_new_node(ctx: *i64, nkind: i64, a: i64, b: i64, c: i64, tokidx: i64, extra: i64) -> i64 {
378 let p: *i64 = jp_pst(ctx)
379 let idx: i64 = p[PST_NNODE]
380 if idx >= ctx[CTX_MAXNODE] { jp_set_err(ctx); return -1 }
381 let nodes: *i64 = jp_nodes(ctx)
382 let base: i64 = idx * NODE_SLOTS
383 nodes[base + 0] = nkind
384 nodes[base + 1] = a
385 nodes[base + 2] = b
386 nodes[base + 3] = c
387 nodes[base + 4] = tokidx
388 nodes[base + 5] = extra
389 p[PST_NNODE] = idx + 1
390 return idx
391}
392func jp_error_node(ctx: *i64) -> i64 {
393 jp_set_err(ctx)
394 return jp_new_node(ctx, ND_ERROR, -1, -1, -1, jp_cur(ctx), 0)
395}
396
397// ---- node field reads (used by the gate to assert structure) ----
398func jp_nkind(ctx: *i64, idx: i64) -> i64 { let n: *i64 = jp_nodes(ctx); return n[idx * NODE_SLOTS + 0] }
399func jp_na(ctx: *i64, idx: i64) -> i64 { let n: *i64 = jp_nodes(ctx); return n[idx * NODE_SLOTS + 1] }
400func jp_nb(ctx: *i64, idx: i64) -> i64 { let n: *i64 = jp_nodes(ctx); return n[idx * NODE_SLOTS + 2] }
401func jp_nc(ctx: *i64, idx: i64) -> i64 { let n: *i64 = jp_nodes(ctx); return n[idx * NODE_SLOTS + 3] }
402func jp_nextra(ctx: *i64, idx: i64) -> i64 { let n: *i64 = jp_nodes(ctx); return n[idx * NODE_SLOTS + 5] }
403
404// child-list arena append: store a node index, return the slot it landed in.
405func jp_child_push(ctx: *i64, node_idx: i64) -> i64 {
406 let p: *i64 = jp_pst(ctx)
407 let slot: i64 = p[PST_NCHILD]
408 if slot >= ctx[CTX_MAXCHILD] { jp_set_err(ctx); return -1 }
409 let ch: *i64 = jp_children(ctx)
410 ch[slot] = node_idx
411 p[PST_NCHILD] = slot + 1
412 return slot
413}
414// read child k of a list-node whose a=child_start, b=child_count
415func jp_child_at(ctx: *i64, list_idx: i64, k: i64) -> i64 {
416 let start: i64 = jp_na(ctx, list_idx)
417 let ch: *i64 = jp_children(ctx)
418 return ch[start + k]
419}
420
421// Copy `count` node-indices from a caller scratch buffer into a CONTIGUOUS run of
422// the shared child arena; return the start slot. List nodes (PROGRAM/BLOCK/CALL/
423// PARAMS) buffer their direct children in local scratch FIRST -- because a nested
424// list (e.g. a call inside an argument) pushes into the same arena mid-parse and
425// would otherwise interleave -- then commit them contiguously here so child_at is
426// correct. Returns -1 (and sets error) if the arena would overflow.
427func jp_child_commit(ctx: *i64, scratch: *i64, count: i64) -> i64 {
428 let p: *i64 = jp_pst(ctx)
429 let start: i64 = p[PST_NCHILD]
430 if start + count > ctx[CTX_MAXCHILD] { jp_set_err(ctx); return -1 }
431 let ch: *i64 = jp_children(ctx)
432 var i: i64 = 0
433 while i < count {
434 ch[start + i] = scratch[i]
435 i = i + 1
436 }
437 p[PST_NCHILD] = start + count
438 return start
439}
440
441// ===================== expression parsing (precedence climb) =====================
442// forward refs are fine (whole-program resolution): parse_expr is the entry, the
443// binary tiers call down to parse_unary -> parse_postfix -> parse_primary.
444
445// array literal: [ e0 , e1 , ... ] (trailing comma + empty [] allowed) -> ND_ARRAY
446// list-node a=child_start, b=elem_count. Cursor is on '['. Elements are assignment-
447// expressions (so a comma operator does not bleed across element boundaries).
448func jp_parse_array(ctx: *i64) -> i64 {
449 jp_advance(ctx) // consume '['
450 let scratch: *i64 = sys_mmap(ND_MAGIC_1024 * 8) as *i64
451 var count: i64 = 0
452 var go: i64 = 1
453 if jp_is_punct(ctx, "]\x00" as *u8) == 1 { jp_advance(ctx); go = 0 } // empty []
454 while go == 1 {
455 var el: i64 = 0
456 if jp_is_punct(ctx, ",\x00" as *u8) == 1 { // ELISION: `[0,4,,5]` / `[,1]`
457 // An element position holding only a comma is an omitted element. Emit a HOLE
458 // and fall through to the separator logic below, which consumes that comma --
459 // so the slot is counted and the following element parses normally.
460 el = jp_new_node(ctx, ND_HOLE, -1, -1, -1, -1, 0)
461 } else {
462 if jp_is_punct(ctx, "...\x00" as *u8) == 1 { // spread element [...a]
463 jp_advance(ctx)
464 let inner: i64 = jp_parse_assign(ctx)
465 el = jp_new_node(ctx, ND_SPREAD, inner, -1, -1, -1, 0)
466 } else {
467 el = jp_parse_assign(ctx)
468 }
469 }
470 if count < ND_MAGIC_1024 { scratch[count] = el }
471 count = count + 1
472 if jp_err(ctx) == 1 { go = 0 }
473 if go == 1 {
474 if jp_eat_punct(ctx, ",\x00" as *u8) == 1 {
475 // trailing comma before ']' is allowed: [1,2,]
476 if jp_is_punct(ctx, "]\x00" as *u8) == 1 { jp_advance(ctx); go = 0 }
477 } else {
478 if jp_eat_punct(ctx, "]\x00" as *u8) == 1 { go = 0 }
479 else { jp_set_err(ctx); go = 0 }
480 }
481 }
482 }
483 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
484 let start: i64 = jp_child_commit(ctx, scratch, count)
485 return jp_new_node(ctx, ND_ARRAY, start, count, -1, -1, 0)
486}
487
488// object literal: { key : expr , ... } where key is an IDENT or STRING token.
489// (trailing comma + empty {} allowed) -> ND_OBJECT list-node a=child_start, b=prop_count.
490// Each child is an ND_PROP whose tokidx = key token and a = value-expr node. Cursor is
491// on '{'. Values are assignment-expressions. Keyword-named keys (e.g. {return:1}) are an
492// honest OPEN here (only IDENT/STRING keys are accepted; others -> ERROR, never faked).
493func jp_parse_object(ctx: *i64) -> i64 {
494 jp_advance(ctx) // consume '{'
495 let scratch: *i64 = sys_mmap(ND_MAGIC_1024 * 8) as *i64
496 var count: i64 = 0
497 var go: i64 = 1
498 if jp_is_punct(ctx, "}\x00" as *u8) == 1 { jp_advance(ctx); go = 0 } // empty {}
499 while go == 1 {
500 // key = IdentifierName (incl. reserved words) | StringLiteral | NumericLiteral (real JS: any reserved
501 // word is a valid property name -- minified jQuery ships {async:!0,default:...,delete:...,in:...}).
502 // OBJECT SPREAD `{...e, b:2}` (ES2018; the netflix frontier after accessors: `{...e.style}`).
503 // Reuses ND_SPREAD (already used by array literals + call args) so eval has ONE spread shape.
504 var didspread: i64 = 0
505 if jp_is_punct(ctx, "...\x00" as *u8) == 1 {
506 didspread = 1
507 jp_advance(ctx)
508 let sinner: i64 = jp_parse_assign(ctx)
509 let sprop: i64 = jp_new_node(ctx, ND_SPREAD, sinner, -1, -1, -1, 0)
510 if count < ND_MAGIC_1024 { scratch[count] = sprop }
511 count = count + 1
512 if jp_err(ctx) == 1 { go = 0 }
513 if go == 1 {
514 if jp_eat_punct(ctx, ",\x00" as *u8) == 1 {
515 if jp_is_punct(ctx, "}\x00" as *u8) == 1 { jp_advance(ctx); go = 0 }
516 } else {
517 if jp_eat_punct(ctx, "}\x00" as *u8) == 1 { go = 0 }
518 else { jp_set_err(ctx); go = 0 }
519 }
520 }
521 }
522 var keyok: i64 = 0
523 if didspread == 0 {
524 if jp_tok_kind(ctx) == JS_TOK_IDENT { keyok = 1 }
525 if jp_tok_kind(ctx) == JS_TOK_STRING { keyok = 1 }
526 if jp_tok_kind(ctx) == JS_TOK_KEYWORD { keyok = 1 }
527 if jp_tok_kind(ctx) == JS_TOK_NUMBER { keyok = 1 }
528 if keyok == 0 { jp_set_err(ctx); go = 0 }
529 } else { go = go * 0 + go } // spread already consumed this member (+ its separator)
530 // ACCESSORS `{get k(){...}, set k(v){...}}` (ES5, ubiquitous in modern bundles -- the netflix
531 // frontier after generators: `{get passive(){o=!0;return}}`). `get`/`set` are CONTEXTUAL: a
532 // real accessor has get/set followed by a KEY token, whereas `{get:1}` / `{get(){}}` use them
533 // as plain names -- so only treat as accessor when the NEXT token is a key and not ':' or '('.
534 // Parsed as a normal method-valued property (the getter body becomes the value); true
535 // accessor invocation semantics = a later rung, declared not faked.
536 if go == 1 { if jp_tok_kind(ctx) == JS_TOK_IDENT {
537 var isacc: i64 = 0
538 if jp_is_lex(ctx, JS_TOK_IDENT, "get\x00" as *u8) == 1 { isacc = 1 }
539 if jp_is_lex(ctx, JS_TOK_IDENT, "set\x00" as *u8) == 1 { isacc = 1 }
540 if isacc == 1 {
541 let nk: i64 = jp_kind_at(ctx, jp_cur(ctx) + 1) // âš ABSOLUTE index (not an offset)
542 var nextiskey: i64 = 0
543 if nk == JS_TOK_IDENT { nextiskey = 1 }
544 if nk == JS_TOK_STRING { nextiskey = 1 }
545 if nk == JS_TOK_KEYWORD { nextiskey = 1 }
546 if nk == JS_TOK_NUMBER { nextiskey = 1 }
547 if nextiskey == 1 { jp_advance(ctx) } // consume `get`/`set`; the KEY is now current
548 }
549 } }
550 if go == 1 {
551 let keytok: i64 = jp_cur(ctx)
552 jp_advance(ctx)
553 var val: i64 = 0
554 if jp_is_punct(ctx, "(\x00" as *u8) == 1 { // method shorthand: key(params){body}
555 let mparams: i64 = jp_parse_params(ctx)
556 let mbody: i64 = jp_parse_block(ctx)
557 val = jp_new_node(ctx, ND_FUNC_DECL, -1, mparams, mbody, -1, 0)
558 if jp_err(ctx) == 1 { go = 0 }
559 } else {
560 // SHORTHAND PROPERTY `{dev,gctx}` (ES6, ubiquitous): an IDENT key followed directly by
561 // `,` or `}` means {dev:dev} -- the value IS the same-named variable. Found by walking
562 // the real shipped page (byte 293,650: GPU={dev,gctx,t3,...}). Only IDENT keys qualify;
563 // {return,} stays an error, exactly as a browser treats it.
564 var jshort: i64 = 0
565 if jp_kind_at(ctx, jp_cur(ctx) - 1) == JS_TOK_IDENT {
566 if jp_is_punct(ctx, ",\x00" as *u8) == 1 { jshort = 1 }
567 if jp_is_punct(ctx, "}\x00" as *u8) == 1 { jshort = 1 }
568 }
569 if jshort == 1 {
570 val = jp_new_node(ctx, ND_IDENT, -1, -1, -1, keytok, 0)
571 } else {
572 if jp_eat_punct(ctx, ":\x00" as *u8) == 0 { jp_set_err(ctx); go = 0 }
573 if go == 1 { val = jp_parse_assign(ctx) }
574 }
575 }
576 if go == 1 {
577 let prop: i64 = jp_new_node(ctx, ND_PROP, val, -1, -1, keytok, 0)
578 if count < ND_MAGIC_1024 { scratch[count] = prop }
579 count = count + 1
580 if jp_err(ctx) == 1 { go = 0 }
581 if go == 1 {
582 if jp_eat_punct(ctx, ",\x00" as *u8) == 1 {
583 // trailing comma before '}' is allowed: {a:1,}
584 if jp_is_punct(ctx, "}\x00" as *u8) == 1 { jp_advance(ctx); go = 0 }
585 } else {
586 if jp_eat_punct(ctx, "}\x00" as *u8) == 1 { go = 0 }
587 else { jp_set_err(ctx); go = 0 }
588 }
589 }
590 }
591 }
592 }
593 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
594 let start: i64 = jp_child_commit(ctx, scratch, count)
595 return jp_new_node(ctx, ND_OBJECT, start, count, -1, -1, 0)
596}
597
598// `new C(args)` -> ND_NEW. extra=0: a = the ND_CALL node (callee + args). extra=1: a = callee (bare `new C`).
599func jp_parse_new(ctx: *i64) -> i64 {
600 jp_advance(ctx) // consume 'new'
601 var callee: i64 = jp_parse_primary(ctx)
602 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
603 // MemberExpression tail: `new A.b.c(...)` / `new A[k](...)` -- the constructor is the FULL member chain
604 // (`.prop` and `[expr]`), but NOT a call '(' (that begins the `new` Arguments). Mirrors jp_parse_postfix
605 // minus the call tail. (Guarded-if form: NishiLang has no `else if`.)
606 var go: i64 = 1
607 while go == 1 {
608 if jp_err(ctx) == 1 { go = 0 }
609 if go == 1 {
610 if jp_is_punct(ctx, ".\x00" as *u8) == 1 {
611 jp_advance(ctx)
612 if jp_tok_kind(ctx) != JS_TOK_IDENT { if jp_tok_kind(ctx) != JS_TOK_KEYWORD { callee = jp_error_node(ctx); go = 0 } }
613 if go == 1 {
614 let prop: i64 = jp_new_node(ctx, ND_IDENT, -1, -1, -1, jp_cur(ctx), 0)
615 jp_advance(ctx)
616 callee = jp_new_node(ctx, ND_MEMBER, callee, prop, -1, -1, 0)
617 }
618 } else {
619 if jp_is_punct(ctx, "[\x00" as *u8) == 1 {
620 jp_advance(ctx)
621 let ix: i64 = jp_parse_expr(ctx)
622 if jp_eat_punct(ctx, "]\x00" as *u8) == 0 { callee = jp_error_node(ctx); go = 0 }
623 if go == 1 { callee = jp_new_node(ctx, ND_INDEX, callee, ix, -1, -1, 0) }
624 } else {
625 go = 0
626 }
627 }
628 }
629 }
630 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
631 if jp_is_punct(ctx, "(\x00" as *u8) == 1 {
632 let callnode: i64 = jp_parse_call_tail(ctx, callee, 0)
633 return jp_new_node(ctx, ND_NEW, callnode, -1, -1, -1, 0)
634 }
635 return jp_new_node(ctx, ND_NEW, callee, -1, -1, -1, 1)
636}
637// `class Name { constructor(params){body} m(params){body}... }` -> ND_CLASS (a=name, b=ctor ND_FUNC_DECL,
638// c=methods ND_BLOCK of method ND_FUNC_DECLs). Prototype methods are now KEPT (was: dropped). No extends/
639// static/getters yet (those decline gracefully at parse or eval). Method-node = ND_FUNC_DECL(name,params,body).
640func jp_parse_class_decl(ctx: *i64) -> i64 {
641 jp_advance(ctx) // consume 'class'
642 if jp_tok_kind(ctx) != JS_TOK_IDENT { return jp_error_node(ctx) }
643 let nametok: i64 = jp_cur(ctx)
644 let name: i64 = jp_new_node(ctx, ND_IDENT, -1, -1, -1, nametok, 0)
645 jp_advance(ctx)
646 // optional `extends <expr>` -- superclass is usually a bare ident (`extends A`) but may be a
647 // member (`extends React.Component`); parse a postfix so both shapes work. Stored in ND_CLASS extra.
648 var superidx: i64 = 0 - 1
649 if jp_is_kw(ctx, "extends\x00" as *u8) == 1 {
650 jp_advance(ctx)
651 superidx = jp_parse_postfix(ctx)
652 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
653 }
654 if jp_eat_punct(ctx, "{\x00" as *u8) == 0 { return jp_error_node(ctx) }
655 var cparams: i64 = 0 - 1
656 var cbody: i64 = 0 - 1
657 let mscratch: *i64 = sys_mmap(ND_MAGIC_1024 * 8) as *i64 // collected method func-decl nodes
658 var mcount: i64 = 0
659 var go: i64 = 1
660 while go == 1 {
661 if jp_is_punct(ctx, "}\x00" as *u8) == 1 { jp_advance(ctx); go = 0 }
662 else {
663 var isok: i64 = 0
664 if jp_tok_kind(ctx) == JS_TOK_IDENT { isok = 1 }
665 if jp_tok_kind(ctx) == JS_TOK_KEYWORD { isok = 1 }
666 if isok == 0 { jp_set_err(ctx); go = 0 }
667 if go == 1 {
668 let is_ctor: i64 = jp_is_lex(ctx, JS_TOK_IDENT, "constructor\x00" as *u8)
669 let mnametok: i64 = jp_cur(ctx)
670 jp_advance(ctx) // method name
671 let mparams: i64 = jp_parse_params(ctx)
672 let mbody: i64 = jp_parse_block(ctx)
673 if jp_err(ctx) == 1 { go = 0 }
674 if go == 1 {
675 if is_ctor == 1 { cparams = mparams; cbody = mbody }
676 else {
677 let mname: i64 = jp_new_node(ctx, ND_IDENT, -1, -1, -1, mnametok, 0)
678 let mfd: i64 = jp_new_node(ctx, ND_FUNC_DECL, mname, mparams, mbody, -1, 0)
679 if mcount < ND_MAGIC_1024 { mscratch[mcount] = mfd }
680 mcount = mcount + 1
681 }
682 }
683 }
684 }
685 }
686 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
687 if cparams == (0 - 1) {
688 if superidx >= 0 {
689 // DERIVED class with no explicit ctor -> synthesize `constructor(...N){ super(...N) }` so
690 // `new B(args)` runs the PARENT ctor with the forwarded args (spec's implicit derived ctor).
691 // N reuses the class-name token (a harmless local rest param; the body only spreads it).
692 let restp: i64 = jp_new_node(ctx, ND_IDENT, -1, -1, -1, nametok, 1) // rest param (extra=1=rest)
693 let pscr: *i64 = sys_mmap(8) as *i64
694 pscr[0] = restp
695 let pstart: i64 = jp_child_commit(ctx, pscr, 1)
696 cparams = jp_new_node(ctx, ND_PARAMS, pstart, 1, -1, -1, 0)
697 let argid: i64 = jp_new_node(ctx, ND_IDENT, -1, -1, -1, nametok, 0) // reference to N
698 let spread: i64 = jp_new_node(ctx, ND_SPREAD, argid, -1, -1, -1, 0) // ...N
699 let ascr: *i64 = sys_mmap(8) as *i64
700 ascr[0] = spread
701 let astart: i64 = jp_child_commit(ctx, ascr, 1)
702 let supnode: i64 = jp_new_node(ctx, ND_SUPER, -1, -1, -1, nametok, 0)
703 let scall: i64 = jp_new_node(ctx, ND_CALL, supnode, astart, 1, -1, 0) // super(...N)
704 let estmt: i64 = jp_new_node(ctx, ND_EXPR_STMT, scall, -1, -1, -1, 0)
705 let bscr: *i64 = sys_mmap(8) as *i64
706 bscr[0] = estmt
707 let bstart: i64 = jp_child_commit(ctx, bscr, 1)
708 cbody = jp_new_node(ctx, ND_BLOCK, bstart, 1, -1, -1, 0)
709 } else { // non-derived class, no ctor -> empty function
710 let scratch: *i64 = sys_mmap(8) as *i64
711 let ps: i64 = jp_child_commit(ctx, scratch, 0)
712 cparams = jp_new_node(ctx, ND_PARAMS, ps, 0, -1, -1, 0)
713 let bs: i64 = jp_child_commit(ctx, scratch, 0)
714 cbody = jp_new_node(ctx, ND_BLOCK, bs, 0, -1, -1, 0)
715 }
716 }
717 let ctorfd: i64 = jp_new_node(ctx, ND_FUNC_DECL, name, cparams, cbody, -1, 0)
718 let mstart: i64 = jp_child_commit(ctx, mscratch, mcount)
719 let methods: i64 = jp_new_node(ctx, ND_BLOCK, mstart, mcount, -1, -1, 0)
720 return jp_new_node(ctx, ND_CLASS, name, ctorfd, methods, -1, superidx)
721}
722// primary: literal / ident / parenthesized expr
723func jp_parse_primary(ctx: *i64) -> i64 {
724 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
725 let k: i64 = jp_tok_kind(ctx)
726 let tk: i64 = jp_cur(ctx)
727
728 if k == JS_TOK_NUMBER {
729 let n: i64 = jp_new_node(ctx, ND_NUMBER, -1, -1, -1, tk, 0)
730 jp_advance(ctx)
731 return n
732 }
733 if k == JS_TOK_STRING {
734 let n: i64 = jp_new_node(ctx, ND_STRING, -1, -1, -1, tk, 0)
735 jp_advance(ctx)
736 return n
737 }
738 if k == JS_TOK_TEMPLATE {
739 let n: i64 = jp_new_node(ctx, ND_TEMPLATE, -1, -1, -1, tk, 0)
740 jp_advance(ctx)
741 return n
742 }
743 if k == JS_TOK_REGEX {
744 let n: i64 = jp_new_node(ctx, ND_REGEX, -1, -1, -1, tk, 0)
745 jp_advance(ctx)
746 return n
747 }
748 if k == JS_TOK_IDENT {
749 let n: i64 = jp_new_node(ctx, ND_IDENT, -1, -1, -1, tk, 0)
750 jp_advance(ctx)
751 return n
752 }
753 if k == JS_TOK_KEYWORD {
754 if jp_is_kw(ctx, "true\x00" as *u8) == 1 {
755 let n: i64 = jp_new_node(ctx, ND_BOOL, -1, -1, -1, tk, 1)
756 jp_advance(ctx); return n
757 }
758 if jp_is_kw(ctx, "false\x00" as *u8) == 1 {
759 let n: i64 = jp_new_node(ctx, ND_BOOL, -1, -1, -1, tk, 0)
760 jp_advance(ctx); return n
761 }
762 if jp_is_kw(ctx, "this\x00" as *u8) == 1 {
763 let n: i64 = jp_new_node(ctx, ND_THIS, -1, -1, -1, tk, 0) // the this keyword; eval resolves via the frame binding
764 jp_advance(ctx); return n
765 }
766 if jp_is_kw(ctx, "super\x00" as *u8) == 1 {
767 let n: i64 = jp_new_node(ctx, ND_SUPER, -1, -1, -1, tk, 0) // valid only as call callee / member object; js_eval_call routes it
768 jp_advance(ctx); return n
769 }
770 if jp_is_kw(ctx, "null\x00" as *u8) == 1 {
771 let n: i64 = jp_new_node(ctx, ND_NULL, -1, -1, -1, tk, 0)
772 jp_advance(ctx); return n
773 }
774 // FUNCTION EXPRESSION (R-JS-CLOSURE, rung 6): `function (params){body}` or named
775 // `function name(params){body}` in expression/primary position -- so `var f =
776 // function(x){...}` and `(function(){...})()` parse. Same ND_FUNC_DECL shape as a
777 // declaration (the evaluator builds a closure when it EVALUATES this node).
778 if jp_is_kw(ctx, "new\x00" as *u8) == 1 { return jp_parse_new(ctx) }
779 // `async function(){}` EXPRESSION -- the expression twin of the declaration case in
780 // jp_parse_stmt; same contextual consume-or-restore.
781 var easync: i64 = jp_is_lex(ctx, JS_TOK_IDENT, "async\x00" as *u8)
782 if jp_is_kw(ctx, "async\x00" as *u8) == 1 { easync = 1 }
783 if easync == 1 {
784 let esave: i64 = jp_cur(ctx)
785 jp_set_cur(ctx, esave + 1)
786 if jp_is_kw(ctx, "function\x00" as *u8) == 1 { return jp_parse_func_expr(ctx) }
787 jp_set_cur(ctx, esave)
788 }
789 if jp_is_kw(ctx, "function\x00" as *u8) == 1 { return jp_parse_func_expr(ctx) }
790 // any other keyword in expression position is a syntax error (rung 1)
791 return jp_error_node(ctx)
792 }
793 if k == JS_TOK_PUNCT {
794 if jp_is_punct(ctx, "(\x00" as *u8) == 1 {
795 jp_advance(ctx)
796 let inner: i64 = jp_parse_expr(ctx)
797 if jp_eat_punct(ctx, ")\x00" as *u8) == 0 { return jp_error_node(ctx) }
798 return inner
799 }
800 // array literal in expression position: [ ... ]
801 if jp_is_punct(ctx, "[\x00" as *u8) == 1 { return jp_parse_array(ctx) }
802 // object literal in expression position: { ... } (statement-leading '{' is a
803 // BLOCK and never reaches here -- jp_parse_stmt dispatches it before expr parsing).
804 if jp_is_punct(ctx, "{\x00" as *u8) == 1 { return jp_parse_object(ctx) }
805 return jp_error_node(ctx)
806 }
807 // EOF / ERROR token / anything else -> error
808 return jp_error_node(ctx)
809}
810
811// postfix: left-assoc chain of .ident [expr] (args...) applied to a primary
812func jp_parse_postfix(ctx: *i64) -> i64 {
813 var node: i64 = jp_parse_primary(ctx)
814 var go: i64 = 1
815 while go == 1 {
816 if jp_err(ctx) == 1 { go = 0 }
817 if go == 1 {
818 // OPTIONAL CHAINING ?. -- dispatch on what follows: ?.ident (member) / ?.( (call) / ?.[ (index).
819 // vk: `window.CSS?.supports?.(...)`. ND_MEMBER/ND_INDEX/ND_CALL carry extra=1 => short-circuit on nullish.
820 if jp_is_punct(ctx, "?.\x00" as *u8) == 1 {
821 jp_advance(ctx) // consume ?.
822 if jp_is_punct(ctx, "(\x00" as *u8) == 1 {
823 node = jp_parse_call_tail(ctx, node, 1)
824 if jp_err(ctx) == 1 { go = 0 }
825 } else { if jp_is_punct(ctx, "[\x00" as *u8) == 1 {
826 jp_advance(ctx)
827 let ix2: i64 = jp_parse_expr(ctx)
828 if jp_eat_punct(ctx, "]\x00" as *u8) == 0 { node = jp_error_node(ctx); go = 0 }
829 if go == 1 { node = jp_new_node(ctx, ND_INDEX, node, ix2, -1, -1, 1) }
830 } else {
831 if jp_tok_kind(ctx) != JS_TOK_IDENT { if jp_tok_kind(ctx) != JS_TOK_KEYWORD { node = jp_error_node(ctx); go = 0 } }
832 if go == 1 {
833 let prop2: i64 = jp_new_node(ctx, ND_IDENT, -1, -1, -1, jp_cur(ctx), 0)
834 jp_advance(ctx)
835 node = jp_new_node(ctx, ND_MEMBER, node, prop2, -1, -1, 1)
836 }
837 } }
838 } else {
839 // member: .ident
840 var isdot: i64 = 0
841 if jp_is_punct(ctx, ".\x00" as *u8) == 1 { isdot = 1 }
842 if isdot == 1 {
843 jp_advance(ctx)
844 if jp_tok_kind(ctx) != JS_TOK_IDENT {
845 if jp_tok_kind(ctx) != JS_TOK_KEYWORD { node = jp_error_node(ctx); go = 0 }
846 }
847 if go == 1 {
848 let prop: i64 = jp_new_node(ctx, ND_IDENT, -1, -1, -1, jp_cur(ctx), 0)
849 jp_advance(ctx)
850 node = jp_new_node(ctx, ND_MEMBER, node, prop, -1, -1, 0)
851 }
852 } else {
853 // computed index: [expr]
854 if jp_is_punct(ctx, "[\x00" as *u8) == 1 {
855 jp_advance(ctx)
856 let ix: i64 = jp_parse_expr(ctx)
857 if jp_eat_punct(ctx, "]\x00" as *u8) == 0 { node = jp_error_node(ctx); go = 0 }
858 if go == 1 { node = jp_new_node(ctx, ND_INDEX, node, ix, -1, -1, 0) }
859 } else {
860 // call: ( args... )
861 if jp_is_punct(ctx, "(\x00" as *u8) == 1 {
862 node = jp_parse_call_tail(ctx, node, 0)
863 if jp_err(ctx) == 1 { go = 0 }
864 } else {
865 go = 0
866 }
867 }
868 }
869 }
870 }
871 }
872 // postfix increment/decrement: x++ / x-- (binds to the WHOLE member/index chain, one only).
873 // Valid only on a simple lvalue; `5++` stays un-consumed here and errors upstream.
874 // (ASI note: `a \n ++b` is treated as `a++ ... b` here -- line-agnostic; minified
875 // bundles have no newlines, named divergence from the spec's restricted production.)
876 if jp_err(ctx) == 0 {
877 if jp_is_lvalue(ctx, node) == 1 {
878 if jp_is_punct(ctx, "++\x00" as *u8) == 1 { jp_advance(ctx); node = jp_new_node(ctx, ND_UPDATE, node, -1, -1, -1, 3) }
879 else { if jp_is_punct(ctx, "--\x00" as *u8) == 1 { jp_advance(ctx); node = jp_new_node(ctx, ND_UPDATE, node, -1, -1, -1, 4) } }
880 }
881 }
882 return node
883}
884
885// parse `( a , b , ... )` after the callee; build a CALL node whose
886// a=callee, b=child_start, c=child_count (args live in the child arena).
887// Args are buffered in local scratch then committed CONTIGUOUSLY (a nested call
888// inside an arg pushes into the shared arena mid-parse -- see jp_child_commit).
889func jp_parse_call_tail(ctx: *i64, callee: i64, opt: i64) -> i64 { // opt=1 -> optional call a?.(args)
890 jp_advance(ctx) // consume '('
891 let scratch: *i64 = sys_mmap(256 * 8) as *i64
892 var count: i64 = 0
893 // empty arg list?
894 if jp_is_punct(ctx, ")\x00" as *u8) == 1 {
895 jp_advance(ctx)
896 let start0: i64 = jp_child_commit(ctx, scratch, 0)
897 return jp_new_node(ctx, ND_CALL, callee, start0, 0, -1, opt)
898 }
899 var go: i64 = 1
900 while go == 1 {
901 var arg: i64 = 0
902 if jp_is_punct(ctx, "...\x00" as *u8) == 1 { // spread argument f(...xs)
903 jp_advance(ctx)
904 let inner: i64 = jp_parse_assign(ctx)
905 arg = jp_new_node(ctx, ND_SPREAD, inner, -1, -1, -1, 0)
906 } else {
907 arg = jp_parse_assign(ctx)
908 }
909 if count < 256 { scratch[count] = arg }
910 count = count + 1
911 if jp_err(ctx) == 1 { go = 0 }
912 if go == 1 {
913 if jp_eat_punct(ctx, ",\x00" as *u8) == 1 {
914 // continue to next arg
915 } else {
916 if jp_eat_punct(ctx, ")\x00" as *u8) == 1 { go = 0 }
917 else { jp_set_err(ctx); go = 0 }
918 }
919 }
920 }
921 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
922 let start: i64 = jp_child_commit(ctx, scratch, count)
923 return jp_new_node(ctx, ND_CALL, callee, start, count, -1, opt)
924}
925
926// is a node a valid update/assignment TARGET (simple lvalue)?
927// (jp_is_lvalue is defined ONCE, earlier in this file at :367. A byte-identical second
928// definition -- differing only in its parameter NAME (node vs idx) -- used to sit here,
929// the classic copy-paste/merge artefact. nx_cc accepted the redefinition silently and
930// picked a winner; removed 2026-07-31 with debt 1785447657.)
931// unary prefix: ! - + typeof ++ -- (right-recursive onto another unary)
932func jp_parse_unary(ctx: *i64) -> i64 {
933 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
934 // `yield [expr]` / `yield* expr` -- expression-position, lowest-binding-ish; operand optional
935 // (bare `yield` before ; ) } , ] is valid). Parsed everywhere for bundle compatibility (strict
936 // contextual legality is the engine's later concern, not the frontier's).
937 // `await expr` -- phase 3 stub: evaluate the operand and use its VALUE (exact for already-settled
938 // and synchronous values; true suspension rides the existing event loop -- declared later rung).
939 // Contextual like `async`: only when an expression actually follows (a bare `await` identifier
940 // before ) , ; ] } = stays an identifier).
941 var awk: i64 = 0
942 if jp_is_kw(ctx, "await\x00" as *u8) == 1 { awk = 1 }
943 if jp_is_lex(ctx, JS_TOK_IDENT, "await\x00" as *u8) == 1 { awk = 1 }
944 if awk == 1 {
945 let nkw: i64 = jp_kind_at(ctx, jp_cur(ctx) + 1)
946 var starts: i64 = 0
947 if nkw == JS_TOK_IDENT { starts = 1 }
948 if nkw == JS_TOK_NUMBER { starts = 1 }
949 if nkw == JS_TOK_STRING { starts = 1 }
950 if nkw == JS_TOK_KEYWORD { starts = 1 }
951 if jp_punct_at(ctx, jp_cur(ctx) + 1, "(\x00" as *u8) == 1 { starts = 1 }
952 if jp_punct_at(ctx, jp_cur(ctx) + 1, "[\x00" as *u8) == 1 { starts = 1 }
953 if starts == 1 { jp_advance(ctx); return jp_parse_unary(ctx) }
954 }
955 if jp_is_kw(ctx, "yield\x00" as *u8) == 1 {
956 jp_advance(ctx)
957 if jp_is_punct(ctx, "*\x00" as *u8) == 1 { jp_advance(ctx) }
958 var yop: i64 = 0 - 1
959 var bare: i64 = 0
960 if jp_is_punct(ctx, ";\x00" as *u8) == 1 { bare = 1 }
961 if jp_is_punct(ctx, ")\x00" as *u8) == 1 { bare = 1 }
962 if jp_is_punct(ctx, "}\x00" as *u8) == 1 { bare = 1 }
963 if jp_is_punct(ctx, ",\x00" as *u8) == 1 { bare = 1 }
964 if jp_is_punct(ctx, "]\x00" as *u8) == 1 { bare = 1 }
965 if bare == 0 { yop = jp_parse_assign(ctx) }
966 return jp_new_node(ctx, ND_YIELD, yop, -1, -1, -1, 0)
967 }
968 // prefix increment/decrement: ++x / --x ("++"/"--" are single lexer tokens, so the
969 // "+"/"-" checks below can never shadow them). Target must be a simple lvalue
970 // (IDENT/MEMBER/INDEX) -- `++5` is a SyntaxError in real JS, ERROR node here.
971 if jp_is_punct(ctx, "++\x00" as *u8) == 1 {
972 jp_advance(ctx)
973 let operand: i64 = jp_parse_unary(ctx)
974 if jp_is_lvalue(ctx, operand) == 0 { jp_set_err(ctx); return jp_error_node(ctx) }
975 return jp_new_node(ctx, ND_UPDATE, operand, -1, -1, -1, 1)
976 }
977 if jp_is_punct(ctx, "--\x00" as *u8) == 1 {
978 jp_advance(ctx)
979 let operand: i64 = jp_parse_unary(ctx)
980 if jp_is_lvalue(ctx, operand) == 0 { jp_set_err(ctx); return jp_error_node(ctx) }
981 return jp_new_node(ctx, ND_UPDATE, operand, -1, -1, -1, 2)
982 }
983 if jp_is_punct(ctx, "!\x00" as *u8) == 1 {
984 jp_advance(ctx)
985 let operand: i64 = jp_parse_unary(ctx)
986 return jp_new_node(ctx, ND_UNARY, operand, -1, -1, -1, OP_NOT)
987 }
988 if jp_is_punct(ctx, "~\x00" as *u8) == 1 {
989 jp_advance(ctx)
990 let operand: i64 = jp_parse_unary(ctx)
991 return jp_new_node(ctx, ND_UNARY, operand, -1, -1, -1, OP_BNOT)
992 }
993 if jp_is_punct(ctx, "-\x00" as *u8) == 1 {
994 jp_advance(ctx)
995 let operand: i64 = jp_parse_unary(ctx)
996 return jp_new_node(ctx, ND_UNARY, operand, -1, -1, -1, OP_NEG)
997 }
998 if jp_is_punct(ctx, "+\x00" as *u8) == 1 {
999 jp_advance(ctx)
1000 let operand: i64 = jp_parse_unary(ctx)
1001 return jp_new_node(ctx, ND_UNARY, operand, -1, -1, -1, OP_POS)
1002 }
1003 if jp_is_kw(ctx, "typeof\x00" as *u8) == 1 {
1004 jp_advance(ctx)
1005 let operand: i64 = jp_parse_unary(ctx)
1006 return jp_new_node(ctx, ND_UNARY, operand, -1, -1, -1, OP_TYPEOF)
1007 }
1008 if jp_is_kw(ctx, "void\x00" as *u8) == 1 {
1009 jp_advance(ctx)
1010 let operand: i64 = jp_parse_unary(ctx)
1011 return jp_new_node(ctx, ND_UNARY, operand, -1, -1, -1, OP_VOID)
1012 }
1013 if jp_is_kw(ctx, "delete\x00" as *u8) == 1 {
1014 jp_advance(ctx)
1015 let operand: i64 = jp_parse_unary(ctx) // a MEMBER/INDEX ref; the evaluator inspects the node, not its value
1016 return jp_new_node(ctx, ND_UNARY, operand, -1, -1, -1, OP_DELETE)
1017 }
1018 return jp_parse_postfix(ctx)
1019}
1020
1021// ---- binary operator tier helper: returns the op-code for the current token at a
1022// given precedence tier, or 0 if the current token is not an op of that tier.
1023// Tiers are checked by dedicated functions to keep else-if nesting shallow. ----
1024
1025// tier 1 (highest binary): * / %
1026func jp_mul_op(ctx: *i64) -> i64 {
1027 if jp_tok_kind(ctx) != JS_TOK_PUNCT { return 0 }
1028 if jp_is_punct(ctx, "*\x00" as *u8) == 1 { return OP_MUL }
1029 if jp_is_punct(ctx, "/\x00" as *u8) == 1 { return OP_DIV }
1030 if jp_is_punct(ctx, "%\x00" as *u8) == 1 { return OP_MOD }
1031 return 0
1032}
1033// tier 2: + -
1034func jp_add_op(ctx: *i64) -> i64 {
1035 if jp_tok_kind(ctx) != JS_TOK_PUNCT { return 0 }
1036 if jp_is_punct(ctx, "+\x00" as *u8) == 1 { return OP_ADD }
1037 if jp_is_punct(ctx, "-\x00" as *u8) == 1 { return OP_SUB }
1038 return 0
1039}
1040// tier 3: < <= > >= (NOTE: must check the 2-char forms first via lexeme compare;
1041// the lexer already produced "<=" as ONE punct token, so exact-match is safe.)
1042static jp_noin: i64 // 1 while parsing a for-init EXPRESSION: `in` is the for-in separator, NOT the operator
1043func jp_rel_op(ctx: *i64) -> i64 {
1044 // `instanceof`/`in` are KEYWORD tokens (not punct) at the SAME precedence tier as < > (real JS RelationalExpr).
1045 if jp_is_kw(ctx, "instanceof\x00" as *u8) == 1 { return OP_INSTANCEOF }
1046 if jp_is_kw(ctx, "in\x00" as *u8) == 1 { if jp_noin == 0 { return OP_IN } return 0 }
1047 if jp_tok_kind(ctx) != JS_TOK_PUNCT { return 0 }
1048 if jp_is_punct(ctx, "<=\x00" as *u8) == 1 { return OP_LE }
1049 if jp_is_punct(ctx, ">=\x00" as *u8) == 1 { return OP_GE }
1050 if jp_is_punct(ctx, "<\x00" as *u8) == 1 { return OP_LT }
1051 if jp_is_punct(ctx, ">\x00" as *u8) == 1 { return OP_GT }
1052 return 0
1053}
1054// tier 4: == != === !==
1055func jp_eq_op(ctx: *i64) -> i64 {
1056 if jp_tok_kind(ctx) != JS_TOK_PUNCT { return 0 }
1057 if jp_is_punct(ctx, "===\x00" as *u8) == 1 { return OP_SEQ }
1058 if jp_is_punct(ctx, "!==\x00" as *u8) == 1 { return OP_SNE }
1059 if jp_is_punct(ctx, "==\x00" as *u8) == 1 { return OP_EQ }
1060 if jp_is_punct(ctx, "!=\x00" as *u8) == 1 { return OP_NE }
1061 return 0
1062}
1063
1064// Generic left-assoc binary builder is awkward without function pointers, so each
1065// tier is its own small function that loops while its op matches, recursing to the
1066// NEXT-HIGHER tier for each operand. Correct precedence = the recursion order;
1067// correct (left) associativity = the while-loop reassigning `left`.
1068
1069func jp_parse_mul(ctx: *i64) -> i64 {
1070 var left: i64 = jp_parse_unary(ctx)
1071 var go: i64 = 1
1072 while go == 1 {
1073 if jp_err(ctx) == 1 { go = 0 }
1074 if go == 1 {
1075 let op: i64 = jp_mul_op(ctx)
1076 if op == 0 { go = 0 } else {
1077 jp_advance(ctx)
1078 let right: i64 = jp_parse_unary(ctx)
1079 left = jp_new_node(ctx, ND_BINARY, left, right, -1, -1, op)
1080 }
1081 }
1082 }
1083 return left
1084}
1085func jp_parse_add(ctx: *i64) -> i64 {
1086 var left: i64 = jp_parse_mul(ctx)
1087 var go: i64 = 1
1088 while go == 1 {
1089 if jp_err(ctx) == 1 { go = 0 }
1090 if go == 1 {
1091 let op: i64 = jp_add_op(ctx)
1092 if op == 0 { go = 0 } else {
1093 jp_advance(ctx)
1094 let right: i64 = jp_parse_mul(ctx)
1095 left = jp_new_node(ctx, ND_BINARY, left, right, -1, -1, op)
1096 }
1097 }
1098 }
1099 return left
1100}
1101// SHIFT: << >> >>> -- precedence between additive and relational (JS). Distinct lexer tokens, so no
1102// confusion with < / > (relational). Uses variable shift in eval (immediate-shift lexer gotcha).
1103func jp_parse_shift(ctx: *i64) -> i64 {
1104 var left: i64 = jp_parse_add(ctx)
1105 var go: i64 = 1
1106 while go == 1 {
1107 if jp_err(ctx) == 1 { go = 0 }
1108 if go == 1 {
1109 var op: i64 = 0
1110 if jp_is_punct(ctx, "<<\x00" as *u8) == 1 { op = OP_SHL }
1111 if jp_is_punct(ctx, ">>>\x00" as *u8) == 1 { op = OP_USHR }
1112 if op == 0 { if jp_is_punct(ctx, ">>\x00" as *u8) == 1 { op = OP_SHR } }
1113 if op == 0 { go = 0 } else {
1114 jp_advance(ctx)
1115 let right: i64 = jp_parse_add(ctx)
1116 left = jp_new_node(ctx, ND_BINARY, left, right, -1, -1, op)
1117 }
1118 }
1119 }
1120 return left
1121}
1122func jp_parse_rel(ctx: *i64) -> i64 {
1123 var left: i64 = jp_parse_shift(ctx)
1124 var go: i64 = 1
1125 while go == 1 {
1126 if jp_err(ctx) == 1 { go = 0 }
1127 if go == 1 {
1128 let op: i64 = jp_rel_op(ctx)
1129 if op == 0 { go = 0 } else {
1130 jp_advance(ctx)
1131 let right: i64 = jp_parse_shift(ctx)
1132 left = jp_new_node(ctx, ND_BINARY, left, right, -1, -1, op)
1133 }
1134 }
1135 }
1136 return left
1137}
1138func jp_parse_eq(ctx: *i64) -> i64 {
1139 var left: i64 = jp_parse_rel(ctx)
1140 var go: i64 = 1
1141 while go == 1 {
1142 if jp_err(ctx) == 1 { go = 0 }
1143 if go == 1 {
1144 let op: i64 = jp_eq_op(ctx)
1145 if op == 0 { go = 0 } else {
1146 jp_advance(ctx)
1147 let right: i64 = jp_parse_rel(ctx)
1148 left = jp_new_node(ctx, ND_BINARY, left, right, -1, -1, op)
1149 }
1150 }
1151 }
1152 return left
1153}
1154// BITWISE AND / XOR / OR -- precedence & > ^ > | , all below equality and above && (JS). Single-char
1155// lexer tokens, never confused with && / || (2-char tokens).
1156func jp_parse_bitand(ctx: *i64) -> i64 {
1157 var left: i64 = jp_parse_eq(ctx)
1158 var go: i64 = 1
1159 while go == 1 {
1160 if jp_err(ctx) == 1 { go = 0 }
1161 if go == 1 {
1162 if jp_is_punct(ctx, "&\x00" as *u8) == 1 {
1163 jp_advance(ctx)
1164 let right: i64 = jp_parse_eq(ctx)
1165 left = jp_new_node(ctx, ND_BINARY, left, right, -1, -1, OP_BAND)
1166 } else { go = 0 }
1167 }
1168 }
1169 return left
1170}
1171func jp_parse_bitxor(ctx: *i64) -> i64 {
1172 var left: i64 = jp_parse_bitand(ctx)
1173 var go: i64 = 1
1174 while go == 1 {
1175 if jp_err(ctx) == 1 { go = 0 }
1176 if go == 1 {
1177 if jp_is_punct(ctx, "^\x00" as *u8) == 1 {
1178 jp_advance(ctx)
1179 let right: i64 = jp_parse_bitand(ctx)
1180 left = jp_new_node(ctx, ND_BINARY, left, right, -1, -1, OP_BXOR)
1181 } else { go = 0 }
1182 }
1183 }
1184 return left
1185}
1186func jp_parse_bitor(ctx: *i64) -> i64 {
1187 var left: i64 = jp_parse_bitxor(ctx)
1188 var go: i64 = 1
1189 while go == 1 {
1190 if jp_err(ctx) == 1 { go = 0 }
1191 if go == 1 {
1192 if jp_is_punct(ctx, "|\x00" as *u8) == 1 {
1193 jp_advance(ctx)
1194 let right: i64 = jp_parse_bitxor(ctx)
1195 left = jp_new_node(ctx, ND_BINARY, left, right, -1, -1, OP_BOR)
1196 } else { go = 0 }
1197 }
1198 }
1199 return left
1200}
1201func jp_parse_and(ctx: *i64) -> i64 {
1202 var left: i64 = jp_parse_bitor(ctx)
1203 var go: i64 = 1
1204 while go == 1 {
1205 if jp_err(ctx) == 1 { go = 0 }
1206 if go == 1 {
1207 if jp_is_punct(ctx, "&&\x00" as *u8) == 1 {
1208 jp_advance(ctx)
1209 let right: i64 = jp_parse_bitor(ctx)
1210 left = jp_new_node(ctx, ND_BINARY, left, right, -1, -1, OP_AND)
1211 } else { go = 0 }
1212 }
1213 }
1214 return left
1215}
1216func jp_parse_or(ctx: *i64) -> i64 {
1217 var left: i64 = jp_parse_and(ctx)
1218 var go: i64 = 1
1219 while go == 1 {
1220 if jp_err(ctx) == 1 { go = 0 }
1221 if go == 1 {
1222 if jp_is_punct(ctx, "||\x00" as *u8) == 1 {
1223 jp_advance(ctx)
1224 let right: i64 = jp_parse_and(ctx)
1225 left = jp_new_node(ctx, ND_BINARY, left, right, -1, -1, OP_OR)
1226 } else { go = 0 }
1227 }
1228 }
1229 return left
1230}
1231
1232// compound-assignment operator code at the cursor (the BINARY op to fold in), or 0 if
1233// the current token is not a compound assign. The lexer produced "+=" "-=" etc. as
1234// SINGLE multi-char punct tokens (js_punct_len), so an exact-lexeme compare is safe and
1235// can never be confused with bare '=' / '==' / '==='.
1236func jp_compound_op(ctx: *i64) -> i64 {
1237 if jp_tok_kind(ctx) != JS_TOK_PUNCT { return 0 }
1238 if jp_is_punct(ctx, "+=\x00" as *u8) == 1 { return OP_ADD }
1239 if jp_is_punct(ctx, "-=\x00" as *u8) == 1 { return OP_SUB }
1240 if jp_is_punct(ctx, "*=\x00" as *u8) == 1 { return OP_MUL }
1241 if jp_is_punct(ctx, "/=\x00" as *u8) == 1 { return OP_DIV }
1242 if jp_is_punct(ctx, "%=\x00" as *u8) == 1 { return OP_MOD }
1243 // bitwise/shift compounds (Octane Crypto/jsbn: `c >>= ...`). The lexer already emits these as single
1244 // puncts (js_punct_len), and the fold rides the SAME aop path as += (ev_apply_binop / BC_BINOP).
1245 if jp_is_punct(ctx, "&=\x00" as *u8) == 1 { return OP_BAND }
1246 if jp_is_punct(ctx, "|=\x00" as *u8) == 1 { return OP_BOR }
1247 if jp_is_punct(ctx, "^=\x00" as *u8) == 1 { return OP_BXOR }
1248 if jp_is_punct(ctx, "<<=\x00" as *u8) == 1 { return OP_SHL }
1249 if jp_is_punct(ctx, ">>=\x00" as *u8) == 1 { return OP_SHR }
1250 if jp_is_punct(ctx, ">>>=\x00" as *u8) == 1 { return OP_USHR }
1251 return 0
1252}
1253
1254// ternary `cond ? then : else` -- RIGHT-assoc, just ABOVE assignment, just BELOW the
1255// OR tier (real JS: ConditionalExpression). The branches are assignment-expressions
1256// (so `a ? b : c = d` parses the false branch as an assignment, real JS). Returns the
1257// OR-level node unchanged when no '?' follows, so all lower KATs are untouched.
1258// nullish coalescing `a ?? b` -- short-circuit; tier just BELOW ternary, ABOVE ||. Operands are
1259// OR-level expressions (the common `a ?? b` parses correctly; ?? mixing with ||/&& needs parens in real JS).
1260func jp_parse_nullish(ctx: *i64) -> i64 {
1261 var left: i64 = jp_parse_or(ctx)
1262 var go: i64 = 1
1263 while go == 1 {
1264 if jp_err(ctx) == 1 { go = 0 }
1265 if go == 1 {
1266 if jp_is_punct(ctx, "??\x00" as *u8) == 1 {
1267 jp_advance(ctx)
1268 let right: i64 = jp_parse_or(ctx)
1269 left = jp_new_node(ctx, ND_BINARY, left, right, -1, -1, OP_NULLISH)
1270 } else { go = 0 }
1271 }
1272 }
1273 return left
1274}
1275func jp_parse_ternary(ctx: *i64) -> i64 {
1276 let cond: i64 = jp_parse_nullish(ctx)
1277 if jp_err(ctx) == 1 { return cond }
1278 if jp_is_punct(ctx, "?\x00" as *u8) == 1 {
1279 jp_advance(ctx)
1280 let then_e: i64 = jp_parse_assign(ctx) // then-branch is an assignment-expr
1281 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1282 if jp_eat_punct(ctx, ":\x00" as *u8) == 0 { return jp_error_node(ctx) }
1283 let else_e: i64 = jp_parse_assign(ctx) // else-branch right-recurses (right-assoc)
1284 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1285 return jp_new_node(ctx, ND_TERNARY, cond, then_e, else_e, -1, 0)
1286 }
1287 return cond
1288}
1289
1290// ===================== arrow functions (R-JS-CLOSURE, rung 6) =====================
1291// An arrow desugars to the SAME node shape as a function decl/expression -- ND_FUNC_DECL
1292// (a=name|-1, b=params, c=body BLOCK) -- so the evaluator's ONE js_call_userfn path runs
1293// both. Arrows are anonymous (name = -1). The lexical capture happens in the evaluator
1294// (the closure records the env where the literal is evaluated); the parser only produces
1295// the same params+body skeleton, so an arrow is lexically identical to `function(){}`.
1296
1297// wrap a single statement node into a synthetic BLOCK list-node (a=child_start, b=1).
1298// Used for an arrow EXPRESSION body: `x => expr` desugars to a body `{ return expr; }`.
1299func jp_wrap_block1(ctx: *i64, stmt: i64) -> i64 {
1300 let start: i64 = jp_child_push(ctx, stmt)
1301 return jp_new_node(ctx, ND_BLOCK, start, 1, -1, -1, 0)
1302}
1303
1304// parse a PARENTHESIZED arrow param list `( a , b , ... )` (idents only; empty () ok)
1305// -> ND_PARAMS. Identical shape to jp_parse_params (reused), kept as its own call so the
1306// arrow path reads clearly. Default/rest/destructured params are a NAMED OPEN (idents only).
1307func jp_parse_arrow_params(ctx: *i64) -> i64 { return jp_parse_params(ctx) }
1308
1309// parse an arrow BODY after the `=>` has been consumed: a BLOCK body `{ stmts }` runs as-is;
1310// an EXPRESSION body `expr` desugars to `{ return expr; }` (the implicit-return rule). The
1311// expression body is an ASSIGNMENT-expr (so `x => y = 1` parses `y=1` as the body, real JS).
1312func jp_parse_arrow_body(ctx: *i64) -> i64 {
1313 if jp_is_punct(ctx, "{\x00" as *u8) == 1 { return jp_parse_block(ctx) }
1314 let e: i64 = jp_parse_assign(ctx)
1315 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1316 let ret: i64 = jp_new_node(ctx, ND_RETURN, e, -1, -1, -1, 0)
1317 return jp_wrap_block1(ctx, ret)
1318}
1319
1320// SINGLE-PARAM arrow `x => body`. Cursor is on the IDENT (its next token is '=>').
1321func jp_parse_arrow_single(ctx: *i64) -> i64 {
1322 let pn: i64 = jp_new_node(ctx, ND_IDENT, -1, -1, -1, jp_cur(ctx), 0)
1323 jp_advance(ctx) // consume the param ident
1324 let start: i64 = jp_child_push(ctx, pn)
1325 let params: i64 = jp_new_node(ctx, ND_PARAMS, start, 1, -1, -1, 0)
1326 if jp_eat_punct(ctx, "=>\x00" as *u8) == 0 { return jp_error_node(ctx) }
1327 let body: i64 = jp_parse_arrow_body(ctx)
1328 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1329 return jp_new_node(ctx, ND_FUNC_DECL, -1, params, body, -1, 0) // name=-1 (anonymous)
1330}
1331
1332// PAREN-LIST arrow `( a, b ) => body`. Cursor is on '(' (jp_arrow_paren_ahead confirmed
1333// a following '=>'). Parse the param idents, eat '=>', parse the body.
1334func jp_parse_arrow_paren(ctx: *i64) -> i64 {
1335 let params: i64 = jp_parse_arrow_params(ctx)
1336 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1337 if jp_eat_punct(ctx, "=>\x00" as *u8) == 0 { return jp_error_node(ctx) }
1338 let body: i64 = jp_parse_arrow_body(ctx)
1339 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1340 return jp_new_node(ctx, ND_FUNC_DECL, -1, params, body, -1, 0)
1341}
1342
1343// 1 iff an ARROW function begins at the cursor: `x =>` (IDENT then '=>') or `( ... ) =>`
1344// (the cover-grammar paren case resolved by jp_arrow_paren_ahead). Used to fork at the
1345// TOP of jp_parse_assign so the arrow's params are not first mis-parsed as an expression.
1346func jp_arrow_ahead(ctx: *i64) -> i64 {
1347 if jp_tok_kind(ctx) == JS_TOK_IDENT {
1348 if jp_punct_at(ctx, jp_cur(ctx) + 1, "=>\x00" as *u8) == 1 { return 1 }
1349 }
1350 if jp_is_punct(ctx, "(\x00" as *u8) == 1 {
1351 if jp_arrow_paren_ahead(ctx) == 1 { return 1 }
1352 }
1353 return 0
1354}
1355
1356// assignment: RIGHT-associative, LOWEST precedence. `lhs = rhs` (or `lhs += rhs` etc.)
1357// where rhs is itself an assignment (so a=b=c nests right). lhs is whatever the ternary
1358// tier parsed. Compound assigns store the BINARY op-code in the ND_ASSIGN `extra` slot
1359// (0 = plain '='); the evaluator reads lhs, folds the op with rhs, writes back.
1360func jp_parse_assign(ctx: *i64) -> i64 {
1361 // ASYNC ARROWS `async (a,b)=>{…}` / `async x=>…` (JS-SOTA phase 3; the netflix frontier after
1362 // object spread: `a=e=>async(t,n)=>{…}`). `async` is CONTEXTUAL -- `async(1)` is a CALL to a
1363 // function named async -- so only consume it when a real arrow follows; otherwise RESTORE the
1364 // cursor and let it parse as a plain identifier. Async-ness itself is not yet modelled (the
1365 // body runs synchronously and returns its value, not a promise) -- an honest, declared stub.
1366 // `async` lexes as a KEYWORD (nx_js_lex reserves it), so an IDENT-only test here NEVER fired --
1367 // async arrows were unparseable the whole time this branch existed. Same both-kinds fix as the
1368 // `async function` forms (2026-08-25), found by walking the real shipped page byte 254,078:
1369 // WebAssembly.instantiate(bytes).then(async r=>{...}).
1370 var aasync: i64 = jp_is_lex(ctx, JS_TOK_IDENT, "async\x00" as *u8)
1371 if jp_is_kw(ctx, "async\x00" as *u8) == 1 { aasync = 1 }
1372 if aasync == 1 {
1373 let asave: i64 = jp_cur(ctx)
1374 jp_set_cur(ctx, asave + 1)
1375 var aok: i64 = 0
1376 if jp_is_punct(ctx, "(\x00" as *u8) == 1 { if jp_arrow_paren_ahead(ctx) == 1 { aok = 1 } }
1377 if jp_kind_at(ctx, asave + 1) == JS_TOK_IDENT { if jp_punct_at(ctx, asave + 2, "=>\x00" as *u8) == 1 { aok = 1 } }
1378 if aok == 0 { jp_set_cur(ctx, asave) }
1379 }
1380 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1381 // ARROW FUNCTION fork (lowest precedence, right-assoc): detect `x =>` / `(...) =>`
1382 // BEFORE treating the head as an expression. The body recurses through assignment
1383 // (jp_parse_arrow_body), so `x => y => x+y` (curried) right-nests naturally.
1384 if jp_arrow_ahead(ctx) == 1 {
1385 if jp_tok_kind(ctx) == JS_TOK_IDENT { return jp_parse_arrow_single(ctx) }
1386 return jp_parse_arrow_paren(ctx)
1387 }
1388 let lhs: i64 = jp_parse_ternary(ctx)
1389 if jp_err(ctx) == 1 { return lhs }
1390 // bare '=' -- must NOT match == or === (DISTINCT multi-char punct tokens), so an
1391 // exact-lexeme compare against "=" is unambiguous.
1392 if jp_is_punct(ctx, "=\x00" as *u8) == 1 {
1393 // Real JS: the assignment target MUST be an lvalue (IDENT / MEMBER / INDEX).
1394 // '1 = 2' or '(a+b) = c' -> SyntaxError: Invalid left-hand side in assignment.
1395 if jp_is_lvalue(ctx, lhs) == 0 { return jp_error_node(ctx) }
1396 jp_advance(ctx)
1397 let rhs: i64 = jp_parse_assign(ctx) // right-recursion = right associativity
1398 return jp_new_node(ctx, ND_ASSIGN, lhs, rhs, -1, -1, 0)
1399 }
1400 // compound assign lhs (+= -= *= /= %=) rhs -> ND_ASSIGN with op in extra slot.
1401 let cop: i64 = jp_compound_op(ctx)
1402 if cop != 0 {
1403 if jp_is_lvalue(ctx, lhs) == 0 { return jp_error_node(ctx) }
1404 jp_advance(ctx)
1405 let rhs: i64 = jp_parse_assign(ctx)
1406 return jp_new_node(ctx, ND_ASSIGN, lhs, rhs, -1, -1, cop)
1407 }
1408 return lhs
1409}
1410
1411// top-level expression = assignment (which descends through all binary tiers).
1412func jp_parse_expr(ctx: *i64) -> i64 {
1413 let first: i64 = jp_parse_assign(ctx)
1414 if jp_err(ctx) == 1 { return first }
1415 if jp_is_punct(ctx, ",\x00" as *u8) == 0 { return first } // no comma = single expr (the common case, 0 overhead)
1416 // sequence operator `a, b, c` -> ND_SEQ (eval each in order, value = last). All jp_parse_expr callers are
1417 // sequence-valid contexts (for-header, (), stmt, cond, return, switch); list-commas use jp_parse_assign.
1418 let scratch: *i64 = sys_mmap(256 * 8) as *i64
1419 scratch[0] = first
1420 var count: i64 = 1
1421 while jp_is_punct(ctx, ",\x00" as *u8) == 1 {
1422 jp_advance(ctx)
1423 let e: i64 = jp_parse_assign(ctx)
1424 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1425 if count < 256 { scratch[count] = e }
1426 count = count + 1
1427 }
1428 let cs: i64 = jp_child_commit(ctx, scratch, count)
1429 return jp_new_node(ctx, ND_SEQ, cs, count, -1, -1, 0)
1430}
1431
1432// ===================== statement parsing =====================
1433// forward refs: jp_parse_stmt is mutually recursive with block/if/while.
1434
1435// var/let/const decl: KEYWORD IDENT [ = expr ] [ ; ]
1436// array/object destructuring PATTERNS for a var-decl target. Shorthand names only ([a,b] / {a,b});
1437// renaming {a:b}, defaults in patterns, and nested patterns are a NAMED OPEN.
1438func jp_parse_array_pattern(ctx: *i64) -> i64 {
1439 jp_advance(ctx) // '['
1440 let scratch: *i64 = sys_mmap(256 * 8) as *i64
1441 var count: i64 = 0
1442 var go: i64 = 1
1443 if jp_is_punct(ctx, "]\x00" as *u8) == 1 { jp_advance(ctx); go = 0 }
1444 while go == 1 {
1445 var nm: i64 = 0 - 1
1446 // NESTED patterns: [e, [t,n], {x}] -- vk uses `for(let [e,[t,n]] of ...)`.
1447 if jp_is_punct(ctx, "[\x00" as *u8) == 1 { nm = jp_parse_array_pattern(ctx); if jp_err(ctx) == 1 { go = 0 } }
1448 else { if jp_is_punct(ctx, "{\x00" as *u8) == 1 { nm = jp_parse_object_pattern(ctx); if jp_err(ctx) == 1 { go = 0 } }
1449 else { if jp_is_punct(ctx, "...\x00" as *u8) == 1 { jp_advance(ctx); if jp_tok_kind(ctx) == JS_TOK_IDENT { nm = jp_new_node(ctx, ND_IDENT, -1, -1, -1, jp_cur(ctx), 1); jp_advance(ctx) } else { jp_set_err(ctx); go = 0 } }
1450 else {
1451 if jp_tok_kind(ctx) != JS_TOK_IDENT { jp_set_err(ctx); go = 0 }
1452 if go == 1 { nm = jp_new_node(ctx, ND_IDENT, -1, -1, -1, jp_cur(ctx), 0); jp_advance(ctx) }
1453 } } }
1454 if go == 1 {
1455 if jp_is_punct(ctx, "=\x00" as *u8) == 1 { jp_advance(ctx); jp_parse_assign(ctx); if jp_err(ctx) == 1 { go = 0 } } // [a=1] default (parse+ignore)
1456 if go == 1 {
1457 if count < 256 { scratch[count] = nm }
1458 count = count + 1
1459 if jp_eat_punct(ctx, ",\x00" as *u8) == 1 { }
1460 else { if jp_eat_punct(ctx, "]\x00" as *u8) == 1 { go = 0 } else { jp_set_err(ctx); go = 0 } }
1461 }
1462 }
1463 }
1464 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1465 let start: i64 = jp_child_commit(ctx, scratch, count)
1466 return jp_new_node(ctx, ND_ARRAY_PAT, start, count, -1, -1, 0)
1467}
1468func jp_parse_object_pattern(ctx: *i64) -> i64 {
1469 jp_advance(ctx) // '{'
1470 let scratch: *i64 = sys_mmap(256 * 8) as *i64
1471 var count: i64 = 0
1472 var go: i64 = 1
1473 if jp_is_punct(ctx, "}\x00" as *u8) == 1 { jp_advance(ctx); go = 0 }
1474 while go == 1 {
1475 if jp_tok_kind(ctx) != JS_TOK_IDENT { jp_set_err(ctx); go = 0 }
1476 if go == 1 {
1477 let keytok: i64 = jp_cur(ctx) // source property KEY
1478 jp_advance(ctx)
1479 var alias: i64 = 0 - 1
1480 if jp_is_punct(ctx, ":\x00" as *u8) == 1 { // {key: localName} or {key: [a,b]} / {key: {x}} (nested)
1481 jp_advance(ctx)
1482 if jp_is_punct(ctx, "[\x00" as *u8) == 1 { alias = jp_parse_array_pattern(ctx); if jp_err(ctx) == 1 { go = 0 } }
1483 else { if jp_is_punct(ctx, "{\x00" as *u8) == 1 { alias = jp_parse_object_pattern(ctx); if jp_err(ctx) == 1 { go = 0 } }
1484 else { if jp_tok_kind(ctx) == JS_TOK_IDENT { alias = jp_new_node(ctx, ND_IDENT, -1, -1, -1, jp_cur(ctx), 0); jp_advance(ctx) }
1485 else { jp_set_err(ctx); go = 0 } } }
1486 }
1487 if go == 1 { if jp_is_punct(ctx, "=\x00" as *u8) == 1 { // {x = default} -- parse+ignore default (rare)
1488 jp_advance(ctx)
1489 jp_parse_assign(ctx)
1490 if jp_err(ctx) == 1 { go = 0 }
1491 } }
1492 if go == 1 {
1493 let nm: i64 = jp_new_node(ctx, ND_IDENT, alias, -1, -1, keytok, 0) // tok=KEY; slot-a=ALIAS node (-1 if bare)
1494 if count < 256 { scratch[count] = nm }
1495 count = count + 1
1496 if jp_eat_punct(ctx, ",\x00" as *u8) == 1 { }
1497 else { if jp_eat_punct(ctx, "}\x00" as *u8) == 1 { go = 0 } else { jp_set_err(ctx); go = 0 } }
1498 }
1499 }
1500 }
1501 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1502 let start: i64 = jp_child_commit(ctx, scratch, count)
1503 return jp_new_node(ctx, ND_OBJ_PAT, start, count, -1, -1, 0)
1504}
1505// parse ONE declarator `name [= init]` (no var keyword, no terminator) -> an ND_VAR_DECL node.
1506func jp_parse_one_declarator(ctx: *i64) -> i64 {
1507 var name: i64 = 0
1508 if jp_is_punct(ctx, "[\x00" as *u8) == 1 { name = jp_parse_array_pattern(ctx) }
1509 else { if jp_is_punct(ctx, "{\x00" as *u8) == 1 { name = jp_parse_object_pattern(ctx) }
1510 else {
1511 if jp_tok_kind(ctx) != JS_TOK_IDENT { return jp_error_node(ctx) }
1512 name = jp_new_node(ctx, ND_IDENT, -1, -1, -1, jp_cur(ctx), 0)
1513 jp_advance(ctx)
1514 } }
1515 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1516 var init: i64 = -1
1517 if jp_is_punct(ctx, "=\x00" as *u8) == 1 {
1518 jp_advance(ctx)
1519 init = jp_parse_assign(ctx)
1520 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1521 }
1522 return jp_new_node(ctx, ND_VAR_DECL, name, init, -1, -1, 0)
1523}
1524func jp_parse_var_decl(ctx: *i64) -> i64 {
1525 jp_advance(ctx) // consume var/let/const
1526 let first: i64 = jp_parse_one_declarator(ctx)
1527 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1528 // MULTI-declarator: `var a, b=c, d;` -> ND_VAR_LIST of ND_VAR_DECLs (real JS; very common).
1529 if jp_is_punct(ctx, ",\x00" as *u8) == 1 {
1530 let scratch: *i64 = sys_mmap(256 * 8) as *i64
1531 scratch[0] = first
1532 var count: i64 = 1
1533 while jp_is_punct(ctx, ",\x00" as *u8) == 1 {
1534 jp_advance(ctx)
1535 let d: i64 = jp_parse_one_declarator(ctx)
1536 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1537 if count < 256 { scratch[count] = d }
1538 count = count + 1
1539 }
1540 if jp_eat_punct(ctx, ";\x00" as *u8) == 0 { if jp_can_asi(ctx) == 0 { return jp_error_node(ctx) } }
1541 let cs: i64 = jp_child_commit(ctx, scratch, count)
1542 return jp_new_node(ctx, ND_VAR_LIST, cs, count, -1, -1, 0)
1543 }
1544 // single declarator: ASI-lite termination (explicit ';' OR line break / '}' / EOF).
1545 if jp_eat_punct(ctx, ";\x00" as *u8) == 0 {
1546 if jp_can_asi(ctx) == 0 { return jp_error_node(ctx) }
1547 }
1548 return first
1549}
1550
1551// return [ expr ] ;
1552func jp_parse_return(ctx: *i64) -> i64 {
1553 jp_advance(ctx) // consume `return`
1554 var expr: i64 = -1
1555 // bare `return;` or `return}` -> no expression
1556 var has_expr: i64 = 1
1557 if jp_is_punct(ctx, ";\x00" as *u8) == 1 { has_expr = 0 }
1558 if jp_is_punct(ctx, "}\x00" as *u8) == 1 { has_expr = 0 }
1559 if jp_tok_kind(ctx) == JS_TOK_EOF { has_expr = 0 }
1560 if has_expr == 1 {
1561 expr = jp_parse_expr(ctx)
1562 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1563 }
1564 // ASI-lite termination: explicit ';' OR a line break / '}' / EOF.
1565 if jp_eat_punct(ctx, ";\x00" as *u8) == 0 {
1566 if jp_can_asi(ctx) == 0 { return jp_error_node(ctx) }
1567 }
1568 return jp_new_node(ctx, ND_RETURN, expr, -1, -1, -1, 0)
1569}
1570
1571// block: { stmt* } -> list-node a=child_start, b=child_count
1572func jp_parse_block(ctx: *i64) -> i64 {
1573 jp_advance(ctx) // consume '{'
1574 let scratch: *i64 = sys_mmap(ND_MAGIC_2048 * 8) as *i64
1575 var count: i64 = 0
1576 var go: i64 = 1
1577 while go == 1 {
1578 if jp_err(ctx) == 1 { go = 0 }
1579 if go == 1 {
1580 if jp_is_punct(ctx, "}\x00" as *u8) == 1 { jp_advance(ctx); go = 0 }
1581 else {
1582 if jp_tok_kind(ctx) == JS_TOK_EOF { jp_set_err(ctx); go = 0 }
1583 else {
1584 let st: i64 = jp_parse_stmt(ctx)
1585 if count < ND_MAGIC_2048 { scratch[count] = st }
1586 count = count + 1
1587 }
1588 }
1589 }
1590 }
1591 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1592 let start: i64 = jp_child_commit(ctx, scratch, count)
1593 return jp_new_node(ctx, ND_BLOCK, start, count, -1, -1, 0)
1594}
1595
1596// if ( cond ) stmt [ else stmt ]
1597func jp_parse_if(ctx: *i64) -> i64 {
1598 jp_advance(ctx) // consume `if`
1599 if jp_eat_punct(ctx, "(\x00" as *u8) == 0 { return jp_error_node(ctx) }
1600 let cond: i64 = jp_parse_expr(ctx)
1601 if jp_eat_punct(ctx, ")\x00" as *u8) == 0 { return jp_error_node(ctx) }
1602 let then_s: i64 = jp_parse_stmt(ctx)
1603 var else_s: i64 = -1
1604 if jp_is_kw(ctx, "else\x00" as *u8) == 1 {
1605 jp_advance(ctx)
1606 else_s = jp_parse_stmt(ctx)
1607 }
1608 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1609 return jp_new_node(ctx, ND_IF, cond, then_s, else_s, -1, 0)
1610}
1611
1612// while ( cond ) stmt
1613func jp_parse_while(ctx: *i64) -> i64 {
1614 jp_advance(ctx) // consume `while`
1615 if jp_eat_punct(ctx, "(\x00" as *u8) == 0 { return jp_error_node(ctx) }
1616 let cond: i64 = jp_parse_expr(ctx)
1617 if jp_eat_punct(ctx, ")\x00" as *u8) == 0 { return jp_error_node(ctx) }
1618 let body: i64 = jp_parse_stmt(ctx)
1619 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1620 return jp_new_node(ctx, ND_WHILE, cond, body, -1, -1, 0)
1621}
1622
1623// for ( init ; cond ; update ) body -> ND_FOR. Slots: a=init, b=cond, c=update,
1624// tokidx-slot[4]=body. init may be a var-decl OR an expression OR empty; cond/update may
1625// be empty (empty cond = perpetually true, handled in the evaluator). We parse init WITHOUT
1626// consuming the ';' inside the init-decl/expr path so the for-header ';' separators stay
1627// uniform here. (for-in / for-of need iterator protocol -> HONEST OPEN, not parsed here:
1628// after the init clause we REQUIRE a ';'.)
1629func jp_parse_for(ctx: *i64) -> i64 {
1630 jp_advance(ctx) // consume `for`
1631 if jp_eat_punct(ctx, "(\x00" as *u8) == 0 { return jp_error_node(ctx) }
1632 // ---- init clause ---- (empty if the next token is ';')
1633 var init: i64 = -1
1634 if jp_is_punct(ctx, ";\x00" as *u8) == 1 {
1635 jp_advance(ctx)
1636 } else {
1637 var is_decl: i64 = 0
1638 if jp_is_kw(ctx, "var\x00" as *u8) == 1 { is_decl = 1 }
1639 if jp_is_kw(ctx, "let\x00" as *u8) == 1 { is_decl = 1 }
1640 if jp_is_kw(ctx, "const\x00" as *u8) == 1 { is_decl = 1 }
1641 if is_decl == 1 { init = jp_parse_for_decl(ctx) }
1642 else { jp_noin = 1; init = jp_parse_expr(ctx); jp_noin = 0 } // `in` here = for-in separator, not operator
1643 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1644 // ---- R-JS-SYNTAX2 (rung 8): for-of / for-in disambiguation. After the loop var
1645 // (a decl `var x` or a bare lvalue expr `x`), a contextual `of`/`in` keyword forks
1646 // to the iterator forms; a `;` continues the classic C-style for(init;cond;update).
1647 if jp_is_kw(ctx, "of\x00" as *u8) == 1 { return jp_parse_for_in_of(ctx, init, ND_FOR_OF) }
1648 if jp_is_kw(ctx, "in\x00" as *u8) == 1 { return jp_parse_for_in_of(ctx, init, ND_FOR_IN) }
1649 // the for-header requires an explicit ';' after the init clause (no ASI inside ()).
1650 if jp_eat_punct(ctx, ";\x00" as *u8) == 0 { return jp_error_node(ctx) }
1651 }
1652 // ---- cond clause ---- (empty = true)
1653 var cond: i64 = -1
1654 if jp_is_punct(ctx, ";\x00" as *u8) == 1 {
1655 jp_advance(ctx)
1656 } else {
1657 cond = jp_parse_expr(ctx)
1658 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1659 if jp_eat_punct(ctx, ";\x00" as *u8) == 0 { return jp_error_node(ctx) }
1660 }
1661 // ---- update clause ---- (empty if next is ')')
1662 var update: i64 = -1
1663 if jp_is_punct(ctx, ")\x00" as *u8) == 1 {
1664 jp_advance(ctx)
1665 } else {
1666 update = jp_parse_expr(ctx)
1667 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1668 if jp_eat_punct(ctx, ")\x00" as *u8) == 0 { return jp_error_node(ctx) }
1669 }
1670 let body: i64 = jp_parse_stmt(ctx)
1671 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1672 // store body in the tokidx slot (FOR has 4 children; a/b/c hold init/cond/update).
1673 return jp_new_node(ctx, ND_FOR, init, cond, update, body, 0)
1674}
1675
1676// for-header var-decl WITHOUT the terminating ';' (the for-loop header owns the ';'
1677// separators, so the decl must NOT consume one or run ASI). Mirrors jp_parse_var_decl
1678// minus the termination clause. Produces an ND_VAR_DECL (a=name, b=init|-1).
1679func jp_parse_for_decl(ctx: *i64) -> i64 {
1680 jp_advance(ctx) // consume var/let/const
1681 let first: i64 = jp_parse_one_declarator(ctx)
1682 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1683 // MULTI-declarator for-init `for(var i=0, j=n; ...)` -> ND_VAR_LIST (common; scheme2js Boyer uses it).
1684 // NO terminator here -- the for-header owns the ';'. `in`/`of` after a MULTI list = SyntaxError anyway.
1685 if jp_is_punct(ctx, ",\x00" as *u8) == 1 {
1686 let scratch: *i64 = sys_mmap(256 * 8) as *i64
1687 scratch[0] = first
1688 var count: i64 = 1
1689 while jp_is_punct(ctx, ",\x00" as *u8) == 1 {
1690 jp_advance(ctx)
1691 let d: i64 = jp_parse_one_declarator(ctx)
1692 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1693 if count < 256 { scratch[count] = d }
1694 count = count + 1
1695 }
1696 let cs: i64 = jp_child_commit(ctx, scratch, count)
1697 return jp_new_node(ctx, ND_VAR_LIST, cs, count, -1, -1, 0)
1698 }
1699 return first
1700}
1701
1702// R-JS-SYNTAX2 (rung 8): finish a for-of / for-in header after the contextual `of`/`in`
1703// keyword. `target` is the already-parsed loop variable (an ND_VAR_DECL `var x` carrying its
1704// name IDENT, or a bare lvalue expr) and `nkind` is ND_FOR_OF or ND_FOR_IN. Cursor sits on
1705// the `of`/`in` keyword. Shape: a=target, b=iterable/object-expr, c=body. The right-hand side
1706// is an ASSIGNMENT-expr (real JS: `for(x of a)`); then ')' and the loop body statement.
1707func jp_parse_for_in_of(ctx: *i64, target: i64, nkind: i64) -> i64 {
1708 jp_advance(ctx) // consume `of` / `in`
1709 // for-IN head is `Expression` (comma-inclusive) per the grammar -- minified jQuery ships `for(e in a,b,...)`;
1710 // for-OF head is `AssignmentExpression` (a comma there is a real SyntaxError), so keep it single.
1711 var rhs: i64 = 0
1712 if nkind == ND_FOR_IN { rhs = jp_parse_expr(ctx) } else { rhs = jp_parse_assign(ctx) }
1713 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1714 if jp_eat_punct(ctx, ")\x00" as *u8) == 0 { return jp_error_node(ctx) }
1715 let body: i64 = jp_parse_stmt(ctx)
1716 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1717 return jp_new_node(ctx, nkind, target, rhs, body, -1, 0)
1718}
1719
1720// R-JS-SYNTAX2 (rung 8): switch ( disc ) { case E: stmts... case E2: stmts... default: stmts... }
1721// -> ND_SWITCH (a=disc-expr, b=case_list_child_start, c=case_count). Each clause is an ND_CASE
1722// node (a=case-expr or -1 for default, b=stmt_list_child_start, c=stmt_count) committed
1723// CONTIGUOUSLY in the child arena, and the ND_CASE list itself is committed contiguously after.
1724// A clause's statement list runs until the next `case`/`default`/`}` (NOT terminated by a
1725// keyword), so fall-through is preserved by the EVALUATOR (the parser just records clause order).
1726// Multiple `default` clauses are an honest OPEN; the first `default` wins at eval.
1727func jp_parse_switch(ctx: *i64) -> i64 {
1728 jp_advance(ctx) // consume `switch`
1729 if jp_eat_punct(ctx, "(\x00" as *u8) == 0 { return jp_error_node(ctx) }
1730 let disc: i64 = jp_parse_expr(ctx)
1731 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1732 if jp_eat_punct(ctx, ")\x00" as *u8) == 0 { return jp_error_node(ctx) }
1733 if jp_eat_punct(ctx, "{\x00" as *u8) == 0 { return jp_error_node(ctx) }
1734 let cases: *i64 = sys_mmap(256 * 8) as *i64 // ND_CASE node indices (clause order)
1735 var ccount: i64 = 0
1736 var go: i64 = 1
1737 while go == 1 {
1738 if jp_err(ctx) == 1 { go = 0 }
1739 if go == 1 {
1740 if jp_is_punct(ctx, "}\x00" as *u8) == 1 { jp_advance(ctx); go = 0 }
1741 else {
1742 if jp_tok_kind(ctx) == JS_TOK_EOF { jp_set_err(ctx); go = 0 }
1743 else {
1744 // clause head: `case EXPR :` or `default :`
1745 var case_expr: i64 = -1
1746 var headok: i64 = 0
1747 if jp_is_kw(ctx, "case\x00" as *u8) == 1 {
1748 jp_advance(ctx)
1749 case_expr = jp_parse_expr(ctx)
1750 headok = 1
1751 }
1752 if headok == 0 { if jp_is_kw(ctx, "default\x00" as *u8) == 1 { jp_advance(ctx); headok = 1 } }
1753 if headok == 0 { jp_set_err(ctx); go = 0 }
1754 if go == 1 {
1755 if jp_eat_punct(ctx, ":\x00" as *u8) == 0 { jp_set_err(ctx); go = 0 }
1756 }
1757 if go == 1 {
1758 // clause statement list: parse statements until the next clause head or '}'.
1759 let cnode: i64 = jp_parse_case_body(ctx, case_expr)
1760 if jp_err(ctx) == 1 { go = 0 }
1761 else {
1762 if ccount < 256 { cases[ccount] = cnode }
1763 ccount = ccount + 1
1764 }
1765 }
1766 }
1767 }
1768 }
1769 }
1770 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1771 let cstart: i64 = jp_child_commit(ctx, cases, ccount)
1772 // LIST-node layout (a=child_start, b=child_count) so jp_child_at reads the ND_CASE list;
1773 // the discriminant rides slot c.
1774 return jp_new_node(ctx, ND_SWITCH, cstart, ccount, disc, -1, 0)
1775}
1776
1777// parse ONE switch clause's statement list (the head `case E:`/`default:` is already consumed;
1778// `case_expr` = the case test expr or -1 for default). Statements run until the next `case`/
1779// `default`/`}` boundary. Returns an ND_CASE node (a=case_expr, b=stmt_child_start, c=stmt_count).
1780func jp_parse_case_body(ctx: *i64, case_expr: i64) -> i64 {
1781 let scratch: *i64 = sys_mmap(ND_MAGIC_1024 * 8) as *i64
1782 var count: i64 = 0
1783 var go: i64 = 1
1784 while go == 1 {
1785 if jp_err(ctx) == 1 { go = 0 }
1786 if go == 1 {
1787 if jp_is_punct(ctx, "}\x00" as *u8) == 1 { go = 0 } // end of switch (do NOT consume)
1788 else { if jp_is_kw(ctx, "case\x00" as *u8) == 1 { go = 0 } // next clause (do NOT consume)
1789 else { if jp_is_kw(ctx, "default\x00" as *u8) == 1 { go = 0 } // default clause (do NOT consume)
1790 else {
1791 if jp_tok_kind(ctx) == JS_TOK_EOF { jp_set_err(ctx); go = 0 }
1792 else {
1793 let st: i64 = jp_parse_stmt(ctx)
1794 if count < ND_MAGIC_1024 { scratch[count] = st }
1795 count = count + 1
1796 }
1797 } } }
1798 }
1799 }
1800 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1801 let start: i64 = jp_child_commit(ctx, scratch, count)
1802 // LIST-node layout (a=child_start, b=child_count) so jp_child_at reads the clause's stmts;
1803 // the case-test expr (-1 for default) rides slot c.
1804 return jp_new_node(ctx, ND_CASE, start, count, case_expr, -1, 0)
1805}
1806
1807// do body while ( cond ) ; -> ND_DOWHILE (a=cond, b=body). Body runs at least once.
1808func jp_parse_dowhile(ctx: *i64) -> i64 {
1809 jp_advance(ctx) // consume `do`
1810 let body: i64 = jp_parse_stmt(ctx)
1811 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1812 if jp_is_kw(ctx, "while\x00" as *u8) == 0 { return jp_error_node(ctx) }
1813 jp_advance(ctx) // consume `while`
1814 if jp_eat_punct(ctx, "(\x00" as *u8) == 0 { return jp_error_node(ctx) }
1815 let cond: i64 = jp_parse_expr(ctx)
1816 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1817 if jp_eat_punct(ctx, ")\x00" as *u8) == 0 { return jp_error_node(ctx) }
1818 // optional trailing ';' (ASI applies); real JS allows `do{}while(c)` then a break.
1819 if jp_eat_punct(ctx, ";\x00" as *u8) == 0 {
1820 if jp_can_asi(ctx) == 0 { return jp_error_node(ctx) }
1821 }
1822 return jp_new_node(ctx, ND_DOWHILE, cond, body, -1, -1, 0)
1823}
1824
1825// break ; / continue ; -> ND_BREAK / ND_CONTINUE (no children). ASI-lite termination.
1826// (labeled break/continue need a label table -> HONEST OPEN, not parsed: a label after
1827// the keyword is not consumed, so `break outer;` would fall to the ';' check and error.)
1828func jp_parse_break(ctx: *i64) -> i64 {
1829 jp_advance(ctx) // consume `break`
1830 if jp_tok_kind(ctx) == JS_TOK_IDENT { jp_advance(ctx) } // optional label -> consumed (targets nearest loop; see labeled-stmt note)
1831 if jp_eat_punct(ctx, ";\x00" as *u8) == 0 {
1832 if jp_can_asi(ctx) == 0 { return jp_error_node(ctx) }
1833 }
1834 return jp_new_node(ctx, ND_BREAK, -1, -1, -1, -1, 0)
1835}
1836func jp_parse_continue(ctx: *i64) -> i64 {
1837 jp_advance(ctx) // consume `continue`
1838 if jp_tok_kind(ctx) == JS_TOK_IDENT { jp_advance(ctx) } // optional label -> consumed (targets nearest loop)
1839 if jp_eat_punct(ctx, ";\x00" as *u8) == 0 {
1840 if jp_can_asi(ctx) == 0 { return jp_error_node(ctx) }
1841 }
1842 return jp_new_node(ctx, ND_CONTINUE, -1, -1, -1, -1, 0)
1843}
1844
1845// param list `( a , b , ... )` -> PARAMS list-node a=child_start, b=child_count
1846func jp_parse_params(ctx: *i64) -> i64 {
1847 if jp_eat_punct(ctx, "(\x00" as *u8) == 0 { return jp_error_node(ctx) }
1848 let scratch: *i64 = sys_mmap(256 * 8) as *i64
1849 var count: i64 = 0
1850 if jp_is_punct(ctx, ")\x00" as *u8) == 1 {
1851 jp_advance(ctx)
1852 let start0: i64 = jp_child_commit(ctx, scratch, 0)
1853 return jp_new_node(ctx, ND_PARAMS, start0, 0, -1, -1, 0)
1854 }
1855 var go: i64 = 1
1856 while go == 1 {
1857 var isrest: i64 = 0
1858 if jp_is_punct(ctx, "...\x00" as *u8) == 1 { isrest = 1; jp_advance(ctx) } // rest param ...r
1859 var pn: i64 = 0 - 1
1860 var handled: i64 = 0
1861 // DESTRUCTURING params (ES6): `function f({a, b: c}, [x, y])` -- vk's framework uses these.
1862 if isrest == 0 { if jp_is_punct(ctx, "{\x00" as *u8) == 1 { pn = jp_parse_object_pattern(ctx); handled = 1; if jp_err(ctx) == 1 { go = 0 } } }
1863 if handled == 0 { if isrest == 0 { if jp_is_punct(ctx, "[\x00" as *u8) == 1 { pn = jp_parse_array_pattern(ctx); handled = 1; if jp_err(ctx) == 1 { go = 0 } } } }
1864 if handled == 0 {
1865 if jp_tok_kind(ctx) != JS_TOK_IDENT { jp_set_err(ctx); go = 0 }
1866 if go == 1 {
1867 let ptok: i64 = jp_cur(ctx)
1868 jp_advance(ctx)
1869 var defexpr: i64 = 0 - 1
1870 if isrest == 0 { if jp_is_punct(ctx, "=\x00" as *u8) == 1 { // default param: name = expr
1871 jp_advance(ctx)
1872 defexpr = jp_parse_assign(ctx)
1873 if jp_err(ctx) == 1 { go = 0 }
1874 } }
1875 pn = jp_new_node(ctx, ND_IDENT, defexpr, -1, -1, ptok, isrest) // extra=isrest; slot a=default/-1
1876 }
1877 }
1878 if go == 1 {
1879 if count < 256 { scratch[count] = pn }
1880 count = count + 1
1881 if jp_eat_punct(ctx, ",\x00" as *u8) == 1 {
1882 // next param
1883 } else {
1884 if jp_eat_punct(ctx, ")\x00" as *u8) == 1 { go = 0 }
1885 else { jp_set_err(ctx); go = 0 }
1886 }
1887 }
1888 }
1889 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1890 let start: i64 = jp_child_commit(ctx, scratch, count)
1891 return jp_new_node(ctx, ND_PARAMS, start, count, -1, -1, 0)
1892}
1893
1894// function NAME ( params ) { body }
1895func jp_parse_func_decl(ctx: *i64) -> i64 {
1896 jp_advance(ctx) // consume `function`
1897 if jp_is_punct(ctx, "*\x00" as *u8) == 1 { jp_advance(ctx) } // generator marker (stub semantics; ND_YIELD)
1898 if jp_tok_kind(ctx) != JS_TOK_IDENT { return jp_error_node(ctx) }
1899 let name: i64 = jp_new_node(ctx, ND_IDENT, -1, -1, -1, jp_cur(ctx), 0)
1900 jp_advance(ctx)
1901 let params: i64 = jp_parse_params(ctx)
1902 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1903 if jp_is_punct(ctx, "{\x00" as *u8) == 0 { return jp_error_node(ctx) }
1904 let body: i64 = jp_parse_block(ctx)
1905 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1906 return jp_new_node(ctx, ND_FUNC_DECL, name, params, body, -1, 0)
1907}
1908
1909// FUNCTION EXPRESSION (R-JS-CLOSURE, rung 6): `function (params){body}` (anonymous) or
1910// `function name(params){body}` (named) in EXPRESSION position. Same ND_FUNC_DECL node
1911// shape as a declaration (a=name|-1, b=params, c=body); name = -1 when anonymous. The
1912// expression-vs-statement distinction is purely WHERE it is parsed -- a statement-leading
1913// `function` is a declaration (jp_parse_stmt), here it is a value. Cursor is on `function`.
1914func jp_parse_func_expr(ctx: *i64) -> i64 {
1915 jp_advance(ctx) // consume `function`
1916 if jp_is_punct(ctx, "*\x00" as *u8) == 1 { jp_advance(ctx) } // generator marker (stub semantics; ND_YIELD)
1917 var name: i64 = -1
1918 if jp_tok_kind(ctx) == JS_TOK_IDENT { // optional name (named function expression)
1919 name = jp_new_node(ctx, ND_IDENT, -1, -1, -1, jp_cur(ctx), 0)
1920 jp_advance(ctx)
1921 }
1922 let params: i64 = jp_parse_params(ctx)
1923 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1924 if jp_is_punct(ctx, "{\x00" as *u8) == 0 { return jp_error_node(ctx) }
1925 let body: i64 = jp_parse_block(ctx)
1926 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1927 return jp_new_node(ctx, ND_FUNC_DECL, name, params, body, -1, 0)
1928}
1929
1930// expression statement: expr [ ; | ASI ]
1931// Termination is REQUIRED -- but the semicolon may be inserted automatically (ASI):
1932// after a complete expression, accept an explicit ';', else require an ASI point
1933// (line break, '}', or EOF). Adjacent statements on ONE line with no separator
1934// (`a b`, `1 2`) are a SyntaxError -- jp_can_asi returns 0 there, so we error.
1935func jp_parse_expr_stmt(ctx: *i64) -> i64 {
1936 let e: i64 = jp_parse_expr(ctx)
1937 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1938 if jp_eat_punct(ctx, ";\x00" as *u8) == 1 {
1939 return jp_new_node(ctx, ND_EXPR_STMT, e, -1, -1, -1, 0)
1940 }
1941 if jp_can_asi(ctx) == 0 { return jp_error_node(ctx) } // no ';', no ASI point
1942 return jp_new_node(ctx, ND_EXPR_STMT, e, -1, -1, -1, 0)
1943}
1944
1945// one statement (dispatch on leading keyword / punct). Shallow if-chain by design.
1946// try { ... } catch (e) { ... } finally { ... } -- catch param optional (`catch {`),
1947// at least one of catch/finally required (real JS). Finally block rides the tokidx slot.
1948func jp_parse_try(ctx: *i64) -> i64 {
1949 jp_advance(ctx) // consume `try`
1950 if jp_is_punct(ctx, "{\x00" as *u8) == 0 { jp_set_err(ctx); return jp_error_node(ctx) }
1951 let tryb: i64 = jp_parse_block(ctx)
1952 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1953 var param: i64 = 0 - 1
1954 var catchb: i64 = 0 - 1
1955 var finb: i64 = 0 - 1
1956 if jp_is_kw(ctx, "catch\x00" as *u8) == 1 {
1957 jp_advance(ctx)
1958 if jp_is_punct(ctx, "(\x00" as *u8) == 1 {
1959 jp_advance(ctx)
1960 if jp_tok_kind(ctx) != JS_TOK_IDENT { jp_set_err(ctx); return jp_error_node(ctx) }
1961 param = jp_new_node(ctx, ND_IDENT, -1, -1, -1, jp_cur(ctx), 0)
1962 jp_advance(ctx)
1963 if jp_eat_punct(ctx, ")\x00" as *u8) == 0 { jp_set_err(ctx); return jp_error_node(ctx) }
1964 }
1965 if jp_is_punct(ctx, "{\x00" as *u8) == 0 { jp_set_err(ctx); return jp_error_node(ctx) }
1966 catchb = jp_parse_block(ctx)
1967 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1968 }
1969 if jp_is_kw(ctx, "finally\x00" as *u8) == 1 {
1970 jp_advance(ctx)
1971 if jp_is_punct(ctx, "{\x00" as *u8) == 0 { jp_set_err(ctx); return jp_error_node(ctx) }
1972 finb = jp_parse_block(ctx)
1973 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1974 }
1975 if catchb < 0 { if finb < 0 { jp_set_err(ctx); return jp_error_node(ctx) } }
1976 return jp_new_node(ctx, ND_TRY, tryb, param, catchb, finb, 0)
1977}
1978// throw expr ;
1979func jp_parse_throw(ctx: *i64) -> i64 {
1980 jp_advance(ctx) // consume `throw`
1981 let e: i64 = jp_parse_expr(ctx)
1982 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1983 if jp_eat_punct(ctx, ";\x00" as *u8) == 0 {
1984 if jp_can_asi(ctx) == 0 { return jp_error_node(ctx) }
1985 }
1986 return jp_new_node(ctx, ND_THROW, e, -1, -1, -1, 0)
1987}
1988func jp_parse_stmt(ctx: *i64) -> i64 {
1989 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1990 // lone semicolon = empty statement -> treat as empty EXPR_STMT? keep strict:
1991 // we just consume a stray ';' as an empty block of work by recursing once.
1992 if jp_is_punct(ctx, ";\x00" as *u8) == 1 {
1993 jp_advance(ctx)
1994 return jp_new_node(ctx, ND_BLOCK, jp_pst(ctx)[PST_NCHILD], 0, -1, -1, 0)
1995 }
1996 if jp_is_punct(ctx, "{\x00" as *u8) == 1 { return jp_parse_block(ctx) }
1997 if jp_is_kw(ctx, "var\x00" as *u8) == 1 { return jp_parse_var_decl(ctx) }
1998 if jp_is_kw(ctx, "let\x00" as *u8) == 1 { return jp_parse_var_decl(ctx) }
1999 if jp_is_kw(ctx, "const\x00" as *u8) == 1 { return jp_parse_var_decl(ctx) }
2000 if jp_is_kw(ctx, "return\x00" as *u8) == 1 { return jp_parse_return(ctx) }
2001 if jp_is_kw(ctx, "if\x00" as *u8) == 1 { return jp_parse_if(ctx) }
2002 if jp_is_kw(ctx, "while\x00" as *u8) == 1 { return jp_parse_while(ctx) }
2003 if jp_is_kw(ctx, "for\x00" as *u8) == 1 { return jp_parse_for(ctx) }
2004 if jp_is_kw(ctx, "do\x00" as *u8) == 1 { return jp_parse_dowhile(ctx) }
2005 if jp_is_kw(ctx, "switch\x00" as *u8) == 1 { return jp_parse_switch(ctx) }
2006 if jp_is_kw(ctx, "break\x00" as *u8) == 1 { return jp_parse_break(ctx) }
2007 if jp_is_kw(ctx, "continue\x00" as *u8) == 1 { return jp_parse_continue(ctx) }
2008 // `async function NAME(){}` DECLARATION (2026-08-25). Same contextual idiom as the async arrow in
2009 // jp_parse_assign: `async` is an IDENT, so consume it only when `function` genuinely follows,
2010 // else restore the cursor and let it parse as a plain name. Async-ness itself is not modelled
2011 // (declared stub, as for arrows). Found because the shipped /world pages declare
2012 // `async function loadNPC` and the parser refused them at token 0 -- which made every static
2013 // referee over the page (nx_jsscope) unable to judge the real subject.
2014 // `async` may lex as IDENT or as KEYWORD depending on the lexer's reserved list -- accept both,
2015 // because a guard keyed on one kind silently never fires under the other (measured: the IDENT-only
2016 // form left the parse error at token 0 exactly as before).
2017 var fasync: i64 = jp_is_lex(ctx, JS_TOK_IDENT, "async\x00" as *u8)
2018 if jp_is_kw(ctx, "async\x00" as *u8) == 1 { fasync = 1 }
2019 if fasync == 1 {
2020 let fsave: i64 = jp_cur(ctx)
2021 jp_set_cur(ctx, fsave + 1)
2022 if jp_is_kw(ctx, "function\x00" as *u8) == 1 { return jp_parse_func_decl(ctx) }
2023 jp_set_cur(ctx, fsave)
2024 }
2025 if jp_is_kw(ctx, "function\x00" as *u8) == 1 { return jp_parse_func_decl(ctx) }
2026 if jp_is_kw(ctx, "class\x00" as *u8) == 1 { return jp_parse_class_decl(ctx) }
2027 if jp_is_kw(ctx, "try\x00" as *u8) == 1 { return jp_parse_try(ctx) }
2028 if jp_is_kw(ctx, "throw\x00" as *u8) == 1 { return jp_parse_throw(ctx) }
2029 // labeled statement: IDENT ':' stmt (minified jQuery/Sizzle use labeled loops). Pragmatic support: the label
2030 // is a TRANSPARENT wrapper -> parse+discard it, return the inner statement. break/continue [label] consume the
2031 // label but target the NEAREST loop (correct when the label is on the enclosing loop; a labeled break to a
2032 // NON-nearest outer loop is the known approximation -> a full label table is the follow-on).
2033 if jp_tok_kind(ctx) == JS_TOK_IDENT { if jp_punct_at(ctx, jp_cur(ctx) + 1, ":\x00" as *u8) == 1 {
2034 jp_advance(ctx) // label IDENT
2035 jp_advance(ctx) // ':'
2036 return jp_parse_stmt(ctx)
2037 } }
2038 // default: expression statement
2039 return jp_parse_expr_stmt(ctx)
2040}
2041
2042// PROGRAM = stmt* until EOF. Returns the PROGRAM node index (list-node).
2043func jp_parse_program(ctx: *i64) -> i64 {
2044 let scratch: *i64 = sys_mmap(ND_MAGIC_4096 * 8) as *i64
2045 var count: i64 = 0
2046 var go: i64 = 1
2047 while go == 1 {
2048 if jp_tok_kind(ctx) == JS_TOK_EOF { go = 0 }
2049 if go == 1 { if jp_cur(ctx) >= ctx[CTX_NTOK] { go = 0 } }
2050 if go == 1 { if jp_err(ctx) == 1 { go = 0 } }
2051 if go == 1 {
2052 let st: i64 = jp_parse_stmt(ctx)
2053 if count < ND_MAGIC_4096 { scratch[count] = st }
2054 count = count + 1
2055 }
2056 }
2057 let start: i64 = jp_child_commit(ctx, scratch, count)
2058 return jp_new_node(ctx, ND_PROGRAM, start, count, -1, -1, 0)
2059}
2060
2061// ===================== top-level driver =====================
2062// Build a parse context over freshly-mmap'd arenas, lex `src`, then parse a PROGRAM.
2063// Returns the PROGRAM node index. Writes the ctx pointer to *ctx_out so the caller
2064// (gate) can inspect the AST. Sets the error flag inside pst on any syntax error.
2065func jp_parse_source(src: *u8, srclen: i64, ctx_out: *i64) -> i64 {
2066 // Budgets: js_lex TRUNCATES SILENTLY at maxtoks (parse then dies mid-file with a misleading cursor) --
2067 // 4096 tokens capped real sources at ~16-26KB (Octane RayTrace 28KB / Crypto 48KB both hit it). mmap is
2068 // virtual (only touched pages cost), so size for real-page bundles. Dynamic growth = a follow-on.
2069 // ADAPTIVE budgets sized to the input: real pages (vk = ~250KB of concatenated inline JS across 70 scripts)
2070 // blew the fixed 65536-token cap -> js_lex truncated -> parse died mid-file (rc=1). ~1 token per 2 bytes
2071 // covers dense minified JS; nodes/children scale with tokens. mmap is virtual (only touched pages cost).
2072 var maxtoks: i64 = ND_MAGIC_65536
2073 if srclen / 2 > maxtoks { maxtoks = srclen / 2 }
2074 var maxnodes: i64 = maxtoks * 2
2075 var maxchild: i64 = maxtoks * 2
2076 if maxnodes < ND_MAGIC_131072 { maxnodes = ND_MAGIC_131072 }
2077 if maxchild < ND_MAGIC_131072 { maxchild = ND_MAGIC_131072 }
2078 let toks: *i64 = sys_mmap(maxtoks * 3 * 8) as *i64
2079 let ntb: *i64 = sys_mmap(16) as *i64
2080 let nodes: *i64 = sys_mmap(maxnodes * NODE_SLOTS * 8) as *i64
2081 let children: *i64 = sys_mmap(maxchild * 8) as *i64
2082 let pst: *i64 = sys_mmap(64) as *i64
2083 let ctx: *i64 = sys_mmap(CTX_MAXCHILD * 8 + 64) as *i64
2084
2085 let ntok: i64 = js_lex(src, srclen, toks, maxtoks, ntb)
2086
2087 pst[PST_CUR] = 0
2088 pst[PST_ERR] = 0
2089 pst[PST_NNODE] = 0
2090 pst[PST_NCHILD] = 0
2091 pst[PST_ERR_POS] = 0 - 1
2092
2093 ctx[CTX_SRC] = src as i64
2094 ctx[CTX_TOKS] = toks as i64
2095 ctx[CTX_NTOK] = ntok
2096 ctx[CTX_NODES] = nodes as i64
2097 ctx[CTX_CHILDREN] = children as i64
2098 ctx[CTX_PST] = pst as i64
2099 ctx[CTX_MAXNODE] = maxnodes
2100 ctx[CTX_MAXCHILD] = maxchild
2101
2102 // If the lexer itself emitted an ERROR token, the source is malformed.
2103 var li: i64 = 0
2104 while li < ntok {
2105 if toks[li * 3 + 0] == JS_TOK_ERROR { if pst[PST_ERR] == 0 { pst[PST_ERR_POS] = toks[li * 3 + 1] } pst[PST_ERR] = 1 }
2106 li = li + 1
2107 }
2108
2109 let prog: i64 = jp_parse_program(ctx)
2110
2111 // Trailing unconsumed tokens (after a complete parse) = syntax error too:
2112 // e.g. "1 + )" leaves the ')' dangling / errors mid-expr. Already covered by
2113 // err flag, but guard the case where the program loop stopped early WITHOUT
2114 // consuming everything and WITHOUT an error (shouldn't happen, but honest).
2115 if pst[PST_ERR] == 0 {
2116 if pst[PST_CUR] < ntok { if pst[PST_CUR] < ntok { pst[PST_ERR_POS] = toks[pst[PST_CUR] * 3 + 1] } pst[PST_ERR] = 1 }
2117 }
2118
2119 ctx_out[0] = ctx as i64
2120 return prog
2121}
2122
2123// ===================== GATE =====================
2124func jq_puts(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
2125// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
2126// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
2127// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
2128// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
2129func jq_putn(v: i64) -> i64 { nxi_out(v); return 0 }
2130func jq_strlen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
2131// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
2132// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
2133// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
2134// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
2135func jq_fdn(fd: i64, v: i64) -> i64 { nxi_fd(fd, v); return 0 }
2136
2137// helper: parse a NUL-terminated source string, return ctx (via box) + prog index.
2138func jq_parse(s: *u8, ctxbox: *i64) -> i64 {
2139 return jp_parse_source(s, jq_strlen(s), ctxbox)
2140}
2141func jq_ctx(ctxbox: *i64) -> *i64 { return (ctxbox[0]) as *i64 }
2142func jq_haserr(ctxbox: *i64) -> i64 { let c: *i64 = jq_ctx(ctxbox); let p: *i64 = jp_pst(c); return p[PST_ERR] }
2143
2144// node index of statement-0's top expression (a-slot of the first EXPR_STMT).
2145func jq_expr0(ctxbox: *i64, prog: i64) -> i64 {
2146 let c: *i64 = jq_ctx(ctxbox)
2147 let st: i64 = jp_child_at(c, prog, 0)
2148 return jp_na(c, st)
2149}
2150// kind of statement-0's top expression.
2151func jq_expr0_kind(ctxbox: *i64, prog: i64) -> i64 {
2152 let c: *i64 = jq_ctx(ctxbox)
2153 return jp_nkind(c, jq_expr0(ctxbox, prog))
2154}
2155// op-code (extra slot) of statement-0's top expression.
2156func jq_expr0_op(ctxbox: *i64, prog: i64) -> i64 {
2157 let c: *i64 = jq_ctx(ctxbox)
2158 return jp_nextra(c, jq_expr0(ctxbox, prog))
2159}
2160// 1 iff `src` parses with NO error AND its statement-0 expression is a BINARY whose
2161// op-code == want. Used by KAT10 to sweep an operator family compactly.
2162func jq_binop_is(src: *u8, ctxbox: *i64, want: i64) -> i64 {
2163 let prog: i64 = jq_parse(src, ctxbox)
2164 if jq_haserr(ctxbox) == 1 { return 0 }
2165 if jq_expr0_kind(ctxbox, prog) != ND_BINARY { return 0 }
2166 if jq_expr0_op(ctxbox, prog) != want { return 0 }
2167 return 1
2168}
2169// 1 iff `src` parses with NO error AND its statement-0 expression is a UNARY whose
2170// op-code == want.
2171func jq_unop_is(src: *u8, ctxbox: *i64, want: i64) -> i64 {
2172 let prog: i64 = jq_parse(src, ctxbox)
2173 if jq_haserr(ctxbox) == 1 { return 0 }
2174 if jq_expr0_kind(ctxbox, prog) != ND_UNARY { return 0 }
2175 if jq_expr0_op(ctxbox, prog) != want { return 0 }
2176 return 1
2177}
2178
2179func main() -> i64 {
2180 let ctxbox: *i64 = sys_mmap(16) as *i64
2181 var pass: i64 = 0
2182 var tot: i64 = 0
2183 jq_puts("nx_js_parse gate (R-JS-PARSE, WB-JS-001 rung 1)\n" as *u8)
2184
2185 // ---- KAT 1 (precedence): 1+2*3 -> EXPR_STMT( BINARY(+) ) whose RIGHT child is
2186 // BINARY(*). Proves * binds tighter than + (NOT left-flat). ----
2187 let s1: *u8 = "1+2*3\x00" as *u8
2188 let prog1: i64 = jq_parse(s1, ctxbox)
2189 let c1: *i64 = jq_ctx(ctxbox)
2190 var r1: i64 = 1
2191 if jq_haserr(ctxbox) == 1 { r1 = 0 }
2192 if jp_nkind(c1, prog1) != ND_PROGRAM { r1 = 0 }
2193 if jp_nb(c1, prog1) != 1 { r1 = 0 } // exactly 1 statement
2194 if r1 == 1 {
2195 let st: i64 = jp_child_at(c1, prog1, 0)
2196 if jp_nkind(c1, st) != ND_EXPR_STMT { r1 = 0 }
2197 if r1 == 1 {
2198 let add: i64 = jp_na(c1, st)
2199 if jp_nkind(c1, add) != ND_BINARY { r1 = 0 }
2200 if r1 == 1 { if jp_nextra(c1, add) != OP_ADD { r1 = 0 } }
2201 if r1 == 1 {
2202 let lhs: i64 = jp_na(c1, add)
2203 let rhs: i64 = jp_nb(c1, add)
2204 if jp_nkind(c1, lhs) != ND_NUMBER { r1 = 0 } // left = 1 (a NUMBER)
2205 if jp_nkind(c1, rhs) != ND_BINARY { r1 = 0 } // right = (2*3)
2206 if r1 == 1 { if jp_nextra(c1, rhs) != OP_MUL { r1 = 0 } }
2207 }
2208 }
2209 }
2210 if r1 == 1 { jq_puts(" PASS KAT1 precedence 1+2*3 -> +(1, *(2,3))\n" as *u8); pass=pass+1 } else { jq_puts(" FAIL KAT1 precedence\n" as *u8) }
2211 tot = tot + 1
2212
2213 // ---- KAT 2 (assoc): a=b=c -> ASSIGN(a, ASSIGN(b, c)) right-assoc;
2214 // AND 2-3-4 -> BINARY(-) whose LEFT is BINARY(-) (left-assoc). ----
2215 let s2: *u8 = "a=b=c\x00" as *u8
2216 let prog2: i64 = jq_parse(s2, ctxbox)
2217 let c2: *i64 = jq_ctx(ctxbox)
2218 var r2: i64 = 1
2219 if jq_haserr(ctxbox) == 1 { r2 = 0 }
2220 if r2 == 1 {
2221 let st2: i64 = jp_child_at(c2, prog2, 0)
2222 let asn: i64 = jp_na(c2, st2)
2223 if jp_nkind(c2, asn) != ND_ASSIGN { r2 = 0 }
2224 if r2 == 1 {
2225 let lhs: i64 = jp_na(c2, asn)
2226 let rhs: i64 = jp_nb(c2, asn)
2227 if jp_nkind(c2, lhs) != ND_IDENT { r2 = 0 } // a
2228 if jp_nkind(c2, rhs) != ND_ASSIGN { r2 = 0 } // (b=c) nested RIGHT
2229 }
2230 }
2231 // left-assoc subtraction
2232 let s2b: *u8 = "2-3-4\x00" as *u8
2233 let prog2b: i64 = jq_parse(s2b, ctxbox)
2234 let c2b: *i64 = jq_ctx(ctxbox)
2235 if jq_haserr(ctxbox) == 1 { r2 = 0 }
2236 if r2 == 1 {
2237 let st: i64 = jp_child_at(c2b, prog2b, 0)
2238 let sub: i64 = jp_na(c2b, st)
2239 if jp_nkind(c2b, sub) != ND_BINARY { r2 = 0 }
2240 if r2 == 1 { if jp_nextra(c2b, sub) != OP_SUB { r2 = 0 } }
2241 if r2 == 1 {
2242 let lhs: i64 = jp_na(c2b, sub)
2243 let rhs: i64 = jp_nb(c2b, sub)
2244 if jp_nkind(c2b, lhs) != ND_BINARY { r2 = 0 } // (2-3) nested LEFT
2245 if jp_nkind(c2b, rhs) != ND_NUMBER { r2 = 0 } // 4 on the right
2246 }
2247 }
2248 if r2 == 1 { jq_puts(" PASS KAT2 assoc a=b=c right + 2-3-4 left\n" as *u8); pass=pass+1 } else { jq_puts(" FAIL KAT2 assoc\n" as *u8) }
2249 tot = tot + 1
2250
2251 // ---- KAT 3 (call/member chain): foo.bar(x) -> CALL of MEMBER(foo,bar), 1 arg. ----
2252 let s3: *u8 = "foo.bar(x)\x00" as *u8
2253 let prog3: i64 = jq_parse(s3, ctxbox)
2254 let c3: *i64 = jq_ctx(ctxbox)
2255 var r3: i64 = 1
2256 if jq_haserr(ctxbox) == 1 { r3 = 0 }
2257 if r3 == 1 {
2258 let st: i64 = jp_child_at(c3, prog3, 0)
2259 let call: i64 = jp_na(c3, st)
2260 if jp_nkind(c3, call) != ND_CALL { r3 = 0 }
2261 if r3 == 1 {
2262 let callee: i64 = jp_na(c3, call) // a=callee
2263 let argc: i64 = jp_nc(c3, call) // c=arg count
2264 if jp_nkind(c3, callee) != ND_MEMBER { r3 = 0 }
2265 if argc != 1 { r3 = 0 }
2266 if r3 == 1 {
2267 let obj: i64 = jp_na(c3, callee)
2268 let prop: i64 = jp_nb(c3, callee)
2269 if jp_nkind(c3, obj) != ND_IDENT { r3 = 0 } // foo
2270 if jp_nkind(c3, prop) != ND_IDENT { r3 = 0 } // bar
2271 }
2272 }
2273 }
2274 if r3 == 1 { jq_puts(" PASS KAT3 chain foo.bar(x) -> CALL(MEMBER(foo,bar), [x])\n" as *u8); pass=pass+1 } else { jq_puts(" FAIL KAT3 chain\n" as *u8) }
2275 tot = tot + 1
2276
2277 // ---- KAT 4 (statements): var decl + if/else + while + function decl + return. ----
2278 let s4: *u8 = "var x = 1; if (x) { return x; } else { x = 2; } while (x) { x = x - 1; } function f(a, b) { return a + b; }\x00" as *u8
2279 let prog4: i64 = jq_parse(s4, ctxbox)
2280 let c4: *i64 = jq_ctx(ctxbox)
2281 var r4: i64 = 1
2282 if jq_haserr(ctxbox) == 1 { r4 = 0 }
2283 if jp_nkind(c4, prog4) != ND_PROGRAM { r4 = 0 }
2284 if r4 == 1 { if jp_nb(c4, prog4) != 4 { r4 = 0 } } // 4 top-level statements
2285 if r4 == 1 {
2286 let s_var: i64 = jp_child_at(c4, prog4, 0)
2287 let s_if: i64 = jp_child_at(c4, prog4, 1)
2288 let s_wh: i64 = jp_child_at(c4, prog4, 2)
2289 let s_fn: i64 = jp_child_at(c4, prog4, 3)
2290 if jp_nkind(c4, s_var) != ND_VAR_DECL { r4 = 0 }
2291 if jp_nkind(c4, s_if) != ND_IF { r4 = 0 }
2292 if jp_nkind(c4, s_wh) != ND_WHILE { r4 = 0 }
2293 if jp_nkind(c4, s_fn) != ND_FUNC_DECL { r4 = 0 }
2294 // var decl has an initializer (b != -1)
2295 if r4 == 1 { if jp_nb(c4, s_var) == -1 { r4 = 0 } }
2296 // if has an else branch (c != -1) and both branches are BLOCKs
2297 if r4 == 1 {
2298 if jp_nc(c4, s_if) == -1 { r4 = 0 }
2299 if r4 == 1 {
2300 if jp_nkind(c4, jp_nb(c4, s_if)) != ND_BLOCK { r4 = 0 } // then
2301 if jp_nkind(c4, jp_nc(c4, s_if)) != ND_BLOCK { r4 = 0 } // else
2302 }
2303 }
2304 // then-block contains a RETURN
2305 if r4 == 1 {
2306 let then_b: i64 = jp_nb(c4, s_if)
2307 if jp_nb(c4, then_b) < 1 { r4 = 0 }
2308 if r4 == 1 {
2309 let ret: i64 = jp_child_at(c4, then_b, 0)
2310 if jp_nkind(c4, ret) != ND_RETURN { r4 = 0 }
2311 if r4 == 1 { if jp_na(c4, ret) == -1 { r4 = 0 } } // return HAS an expr
2312 }
2313 }
2314 // function f(a,b){...} -> 2 params, body BLOCK with a RETURN of a BINARY(+)
2315 if r4 == 1 {
2316 let params: i64 = jp_nb(c4, s_fn)
2317 let body: i64 = jp_nc(c4, s_fn)
2318 if jp_nkind(c4, params) != ND_PARAMS { r4 = 0 }
2319 if r4 == 1 { if jp_nb(c4, params) != 2 { r4 = 0 } } // 2 params
2320 if jp_nkind(c4, body) != ND_BLOCK { r4 = 0 }
2321 if r4 == 1 {
2322 let ret: i64 = jp_child_at(c4, body, 0)
2323 if jp_nkind(c4, ret) != ND_RETURN { r4 = 0 }
2324 if r4 == 1 {
2325 let plus: i64 = jp_na(c4, ret)
2326 if jp_nkind(c4, plus) != ND_BINARY { r4 = 0 }
2327 if r4 == 1 { if jp_nextra(c4, plus) != OP_ADD { r4 = 0 } }
2328 }
2329 }
2330 }
2331 }
2332 if r4 == 1 { jq_puts(" PASS KAT4 statements var/if-else/while/func+return\n" as *u8); pass=pass+1 } else { jq_puts(" FAIL KAT4 statements\n" as *u8) }
2333 tot = tot + 1
2334
2335 // ---- KAT 5 (TAMPER): malformed "1 + )" MUST be an error, never a fake AST. ----
2336 let s5: *u8 = "1 + )\x00" as *u8
2337 let prog5: i64 = jq_parse(s5, ctxbox)
2338 var r5: i64 = 1
2339 if jq_haserr(ctxbox) != 1 { r5 = 0 } // must have flagged an error
2340 // second malformed sample: "var = ;" (decl with no name)
2341 let s5b: *u8 = "var = ;\x00" as *u8
2342 let prog5b: i64 = jq_parse(s5b, ctxbox)
2343 if jq_haserr(ctxbox) != 1 { r5 = 0 }
2344 // third: dangling operator "a *" (RHS missing)
2345 let s5c: *u8 = "a *\x00" as *u8
2346 let prog5c: i64 = jq_parse(s5c, ctxbox)
2347 if jq_haserr(ctxbox) != 1 { r5 = 0 }
2348 if r5 == 1 { jq_puts(" PASS KAT5 tamper: 3 malformed inputs all -> ERROR (no fake AST)\n" as *u8); pass=pass+1 } else { jq_puts(" FAIL KAT5 tamper (a malformed input was accepted!)\n" as *u8) }
2349 tot = tot + 1
2350
2351 // ---- KAT 6 (precedence ladder + unary): !a && b || c == d -> top is OR, whose
2352 // left is AND, the AND's left is UNARY(!a), and == binds tighter than ==. ---
2353 let s6: *u8 = "!a && b || c == d\x00" as *u8
2354 let prog6: i64 = jq_parse(s6, ctxbox)
2355 let c6: *i64 = jq_ctx(ctxbox)
2356 var r6: i64 = 1
2357 if jq_haserr(ctxbox) == 1 { r6 = 0 }
2358 if r6 == 1 {
2359 let st: i64 = jp_child_at(c6, prog6, 0)
2360 let or_n: i64 = jp_na(c6, st)
2361 if jp_nkind(c6, or_n) != ND_BINARY { r6 = 0 }
2362 if r6 == 1 { if jp_nextra(c6, or_n) != OP_OR { r6 = 0 } } // top = ||
2363 if r6 == 1 {
2364 let and_n: i64 = jp_na(c6, or_n) // left of ||
2365 let eq_n: i64 = jp_nb(c6, or_n) // right of ||
2366 if jp_nextra(c6, and_n) != OP_AND { r6 = 0 } // && under ||
2367 if r6 == 1 { if jp_nextra(c6, eq_n) != OP_EQ { r6 = 0 } } // == under ||
2368 if r6 == 1 {
2369 let not_n: i64 = jp_na(c6, and_n) // left of &&
2370 if jp_nkind(c6, not_n) != ND_UNARY { r6 = 0 }
2371 if r6 == 1 { if jp_nextra(c6, not_n) != OP_NOT { r6 = 0 } }
2372 }
2373 }
2374 }
2375 if r6 == 1 { jq_puts(" PASS KAT6 ladder !a && b || c==d -> ||(&&(!a,b), ==(c,d))\n" as *u8); pass=pass+1 } else { jq_puts(" FAIL KAT6 precedence ladder\n" as *u8) }
2376 tot = tot + 1
2377
2378 // ---- KAT 7 (parenthesized regrouping): (1+2)*3 -> BINARY(*) whose LEFT is
2379 // BINARY(+). Proves parens override precedence (distinct from KAT1). ----
2380 let s7: *u8 = "(1+2)*3\x00" as *u8
2381 let prog7: i64 = jq_parse(s7, ctxbox)
2382 let c7: *i64 = jq_ctx(ctxbox)
2383 var r7: i64 = 1
2384 if jq_haserr(ctxbox) == 1 { r7 = 0 }
2385 if r7 == 1 {
2386 let st: i64 = jp_child_at(c7, prog7, 0)
2387 let mul: i64 = jp_na(c7, st)
2388 if jp_nkind(c7, mul) != ND_BINARY { r7 = 0 }
2389 if r7 == 1 { if jp_nextra(c7, mul) != OP_MUL { r7 = 0 } }
2390 if r7 == 1 {
2391 let lhs: i64 = jp_na(c7, mul)
2392 let rhs: i64 = jp_nb(c7, mul)
2393 if jp_nkind(c7, lhs) != ND_BINARY { r7 = 0 } // (1+2) on the LEFT now
2394 if r7 == 1 { if jp_nextra(c7, lhs) != OP_ADD { r7 = 0 } }
2395 if jp_nkind(c7, rhs) != ND_NUMBER { r7 = 0 } // 3 on the right
2396 }
2397 }
2398 if r7 == 1 { jq_puts(" PASS KAT7 parens (1+2)*3 -> *(+(1,2), 3)\n" as *u8); pass=pass+1 } else { jq_puts(" FAIL KAT7 parens\n" as *u8) }
2399 tot = tot + 1
2400
2401 // ---- KAT 8 (index + nested call chain): a[b].c(d, e) -> CALL(MEMBER(INDEX(a,b),c),[d,e]) ----
2402 let s8: *u8 = "a[b].c(d, e)\x00" as *u8
2403 let prog8: i64 = jq_parse(s8, ctxbox)
2404 let c8: *i64 = jq_ctx(ctxbox)
2405 var r8: i64 = 1
2406 if jq_haserr(ctxbox) == 1 { r8 = 0 }
2407 if r8 == 1 {
2408 let st: i64 = jp_child_at(c8, prog8, 0)
2409 let call: i64 = jp_na(c8, st)
2410 if jp_nkind(c8, call) != ND_CALL { r8 = 0 }
2411 if r8 == 1 { if jp_nc(c8, call) != 2 { r8 = 0 } } // 2 args
2412 if r8 == 1 {
2413 let mem: i64 = jp_na(c8, call)
2414 if jp_nkind(c8, mem) != ND_MEMBER { r8 = 0 }
2415 if r8 == 1 {
2416 let idx: i64 = jp_na(c8, mem)
2417 if jp_nkind(c8, idx) != ND_INDEX { r8 = 0 }
2418 if r8 == 1 {
2419 if jp_nkind(c8, jp_na(c8, idx)) != ND_IDENT { r8 = 0 } // a
2420 if jp_nkind(c8, jp_nb(c8, idx)) != ND_IDENT { r8 = 0 } // b
2421 }
2422 }
2423 }
2424 }
2425 if r8 == 1 { jq_puts(" PASS KAT8 a[b].c(d,e) -> CALL(MEMBER(INDEX(a,b),c),[d,e])\n" as *u8); pass=pass+1 } else { jq_puts(" FAIL KAT8 index/call chain\n" as *u8) }
2426 tot = tot + 1
2427
2428 // ---- KAT 9 (literals + unary families that had ZERO coverage before): assert
2429 // STRING / null (NULL) / true,false (BOOL) primary nodes AND typeof / unary
2430 // '-' / unary '+' unary-op codes. Council BLOCKER fix: these code paths
2431 // existed but no KAT ever asserted their nodes -> 'ALL gated' was false. ----
2432 var r9: i64 = 1
2433 // string literal "x" -- byte-built so the source escape is unambiguous.
2434 let s9str: *u8 = sys_mmap(8)
2435 s9str[0] = 34 as u8; s9str[1] = 120 as u8; s9str[2] = 34 as u8; s9str[3] = 0 as u8 // "x"
2436 let p9str: i64 = jq_parse(s9str, ctxbox)
2437 if jq_haserr(ctxbox) == 1 { r9 = 0 }
2438 if r9 == 1 { if jq_expr0_kind(ctxbox, p9str) != ND_STRING { r9 = 0 } }
2439 // null
2440 if r9 == 1 {
2441 let p: i64 = jq_parse("null\x00" as *u8, ctxbox)
2442 if jq_haserr(ctxbox) == 1 { r9 = 0 }
2443 if r9 == 1 { if jq_expr0_kind(ctxbox, p) != ND_NULL { r9 = 0 } }
2444 }
2445 // true -> BOOL extra=1
2446 if r9 == 1 {
2447 let p: i64 = jq_parse("true\x00" as *u8, ctxbox)
2448 if jq_haserr(ctxbox) == 1 { r9 = 0 }
2449 if r9 == 1 { if jq_expr0_kind(ctxbox, p) != ND_BOOL { r9 = 0 } }
2450 if r9 == 1 { if jq_expr0_op(ctxbox, p) != 1 { r9 = 0 } }
2451 }
2452 // false -> BOOL extra=0
2453 if r9 == 1 {
2454 let p: i64 = jq_parse("false\x00" as *u8, ctxbox)
2455 if jq_haserr(ctxbox) == 1 { r9 = 0 }
2456 if r9 == 1 { if jq_expr0_kind(ctxbox, p) != ND_BOOL { r9 = 0 } }
2457 if r9 == 1 { if jq_expr0_op(ctxbox, p) != 0 { r9 = 0 } }
2458 }
2459 if r9 == 1 { if jq_unop_is("typeof a\x00" as *u8, ctxbox, OP_TYPEOF) == 0 { r9 = 0 } }
2460 if r9 == 1 { if jq_unop_is("-a\x00" as *u8, ctxbox, OP_NEG) == 0 { r9 = 0 } }
2461 if r9 == 1 { if jq_unop_is("+a\x00" as *u8, ctxbox, OP_POS) == 0 { r9 = 0 } }
2462 if r9 == 1 { jq_puts(" PASS KAT9 literals STRING/null/true/false + typeof/-/+ unary\n" as *u8); pass=pass+1 } else { jq_puts(" FAIL KAT9 literals/unary coverage\n" as *u8) }
2463 tot = tot + 1
2464
2465 // ---- KAT 10 (operator families that had ZERO coverage): one BINARY node per
2466 // previously-untested operator -- '/' '%' (mul tier), '<' '<=' '>' '>='
2467 // (rel tier), '!=' '===' '!==' (eq tier). Makes 'ALL gated' honest. ----
2468 var r10: i64 = 1
2469 if jq_binop_is("a/b\x00" as *u8, ctxbox, OP_DIV) == 0 { r10 = 0 }
2470 if jq_binop_is("a%b\x00" as *u8, ctxbox, OP_MOD) == 0 { r10 = 0 }
2471 if jq_binop_is("a<b\x00" as *u8, ctxbox, OP_LT) == 0 { r10 = 0 }
2472 if jq_binop_is("a<=b\x00" as *u8, ctxbox, OP_LE) == 0 { r10 = 0 }
2473 if jq_binop_is("a>b\x00" as *u8, ctxbox, OP_GT) == 0 { r10 = 0 }
2474 if jq_binop_is("a>=b\x00" as *u8, ctxbox, OP_GE) == 0 { r10 = 0 }
2475 if jq_binop_is("a!=b\x00" as *u8, ctxbox, OP_NE) == 0 { r10 = 0 }
2476 if jq_binop_is("a===b\x00" as *u8, ctxbox, OP_SEQ) == 0 { r10 = 0 }
2477 if jq_binop_is("a!==b\x00" as *u8, ctxbox, OP_SNE) == 0 { r10 = 0 }
2478 if r10 == 1 { jq_puts(" PASS KAT10 op families / % < <= > >= != === !== all gated\n" as *u8); pass=pass+1 } else { jq_puts(" FAIL KAT10 op family coverage\n" as *u8) }
2479 tot = tot + 1
2480
2481 // ---- KAT 11 (ASI tamper -- the BLOCKER): adjacent statements on ONE LINE with
2482 // no separator MUST error ('a b', '1 2' are real-JS SyntaxErrors); but a
2483 // LINE BREAK between them is a valid ASI point and MUST parse as 2 stmts.
2484 // Proves we ENFORCE same-line separation yet still honor ASI (not just
2485 // reject-everything). ----
2486 var r11: i64 = 1
2487 // 'a b' -> error
2488 let p11a: i64 = jq_parse("a b\x00" as *u8, ctxbox)
2489 if jq_haserr(ctxbox) != 1 { r11 = 0 }
2490 // '1 2' -> error
2491 let p11b: i64 = jq_parse("1 2\x00" as *u8, ctxbox)
2492 if jq_haserr(ctxbox) != 1 { r11 = 0 }
2493 // 'a\nb' (newline separator) -> OK, 2 statements, no error
2494 let s11c: *u8 = sys_mmap(8)
2495 s11c[0] = 97 as u8; s11c[1] = 10 as u8; s11c[2] = 98 as u8; s11c[3] = 0 as u8 // a<LF>b
2496 let p11c: i64 = jq_parse(s11c, ctxbox)
2497 let c11c: *i64 = jq_ctx(ctxbox)
2498 if jq_haserr(ctxbox) == 1 { r11 = 0 }
2499 if r11 == 1 { if jp_nb(c11c, p11c) != 2 { r11 = 0 } } // exactly 2 statements
2500 if r11 == 1 { jq_puts(" PASS KAT11 ASI: 'a b'/'1 2' -> ERROR, 'a<LF>b' -> 2 stmts OK\n" as *u8); pass=pass+1 } else { jq_puts(" FAIL KAT11 ASI (adjacent same-line accepted, or newline rejected!)\n" as *u8) }
2501 tot = tot + 1
2502
2503 // ---- KAT 12 (lvalue tamper -- the BLOCKER): assignment to a non-lvalue MUST
2504 // error. '1 = 2' (NUMBER lhs) and '(a+b) = c' (BINARY lhs) are real-JS
2505 // 'Invalid left-hand side in assignment'. Also confirm a VALID lvalue
2506 // 'a.b = c' (MEMBER) still parses, so we reject only the invalid ones. ----
2507 var r12: i64 = 1
2508 let p12a: i64 = jq_parse("1 = 2\x00" as *u8, ctxbox)
2509 if jq_haserr(ctxbox) != 1 { r12 = 0 }
2510 let p12b: i64 = jq_parse("(a+b) = c\x00" as *u8, ctxbox)
2511 if jq_haserr(ctxbox) != 1 { r12 = 0 }
2512 // sanity: a valid lvalue target (member) still parses as ASSIGN, no error.
2513 let p12c: i64 = jq_parse("a.b = c\x00" as *u8, ctxbox)
2514 if jq_haserr(ctxbox) == 1 { r12 = 0 }
2515 if r12 == 1 { if jq_expr0_kind(ctxbox, p12c) != ND_ASSIGN { r12 = 0 } }
2516 if r12 == 1 { jq_puts(" PASS KAT12 lvalue: '1=2'/'(a+b)=c' -> ERROR, 'a.b=c' -> ASSIGN OK\n" as *u8); pass=pass+1 } else { jq_puts(" FAIL KAT12 lvalue (non-lvalue assignment accepted!)\n" as *u8) }
2517 tot = tot + 1
2518
2519 jq_puts("---- nx_js_parse gate: passed " as *u8); jq_putn(pass); jq_puts(" / " as *u8); jq_putn(tot); jq_puts("\n" as *u8)
2520 let lfd: i64 = sys_openat_append("knowledge/status/js_engine.log\x00" as *u8, 0x1a4)
2521 if lfd >= 0 {
2522 sys_write(lfd, "R-JS-PARSE organ=nx_js_parse kats=" as *u8, 34)
2523 jq_fdn(lfd, pass); sys_write(lfd, "/" as *u8, 1); jq_fdn(lfd, tot)
2524 if pass == tot { sys_write(lfd, " tamper=ok verdict=GREEN\n" as *u8, 25) }
2525 if pass != tot { sys_write(lfd, " tamper=?? verdict=RED\n" as *u8, 23) }
2526 sys_close(lfd)
2527 }
2528 if pass == tot { sys_exit(0); return 0 }
2529 sys_exit(1)
2530 return 1
2531}