nx_js_parse.nx source
↩ module page · 2485 lines · 124093 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 if jp_eat_punct(ctx, ":\x00" as *u8) == 0 { jp_set_err(ctx); go = 0 }
561 if go == 1 { val = jp_parse_assign(ctx) }
562 }
563 if go == 1 {
564 let prop: i64 = jp_new_node(ctx, ND_PROP, val, -1, -1, keytok, 0)
565 if count < ND_MAGIC_1024 { scratch[count] = prop }
566 count = count + 1
567 if jp_err(ctx) == 1 { go = 0 }
568 if go == 1 {
569 if jp_eat_punct(ctx, ",\x00" as *u8) == 1 {
570 // trailing comma before '}' is allowed: {a:1,}
571 if jp_is_punct(ctx, "}\x00" as *u8) == 1 { jp_advance(ctx); go = 0 }
572 } else {
573 if jp_eat_punct(ctx, "}\x00" as *u8) == 1 { go = 0 }
574 else { jp_set_err(ctx); go = 0 }
575 }
576 }
577 }
578 }
579 }
580 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
581 let start: i64 = jp_child_commit(ctx, scratch, count)
582 return jp_new_node(ctx, ND_OBJECT, start, count, -1, -1, 0)
583}
584
585// `new C(args)` -> ND_NEW. extra=0: a = the ND_CALL node (callee + args). extra=1: a = callee (bare `new C`).
586func jp_parse_new(ctx: *i64) -> i64 {
587 jp_advance(ctx) // consume 'new'
588 var callee: i64 = jp_parse_primary(ctx)
589 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
590 // MemberExpression tail: `new A.b.c(...)` / `new A[k](...)` -- the constructor is the FULL member chain
591 // (`.prop` and `[expr]`), but NOT a call '(' (that begins the `new` Arguments). Mirrors jp_parse_postfix
592 // minus the call tail. (Guarded-if form: NishiLang has no `else if`.)
593 var go: i64 = 1
594 while go == 1 {
595 if jp_err(ctx) == 1 { go = 0 }
596 if go == 1 {
597 if jp_is_punct(ctx, ".\x00" as *u8) == 1 {
598 jp_advance(ctx)
599 if jp_tok_kind(ctx) != JS_TOK_IDENT { if jp_tok_kind(ctx) != JS_TOK_KEYWORD { callee = jp_error_node(ctx); go = 0 } }
600 if go == 1 {
601 let prop: i64 = jp_new_node(ctx, ND_IDENT, -1, -1, -1, jp_cur(ctx), 0)
602 jp_advance(ctx)
603 callee = jp_new_node(ctx, ND_MEMBER, callee, prop, -1, -1, 0)
604 }
605 } else {
606 if jp_is_punct(ctx, "[\x00" as *u8) == 1 {
607 jp_advance(ctx)
608 let ix: i64 = jp_parse_expr(ctx)
609 if jp_eat_punct(ctx, "]\x00" as *u8) == 0 { callee = jp_error_node(ctx); go = 0 }
610 if go == 1 { callee = jp_new_node(ctx, ND_INDEX, callee, ix, -1, -1, 0) }
611 } else {
612 go = 0
613 }
614 }
615 }
616 }
617 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
618 if jp_is_punct(ctx, "(\x00" as *u8) == 1 {
619 let callnode: i64 = jp_parse_call_tail(ctx, callee, 0)
620 return jp_new_node(ctx, ND_NEW, callnode, -1, -1, -1, 0)
621 }
622 return jp_new_node(ctx, ND_NEW, callee, -1, -1, -1, 1)
623}
624// `class Name { constructor(params){body} m(params){body}... }` -> ND_CLASS (a=name, b=ctor ND_FUNC_DECL,
625// c=methods ND_BLOCK of method ND_FUNC_DECLs). Prototype methods are now KEPT (was: dropped). No extends/
626// static/getters yet (those decline gracefully at parse or eval). Method-node = ND_FUNC_DECL(name,params,body).
627func jp_parse_class_decl(ctx: *i64) -> i64 {
628 jp_advance(ctx) // consume 'class'
629 if jp_tok_kind(ctx) != JS_TOK_IDENT { return jp_error_node(ctx) }
630 let nametok: i64 = jp_cur(ctx)
631 let name: i64 = jp_new_node(ctx, ND_IDENT, -1, -1, -1, nametok, 0)
632 jp_advance(ctx)
633 // optional `extends <expr>` -- superclass is usually a bare ident (`extends A`) but may be a
634 // member (`extends React.Component`); parse a postfix so both shapes work. Stored in ND_CLASS extra.
635 var superidx: i64 = 0 - 1
636 if jp_is_kw(ctx, "extends\x00" as *u8) == 1 {
637 jp_advance(ctx)
638 superidx = jp_parse_postfix(ctx)
639 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
640 }
641 if jp_eat_punct(ctx, "{\x00" as *u8) == 0 { return jp_error_node(ctx) }
642 var cparams: i64 = 0 - 1
643 var cbody: i64 = 0 - 1
644 let mscratch: *i64 = sys_mmap(ND_MAGIC_1024 * 8) as *i64 // collected method func-decl nodes
645 var mcount: i64 = 0
646 var go: i64 = 1
647 while go == 1 {
648 if jp_is_punct(ctx, "}\x00" as *u8) == 1 { jp_advance(ctx); go = 0 }
649 else {
650 var isok: i64 = 0
651 if jp_tok_kind(ctx) == JS_TOK_IDENT { isok = 1 }
652 if jp_tok_kind(ctx) == JS_TOK_KEYWORD { isok = 1 }
653 if isok == 0 { jp_set_err(ctx); go = 0 }
654 if go == 1 {
655 let is_ctor: i64 = jp_is_lex(ctx, JS_TOK_IDENT, "constructor\x00" as *u8)
656 let mnametok: i64 = jp_cur(ctx)
657 jp_advance(ctx) // method name
658 let mparams: i64 = jp_parse_params(ctx)
659 let mbody: i64 = jp_parse_block(ctx)
660 if jp_err(ctx) == 1 { go = 0 }
661 if go == 1 {
662 if is_ctor == 1 { cparams = mparams; cbody = mbody }
663 else {
664 let mname: i64 = jp_new_node(ctx, ND_IDENT, -1, -1, -1, mnametok, 0)
665 let mfd: i64 = jp_new_node(ctx, ND_FUNC_DECL, mname, mparams, mbody, -1, 0)
666 if mcount < ND_MAGIC_1024 { mscratch[mcount] = mfd }
667 mcount = mcount + 1
668 }
669 }
670 }
671 }
672 }
673 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
674 if cparams == (0 - 1) {
675 if superidx >= 0 {
676 // DERIVED class with no explicit ctor -> synthesize `constructor(...N){ super(...N) }` so
677 // `new B(args)` runs the PARENT ctor with the forwarded args (spec's implicit derived ctor).
678 // N reuses the class-name token (a harmless local rest param; the body only spreads it).
679 let restp: i64 = jp_new_node(ctx, ND_IDENT, -1, -1, -1, nametok, 1) // rest param (extra=1=rest)
680 let pscr: *i64 = sys_mmap(8) as *i64
681 pscr[0] = restp
682 let pstart: i64 = jp_child_commit(ctx, pscr, 1)
683 cparams = jp_new_node(ctx, ND_PARAMS, pstart, 1, -1, -1, 0)
684 let argid: i64 = jp_new_node(ctx, ND_IDENT, -1, -1, -1, nametok, 0) // reference to N
685 let spread: i64 = jp_new_node(ctx, ND_SPREAD, argid, -1, -1, -1, 0) // ...N
686 let ascr: *i64 = sys_mmap(8) as *i64
687 ascr[0] = spread
688 let astart: i64 = jp_child_commit(ctx, ascr, 1)
689 let supnode: i64 = jp_new_node(ctx, ND_SUPER, -1, -1, -1, nametok, 0)
690 let scall: i64 = jp_new_node(ctx, ND_CALL, supnode, astart, 1, -1, 0) // super(...N)
691 let estmt: i64 = jp_new_node(ctx, ND_EXPR_STMT, scall, -1, -1, -1, 0)
692 let bscr: *i64 = sys_mmap(8) as *i64
693 bscr[0] = estmt
694 let bstart: i64 = jp_child_commit(ctx, bscr, 1)
695 cbody = jp_new_node(ctx, ND_BLOCK, bstart, 1, -1, -1, 0)
696 } else { // non-derived class, no ctor -> empty function
697 let scratch: *i64 = sys_mmap(8) as *i64
698 let ps: i64 = jp_child_commit(ctx, scratch, 0)
699 cparams = jp_new_node(ctx, ND_PARAMS, ps, 0, -1, -1, 0)
700 let bs: i64 = jp_child_commit(ctx, scratch, 0)
701 cbody = jp_new_node(ctx, ND_BLOCK, bs, 0, -1, -1, 0)
702 }
703 }
704 let ctorfd: i64 = jp_new_node(ctx, ND_FUNC_DECL, name, cparams, cbody, -1, 0)
705 let mstart: i64 = jp_child_commit(ctx, mscratch, mcount)
706 let methods: i64 = jp_new_node(ctx, ND_BLOCK, mstart, mcount, -1, -1, 0)
707 return jp_new_node(ctx, ND_CLASS, name, ctorfd, methods, -1, superidx)
708}
709// primary: literal / ident / parenthesized expr
710func jp_parse_primary(ctx: *i64) -> i64 {
711 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
712 let k: i64 = jp_tok_kind(ctx)
713 let tk: i64 = jp_cur(ctx)
714
715 if k == JS_TOK_NUMBER {
716 let n: i64 = jp_new_node(ctx, ND_NUMBER, -1, -1, -1, tk, 0)
717 jp_advance(ctx)
718 return n
719 }
720 if k == JS_TOK_STRING {
721 let n: i64 = jp_new_node(ctx, ND_STRING, -1, -1, -1, tk, 0)
722 jp_advance(ctx)
723 return n
724 }
725 if k == JS_TOK_TEMPLATE {
726 let n: i64 = jp_new_node(ctx, ND_TEMPLATE, -1, -1, -1, tk, 0)
727 jp_advance(ctx)
728 return n
729 }
730 if k == JS_TOK_REGEX {
731 let n: i64 = jp_new_node(ctx, ND_REGEX, -1, -1, -1, tk, 0)
732 jp_advance(ctx)
733 return n
734 }
735 if k == JS_TOK_IDENT {
736 let n: i64 = jp_new_node(ctx, ND_IDENT, -1, -1, -1, tk, 0)
737 jp_advance(ctx)
738 return n
739 }
740 if k == JS_TOK_KEYWORD {
741 if jp_is_kw(ctx, "true\x00" as *u8) == 1 {
742 let n: i64 = jp_new_node(ctx, ND_BOOL, -1, -1, -1, tk, 1)
743 jp_advance(ctx); return n
744 }
745 if jp_is_kw(ctx, "false\x00" as *u8) == 1 {
746 let n: i64 = jp_new_node(ctx, ND_BOOL, -1, -1, -1, tk, 0)
747 jp_advance(ctx); return n
748 }
749 if jp_is_kw(ctx, "this\x00" as *u8) == 1 {
750 let n: i64 = jp_new_node(ctx, ND_THIS, -1, -1, -1, tk, 0) // the this keyword; eval resolves via the frame binding
751 jp_advance(ctx); return n
752 }
753 if jp_is_kw(ctx, "super\x00" as *u8) == 1 {
754 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
755 jp_advance(ctx); return n
756 }
757 if jp_is_kw(ctx, "null\x00" as *u8) == 1 {
758 let n: i64 = jp_new_node(ctx, ND_NULL, -1, -1, -1, tk, 0)
759 jp_advance(ctx); return n
760 }
761 // FUNCTION EXPRESSION (R-JS-CLOSURE, rung 6): `function (params){body}` or named
762 // `function name(params){body}` in expression/primary position -- so `var f =
763 // function(x){...}` and `(function(){...})()` parse. Same ND_FUNC_DECL shape as a
764 // declaration (the evaluator builds a closure when it EVALUATES this node).
765 if jp_is_kw(ctx, "new\x00" as *u8) == 1 { return jp_parse_new(ctx) }
766 if jp_is_kw(ctx, "function\x00" as *u8) == 1 { return jp_parse_func_expr(ctx) }
767 // any other keyword in expression position is a syntax error (rung 1)
768 return jp_error_node(ctx)
769 }
770 if k == JS_TOK_PUNCT {
771 if jp_is_punct(ctx, "(\x00" as *u8) == 1 {
772 jp_advance(ctx)
773 let inner: i64 = jp_parse_expr(ctx)
774 if jp_eat_punct(ctx, ")\x00" as *u8) == 0 { return jp_error_node(ctx) }
775 return inner
776 }
777 // array literal in expression position: [ ... ]
778 if jp_is_punct(ctx, "[\x00" as *u8) == 1 { return jp_parse_array(ctx) }
779 // object literal in expression position: { ... } (statement-leading '{' is a
780 // BLOCK and never reaches here -- jp_parse_stmt dispatches it before expr parsing).
781 if jp_is_punct(ctx, "{\x00" as *u8) == 1 { return jp_parse_object(ctx) }
782 return jp_error_node(ctx)
783 }
784 // EOF / ERROR token / anything else -> error
785 return jp_error_node(ctx)
786}
787
788// postfix: left-assoc chain of .ident [expr] (args...) applied to a primary
789func jp_parse_postfix(ctx: *i64) -> i64 {
790 var node: i64 = jp_parse_primary(ctx)
791 var go: i64 = 1
792 while go == 1 {
793 if jp_err(ctx) == 1 { go = 0 }
794 if go == 1 {
795 // OPTIONAL CHAINING ?. -- dispatch on what follows: ?.ident (member) / ?.( (call) / ?.[ (index).
796 // vk: `window.CSS?.supports?.(...)`. ND_MEMBER/ND_INDEX/ND_CALL carry extra=1 => short-circuit on nullish.
797 if jp_is_punct(ctx, "?.\x00" as *u8) == 1 {
798 jp_advance(ctx) // consume ?.
799 if jp_is_punct(ctx, "(\x00" as *u8) == 1 {
800 node = jp_parse_call_tail(ctx, node, 1)
801 if jp_err(ctx) == 1 { go = 0 }
802 } else { if jp_is_punct(ctx, "[\x00" as *u8) == 1 {
803 jp_advance(ctx)
804 let ix2: i64 = jp_parse_expr(ctx)
805 if jp_eat_punct(ctx, "]\x00" as *u8) == 0 { node = jp_error_node(ctx); go = 0 }
806 if go == 1 { node = jp_new_node(ctx, ND_INDEX, node, ix2, -1, -1, 1) }
807 } else {
808 if jp_tok_kind(ctx) != JS_TOK_IDENT { if jp_tok_kind(ctx) != JS_TOK_KEYWORD { node = jp_error_node(ctx); go = 0 } }
809 if go == 1 {
810 let prop2: i64 = jp_new_node(ctx, ND_IDENT, -1, -1, -1, jp_cur(ctx), 0)
811 jp_advance(ctx)
812 node = jp_new_node(ctx, ND_MEMBER, node, prop2, -1, -1, 1)
813 }
814 } }
815 } else {
816 // member: .ident
817 var isdot: i64 = 0
818 if jp_is_punct(ctx, ".\x00" as *u8) == 1 { isdot = 1 }
819 if isdot == 1 {
820 jp_advance(ctx)
821 if jp_tok_kind(ctx) != JS_TOK_IDENT {
822 if jp_tok_kind(ctx) != JS_TOK_KEYWORD { node = jp_error_node(ctx); go = 0 }
823 }
824 if go == 1 {
825 let prop: i64 = jp_new_node(ctx, ND_IDENT, -1, -1, -1, jp_cur(ctx), 0)
826 jp_advance(ctx)
827 node = jp_new_node(ctx, ND_MEMBER, node, prop, -1, -1, 0)
828 }
829 } else {
830 // computed index: [expr]
831 if jp_is_punct(ctx, "[\x00" as *u8) == 1 {
832 jp_advance(ctx)
833 let ix: i64 = jp_parse_expr(ctx)
834 if jp_eat_punct(ctx, "]\x00" as *u8) == 0 { node = jp_error_node(ctx); go = 0 }
835 if go == 1 { node = jp_new_node(ctx, ND_INDEX, node, ix, -1, -1, 0) }
836 } else {
837 // call: ( args... )
838 if jp_is_punct(ctx, "(\x00" as *u8) == 1 {
839 node = jp_parse_call_tail(ctx, node, 0)
840 if jp_err(ctx) == 1 { go = 0 }
841 } else {
842 go = 0
843 }
844 }
845 }
846 }
847 }
848 }
849 // postfix increment/decrement: x++ / x-- (binds to the WHOLE member/index chain, one only).
850 // Valid only on a simple lvalue; `5++` stays un-consumed here and errors upstream.
851 // (ASI note: `a \n ++b` is treated as `a++ ... b` here -- line-agnostic; minified
852 // bundles have no newlines, named divergence from the spec's restricted production.)
853 if jp_err(ctx) == 0 {
854 if jp_is_lvalue(ctx, node) == 1 {
855 if jp_is_punct(ctx, "++\x00" as *u8) == 1 { jp_advance(ctx); node = jp_new_node(ctx, ND_UPDATE, node, -1, -1, -1, 3) }
856 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) } }
857 }
858 }
859 return node
860}
861
862// parse `( a , b , ... )` after the callee; build a CALL node whose
863// a=callee, b=child_start, c=child_count (args live in the child arena).
864// Args are buffered in local scratch then committed CONTIGUOUSLY (a nested call
865// inside an arg pushes into the shared arena mid-parse -- see jp_child_commit).
866func jp_parse_call_tail(ctx: *i64, callee: i64, opt: i64) -> i64 { // opt=1 -> optional call a?.(args)
867 jp_advance(ctx) // consume '('
868 let scratch: *i64 = sys_mmap(256 * 8) as *i64
869 var count: i64 = 0
870 // empty arg list?
871 if jp_is_punct(ctx, ")\x00" as *u8) == 1 {
872 jp_advance(ctx)
873 let start0: i64 = jp_child_commit(ctx, scratch, 0)
874 return jp_new_node(ctx, ND_CALL, callee, start0, 0, -1, opt)
875 }
876 var go: i64 = 1
877 while go == 1 {
878 var arg: i64 = 0
879 if jp_is_punct(ctx, "...\x00" as *u8) == 1 { // spread argument f(...xs)
880 jp_advance(ctx)
881 let inner: i64 = jp_parse_assign(ctx)
882 arg = jp_new_node(ctx, ND_SPREAD, inner, -1, -1, -1, 0)
883 } else {
884 arg = jp_parse_assign(ctx)
885 }
886 if count < 256 { scratch[count] = arg }
887 count = count + 1
888 if jp_err(ctx) == 1 { go = 0 }
889 if go == 1 {
890 if jp_eat_punct(ctx, ",\x00" as *u8) == 1 {
891 // continue to next arg
892 } else {
893 if jp_eat_punct(ctx, ")\x00" as *u8) == 1 { go = 0 }
894 else { jp_set_err(ctx); go = 0 }
895 }
896 }
897 }
898 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
899 let start: i64 = jp_child_commit(ctx, scratch, count)
900 return jp_new_node(ctx, ND_CALL, callee, start, count, -1, opt)
901}
902
903// is a node a valid update/assignment TARGET (simple lvalue)?
904// (jp_is_lvalue is defined ONCE, earlier in this file at :367. A byte-identical second
905// definition -- differing only in its parameter NAME (node vs idx) -- used to sit here,
906// the classic copy-paste/merge artefact. nx_cc accepted the redefinition silently and
907// picked a winner; removed 2026-07-31 with debt 1785447657.)
908// unary prefix: ! - + typeof ++ -- (right-recursive onto another unary)
909func jp_parse_unary(ctx: *i64) -> i64 {
910 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
911 // `yield [expr]` / `yield* expr` -- expression-position, lowest-binding-ish; operand optional
912 // (bare `yield` before ; ) } , ] is valid). Parsed everywhere for bundle compatibility (strict
913 // contextual legality is the engine's later concern, not the frontier's).
914 // `await expr` -- phase 3 stub: evaluate the operand and use its VALUE (exact for already-settled
915 // and synchronous values; true suspension rides the existing event loop -- declared later rung).
916 // Contextual like `async`: only when an expression actually follows (a bare `await` identifier
917 // before ) , ; ] } = stays an identifier).
918 var awk: i64 = 0
919 if jp_is_kw(ctx, "await\x00" as *u8) == 1 { awk = 1 }
920 if jp_is_lex(ctx, JS_TOK_IDENT, "await\x00" as *u8) == 1 { awk = 1 }
921 if awk == 1 {
922 let nkw: i64 = jp_kind_at(ctx, jp_cur(ctx) + 1)
923 var starts: i64 = 0
924 if nkw == JS_TOK_IDENT { starts = 1 }
925 if nkw == JS_TOK_NUMBER { starts = 1 }
926 if nkw == JS_TOK_STRING { starts = 1 }
927 if nkw == JS_TOK_KEYWORD { starts = 1 }
928 if jp_punct_at(ctx, jp_cur(ctx) + 1, "(\x00" as *u8) == 1 { starts = 1 }
929 if jp_punct_at(ctx, jp_cur(ctx) + 1, "[\x00" as *u8) == 1 { starts = 1 }
930 if starts == 1 { jp_advance(ctx); return jp_parse_unary(ctx) }
931 }
932 if jp_is_kw(ctx, "yield\x00" as *u8) == 1 {
933 jp_advance(ctx)
934 if jp_is_punct(ctx, "*\x00" as *u8) == 1 { jp_advance(ctx) }
935 var yop: i64 = 0 - 1
936 var bare: i64 = 0
937 if jp_is_punct(ctx, ";\x00" as *u8) == 1 { bare = 1 }
938 if jp_is_punct(ctx, ")\x00" as *u8) == 1 { bare = 1 }
939 if jp_is_punct(ctx, "}\x00" as *u8) == 1 { bare = 1 }
940 if jp_is_punct(ctx, ",\x00" as *u8) == 1 { bare = 1 }
941 if jp_is_punct(ctx, "]\x00" as *u8) == 1 { bare = 1 }
942 if bare == 0 { yop = jp_parse_assign(ctx) }
943 return jp_new_node(ctx, ND_YIELD, yop, -1, -1, -1, 0)
944 }
945 // prefix increment/decrement: ++x / --x ("++"/"--" are single lexer tokens, so the
946 // "+"/"-" checks below can never shadow them). Target must be a simple lvalue
947 // (IDENT/MEMBER/INDEX) -- `++5` is a SyntaxError in real JS, ERROR node here.
948 if jp_is_punct(ctx, "++\x00" as *u8) == 1 {
949 jp_advance(ctx)
950 let operand: i64 = jp_parse_unary(ctx)
951 if jp_is_lvalue(ctx, operand) == 0 { jp_set_err(ctx); return jp_error_node(ctx) }
952 return jp_new_node(ctx, ND_UPDATE, operand, -1, -1, -1, 1)
953 }
954 if jp_is_punct(ctx, "--\x00" as *u8) == 1 {
955 jp_advance(ctx)
956 let operand: i64 = jp_parse_unary(ctx)
957 if jp_is_lvalue(ctx, operand) == 0 { jp_set_err(ctx); return jp_error_node(ctx) }
958 return jp_new_node(ctx, ND_UPDATE, operand, -1, -1, -1, 2)
959 }
960 if jp_is_punct(ctx, "!\x00" as *u8) == 1 {
961 jp_advance(ctx)
962 let operand: i64 = jp_parse_unary(ctx)
963 return jp_new_node(ctx, ND_UNARY, operand, -1, -1, -1, OP_NOT)
964 }
965 if jp_is_punct(ctx, "~\x00" as *u8) == 1 {
966 jp_advance(ctx)
967 let operand: i64 = jp_parse_unary(ctx)
968 return jp_new_node(ctx, ND_UNARY, operand, -1, -1, -1, OP_BNOT)
969 }
970 if jp_is_punct(ctx, "-\x00" as *u8) == 1 {
971 jp_advance(ctx)
972 let operand: i64 = jp_parse_unary(ctx)
973 return jp_new_node(ctx, ND_UNARY, operand, -1, -1, -1, OP_NEG)
974 }
975 if jp_is_punct(ctx, "+\x00" as *u8) == 1 {
976 jp_advance(ctx)
977 let operand: i64 = jp_parse_unary(ctx)
978 return jp_new_node(ctx, ND_UNARY, operand, -1, -1, -1, OP_POS)
979 }
980 if jp_is_kw(ctx, "typeof\x00" as *u8) == 1 {
981 jp_advance(ctx)
982 let operand: i64 = jp_parse_unary(ctx)
983 return jp_new_node(ctx, ND_UNARY, operand, -1, -1, -1, OP_TYPEOF)
984 }
985 if jp_is_kw(ctx, "void\x00" as *u8) == 1 {
986 jp_advance(ctx)
987 let operand: i64 = jp_parse_unary(ctx)
988 return jp_new_node(ctx, ND_UNARY, operand, -1, -1, -1, OP_VOID)
989 }
990 if jp_is_kw(ctx, "delete\x00" as *u8) == 1 {
991 jp_advance(ctx)
992 let operand: i64 = jp_parse_unary(ctx) // a MEMBER/INDEX ref; the evaluator inspects the node, not its value
993 return jp_new_node(ctx, ND_UNARY, operand, -1, -1, -1, OP_DELETE)
994 }
995 return jp_parse_postfix(ctx)
996}
997
998// ---- binary operator tier helper: returns the op-code for the current token at a
999// given precedence tier, or 0 if the current token is not an op of that tier.
1000// Tiers are checked by dedicated functions to keep else-if nesting shallow. ----
1001
1002// tier 1 (highest binary): * / %
1003func jp_mul_op(ctx: *i64) -> i64 {
1004 if jp_tok_kind(ctx) != JS_TOK_PUNCT { return 0 }
1005 if jp_is_punct(ctx, "*\x00" as *u8) == 1 { return OP_MUL }
1006 if jp_is_punct(ctx, "/\x00" as *u8) == 1 { return OP_DIV }
1007 if jp_is_punct(ctx, "%\x00" as *u8) == 1 { return OP_MOD }
1008 return 0
1009}
1010// tier 2: + -
1011func jp_add_op(ctx: *i64) -> i64 {
1012 if jp_tok_kind(ctx) != JS_TOK_PUNCT { return 0 }
1013 if jp_is_punct(ctx, "+\x00" as *u8) == 1 { return OP_ADD }
1014 if jp_is_punct(ctx, "-\x00" as *u8) == 1 { return OP_SUB }
1015 return 0
1016}
1017// tier 3: < <= > >= (NOTE: must check the 2-char forms first via lexeme compare;
1018// the lexer already produced "<=" as ONE punct token, so exact-match is safe.)
1019static jp_noin: i64 // 1 while parsing a for-init EXPRESSION: `in` is the for-in separator, NOT the operator
1020func jp_rel_op(ctx: *i64) -> i64 {
1021 // `instanceof`/`in` are KEYWORD tokens (not punct) at the SAME precedence tier as < > (real JS RelationalExpr).
1022 if jp_is_kw(ctx, "instanceof\x00" as *u8) == 1 { return OP_INSTANCEOF }
1023 if jp_is_kw(ctx, "in\x00" as *u8) == 1 { if jp_noin == 0 { return OP_IN } return 0 }
1024 if jp_tok_kind(ctx) != JS_TOK_PUNCT { return 0 }
1025 if jp_is_punct(ctx, "<=\x00" as *u8) == 1 { return OP_LE }
1026 if jp_is_punct(ctx, ">=\x00" as *u8) == 1 { return OP_GE }
1027 if jp_is_punct(ctx, "<\x00" as *u8) == 1 { return OP_LT }
1028 if jp_is_punct(ctx, ">\x00" as *u8) == 1 { return OP_GT }
1029 return 0
1030}
1031// tier 4: == != === !==
1032func jp_eq_op(ctx: *i64) -> i64 {
1033 if jp_tok_kind(ctx) != JS_TOK_PUNCT { return 0 }
1034 if jp_is_punct(ctx, "===\x00" as *u8) == 1 { return OP_SEQ }
1035 if jp_is_punct(ctx, "!==\x00" as *u8) == 1 { return OP_SNE }
1036 if jp_is_punct(ctx, "==\x00" as *u8) == 1 { return OP_EQ }
1037 if jp_is_punct(ctx, "!=\x00" as *u8) == 1 { return OP_NE }
1038 return 0
1039}
1040
1041// Generic left-assoc binary builder is awkward without function pointers, so each
1042// tier is its own small function that loops while its op matches, recursing to the
1043// NEXT-HIGHER tier for each operand. Correct precedence = the recursion order;
1044// correct (left) associativity = the while-loop reassigning `left`.
1045
1046func jp_parse_mul(ctx: *i64) -> i64 {
1047 var left: i64 = jp_parse_unary(ctx)
1048 var go: i64 = 1
1049 while go == 1 {
1050 if jp_err(ctx) == 1 { go = 0 }
1051 if go == 1 {
1052 let op: i64 = jp_mul_op(ctx)
1053 if op == 0 { go = 0 } else {
1054 jp_advance(ctx)
1055 let right: i64 = jp_parse_unary(ctx)
1056 left = jp_new_node(ctx, ND_BINARY, left, right, -1, -1, op)
1057 }
1058 }
1059 }
1060 return left
1061}
1062func jp_parse_add(ctx: *i64) -> i64 {
1063 var left: i64 = jp_parse_mul(ctx)
1064 var go: i64 = 1
1065 while go == 1 {
1066 if jp_err(ctx) == 1 { go = 0 }
1067 if go == 1 {
1068 let op: i64 = jp_add_op(ctx)
1069 if op == 0 { go = 0 } else {
1070 jp_advance(ctx)
1071 let right: i64 = jp_parse_mul(ctx)
1072 left = jp_new_node(ctx, ND_BINARY, left, right, -1, -1, op)
1073 }
1074 }
1075 }
1076 return left
1077}
1078// SHIFT: << >> >>> -- precedence between additive and relational (JS). Distinct lexer tokens, so no
1079// confusion with < / > (relational). Uses variable shift in eval (immediate-shift lexer gotcha).
1080func jp_parse_shift(ctx: *i64) -> i64 {
1081 var left: i64 = jp_parse_add(ctx)
1082 var go: i64 = 1
1083 while go == 1 {
1084 if jp_err(ctx) == 1 { go = 0 }
1085 if go == 1 {
1086 var op: i64 = 0
1087 if jp_is_punct(ctx, "<<\x00" as *u8) == 1 { op = OP_SHL }
1088 if jp_is_punct(ctx, ">>>\x00" as *u8) == 1 { op = OP_USHR }
1089 if op == 0 { if jp_is_punct(ctx, ">>\x00" as *u8) == 1 { op = OP_SHR } }
1090 if op == 0 { go = 0 } else {
1091 jp_advance(ctx)
1092 let right: i64 = jp_parse_add(ctx)
1093 left = jp_new_node(ctx, ND_BINARY, left, right, -1, -1, op)
1094 }
1095 }
1096 }
1097 return left
1098}
1099func jp_parse_rel(ctx: *i64) -> i64 {
1100 var left: i64 = jp_parse_shift(ctx)
1101 var go: i64 = 1
1102 while go == 1 {
1103 if jp_err(ctx) == 1 { go = 0 }
1104 if go == 1 {
1105 let op: i64 = jp_rel_op(ctx)
1106 if op == 0 { go = 0 } else {
1107 jp_advance(ctx)
1108 let right: i64 = jp_parse_shift(ctx)
1109 left = jp_new_node(ctx, ND_BINARY, left, right, -1, -1, op)
1110 }
1111 }
1112 }
1113 return left
1114}
1115func jp_parse_eq(ctx: *i64) -> i64 {
1116 var left: i64 = jp_parse_rel(ctx)
1117 var go: i64 = 1
1118 while go == 1 {
1119 if jp_err(ctx) == 1 { go = 0 }
1120 if go == 1 {
1121 let op: i64 = jp_eq_op(ctx)
1122 if op == 0 { go = 0 } else {
1123 jp_advance(ctx)
1124 let right: i64 = jp_parse_rel(ctx)
1125 left = jp_new_node(ctx, ND_BINARY, left, right, -1, -1, op)
1126 }
1127 }
1128 }
1129 return left
1130}
1131// BITWISE AND / XOR / OR -- precedence & > ^ > | , all below equality and above && (JS). Single-char
1132// lexer tokens, never confused with && / || (2-char tokens).
1133func jp_parse_bitand(ctx: *i64) -> i64 {
1134 var left: i64 = jp_parse_eq(ctx)
1135 var go: i64 = 1
1136 while go == 1 {
1137 if jp_err(ctx) == 1 { go = 0 }
1138 if go == 1 {
1139 if jp_is_punct(ctx, "&\x00" as *u8) == 1 {
1140 jp_advance(ctx)
1141 let right: i64 = jp_parse_eq(ctx)
1142 left = jp_new_node(ctx, ND_BINARY, left, right, -1, -1, OP_BAND)
1143 } else { go = 0 }
1144 }
1145 }
1146 return left
1147}
1148func jp_parse_bitxor(ctx: *i64) -> i64 {
1149 var left: i64 = jp_parse_bitand(ctx)
1150 var go: i64 = 1
1151 while go == 1 {
1152 if jp_err(ctx) == 1 { go = 0 }
1153 if go == 1 {
1154 if jp_is_punct(ctx, "^\x00" as *u8) == 1 {
1155 jp_advance(ctx)
1156 let right: i64 = jp_parse_bitand(ctx)
1157 left = jp_new_node(ctx, ND_BINARY, left, right, -1, -1, OP_BXOR)
1158 } else { go = 0 }
1159 }
1160 }
1161 return left
1162}
1163func jp_parse_bitor(ctx: *i64) -> i64 {
1164 var left: i64 = jp_parse_bitxor(ctx)
1165 var go: i64 = 1
1166 while go == 1 {
1167 if jp_err(ctx) == 1 { go = 0 }
1168 if go == 1 {
1169 if jp_is_punct(ctx, "|\x00" as *u8) == 1 {
1170 jp_advance(ctx)
1171 let right: i64 = jp_parse_bitxor(ctx)
1172 left = jp_new_node(ctx, ND_BINARY, left, right, -1, -1, OP_BOR)
1173 } else { go = 0 }
1174 }
1175 }
1176 return left
1177}
1178func jp_parse_and(ctx: *i64) -> i64 {
1179 var left: i64 = jp_parse_bitor(ctx)
1180 var go: i64 = 1
1181 while go == 1 {
1182 if jp_err(ctx) == 1 { go = 0 }
1183 if go == 1 {
1184 if jp_is_punct(ctx, "&&\x00" as *u8) == 1 {
1185 jp_advance(ctx)
1186 let right: i64 = jp_parse_bitor(ctx)
1187 left = jp_new_node(ctx, ND_BINARY, left, right, -1, -1, OP_AND)
1188 } else { go = 0 }
1189 }
1190 }
1191 return left
1192}
1193func jp_parse_or(ctx: *i64) -> i64 {
1194 var left: i64 = jp_parse_and(ctx)
1195 var go: i64 = 1
1196 while go == 1 {
1197 if jp_err(ctx) == 1 { go = 0 }
1198 if go == 1 {
1199 if jp_is_punct(ctx, "||\x00" as *u8) == 1 {
1200 jp_advance(ctx)
1201 let right: i64 = jp_parse_and(ctx)
1202 left = jp_new_node(ctx, ND_BINARY, left, right, -1, -1, OP_OR)
1203 } else { go = 0 }
1204 }
1205 }
1206 return left
1207}
1208
1209// compound-assignment operator code at the cursor (the BINARY op to fold in), or 0 if
1210// the current token is not a compound assign. The lexer produced "+=" "-=" etc. as
1211// SINGLE multi-char punct tokens (js_punct_len), so an exact-lexeme compare is safe and
1212// can never be confused with bare '=' / '==' / '==='.
1213func jp_compound_op(ctx: *i64) -> i64 {
1214 if jp_tok_kind(ctx) != JS_TOK_PUNCT { return 0 }
1215 if jp_is_punct(ctx, "+=\x00" as *u8) == 1 { return OP_ADD }
1216 if jp_is_punct(ctx, "-=\x00" as *u8) == 1 { return OP_SUB }
1217 if jp_is_punct(ctx, "*=\x00" as *u8) == 1 { return OP_MUL }
1218 if jp_is_punct(ctx, "/=\x00" as *u8) == 1 { return OP_DIV }
1219 if jp_is_punct(ctx, "%=\x00" as *u8) == 1 { return OP_MOD }
1220 // bitwise/shift compounds (Octane Crypto/jsbn: `c >>= ...`). The lexer already emits these as single
1221 // puncts (js_punct_len), and the fold rides the SAME aop path as += (ev_apply_binop / BC_BINOP).
1222 if jp_is_punct(ctx, "&=\x00" as *u8) == 1 { return OP_BAND }
1223 if jp_is_punct(ctx, "|=\x00" as *u8) == 1 { return OP_BOR }
1224 if jp_is_punct(ctx, "^=\x00" as *u8) == 1 { return OP_BXOR }
1225 if jp_is_punct(ctx, "<<=\x00" as *u8) == 1 { return OP_SHL }
1226 if jp_is_punct(ctx, ">>=\x00" as *u8) == 1 { return OP_SHR }
1227 if jp_is_punct(ctx, ">>>=\x00" as *u8) == 1 { return OP_USHR }
1228 return 0
1229}
1230
1231// ternary `cond ? then : else` -- RIGHT-assoc, just ABOVE assignment, just BELOW the
1232// OR tier (real JS: ConditionalExpression). The branches are assignment-expressions
1233// (so `a ? b : c = d` parses the false branch as an assignment, real JS). Returns the
1234// OR-level node unchanged when no '?' follows, so all lower KATs are untouched.
1235// nullish coalescing `a ?? b` -- short-circuit; tier just BELOW ternary, ABOVE ||. Operands are
1236// OR-level expressions (the common `a ?? b` parses correctly; ?? mixing with ||/&& needs parens in real JS).
1237func jp_parse_nullish(ctx: *i64) -> i64 {
1238 var left: i64 = jp_parse_or(ctx)
1239 var go: i64 = 1
1240 while go == 1 {
1241 if jp_err(ctx) == 1 { go = 0 }
1242 if go == 1 {
1243 if jp_is_punct(ctx, "??\x00" as *u8) == 1 {
1244 jp_advance(ctx)
1245 let right: i64 = jp_parse_or(ctx)
1246 left = jp_new_node(ctx, ND_BINARY, left, right, -1, -1, OP_NULLISH)
1247 } else { go = 0 }
1248 }
1249 }
1250 return left
1251}
1252func jp_parse_ternary(ctx: *i64) -> i64 {
1253 let cond: i64 = jp_parse_nullish(ctx)
1254 if jp_err(ctx) == 1 { return cond }
1255 if jp_is_punct(ctx, "?\x00" as *u8) == 1 {
1256 jp_advance(ctx)
1257 let then_e: i64 = jp_parse_assign(ctx) // then-branch is an assignment-expr
1258 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1259 if jp_eat_punct(ctx, ":\x00" as *u8) == 0 { return jp_error_node(ctx) }
1260 let else_e: i64 = jp_parse_assign(ctx) // else-branch right-recurses (right-assoc)
1261 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1262 return jp_new_node(ctx, ND_TERNARY, cond, then_e, else_e, -1, 0)
1263 }
1264 return cond
1265}
1266
1267// ===================== arrow functions (R-JS-CLOSURE, rung 6) =====================
1268// An arrow desugars to the SAME node shape as a function decl/expression -- ND_FUNC_DECL
1269// (a=name|-1, b=params, c=body BLOCK) -- so the evaluator's ONE js_call_userfn path runs
1270// both. Arrows are anonymous (name = -1). The lexical capture happens in the evaluator
1271// (the closure records the env where the literal is evaluated); the parser only produces
1272// the same params+body skeleton, so an arrow is lexically identical to `function(){}`.
1273
1274// wrap a single statement node into a synthetic BLOCK list-node (a=child_start, b=1).
1275// Used for an arrow EXPRESSION body: `x => expr` desugars to a body `{ return expr; }`.
1276func jp_wrap_block1(ctx: *i64, stmt: i64) -> i64 {
1277 let start: i64 = jp_child_push(ctx, stmt)
1278 return jp_new_node(ctx, ND_BLOCK, start, 1, -1, -1, 0)
1279}
1280
1281// parse a PARENTHESIZED arrow param list `( a , b , ... )` (idents only; empty () ok)
1282// -> ND_PARAMS. Identical shape to jp_parse_params (reused), kept as its own call so the
1283// arrow path reads clearly. Default/rest/destructured params are a NAMED OPEN (idents only).
1284func jp_parse_arrow_params(ctx: *i64) -> i64 { return jp_parse_params(ctx) }
1285
1286// parse an arrow BODY after the `=>` has been consumed: a BLOCK body `{ stmts }` runs as-is;
1287// an EXPRESSION body `expr` desugars to `{ return expr; }` (the implicit-return rule). The
1288// expression body is an ASSIGNMENT-expr (so `x => y = 1` parses `y=1` as the body, real JS).
1289func jp_parse_arrow_body(ctx: *i64) -> i64 {
1290 if jp_is_punct(ctx, "{\x00" as *u8) == 1 { return jp_parse_block(ctx) }
1291 let e: i64 = jp_parse_assign(ctx)
1292 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1293 let ret: i64 = jp_new_node(ctx, ND_RETURN, e, -1, -1, -1, 0)
1294 return jp_wrap_block1(ctx, ret)
1295}
1296
1297// SINGLE-PARAM arrow `x => body`. Cursor is on the IDENT (its next token is '=>').
1298func jp_parse_arrow_single(ctx: *i64) -> i64 {
1299 let pn: i64 = jp_new_node(ctx, ND_IDENT, -1, -1, -1, jp_cur(ctx), 0)
1300 jp_advance(ctx) // consume the param ident
1301 let start: i64 = jp_child_push(ctx, pn)
1302 let params: i64 = jp_new_node(ctx, ND_PARAMS, start, 1, -1, -1, 0)
1303 if jp_eat_punct(ctx, "=>\x00" as *u8) == 0 { return jp_error_node(ctx) }
1304 let body: i64 = jp_parse_arrow_body(ctx)
1305 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1306 return jp_new_node(ctx, ND_FUNC_DECL, -1, params, body, -1, 0) // name=-1 (anonymous)
1307}
1308
1309// PAREN-LIST arrow `( a, b ) => body`. Cursor is on '(' (jp_arrow_paren_ahead confirmed
1310// a following '=>'). Parse the param idents, eat '=>', parse the body.
1311func jp_parse_arrow_paren(ctx: *i64) -> i64 {
1312 let params: i64 = jp_parse_arrow_params(ctx)
1313 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1314 if jp_eat_punct(ctx, "=>\x00" as *u8) == 0 { return jp_error_node(ctx) }
1315 let body: i64 = jp_parse_arrow_body(ctx)
1316 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1317 return jp_new_node(ctx, ND_FUNC_DECL, -1, params, body, -1, 0)
1318}
1319
1320// 1 iff an ARROW function begins at the cursor: `x =>` (IDENT then '=>') or `( ... ) =>`
1321// (the cover-grammar paren case resolved by jp_arrow_paren_ahead). Used to fork at the
1322// TOP of jp_parse_assign so the arrow's params are not first mis-parsed as an expression.
1323func jp_arrow_ahead(ctx: *i64) -> i64 {
1324 if jp_tok_kind(ctx) == JS_TOK_IDENT {
1325 if jp_punct_at(ctx, jp_cur(ctx) + 1, "=>\x00" as *u8) == 1 { return 1 }
1326 }
1327 if jp_is_punct(ctx, "(\x00" as *u8) == 1 {
1328 if jp_arrow_paren_ahead(ctx) == 1 { return 1 }
1329 }
1330 return 0
1331}
1332
1333// assignment: RIGHT-associative, LOWEST precedence. `lhs = rhs` (or `lhs += rhs` etc.)
1334// where rhs is itself an assignment (so a=b=c nests right). lhs is whatever the ternary
1335// tier parsed. Compound assigns store the BINARY op-code in the ND_ASSIGN `extra` slot
1336// (0 = plain '='); the evaluator reads lhs, folds the op with rhs, writes back.
1337func jp_parse_assign(ctx: *i64) -> i64 {
1338 // ASYNC ARROWS `async (a,b)=>{…}` / `async x=>…` (JS-SOTA phase 3; the netflix frontier after
1339 // object spread: `a=e=>async(t,n)=>{…}`). `async` is CONTEXTUAL -- `async(1)` is a CALL to a
1340 // function named async -- so only consume it when a real arrow follows; otherwise RESTORE the
1341 // cursor and let it parse as a plain identifier. Async-ness itself is not yet modelled (the
1342 // body runs synchronously and returns its value, not a promise) -- an honest, declared stub.
1343 if jp_is_lex(ctx, JS_TOK_IDENT, "async\x00" as *u8) == 1 {
1344 let asave: i64 = jp_cur(ctx)
1345 jp_set_cur(ctx, asave + 1)
1346 var aok: i64 = 0
1347 if jp_is_punct(ctx, "(\x00" as *u8) == 1 { if jp_arrow_paren_ahead(ctx) == 1 { aok = 1 } }
1348 if jp_kind_at(ctx, asave + 1) == JS_TOK_IDENT { if jp_punct_at(ctx, asave + 2, "=>\x00" as *u8) == 1 { aok = 1 } }
1349 if aok == 0 { jp_set_cur(ctx, asave) }
1350 }
1351 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1352 // ARROW FUNCTION fork (lowest precedence, right-assoc): detect `x =>` / `(...) =>`
1353 // BEFORE treating the head as an expression. The body recurses through assignment
1354 // (jp_parse_arrow_body), so `x => y => x+y` (curried) right-nests naturally.
1355 if jp_arrow_ahead(ctx) == 1 {
1356 if jp_tok_kind(ctx) == JS_TOK_IDENT { return jp_parse_arrow_single(ctx) }
1357 return jp_parse_arrow_paren(ctx)
1358 }
1359 let lhs: i64 = jp_parse_ternary(ctx)
1360 if jp_err(ctx) == 1 { return lhs }
1361 // bare '=' -- must NOT match == or === (DISTINCT multi-char punct tokens), so an
1362 // exact-lexeme compare against "=" is unambiguous.
1363 if jp_is_punct(ctx, "=\x00" as *u8) == 1 {
1364 // Real JS: the assignment target MUST be an lvalue (IDENT / MEMBER / INDEX).
1365 // '1 = 2' or '(a+b) = c' -> SyntaxError: Invalid left-hand side in assignment.
1366 if jp_is_lvalue(ctx, lhs) == 0 { return jp_error_node(ctx) }
1367 jp_advance(ctx)
1368 let rhs: i64 = jp_parse_assign(ctx) // right-recursion = right associativity
1369 return jp_new_node(ctx, ND_ASSIGN, lhs, rhs, -1, -1, 0)
1370 }
1371 // compound assign lhs (+= -= *= /= %=) rhs -> ND_ASSIGN with op in extra slot.
1372 let cop: i64 = jp_compound_op(ctx)
1373 if cop != 0 {
1374 if jp_is_lvalue(ctx, lhs) == 0 { return jp_error_node(ctx) }
1375 jp_advance(ctx)
1376 let rhs: i64 = jp_parse_assign(ctx)
1377 return jp_new_node(ctx, ND_ASSIGN, lhs, rhs, -1, -1, cop)
1378 }
1379 return lhs
1380}
1381
1382// top-level expression = assignment (which descends through all binary tiers).
1383func jp_parse_expr(ctx: *i64) -> i64 {
1384 let first: i64 = jp_parse_assign(ctx)
1385 if jp_err(ctx) == 1 { return first }
1386 if jp_is_punct(ctx, ",\x00" as *u8) == 0 { return first } // no comma = single expr (the common case, 0 overhead)
1387 // sequence operator `a, b, c` -> ND_SEQ (eval each in order, value = last). All jp_parse_expr callers are
1388 // sequence-valid contexts (for-header, (), stmt, cond, return, switch); list-commas use jp_parse_assign.
1389 let scratch: *i64 = sys_mmap(256 * 8) as *i64
1390 scratch[0] = first
1391 var count: i64 = 1
1392 while jp_is_punct(ctx, ",\x00" as *u8) == 1 {
1393 jp_advance(ctx)
1394 let e: i64 = jp_parse_assign(ctx)
1395 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1396 if count < 256 { scratch[count] = e }
1397 count = count + 1
1398 }
1399 let cs: i64 = jp_child_commit(ctx, scratch, count)
1400 return jp_new_node(ctx, ND_SEQ, cs, count, -1, -1, 0)
1401}
1402
1403// ===================== statement parsing =====================
1404// forward refs: jp_parse_stmt is mutually recursive with block/if/while.
1405
1406// var/let/const decl: KEYWORD IDENT [ = expr ] [ ; ]
1407// array/object destructuring PATTERNS for a var-decl target. Shorthand names only ([a,b] / {a,b});
1408// renaming {a:b}, defaults in patterns, and nested patterns are a NAMED OPEN.
1409func jp_parse_array_pattern(ctx: *i64) -> i64 {
1410 jp_advance(ctx) // '['
1411 let scratch: *i64 = sys_mmap(256 * 8) as *i64
1412 var count: i64 = 0
1413 var go: i64 = 1
1414 if jp_is_punct(ctx, "]\x00" as *u8) == 1 { jp_advance(ctx); go = 0 }
1415 while go == 1 {
1416 var nm: i64 = 0 - 1
1417 // NESTED patterns: [e, [t,n], {x}] -- vk uses `for(let [e,[t,n]] of ...)`.
1418 if jp_is_punct(ctx, "[\x00" as *u8) == 1 { nm = jp_parse_array_pattern(ctx); if jp_err(ctx) == 1 { go = 0 } }
1419 else { if jp_is_punct(ctx, "{\x00" as *u8) == 1 { nm = jp_parse_object_pattern(ctx); if jp_err(ctx) == 1 { go = 0 } }
1420 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 } }
1421 else {
1422 if jp_tok_kind(ctx) != JS_TOK_IDENT { jp_set_err(ctx); go = 0 }
1423 if go == 1 { nm = jp_new_node(ctx, ND_IDENT, -1, -1, -1, jp_cur(ctx), 0); jp_advance(ctx) }
1424 } } }
1425 if go == 1 {
1426 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)
1427 if go == 1 {
1428 if count < 256 { scratch[count] = nm }
1429 count = count + 1
1430 if jp_eat_punct(ctx, ",\x00" as *u8) == 1 { }
1431 else { if jp_eat_punct(ctx, "]\x00" as *u8) == 1 { go = 0 } else { jp_set_err(ctx); go = 0 } }
1432 }
1433 }
1434 }
1435 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1436 let start: i64 = jp_child_commit(ctx, scratch, count)
1437 return jp_new_node(ctx, ND_ARRAY_PAT, start, count, -1, -1, 0)
1438}
1439func jp_parse_object_pattern(ctx: *i64) -> i64 {
1440 jp_advance(ctx) // '{'
1441 let scratch: *i64 = sys_mmap(256 * 8) as *i64
1442 var count: i64 = 0
1443 var go: i64 = 1
1444 if jp_is_punct(ctx, "}\x00" as *u8) == 1 { jp_advance(ctx); go = 0 }
1445 while go == 1 {
1446 if jp_tok_kind(ctx) != JS_TOK_IDENT { jp_set_err(ctx); go = 0 }
1447 if go == 1 {
1448 let keytok: i64 = jp_cur(ctx) // source property KEY
1449 jp_advance(ctx)
1450 var alias: i64 = 0 - 1
1451 if jp_is_punct(ctx, ":\x00" as *u8) == 1 { // {key: localName} or {key: [a,b]} / {key: {x}} (nested)
1452 jp_advance(ctx)
1453 if jp_is_punct(ctx, "[\x00" as *u8) == 1 { alias = jp_parse_array_pattern(ctx); if jp_err(ctx) == 1 { go = 0 } }
1454 else { if jp_is_punct(ctx, "{\x00" as *u8) == 1 { alias = jp_parse_object_pattern(ctx); if jp_err(ctx) == 1 { go = 0 } }
1455 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) }
1456 else { jp_set_err(ctx); go = 0 } } }
1457 }
1458 if go == 1 { if jp_is_punct(ctx, "=\x00" as *u8) == 1 { // {x = default} -- parse+ignore default (rare)
1459 jp_advance(ctx)
1460 jp_parse_assign(ctx)
1461 if jp_err(ctx) == 1 { go = 0 }
1462 } }
1463 if go == 1 {
1464 let nm: i64 = jp_new_node(ctx, ND_IDENT, alias, -1, -1, keytok, 0) // tok=KEY; slot-a=ALIAS node (-1 if bare)
1465 if count < 256 { scratch[count] = nm }
1466 count = count + 1
1467 if jp_eat_punct(ctx, ",\x00" as *u8) == 1 { }
1468 else { if jp_eat_punct(ctx, "}\x00" as *u8) == 1 { go = 0 } else { jp_set_err(ctx); go = 0 } }
1469 }
1470 }
1471 }
1472 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1473 let start: i64 = jp_child_commit(ctx, scratch, count)
1474 return jp_new_node(ctx, ND_OBJ_PAT, start, count, -1, -1, 0)
1475}
1476// parse ONE declarator `name [= init]` (no var keyword, no terminator) -> an ND_VAR_DECL node.
1477func jp_parse_one_declarator(ctx: *i64) -> i64 {
1478 var name: i64 = 0
1479 if jp_is_punct(ctx, "[\x00" as *u8) == 1 { name = jp_parse_array_pattern(ctx) }
1480 else { if jp_is_punct(ctx, "{\x00" as *u8) == 1 { name = jp_parse_object_pattern(ctx) }
1481 else {
1482 if jp_tok_kind(ctx) != JS_TOK_IDENT { return jp_error_node(ctx) }
1483 name = jp_new_node(ctx, ND_IDENT, -1, -1, -1, jp_cur(ctx), 0)
1484 jp_advance(ctx)
1485 } }
1486 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1487 var init: i64 = -1
1488 if jp_is_punct(ctx, "=\x00" as *u8) == 1 {
1489 jp_advance(ctx)
1490 init = jp_parse_assign(ctx)
1491 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1492 }
1493 return jp_new_node(ctx, ND_VAR_DECL, name, init, -1, -1, 0)
1494}
1495func jp_parse_var_decl(ctx: *i64) -> i64 {
1496 jp_advance(ctx) // consume var/let/const
1497 let first: i64 = jp_parse_one_declarator(ctx)
1498 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1499 // MULTI-declarator: `var a, b=c, d;` -> ND_VAR_LIST of ND_VAR_DECLs (real JS; very common).
1500 if jp_is_punct(ctx, ",\x00" as *u8) == 1 {
1501 let scratch: *i64 = sys_mmap(256 * 8) as *i64
1502 scratch[0] = first
1503 var count: i64 = 1
1504 while jp_is_punct(ctx, ",\x00" as *u8) == 1 {
1505 jp_advance(ctx)
1506 let d: i64 = jp_parse_one_declarator(ctx)
1507 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1508 if count < 256 { scratch[count] = d }
1509 count = count + 1
1510 }
1511 if jp_eat_punct(ctx, ";\x00" as *u8) == 0 { if jp_can_asi(ctx) == 0 { return jp_error_node(ctx) } }
1512 let cs: i64 = jp_child_commit(ctx, scratch, count)
1513 return jp_new_node(ctx, ND_VAR_LIST, cs, count, -1, -1, 0)
1514 }
1515 // single declarator: ASI-lite termination (explicit ';' OR line break / '}' / EOF).
1516 if jp_eat_punct(ctx, ";\x00" as *u8) == 0 {
1517 if jp_can_asi(ctx) == 0 { return jp_error_node(ctx) }
1518 }
1519 return first
1520}
1521
1522// return [ expr ] ;
1523func jp_parse_return(ctx: *i64) -> i64 {
1524 jp_advance(ctx) // consume `return`
1525 var expr: i64 = -1
1526 // bare `return;` or `return}` -> no expression
1527 var has_expr: i64 = 1
1528 if jp_is_punct(ctx, ";\x00" as *u8) == 1 { has_expr = 0 }
1529 if jp_is_punct(ctx, "}\x00" as *u8) == 1 { has_expr = 0 }
1530 if jp_tok_kind(ctx) == JS_TOK_EOF { has_expr = 0 }
1531 if has_expr == 1 {
1532 expr = jp_parse_expr(ctx)
1533 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1534 }
1535 // ASI-lite termination: explicit ';' OR a line break / '}' / EOF.
1536 if jp_eat_punct(ctx, ";\x00" as *u8) == 0 {
1537 if jp_can_asi(ctx) == 0 { return jp_error_node(ctx) }
1538 }
1539 return jp_new_node(ctx, ND_RETURN, expr, -1, -1, -1, 0)
1540}
1541
1542// block: { stmt* } -> list-node a=child_start, b=child_count
1543func jp_parse_block(ctx: *i64) -> i64 {
1544 jp_advance(ctx) // consume '{'
1545 let scratch: *i64 = sys_mmap(ND_MAGIC_2048 * 8) as *i64
1546 var count: i64 = 0
1547 var go: i64 = 1
1548 while go == 1 {
1549 if jp_err(ctx) == 1 { go = 0 }
1550 if go == 1 {
1551 if jp_is_punct(ctx, "}\x00" as *u8) == 1 { jp_advance(ctx); go = 0 }
1552 else {
1553 if jp_tok_kind(ctx) == JS_TOK_EOF { jp_set_err(ctx); go = 0 }
1554 else {
1555 let st: i64 = jp_parse_stmt(ctx)
1556 if count < ND_MAGIC_2048 { scratch[count] = st }
1557 count = count + 1
1558 }
1559 }
1560 }
1561 }
1562 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1563 let start: i64 = jp_child_commit(ctx, scratch, count)
1564 return jp_new_node(ctx, ND_BLOCK, start, count, -1, -1, 0)
1565}
1566
1567// if ( cond ) stmt [ else stmt ]
1568func jp_parse_if(ctx: *i64) -> i64 {
1569 jp_advance(ctx) // consume `if`
1570 if jp_eat_punct(ctx, "(\x00" as *u8) == 0 { return jp_error_node(ctx) }
1571 let cond: i64 = jp_parse_expr(ctx)
1572 if jp_eat_punct(ctx, ")\x00" as *u8) == 0 { return jp_error_node(ctx) }
1573 let then_s: i64 = jp_parse_stmt(ctx)
1574 var else_s: i64 = -1
1575 if jp_is_kw(ctx, "else\x00" as *u8) == 1 {
1576 jp_advance(ctx)
1577 else_s = jp_parse_stmt(ctx)
1578 }
1579 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1580 return jp_new_node(ctx, ND_IF, cond, then_s, else_s, -1, 0)
1581}
1582
1583// while ( cond ) stmt
1584func jp_parse_while(ctx: *i64) -> i64 {
1585 jp_advance(ctx) // consume `while`
1586 if jp_eat_punct(ctx, "(\x00" as *u8) == 0 { return jp_error_node(ctx) }
1587 let cond: i64 = jp_parse_expr(ctx)
1588 if jp_eat_punct(ctx, ")\x00" as *u8) == 0 { return jp_error_node(ctx) }
1589 let body: i64 = jp_parse_stmt(ctx)
1590 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1591 return jp_new_node(ctx, ND_WHILE, cond, body, -1, -1, 0)
1592}
1593
1594// for ( init ; cond ; update ) body -> ND_FOR. Slots: a=init, b=cond, c=update,
1595// tokidx-slot[4]=body. init may be a var-decl OR an expression OR empty; cond/update may
1596// be empty (empty cond = perpetually true, handled in the evaluator). We parse init WITHOUT
1597// consuming the ';' inside the init-decl/expr path so the for-header ';' separators stay
1598// uniform here. (for-in / for-of need iterator protocol -> HONEST OPEN, not parsed here:
1599// after the init clause we REQUIRE a ';'.)
1600func jp_parse_for(ctx: *i64) -> i64 {
1601 jp_advance(ctx) // consume `for`
1602 if jp_eat_punct(ctx, "(\x00" as *u8) == 0 { return jp_error_node(ctx) }
1603 // ---- init clause ---- (empty if the next token is ';')
1604 var init: i64 = -1
1605 if jp_is_punct(ctx, ";\x00" as *u8) == 1 {
1606 jp_advance(ctx)
1607 } else {
1608 var is_decl: i64 = 0
1609 if jp_is_kw(ctx, "var\x00" as *u8) == 1 { is_decl = 1 }
1610 if jp_is_kw(ctx, "let\x00" as *u8) == 1 { is_decl = 1 }
1611 if jp_is_kw(ctx, "const\x00" as *u8) == 1 { is_decl = 1 }
1612 if is_decl == 1 { init = jp_parse_for_decl(ctx) }
1613 else { jp_noin = 1; init = jp_parse_expr(ctx); jp_noin = 0 } // `in` here = for-in separator, not operator
1614 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1615 // ---- R-JS-SYNTAX2 (rung 8): for-of / for-in disambiguation. After the loop var
1616 // (a decl `var x` or a bare lvalue expr `x`), a contextual `of`/`in` keyword forks
1617 // to the iterator forms; a `;` continues the classic C-style for(init;cond;update).
1618 if jp_is_kw(ctx, "of\x00" as *u8) == 1 { return jp_parse_for_in_of(ctx, init, ND_FOR_OF) }
1619 if jp_is_kw(ctx, "in\x00" as *u8) == 1 { return jp_parse_for_in_of(ctx, init, ND_FOR_IN) }
1620 // the for-header requires an explicit ';' after the init clause (no ASI inside ()).
1621 if jp_eat_punct(ctx, ";\x00" as *u8) == 0 { return jp_error_node(ctx) }
1622 }
1623 // ---- cond clause ---- (empty = true)
1624 var cond: i64 = -1
1625 if jp_is_punct(ctx, ";\x00" as *u8) == 1 {
1626 jp_advance(ctx)
1627 } else {
1628 cond = jp_parse_expr(ctx)
1629 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1630 if jp_eat_punct(ctx, ";\x00" as *u8) == 0 { return jp_error_node(ctx) }
1631 }
1632 // ---- update clause ---- (empty if next is ')')
1633 var update: i64 = -1
1634 if jp_is_punct(ctx, ")\x00" as *u8) == 1 {
1635 jp_advance(ctx)
1636 } else {
1637 update = jp_parse_expr(ctx)
1638 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1639 if jp_eat_punct(ctx, ")\x00" as *u8) == 0 { return jp_error_node(ctx) }
1640 }
1641 let body: i64 = jp_parse_stmt(ctx)
1642 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1643 // store body in the tokidx slot (FOR has 4 children; a/b/c hold init/cond/update).
1644 return jp_new_node(ctx, ND_FOR, init, cond, update, body, 0)
1645}
1646
1647// for-header var-decl WITHOUT the terminating ';' (the for-loop header owns the ';'
1648// separators, so the decl must NOT consume one or run ASI). Mirrors jp_parse_var_decl
1649// minus the termination clause. Produces an ND_VAR_DECL (a=name, b=init|-1).
1650func jp_parse_for_decl(ctx: *i64) -> i64 {
1651 jp_advance(ctx) // consume var/let/const
1652 let first: i64 = jp_parse_one_declarator(ctx)
1653 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1654 // MULTI-declarator for-init `for(var i=0, j=n; ...)` -> ND_VAR_LIST (common; scheme2js Boyer uses it).
1655 // NO terminator here -- the for-header owns the ';'. `in`/`of` after a MULTI list = SyntaxError anyway.
1656 if jp_is_punct(ctx, ",\x00" as *u8) == 1 {
1657 let scratch: *i64 = sys_mmap(256 * 8) as *i64
1658 scratch[0] = first
1659 var count: i64 = 1
1660 while jp_is_punct(ctx, ",\x00" as *u8) == 1 {
1661 jp_advance(ctx)
1662 let d: i64 = jp_parse_one_declarator(ctx)
1663 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1664 if count < 256 { scratch[count] = d }
1665 count = count + 1
1666 }
1667 let cs: i64 = jp_child_commit(ctx, scratch, count)
1668 return jp_new_node(ctx, ND_VAR_LIST, cs, count, -1, -1, 0)
1669 }
1670 return first
1671}
1672
1673// R-JS-SYNTAX2 (rung 8): finish a for-of / for-in header after the contextual `of`/`in`
1674// keyword. `target` is the already-parsed loop variable (an ND_VAR_DECL `var x` carrying its
1675// name IDENT, or a bare lvalue expr) and `nkind` is ND_FOR_OF or ND_FOR_IN. Cursor sits on
1676// the `of`/`in` keyword. Shape: a=target, b=iterable/object-expr, c=body. The right-hand side
1677// is an ASSIGNMENT-expr (real JS: `for(x of a)`); then ')' and the loop body statement.
1678func jp_parse_for_in_of(ctx: *i64, target: i64, nkind: i64) -> i64 {
1679 jp_advance(ctx) // consume `of` / `in`
1680 // for-IN head is `Expression` (comma-inclusive) per the grammar -- minified jQuery ships `for(e in a,b,...)`;
1681 // for-OF head is `AssignmentExpression` (a comma there is a real SyntaxError), so keep it single.
1682 var rhs: i64 = 0
1683 if nkind == ND_FOR_IN { rhs = jp_parse_expr(ctx) } else { rhs = jp_parse_assign(ctx) }
1684 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1685 if jp_eat_punct(ctx, ")\x00" as *u8) == 0 { return jp_error_node(ctx) }
1686 let body: i64 = jp_parse_stmt(ctx)
1687 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1688 return jp_new_node(ctx, nkind, target, rhs, body, -1, 0)
1689}
1690
1691// R-JS-SYNTAX2 (rung 8): switch ( disc ) { case E: stmts... case E2: stmts... default: stmts... }
1692// -> ND_SWITCH (a=disc-expr, b=case_list_child_start, c=case_count). Each clause is an ND_CASE
1693// node (a=case-expr or -1 for default, b=stmt_list_child_start, c=stmt_count) committed
1694// CONTIGUOUSLY in the child arena, and the ND_CASE list itself is committed contiguously after.
1695// A clause's statement list runs until the next `case`/`default`/`}` (NOT terminated by a
1696// keyword), so fall-through is preserved by the EVALUATOR (the parser just records clause order).
1697// Multiple `default` clauses are an honest OPEN; the first `default` wins at eval.
1698func jp_parse_switch(ctx: *i64) -> i64 {
1699 jp_advance(ctx) // consume `switch`
1700 if jp_eat_punct(ctx, "(\x00" as *u8) == 0 { return jp_error_node(ctx) }
1701 let disc: i64 = jp_parse_expr(ctx)
1702 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1703 if jp_eat_punct(ctx, ")\x00" as *u8) == 0 { return jp_error_node(ctx) }
1704 if jp_eat_punct(ctx, "{\x00" as *u8) == 0 { return jp_error_node(ctx) }
1705 let cases: *i64 = sys_mmap(256 * 8) as *i64 // ND_CASE node indices (clause order)
1706 var ccount: i64 = 0
1707 var go: i64 = 1
1708 while go == 1 {
1709 if jp_err(ctx) == 1 { go = 0 }
1710 if go == 1 {
1711 if jp_is_punct(ctx, "}\x00" as *u8) == 1 { jp_advance(ctx); go = 0 }
1712 else {
1713 if jp_tok_kind(ctx) == JS_TOK_EOF { jp_set_err(ctx); go = 0 }
1714 else {
1715 // clause head: `case EXPR :` or `default :`
1716 var case_expr: i64 = -1
1717 var headok: i64 = 0
1718 if jp_is_kw(ctx, "case\x00" as *u8) == 1 {
1719 jp_advance(ctx)
1720 case_expr = jp_parse_expr(ctx)
1721 headok = 1
1722 }
1723 if headok == 0 { if jp_is_kw(ctx, "default\x00" as *u8) == 1 { jp_advance(ctx); headok = 1 } }
1724 if headok == 0 { jp_set_err(ctx); go = 0 }
1725 if go == 1 {
1726 if jp_eat_punct(ctx, ":\x00" as *u8) == 0 { jp_set_err(ctx); go = 0 }
1727 }
1728 if go == 1 {
1729 // clause statement list: parse statements until the next clause head or '}'.
1730 let cnode: i64 = jp_parse_case_body(ctx, case_expr)
1731 if jp_err(ctx) == 1 { go = 0 }
1732 else {
1733 if ccount < 256 { cases[ccount] = cnode }
1734 ccount = ccount + 1
1735 }
1736 }
1737 }
1738 }
1739 }
1740 }
1741 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1742 let cstart: i64 = jp_child_commit(ctx, cases, ccount)
1743 // LIST-node layout (a=child_start, b=child_count) so jp_child_at reads the ND_CASE list;
1744 // the discriminant rides slot c.
1745 return jp_new_node(ctx, ND_SWITCH, cstart, ccount, disc, -1, 0)
1746}
1747
1748// parse ONE switch clause's statement list (the head `case E:`/`default:` is already consumed;
1749// `case_expr` = the case test expr or -1 for default). Statements run until the next `case`/
1750// `default`/`}` boundary. Returns an ND_CASE node (a=case_expr, b=stmt_child_start, c=stmt_count).
1751func jp_parse_case_body(ctx: *i64, case_expr: i64) -> i64 {
1752 let scratch: *i64 = sys_mmap(ND_MAGIC_1024 * 8) as *i64
1753 var count: i64 = 0
1754 var go: i64 = 1
1755 while go == 1 {
1756 if jp_err(ctx) == 1 { go = 0 }
1757 if go == 1 {
1758 if jp_is_punct(ctx, "}\x00" as *u8) == 1 { go = 0 } // end of switch (do NOT consume)
1759 else { if jp_is_kw(ctx, "case\x00" as *u8) == 1 { go = 0 } // next clause (do NOT consume)
1760 else { if jp_is_kw(ctx, "default\x00" as *u8) == 1 { go = 0 } // default clause (do NOT consume)
1761 else {
1762 if jp_tok_kind(ctx) == JS_TOK_EOF { jp_set_err(ctx); go = 0 }
1763 else {
1764 let st: i64 = jp_parse_stmt(ctx)
1765 if count < ND_MAGIC_1024 { scratch[count] = st }
1766 count = count + 1
1767 }
1768 } } }
1769 }
1770 }
1771 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1772 let start: i64 = jp_child_commit(ctx, scratch, count)
1773 // LIST-node layout (a=child_start, b=child_count) so jp_child_at reads the clause's stmts;
1774 // the case-test expr (-1 for default) rides slot c.
1775 return jp_new_node(ctx, ND_CASE, start, count, case_expr, -1, 0)
1776}
1777
1778// do body while ( cond ) ; -> ND_DOWHILE (a=cond, b=body). Body runs at least once.
1779func jp_parse_dowhile(ctx: *i64) -> i64 {
1780 jp_advance(ctx) // consume `do`
1781 let body: i64 = jp_parse_stmt(ctx)
1782 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1783 if jp_is_kw(ctx, "while\x00" as *u8) == 0 { return jp_error_node(ctx) }
1784 jp_advance(ctx) // consume `while`
1785 if jp_eat_punct(ctx, "(\x00" as *u8) == 0 { return jp_error_node(ctx) }
1786 let cond: i64 = jp_parse_expr(ctx)
1787 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1788 if jp_eat_punct(ctx, ")\x00" as *u8) == 0 { return jp_error_node(ctx) }
1789 // optional trailing ';' (ASI applies); real JS allows `do{}while(c)` then a break.
1790 if jp_eat_punct(ctx, ";\x00" as *u8) == 0 {
1791 if jp_can_asi(ctx) == 0 { return jp_error_node(ctx) }
1792 }
1793 return jp_new_node(ctx, ND_DOWHILE, cond, body, -1, -1, 0)
1794}
1795
1796// break ; / continue ; -> ND_BREAK / ND_CONTINUE (no children). ASI-lite termination.
1797// (labeled break/continue need a label table -> HONEST OPEN, not parsed: a label after
1798// the keyword is not consumed, so `break outer;` would fall to the ';' check and error.)
1799func jp_parse_break(ctx: *i64) -> i64 {
1800 jp_advance(ctx) // consume `break`
1801 if jp_tok_kind(ctx) == JS_TOK_IDENT { jp_advance(ctx) } // optional label -> consumed (targets nearest loop; see labeled-stmt note)
1802 if jp_eat_punct(ctx, ";\x00" as *u8) == 0 {
1803 if jp_can_asi(ctx) == 0 { return jp_error_node(ctx) }
1804 }
1805 return jp_new_node(ctx, ND_BREAK, -1, -1, -1, -1, 0)
1806}
1807func jp_parse_continue(ctx: *i64) -> i64 {
1808 jp_advance(ctx) // consume `continue`
1809 if jp_tok_kind(ctx) == JS_TOK_IDENT { jp_advance(ctx) } // optional label -> consumed (targets nearest loop)
1810 if jp_eat_punct(ctx, ";\x00" as *u8) == 0 {
1811 if jp_can_asi(ctx) == 0 { return jp_error_node(ctx) }
1812 }
1813 return jp_new_node(ctx, ND_CONTINUE, -1, -1, -1, -1, 0)
1814}
1815
1816// param list `( a , b , ... )` -> PARAMS list-node a=child_start, b=child_count
1817func jp_parse_params(ctx: *i64) -> i64 {
1818 if jp_eat_punct(ctx, "(\x00" as *u8) == 0 { return jp_error_node(ctx) }
1819 let scratch: *i64 = sys_mmap(256 * 8) as *i64
1820 var count: i64 = 0
1821 if jp_is_punct(ctx, ")\x00" as *u8) == 1 {
1822 jp_advance(ctx)
1823 let start0: i64 = jp_child_commit(ctx, scratch, 0)
1824 return jp_new_node(ctx, ND_PARAMS, start0, 0, -1, -1, 0)
1825 }
1826 var go: i64 = 1
1827 while go == 1 {
1828 var isrest: i64 = 0
1829 if jp_is_punct(ctx, "...\x00" as *u8) == 1 { isrest = 1; jp_advance(ctx) } // rest param ...r
1830 var pn: i64 = 0 - 1
1831 var handled: i64 = 0
1832 // DESTRUCTURING params (ES6): `function f({a, b: c}, [x, y])` -- vk's framework uses these.
1833 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 } } }
1834 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 } } } }
1835 if handled == 0 {
1836 if jp_tok_kind(ctx) != JS_TOK_IDENT { jp_set_err(ctx); go = 0 }
1837 if go == 1 {
1838 let ptok: i64 = jp_cur(ctx)
1839 jp_advance(ctx)
1840 var defexpr: i64 = 0 - 1
1841 if isrest == 0 { if jp_is_punct(ctx, "=\x00" as *u8) == 1 { // default param: name = expr
1842 jp_advance(ctx)
1843 defexpr = jp_parse_assign(ctx)
1844 if jp_err(ctx) == 1 { go = 0 }
1845 } }
1846 pn = jp_new_node(ctx, ND_IDENT, defexpr, -1, -1, ptok, isrest) // extra=isrest; slot a=default/-1
1847 }
1848 }
1849 if go == 1 {
1850 if count < 256 { scratch[count] = pn }
1851 count = count + 1
1852 if jp_eat_punct(ctx, ",\x00" as *u8) == 1 {
1853 // next param
1854 } else {
1855 if jp_eat_punct(ctx, ")\x00" as *u8) == 1 { go = 0 }
1856 else { jp_set_err(ctx); go = 0 }
1857 }
1858 }
1859 }
1860 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1861 let start: i64 = jp_child_commit(ctx, scratch, count)
1862 return jp_new_node(ctx, ND_PARAMS, start, count, -1, -1, 0)
1863}
1864
1865// function NAME ( params ) { body }
1866func jp_parse_func_decl(ctx: *i64) -> i64 {
1867 jp_advance(ctx) // consume `function`
1868 if jp_is_punct(ctx, "*\x00" as *u8) == 1 { jp_advance(ctx) } // generator marker (stub semantics; ND_YIELD)
1869 if jp_tok_kind(ctx) != JS_TOK_IDENT { return jp_error_node(ctx) }
1870 let name: i64 = jp_new_node(ctx, ND_IDENT, -1, -1, -1, jp_cur(ctx), 0)
1871 jp_advance(ctx)
1872 let params: i64 = jp_parse_params(ctx)
1873 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1874 if jp_is_punct(ctx, "{\x00" as *u8) == 0 { return jp_error_node(ctx) }
1875 let body: i64 = jp_parse_block(ctx)
1876 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1877 return jp_new_node(ctx, ND_FUNC_DECL, name, params, body, -1, 0)
1878}
1879
1880// FUNCTION EXPRESSION (R-JS-CLOSURE, rung 6): `function (params){body}` (anonymous) or
1881// `function name(params){body}` (named) in EXPRESSION position. Same ND_FUNC_DECL node
1882// shape as a declaration (a=name|-1, b=params, c=body); name = -1 when anonymous. The
1883// expression-vs-statement distinction is purely WHERE it is parsed -- a statement-leading
1884// `function` is a declaration (jp_parse_stmt), here it is a value. Cursor is on `function`.
1885func jp_parse_func_expr(ctx: *i64) -> i64 {
1886 jp_advance(ctx) // consume `function`
1887 if jp_is_punct(ctx, "*\x00" as *u8) == 1 { jp_advance(ctx) } // generator marker (stub semantics; ND_YIELD)
1888 var name: i64 = -1
1889 if jp_tok_kind(ctx) == JS_TOK_IDENT { // optional name (named function expression)
1890 name = jp_new_node(ctx, ND_IDENT, -1, -1, -1, jp_cur(ctx), 0)
1891 jp_advance(ctx)
1892 }
1893 let params: i64 = jp_parse_params(ctx)
1894 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1895 if jp_is_punct(ctx, "{\x00" as *u8) == 0 { return jp_error_node(ctx) }
1896 let body: i64 = jp_parse_block(ctx)
1897 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1898 return jp_new_node(ctx, ND_FUNC_DECL, name, params, body, -1, 0)
1899}
1900
1901// expression statement: expr [ ; | ASI ]
1902// Termination is REQUIRED -- but the semicolon may be inserted automatically (ASI):
1903// after a complete expression, accept an explicit ';', else require an ASI point
1904// (line break, '}', or EOF). Adjacent statements on ONE line with no separator
1905// (`a b`, `1 2`) are a SyntaxError -- jp_can_asi returns 0 there, so we error.
1906func jp_parse_expr_stmt(ctx: *i64) -> i64 {
1907 let e: i64 = jp_parse_expr(ctx)
1908 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1909 if jp_eat_punct(ctx, ";\x00" as *u8) == 1 {
1910 return jp_new_node(ctx, ND_EXPR_STMT, e, -1, -1, -1, 0)
1911 }
1912 if jp_can_asi(ctx) == 0 { return jp_error_node(ctx) } // no ';', no ASI point
1913 return jp_new_node(ctx, ND_EXPR_STMT, e, -1, -1, -1, 0)
1914}
1915
1916// one statement (dispatch on leading keyword / punct). Shallow if-chain by design.
1917// try { ... } catch (e) { ... } finally { ... } -- catch param optional (`catch {`),
1918// at least one of catch/finally required (real JS). Finally block rides the tokidx slot.
1919func jp_parse_try(ctx: *i64) -> i64 {
1920 jp_advance(ctx) // consume `try`
1921 if jp_is_punct(ctx, "{\x00" as *u8) == 0 { jp_set_err(ctx); return jp_error_node(ctx) }
1922 let tryb: i64 = jp_parse_block(ctx)
1923 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1924 var param: i64 = 0 - 1
1925 var catchb: i64 = 0 - 1
1926 var finb: i64 = 0 - 1
1927 if jp_is_kw(ctx, "catch\x00" as *u8) == 1 {
1928 jp_advance(ctx)
1929 if jp_is_punct(ctx, "(\x00" as *u8) == 1 {
1930 jp_advance(ctx)
1931 if jp_tok_kind(ctx) != JS_TOK_IDENT { jp_set_err(ctx); return jp_error_node(ctx) }
1932 param = jp_new_node(ctx, ND_IDENT, -1, -1, -1, jp_cur(ctx), 0)
1933 jp_advance(ctx)
1934 if jp_eat_punct(ctx, ")\x00" as *u8) == 0 { jp_set_err(ctx); return jp_error_node(ctx) }
1935 }
1936 if jp_is_punct(ctx, "{\x00" as *u8) == 0 { jp_set_err(ctx); return jp_error_node(ctx) }
1937 catchb = jp_parse_block(ctx)
1938 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1939 }
1940 if jp_is_kw(ctx, "finally\x00" as *u8) == 1 {
1941 jp_advance(ctx)
1942 if jp_is_punct(ctx, "{\x00" as *u8) == 0 { jp_set_err(ctx); return jp_error_node(ctx) }
1943 finb = jp_parse_block(ctx)
1944 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1945 }
1946 if catchb < 0 { if finb < 0 { jp_set_err(ctx); return jp_error_node(ctx) } }
1947 return jp_new_node(ctx, ND_TRY, tryb, param, catchb, finb, 0)
1948}
1949// throw expr ;
1950func jp_parse_throw(ctx: *i64) -> i64 {
1951 jp_advance(ctx) // consume `throw`
1952 let e: i64 = jp_parse_expr(ctx)
1953 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1954 if jp_eat_punct(ctx, ";\x00" as *u8) == 0 {
1955 if jp_can_asi(ctx) == 0 { return jp_error_node(ctx) }
1956 }
1957 return jp_new_node(ctx, ND_THROW, e, -1, -1, -1, 0)
1958}
1959func jp_parse_stmt(ctx: *i64) -> i64 {
1960 if jp_err(ctx) == 1 { return jp_error_node(ctx) }
1961 // lone semicolon = empty statement -> treat as empty EXPR_STMT? keep strict:
1962 // we just consume a stray ';' as an empty block of work by recursing once.
1963 if jp_is_punct(ctx, ";\x00" as *u8) == 1 {
1964 jp_advance(ctx)
1965 return jp_new_node(ctx, ND_BLOCK, jp_pst(ctx)[PST_NCHILD], 0, -1, -1, 0)
1966 }
1967 if jp_is_punct(ctx, "{\x00" as *u8) == 1 { return jp_parse_block(ctx) }
1968 if jp_is_kw(ctx, "var\x00" as *u8) == 1 { return jp_parse_var_decl(ctx) }
1969 if jp_is_kw(ctx, "let\x00" as *u8) == 1 { return jp_parse_var_decl(ctx) }
1970 if jp_is_kw(ctx, "const\x00" as *u8) == 1 { return jp_parse_var_decl(ctx) }
1971 if jp_is_kw(ctx, "return\x00" as *u8) == 1 { return jp_parse_return(ctx) }
1972 if jp_is_kw(ctx, "if\x00" as *u8) == 1 { return jp_parse_if(ctx) }
1973 if jp_is_kw(ctx, "while\x00" as *u8) == 1 { return jp_parse_while(ctx) }
1974 if jp_is_kw(ctx, "for\x00" as *u8) == 1 { return jp_parse_for(ctx) }
1975 if jp_is_kw(ctx, "do\x00" as *u8) == 1 { return jp_parse_dowhile(ctx) }
1976 if jp_is_kw(ctx, "switch\x00" as *u8) == 1 { return jp_parse_switch(ctx) }
1977 if jp_is_kw(ctx, "break\x00" as *u8) == 1 { return jp_parse_break(ctx) }
1978 if jp_is_kw(ctx, "continue\x00" as *u8) == 1 { return jp_parse_continue(ctx) }
1979 if jp_is_kw(ctx, "function\x00" as *u8) == 1 { return jp_parse_func_decl(ctx) }
1980 if jp_is_kw(ctx, "class\x00" as *u8) == 1 { return jp_parse_class_decl(ctx) }
1981 if jp_is_kw(ctx, "try\x00" as *u8) == 1 { return jp_parse_try(ctx) }
1982 if jp_is_kw(ctx, "throw\x00" as *u8) == 1 { return jp_parse_throw(ctx) }
1983 // labeled statement: IDENT ':' stmt (minified jQuery/Sizzle use labeled loops). Pragmatic support: the label
1984 // is a TRANSPARENT wrapper -> parse+discard it, return the inner statement. break/continue [label] consume the
1985 // label but target the NEAREST loop (correct when the label is on the enclosing loop; a labeled break to a
1986 // NON-nearest outer loop is the known approximation -> a full label table is the follow-on).
1987 if jp_tok_kind(ctx) == JS_TOK_IDENT { if jp_punct_at(ctx, jp_cur(ctx) + 1, ":\x00" as *u8) == 1 {
1988 jp_advance(ctx) // label IDENT
1989 jp_advance(ctx) // ':'
1990 return jp_parse_stmt(ctx)
1991 } }
1992 // default: expression statement
1993 return jp_parse_expr_stmt(ctx)
1994}
1995
1996// PROGRAM = stmt* until EOF. Returns the PROGRAM node index (list-node).
1997func jp_parse_program(ctx: *i64) -> i64 {
1998 let scratch: *i64 = sys_mmap(ND_MAGIC_4096 * 8) as *i64
1999 var count: i64 = 0
2000 var go: i64 = 1
2001 while go == 1 {
2002 if jp_tok_kind(ctx) == JS_TOK_EOF { go = 0 }
2003 if go == 1 { if jp_cur(ctx) >= ctx[CTX_NTOK] { go = 0 } }
2004 if go == 1 { if jp_err(ctx) == 1 { go = 0 } }
2005 if go == 1 {
2006 let st: i64 = jp_parse_stmt(ctx)
2007 if count < ND_MAGIC_4096 { scratch[count] = st }
2008 count = count + 1
2009 }
2010 }
2011 let start: i64 = jp_child_commit(ctx, scratch, count)
2012 return jp_new_node(ctx, ND_PROGRAM, start, count, -1, -1, 0)
2013}
2014
2015// ===================== top-level driver =====================
2016// Build a parse context over freshly-mmap'd arenas, lex `src`, then parse a PROGRAM.
2017// Returns the PROGRAM node index. Writes the ctx pointer to *ctx_out so the caller
2018// (gate) can inspect the AST. Sets the error flag inside pst on any syntax error.
2019func jp_parse_source(src: *u8, srclen: i64, ctx_out: *i64) -> i64 {
2020 // Budgets: js_lex TRUNCATES SILENTLY at maxtoks (parse then dies mid-file with a misleading cursor) --
2021 // 4096 tokens capped real sources at ~16-26KB (Octane RayTrace 28KB / Crypto 48KB both hit it). mmap is
2022 // virtual (only touched pages cost), so size for real-page bundles. Dynamic growth = a follow-on.
2023 // ADAPTIVE budgets sized to the input: real pages (vk = ~250KB of concatenated inline JS across 70 scripts)
2024 // blew the fixed 65536-token cap -> js_lex truncated -> parse died mid-file (rc=1). ~1 token per 2 bytes
2025 // covers dense minified JS; nodes/children scale with tokens. mmap is virtual (only touched pages cost).
2026 var maxtoks: i64 = ND_MAGIC_65536
2027 if srclen / 2 > maxtoks { maxtoks = srclen / 2 }
2028 var maxnodes: i64 = maxtoks * 2
2029 var maxchild: i64 = maxtoks * 2
2030 if maxnodes < ND_MAGIC_131072 { maxnodes = ND_MAGIC_131072 }
2031 if maxchild < ND_MAGIC_131072 { maxchild = ND_MAGIC_131072 }
2032 let toks: *i64 = sys_mmap(maxtoks * 3 * 8) as *i64
2033 let ntb: *i64 = sys_mmap(16) as *i64
2034 let nodes: *i64 = sys_mmap(maxnodes * NODE_SLOTS * 8) as *i64
2035 let children: *i64 = sys_mmap(maxchild * 8) as *i64
2036 let pst: *i64 = sys_mmap(64) as *i64
2037 let ctx: *i64 = sys_mmap(CTX_MAXCHILD * 8 + 64) as *i64
2038
2039 let ntok: i64 = js_lex(src, srclen, toks, maxtoks, ntb)
2040
2041 pst[PST_CUR] = 0
2042 pst[PST_ERR] = 0
2043 pst[PST_NNODE] = 0
2044 pst[PST_NCHILD] = 0
2045 pst[PST_ERR_POS] = 0 - 1
2046
2047 ctx[CTX_SRC] = src as i64
2048 ctx[CTX_TOKS] = toks as i64
2049 ctx[CTX_NTOK] = ntok
2050 ctx[CTX_NODES] = nodes as i64
2051 ctx[CTX_CHILDREN] = children as i64
2052 ctx[CTX_PST] = pst as i64
2053 ctx[CTX_MAXNODE] = maxnodes
2054 ctx[CTX_MAXCHILD] = maxchild
2055
2056 // If the lexer itself emitted an ERROR token, the source is malformed.
2057 var li: i64 = 0
2058 while li < ntok {
2059 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 }
2060 li = li + 1
2061 }
2062
2063 let prog: i64 = jp_parse_program(ctx)
2064
2065 // Trailing unconsumed tokens (after a complete parse) = syntax error too:
2066 // e.g. "1 + )" leaves the ')' dangling / errors mid-expr. Already covered by
2067 // err flag, but guard the case where the program loop stopped early WITHOUT
2068 // consuming everything and WITHOUT an error (shouldn't happen, but honest).
2069 if pst[PST_ERR] == 0 {
2070 if pst[PST_CUR] < ntok { if pst[PST_CUR] < ntok { pst[PST_ERR_POS] = toks[pst[PST_CUR] * 3 + 1] } pst[PST_ERR] = 1 }
2071 }
2072
2073 ctx_out[0] = ctx as i64
2074 return prog
2075}
2076
2077// ===================== GATE =====================
2078func 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 }
2079// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
2080// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
2081// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
2082// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
2083func jq_putn(v: i64) -> i64 { nxi_out(v); return 0 }
2084func jq_strlen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
2085// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
2086// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
2087// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
2088// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
2089func jq_fdn(fd: i64, v: i64) -> i64 { nxi_fd(fd, v); return 0 }
2090
2091// helper: parse a NUL-terminated source string, return ctx (via box) + prog index.
2092func jq_parse(s: *u8, ctxbox: *i64) -> i64 {
2093 return jp_parse_source(s, jq_strlen(s), ctxbox)
2094}
2095func jq_ctx(ctxbox: *i64) -> *i64 { return (ctxbox[0]) as *i64 }
2096func jq_haserr(ctxbox: *i64) -> i64 { let c: *i64 = jq_ctx(ctxbox); let p: *i64 = jp_pst(c); return p[PST_ERR] }
2097
2098// node index of statement-0's top expression (a-slot of the first EXPR_STMT).
2099func jq_expr0(ctxbox: *i64, prog: i64) -> i64 {
2100 let c: *i64 = jq_ctx(ctxbox)
2101 let st: i64 = jp_child_at(c, prog, 0)
2102 return jp_na(c, st)
2103}
2104// kind of statement-0's top expression.
2105func jq_expr0_kind(ctxbox: *i64, prog: i64) -> i64 {
2106 let c: *i64 = jq_ctx(ctxbox)
2107 return jp_nkind(c, jq_expr0(ctxbox, prog))
2108}
2109// op-code (extra slot) of statement-0's top expression.
2110func jq_expr0_op(ctxbox: *i64, prog: i64) -> i64 {
2111 let c: *i64 = jq_ctx(ctxbox)
2112 return jp_nextra(c, jq_expr0(ctxbox, prog))
2113}
2114// 1 iff `src` parses with NO error AND its statement-0 expression is a BINARY whose
2115// op-code == want. Used by KAT10 to sweep an operator family compactly.
2116func jq_binop_is(src: *u8, ctxbox: *i64, want: i64) -> i64 {
2117 let prog: i64 = jq_parse(src, ctxbox)
2118 if jq_haserr(ctxbox) == 1 { return 0 }
2119 if jq_expr0_kind(ctxbox, prog) != ND_BINARY { return 0 }
2120 if jq_expr0_op(ctxbox, prog) != want { return 0 }
2121 return 1
2122}
2123// 1 iff `src` parses with NO error AND its statement-0 expression is a UNARY whose
2124// op-code == want.
2125func jq_unop_is(src: *u8, ctxbox: *i64, want: i64) -> i64 {
2126 let prog: i64 = jq_parse(src, ctxbox)
2127 if jq_haserr(ctxbox) == 1 { return 0 }
2128 if jq_expr0_kind(ctxbox, prog) != ND_UNARY { return 0 }
2129 if jq_expr0_op(ctxbox, prog) != want { return 0 }
2130 return 1
2131}
2132
2133func main() -> i64 {
2134 let ctxbox: *i64 = sys_mmap(16) as *i64
2135 var pass: i64 = 0
2136 var tot: i64 = 0
2137 jq_puts("nx_js_parse gate (R-JS-PARSE, WB-JS-001 rung 1)\n" as *u8)
2138
2139 // ---- KAT 1 (precedence): 1+2*3 -> EXPR_STMT( BINARY(+) ) whose RIGHT child is
2140 // BINARY(*). Proves * binds tighter than + (NOT left-flat). ----
2141 let s1: *u8 = "1+2*3\x00" as *u8
2142 let prog1: i64 = jq_parse(s1, ctxbox)
2143 let c1: *i64 = jq_ctx(ctxbox)
2144 var r1: i64 = 1
2145 if jq_haserr(ctxbox) == 1 { r1 = 0 }
2146 if jp_nkind(c1, prog1) != ND_PROGRAM { r1 = 0 }
2147 if jp_nb(c1, prog1) != 1 { r1 = 0 } // exactly 1 statement
2148 if r1 == 1 {
2149 let st: i64 = jp_child_at(c1, prog1, 0)
2150 if jp_nkind(c1, st) != ND_EXPR_STMT { r1 = 0 }
2151 if r1 == 1 {
2152 let add: i64 = jp_na(c1, st)
2153 if jp_nkind(c1, add) != ND_BINARY { r1 = 0 }
2154 if r1 == 1 { if jp_nextra(c1, add) != OP_ADD { r1 = 0 } }
2155 if r1 == 1 {
2156 let lhs: i64 = jp_na(c1, add)
2157 let rhs: i64 = jp_nb(c1, add)
2158 if jp_nkind(c1, lhs) != ND_NUMBER { r1 = 0 } // left = 1 (a NUMBER)
2159 if jp_nkind(c1, rhs) != ND_BINARY { r1 = 0 } // right = (2*3)
2160 if r1 == 1 { if jp_nextra(c1, rhs) != OP_MUL { r1 = 0 } }
2161 }
2162 }
2163 }
2164 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) }
2165 tot = tot + 1
2166
2167 // ---- KAT 2 (assoc): a=b=c -> ASSIGN(a, ASSIGN(b, c)) right-assoc;
2168 // AND 2-3-4 -> BINARY(-) whose LEFT is BINARY(-) (left-assoc). ----
2169 let s2: *u8 = "a=b=c\x00" as *u8
2170 let prog2: i64 = jq_parse(s2, ctxbox)
2171 let c2: *i64 = jq_ctx(ctxbox)
2172 var r2: i64 = 1
2173 if jq_haserr(ctxbox) == 1 { r2 = 0 }
2174 if r2 == 1 {
2175 let st2: i64 = jp_child_at(c2, prog2, 0)
2176 let asn: i64 = jp_na(c2, st2)
2177 if jp_nkind(c2, asn) != ND_ASSIGN { r2 = 0 }
2178 if r2 == 1 {
2179 let lhs: i64 = jp_na(c2, asn)
2180 let rhs: i64 = jp_nb(c2, asn)
2181 if jp_nkind(c2, lhs) != ND_IDENT { r2 = 0 } // a
2182 if jp_nkind(c2, rhs) != ND_ASSIGN { r2 = 0 } // (b=c) nested RIGHT
2183 }
2184 }
2185 // left-assoc subtraction
2186 let s2b: *u8 = "2-3-4\x00" as *u8
2187 let prog2b: i64 = jq_parse(s2b, ctxbox)
2188 let c2b: *i64 = jq_ctx(ctxbox)
2189 if jq_haserr(ctxbox) == 1 { r2 = 0 }
2190 if r2 == 1 {
2191 let st: i64 = jp_child_at(c2b, prog2b, 0)
2192 let sub: i64 = jp_na(c2b, st)
2193 if jp_nkind(c2b, sub) != ND_BINARY { r2 = 0 }
2194 if r2 == 1 { if jp_nextra(c2b, sub) != OP_SUB { r2 = 0 } }
2195 if r2 == 1 {
2196 let lhs: i64 = jp_na(c2b, sub)
2197 let rhs: i64 = jp_nb(c2b, sub)
2198 if jp_nkind(c2b, lhs) != ND_BINARY { r2 = 0 } // (2-3) nested LEFT
2199 if jp_nkind(c2b, rhs) != ND_NUMBER { r2 = 0 } // 4 on the right
2200 }
2201 }
2202 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) }
2203 tot = tot + 1
2204
2205 // ---- KAT 3 (call/member chain): foo.bar(x) -> CALL of MEMBER(foo,bar), 1 arg. ----
2206 let s3: *u8 = "foo.bar(x)\x00" as *u8
2207 let prog3: i64 = jq_parse(s3, ctxbox)
2208 let c3: *i64 = jq_ctx(ctxbox)
2209 var r3: i64 = 1
2210 if jq_haserr(ctxbox) == 1 { r3 = 0 }
2211 if r3 == 1 {
2212 let st: i64 = jp_child_at(c3, prog3, 0)
2213 let call: i64 = jp_na(c3, st)
2214 if jp_nkind(c3, call) != ND_CALL { r3 = 0 }
2215 if r3 == 1 {
2216 let callee: i64 = jp_na(c3, call) // a=callee
2217 let argc: i64 = jp_nc(c3, call) // c=arg count
2218 if jp_nkind(c3, callee) != ND_MEMBER { r3 = 0 }
2219 if argc != 1 { r3 = 0 }
2220 if r3 == 1 {
2221 let obj: i64 = jp_na(c3, callee)
2222 let prop: i64 = jp_nb(c3, callee)
2223 if jp_nkind(c3, obj) != ND_IDENT { r3 = 0 } // foo
2224 if jp_nkind(c3, prop) != ND_IDENT { r3 = 0 } // bar
2225 }
2226 }
2227 }
2228 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) }
2229 tot = tot + 1
2230
2231 // ---- KAT 4 (statements): var decl + if/else + while + function decl + return. ----
2232 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
2233 let prog4: i64 = jq_parse(s4, ctxbox)
2234 let c4: *i64 = jq_ctx(ctxbox)
2235 var r4: i64 = 1
2236 if jq_haserr(ctxbox) == 1 { r4 = 0 }
2237 if jp_nkind(c4, prog4) != ND_PROGRAM { r4 = 0 }
2238 if r4 == 1 { if jp_nb(c4, prog4) != 4 { r4 = 0 } } // 4 top-level statements
2239 if r4 == 1 {
2240 let s_var: i64 = jp_child_at(c4, prog4, 0)
2241 let s_if: i64 = jp_child_at(c4, prog4, 1)
2242 let s_wh: i64 = jp_child_at(c4, prog4, 2)
2243 let s_fn: i64 = jp_child_at(c4, prog4, 3)
2244 if jp_nkind(c4, s_var) != ND_VAR_DECL { r4 = 0 }
2245 if jp_nkind(c4, s_if) != ND_IF { r4 = 0 }
2246 if jp_nkind(c4, s_wh) != ND_WHILE { r4 = 0 }
2247 if jp_nkind(c4, s_fn) != ND_FUNC_DECL { r4 = 0 }
2248 // var decl has an initializer (b != -1)
2249 if r4 == 1 { if jp_nb(c4, s_var) == -1 { r4 = 0 } }
2250 // if has an else branch (c != -1) and both branches are BLOCKs
2251 if r4 == 1 {
2252 if jp_nc(c4, s_if) == -1 { r4 = 0 }
2253 if r4 == 1 {
2254 if jp_nkind(c4, jp_nb(c4, s_if)) != ND_BLOCK { r4 = 0 } // then
2255 if jp_nkind(c4, jp_nc(c4, s_if)) != ND_BLOCK { r4 = 0 } // else
2256 }
2257 }
2258 // then-block contains a RETURN
2259 if r4 == 1 {
2260 let then_b: i64 = jp_nb(c4, s_if)
2261 if jp_nb(c4, then_b) < 1 { r4 = 0 }
2262 if r4 == 1 {
2263 let ret: i64 = jp_child_at(c4, then_b, 0)
2264 if jp_nkind(c4, ret) != ND_RETURN { r4 = 0 }
2265 if r4 == 1 { if jp_na(c4, ret) == -1 { r4 = 0 } } // return HAS an expr
2266 }
2267 }
2268 // function f(a,b){...} -> 2 params, body BLOCK with a RETURN of a BINARY(+)
2269 if r4 == 1 {
2270 let params: i64 = jp_nb(c4, s_fn)
2271 let body: i64 = jp_nc(c4, s_fn)
2272 if jp_nkind(c4, params) != ND_PARAMS { r4 = 0 }
2273 if r4 == 1 { if jp_nb(c4, params) != 2 { r4 = 0 } } // 2 params
2274 if jp_nkind(c4, body) != ND_BLOCK { r4 = 0 }
2275 if r4 == 1 {
2276 let ret: i64 = jp_child_at(c4, body, 0)
2277 if jp_nkind(c4, ret) != ND_RETURN { r4 = 0 }
2278 if r4 == 1 {
2279 let plus: i64 = jp_na(c4, ret)
2280 if jp_nkind(c4, plus) != ND_BINARY { r4 = 0 }
2281 if r4 == 1 { if jp_nextra(c4, plus) != OP_ADD { r4 = 0 } }
2282 }
2283 }
2284 }
2285 }
2286 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) }
2287 tot = tot + 1
2288
2289 // ---- KAT 5 (TAMPER): malformed "1 + )" MUST be an error, never a fake AST. ----
2290 let s5: *u8 = "1 + )\x00" as *u8
2291 let prog5: i64 = jq_parse(s5, ctxbox)
2292 var r5: i64 = 1
2293 if jq_haserr(ctxbox) != 1 { r5 = 0 } // must have flagged an error
2294 // second malformed sample: "var = ;" (decl with no name)
2295 let s5b: *u8 = "var = ;\x00" as *u8
2296 let prog5b: i64 = jq_parse(s5b, ctxbox)
2297 if jq_haserr(ctxbox) != 1 { r5 = 0 }
2298 // third: dangling operator "a *" (RHS missing)
2299 let s5c: *u8 = "a *\x00" as *u8
2300 let prog5c: i64 = jq_parse(s5c, ctxbox)
2301 if jq_haserr(ctxbox) != 1 { r5 = 0 }
2302 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) }
2303 tot = tot + 1
2304
2305 // ---- KAT 6 (precedence ladder + unary): !a && b || c == d -> top is OR, whose
2306 // left is AND, the AND's left is UNARY(!a), and == binds tighter than ==. ---
2307 let s6: *u8 = "!a && b || c == d\x00" as *u8
2308 let prog6: i64 = jq_parse(s6, ctxbox)
2309 let c6: *i64 = jq_ctx(ctxbox)
2310 var r6: i64 = 1
2311 if jq_haserr(ctxbox) == 1 { r6 = 0 }
2312 if r6 == 1 {
2313 let st: i64 = jp_child_at(c6, prog6, 0)
2314 let or_n: i64 = jp_na(c6, st)
2315 if jp_nkind(c6, or_n) != ND_BINARY { r6 = 0 }
2316 if r6 == 1 { if jp_nextra(c6, or_n) != OP_OR { r6 = 0 } } // top = ||
2317 if r6 == 1 {
2318 let and_n: i64 = jp_na(c6, or_n) // left of ||
2319 let eq_n: i64 = jp_nb(c6, or_n) // right of ||
2320 if jp_nextra(c6, and_n) != OP_AND { r6 = 0 } // && under ||
2321 if r6 == 1 { if jp_nextra(c6, eq_n) != OP_EQ { r6 = 0 } } // == under ||
2322 if r6 == 1 {
2323 let not_n: i64 = jp_na(c6, and_n) // left of &&
2324 if jp_nkind(c6, not_n) != ND_UNARY { r6 = 0 }
2325 if r6 == 1 { if jp_nextra(c6, not_n) != OP_NOT { r6 = 0 } }
2326 }
2327 }
2328 }
2329 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) }
2330 tot = tot + 1
2331
2332 // ---- KAT 7 (parenthesized regrouping): (1+2)*3 -> BINARY(*) whose LEFT is
2333 // BINARY(+). Proves parens override precedence (distinct from KAT1). ----
2334 let s7: *u8 = "(1+2)*3\x00" as *u8
2335 let prog7: i64 = jq_parse(s7, ctxbox)
2336 let c7: *i64 = jq_ctx(ctxbox)
2337 var r7: i64 = 1
2338 if jq_haserr(ctxbox) == 1 { r7 = 0 }
2339 if r7 == 1 {
2340 let st: i64 = jp_child_at(c7, prog7, 0)
2341 let mul: i64 = jp_na(c7, st)
2342 if jp_nkind(c7, mul) != ND_BINARY { r7 = 0 }
2343 if r7 == 1 { if jp_nextra(c7, mul) != OP_MUL { r7 = 0 } }
2344 if r7 == 1 {
2345 let lhs: i64 = jp_na(c7, mul)
2346 let rhs: i64 = jp_nb(c7, mul)
2347 if jp_nkind(c7, lhs) != ND_BINARY { r7 = 0 } // (1+2) on the LEFT now
2348 if r7 == 1 { if jp_nextra(c7, lhs) != OP_ADD { r7 = 0 } }
2349 if jp_nkind(c7, rhs) != ND_NUMBER { r7 = 0 } // 3 on the right
2350 }
2351 }
2352 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) }
2353 tot = tot + 1
2354
2355 // ---- KAT 8 (index + nested call chain): a[b].c(d, e) -> CALL(MEMBER(INDEX(a,b),c),[d,e]) ----
2356 let s8: *u8 = "a[b].c(d, e)\x00" as *u8
2357 let prog8: i64 = jq_parse(s8, ctxbox)
2358 let c8: *i64 = jq_ctx(ctxbox)
2359 var r8: i64 = 1
2360 if jq_haserr(ctxbox) == 1 { r8 = 0 }
2361 if r8 == 1 {
2362 let st: i64 = jp_child_at(c8, prog8, 0)
2363 let call: i64 = jp_na(c8, st)
2364 if jp_nkind(c8, call) != ND_CALL { r8 = 0 }
2365 if r8 == 1 { if jp_nc(c8, call) != 2 { r8 = 0 } } // 2 args
2366 if r8 == 1 {
2367 let mem: i64 = jp_na(c8, call)
2368 if jp_nkind(c8, mem) != ND_MEMBER { r8 = 0 }
2369 if r8 == 1 {
2370 let idx: i64 = jp_na(c8, mem)
2371 if jp_nkind(c8, idx) != ND_INDEX { r8 = 0 }
2372 if r8 == 1 {
2373 if jp_nkind(c8, jp_na(c8, idx)) != ND_IDENT { r8 = 0 } // a
2374 if jp_nkind(c8, jp_nb(c8, idx)) != ND_IDENT { r8 = 0 } // b
2375 }
2376 }
2377 }
2378 }
2379 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) }
2380 tot = tot + 1
2381
2382 // ---- KAT 9 (literals + unary families that had ZERO coverage before): assert
2383 // STRING / null (NULL) / true,false (BOOL) primary nodes AND typeof / unary
2384 // '-' / unary '+' unary-op codes. Council BLOCKER fix: these code paths
2385 // existed but no KAT ever asserted their nodes -> 'ALL gated' was false. ----
2386 var r9: i64 = 1
2387 // string literal "x" -- byte-built so the source escape is unambiguous.
2388 let s9str: *u8 = sys_mmap(8)
2389 s9str[0] = 34 as u8; s9str[1] = 120 as u8; s9str[2] = 34 as u8; s9str[3] = 0 as u8 // "x"
2390 let p9str: i64 = jq_parse(s9str, ctxbox)
2391 if jq_haserr(ctxbox) == 1 { r9 = 0 }
2392 if r9 == 1 { if jq_expr0_kind(ctxbox, p9str) != ND_STRING { r9 = 0 } }
2393 // null
2394 if r9 == 1 {
2395 let p: i64 = jq_parse("null\x00" as *u8, ctxbox)
2396 if jq_haserr(ctxbox) == 1 { r9 = 0 }
2397 if r9 == 1 { if jq_expr0_kind(ctxbox, p) != ND_NULL { r9 = 0 } }
2398 }
2399 // true -> BOOL extra=1
2400 if r9 == 1 {
2401 let p: i64 = jq_parse("true\x00" as *u8, ctxbox)
2402 if jq_haserr(ctxbox) == 1 { r9 = 0 }
2403 if r9 == 1 { if jq_expr0_kind(ctxbox, p) != ND_BOOL { r9 = 0 } }
2404 if r9 == 1 { if jq_expr0_op(ctxbox, p) != 1 { r9 = 0 } }
2405 }
2406 // false -> BOOL extra=0
2407 if r9 == 1 {
2408 let p: i64 = jq_parse("false\x00" as *u8, ctxbox)
2409 if jq_haserr(ctxbox) == 1 { r9 = 0 }
2410 if r9 == 1 { if jq_expr0_kind(ctxbox, p) != ND_BOOL { r9 = 0 } }
2411 if r9 == 1 { if jq_expr0_op(ctxbox, p) != 0 { r9 = 0 } }
2412 }
2413 if r9 == 1 { if jq_unop_is("typeof a\x00" as *u8, ctxbox, OP_TYPEOF) == 0 { r9 = 0 } }
2414 if r9 == 1 { if jq_unop_is("-a\x00" as *u8, ctxbox, OP_NEG) == 0 { r9 = 0 } }
2415 if r9 == 1 { if jq_unop_is("+a\x00" as *u8, ctxbox, OP_POS) == 0 { r9 = 0 } }
2416 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) }
2417 tot = tot + 1
2418
2419 // ---- KAT 10 (operator families that had ZERO coverage): one BINARY node per
2420 // previously-untested operator -- '/' '%' (mul tier), '<' '<=' '>' '>='
2421 // (rel tier), '!=' '===' '!==' (eq tier). Makes 'ALL gated' honest. ----
2422 var r10: i64 = 1
2423 if jq_binop_is("a/b\x00" as *u8, ctxbox, OP_DIV) == 0 { r10 = 0 }
2424 if jq_binop_is("a%b\x00" as *u8, ctxbox, OP_MOD) == 0 { r10 = 0 }
2425 if jq_binop_is("a<b\x00" as *u8, ctxbox, OP_LT) == 0 { r10 = 0 }
2426 if jq_binop_is("a<=b\x00" as *u8, ctxbox, OP_LE) == 0 { r10 = 0 }
2427 if jq_binop_is("a>b\x00" as *u8, ctxbox, OP_GT) == 0 { r10 = 0 }
2428 if jq_binop_is("a>=b\x00" as *u8, ctxbox, OP_GE) == 0 { r10 = 0 }
2429 if jq_binop_is("a!=b\x00" as *u8, ctxbox, OP_NE) == 0 { r10 = 0 }
2430 if jq_binop_is("a===b\x00" as *u8, ctxbox, OP_SEQ) == 0 { r10 = 0 }
2431 if jq_binop_is("a!==b\x00" as *u8, ctxbox, OP_SNE) == 0 { r10 = 0 }
2432 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) }
2433 tot = tot + 1
2434
2435 // ---- KAT 11 (ASI tamper -- the BLOCKER): adjacent statements on ONE LINE with
2436 // no separator MUST error ('a b', '1 2' are real-JS SyntaxErrors); but a
2437 // LINE BREAK between them is a valid ASI point and MUST parse as 2 stmts.
2438 // Proves we ENFORCE same-line separation yet still honor ASI (not just
2439 // reject-everything). ----
2440 var r11: i64 = 1
2441 // 'a b' -> error
2442 let p11a: i64 = jq_parse("a b\x00" as *u8, ctxbox)
2443 if jq_haserr(ctxbox) != 1 { r11 = 0 }
2444 // '1 2' -> error
2445 let p11b: i64 = jq_parse("1 2\x00" as *u8, ctxbox)
2446 if jq_haserr(ctxbox) != 1 { r11 = 0 }
2447 // 'a\nb' (newline separator) -> OK, 2 statements, no error
2448 let s11c: *u8 = sys_mmap(8)
2449 s11c[0] = 97 as u8; s11c[1] = 10 as u8; s11c[2] = 98 as u8; s11c[3] = 0 as u8 // a<LF>b
2450 let p11c: i64 = jq_parse(s11c, ctxbox)
2451 let c11c: *i64 = jq_ctx(ctxbox)
2452 if jq_haserr(ctxbox) == 1 { r11 = 0 }
2453 if r11 == 1 { if jp_nb(c11c, p11c) != 2 { r11 = 0 } } // exactly 2 statements
2454 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) }
2455 tot = tot + 1
2456
2457 // ---- KAT 12 (lvalue tamper -- the BLOCKER): assignment to a non-lvalue MUST
2458 // error. '1 = 2' (NUMBER lhs) and '(a+b) = c' (BINARY lhs) are real-JS
2459 // 'Invalid left-hand side in assignment'. Also confirm a VALID lvalue
2460 // 'a.b = c' (MEMBER) still parses, so we reject only the invalid ones. ----
2461 var r12: i64 = 1
2462 let p12a: i64 = jq_parse("1 = 2\x00" as *u8, ctxbox)
2463 if jq_haserr(ctxbox) != 1 { r12 = 0 }
2464 let p12b: i64 = jq_parse("(a+b) = c\x00" as *u8, ctxbox)
2465 if jq_haserr(ctxbox) != 1 { r12 = 0 }
2466 // sanity: a valid lvalue target (member) still parses as ASSIGN, no error.
2467 let p12c: i64 = jq_parse("a.b = c\x00" as *u8, ctxbox)
2468 if jq_haserr(ctxbox) == 1 { r12 = 0 }
2469 if r12 == 1 { if jq_expr0_kind(ctxbox, p12c) != ND_ASSIGN { r12 = 0 } }
2470 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) }
2471 tot = tot + 1
2472
2473 jq_puts("---- nx_js_parse gate: passed " as *u8); jq_putn(pass); jq_puts(" / " as *u8); jq_putn(tot); jq_puts("\n" as *u8)
2474 let lfd: i64 = sys_openat_append("knowledge/status/js_engine.log\x00" as *u8, 0x1a4)
2475 if lfd >= 0 {
2476 sys_write(lfd, "R-JS-PARSE organ=nx_js_parse kats=" as *u8, 34)
2477 jq_fdn(lfd, pass); sys_write(lfd, "/" as *u8, 1); jq_fdn(lfd, tot)
2478 if pass == tot { sys_write(lfd, " tamper=ok verdict=GREEN\n" as *u8, 25) }
2479 if pass != tot { sys_write(lfd, " tamper=?? verdict=RED\n" as *u8, 23) }
2480 sys_close(lfd)
2481 }
2482 if pass == tot { sys_exit(0); return 0 }
2483 sys_exit(1)
2484 return 1
2485}