nx_js_eval.nx source
↩ module page · 4641 lines · 262144 B
1// nx_js_eval.nx -- R-JS-EVAL (WB-JS-001 rung 2): the ECMAScript tree-walking
2// EVALUATOR with a variable ENVIRONMENT (scope chain) + USER FUNCTIONS, riding the
3// gated parser (nx_js_parse) per the no-floating law. Shape = recursive tree-walk
4// over the parser's flat node arena. Founds R-JS-RUNTIME (objects/builtins) and the
5// DOM bindings that render JS-walled pages (google image results are JS-walled).
6//
7// VALUE = 2 i64 slots [type, payload]:
8// VAL_UNDEF / VAL_NULL ; VAL_BOOL: 0|1 ; VAL_NUM: integer ;
9// VAL_STR: pointer (as i64) to a string-heap RECORD [len:i64][bytes...] -- so '+'
10// can produce real, comparable byte strings (concat) ; VAL_FUNC: function-decl
11// node index (params+body live in the AST; the call frame parents the global env).
12//
13// ERROR is signalled by an eval return code of 1 (NOT a tag): a ReferenceError --
14// reading or assigning an UNDECLARED name -- is an ERROR, never a silent undefined
15// (`y+1` with y undeclared yields an error result, matching real JS "y is not defined").
16//
17// RUNG-2 (ALL gated):
18// expressions: literals (number/string/bool/null), identifier lookup with
19// ReferenceError, unary ! - + typeof, binary + - * / % (integer; + does STRING
20// CONCAT when either operand is a string, coercing the other), comparison
21// < <= > >= (numeric; AND byte-lexicographic when BOTH operands are strings:
22// 'a'<'b', 'apple'<'banana', 'ab'<'abc'), equality == != === !== (strings
23// compared by bytes), logical && ||
24// SHORT-CIRCUIT (returns the deciding OPERAND value = real JS), assignment (incl
25// chained a=b=c -- updates the DECLARING scope, never auto-creates a global).
26// statements: var/let/const decl, assignment, expr stmt, { blocks }, if/else,
27// while (iteration cap), return.
28// FUNCTIONS: `function f(params){body}` is a VAL_FUNC; CALL f(args) opens a fresh
29// call env (parent = global env -> globals + recursion resolve), binds params
30// (a param SHADOWS an outer same-named var), runs the body, and RETURN unwinds
31// (completion status 2) out of any nested if/while to the call boundary.
32// Recursion works (fact(5)=120, fib(10)=55). Function declarations are HOISTED both at
33// the TOP LEVEL (js_run_source) and INTRA-BODY (js_call_core pre-pass over the body
34// block's DIRECT children) -- so a nested `function inner(){}` is callable BEFORE its
35// textual position within the same function body (rung 7). Decls inside inner blocks
36// (if/for) are bound when reached, not hoisted across block boundaries (real-JS `var`-
37// style function hoisting to the enclosing function scope is a NAMED OPEN -- only the
38// body's DIRECT children are pre-bound).
39//
40// RUNG-6 (R-JS-CLOSURE, NOW DONE + gated): FIRST-CLASS FUNCTIONS with PROPER LEXICAL
41// CLOSURES. A function VALUE is now a CLOSURE record [fnode, defenv] (clos_new): when a
42// function/arrow LITERAL or DECL is evaluated it captures the env in scope at THAT point,
43// and js_call_userfn parents the call frame to the captured DEFENV (not genv) -- so an
44// inner function returned from an outer one resolves free vars up the LEXICAL chain to the
45// enclosing function's locals (makeAdder(5)->add5(3)=8; a returned counter mutates its
46// captured `c` across calls; two closures from one factory have INDEPENDENT frames). Also:
47// FUNCTION EXPRESSIONS (`var f=function(x){...}`, named fn-exprs, IIFE `(function(){})()`),
48// and ARROW functions (`x=>x+1`, `(a,b)=>a+b`, `()=>e`, block-body `(x)=>{...}`) -- arrows
49// desugar to the same ND_FUNC_DECL node and are CLOSURES too. Lexical (NOT dynamic) scope is
50// gated by a discriminating case (a closure reads its DEFINING binding, never the caller's),
51// and a free var absent in chain+global is still a ReferenceError (rc=1).
52//
53// RUNG-7 (R-JS-CALLBACK, NOW DONE + gated): ARRAY ITERATION methods that CALL BACK into a
54// user CLOSURE, plus INTRA-BODY function-decl hoisting. A reusable closure-invoke core
55// (js_call_core) takes a closure value + an array of PRE-EVALUATED arg value-cells (the
56// inverse of normal ND_CALL where args live in the AST), builds the call frame parented to
57// the closure's captured defenv, binds params positionally, hoists the body's nested decls,
58// runs the body, and returns the value. Both ND_CALL (js_call_userfn) and the array methods
59// route through this ONE core. Methods (this = the array, arg0 = the callback closure):
60// .forEach(fn) -> call fn(el, i) in order; returns undefined.
61// .map(fn) -> a NEW array of fn(el, i) (same length).
62// .filter(fn) -> a NEW array of els where fn(el, i) is truthy.
63// .reduce(fn[,init]) -> acc = fn(acc, el, i) from init (no-init: start at el0; empty+
64// no-init = honest ERROR rc=1).
65// .some(fn) -> true if fn(el,i) truthy for ANY (short-circuit); .every -> ALL.
66// A NON-FUNCTION callback (`[1,2].map(5)`) is an HONEST ERROR (rc=1), never silent. The
67// callback may be a CLOSURE over outer vars (forEach side-effect into an outer `s`) or an
68// arrow. INTRA-BODY HOISTING: js_call_core pre-binds each ND_FUNC_DECL among the body
69// block's DIRECT children before executing, so `inner()` is callable before its decl.
70// HONEST OPEN (rung 7, NAMED not faked): the callback's 3rd argument (the ARRAY ITSELF) is
71// not passed (only element + index); thisArg (the 2nd .map/.forEach argument) is not bound;
72// .sort() / .sort(cmp), .reduceRight, .flat / .flatMap, .find / .findIndex are NOT
73// implemented and NOT gated. Decls inside an inner block (if/for) hoist only to that block
74// when reached (function-scope `var`-style hoisting across blocks remains a NAMED OPEN).
75//
76// HONEST OPEN (named, NOT faked/stubbed-as-done):
77// - f64 / IEEE-754 floating point: NishiLang has NO native float, so JS numbers are
78// i64 INTEGERS this rung; non-integer `/` truncates, float literals (3.14, 1e3),
79// NaN/Infinity are rung-2b. Division by zero -> ERROR (no Infinity).
80// - `this` binding for user functions does NOT exist yet (user fns have no receiver this
81// rung), so `this` INSIDE AN ARROW (which should lexically inherit the enclosing `this`)
82// is a NAMED OPEN -- there is no `this` for it to capture until the `this`/`new`/method
83// rung lands. Default/rest params and destructured arrow params are also NAMED OPEN
84// (arrow + fn-expr params are plain idents only). The cover-grammar disambiguation handles
85// the common `(x)=>`, `(x,y)=>`, `x=>`, `()=>` cases; deeply ambiguous covers (e.g. an
86// arrow head buried after a comma in a larger expression) remain a NAMED OPEN.
87// - RUNG-3 (R-JS-OBJ, NOW DONE + gated): a HEAP object model -- VAL_OBJECT (a string
88// keyed property table) and VAL_ARRAY (length + indexed cells, `.length`), allocated
89// via sys_mmap. Object {k:v} / array [e0,e1] LITERALS, MEMBER read (o.x -> value or
90// UNDEFINED when absent, real JS), INDEX read (o[i]/arr[i], out-of-range arr -> undefined,
91// object key coerced to a string name), and MEMBER/INDEX WRITE (o.x=v, arr[i]=v create/
92// update; array write past length EXTENDS length). typeof object/array -> "object".
93// Member/index on a PRIMITIVE (number/bool) is a NAMED OPEN -> ERROR (rc=1), never a
94// fabricated value (KAT16 tamper). `str.length` + string indexing = R-JS-RUNTIME open.
95// - STILL OPEN (R-JS-RUNTIME / later rungs, NAMED not faked): prototype methods
96// (Array.push/pop, Object.keys, String methods), `this`/`new`/classes, getters/setters,
97// computed object keys {[k]:v}, shorthand/method props, spread/rest, for-in over keys,
98// property delete. These are NOT claimed and NOT gated as working.
99// - relational < <= > >= with MIXED string/number operands now COERCES NUMERICALLY for
100// the integer subset (council fix): a string that is a clean (optionally negative) integer
101// run parses via ToNumber, so '10'<5 = false, '5'<6 = true, 6>'5' = true -- NOT the old
102// silent str->0. A NON-numeric string is NaN in JS, which needs the float ladder, so it
103// ERRORs (rc=1) instead of the banned coerce-to-0. Full float/whitespace ToNumber + mixed
104// ARITHMETIC ('5'-1) string coercion remain rung-2b opens (arithmetic still str->0/concat).
105// - RUNG-4 (R-JS-CTRL, NOW DONE + gated): C-style `for(init;cond;update)body` (init = var-
106// decl/expr/empty, cond/update may be empty), `do body while(cond)`, ternary `cond?a:b`
107// (only the taken branch evaluated -- short-circuit proven), compound assign `+= -= *= /= %=`
108// (read-op-write; works for IDENT, MEMBER `o.n+=6`, and INDEX `a[i]+=10` lvalues; `+=` does
109// STRING CONCAT like `+`), and `break`/`continue` (completion signals CS_BREAK/CS_CONTINUE
110// consumed by the nearest enclosing while/for/do-while; a stray break/continue OUTSIDE any
111// loop -- at a function boundary or program top -- is a NAMED ERROR, real JS Illegal break).
112// - STILL OPEN (R-JS-CTRL successors, NAMED not faked): for-in (key iteration) / for-of
113// (iterator protocol), switch/case, LABELED break/continue (need a label table), the comma
114// operator -- these are later rungs; the gate does NOT assert them. try/catch/throw,
115// arguments object = 2d. String<->number coercion beyond '+' and loose == = 2b.
116// - Inherits the parser's rung-1b opens (template literals, arrows, classes, destructuring,
117// bitwise & | ^ << >> >>>, ?? / ?., bitwise/shift compound assigns &= |= **= etc.).
118// - DECLARATION-KEYWORD SEMANTICS (council minor): var/let/const all PARSE + bind, but
119// `const` immutability (`const k=1; k=2` is a no-op here; JS throws TypeError) and
120// `let`/`const` BLOCK-scoping (`{let y=5} y` leaks; JS = ReferenceError) are rung-2b --
121// the parser maps all three to ND_VAR_DECL with no keyword code, so the var/let/const
122// distinction is not yet enforced. NB: scoping is BLOCK-based for every declarator incl
123// a for-loop `var` init (`var i=99; for(var i=0;...){} i` => 99, NOT real-JS hoisted 3) --
124// real-JS `var` function-hoisting/leak is the rung-2b open, the OPPOSITE direction.
125//
126// GATE (main): 51 KATs evaluate whole programs and ASSERT the COMPUTED result value --
127// (KAT43-51 add R-JS-CLOSURE: closure capture, mutable-local counters, independent frames,
128// function expressions + IIFE, arrow expr/block bodies, arrow lexical capture, lexical-not-
129// dynamic scope, and the unresolvable-free-var ReferenceError tamper) --
130// precedence-through-eval (2+3*4=14), recursion (fact(5)=120), while-accumulation
131// (sum 1..5=15), assignment mutation, if/else branch selection, logical short-circuit,
132// STRING CONCAT ('a'+'b'="ab"), typeof, functions+lexical scope, STRING-RELATIONAL
133// ordering by bytes ('a'<'b'=true, 'apple'<'banana'=true, 'ab'<'abc'=true, 'b'>'a'=true,
134// 'a'<='a'=true, 'b'<'a'=false), object/array model, control-flow, and the R-JS-RUNTIME
135// builtins -- PLUS TAMPER KATs (an UNDECLARED identifier MUST be an ERROR, never a silent
136// 0/undefined; an OPEN node kind / missing method MUST surface as ERROR/rc=1, never silent-
137// undefined-success; a DETACHED string/array method value called with no receiver MUST
138// error rc=1 and NOT crash -- KAT41; a MIXED string/number relational MUST coerce
139// numerically, NOT silently treat the string as 0 -- KAT42).
140// Self-validating; exit 0 iff all pass; appends knowledge/status/js_engine.log.
141// license_tier: ORIGINAL (tutor-bootstrap; team re-authors from the eval spec).
142import "nx_js_parse.nx"
143import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
144import "nx_dom_query.nx" // DOM binding: document.getElementById composes nx_dom_find_attr_eq
145import "nx_html_tokenizer.nx" // querySelectorAll drives the tokenizer directly to collect all matches
146import "nx_html_entities.nx" // textContent/getAttribute decode &/'/ via the gated table
147import "nx_js_f64.nx" // R-JS-F64-2: f64 JS numbers (VAL_FLOAT) -- int/string converters + soft-float
148import "nx_f64_sqrt.nx" // Math.sqrt (soft-float sqrt; shares nx_f64.nx with nx_js_f64)
149import "_pe_f64pow.nx" // Math.pow over REALS (fdlibm-shape, <=1 ULP; was int-truncating base+exp)
150import "nx_domtree.nx" // R-JS-DOMWRITE: mutable DOM so page scripts can BUILD content (rung B/C)
151import "nx_json.nx" // JSON.parse: REUSE the sovereign JSON tokenizer (don't duplicate a parser)
152import "nx_rxfull.nx" // R-JS-REGEX: the sovereign regex engine (RegExp value + .test / String.search)
153
154// ---- EVAL-CELL ARENA (2026-07-27) -------------------------------------------------
155// Every eval step allocates a 2-slot VALUE box. Those were 204 separate `sys_mmap(16)`
156// call sites: the kernel rounds EACH to a 4096-byte page and, once written, each page is
157// RESIDENT -- 256x memory amplification. MEASURED: a 20M-step script OOM-killed the whole
158// WSL VM, which is why the page fuel had to be capped at 1M steps. Nothing in the engine
159// ever FREES a value box (they live to process exit), so a bump arena is lifetime-identical
160// by construction -- it only stops paying a page per cell. Cells are ZEROED to match
161// MAP_ANONYMOUS semantics exactly, and 16-byte aligned. Chunk exhaustion mmaps another
162// chunk (never fails closed); `ev_cell_bytes()` reports arena footprint for the gate.
163// Statics live HERE, above every reader, per the forward-static-ref miscompile rule.
164const EV_ARENA_CHUNK: i64 = 1048576 // 256 chunks = 1 GiB ceiling at the raised fuel
165static ev_arena_base: i64 = 0
166static ev_arena_off: i64 = 0
167static ev_arena_bytes: i64 = 0
168
169func ev_cell() -> *i64 {
170 if ev_arena_base == 0 {
171 ev_arena_base = sys_mmap(EV_ARENA_CHUNK) as i64
172 ev_arena_off = 0
173 ev_arena_bytes = ev_arena_bytes + EV_ARENA_CHUNK
174 }
175 if (ev_arena_off + 16) > EV_ARENA_CHUNK {
176 ev_arena_base = sys_mmap(EV_ARENA_CHUNK) as i64
177 ev_arena_off = 0
178 ev_arena_bytes = ev_arena_bytes + EV_ARENA_CHUNK
179 }
180 let p: *i64 = (ev_arena_base + ev_arena_off) as *i64
181 ev_arena_off = ev_arena_off + 16
182 p[0] = 0
183 p[1] = 0
184 return p
185}
186func ev_cell_bytes() -> i64 { return ev_arena_bytes }
187
188const VAL_UNDEF: i64 = 0
189const VAL_NULL: i64 = 1
190const VAL_BOOL: i64 = 2
191const VAL_NUM: i64 = 3
192const VAL_STR: i64 = 4
193const VAL_FUNC: i64 = 5
194const VAL_OBJECT: i64 = 6 // payload = ptr(as i64) to an object record (property table)
195const VAL_ARRAY: i64 = 7 // payload = ptr(as i64) to an array record (length + cells)
196// R-JS-RUNTIME (rung 5): builtin library values.
197const VAL_NATIVE: i64 = 8 // payload = a builtin id (BI_*). A native function value.
198const VAL_GLOBALNS: i64 = 9 // payload = a namespace id (NS_*). The Math/Object/console/JSON globals.
199const VAL_FLOAT: i64 = 10 // payload = an f64 BIT-PATTERN (software-IEEE). A JS Number with a fraction.
200const VAL_REGEX: i64 = 15 // payload = ptr to a regex record [*Regex(nx_rxfull), patptr, patlen, flagbits] (moved up: read before its old decl)
201const VAL_PROMISE: i64 = 11 // payload = ptr(as i64) to a promise record (moved up: forward-const fix)
202const VAL_RESOLVE: i64 = 12 // internal handler value: promise ptr to FULFILL (moved up: forward-const fix)
203const VAL_REJECT: i64 = 13 // internal handler value: promise ptr to REJECT (moved up: forward-const fix)
204const VAL_RESPONSE: i64 = 14 // fetch Response record tag (moved up: forward-const fix)
205
206// ---- global-namespace ids (VAL_GLOBALNS payload) -- the bound global objects. ----
207const NS_MATH: i64 = 1
208const NS_OBJECT: i64 = 2
209const NS_CONSOLE: i64 = 3
210const NS_JSON: i64 = 4
211const NS_DOCUMENT: i64 = 5 // the `document` host global (DOM binding)
212
213// ---- builtin ids (VAL_NATIVE payload) -- one per method/free-function. The dispatch
214// tables (ev_native_str/ev_native_arr/ev_native_global) map a method NAME on a given
215// receiver type to one of these; ev_invoke_native runs it with `this` + arg values. ----
216// String methods (this = a string value):
217// Gated runtime diagnostics (CALL-ERR/REF-ERR/PROP-ERR/STMT-ERR raise-site prints). MUST live at the TOP:
218// readers span the whole file and a static defined BELOW a reader is a forward-static-ref MISCOMPILE (reads
219// garbage -> the gates were silently dead when this sat at the bottom; see gotchas). Zero-init = off.
220static js_rt_dbg: i64
221func js_set_rt_dbg(v: i64) -> i64 { js_rt_dbg = v; return 0 }
222// Runtime error-POSITION recorder (the runtime analog of the parser's PST_ERR_POS). A silently-erroring
223// node (member/index assign to a non-object, `in`/binop type errors, `new` of a non-ctor) returns 1 with
224// no leaf print, so an unwinding STMT-ERR only shows the top statement. js_rt_mark records the FIRST such
225// offset; typeof/try SWALLOW their operand errors, so they save+restore this around the swallowed eval.
226static js_rt_errpos: i64 // token offset of the innermost UNSWALLOWED runtime error; -1 = none recorded
227func js_rt_errpos_reset() -> i64 { js_rt_errpos = 0 - 1; return 0 }
228func js_rt_errpos_get() -> i64 { return js_rt_errpos }
229func js_rt_mark(ctx: *i64, idx: i64) -> i64 {
230 if js_rt_errpos < 0 {
231 js_rt_errpos = ev_tok_start(ctx, idx)
232 if js_rt_dbg == 2 { sys_write(2, "MARK @" as *u8, 6); nx_dbg_num(js_rt_errpos); sys_write(2, " '" as *u8, 2); let sp: *u8 = jp_src(ctx); var st: i64 = js_rt_errpos - 20; if st < 0 { st = 0 } var d: i64 = 0; while d < 64 { if sp[st + d] == (0 as u8) { d = 64 } else { sys_write(2, ((sp as i64) + st + d) as *u8, 1); d = d + 1 } } sys_write(2, "'\n" as *u8, 2) }
233 }
234 return 0
235}
236const BI_STR_CHARAT: i64 = 1
237const BI_STR_INDEXOF: i64 = 2
238const BI_STR_SLICE: i64 = 3
239const BI_STR_UPPER: i64 = 4
240const BI_STR_LOWER: i64 = 5
241const BI_STR_INCLUDES:i64 = 6
242const BI_STR_SEARCH: i64 = 145 // str.search(re) -> match index or -1
243const BI_RE_TEST: i64 = 146 // re.test(str) -> bool (unique in js_native_apply; NOT 103/110-range)
244const BI_STR_MATCH: i64 = 147 // str.match(re) -> array (g: full matches; else [full,g1,..]) or null
245const BI_STR_REPLACE: i64 = 148 // str.replace(re,repl) -> string (g: all; $& $1..$9 $$ substitution)
246const BI_REGEX_CTOR: i64 = 149 // new RegExp(pat,flags) / RegExp(pat,flags) -> a VAL_REGEX
247const BI_STR_SPLIT: i64 = 150 // str.split(re) -> array of pieces
248const BI_RE_EXEC: i64 = 151 // re.exec(str) -> [full,g1,..] or null; g-flag advances re record's lastIndex (slot 4)
249const BI_STR_CHARCODEAT: i64 = 152 // "s".charCodeAt(i) -> byte value (ASCII code unit); OOR -> NaN
250const BI_STR_FROMCHARCODE: i64 = 153 // String.fromCharCode(a,b,..) -> string (static on the String native)
251const BI_PARSEINT: i64 = 154 // parseInt(str, radix?) -> int (leading-ws/sign/prefix-digits; none -> NaN)
252const BI_STR_SUBSTRING: i64 = 155 // s.substring(a,b?) -> clamp+swap slice
253const BI_STR_SUBSTR: i64 = 156 // s.substr(a,len?) -> start+length slice
254// Array methods (this = an array value):
255const BI_ARR_PUSH: i64 = 10
256const BI_ARR_POP: i64 = 11
257const BI_ARR_INDEXOF: i64 = 12
258const BI_ARR_JOIN: i64 = 13
259const BI_ARR_SLICE: i64 = 14
260const BI_ARR_REVERSE: i64 = 15 // arr.reverse() -- in-place, returns the array (YouTube sig transform)
261const BI_ARR_SPLICE: i64 = 16 // arr.splice(start,deleteCount,...items) -- mutates, returns removed (YouTube sig transform)
262const BI_ARR_SORT: i64 = 17 // arr.sort([cmp]) -- STABLE in-place sort (Sizzle sortStable needs stability); cmp is a user closure
263const BI_ARR_SHIFT: i64 = 18 // arr.shift() -- remove+return arr[0], shift rest down (jQuery Callbacks/queue)
264const BI_ARR_UNSHIFT: i64 = 19 // arr.unshift(...items) -- prepend items, return new length
265const BI_STUB_NULL: i64 = 200 // headless element-stub method -> null (getAttribute/closest/querySelector)
266const BI_STUB_RETARG: i64 = 201 // headless element-stub method -> arg0 (appendChild/insertBefore are chainable)
267const BI_EL_CLONE: i64 = 202 // element-stub cloneNode -> a fresh element stub
268const BI_STUB_EMPTYARR: i64 = 203 // -> a fresh empty array (getElementsByTagName/querySelectorAll)
269const BI_STUB_FALSE: i64 = 204 // -> false (hasAttribute/contains/matches)
270const BI_EL_CLONE_TREE: i64 = 205 // tree Element.cloneNode(deep) -> wrap dt_clone_subtree of this @node
271const BI_EL_CMPDOCPOS: i64 = 206 // Element.compareDocumentPosition(other) -> DOM-order bitmask (Sizzle sort)
272const BI_ARR_CONCAT: i64 = 207 // arr.concat(...items) -> NEW array (array args spread one level); jQuery flat helper
273const BI_STR_CONCAT: i64 = 208 // str.concat(...args) -> this + args coerced to strings
274const BI_ARR_ISARRAY: i64 = 209 // Array.isArray(x) -> x is a VAL_ARRAY (static on the Array constructor)
275const BI_DOC_CREATE_FRAGMENT: i64 = 210 // document.createDocumentFragment() -> REAL dt container node (nodeType 11)
276const BI_DOC_CREATE_TEXT: i64 = 211 // document.createTextNode(s) -> REAL dt text node (nodeType 3)
277const BI_EL_GET_BY_TAG: i64 = 212 // element.getElementsByTagName(tag) -> descendant elements (Sizzle .find)
278const BI_EL_QSA: i64 = 213 // element.querySelectorAll(sel) -> descendant matches (#id/.class/tag)
279const BI_EL_QS: i64 = 214 // element.querySelector(sel) -> first descendant match or null
280const BI_EL_GET_BY_CLASS: i64 = 215 // element.getElementsByClassName(cls) -> descendant elements w/ class
281const BI_EL_REMOVE_CHILD: i64 = 216 // element.removeChild(child) -> unlink child from tree, return it (.remove())
282const BI_DOC_GET_BY_TAG: i64 = 217 // document.getElementsByTagName(tag) -> whole-tree tag query (Sizzle $('a'))
283const BI_DOC_GET_BY_CLASS: i64 = 218 // document.getElementsByClassName(cls) -> whole-tree class query
284const BI_EL_MATCHES: i64 = 219 // element.matches(sel) -> CSS complex-selector predicate (Sizzle .is/.filter/.closest)
285const BI_EL_GET_ATTR_NODE: i64 = 220 // element.getAttributeNode(name) -> {value,name} or null (Sizzle ID seed-filter + delegation)
286const BI_WIN_ADD_LISTENER: i64 = 221 // window.addEventListener(type,fn) -> STORE + fire DOMContentLoaded/load at render end
287const WIN_SENTINEL: i64 = 0 - 999 // listener-table `node` value marking a window-level handler (tree=0)
288// Array ITERATION methods (R-JS-CALLBACK rung 7) -- this = an array, arg0 = a callback
289// CLOSURE (VAL_FUNC) or arrow. Each calls back into the user closure per element.
290// Distinct 50-range so they never collide with Object(20/21)/Math(30-35)/console(40) ids.
291const BI_ARR_FOREACH: i64 = 50
292const BI_ARR_MAP: i64 = 51
293const BI_ARR_FILTER: i64 = 52
294const BI_ARR_REDUCE: i64 = 53
295const BI_ARR_SOME: i64 = 54
296const BI_ARR_EVERY: i64 = 55
297// Object.* free functions (this ignored; arg0 = the operand object):
298const BI_OBJ_KEYS: i64 = 20
299const BI_OBJ_VALUES: i64 = 21
300const BI_OBJ_DEFINEPROP: i64 = 22 // Object.defineProperty(target, key, {value: v}) -> target[key]=v
301// Object.prototype methods (inherited by every VAL_OBJECT/VAL_FUNC) + Object.getPrototypeOf. jQuery's very
302// first module statements read these off a plain `{}` (class2type.toString / .hasOwnProperty) and dispatch
303// them via `.call(obj)` -- the R-JS-RUNTIME stdlib floor. `this` = the receiver (thisv), not an arg.
304const BI_OBJ_TOSTRING: i64 = 23 // ({}).toString() / toString.call(x) -> "[object Type]" (the toType tag)
305const BI_OBJ_HASOWN: i64 = 24 // ({}).hasOwnProperty(k) / hasOwn.call(o,k) -> bool (OWN props only)
306const BI_OBJ_VALUEOF: i64 = 25 // ({}).valueOf() -> the receiver itself (identity for objects)
307const BI_OBJ_ISPROTOTYPEOF: i64 = 26 // proto.isPrototypeOf(o) -> is `this` on o's [[Prototype]] chain
308const BI_FN_TOSTRING: i64 = 27 // Function.prototype.toString -> a source-ish string (jQuery fnToString)
309const BI_OBJ_GETPROTO: i64 = 28 // Object.getPrototypeOf(o) -> o's [[Prototype]] object or null
310const BI_OBJ_CREATE: i64 = 29 // Object.create(proto) -> new object with [[Prototype]]=proto (jQuery event storage: Object.create(null))
311const BI_OBJ_ASSIGN: i64 = 222 // Object.assign(target, ...sources) -> copy own props of sources into target, return target (ES6, ubiquitous)
312// Math.* free functions (this ignored):
313const BI_MATH_MAX: i64 = 30
314const BI_MATH_MIN: i64 = 31
315const BI_MATH_ABS: i64 = 32
316const BI_MATH_FLOOR: i64 = 33
317const BI_MATH_CEIL: i64 = 34
318const BI_MATH_POW: i64 = 35
319const BI_MATH_RANDOM: i64 = 36 // xorshift64 PRNG -> VAL_FLOAT in [0,1)
320const BI_MATH_SQRT: i64 = 37 // Math.sqrt -> VAL_FLOAT (soft-float sqrt)
321// console.* (this ignored):
322const BI_CONSOLE_LOG: i64 = 40
323
324// DOM builtins (document.*) -- compose nx_dom_query over the page HTML seeded into the global env.
325const BI_DOC_GET_BY_ID: i64 = 60
326const BI_DOC_QUERY_SELECTOR: i64 = 61
327const BI_EL_GET_ATTR: i64 = 62 // element.getAttribute(name) (method on a snapshot element)
328const BI_DOC_QUERY_SELECTOR_ALL: i64 = 63 // document.querySelectorAll(sel) -> array of elements
329// DOM-WRITE mode: when genv[3]==DOM_TREE_SENTINEL, genv[2] is a *DomTree (mutable), not html bytes.
330const DOM_TREE_SENTINEL: i64 = 0 - 424242
331const BI_DOC_CREATE_ELEMENT: i64 = 64 // document.createElement(tag) (tree mode)
332const BI_EL_APPEND_CHILD: i64 = 65 // element.appendChild(child)
333const BI_EL_SET_ATTR: i64 = 66 // element.setAttribute(name, value)
334const BI_JSON_PARSE: i64 = 67 // JSON.parse(text)
335const BI_EL_ADD_LISTENER: i64 = 68 // element.addEventListener(type, fn) -- R-JS-EVENT (execution verify)
336const BI_EL_CLICK: i64 = 69 // element.click() -- fire the element's click listeners (test driver)
337const BI_CLS_TOGGLE: i64 = 70 // element.classList.toggle(name[, force]) -- R-JS-CLASSLIST
338const BI_CLS_ADD: i64 = 71 // element.classList.add(name)
339const BI_CLS_REMOVE: i64 = 72 // element.classList.remove(name)
340const BI_CLS_CONTAINS: i64 = 73 // element.classList.contains(name)
341
342// statement completion status
343const CS_NORMAL: i64 = 0
344const CS_ERROR: i64 = 1
345const CS_RETURN: i64 = 2
346const CS_BREAK: i64 = 3 // R-JS-CTRL: nearest enclosing loop stops
347const CS_CONTINUE: i64 = 4 // R-JS-CTRL: nearest enclosing loop skips to its update/next iter
348
349const EV_LOOP_CAP: i64 = 10000000
350
351// GLOBAL EXECUTION FUEL (2026-07-27, JS-lane T1 prerequisite): EV_LOOP_CAP is PER
352// LOOP -- two nested capped loops = 10^14 potential steps, and recursion has no cap
353// at all, so a hostile/heavy real-page script could still wall-hang the fetcher.
354// Fuel counts EVERY js_eval dispatch globally; exhaustion surfaces as an eval error
355// (rc=1) that unwinds like any ReferenceError -- fail-safe, page renders script-less.
356// 0 = disarmed (gates/tests keep exact old behavior); page renders ARM it.
357// Statics live HERE (top, before all readers) per the fwd-static-ref miscompile rule.
358// SIZED BY MEASUREMENT, not taste (rule 11). With the eval-cell arena live, footprint is
359// a MEASURED 12.5 bytes/step and ~32 ms per million steps (nx_jsfuel_probe at 200k/1M/5M
360// = 2048/12288/62464 KiB, linear). 10M steps => ~125 MiB arena, ~320 ms worst case: a
361// sane per-page ceiling for a browser tab. BEFORE the arena each step cost a RESIDENT
362// 4096-byte page (328x more) and a 20M-step burn OOM-killed the whole VM -- that is what
363// held this at 1M. Re-measure with nx_jsfuel_probe <budget> before changing it.
364const EV_FUEL_PAGE_DEFAULT: i64 = 10000000
365static ev_fuel_max: i64 = 0
366static ev_fuel_used: i64 = 0
367static ev_fuel_hit: i64 = 0
368
369func ev_fuel_arm(n: i64) -> i64 { ev_fuel_max = n; ev_fuel_used = 0; ev_fuel_hit = 0; return 0 }
370func ev_fuel_spent() -> i64 { return ev_fuel_used }
371func ev_fuel_exhausted() -> i64 { return ev_fuel_hit }
372// Per-run reset at every run entry (debt 1785032369): keep a caller's custom armed budget, else arm the
373// measured page default -- DISARMED-BY-DEFAULT left every consumer unbounded in practice, which is the
374// hostile-input hole the debt measured (the crawler at minutes-per-page). A run entry re-arms, so a spent
375// budget never poisons the next program.
376func ev_fuel_run_reset() -> i64 {
377 var b: i64 = ev_fuel_max
378 if b == 0 { b = EV_FUEL_PAGE_DEFAULT }
379 return ev_fuel_arm(b)
380}
381
382// read the FOR body node (stored in the tokidx slot[4] of an ND_FOR; a/b/c hold
383// init/cond/update). Kept as a named accessor so the loop reader stays readable.
384func ev_for_body(ctx: *i64, idx: i64) -> i64 { let nodes: *i64 = jp_nodes(ctx); return nodes[idx * NODE_SLOTS + 4] }
385// ND_TRY's finally BLOCK rides the same slot-4 (tokidx) position ev_for_body reads for ND_FOR.
386func ev_try_finally(ctx: *i64, idx: i64) -> i64 { let nodes: *i64 = jp_nodes(ctx); return nodes[idx * NODE_SLOTS + 4] }
387// pending-throw channel on the GLOBAL env (see ENV_HDR note): set by ND_THROW, taken by ND_TRY.
388func ev_throw_set(genv: *i64, t: i64, p: i64) -> i64 { genv[6] = 1; genv[7] = t; genv[8] = p; return 0 }
389func ev_throw_take(genv: *i64, out: *i64) -> i64 {
390 if genv[6] == 0 { return 0 }
391 genv[6] = 0
392 ev_set(out, genv[7], genv[8])
393 return 1
394}
395// non-consuming: is a thrown VALUE pending? (diagnostics only -- a catching ND_TRY must use take.)
396func ev_throw_peek(genv: *i64) -> i64 { return genv[6] }
397
398func ev_set(out: *i64, t: i64, p: i64) -> i64 { out[0] = t; out[1] = p; return 0 }
399func ev_copy(out: *i64, src: *i64) -> i64 { out[0] = src[0]; out[1] = src[1]; return 0 }
400func ev_b2(cond: i64) -> i64 { if cond == 1 { return 1 } return 0 }
401func ev_isnull(e: *i64) -> i64 { if (e as i64) == 0 { return 1 } return 0 }
402
403func ev_atoi(src: *u8, start: i64, len: i64) -> i64 {
404 var n: i64 = 0
405 var i: i64 = 0
406 while i < len { let c: i64 = src[start + i] & 0xff; if c >= 48 { if c <= 57 { n = n * 10 + (c - 48) } } i = i + 1 }
407 return n
408}
409func ev_tok(ctx: *i64, idx: i64) -> i64 { let nodes: *i64 = jp_nodes(ctx); return nodes[idx * NODE_SLOTS + 4] }
410func ev_tok_kind(ctx: *i64, idx: i64) -> i64 { let toks: *i64 = jp_toks(ctx); return toks[ev_tok(ctx, idx) * 3 + 0] }
411func ev_tok_start(ctx: *i64, idx: i64) -> i64 { let toks: *i64 = jp_toks(ctx); return toks[ev_tok(ctx, idx) * 3 + 1] }
412func ev_tok_len(ctx: *i64, idx: i64) -> i64 { let toks: *i64 = jp_toks(ctx); return toks[ev_tok(ctx, idx) * 3 + 2] }
413func ev_num_of(ctx: *i64, idx: i64) -> i64 { return ev_atoi(jp_src(ctx), ev_tok_start(ctx, idx), ev_tok_len(ctx, idx)) }
414
415// ===================== string heap =====================
416// A VAL_STR payload is a pointer (cast to i64) to a RECORD: [len: i64][bytes...].
417// String literals copy their INNER bytes (quotes stripped) into a fresh record; '+'
418// concat allocates a new record. This replaces the old "payload = AST node index"
419// model so concatenation produces real, comparable byte strings.
420// BUMP POOL: objects/arrays/strings were each a separate sys_mmap syscall (churn-heavy code = one syscall
421// PER allocation -- 50k objects = 50k mmaps; the dominant object-churn cost vs V8's bump allocator). Pool
422// them: bump-allocate from 16MB chunks, one mmap per chunk. Callers already init every field they read
423// (obj_new sets the header, obj_get bounds-checks count, arr_set fills gaps, ev_str_new's caller writes all
424// len bytes) so the pool NOT re-zeroing per alloc is safe. Pool never frees (same as before; object GC = P7).
425static nx_pool: i64
426static nx_pool_hp: i64
427static nx_pool_cap: i64
428static nx_pool_total: i64
429static nx_pool_count: i64
430// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
431// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
432// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
433// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
434func nx_dbg_num(v: i64) -> i64 { nxi_out(v); return 0 }
435// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
436// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
437// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
438// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
439func nx_dbg_num1(v: i64) -> i64 { nxi_out(v); return 0 }
440// ---- GC HEAP: CONSERVATIVE mark-sweep over the object/array/string pool, made SAFE by a BLOCK-START BITMAP.
441// Each block = [ i64 meta ][ payload... ], meta = (payload_bytes << 3) | (free<<1) | mark; nx_pool_alloc returns
442// the PAYLOAD ptr. Per chunk a BITMAP marks every block-START word, so a candidate pointer is validated EXACTLY
443// (interior/false pointers are rejected -> no corruption; the ONLY hazard of conservative GC is eliminated).
444// The tracer scans every word of every reachable block + root region and follows only words the bitmap confirms
445// are real payloads -> never under-retains (safe); may over-retain a little (a leak, acceptable). Chunks are
446// RETAINED [base,used,bitmap] so the sweep walks all blocks linearly; freed blocks go to size-class free-lists.
447// gc_enabled gates collection; the VM triggers it at a SAFE instruction boundary (precise root regions).
448const GC_NCLASS: i64 = 8192 // free-list size classes by payload_words (nb/8); >= this = no reuse
449const GC_CST: i64 = 3 // chunk-table stride: [base, used_bytes, bitmap_ptr]
450static gc_freelist: i64 // *i64[GC_NCLASS] heads (payload ptr of first free block; 0=none)
451static gc_chunks: i64 // *i64 retired-chunk table (GC_CST per entry)
452static gc_chunk_n: i64
453static gc_chunk_cap: i64
454static nx_pool_bitmap: i64 // *u8 block-start bitmap for the CURRENT chunk (1 bit / 8 bytes)
455static gc_enabled: i64 // 0 = bump+headers only; 1 = collect
456static gc_since: i64 // bytes bump-allocated since the last collection
457static gc_collections: i64
458static gc_work: i64 // *i64 mark worklist (block HEADER addrs to trace)
459static gc_work_n: i64
460static gc_work_cap: i64
461static gc_threshold: i64 // collect once gc_since bytes bump-allocated since the last cycle (0=use default)
462static gc_nosweep: i64 // DIAGNOSTIC: 1 = mark/trace but never free (isolates scan bugs from sweep bugs)
463const GC_THRESHOLD_DEFAULT: i64 = 16777216 // 16MB = one chunk; small programs never cross it (0 collections)
464// GC is ON BY DEFAULT (a SOTA engine collects): the JIT stays enabled (roots on the vs-stack, synced at calls +
465// inline loop-back-edge safepoints = ~0 overhead), so default-on costs small programs nothing (they never cross
466// the threshold) and makes allocation-heavy programs sustainable. A gate that must measure UNCOLLECTED behavior
467// (e.g. nx_js_octane_memcensus) calls gc_enable(0). gc_configure(N) overrides the threshold.
468func gc_init() -> i64 {
469 if gc_freelist == 0 {
470 gc_freelist = sys_mmap(GC_NCLASS * 8) as i64
471 gc_chunk_cap = 8192
472 gc_chunks = sys_mmap(gc_chunk_cap * GC_CST * 8) as i64
473 gc_chunk_n = 0
474 gc_work_cap = 1048576
475 gc_work = sys_mmap(gc_work_cap * 8) as i64
476 gc_work_n = 0
477 gc_enabled = 1 // DEFAULT-ON
478 gc_threshold = GC_THRESHOLD_DEFAULT
479 }
480 return 0
481}
482func gc_setbit(bm: i64, wordidx: i64) -> i64 { let b: *u8 = bm as *u8; let byi: i64 = wordidx / 8; b[byi] = (b[byi] | (1 << (wordidx % 8))) as u8; return 0 }
483func gc_getbit(bm: i64, wordidx: i64) -> i64 { let b: *u8 = bm as *u8; return (b[wordidx / 8] >> (wordidx % 8)) & 1 }
484// record the CURRENT chunk (about to be replaced) so the sweep can still walk it.
485func gc_retire_chunk() -> i64 {
486 if nx_pool == 0 { return 0 }
487 gc_init()
488 if gc_chunk_n >= gc_chunk_cap {
489 let ncap: i64 = gc_chunk_cap * 2
490 let nt: *i64 = sys_mmap(ncap * GC_CST * 8) as *i64
491 let ot: *i64 = gc_chunks as *i64
492 var i: i64 = 0
493 while i < (gc_chunk_n * GC_CST) { nt[i] = ot[i]; i = i + 1 }
494 gc_chunks = nt as i64
495 gc_chunk_cap = ncap
496 }
497 let t: *i64 = gc_chunks as *i64
498 t[gc_chunk_n * GC_CST] = nx_pool
499 t[gc_chunk_n * GC_CST + 1] = nx_pool_hp
500 t[gc_chunk_n * GC_CST + 2] = nx_pool_bitmap
501 gc_chunk_n = gc_chunk_n + 1
502 return 0
503}
504func nx_pool_alloc(bytes: i64) -> i64 {
505 let nb: i64 = (bytes + 7) / 8 * 8
506 nx_pool_count = nx_pool_count + 1
507 // reuse a freed block of the SAME size class (nb/8) if one is on the free-list.
508 let cls: i64 = nb / 8
509 if cls < GC_NCLASS {
510 gc_init()
511 let fl: *i64 = gc_freelist as *i64
512 let head: i64 = fl[cls]
513 if head != 0 {
514 let hpp: *i64 = head as *i64 // payload; payload[0] links to the next free block
515 fl[cls] = hpp[0]
516 let meta: *i64 = (head - 8) as *i64
517 meta[0] = (nb << 3) // in-use: free=0 mark=0
518 var z: i64 = 0
519 while z < cls { hpp[z] = 0; z = z + 1 } // restore the mmap zero-fill invariant
520 return head
521 }
522 }
523 let need: i64 = nb + 8
524 nx_pool_total = nx_pool_total + need
525 gc_since = gc_since + need
526 if (nx_pool_hp + need) > nx_pool_cap {
527 gc_retire_chunk()
528 var cs: i64 = 16777216 // 16MB chunk
529 if need > cs { cs = need }
530 gc_init()
531 nx_pool = sys_mmap(cs) as i64
532 nx_pool_bitmap = sys_mmap(cs / 64 + 8) as i64 // 1 bit per 8-byte word = cs/8 bits = cs/64 bytes
533 nx_pool_hp = 0
534 nx_pool_cap = cs
535 }
536 let base: i64 = nx_pool + nx_pool_hp
537 gc_setbit(nx_pool_bitmap as i64, nx_pool_hp / 8) // mark this block START word for exact pointer validation
538 nx_pool_hp = nx_pool_hp + need
539 let m: *i64 = base as *i64
540 m[0] = (nb << 3) // size, free=0 mark=0
541 return base + 8
542}
543// validate a candidate payload pointer: return its HEADER addr if `w` is a real in-pool block start, else 0.
544func gc_is_start(w: i64) -> i64 {
545 let h: i64 = w - 8
546 if nx_pool != 0 { if h >= nx_pool { if h < (nx_pool + nx_pool_hp) {
547 if gc_getbit(nx_pool_bitmap as i64, (h - nx_pool) / 8) == 1 { return h }
548 return 0
549 } } }
550 let t: *i64 = gc_chunks as *i64
551 var c: i64 = 0
552 while c < gc_chunk_n {
553 let cb: i64 = t[c * GC_CST]
554 if h >= cb { if h < (cb + t[c * GC_CST + 1]) {
555 if gc_getbit(t[c * GC_CST + 2] as i64, (h - cb) / 8) == 1 { return h }
556 return 0
557 } }
558 c = c + 1
559 }
560 return 0
561}
562// mark the block at header `h` (if in-use + not already marked) and enqueue it for tracing.
563func gc_mark_header(h: i64) -> i64 {
564 let meta: *i64 = h as *i64
565 let m: i64 = meta[0]
566 if (m & 1) == 1 { return 0 } // already marked
567 if (m & 2) == 2 { return 0 } // free block -> never mark (a live ptr never points here)
568 meta[0] = m | 1
569 if gc_work_n >= gc_work_cap {
570 let ncap: i64 = gc_work_cap * 2
571 let nw: *i64 = sys_mmap(ncap * 8) as *i64
572 let ow: *i64 = gc_work as *i64
573 var i: i64 = 0
574 while i < gc_work_n { nw[i] = ow[i]; i = i + 1 }
575 gc_work = nw as i64
576 gc_work_cap = ncap
577 }
578 let wl: *i64 = gc_work as *i64
579 wl[gc_work_n] = h
580 gc_work_n = gc_work_n + 1
581 return 0
582}
583// scan `nwords` i64 words at `base_addr`, marking every one that is a valid pool payload pointer.
584func gc_scan_region(base_addr: i64, nwords: i64) -> i64 {
585 if base_addr == 0 { return 0 }
586 let p: *i64 = base_addr as *i64
587 var i: i64 = 0
588 while i < nwords {
589 let w: i64 = p[i]
590 if w != 0 { let h: i64 = gc_is_start(w as i64); if h != 0 { gc_mark_header(h as i64) } }
591 i = i + 1
592 }
593 return 0
594}
595// drain the worklist: trace each marked block by scanning its payload words (conservative: the bitmap keeps it safe).
596func gc_trace_all() -> i64 {
597 while gc_work_n > 0 {
598 gc_work_n = gc_work_n - 1
599 let wl: *i64 = gc_work as *i64
600 let h: i64 = wl[gc_work_n]
601 let hp: *i64 = h as *i64 // hoist: (cast)[i] >> n desyncs the parser; a let-bound ptr is safe
602 let size: i64 = hp[0] >> 3
603 gc_scan_region(h + 8, size / 8)
604 }
605 return 0
606}
607// gc_mark_globals is defined LATER (after fproto_*/js_gthis/js_objproto/js_nsstat_tab/sh_* are declared): nx_cc
608// miscompiles a FORWARD reference to a module static (reads garbage, not the static's address), so any function
609// touching those statics MUST be positioned after their declarations.
610// SWEEP: walk every retained + current chunk linearly; free unmarked in-use blocks to the free-lists, clear marks.
611func gc_sweep_range(base: i64, used: i64) -> i64 {
612 var off: i64 = 0
613 let fl: *i64 = gc_freelist as *i64
614 while off < used {
615 let hp2: i64 = base + off
616 let meta: *i64 = hp2 as *i64
617 let m: i64 = meta[0]
618 let size: i64 = m >> 3
619 let total: i64 = size + 8
620 if (m & 2) == 0 { // not already free
621 if (m & 1) == 1 { meta[0] = (size << 3) } // live: clear mark
622 else { if gc_nosweep == 1 { meta[0] = (size << 3) } // DIAGNOSTIC: leave dead blocks in-use (no free)
623 else { // dead: reclaim
624 let cls: i64 = size / 8
625 if cls < GC_NCLASS { if cls > 0 {
626 let payload: i64 = hp2 + 8
627 let pl: *i64 = payload as *i64 // hoist cast: nx_cc rejects an inline (cast)[i] LVALUE
628 pl[0] = fl[cls] // free-list link stored in the dead block's first word
629 fl[cls] = payload
630 } }
631 meta[0] = (size << 3) | 2 // free=1
632 } }
633 }
634 if total <= 0 { off = used } else { off = off + total } // guard against a corrupt zero-size header
635 }
636 return 0
637}
638func gc_sweep_all() -> i64 {
639 let t: *i64 = gc_chunks as *i64
640 var c: i64 = 0
641 while c < gc_chunk_n { gc_sweep_range(t[c * GC_CST] as i64, t[c * GC_CST + 1] as i64); c = c + 1 }
642 if nx_pool != 0 { gc_sweep_range(nx_pool as i64, nx_pool_hp as i64) }
643 gc_collections = gc_collections + 1
644 gc_since = 0
645 return 0
646}
647func gc_enable(v: i64) -> i64 { gc_init(); gc_enabled = v; if gc_threshold == 0 { gc_threshold = GC_THRESHOLD_DEFAULT }; return 0 }
648func gc_configure(threshold: i64) -> i64 { gc_init(); gc_threshold = threshold; return 0 }
649func gc_set_nosweep(v: i64) -> i64 { gc_nosweep = v; return 0 } // DIAGNOSTIC toggle
650func gc_is_enabled() -> i64 { return gc_enabled }
651func gc_since_bytes() -> i64 { return gc_since }
652func gc_collection_count() -> i64 { return gc_collections }
653// loop-top poll: 1 = a collection is due (GC on + threshold crossed). Cheap: two loads + a compare.
654func gc_poll() -> i64 { if gc_enabled == 0 { return 0 } if gc_since >= gc_threshold { return 1 } return 0 }
655// absolute addresses of the two poll statics, so the JIT can bake an INLINE `gc_since >= gc_threshold` compare
656// at loop back-edges (no per-iteration call in the common no-collect case). Stable: statics never relocate.
657func gc_since_addr() -> i64 { return (&gc_since) as i64 }
658func gc_threshold_addr() -> i64 { return (&gc_threshold) as i64 }
659func nx_pool_total_mb() -> i64 { return nx_pool_total / 1048576 }
660func nx_pool_total_kb() -> i64 { return nx_pool_total / 1024 }
661func nx_pool_alloc_count() -> i64 { return nx_pool_count }
662func nx_pool_hp_bytes() -> i64 { return nx_pool_hp } // current-chunk bump offset = RESIDENT proxy (plateaus under GC)
663func nx_pool_hp_kb() -> i64 { return nx_pool_hp / 1024 }
664func gc_chunk_count() -> i64 { return gc_chunk_n } // # retired 16MB chunks (stays low if GC reuses freed blocks)
665func ev_str_new(len: i64) -> *i64 { let rec: *i64 = (nx_pool_alloc(16 + len)) as *i64; rec[0] = len; return rec }
666// ROPE / CONS-STRING (V8-style): `a + b` builds an O(1) CONS node instead of copying (str-build was O(n^2)
667// for the s=s+x loop = the 2311x-vs-V8 gap). A cons rec = [CONS_MARK, left, right, total_len, flat_cache,
668// leaf_count], told apart from a flat rec ([len>=0, bytes...]) by the NEGATIVE marker in slot 0. ev_str_bytes
669// FLATTENS on demand (iterative in-order, O(total_len) ONCE, cached in slot 4) so all ~70 call sites
670// transparently get flat bytes; ev_str_len returns the cached total (no flatten). [[project-nishi-bytecode-vm-2026-07-06]]
671const CONS_MARK: i64 = 0 - 700701
672func str_leaves(rec: *i64) -> i64 { if rec[0] == CONS_MARK { return rec[5] } return 1 }
673func str_flatten(rec: *i64) -> *i64 {
674 if rec[4] != 0 { return (rec[4]) as *i64 }
675 let flat: *i64 = ev_str_new(rec[3])
676 let dst: *u8 = (flat as i64 + 8) as *u8
677 let stack: *i64 = (nx_pool_alloc(8 * (rec[5] + 4))) as *i64 // stack depth <= leaf count (left-leaning worst case)
678 var sp: i64 = 0
679 stack[0] = rec as i64
680 sp = 1
681 var off: i64 = 0
682 while sp > 0 {
683 sp = sp - 1
684 let r: *i64 = (stack[sp]) as *i64
685 if r[0] == CONS_MARK {
686 stack[sp] = r[2]; sp = sp + 1 // push right, then left -> left processed first (in order)
687 stack[sp] = r[1]; sp = sp + 1
688 } else {
689 let n: i64 = r[0]
690 let src: *u8 = (r as i64 + 8) as *u8
691 var i: i64 = 0
692 while i < n { dst[off + i] = src[i]; i = i + 1 }
693 off = off + n
694 }
695 }
696 rec[4] = flat as i64
697 return flat
698}
699func ev_str_len(rec: *i64) -> i64 { if rec[0] == CONS_MARK { return rec[3] } return rec[0] }
700func ev_str_bytes(rec: *i64) -> *u8 { if rec[0] == CONS_MARK { let f: *i64 = str_flatten(rec); return (f as i64 + 8) as *u8 } return (rec as i64 + 8) as *u8 }
701
702// build a string VALUE from a STRING literal node (lexer keeps the quotes -> strip).
703// hex digit -> value (0-15), or -1.
704func ev_hexval(c: i64) -> i64 {
705 let x: i64 = c & 0xff
706 if x >= 48 { if x <= 57 { return x - 48 } }
707 if x >= 97 { if x <= 102 { return x - 87 } }
708 if x >= 65 { if x <= 70 { return x - 55 } }
709 return 0 - 1
710}
711// write code point cp (<= 0xFFFF) as UTF-8 at dst[at]; returns byte count (1-3). NAMED DIVERGENCE:
712// our strings are BYTE strings, so "ẜ".length == 3 (UTF-8 bytes) where V8 counts 1 UTF-16 unit;
713// equality/ordering/concat stay self-consistent, which is what algorithms depend on.
714func ev_utf8_put(dst: *u8, at: i64, cp: i64) -> i64 {
715 if cp < 128 { dst[at] = cp as u8; return 1 }
716 if cp < 2048 {
717 dst[at] = (192 + (cp / 64)) as u8
718 dst[at + 1] = (128 + (cp % 64)) as u8
719 return 2
720 }
721 dst[at] = (224 + (cp / 4096)) as u8
722 dst[at + 1] = (128 + ((cp / 64) % 64)) as u8
723 dst[at + 2] = (128 + (cp % 64)) as u8
724 return 3
725}
726// string literal -> value record with JS ESCAPE DECODING (was a raw byte copy -- "A" compared
727// UNEQUAL to "A" and "\n" was 2 chars; scheme2js/Octane bundles use \uXXXX symbol prefixes heavily).
728// Handles \n \t \r \b \f \v \0, \xHH, \uXXXX (UTF-8 encoded), line continuation \<LF>, and the spec
729// identity fallback (\" \' \\ \/ + any unknown escape -> the char itself). Decoded length is always
730// <= raw length, so the raw-sized allocation is safe; the record length is set to the decoded size.
731func ev_str_from_lit(ctx: *i64, idx: i64, out: *i64) -> i64 {
732 let src: *u8 = jp_src(ctx)
733 let s: i64 = ev_tok_start(ctx, idx)
734 let l: i64 = ev_tok_len(ctx, idx)
735 var inner: i64 = l - 2
736 if inner < 0 { inner = 0 }
737 let rec: *i64 = ev_str_new(inner)
738 let dst: *u8 = ev_str_bytes(rec)
739 var di: i64 = 0
740 var i: i64 = 0
741 while i < inner {
742 let ch: i64 = (src[s + 1 + i]) & 0xff
743 if ch != 92 {
744 dst[di] = ch as u8
745 di = di + 1
746 i = i + 1
747 } else {
748 if i + 1 >= inner { // trailing lone backslash: keep it
749 dst[di] = 92 as u8
750 di = di + 1
751 i = i + 1
752 } else {
753 let e: i64 = (src[s + 1 + i + 1]) & 0xff
754 var adv: i64 = 2
755 var outc: i64 = 0 - 1 // -1 = identity(e); -2 = emit nothing; -3 = already emitted
756 if e == 110 { outc = 10 } // \n
757 if e == 116 { outc = 9 } // \t
758 if e == 114 { outc = 13 } // \r
759 if e == 98 { outc = 8 } // \b
760 if e == 102 { outc = 12 } // \f
761 if e == 118 { outc = 11 } // \v
762 if e == 48 { outc = 0 } // \0
763 if e == 10 { outc = 0 - 2 } // line continuation: swallow
764 if e == 13 { outc = 0 - 2; if i + 2 < inner { if ((src[s + 1 + i + 2]) & 0xff) == 10 { adv = 3 } } } // \<CR>[LF]
765 if e == 120 { // \xHH -> a SINGLE byte 0x00-0xFF (V8 length==1; keeps
766 if i + 3 < inner { // string `\xNN` byte-consistent with regex `\xNN`).
767 let h1: i64 = ev_hexval(src[s + 1 + i + 2])
768 let h2: i64 = ev_hexval(src[s + 1 + i + 3])
769 if h1 >= 0 { if h2 >= 0 {
770 dst[di] = (h1 * 16 + h2) as u8
771 di = di + 1
772 adv = 4
773 outc = 0 - 3
774 } }
775 }
776 }
777 if e == 117 { // \uHHHH -> a SINGLE byte (cp & 0xff), consistent with \xNN
778 if i + 5 < inner { // + String.fromCharCode: every char = 1 byte, so `.length`
779 let u1: i64 = ev_hexval(src[s + 1 + i + 2]) // counts 1 per \u = V8's UTF-16 code-unit
780 let u2: i64 = ev_hexval(src[s + 1 + i + 3]) // count for the BMP (EXACT incl charCodeAt
781 let u3: i64 = ev_hexval(src[s + 1 + i + 4]) // for cp<=0xFF; low-byte for cp>=0x100 which
782 let u4: i64 = ev_hexval(src[s + 1 + i + 5]) // a byte engine cannot hold as one char).
783 if u1 >= 0 { if u2 >= 0 { if u3 >= 0 { if u4 >= 0 {
784 let ucp: i64 = ((u1 * 16 + u2) * 16 + u3) * 16 + u4
785 dst[di] = (ucp & 0xff) as u8
786 di = di + 1
787 adv = 6
788 outc = 0 - 3
789 } } } }
790 }
791 }
792 if outc >= 0 { dst[di] = outc as u8; di = di + 1 }
793 if outc == (0 - 1) { dst[di] = e as u8; di = di + 1 }
794 i = i + adv
795 }
796 }
797 }
798 rec[0] = di
799 ev_set(out, VAL_STR, rec as i64)
800 return 0
801}
802func ev_str_eq(a: *i64, b: *i64) -> i64 {
803 let la: i64 = ev_str_len(a)
804 if la != ev_str_len(b) { return 0 }
805 let sa: *u8 = ev_str_bytes(a)
806 let sb: *u8 = ev_str_bytes(b)
807 var i: i64 = 0
808 while i < la { if (sa[i] & 0xff) != (sb[i] & 0xff) { return 0 } i = i + 1 }
809 return 1
810}
811// byte-lexicographic compare of two string records: -1 if a<b, 0 if a==b, 1 if a>b.
812// Matches JS relational string ordering (which compares UTF-16 code units; for the
813// ASCII/byte corpus this rung handles, byte order == code-unit order). Shorter string
814// is "less" when it is a prefix of the longer (real JS: "ab" < "abc").
815func ev_str_cmp(a: *i64, b: *i64) -> i64 {
816 let la: i64 = ev_str_len(a)
817 let lb: i64 = ev_str_len(b)
818 let sa: *u8 = ev_str_bytes(a)
819 let sb: *u8 = ev_str_bytes(b)
820 var n: i64 = la
821 if lb < n { n = lb }
822 var i: i64 = 0
823 while i < n {
824 let ca: i64 = sa[i] & 0xff
825 let cb: i64 = sb[i] & 0xff
826 if ca < cb { return 0 - 1 }
827 if ca > cb { return 1 }
828 i = i + 1
829 }
830 if la < lb { return 0 - 1 }
831 if la > lb { return 1 }
832 return 0
833}
834// strict NUMERIC-STRING parse for MIXED relational coercion (rung-2b ladder, integer
835// subset): a string record that is a pure (optionally one leading '-') run of ASCII
836// digits parses to its integer value -- written to *vout -- and returns 1. ANY other
837// shape (empty, non-digit byte, embedded sign, float/exponent) returns 0 = NOT a clean
838// integer string. We deliberately do NOT coerce a non-numeric string to 0 (that was the
839// silent-wrong bug KAT12 eliminated); the caller errors instead. Real-JS leading/trailing
840// whitespace + float/NaN coercion needs the full float ladder and stays HONEST OPEN.
841func ev_str_numval(rec: *i64, vout: *i64) -> i64 {
842 let l: i64 = ev_str_len(rec)
843 if l == 0 { return 0 }
844 let by: *u8 = ev_str_bytes(rec)
845 var i: i64 = 0
846 var neg: i64 = 0
847 if (by[0] & 0xff) == 45 { neg = 1; i = 1 } // a single leading '-'
848 if i >= l { return 0 } // "-" alone is not a number
849 var n: i64 = 0
850 while i < l {
851 let c: i64 = by[i] & 0xff
852 if c < 48 { return 0 }
853 if c > 57 { return 0 }
854 n = n * 10 + (c - 48)
855 i = i + 1
856 }
857 if neg == 1 { n = 0 - n }
858 vout[0] = n
859 return 1
860}
861// true iff `op` is a relational operator (< <= > >=) -- used to scope the MIXED
862// string<->number coercion fix to ordering comparisons only (NOT arithmetic).
863func ev_is_relop(op: i64) -> i64 {
864 if op == OP_LT { return 1 }
865 if op == OP_LE { return 1 }
866 if op == OP_GT { return 1 }
867 if op == OP_GE { return 1 }
868 return 0
869}
870// a value is "cleanly numeric" for relational coercion iff it is a number/bool/null
871// (each has an unambiguous integer ToNumber this rung). Strings/objects/arrays/undefined
872// are NOT (string needs ev_str_numval; the rest are NaN/open).
873func ev_is_clean_num(v: *i64) -> i64 {
874 if v[0] == VAL_NUM { return 1 }
875 if v[0] == VAL_BOOL { return 1 }
876 if v[0] == VAL_NULL { return 1 }
877 return 0
878}
879// true iff EXACTLY ONE of (lb,rb) is a string and the OTHER is a clean numeric value --
880// the mixed string<->number relational shape we coerce (e.g. '10' < 5, 6 > '5').
881func ev_one_str_one_num(lb: *i64, rb: *i64) -> i64 {
882 if lb[0] == VAL_STR { if ev_is_clean_num(rb) == 1 { return 1 } }
883 if rb[0] == VAL_STR { if ev_is_clean_num(lb) == 1 { return 1 } }
884 return 0
885}
886// coerce one operand of a mixed relational to its integer value into *vout. A clean
887// numeric uses ev_tonum; a STRING must parse as a clean integer (ev_str_numval) else we
888// return 0 = cannot coerce (caller errors -- never silent 0). Returns 1 on success.
889func ev_mixed_relnum(v: *i64, vout: *i64) -> i64 {
890 if v[0] == VAL_STR { return ev_str_numval((v[1]) as *i64, vout) }
891 if ev_is_clean_num(v) == 1 { vout[0] = ev_tonum(v); return 1 }
892 return 0
893}
894func ev_str_concat(a: *i64, b: *i64, out: *i64) -> i64 {
895 // ROPE: O(1) cons node instead of copying both operands (was O(n^2) for s=s+x loops). Flattened lazily
896 // by ev_str_bytes when the bytes are actually needed.
897 let c: *i64 = (nx_pool_alloc(8 * 6)) as *i64
898 c[0] = CONS_MARK
899 c[1] = a as i64
900 c[2] = b as i64
901 c[3] = ev_str_len(a) + ev_str_len(b)
902 c[4] = 0
903 c[5] = str_leaves(a) + str_leaves(b)
904 ev_set(out, VAL_STR, c as i64)
905 return 0
906}
907// string record from a NUL-terminated C-string (for typeof + coercions).
908func ev_cstr(s: *u8) -> *i64 {
909 var n: i64 = 0
910 while s[n] != (0 as u8) { n = n + 1 }
911 let rec: *i64 = ev_str_new(n)
912 let dst: *u8 = ev_str_bytes(rec)
913 var i: i64 = 0
914 while i < n { dst[i] = s[i]; i = i + 1 }
915 return rec
916}
917// number -> decimal-digit string record (for '+' concat coercion). The digit scratch is a REUSED static
918// (built once) -- it was sys_mmap(32) [a SYSCALL] per call, making `''+n` coercion ~115x slower than V8
919// (num->str is everywhere in real JS). Non-reentrant (ev_str_new/ev_str_bytes never recurse into here).
920static ev_n2s_tmp: i64
921func ev_num_to_str(n: i64) -> *i64 {
922 var m: i64 = n
923 var neg: i64 = 0
924 if m < 0 { neg = 1; m = 0 - m }
925 if ev_n2s_tmp == 0 { ev_n2s_tmp = (sys_mmap(32)) as i64 }
926 let tmp: *u8 = ev_n2s_tmp as *u8
927 var k: i64 = 0
928 if m == 0 { tmp[0] = 48 as u8; k = 1 }
929 while m > 0 { tmp[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
930 var total: i64 = k
931 if neg == 1 { total = total + 1 }
932 let rec: *i64 = ev_str_new(total)
933 let dst: *u8 = ev_str_bytes(rec)
934 var off: i64 = 0
935 if neg == 1 { dst[0] = 45 as u8; off = 1 }
936 var i: i64 = 0
937 while i < k { dst[off + i] = tmp[k - 1 - i]; i = i + 1 }
938 return rec
939}
940// coerce any value to a string record (for '+' with a string operand).
941func ev_coerce_str(v: *i64) -> *i64 {
942 let t: i64 = v[0]
943 if t == VAL_STR { return (v[1]) as *i64 }
944 if t == VAL_NUM { return ev_num_to_str(v[1]) }
945 if t == VAL_FLOAT { return ev_f64_str(v[1]) }
946 if t == VAL_BOOL { if v[1] == 1 { return ev_cstr("true\x00" as *u8) } return ev_cstr("false\x00" as *u8) }
947 if t == VAL_NULL { return ev_cstr("null\x00" as *u8) }
948 if t == VAL_FUNC { return ev_cstr("function\x00" as *u8) }
949 if t == VAL_OBJECT { return ev_cstr("[object Object]\x00" as *u8) } // real JS Object.prototype.toString
950 if t == VAL_ARRAY { return ev_cstr("[object Array]\x00" as *u8) } // (Array join is R-JS-RUNTIME; placeholder, never "undefined")
951 if t == VAL_NATIVE { return ev_cstr("function\x00" as *u8) } // a native builtin stringifies as a function
952 if t == VAL_GLOBALNS { return ev_cstr("[object Object]\x00" as *u8) } // Math/Object/console are objects
953 return ev_cstr("undefined\x00" as *u8)
954}
955// build a STRING RECORD from an IDENT/PROP-KEY token's bytes (a property name like `x`).
956// Used for MEMBER access keys: o.x -> key "x". The token is NOT quoted (it is an ident).
957func ev_key_from_ident(ctx: *i64, idx: i64) -> *i64 {
958 let src: *u8 = jp_src(ctx)
959 let s: i64 = ev_tok_start(ctx, idx)
960 let l: i64 = ev_tok_len(ctx, idx)
961 let rec: *i64 = ev_str_new(l)
962 let dst: *u8 = ev_str_bytes(rec)
963 var i: i64 = 0
964 while i < l { dst[i] = src[s + i]; i = i + 1 }
965 return rec
966}
967// build a STRING RECORD key from a STRING-literal token (quotes stripped), via the IDENT
968// raw form when the prop-key token is a STRING (object literal {'a':1}). Reuses the
969// quote-stripping of ev_str_from_lit by going through a temp value.
970func ev_key_from_strtok(ctx: *i64, idx: i64) -> *i64 {
971 let tmp: *i64 = ev_cell()
972 ev_str_from_lit(ctx, idx, tmp)
973 return (tmp[1]) as *i64
974}
975// PROP-key string record for a node whose tokidx names the key: STRING token -> strip
976// quotes, IDENT token -> raw bytes. Used by ND_PROP (object literal) and ND_MEMBER (o.x,
977// where the property child is an IDENT node).
978func ev_prop_key(ctx: *i64, idx: i64) -> *i64 {
979 if ev_tok_kind(ctx, idx) == JS_TOK_STRING { return ev_key_from_strtok(ctx, idx) }
980 return ev_key_from_ident(ctx, idx)
981}
982// typeof tag -> a string record value (real-JS strings; typeof null === "object").
983func ev_typeof_str(tag: i64) -> *i64 {
984 if tag == VAL_NUM { return ev_cstr("number\x00" as *u8) }
985 if tag == VAL_FLOAT { return ev_cstr("number\x00" as *u8) } // typeof 0.5 === "number"
986 if tag == VAL_BOOL { return ev_cstr("boolean\x00" as *u8) }
987 if tag == VAL_STR { return ev_cstr("string\x00" as *u8) }
988 if tag == VAL_FUNC { return ev_cstr("function\x00" as *u8) }
989 if tag == VAL_NULL { return ev_cstr("object\x00" as *u8) }
990 if tag == VAL_OBJECT { return ev_cstr("object\x00" as *u8) } // typeof {} === "object"
991 if tag == VAL_ARRAY { return ev_cstr("object\x00" as *u8) } // typeof [] === "object" (JS)
992 if tag == VAL_NATIVE { return ev_cstr("function\x00" as *u8) } // typeof Math.max === "function"
993 if tag == VAL_GLOBALNS { return ev_cstr("object\x00" as *u8) } // typeof Math === "object"
994 return ev_cstr("undefined\x00" as *u8)
995}
996
997func ev_truthy(v: *i64) -> i64 {
998 let t: i64 = v[0]
999 if t == VAL_UNDEF { return 0 }
1000 if t == VAL_NULL { return 0 }
1001 if t == VAL_BOOL { return v[1] }
1002 if t == VAL_NUM { if v[1] == 0 { return 0 } return 1 }
1003 if t == VAL_FLOAT { if nx_f64_is_nan(v[1]) == 1 { return 0 } if nx_f64_is_zero(v[1]) == 1 { return 0 } return 1 }
1004 if t == VAL_STR { let rec: *i64 = (v[1]) as *i64; if ev_str_len(rec) == 0 { return 0 } return 1 }
1005 return 1
1006}
1007func ev_tonum(v: *i64) -> i64 {
1008 let t: i64 = v[0]
1009 if t == VAL_NUM { return v[1] }
1010 if t == VAL_FLOAT { return jf2i(v[1]) } // truncate toward zero (ToInt32/array-index coercion)
1011 if t == VAL_BOOL { return v[1] }
1012 if t == VAL_NULL { return 0 }
1013 return 0
1014}
1015func ev_is_numlike(v: *i64) -> i64 { if v[0] == VAL_NUM { return 1 } if v[0] == VAL_FLOAT { return 1 } return 0 }
1016func ev_strict_eq(l: *i64, r: *i64) -> i64 {
1017 // both numeric (int or float) -> NUMERIC equality (1.0 === 1 is true; JS has one number type).
1018 if ev_is_numlike(l) == 1 { if ev_is_numlike(r) == 1 {
1019 if l[0] == VAL_FLOAT { return nx_f64_eq(ev_tof64(l), ev_tof64(r)) }
1020 if r[0] == VAL_FLOAT { return nx_f64_eq(ev_tof64(l), ev_tof64(r)) }
1021 if l[1] == r[1] { return 1 } // both integers -> exact (no f64 precision loss)
1022 return 0
1023 } }
1024 if l[0] != r[0] { return 0 }
1025 if l[0] == VAL_NULL { return 1 }
1026 if l[0] == VAL_UNDEF { return 1 }
1027 if l[0] == VAL_STR { return ev_str_eq((l[1]) as *i64, (r[1]) as *i64) }
1028 if l[1] == r[1] { return 1 }
1029 return 0
1030}
1031func ev_loose_eq(l: *i64, r: *i64) -> i64 {
1032 // ECMAScript Abstract Equality (council fix): null/undefined loosely-equal ONLY
1033 // each other -- NEVER a number/bool/string. If EITHER side is null/undefined the
1034 // result is true IFF BOTH are; NO numeric coercion (was the null==0 -> true bug).
1035 var lnu: i64 = 0
1036 if l[0] == VAL_NULL { lnu = 1 }
1037 if l[0] == VAL_UNDEF { lnu = 1 }
1038 var rnu: i64 = 0
1039 if r[0] == VAL_NULL { rnu = 1 }
1040 if r[0] == VAL_UNDEF { rnu = 1 }
1041 if lnu == 1 { if rnu == 1 { return 1 } return 0 }
1042 if rnu == 1 { return 0 }
1043 if l[0] == VAL_STR { if r[0] == VAL_STR { return ev_str_eq((l[1]) as *i64, (r[1]) as *i64) } }
1044 if l[0] == VAL_STR { return 0 } // str<->number coercion = rung-2b open
1045 if r[0] == VAL_STR { return 0 }
1046 // REFERENCE types (object/array/function/regex/...) compare by IDENTITY (payload ptr). Was falling
1047 // through to ev_tonum (0 for every object) -> ANY two objects compared == (bit DeltaBlue's solver,
1048 // whose logic lives on identity: out.determinedBy == c). Ref-vs-number ToPrimitive = named open (0).
1049 var lrf: i64 = 0
1050 if l[0] == VAL_OBJECT { lrf = 1 }
1051 if l[0] == VAL_ARRAY { lrf = 1 }
1052 if l[0] == VAL_FUNC { lrf = 1 }
1053 if l[0] == VAL_NATIVE { lrf = 1 }
1054 if l[0] == VAL_GLOBALNS { lrf = 1 }
1055 if l[0] == VAL_REGEX { lrf = 1 }
1056 if l[0] == VAL_PROMISE { lrf = 1 }
1057 if l[0] == VAL_RESPONSE { lrf = 1 }
1058 var rrf: i64 = 0
1059 if r[0] == VAL_OBJECT { rrf = 1 }
1060 if r[0] == VAL_ARRAY { rrf = 1 }
1061 if r[0] == VAL_FUNC { rrf = 1 }
1062 if r[0] == VAL_NATIVE { rrf = 1 }
1063 if r[0] == VAL_GLOBALNS { rrf = 1 }
1064 if r[0] == VAL_REGEX { rrf = 1 }
1065 if r[0] == VAL_PROMISE { rrf = 1 }
1066 if r[0] == VAL_RESPONSE { rrf = 1 }
1067 if lrf == 1 { if rrf == 1 { if l[0] == r[0] { if l[1] == r[1] { return 1 } } return 0 } return 0 }
1068 if rrf == 1 { return 0 }
1069 if l[0] == VAL_FLOAT { return nx_f64_eq(ev_tof64(l), ev_tof64(r)) } // float == : numeric (not int-trunc)
1070 if r[0] == VAL_FLOAT { return nx_f64_eq(ev_tof64(l), ev_tof64(r)) }
1071 if ev_tonum(l) == ev_tonum(r) { return 1 }
1072 return 0
1073}
1074
1075// apply an ARITHMETIC binary op (OP_ADD/SUB/MUL/DIV/MOD) to two already-evaluated
1076// VALUES, writing the result to out. Returns 0 ok, 1 ERROR (div/mod by zero -- no float
1077// Infinity this rung). '+' does STRING CONCAT when EITHER operand is a string (coercing
1078// the other), exactly mirroring the ND_BINARY '+' path -- so `s += 5` and `s = s + 5`
1079// agree. Used by COMPOUND ASSIGNMENT (a += b folds a and b through this).
1080// ---- f64 JS numbers (R-JS-F64-2) ----
1081// a NUMBER literal node -> VAL_FLOAT iff its token contains a '.', else VAL_NUM (integer). Keeps the
1082// integer fast-path EXACTLY as before (2+3 stays integer 5); 0.5 becomes a float.
1083func ev_hexdigit(c: i64) -> i64 {
1084 if c >= 48 { if c <= 57 { return c - 48 } } // 0-9
1085 if c >= 97 { if c <= 102 { return c - 87 } } // a-f
1086 if c >= 65 { if c <= 70 { return c - 55 } } // A-F
1087 return 0 - 1
1088}
1089func ev_parse_radix(src: *u8, s: i64, l: i64, base: i64) -> i64 {
1090 var v: i64 = 0
1091 var i: i64 = 0
1092 var go: i64 = 1
1093 while go == 1 {
1094 if i >= l { go = 0 } else {
1095 let d: i64 = ev_hexdigit(src[s + i] & 0xff)
1096 if d < 0 { go = 0 } else { if d >= base { go = 0 } else { v = v * base + d; i = i + 1 } }
1097 }
1098 }
1099 return v
1100}
1101func ev_num_node(ctx: *i64, idx: i64, out: *i64) -> i64 {
1102 let src: *u8 = jp_src(ctx)
1103 let s: i64 = ev_tok_start(ctx, idx)
1104 let l: i64 = ev_tok_len(ctx, idx)
1105 // RADIX prefixes 0x/0o/0b (real JS) -- must precede the decimal/'.' path.
1106 if l >= 2 { if (src[s] & 0xff) == 48 {
1107 let c1: i64 = src[s + 1] & 0xff
1108 if c1 == 120 { ev_set(out, VAL_NUM, ev_parse_radix(src, s + 2, l - 2, 16)); return 0 }
1109 if c1 == 88 { ev_set(out, VAL_NUM, ev_parse_radix(src, s + 2, l - 2, 16)); return 0 }
1110 if c1 == 111 { ev_set(out, VAL_NUM, ev_parse_radix(src, s + 2, l - 2, 8)); return 0 }
1111 if c1 == 79 { ev_set(out, VAL_NUM, ev_parse_radix(src, s + 2, l - 2, 8)); return 0 }
1112 if c1 == 98 { ev_set(out, VAL_NUM, ev_parse_radix(src, s + 2, l - 2, 2)); return 0 }
1113 if c1 == 66 { ev_set(out, VAL_NUM, ev_parse_radix(src, s + 2, l - 2, 2)); return 0 }
1114 } }
1115 var isf: i64 = 0
1116 var i: i64 = 0
1117 while i < l { if (src[s + i] & 0xff) == 46 { isf = 1 } i = i + 1 } // '.' -> float
1118 if isf == 1 { ev_set(out, VAL_FLOAT, jparse_f64(((src as i64) + s) as *u8, l)); return 0 }
1119 ev_set(out, VAL_NUM, ev_atoi(src, s, l)); return 0
1120}
1121// coerce a value to f64 bits (VAL_FLOAT -> raw; VAL_NUM/other -> integer-as-f64).
1122func ev_tof64(v: *i64) -> i64 {
1123 if v[0] == VAL_FLOAT { return v[1] }
1124 return ji2f(ev_tonum(v))
1125}
1126// f64 bits -> a JS string record (composes jf64_to_str).
1127static ev_f2s_tmp: i64 // reused float->string scratch (was sys_mmap(40) per call -- same syscall bug)
1128func ev_f64_str(bits: i64) -> *i64 {
1129 if ev_f2s_tmp == 0 { ev_f2s_tmp = (sys_mmap(40)) as i64 }
1130 let buf: *u8 = ev_f2s_tmp as *u8
1131 let n: i64 = jf64_to_str(bits, buf)
1132 let rec: *i64 = ev_str_new(n)
1133 let d: *u8 = ev_str_bytes(rec)
1134 var i: i64 = 0
1135 while i < n { d[i] = buf[i]; i = i + 1 }
1136 return rec
1137}
1138// f64 arithmetic for the float-promotion path. JS x/0 = Infinity (nx_f64_div handles); % = trunc-fmod.
1139func ev_binop_f64(op: i64, fa: i64, fb: i64, out: *i64) -> i64 {
1140 if op == OP_ADD { ev_set(out, VAL_FLOAT, nx_f64_add(fa, fb)); return 0 }
1141 if op == OP_SUB { ev_set(out, VAL_FLOAT, nx_f64_add(fa, nx_f64_neg(fb))); return 0 }
1142 if op == OP_MUL { ev_set(out, VAL_FLOAT, nx_f64_mul(fa, fb)); return 0 }
1143 if op == OP_DIV { ev_set(out, VAL_FLOAT, nx_f64_div(fa, fb)); return 0 }
1144 if op == OP_MOD {
1145 let q: i64 = ji2f(jf2i(nx_f64_div(fa, fb))) // trunc(a/b)
1146 ev_set(out, VAL_FLOAT, nx_f64_add(fa, nx_f64_neg(nx_f64_mul(q, fb)))); return 0
1147 }
1148 // relational (R-JS-F64-3): float ordering via the gated nx_f64 comparisons -> VAL_BOOL.
1149 if op == OP_LT { ev_set(out, VAL_BOOL, nx_f64_lt(fa, fb)); return 0 }
1150 if op == OP_GT { ev_set(out, VAL_BOOL, nx_f64_gt(fa, fb)); return 0 }
1151 if op == OP_LE { if nx_f64_lt(fa, fb) == 1 { ev_set(out, VAL_BOOL, 1); return 0 } ev_set(out, VAL_BOOL, nx_f64_eq(fa, fb)); return 0 }
1152 if op == OP_GE { if nx_f64_gt(fa, fb) == 1 { ev_set(out, VAL_BOOL, 1); return 0 } ev_set(out, VAL_BOOL, nx_f64_eq(fa, fb)); return 0 }
1153 ev_set(out, VAL_UNDEF, 0); return 1
1154}
1155// P8 (real-JS IEEE-double number model): integer DIV/MOD no longer int-truncates or errors. An EXACT
1156// quotient stays a fast VAL_NUM (6/2 -> 3, JS value-equal); a NON-EXACT or /0 result promotes to a
1157// VAL_FLOAT via the soft-IEEE divider (7/2 -> 3.5, 1/0 -> Infinity, -1/0 -> -Infinity, 0/0 -> NaN,
1158// a%0 -> NaN). This is V8's Smi-with-double-fallback: ints for speed, doubles when the result needs one.
1159func ev_num_div(a: i64, b: i64, out: *i64) -> i64 {
1160 if b == 0 { return ev_binop_f64(OP_DIV, ji2f(a), ji2f(b), out) }
1161 let q: i64 = a / b
1162 if q * b == a { ev_set(out, VAL_NUM, q); return 0 }
1163 return ev_binop_f64(OP_DIV, ji2f(a), ji2f(b), out)
1164}
1165func ev_num_mod(a: i64, b: i64, out: *i64) -> i64 {
1166 if b == 0 { ev_set(out, VAL_FLOAT, nx_f64_div(0, 0)); return 0 } // a % 0 = NaN (0.0/0.0 bits)
1167 ev_set(out, VAL_NUM, a - (a / b) * b)
1168 return 0
1169}
1170
1171// JS ToInt32: take the low 32 bits and interpret as a signed 32-bit integer (bitwise ops operate on these).
1172func js_toint32(v: i64) -> i64 {
1173 let m: i64 = v & 0xffffffff
1174 if m >= 2147483648 { return m - 4294967296 }
1175 return m
1176}
1177// `a instanceof B` -> true iff B.prototype appears on a's [[Prototype]] chain. B must be callable
1178// (VAL_FUNC/VAL_NATIVE); a non-object left operand is simply false (real JS). Shared by both engines.
1179func ev_instanceof(lb: *i64, rb: *i64, out: *i64) -> i64 {
1180 if rb[0] != VAL_FUNC { if rb[0] != VAL_NATIVE { ev_set(out, VAL_UNDEF, 0); return 1 } } // RHS not callable -> TypeError
1181 if lb[0] != VAL_OBJECT { ev_set(out, VAL_BOOL, 0); return 0 } // primitive LHS -> false
1182 let target: i64 = func_prototype(rb[1])
1183 var o: *i64 = (lb[1]) as *i64
1184 var guard: i64 = 0
1185 while guard < 200 {
1186 let p: i64 = obj_proto(o)
1187 if p == 0 { ev_set(out, VAL_BOOL, 0); return 0 }
1188 if p == target { ev_set(out, VAL_BOOL, 1); return 0 }
1189 o = p as *i64
1190 guard = guard + 1
1191 }
1192 ev_set(out, VAL_BOOL, 0); return 0
1193}
1194// `k in obj` -> true if obj has property named ToString(k), OWN or INHERITED. Objects: obj_get + proto chain.
1195// Arrays: a numeric index within bounds (methods/length = follow-on; scheme2js uses the object form). key from
1196// ev_key_of_val (defined later; forward function call is fine).
1197func ev_in(lb: *i64, rb: *i64, out: *i64) -> i64 {
1198 if rb[0] == VAL_OBJECT {
1199 let key: *i64 = ev_key_of_val(lb)
1200 let tmp: *i64 = ev_cell()
1201 let o: *i64 = (rb[1]) as *i64
1202 if obj_get(o, key, tmp) == 1 { ev_set(out, VAL_BOOL, 1); return 0 }
1203 if obj_proto_lookup(o, key, tmp) == 1 { ev_set(out, VAL_BOOL, 1); return 0 }
1204 ev_set(out, VAL_BOOL, 0); return 0
1205 }
1206 if rb[0] == VAL_ARRAY {
1207 let a: *i64 = (rb[1]) as *i64
1208 // `"length" in arr` === true (arrays own a length property -- jQuery's isArrayLike hinges on this;
1209 // without it every array misclassifies as a plain object -> ce.each takes the for-in branch and the
1210 // class2type population throws). A numeric key in bounds is also present. Anything else -> false.
1211 let key: *i64 = ev_key_of_val(lb)
1212 if ev_is_length_key(key) == 1 { ev_set(out, VAL_BOOL, 1); return 0 }
1213 let ki: i64 = ev_str_to_index(key)
1214 if ki >= 0 { if ki < arr_len(a) { ev_set(out, VAL_BOOL, 1); return 0 } }
1215 ev_set(out, VAL_BOOL, 0); return 0
1216 }
1217 ev_set(out, VAL_BOOL, 0); return 0 // primitive RHS: real JS TypeErrors; lenient false (scheme2js never hits)
1218}
1219func ev_ws_byte(c: i64) -> i64 { if c == 32 { return 1 } if c == 9 { return 1 } if c == 10 { return 1 } if c == 13 { return 1 } return 0 }
1220// JS ToNumber(v) -> a numeric value cell (VAL_NUM int / VAL_FLOAT incl NaN). Strings parse
1221// [ws][sign]digits[.digits][ws] (exponent/hex/Infinity = follow-on): non-numeric -> NaN, ""/all-ws -> 0.
1222// bool->0/1, null->0, undefined/object -> NaN. Fixes `100/"5"`=20 (was "5"->0 -> 100/0 -> Infinity).
1223func ev_coerce_num(v: *i64, out: *i64) -> i64 {
1224 let t: i64 = v[0]
1225 if t == VAL_NUM { out[0] = VAL_NUM; out[1] = v[1]; return 0 }
1226 if t == VAL_FLOAT { out[0] = VAL_FLOAT; out[1] = v[1]; return 0 }
1227 if t == VAL_BOOL { out[0] = VAL_NUM; out[1] = v[1]; return 0 }
1228 if t == VAL_NULL { out[0] = VAL_NUM; out[1] = 0; return 0 }
1229 if t != VAL_STR { out[0] = VAL_FLOAT; out[1] = NX_F64_NAN_RAW; return 0 }
1230 let rec: *i64 = (v[1]) as *i64
1231 let l: i64 = ev_str_len(rec)
1232 let by: *u8 = ev_str_bytes(rec)
1233 var a: i64 = 0
1234 var go: i64 = 1
1235 while go == 1 { if a < l { if ev_ws_byte(by[a] & 0xff) == 1 { a = a + 1 } else { go = 0 } } else { go = 0 } }
1236 var b: i64 = l
1237 go = 1
1238 while go == 1 { if b > a { if ev_ws_byte(by[b - 1] & 0xff) == 1 { b = b - 1 } else { go = 0 } } else { go = 0 } }
1239 if b <= a { out[0] = VAL_NUM; out[1] = 0; return 0 }
1240 var j: i64 = a
1241 let c0: i64 = by[a] & 0xff
1242 if c0 == 43 { j = j + 1 }
1243 if c0 == 45 { j = j + 1 }
1244 var dots: i64 = 0
1245 var digits: i64 = 0
1246 var valid: i64 = 1
1247 while j < b {
1248 let c: i64 = by[j] & 0xff
1249 if c == 46 { dots = dots + 1 } else { if c >= 48 { if c <= 57 { digits = digits + 1 } else { valid = 0 } } else { valid = 0 } }
1250 j = j + 1
1251 }
1252 if valid == 0 { out[0] = VAL_FLOAT; out[1] = NX_F64_NAN_RAW; return 0 }
1253 if digits == 0 { out[0] = VAL_FLOAT; out[1] = NX_F64_NAN_RAW; return 0 }
1254 if dots > 1 { out[0] = VAL_FLOAT; out[1] = NX_F64_NAN_RAW; return 0 }
1255 if dots == 0 {
1256 var neg: i64 = 0
1257 var k: i64 = a
1258 if c0 == 45 { neg = 1; k = a + 1 }
1259 if c0 == 43 { k = a + 1 }
1260 var n: i64 = 0
1261 while k < b { n = n * 10 + ((by[k] & 0xff) - 48); k = k + 1 }
1262 if neg == 1 { n = 0 - n }
1263 out[0] = VAL_NUM; out[1] = n
1264 return 0
1265 }
1266 out[0] = VAL_FLOAT
1267 out[1] = jparse_f64(((by as i64) + a) as *u8, b - a)
1268 return 0
1269}
1270func ev_apply_binop(op: i64, lb: *i64, rb: *i64, out: *i64) -> i64 {
1271 if op == OP_INSTANCEOF { return ev_instanceof(lb, rb, out) }
1272 if op == OP_IN { return ev_in(lb, rb, out) }
1273 if op == OP_ADD {
1274 if lb[0] == VAL_STR { return ev_str_concat(ev_coerce_str(lb), ev_coerce_str(rb), out) }
1275 if rb[0] == VAL_STR { return ev_str_concat(ev_coerce_str(lb), ev_coerce_str(rb), out) }
1276 }
1277 // ToNumber coercion for the non-`+` numeric/bitwise ops: a string/bool/null operand becomes a number
1278 // (JS ToNumber). Without this `100 / "5"` did "5"->0 -> 100/0 -> Infinity, spinning RayTrace's pixel
1279 // loop into 4M+ allocations. `+` with a string already concatenated above, so it never reaches here.
1280 var lc: *i64 = lb
1281 var rc: *i64 = rb
1282 if lb[0] == VAL_STR { let lt: *i64 = ev_cell(); ev_coerce_num(lb, lt); lc = lt }
1283 if rb[0] == VAL_STR { let rt: *i64 = ev_cell(); ev_coerce_num(rb, rt); rc = rt }
1284 // BITWISE (&|^ << >> >>>) -- real JS ToInt32 both sides (never float-promote); result is a 32-bit int.
1285 if op == OP_BAND { ev_set(out, VAL_NUM, js_toint32(ev_tonum(lc)) & js_toint32(ev_tonum(rc))); return 0 }
1286 if op == OP_BOR { ev_set(out, VAL_NUM, js_toint32(ev_tonum(lc)) | js_toint32(ev_tonum(rc))); return 0 }
1287 if op == OP_BXOR { ev_set(out, VAL_NUM, js_toint32(ev_tonum(lc)) ^ js_toint32(ev_tonum(rc))); return 0 }
1288 if op == OP_SHL { let sa: i64 = js_toint32(ev_tonum(lc)); let sb: i64 = ev_tonum(rc) & 31; ev_set(out, VAL_NUM, js_toint32(sa << sb)); return 0 }
1289 if op == OP_SHR { let sa: i64 = js_toint32(ev_tonum(lc)); let sb: i64 = ev_tonum(rc) & 31; ev_set(out, VAL_NUM, sa >> sb); return 0 }
1290 if op == OP_USHR { let ua: i64 = ev_tonum(lc) & 0xffffffff; let sb: i64 = ev_tonum(rc) & 31; ev_set(out, VAL_NUM, ua >> sb); return 0 }
1291 // FLOAT promotion: if either operand is a float, do f64 arithmetic (result VAL_FLOAT).
1292 if lc[0] == VAL_FLOAT { return ev_binop_f64(op, ev_tof64(lc), ev_tof64(rc), out) }
1293 if rc[0] == VAL_FLOAT { return ev_binop_f64(op, ev_tof64(lc), ev_tof64(rc), out) }
1294 let a: i64 = ev_tonum(lc)
1295 let b: i64 = ev_tonum(rc)
1296 if op == OP_ADD { ev_set(out, VAL_NUM, a + b); return 0 }
1297 if op == OP_SUB { ev_set(out, VAL_NUM, a - b); return 0 }
1298 if op == OP_MUL { ev_set(out, VAL_NUM, a * b); return 0 }
1299 if op == OP_DIV { return ev_num_div(a, b, out) }
1300 if op == OP_MOD { return ev_num_mod(a, b, out) }
1301 ev_set(out, VAL_UNDEF, 0); return 1
1302}
1303
1304// ===================== heap object model (R-JS-OBJ, rung 3) =====================
1305// OBJECT record (VAL_OBJECT payload = ptr as i64):
1306// [0]=count, then count props at OBJ_HDR + i*OBJ_ENT = [key-strrec(*i64 as i64), vt, vp].
1307// Keys are STRING RECORDS (ev_str_*), so any property name -- ident OR computed string
1308// key -- is a real comparable byte string. Linear scan (objects are small in practice;
1309// a hash table is a later perf rung, NOT a correctness gap).
1310// ARRAY record (VAL_ARRAY payload = ptr as i64):
1311// [0]=length, then ARR_HDR + i*ARR_ENT = [vt, vp] per element cell up to ARR_CAP.
1312// GROWABLE OBJECT: STABLE 5-word header [0]=count [1]=SHAPE id [2]=[[Prototype]] [3]=capacity [4]=backing-ptr.
1313// The header ptr is the VAL_OBJECT payload and never moves; props live in the backing block at
1314// backing[i*OBJ_ENT + 0/1/2] = key,vt,vp, which DOUBLES on demand. Was a fixed OBJ_CAP=256 inline block =
1315// ~6KB PER OBJECT (used ~48B) -> obj_new page-faults dominated churn (measured 2/3 of cost). Now ~small.
1316const OBJ_PROTO: i64 = 2 // [[Prototype]] link (set by `new`; walked on property miss)
1317const OBJ_CAPF: i64 = 3 // backing capacity (elements)
1318const OBJ_BACK: i64 = 4 // backing-block ptr (i64)
1319const OBJ_HDRW: i64 = 5 // header word count
1320const OBJ_ENT: i64 = 3
1321const OBJ_INITCAP: i64 = 4 // most objects have <=4 props (Vector{x,y,z}, Node{key,value,left,right}); 8 was
1322 // 2-4x oversized -> ~40% less object memory (Splay 249MB->~150MB). Grows if >4.
1323const OBJ_MAXCAP: i64 = 1048576 // 1M props -- runaway guard, not a real-workload limit
1324const ARR_HDR: i64 = 1
1325const ARR_ENT: i64 = 2
1326const ARR_CAP: i64 = 4096
1327
1328// ===================== SHAPES / hidden classes (P3, SOTA ladder) =====================
1329// A SHAPE is the interned identity of an object's property LAYOUT (V8 hidden class / map). Because our
1330// objects are ADDITIVE-ONLY and slots are insertion-ordered, an object's layout is fully determined by
1331// the ORDERED sequence of keys it has added -- so a shape = a node in a TRANSITION TREE: shape 0 =
1332// EMPTY; adding key K to shape S transitions to a child shape (created once, then SHARED by every object
1333// that adds K after the same prefix). Objects built the same way share a shape id, so an inline cache
1334// keyed on (shape,offset) hits across ALL of them -- the SOTA property model, vs a per-object cache that
1335// misses on every distinct object. Object stores its shape id at o[1]; the key->offset map is just
1336// insertion order, so obj_find on an IC miss yields the offset and the shape makes it reusable. Pure
1337// fast-path: any miss falls back to the linear obj_find, so shapes NEVER change correctness.
1338const SH_ENT: i64 = 5 // [parent, added_key(strrec as i64), nprops, first_child, next_sibling] (0 = none)
1339const SH_MAX: i64 = 262144
1340static sh_arena: i64
1341static sh_count: i64
1342// shape 0 is RESERVED-INVALID so a cold inline-cache slot (mmap-zero) can NEVER false-hit a real
1343// shape; the EMPTY object shape (the root of the transition tree) is id SH_EMPTY = 1.
1344const SH_EMPTY: i64 = 1
1345func sh_init() -> i64 {
1346 if sh_count == 0 {
1347 let a: *i64 = sys_mmap(SH_MAX * SH_ENT * 8) as *i64
1348 sh_arena = a as i64
1349 a[0] = 0; a[1] = 0; a[2] = 0; a[3] = 0; a[4] = 0 // shape 0 = reserved / invalid (cold-IC sentinel)
1350 a[5] = 0; a[6] = 0; a[7] = 0; a[8] = 0; a[9] = 0 // shape 1 = EMPTY (every object starts here)
1351 sh_count = 2
1352 }
1353 return 0
1354}
1355func sh_rec(id: i64) -> *i64 { return ((sh_arena) + id * SH_ENT * 8) as *i64 }
1356// shape reached by adding `key` to shape `from`; interned (shared child) so same construction path ->
1357// same shape id. Returns `from` unchanged if the registry is full (fast-path just degrades).
1358func sh_transition(from: i64, key: *i64) -> i64 {
1359 sh_init()
1360 let fr: *i64 = sh_rec(from as i64)
1361 var c: i64 = fr[3]
1362 while c != 0 {
1363 let cr: *i64 = sh_rec(c as i64)
1364 if ev_str_eq((cr[1]) as *i64, key) == 1 { return c }
1365 c = cr[4]
1366 }
1367 if sh_count >= SH_MAX { return from }
1368 let nid: i64 = sh_count
1369 sh_count = nid + 1
1370 let nr: *i64 = sh_rec(nid as i64)
1371 let oldhead: i64 = fr[3]
1372 nr[0] = from
1373 nr[1] = key as i64
1374 nr[2] = fr[2] + 1
1375 nr[3] = 0
1376 nr[4] = oldhead
1377 fr[3] = nid
1378 return nid
1379}
1380func obj_shape(o: *i64) -> i64 { return o[1] }
1381// GC root: shape records intern property-NAME key strings BY POINTER (word 1 of each SH_ENT record). An
1382// interned shape can outlive every object that carried that property, becoming the SOLE holder of a runtime
1383// key string -> retain them, or a later sh_transition ev_str_eq would read freed memory. Other record words
1384// are small ids (gc_is_start rejects them). Defined here so SH_ENT/sh_arena/sh_count are in lexical scope.
1385func gc_scan_shapes() -> i64 { if sh_arena != 0 { gc_scan_region(sh_arena as i64, sh_count * SH_ENT) } return 0 }
1386
1387func obj_new() -> *i64 {
1388 let o: *i64 = (nx_pool_alloc(8 * OBJ_HDRW)) as *i64
1389 o[0] = 0; o[1] = SH_EMPTY; o[OBJ_PROTO] = 0; o[OBJ_CAPF] = OBJ_INITCAP
1390 o[OBJ_BACK] = nx_pool_alloc(8 * OBJ_INITCAP * OBJ_ENT)
1391 return o
1392}
1393func obj_proto(o: *i64) -> i64 { return o[OBJ_PROTO] }
1394func obj_set_proto(o: *i64, p: i64) -> i64 { o[OBJ_PROTO] = p; return 0 }
1395// walk the [[Prototype]] chain from `o` (NOT checking o's own props -- caller already did) for `key`.
1396// 1 = found (into out), 0 = absent. Depth-bounded so a proto cycle can't hang the engine.
1397func obj_proto_lookup(o: *i64, key: *i64, out: *i64) -> i64 {
1398 var p: i64 = obj_proto(o)
1399 var depth: i64 = 0
1400 while depth < 200 {
1401 if p == 0 { return 0 }
1402 let po: *i64 = p as *i64
1403 if obj_get(po, key, out) == 1 { return 1 }
1404 p = obj_proto(po)
1405 depth = depth + 1
1406 }
1407 return 0
1408}
1409// ===== function .prototype objects: `Foo.prototype` is a STABLE object created lazily; methods added to it
1410// are inherited by every `new Foo()` instance (via the instance's OBJ_PROTO link). Uniform across tree &
1411// VM closures because it is keyed by the function VALUE-PAYLOAD (closure ptr). Growable side table. =====
1412static fproto_keys: i64
1413static fproto_vals: i64
1414static fproto_n: i64
1415static fproto_cap: i64
1416func fproto_init() -> i64 {
1417 if fproto_cap == 0 { fproto_cap = 64; fproto_keys = sys_mmap(8 * fproto_cap) as i64; fproto_vals = sys_mmap(8 * fproto_cap) as i64; fproto_n = 0 }
1418 return 0
1419}
1420func fproto_find(fnpay: i64) -> i64 {
1421 fproto_init()
1422 let k: *i64 = fproto_keys as *i64
1423 var i: i64 = 0
1424 while i < fproto_n { if k[i] == fnpay { return i } i = i + 1 }
1425 return 0 - 1
1426}
1427func fproto_put(fnpay: i64, protopay: i64) -> i64 {
1428 fproto_init()
1429 let fi: i64 = fproto_find(fnpay)
1430 if fi >= 0 { let v: *i64 = fproto_vals as *i64; v[fi] = protopay; return 0 }
1431 if fproto_n >= fproto_cap {
1432 let ncap: i64 = fproto_cap * 2
1433 let nk: *i64 = sys_mmap(8 * ncap) as *i64
1434 let nv: *i64 = sys_mmap(8 * ncap) as *i64
1435 let ok: *i64 = fproto_keys as *i64
1436 let ov: *i64 = fproto_vals as *i64
1437 var j: i64 = 0
1438 while j < fproto_n { nk[j] = ok[j]; nv[j] = ov[j]; j = j + 1 }
1439 fproto_keys = nk as i64; fproto_vals = nv as i64; fproto_cap = ncap
1440 }
1441 let k: *i64 = fproto_keys as *i64
1442 let v: *i64 = fproto_vals as *i64
1443 k[fproto_n] = fnpay
1444 v[fproto_n] = protopay
1445 fproto_n = fproto_n + 1
1446 return 0
1447}
1448// globalThis: the object bound to `this` in a plain (non-method, non-new) call -- NON-STRICT semantics
1449// (Octane's mode; e.g. NavierStokes checkResult uses `this.result` as scratch). Lazily created + PERSISTENT
1450// so a top-level `this.x` accumulates across calls. Strict-mode undefined-this is a follow-on rung.
1451static js_gthis: i64
1452func js_globalthis() -> i64 {
1453 if js_gthis == 0 { js_gthis = (obj_new()) as i64 }
1454 return js_gthis
1455}
1456// Object.prototype: the shared root object every VAL_OBJECT/VAL_FUNC inherits from on a property miss
1457// (real JS's implicit top of the [[Prototype]] chain). Methods installed here via Object.defineProperty
1458// (jsbn/DeltaBlue `inheritsFrom`) become visible on all objects + constructor functions. Persistent.
1459static js_objproto: i64
1460func js_object_prototype() -> i64 {
1461 if js_objproto == 0 { js_objproto = (obj_new()) as i64 }
1462 return js_objproto
1463}
1464// String.prototype / Array.prototype: persistent WRITABLE objects so user code can extend the built-ins
1465// (`String.prototype.m = fn`; common in scheme2js/prototype.js). A method call on a string/array primitive
1466// that ISN'T a hardcoded builtin falls back to these (js_eval_call). GC roots (gc_mark_globals scans them).
1467static js_strproto: i64
1468func js_string_prototype() -> i64 {
1469 if js_strproto == 0 { js_strproto = (obj_new()) as i64 }
1470 return js_strproto
1471}
1472static js_arrproto: i64
1473func js_array_prototype() -> i64 {
1474 if js_arrproto == 0 { js_arrproto = (obj_new()) as i64 }
1475 return js_arrproto
1476}
1477// FAST-TIER bridge for user-defined String/Array.prototype methods. The VM/JIT (nx_js_vm.nx) can't read
1478// these file-scoped statics directly, so it resolves a proto method through this accessor -- mirroring the
1479// tree-tier js_eval_call fallback so all three tiers behave identically. Reads the RAW static (no lazy
1480// obj_new) so a dispatch miss never allocates mid-call; returns the method's VAL_FUNC payload (the closure
1481// record ptr, as i64) when the key resolves to a function, else 0. `out` is caller scratch [tag,pay].
1482func js_userproto_method(tag: i64, key: *i64, out: *i64) -> i64 {
1483 var proto: i64 = 0
1484 if tag == VAL_STR { proto = js_strproto }
1485 if tag == VAL_ARRAY { proto = js_arrproto }
1486 if proto == 0 { return 0 }
1487 if obj_get((proto) as *i64, key, out) == 1 {
1488 if out[0] == VAL_FUNC { return out[1] }
1489 }
1490 return 0
1491}
1492// USER STATICS on a global NAMESPACE (`Object.extend = fn` -- prototype.js/RayTrace idiom): a per-ns
1493// property store, read before the fixed natives so user overrides win. Lazily created, persistent. Backed
1494// by a single mmap'd 16-slot table (a static array-of-i64 type miscompiles; a lone i64 ptr is safe).
1495static js_nsstat_tab: i64
1496func js_ns_statics(ns: i64) -> *i64 {
1497 if js_nsstat_tab == 0 { js_nsstat_tab = sys_mmap(16 * 8) as i64 }
1498 let tab: *i64 = js_nsstat_tab as *i64
1499 var k: i64 = ns
1500 if k < 0 { k = 0 }
1501 if k > 15 { k = 15 }
1502 if tab[k] == 0 { tab[k] = (obj_new()) as i64 }
1503 return (tab[k]) as *i64
1504}
1505// GC: mark the PERSISTENT global roots that hold pool objects (prototypes, ns-statics, globalThis, shape keys).
1506// MUST be defined HERE, after all the statics it reads -- nx_cc miscompiles forward static references.
1507func gc_mark_globals() -> i64 {
1508 if js_gthis != 0 { let h: i64 = gc_is_start(js_gthis as i64); if h != 0 { gc_mark_header(h as i64) } }
1509 if js_objproto != 0 { let h2: i64 = gc_is_start(js_objproto as i64); if h2 != 0 { gc_mark_header(h2 as i64) } }
1510 if js_strproto != 0 { let h3: i64 = gc_is_start(js_strproto as i64); if h3 != 0 { gc_mark_header(h3 as i64) } }
1511 if js_arrproto != 0 { let h4: i64 = gc_is_start(js_arrproto as i64); if h4 != 0 { gc_mark_header(h4 as i64) } }
1512 if js_nsstat_tab != 0 { gc_scan_region(js_nsstat_tab as i64, 16) }
1513 if fproto_vals != 0 { gc_scan_region(fproto_vals as i64, fproto_n as i64) } // function own-property objects (protos+methods)
1514 gc_scan_shapes() // shape records intern property-name key strings
1515 return 0
1516}
1517// A function is also an OBJECT: func_own(fnpay) is its own-property store (holds static props like
1518// `Foo.Node`, `Foo.count`, AND `prototype`), created empty on first access. Keyed by closure ptr in the
1519// fproto side-table (which now maps fnpay -> this own-object, not the prototype directly).
1520func func_own(fnpay: i64) -> *i64 {
1521 let fi: i64 = fproto_find(fnpay)
1522 if fi >= 0 { let v: *i64 = fproto_vals as *i64; return (v[fi]) as *i64 }
1523 let o: *i64 = obj_new()
1524 fproto_put(fnpay, o as i64)
1525 return o
1526}
1527// Foo.prototype (GET): the [[Prototype]] object shared by `new Foo()` instances -- stored as the "prototype"
1528// own-property, created empty on first access (so methods added to Foo.prototype are inherited).
1529// PERF: this runs on EVERY `new Foo()`. The "prototype" key string + the out-scratch are cached in statics
1530// (built ONCE) -- previously it did sys_mmap(16) [a SYSCALL] + ev_cstr("prototype") [a string alloc] PER call,
1531// which was the dominant ~1.7us/object cost of object churn (K4). Non-reentrant: obj_get/obj_new never recurse
1532// into func_prototype, so the single shared scratch is safe.
1533static fproto_key: i64
1534static fproto_scratch: i64
1535func func_prototype(fnpay: i64) -> i64 {
1536 let own: *i64 = func_own(fnpay)
1537 if fproto_key == 0 { fproto_key = (ev_cstr("prototype\x00" as *u8)) as i64 }
1538 if fproto_scratch == 0 { fproto_scratch = sys_mmap(16) as i64 }
1539 let pk: *i64 = fproto_key as *i64
1540 let pb: *i64 = fproto_scratch as *i64
1541 if obj_get(own, pk, pb) == 1 { if pb[0] == VAL_OBJECT { return pb[1] } }
1542 let p: *i64 = obj_new()
1543 obj_set(own, pk, VAL_OBJECT, p as i64)
1544 return p as i64
1545}
1546func obj_count(o: *i64) -> i64 { return o[0] }
1547func obj_key(o: *i64, i: i64) -> *i64 { let d: *i64 = (o[OBJ_BACK]) as *i64; return (d[i * OBJ_ENT]) as *i64 }
1548// find property index by string-record key, or -1 if absent. Skips TOMBSTONED slots (key==0, from delete).
1549func obj_find(o: *i64, key: *i64) -> i64 {
1550 let c: i64 = o[0]
1551 var i: i64 = 0
1552 while i < c { let kp: *i64 = obj_key(o, i); if (kp as i64) != 0 { if ev_str_eq(kp, key) == 1 { return i } } i = i + 1 }
1553 return 0 - 1
1554}
1555// DELETE a property: TOMBSTONE the slot (key=0, value=undefined) rather than remove+shift. This keeps the
1556// object's SHAPE + slot layout STABLE, so the inline cache stays valid -- a later IC read of a deleted prop's
1557// offset returns the undefined we stored, which IS the correct JS semantics. obj_find/for-in skip key==0.
1558// Returns 1 always (JS `delete` on a configurable/absent prop is true). Re-adding the key appends a fresh slot.
1559func obj_delete(o: *i64, key: *i64) -> i64 {
1560 let fi: i64 = obj_find(o, key)
1561 if fi < 0 { return 1 }
1562 let d: *i64 = (o[OBJ_BACK]) as *i64
1563 let b: i64 = fi * OBJ_ENT
1564 d[b + 0] = 0 // tombstone key
1565 d[b + 1] = VAL_UNDEF // value -> undefined
1566 d[b + 2] = 0
1567 return 1
1568}
1569// grow the backing store (doubling) + copy the live props. 1 = cap-exceeded, 0 ok.
1570func obj_grow(o: *i64) -> i64 {
1571 let nc: i64 = o[OBJ_CAPF] * 2
1572 if nc > OBJ_MAXCAP { return 1 }
1573 let nd: *i64 = (nx_pool_alloc(8 * nc * OBJ_ENT)) as *i64
1574 let od: *i64 = (o[OBJ_BACK]) as *i64
1575 let n: i64 = o[0] * OBJ_ENT
1576 var k: i64 = 0
1577 while k < n { nd[k] = od[k]; k = k + 1 }
1578 o[OBJ_BACK] = nd as i64
1579 o[OBJ_CAPF] = nc
1580 return 0
1581}
1582// SET property (create or update) by string-record key. Idempotent on the key.
1583func obj_set(o: *i64, key: *i64, vt: i64, vp: i64) -> i64 {
1584 let fi: i64 = obj_find(o, key)
1585 if fi >= 0 { let d0: *i64 = (o[OBJ_BACK]) as *i64; let b: i64 = fi * OBJ_ENT; d0[b + 1] = vt; d0[b + 2] = vp; return 0 }
1586 let c: i64 = o[0]
1587 if c >= o[OBJ_CAPF] { if obj_grow(o) == 1 { return 1 } }
1588 let d: *i64 = (o[OBJ_BACK]) as *i64
1589 let b: i64 = c * OBJ_ENT
1590 d[b + 0] = key as i64; d[b + 1] = vt; d[b + 2] = vp
1591 o[0] = c + 1
1592 let ns: i64 = sh_transition(o[1], key) // P3: advance the object's shape (call hoisted -- array-store gotcha)
1593 o[1] = ns
1594 return 0
1595}
1596// GET property into out; returns 1 if FOUND, 0 if absent (caller sets undefined on 0).
1597func obj_get(o: *i64, key: *i64, out: *i64) -> i64 {
1598 let fi: i64 = obj_find(o, key)
1599 if fi < 0 { return 0 }
1600 let d: *i64 = (o[OBJ_BACK]) as *i64
1601 let b: i64 = fi * OBJ_ENT
1602 ev_set(out, d[b + 1], d[b + 2])
1603 return 1
1604}
1605
1606// GROWABLE array: STABLE 3-word header [0]=length [1]=capacity [2]=backing-data-ptr. The header pointer
1607// is the VAL_ARRAY payload and never moves; only the backing store is reallocated (doubling) as the array
1608// grows -- so all references stay valid while a[5000], a[1e6] etc. now work (was a hard 4096 cap). Cells
1609// live in the backing block at data[i*ARR_ENT + 0/1]. All array access goes through arr_get/arr_set/arr_len.
1610const ARR_INITCAP: i64 = 8
1611const ARR_MAXCAP: i64 = 67108864 // 64M elements -- runaway guard, NOT a real-workload limit
1612func arr_new() -> *i64 {
1613 let a: *i64 = (nx_pool_alloc(8 * 4)) as *i64 // [0]len [1]cap [2]data [3]expando-obj (lazy, named props)
1614 a[0] = 0
1615 a[1] = ARR_INITCAP
1616 a[2] = nx_pool_alloc(8 * ARR_INITCAP * ARR_ENT)
1617 a[3] = 0
1618 return a
1619}
1620// arrays ARE objects in JS: they carry arbitrary NAMED (non-index) properties (jQuery event storage sets
1621// `handlers.delegateCount` on a handlers array). a[3] lazily holds an expando object for those props.
1622func arr_expando(a: *i64) -> *i64 {
1623 if a[3] == 0 { a[3] = (obj_new()) as i64 }
1624 return (a[3]) as *i64
1625}
1626func arr_len(a: *i64) -> i64 { return a[0] }
1627// GET element i into out; returns 1 if in-range, 0 if out-of-range (undefined).
1628func arr_get(a: *i64, i: i64, out: *i64) -> i64 {
1629 if i < 0 { return 0 }
1630 if i >= a[0] { return 0 }
1631 let d: *i64 = (a[2]) as *i64
1632 let b: i64 = i * ARR_ENT
1633 ev_set(out, d[b + 0], d[b + 1])
1634 return 1
1635}
1636// grow the backing store (doubling) so index `need` fits + copy the live prefix. 1 = cap-exceeded, 0 ok.
1637func arr_grow(a: *i64, need: i64) -> i64 {
1638 if need >= ARR_MAXCAP { return 1 }
1639 var nc: i64 = a[1]
1640 while nc <= need { nc = nc * 2 }
1641 if nc > ARR_MAXCAP { nc = ARR_MAXCAP }
1642 let nd: *i64 = (nx_pool_alloc(8 * nc * ARR_ENT)) as *i64
1643 let od: *i64 = (a[2]) as *i64
1644 let n: i64 = a[0] * ARR_ENT
1645 var k: i64 = 0
1646 while k < n { nd[k] = od[k]; k = k + 1 }
1647 a[2] = nd as i64
1648 a[1] = nc
1649 return 0
1650}
1651// SET element i; write past current length EXTENDS length (real JS), filling the gap with undefined.
1652// Grows the backing store on demand. Returns 1 only on the 64M runaway guard.
1653func arr_set(a: *i64, i: i64, vt: i64, vp: i64) -> i64 {
1654 if i < 0 { return 1 }
1655 if i >= a[1] { if arr_grow(a, i) == 1 { return 1 } }
1656 let d: *i64 = (a[2]) as *i64
1657 if i >= a[0] {
1658 var j: i64 = a[0]
1659 while j < i { let bb: i64 = j * ARR_ENT; d[bb + 0] = VAL_UNDEF; d[bb + 1] = 0; j = j + 1 }
1660 a[0] = i + 1
1661 }
1662 let b: i64 = i * ARR_ENT
1663 d[b + 0] = vt; d[b + 1] = vp
1664 return 0
1665}
1666
1667// ===================== environment with scope chain =====================
1668// env layout: [0]=count, [1]=parent(*i64 as i64; 0=none), [2]=doc_ptr, [3]=doc_len (DOM document,
1669// set only on the GLOBAL env by js_run_source_doc; 0 on all others -- mmap zero-fill), entries at
1670// ENV_HDR + i*4 = [ns,nl,vt,vp]. The document.* builtins read genv[2]/genv[3].
1671// ENV_HDR bumped 4->5 (R-JS-EVENT): genv[4] = the DOM event-listener table (lazy). 5->6 (R-JS-EVENTLOOP):
1672// genv[5] = the event-loop queue state (lazy, 0=never used). genv[0]=count, genv[1]=parent, genv[2]/[3]=
1673// DOM doc ptr/len (unchanged). All variable slots are ENV_HDR + i*ENV_ENTRY so they shift uniformly --
1674// self-consistent. Only genv (global) roots the table/queues; child envs waste slots 4/5.
1675// ENV_HDR 6->9 (R-JS-TRY): genv[6]=pending-throw flag, genv[7]=thrown tag, genv[8]=thrown pay --
1676// the `throw` VALUE channel (CS_ERROR carries no value; a catching ND_TRY reads+clears these;
1677// a plain runtime error leaves flag 0 so catch binds undefined). Only genv uses them.
1678const ENV_HDR: i64 = 9
1679const ENV_ENTRY: i64 = 5 // [ns, nl, vt, vp, namesrc] -- namesrc lets names compare ACROSS parse contexts
1680const ENV_MAX: i64 = 512
1681const THIS_NS: i64 = 0 - 999999 // sentinel name-offset for the `this` binding (matches only another THIS_NS)
1682const SUPER_NS: i64 = 0 - 999998 // sentinel name-offset for the `@super` binding (the parent constructor in an `extends` class body; matches only another SUPER_NS)
1683// R-JS-EVENTLOOP builtin ids (VAL_NATIVE payload -- bare global functions, resolved by ev_global_native).
1684const BI_SETTIMEOUT: i64 = 80
1685const BI_QUEUEMICROTASK: i64 = 81
1686const BI_RAF: i64 = 82 // requestAnimationFrame
1687const BI_CLEARTIMEOUT: i64 = 83 // no-op (timers run to completion in the drain; nothing to cancel)
1688// event-loop queue state (genv[5]): [0]=micro_next [1]=micro_end [2]=macro_next [3]=macro_end
1689const EL_HDR: i64 = 4
1690const EL_MAXQ: i64 = 8192 // bounded per-queue depth (named cap; overflow -> drop + honest 1 from el_push)
1691// R-JS-PROMISE (rung 2): Promises ride the microtask queue -- .then reactions schedule as microtasks.
1692// VAL_PROMISE/RESOLVE/REJECT (11/12/13) moved up to the VAL_ block near VAL_FLOAT (forward-const fix)
1693const NS_PROMISE: i64 = 6
1694const BI_PROM_RESOLVE: i64 = 90
1695const BI_PROM_REJECT: i64 = 91
1696const BI_PROM_THEN: i64 = 92
1697const BI_PROM_CATCH: i64 = 93
1698// promise record: [0]=state (0 pending / 1 fulfilled / 2 rejected) [1]=value tag [2]=value payload
1699// [3]=reaction count ; reactions at PROM_HDR + i*PROM_REACT = [onFtag onFpay onRtag onRpay resultprom].
1700const PROM_HDR: i64 = 4
1701const PROM_REACT: i64 = 5
1702const PROM_MAXR: i64 = 64
1703// micro-queue TYPED job (8 i64): [0]=kind (0 plain closure / 1 promise reaction). kind0: [1]=clos ptr.
1704// kind1: [1]=handler tag [2]=handler pay [3]=value tag [4]=value pay [5]=result promise ptr [6]=pass-through state.
1705const JOB_SZ: i64 = 8
1706// R-JS-PENDING-FETCH: a fetch/xhr to a NON-data URL records a PENDING request in the EL buffer, PAST the
1707// two queues (PEND_BASE = EL_HDR + 2*EL_MAXQ = 4 + 16384). The engine stays TLS-free; the CONSUMER (headless
1708// render) enumerates pending, fetches each over sovereign TLS, services it, and re-drains -- the real-web loop.
1709const PEND_BASE: i64 = 16388 // EL_HDR(4) + 2*EL_MAXQ(16384); pending count at [PEND_BASE], entries after
1710const PEND_MAX: i64 = 128
1711const PEND_ENT: i64 = 3 // [url-str-rec ptr, promise ptr, serviced flag]
1712// R-JS-FETCH (rung 3): fetch() -> Promise<Response>. Engine stays TLS-free -- a data: URL resolves inline;
1713// the CONSUMER (headless renderer, rung 5) drives real network. Response record = [status, body tag, body pay].
1714// VAL_RESPONSE (=14) and VAL_REGEX (=15) moved up to the VAL_ block near VAL_FLOAT (forward-const fix)
1715const RESP_HDR: i64 = 3
1716const BI_FETCH: i64 = 96
1717const BI_RESP_TEXT: i64 = 94
1718const BI_RESP_JSON: i64 = 95
1719// R-JS-BOM (rung 4): the browser object model as host namespaces (window/location/navigator/history).
1720const NS_WINDOW: i64 = 7
1721const NS_LOCATION: i64 = 8
1722const NS_NAVIGATOR: i64 = 9
1723const NS_HISTORY: i64 = 10
1724const NS_DATE: i64 = 11 // the Date global (Date.now() -> ms timestamp; new Date()/getTime = follow-on)
1725const BI_BOM_NOOP: i64 = 97 // location.reload / history.pushState / window.addEventListener ... -> undefined
1726const BI_DATE_NOW: i64 = 98 // Date.now() -> current time in ms (jQuery guid/expando)
1727// R-JS-XHR: XMLHttpRequest (older $.ajax / jQuery sites). Rides the same data: resolution as fetch; the XHR
1728// value is a plain VAL_OBJECT whose open/send are VAL_NATIVE props (dispatched by the existing object path).
1729const BI_XHR_NEW: i64 = 100
1730const BI_STRING_CTOR: i64 = 110 // String(x) -- coerce x to a primitive string ("" if no arg). NOT 103 (=BI_XHR_NOOP collision, dispatched in js_native_apply before us).
1731// Global constructors get bids UNIQUE across the whole called-native space (js_native_apply dispatches
1732// plain no-`new` calls by bid, so ARRAY_CTOR=101/ERROR_CTOR=102 silently hit BI_XHR_OPEN/BI_XHR_SEND --
1733// `Array(5)` errored, `Error(m)` returned undefined). Error subtypes get DISTINCT bids so `.name` is right.
1734const BI_ARRAY_CTOR: i64 = 111 // new Array(n) / new Array(a,b,c) / Array(...) -- native array constructor
1735const BI_ERROR_CTOR: i64 = 112 // new Error(msg) / Error(msg) -- object with .message + .name="Error"
1736const BI_TYPEERR_CTOR: i64 = 113 // TypeError -> .name="TypeError"
1737const BI_RANGEERR_CTOR: i64 = 114 // RangeError -> .name="RangeError"
1738const BI_NUM_TOSTRING: i64 = 115 // (n).toString([radix]) -- number/float/bool receiver (Crypto uses radix 16)
1739const BI_XHR_OPEN: i64 = 101
1740const BI_XHR_SEND: i64 = 102
1741const BI_XHR_NOOP: i64 = 103 // setRequestHeader / getResponseHeader / abort -> undefined
1742func env_new() -> *i64 { let e: *i64 = sys_mmap(8 * (ENV_HDR + ENV_MAX * ENV_ENTRY)) as *i64; e[0] = 0; e[1] = 0; return e }
1743// GC root: scan a tree-style env_new structure (the VM's genv) as one flat conservative region -- its whole
1744// fixed mmap extent (unwritten slots read 0 = harmless). Defined here so the ENV_ constants are in scope.
1745func gc_scan_env(envp: i64) -> i64 { if envp != 0 { gc_scan_region(envp as i64, ENV_HDR + ENV_MAX * ENV_ENTRY) } return 0 }
1746func env_child(parent: *i64) -> *i64 { let e: *i64 = sys_mmap(8 * (ENV_HDR + ENV_MAX * ENV_ENTRY)) as *i64; e[0] = 0; e[1] = parent as i64; return e }
1747func env_name_eq(s1src: *u8, s1: i64, l1: i64, s2src: *u8, s2: i64, l2: i64) -> i64 {
1748 if s1 == THIS_NS { if s2 == THIS_NS { return 1 } return 0 } // `this` sentinel matches only another `this`
1749 if s2 == THIS_NS { return 0 }
1750 if s1 == SUPER_NS { if s2 == SUPER_NS { return 1 } return 0 } // `@super` sentinel matches only another `@super`
1751 if s2 == SUPER_NS { return 0 }
1752 if l1 != l2 { return 0 }
1753 var i: i64 = 0
1754 while i < l1 { if (s1src[s1 + i] & 0xff) != (s2src[s2 + i] & 0xff) { return 0 } i = i + 1 }
1755 return 1
1756}
1757func env_find_local(env: *i64, src: *u8, ns: i64, nl: i64) -> i64 {
1758 let c: i64 = env[0]
1759 var i: i64 = 0
1760 while i < c { let b: i64 = ENV_HDR + i * ENV_ENTRY; if env_name_eq((env[b + 4]) as *u8, env[b], env[b + 1], src, ns, nl) == 1 { return i } i = i + 1 }
1761 return 0 - 1
1762}
1763func env_define(env: *i64, src: *u8, ns: i64, nl: i64, vt: i64, vp: i64) -> i64 {
1764 let fi: i64 = env_find_local(env, src, ns, nl)
1765 if fi >= 0 { let b: i64 = ENV_HDR + fi * ENV_ENTRY; env[b + 2] = vt; env[b + 3] = vp; return 0 }
1766 let c: i64 = env[0]
1767 if c >= ENV_MAX { return 1 }
1768 let b: i64 = ENV_HDR + c * ENV_ENTRY
1769 env[b] = ns; env[b + 1] = nl; env[b + 2] = vt; env[b + 3] = vp; env[b + 4] = src as i64
1770 env[0] = c + 1
1771 return 0
1772}
1773// LOOKUP: walk the scope chain. Returns 0 if FOUND (value in out), 1 if the name was
1774// never declared anywhere on the chain -- a ReferenceError. The caller MUST treat a 1
1775// as an eval error and NOT silently substitute undefined (real JS: `y+1` with y never
1776// declared throws "y is not defined"). `typeof undeclared` is handled separately.
1777func env_get(env: *i64, src: *u8, ns: i64, nl: i64, out: *i64) -> i64 {
1778 var e: *i64 = env
1779 while ev_isnull(e) == 0 {
1780 let fi: i64 = env_find_local(e, src, ns, nl)
1781 if fi >= 0 { let b: i64 = ENV_HDR + fi * ENV_ENTRY; ev_set(out, e[b + 2], e[b + 3]); return 0 }
1782 e = (e[1]) as *i64
1783 }
1784 ev_set(out, VAL_UNDEF, 0)
1785 return 1
1786}
1787// ASSIGN to an EXISTING binding (walk up to where it was declared). Returns 0 on
1788// success, 1 if the name was never declared on the chain. We do NOT auto-create a
1789// global -- assigning to an undeclared name is a ReferenceError (the spec asks the
1790// evaluator to update the declaring scope, not invent one), so the caller errors.
1791func env_assign(env: *i64, src: *u8, ns: i64, nl: i64, vt: i64, vp: i64) -> i64 {
1792 var e: *i64 = env
1793 while ev_isnull(e) == 0 {
1794 let fi: i64 = env_find_local(e, src, ns, nl)
1795 if fi >= 0 { let b: i64 = ENV_HDR + fi * ENV_ENTRY; e[b + 2] = vt; e[b + 3] = vp; return 0 }
1796 e = (e[1]) as *i64
1797 }
1798 return 1
1799}
1800
1801// ===================== closure records (R-JS-CLOSURE, rung 6) =====================
1802// A FUNCTION VALUE is a CLOSURE: VAL_FUNC's payload is no longer the bare function AST
1803// node -- it is a pointer (cast to i64) to a 2-slot heap record [fnode, defenv]:
1804// [0] = fnode -- the ND_FUNC_DECL AST node (params in slot b, body BLOCK in slot c)
1805// [1] = defenv -- the ENVIRONMENT in scope where the function literal/decl was EVALUATED
1806// The call frame parents `defenv` (NOT the global env), so the body resolves free variables
1807// up the LEXICAL chain to the enclosing function's locals -- the rung-2c closure fix. Envs
1808// are heap and never freed (see header allocate-generously note), so capturing one is just
1809// storing the pointer; a returned inner function keeps its defining frame alive forever.
1810const CLOS_SLOTS: i64 = 2
1811// ---- BYTECODE-VM RE-ENTRY SEAM (rung 5) ----
1812// A closure record whose slot 0 is VMF_MAGIC is a BYTECODE-VM closure [magic, fidx, vmenv],
1813// NOT a tree-walker [fnode, defenv]. When one reaches js_call_core (callback natives, the
1814// event loop, promise reactions, DOM listeners -- ALL closure invocation routes through
1815// js_call_core), the call re-enters the bound VM through a function-pointer hook. The hook
1816// + its VM-state pointer are module statics set by the VM (js_vm_bind) before it runs --
1817// a fn-ptr indirection because the VM imports THIS module (no circular import).
1818// Hook signature: fn(vsaddr, closaddr, argbufaddr, argc, thisvaladdr(0=none), outaddr) -> rc.
1819const VMF_MAGIC: i64 = 780301
1820static js_vm_hook_addr: i64
1821static js_vm_vs_addr: i64
1822func js_vm_bind(hook: i64, vs: i64) -> i64 { js_vm_hook_addr = hook; js_vm_vs_addr = vs; return 0 }
1823// CONSUMER-SWAP (the payoff wiring): a bound DOC HOOK lets js_run_source_doc route a page script through
1824// the bytecode VM/JIT (nx_js_vm's compile_run_doc, registered via js_vm_doc_bind) instead of the tree-walk,
1825// FALLING BACK to the tree-walker when the hook returns DECLINE (-2) for a feature the VM can't compile
1826// (template ${} etc). Default 0 = unbound = tree-walker (unchanged); a consumer opts in. Hook signature:
1827// fn(srcaddr, srclen, dochtml, doclen, outaddr) -> rc (0 ok / 1 error / -2 DECLINE-use-tree-walker).
1828const JS_VM_DOC_DECLINE: i64 = 0 - 2
1829static js_vm_doc_hook: i64
1830func js_vm_doc_bind(hook: i64) -> i64 { js_vm_doc_hook = hook; return 0 }
1831
1832func clos_new(fnode: i64, defenv: *i64) -> *i64 {
1833 let c: *i64 = sys_mmap(8 * CLOS_SLOTS) as *i64
1834 c[0] = fnode
1835 c[1] = defenv as i64
1836 return c
1837}
1838func clos_fnode(c: *i64) -> i64 { return c[0] }
1839func clos_defenv(c: *i64) -> *i64 { return (c[1]) as *i64 }
1840
1841// ===================== expression evaluation =====================
1842// js_eval(ctx, idx, env, genv, out) -> 0 ok, 1 error. genv = global env (call frames parent it).
1843func js_eval(ctx: *i64, idx: i64, env: *i64, genv: *i64, out: *i64) -> i64 {
1844 if idx < 0 { ev_set(out, VAL_UNDEF, 0); return 1 }
1845 if ev_fuel_max > 0 {
1846 ev_fuel_used = ev_fuel_used + 1
1847 if ev_fuel_used > ev_fuel_max { ev_fuel_hit = 1; ev_set(out, VAL_UNDEF, 0); return 1 }
1848 }
1849 let k: i64 = jp_nkind(ctx, idx)
1850
1851 if k == ND_NUMBER { return ev_num_node(ctx, idx, out) }
1852 if k == ND_BOOL { ev_set(out, VAL_BOOL, ev_b2(jp_nextra(ctx, idx))); return 0 }
1853 if k == ND_NULL { ev_set(out, VAL_NULL, 0); return 0 }
1854 // ARRAY ELISION `[0,4,,5]`: an omitted element reads as undefined (never an error).
1855 if k == ND_HOLE { ev_set(out, VAL_UNDEF, 0); return 0 }
1856 if k == ND_YIELD {
1857 // generator STUB (JS-SOTA phase 1): evaluate the operand for its side effects, yield
1858 // expression value = undefined. True suspension/iteration semantics = a later rung.
1859 let yo: i64 = jp_na(ctx, idx)
1860 if yo >= 0 { let ytmp: *i64 = ev_cell(); if js_eval(ctx, yo, env, genv, ytmp) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 } }
1861 ev_set(out, VAL_UNDEF, 0)
1862 return 0
1863 }
1864 if k == ND_STRING { ev_str_from_lit(ctx, idx, out); return 0 }
1865 if k == ND_IDENT {
1866 // declared name (incl a local shadow of `undefined`) -> its bound value.
1867 if env_get(env, jp_src(ctx), ev_tok_start(ctx, idx), ev_tok_len(ctx, idx), out) == 0 { return 0 }
1868 // `undefined` is a global VALUE, not a ReferenceError -- real pages guard with
1869 // `x === undefined` / `x == undefined` everywhere (council fix). A local
1870 // declaration named `undefined` would have been found by env_get above.
1871 if js_lexeme_eq(jp_src(ctx), ev_tok_start(ctx, idx), ev_tok_len(ctx, idx), "undefined\x00" as *u8) == 1 { ev_set(out, VAL_UNDEF, 0); return 0 }
1872 // GLOBAL BUILTIN namespaces (Math/Object/console/JSON) resolve as global objects so
1873 // `Math.max(...)`, `Object.keys(o)`, `console.log(x)` go through the SAME member-call
1874 // path. A user `var Math = ...` would have been found by env_get above (shadows it).
1875 let gns: i64 = ev_global_ns(jp_src(ctx), ev_tok_start(ctx, idx), ev_tok_len(ctx, idx))
1876 if gns != 0 { ev_set(out, VAL_GLOBALNS, gns); return 0 }
1877 // R-JS-EVENTLOOP: bare global FUNCTIONS (setTimeout/queueMicrotask/requestAnimationFrame/
1878 // clearTimeout) resolve to a native value so `setTimeout(fn,0)` goes through the plain-call path.
1879 let gnat: i64 = ev_global_native(jp_src(ctx), ev_tok_start(ctx, idx), ev_tok_len(ctx, idx))
1880 if gnat != 0 { ev_set(out, VAL_NATIVE, gnat); return 0 }
1881 // BROWSER GLOBAL-OBJECT SEMANTICS: `window` IS the global object, so a bare name a script
1882 // attached via `window.$ = jQuery` (jQuery's own exposure line) must resolve. Dynamic window
1883 // props live in the NS_WINDOW statics object (same store ev_get_prop reads for `window.x`); on
1884 // a full scope-chain miss consult it before erroring -- read-side window<->global unification
1885 // (rare path: only on an otherwise-undeclared name, so no hot-path cost).
1886 if obj_get(js_ns_statics(NS_WINDOW), ev_key_from_ident(ctx, idx), out) == 1 { return 0 }
1887 // a truly undeclared name is a ReferenceError, never a silent undefined.
1888 if js_rt_dbg == 1 { sys_write(2, "REF-ERR undeclared '" as *u8, 20); sys_write(2, ((jp_src(ctx) as i64) + ev_tok_start(ctx, idx)) as *u8, ev_tok_len(ctx, idx)); sys_write(2, "'\n" as *u8, 2) }
1889 js_rt_mark(ctx, idx)
1890 ev_set(out, VAL_UNDEF, 0); return 1
1891 }
1892 if k == ND_ERROR { js_rt_mark(ctx, idx); ev_set(out, VAL_UNDEF, 0); return 1 }
1893
1894 if k == ND_ASSIGN {
1895 let lhs: i64 = jp_na(ctx, idx)
1896 let aop: i64 = jp_nextra(ctx, idx) // 0 = plain '='; OP_ADD..OP_MOD = compound (+= etc.)
1897 let lk: i64 = jp_nkind(ctx, lhs)
1898 let rb: *i64 = ev_cell()
1899 // JS-SPEC ORDER (= V8; was rhs-first, which broke Octane NavierStokes' Gauss-Seidel idiom
1900 // `x[cur] = (... x[++cur] ...)`): the target's base+index evaluate ONCE, BEFORE the rhs;
1901 // a compound reads the current value via that same base/index, folds AFTER the rhs, and
1902 // writes back to the SAME slot (`a[++i] -= e` increments i exactly once).
1903 // IDENT target: assign to the DECLARING scope; assign-to-undeclared is a ReferenceError.
1904 if lk == ND_IDENT {
1905 if aop != 0 {
1906 let cur: *i64 = ev_cell()
1907 if js_eval(ctx, lhs, env, genv, cur) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
1908 if js_eval(ctx, jp_nb(ctx, idx), env, genv, rb) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
1909 let folded: *i64 = ev_cell()
1910 if ev_apply_binop(aop, cur, rb, folded) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
1911 ev_copy(rb, folded)
1912 } else {
1913 if js_eval(ctx, jp_nb(ctx, idx), env, genv, rb) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
1914 }
1915 if env_assign(env, jp_src(ctx), ev_tok_start(ctx, lhs), ev_tok_len(ctx, lhs), rb[0], rb[1]) == 1 {
1916 // Undeclared target. NON-STRICT: a PLAIN `x = v` auto-creates a GLOBAL binding (real JS);
1917 // a COMPOUND `x += v` on undeclared is a ReferenceError. (VM declines plain-auto-global to us.)
1918 if aop != 0 { ev_set(out, VAL_UNDEF, 0); return 1 }
1919 env_define(genv, jp_src(ctx), ev_tok_start(ctx, lhs), ev_tok_len(ctx, lhs), rb[0], rb[1])
1920 }
1921 ev_copy(out, rb)
1922 return 0
1923 }
1924 // MEMBER target: o.x = v -> base evaluated ONCE (before rhs); compound reads via ev_get_prop.
1925 if lk == ND_MEMBER {
1926 let ob: *i64 = ev_cell()
1927 if js_eval(ctx, jp_na(ctx, lhs), env, genv, ob) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
1928 let key: *i64 = ev_prop_key(ctx, jp_nb(ctx, lhs))
1929 if aop != 0 {
1930 let cur: *i64 = ev_cell()
1931 if ev_get_prop(ob, key, cur) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
1932 if js_eval(ctx, jp_nb(ctx, idx), env, genv, rb) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
1933 let folded: *i64 = ev_cell()
1934 if ev_apply_binop(aop, cur, rb, folded) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
1935 ev_copy(rb, folded)
1936 } else {
1937 if js_eval(ctx, jp_nb(ctx, idx), env, genv, rb) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
1938 }
1939 if ev_set_prop(ob, key, rb[0], rb[1]) == 1 { js_rt_mark(ctx, idx); ev_set(out, VAL_UNDEF, 0); return 1 }
1940 js_dom_write_hook(ob, key, rb) // R-JS-DOMWRITE: mirror el.textContent=/innerHTML= into the live tree
1941 ev_copy(out, rb)
1942 return 0
1943 }
1944 // INDEX target: o[k] = v -> base+index evaluated ONCE (before rhs); compound reads the element.
1945 if lk == ND_INDEX {
1946 let ob: *i64 = ev_cell()
1947 if js_eval(ctx, jp_na(ctx, lhs), env, genv, ob) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
1948 let kb: *i64 = ev_cell()
1949 if js_eval(ctx, jp_nb(ctx, lhs), env, genv, kb) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
1950 if aop != 0 {
1951 let cur: *i64 = ev_cell()
1952 var have: i64 = 0
1953 if ob[0] == VAL_ARRAY { if kb[0] == VAL_NUM {
1954 let a0: *i64 = (ob[1]) as *i64
1955 if arr_get(a0, kb[1], cur) == 0 { ev_set(cur, VAL_UNDEF, 0) }
1956 have = 1
1957 } }
1958 if have == 0 {
1959 let key0: *i64 = ev_key_of_val(kb)
1960 if ev_get_prop(ob, key0, cur) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
1961 }
1962 if js_eval(ctx, jp_nb(ctx, idx), env, genv, rb) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
1963 let folded: *i64 = ev_cell()
1964 if ev_apply_binop(aop, cur, rb, folded) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
1965 ev_copy(rb, folded)
1966 } else {
1967 if js_eval(ctx, jp_nb(ctx, idx), env, genv, rb) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
1968 }
1969 // array + numeric key -> direct integer write (matches the read fast path).
1970 if ob[0] == VAL_ARRAY {
1971 if kb[0] == VAL_NUM {
1972 let a: *i64 = (ob[1]) as *i64
1973 if arr_set(a, kb[1], rb[0], rb[1]) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
1974 ev_copy(out, rb)
1975 return 0
1976 }
1977 }
1978 let key: *i64 = ev_key_of_val(kb)
1979 if ev_set_prop(ob, key, rb[0], rb[1]) == 1 { js_rt_mark(ctx, idx); ev_set(out, VAL_UNDEF, 0); return 1 }
1980 ev_copy(out, rb)
1981 return 0
1982 }
1983 // any other lhs kind is not assignable (parser already rejects literals etc.).
1984 ev_set(out, VAL_UNDEF, 0); return 1
1985 }
1986
1987 // UPDATE ++x/--x/x++/x-- (extra: 1=++pre 2=--pre 3=++post 4=--post). Read the target's
1988 // CURRENT value, fold cur(+/-)1 via ev_apply_binop (same numeric/float ladder as compound
1989 // assign), write back through the SAME ident/member/index paths as ND_ASSIGN (member/index
1990 // re-evaluate the base for the write, matching compound-assign order). Result = the NEW
1991 // value for prefix, the OLD value for postfix.
1992 if k == ND_UPDATE {
1993 let tgt: i64 = jp_na(ctx, idx)
1994 let mode: i64 = jp_nextra(ctx, idx)
1995 let lk: i64 = jp_nkind(ctx, tgt)
1996 let cur: *i64 = ev_cell()
1997 let one: *i64 = ev_cell()
1998 ev_set(one, VAL_NUM, 1)
1999 var uop: i64 = OP_ADD
2000 if mode == 2 { uop = OP_SUB }
2001 if mode == 4 { uop = OP_SUB }
2002 let nw: *i64 = ev_cell()
2003 // SINGLE-EVAL of the reference (JS spec): the target base (and index) evaluate EXACTLY ONCE, then the
2004 // read + write both go through those already-evaluated values (mirrors the compound-assign path above).
2005 // The OLD code re-evaluated base/key for the write -> `a[k()]++` ran k() twice (spec violation).
2006 if lk == ND_IDENT {
2007 if js_eval(ctx, tgt, env, genv, cur) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2008 if ev_apply_binop(uop, cur, one, nw) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2009 if env_assign(env, jp_src(ctx), ev_tok_start(ctx, tgt), ev_tok_len(ctx, tgt), nw[0], nw[1]) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2010 if mode <= 2 { ev_copy(out, nw) } else { ev_copy(out, cur) }
2011 return 0
2012 }
2013 if lk == ND_MEMBER {
2014 let ob: *i64 = ev_cell()
2015 if js_eval(ctx, jp_na(ctx, tgt), env, genv, ob) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 } // base ONCE
2016 let key: *i64 = ev_prop_key(ctx, jp_nb(ctx, tgt))
2017 if ev_get_prop(ob, key, cur) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 } // read via same base
2018 if ev_apply_binop(uop, cur, one, nw) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2019 if ev_set_prop(ob, key, nw[0], nw[1]) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 } // write via same base
2020 js_dom_write_hook(ob, key, nw)
2021 if mode <= 2 { ev_copy(out, nw) } else { ev_copy(out, cur) }
2022 return 0
2023 }
2024 if lk == ND_INDEX {
2025 let ob: *i64 = ev_cell()
2026 if js_eval(ctx, jp_na(ctx, tgt), env, genv, ob) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 } // base ONCE
2027 let kb: *i64 = ev_cell()
2028 if js_eval(ctx, jp_nb(ctx, tgt), env, genv, kb) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 } // index ONCE
2029 var have: i64 = 0
2030 if ob[0] == VAL_ARRAY { if kb[0] == VAL_NUM {
2031 let a0: *i64 = (ob[1]) as *i64
2032 if arr_get(a0, kb[1], cur) == 0 { ev_set(cur, VAL_UNDEF, 0) }
2033 have = 1
2034 } }
2035 if have == 0 {
2036 let key0: *i64 = ev_key_of_val(kb)
2037 if ev_get_prop(ob, key0, cur) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2038 }
2039 if ev_apply_binop(uop, cur, one, nw) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2040 var wdone: i64 = 0
2041 if ob[0] == VAL_ARRAY { if kb[0] == VAL_NUM {
2042 let a: *i64 = (ob[1]) as *i64
2043 if arr_set(a, kb[1], nw[0], nw[1]) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2044 wdone = 1
2045 } }
2046 if wdone == 0 {
2047 let key2: *i64 = ev_key_of_val(kb)
2048 if ev_set_prop(ob, key2, nw[0], nw[1]) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2049 }
2050 if mode <= 2 { ev_copy(out, nw) } else { ev_copy(out, cur) }
2051 return 0
2052 }
2053 ev_set(out, VAL_UNDEF, 0)
2054 return 1
2055 }
2056
2057 if k == ND_SEQ { // comma operator: eval each child in order; value = the LAST
2058 let sn: i64 = jp_nb(ctx, idx)
2059 var si: i64 = 0
2060 while si < sn {
2061 let sch: i64 = jp_child_at(ctx, idx, si)
2062 if js_eval(ctx, sch, env, genv, out) == 1 {
2063 if js_rt_dbg == 2 { var scb: i64 = ev_tok_start(ctx, sch); var dch: i64 = sch; var dl: i64 = 0; while dl < 5 { let na: i64 = jp_na(ctx, dch); if na < 0 { dl = 5 } else { let noff: i64 = ev_tok_start(ctx, na); if noff > 0 { scb = noff; dl = 5 } else { dch = na; dl = dl + 1 } } } sys_write(2, "SEQ-ERR part " as *u8, 13); nx_dbg_num(si); sys_write(2, "/" as *u8, 1); nx_dbg_num(sn); sys_write(2, " kind=" as *u8, 6); nx_dbg_num(jp_nkind(ctx, sch)); sys_write(2, " @" as *u8, 2); nx_dbg_num(scb); sys_write(2, " '" as *u8, 2); let spq: *u8 = jp_src(ctx); var dq: i64 = 0; while dq < 64 { if spq[scb+dq]==(0 as u8){dq=64} else { sys_write(2, ((spq as i64)+scb+dq) as *u8, 1); dq=dq+1 } } sys_write(2, "'\n" as *u8, 2) }
2064 return 1
2065 }
2066 si = si + 1
2067 }
2068 return 0
2069 }
2070 if k == ND_UNARY {
2071 let op: i64 = jp_nextra(ctx, idx)
2072 // typeof tolerates an UNDECLARED identifier (legal JS -> "undefined"); evaluate
2073 // its operand WITHOUT letting a ReferenceError escape, then map the tag.
2074 if op == OP_TYPEOF {
2075 let tb: *i64 = ev_cell()
2076 var tag: i64 = VAL_UNDEF
2077 let saved_ep: i64 = js_rt_errpos
2078 if js_eval(ctx, jp_na(ctx, idx), env, genv, tb) == 0 { tag = tb[0] }
2079 js_rt_errpos = saved_ep // typeof SWALLOWS a ReferenceError on its operand -> it is NOT the fatal
2080 let rec: *i64 = ev_typeof_str(tag)
2081 ev_set(out, VAL_STR, rec as i64)
2082 return 0
2083 }
2084 // delete obj.k / obj[k] -- operates on the REFERENCE (eval base + key, remove the property), not a value.
2085 if op == OP_DELETE {
2086 let opnd: i64 = jp_na(ctx, idx)
2087 let ok: i64 = jp_nkind(ctx, opnd)
2088 if ok == ND_MEMBER {
2089 let db: *i64 = ev_cell()
2090 if js_eval(ctx, jp_na(ctx, opnd), env, genv, db) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2091 if db[0] == VAL_OBJECT { let key: *i64 = ev_prop_key(ctx, jp_nb(ctx, opnd)); obj_delete((db[1]) as *i64, key) }
2092 ev_set(out, VAL_BOOL, 1); return 0
2093 }
2094 if ok == ND_INDEX {
2095 let db: *i64 = ev_cell()
2096 if js_eval(ctx, jp_na(ctx, opnd), env, genv, db) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2097 let kb: *i64 = ev_cell()
2098 if js_eval(ctx, jp_nb(ctx, opnd), env, genv, kb) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2099 if db[0] == VAL_OBJECT { let key: *i64 = ev_key_of_val(kb); obj_delete((db[1]) as *i64, key) }
2100 ev_set(out, VAL_BOOL, 1); return 0
2101 }
2102 ev_set(out, VAL_BOOL, 1); return 0 // delete of a non-reference (`delete x`) -> true, no effect
2103 }
2104 let ob: *i64 = ev_cell()
2105 if js_eval(ctx, jp_na(ctx, idx), env, genv, ob) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2106 if op == OP_NOT { if ev_truthy(ob) == 1 { ev_set(out, VAL_BOOL, 0) } else { ev_set(out, VAL_BOOL, 1) } return 0 }
2107 // -x / +x on a FLOAT stay float (was int-truncating: -0.5 -> 0, which zeroed NavierStokes' h).
2108 if op == OP_NEG { if ob[0] == VAL_FLOAT { ev_set(out, VAL_FLOAT, nx_f64_neg(ob[1])); return 0 } ev_set(out, VAL_NUM, 0 - ev_tonum(ob)); return 0 }
2109 if op == OP_POS { if ob[0] == VAL_FLOAT { ev_set(out, VAL_FLOAT, ob[1]); return 0 } ev_set(out, VAL_NUM, ev_tonum(ob)); return 0 }
2110 if op == OP_BNOT { ev_set(out, VAL_NUM, 0 - js_toint32(ev_tonum(ob)) - 1); return 0 } // ~x = -(ToInt32 x)-1
2111 if op == OP_VOID { ev_set(out, VAL_UNDEF, 0); return 0 } // void x -> operand evaluated (above) for side effects; yields undefined
2112 ev_set(out, VAL_UNDEF, 0); return 0
2113 }
2114
2115 if k == ND_BINARY {
2116 let op: i64 = jp_nextra(ctx, idx)
2117 let lb: *i64 = ev_cell()
2118 if js_eval(ctx, jp_na(ctx, idx), env, genv, lb) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2119 if op == OP_AND {
2120 if ev_truthy(lb) == 0 { ev_copy(out, lb); return 0 }
2121 return js_eval(ctx, jp_nb(ctx, idx), env, genv, out)
2122 }
2123 if op == OP_OR {
2124 if ev_truthy(lb) == 1 { ev_copy(out, lb); return 0 }
2125 return js_eval(ctx, jp_nb(ctx, idx), env, genv, out)
2126 }
2127 if op == OP_NULLISH {
2128 var nul: i64 = 0
2129 if lb[0] == VAL_NULL { nul = 1 }
2130 if lb[0] == VAL_UNDEF { nul = 1 }
2131 if nul == 0 { ev_copy(out, lb); return 0 }
2132 return js_eval(ctx, jp_nb(ctx, idx), env, genv, out)
2133 }
2134 let rb: *i64 = ev_cell()
2135 if js_eval(ctx, jp_nb(ctx, idx), env, genv, rb) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2136 if op == OP_INSTANCEOF { return ev_instanceof(lb, rb, out) }
2137 if op == OP_IN { return ev_in(lb, rb, out) }
2138 if op == OP_EQ { ev_set(out, VAL_BOOL, ev_loose_eq(lb, rb)); return 0 }
2139 if op == OP_NE { if ev_loose_eq(lb, rb) == 1 { ev_set(out, VAL_BOOL, 0) } else { ev_set(out, VAL_BOOL, 1) } return 0 }
2140 if op == OP_SEQ { ev_set(out, VAL_BOOL, ev_strict_eq(lb, rb)); return 0 }
2141 if op == OP_SNE { if ev_strict_eq(lb, rb) == 1 { ev_set(out, VAL_BOOL, 0) } else { ev_set(out, VAL_BOOL, 1) } return 0 }
2142 // '+' is STRING CONCAT when EITHER operand is a string (coerce the other).
2143 if op == OP_ADD {
2144 if lb[0] == VAL_STR { return ev_str_concat(ev_coerce_str(lb), ev_coerce_str(rb), out) }
2145 if rb[0] == VAL_STR { return ev_str_concat(ev_coerce_str(lb), ev_coerce_str(rb), out) }
2146 }
2147 // Relational < <= > >= on TWO strings = BYTE-lexicographic order (real JS:
2148 // 'a'<'b'=true, 'apple'<'banana'=true, 'ab'<'abc'=true). MIXED str/number relational
2149 // is handled by the next block (numeric coercion); only the str-vs-str case lands here.
2150 if lb[0] == VAL_STR { if rb[0] == VAL_STR {
2151 let c: i64 = ev_str_cmp((lb[1]) as *i64, (rb[1]) as *i64)
2152 if op == OP_LT { ev_set(out, VAL_BOOL, ev_b2(c < 0)); return 0 }
2153 if op == OP_LE { ev_set(out, VAL_BOOL, ev_b2(c <= 0)); return 0 }
2154 if op == OP_GT { ev_set(out, VAL_BOOL, ev_b2(c > 0)); return 0 }
2155 if op == OP_GE { ev_set(out, VAL_BOOL, ev_b2(c >= 0)); return 0 }
2156 } }
2157 // MIXED string<->number relational (council MAJOR fix): ONE operand is a string,
2158 // the other a clean numeric (number/bool/null). Real JS coerces the string with
2159 // ToNumber, so '10'<5 is 10<5=false and '5'<6=true -- NOT both-coerced-to-0. We
2160 // parse the string as an INTEGER (the only numeric form this rung); a NON-numeric
2161 // string (e.g. 'abc'<5) is ToNumber=NaN in JS -> we ERROR (rc=1) rather than the
2162 // banned silent coerce-to-0, since NaN ordering needs the float ladder (rung-2b).
2163 if ev_is_relop(op) == 1 {
2164 if ev_one_str_one_num(lb, rb) == 1 {
2165 let mb: *i64 = ev_cell()
2166 if ev_mixed_relnum(lb, mb) == 0 { js_rt_mark(ctx, idx); ev_set(out, VAL_UNDEF, 0); return 1 }
2167 let nb: *i64 = ev_cell()
2168 if ev_mixed_relnum(rb, nb) == 0 { js_rt_mark(ctx, idx); ev_set(out, VAL_UNDEF, 0); return 1 }
2169 let x: i64 = mb[0]
2170 let y: i64 = nb[0]
2171 if op == OP_LT { ev_set(out, VAL_BOOL, ev_b2(x < y)); return 0 }
2172 if op == OP_LE { ev_set(out, VAL_BOOL, ev_b2(x <= y)); return 0 }
2173 if op == OP_GT { ev_set(out, VAL_BOOL, ev_b2(x > y)); return 0 }
2174 if op == OP_GE { ev_set(out, VAL_BOOL, ev_b2(x >= y)); return 0 }
2175 }
2176 }
2177 // ToNumber coercion for non-`+` ops: string/bool/null operand -> number (JS ToNumber). `100/"5"`=20.
2178 var lc: *i64 = lb
2179 var rc: *i64 = rb
2180 if lb[0] == VAL_STR { let lt: *i64 = ev_cell(); ev_coerce_num(lb, lt); lc = lt }
2181 if rb[0] == VAL_STR { let rt: *i64 = ev_cell(); ev_coerce_num(rb, rt); rc = rt }
2182 // BITWISE (&|^ << >> >>>) -- real JS ToInt32 both sides (never float-promote); 32-bit int result.
2183 if op == OP_BAND { ev_set(out, VAL_NUM, js_toint32(ev_tonum(lc)) & js_toint32(ev_tonum(rc))); return 0 }
2184 if op == OP_BOR { ev_set(out, VAL_NUM, js_toint32(ev_tonum(lc)) | js_toint32(ev_tonum(rc))); return 0 }
2185 if op == OP_BXOR { ev_set(out, VAL_NUM, js_toint32(ev_tonum(lc)) ^ js_toint32(ev_tonum(rc))); return 0 }
2186 if op == OP_SHL { let sa: i64 = js_toint32(ev_tonum(lc)); let sh: i64 = ev_tonum(rc) & 31; ev_set(out, VAL_NUM, js_toint32(sa << sh)); return 0 }
2187 if op == OP_SHR { let sa: i64 = js_toint32(ev_tonum(lc)); let sh: i64 = ev_tonum(rc) & 31; ev_set(out, VAL_NUM, sa >> sh); return 0 }
2188 if op == OP_USHR { let ua: i64 = ev_tonum(lc) & 0xffffffff; let sh: i64 = ev_tonum(rc) & 31; ev_set(out, VAL_NUM, ua >> sh); return 0 }
2189 // FLOAT promotion (R-JS-F64-2): either operand a float -> f64 arithmetic (result VAL_FLOAT).
2190 if lc[0] == VAL_FLOAT { return ev_binop_f64(op, ev_tof64(lc), ev_tof64(rc), out) }
2191 if rc[0] == VAL_FLOAT { return ev_binop_f64(op, ev_tof64(lc), ev_tof64(rc), out) }
2192 let a: i64 = ev_tonum(lc)
2193 let b: i64 = ev_tonum(rc)
2194 if op == OP_ADD { ev_set(out, VAL_NUM, a + b); return 0 }
2195 if op == OP_SUB { ev_set(out, VAL_NUM, a - b); return 0 }
2196 if op == OP_MUL { ev_set(out, VAL_NUM, a * b); return 0 }
2197 // div/mod by zero is an ERROR here (no float Infinity/NaN this rung), not a value.
2198 if op == OP_DIV { let dr: i64 = ev_num_div(a, b, out); if dr == 1 { js_rt_mark(ctx, idx) } return dr }
2199 if op == OP_MOD { let mr: i64 = ev_num_mod(a, b, out); if mr == 1 { js_rt_mark(ctx, idx) } return mr }
2200 if op == OP_LT { ev_set(out, VAL_BOOL, ev_b2(a < b)); return 0 }
2201 if op == OP_LE { ev_set(out, VAL_BOOL, ev_b2(a <= b)); return 0 }
2202 if op == OP_GT { ev_set(out, VAL_BOOL, ev_b2(a > b)); return 0 }
2203 if op == OP_GE { ev_set(out, VAL_BOOL, ev_b2(a >= b)); return 0 }
2204 ev_set(out, VAL_UNDEF, 0); return 0
2205 }
2206
2207 // FUNCTION EXPRESSION / ARROW (R-JS-CLOSURE, rung 6): a function literal in EXPRESSION
2208 // position evaluates to a CLOSURE VALUE capturing the CURRENT env as its defining scope
2209 // (so `var add5 = makeAdder(5)` returns an inner fn that still sees makeAdder's `x`, and
2210 // `(x)=>x+1` / `(function(){...})()` work). The node is ND_FUNC_DECL (function exprs + arrows
2211 // share the decl shape).
2212 if k == ND_FUNC_DECL {
2213 let fnm: i64 = jp_na(ctx, idx)
2214 if fnm >= 0 {
2215 // NAMED function EXPRESSION (`var f = function fact(n){...fact(n-1)...}`): the name is bound
2216 // in an INTERMEDIATE scope visible ONLY to the function's own body (self-recursion), NOT the
2217 // enclosing scope (real JS). Recursive memoizers + some Sizzle helpers depend on this.
2218 let fnenv: *i64 = env_child(env)
2219 let cn: *i64 = clos_new(idx, fnenv)
2220 env_define(fnenv, jp_src(ctx), ev_tok_start(ctx, fnm), ev_tok_len(ctx, fnm), VAL_FUNC, cn as i64)
2221 ev_set(out, VAL_FUNC, cn as i64)
2222 return 0
2223 }
2224 let c: *i64 = clos_new(idx, env)
2225 ev_set(out, VAL_FUNC, c as i64)
2226 return 0
2227 }
2228
2229 // class Name{constructor(){} m(){}...}: ctor closure bound to Name; each method installed on the
2230 // constructor's .prototype (so `new Name()` instances inherit it via the [[Prototype]] chain). a=name
2231 // IDENT, b=ctor ND_FUNC_DECL, c=methods ND_BLOCK (each child = a method ND_FUNC_DECL).
2232 if k == ND_CLASS {
2233 let ctornode: i64 = jp_nb(ctx, idx)
2234 // `extends <expr>`: the superclass node is stored in the ND_CLASS extra slot (-1 = none).
2235 // Evaluate it to the parent constructor, then (a) run ctor+methods in a class-body child
2236 // env that binds `@super` = parent ctor (so `super(...)`/`super.m()` resolve lexically),
2237 // and (b) chain B.prototype -> A.prototype so `new B()` inherits A's methods.
2238 let supidx: i64 = jp_nextra(ctx, idx)
2239 var cenv: *i64 = env
2240 var suppay: i64 = 0
2241 var hassuper: i64 = 0
2242 if supidx >= 0 {
2243 let sv: *i64 = ev_cell()
2244 if js_eval(ctx, supidx, env, genv, sv) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2245 if sv[0] != VAL_FUNC { ev_set(out, VAL_UNDEF, 0); return 1 } // `extends <non-function>` -> error
2246 suppay = sv[1]
2247 hassuper = 1
2248 cenv = env_child(env)
2249 env_define(cenv, jp_src(ctx), SUPER_NS, 5, VAL_FUNC, suppay)
2250 }
2251 let cc: *i64 = clos_new(ctornode, cenv)
2252 let ctorpay: i64 = cc as i64
2253 let nameid: i64 = jp_na(ctx, idx)
2254 env_define(env, jp_src(ctx), ev_tok_start(ctx, nameid), ev_tok_len(ctx, nameid), VAL_FUNC, ctorpay)
2255 let proto: i64 = func_prototype(ctorpay)
2256 if hassuper == 1 { obj_set_proto(proto as *i64, func_prototype(suppay)) } // B.prototype -> A.prototype
2257 let methods: i64 = jp_nc(ctx, idx)
2258 let mcount: i64 = jp_nb(ctx, methods)
2259 var mi: i64 = 0
2260 while mi < mcount {
2261 let mnode: i64 = jp_child_at(ctx, methods, mi)
2262 let mkey: *i64 = ev_prop_key(ctx, jp_na(ctx, mnode))
2263 let mclos: *i64 = clos_new(mnode, cenv)
2264 obj_set(proto as *i64, mkey, VAL_FUNC, mclos as i64)
2265 mi = mi + 1
2266 }
2267 ev_set(out, VAL_FUNC, ctorpay)
2268 return 0
2269 }
2270
2271 if k == ND_CALL { return js_eval_call(ctx, idx, env, genv, out) }
2272
2273 // TERNARY cond ? then : else -- eval cond, then eval ONLY the taken branch (real JS
2274 // short-circuit: the untaken branch is NEVER evaluated, so `1 ? 2 : zzz` is 2 with zzz
2275 // undeclared and no ReferenceError). a=cond, b=then-expr, c=else-expr.
2276 if k == ND_TERNARY {
2277 let cb: *i64 = ev_cell()
2278 if js_eval(ctx, jp_na(ctx, idx), env, genv, cb) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2279 if ev_truthy(cb) == 1 { return js_eval(ctx, jp_nb(ctx, idx), env, genv, out) }
2280 return js_eval(ctx, jp_nc(ctx, idx), env, genv, out)
2281 }
2282
2283 // OBJECT literal { k:v, ... } -> allocate a heap object, eval each prop value, store.
2284 if k == ND_OBJECT {
2285 let o: *i64 = obj_new()
2286 let cnt: i64 = jp_nb(ctx, idx)
2287 var i: i64 = 0
2288 while i < cnt {
2289 let prop: i64 = jp_child_at(ctx, idx, i)
2290 let key: *i64 = ev_prop_key(ctx, prop) // tokidx names the key
2291 let vb: *i64 = ev_cell()
2292 if js_eval(ctx, jp_na(ctx, prop), env, genv, vb) == 1 { if js_rt_dbg == 2 { sys_write(2, "OBJLIT-ERR key='" as *u8, 16); sys_write(2, ev_str_bytes(key), ev_str_len(key)); sys_write(2, "'\n" as *u8, 2) } js_rt_mark(ctx, prop); ev_set(out, VAL_UNDEF, 0); return 1 }
2293 obj_set(o, key, vb[0], vb[1])
2294 i = i + 1
2295 }
2296 ev_set(out, VAL_OBJECT, o as i64)
2297 return 0
2298 }
2299
2300 // ARRAY literal [ e0, e1, ... ] -> allocate a heap array, eval elements in order.
2301 if k == ND_ARRAY {
2302 let a: *i64 = arr_new()
2303 let cnt: i64 = jp_nb(ctx, idx)
2304 var i: i64 = 0
2305 var oi: i64 = 0
2306 while i < cnt {
2307 let el: i64 = jp_child_at(ctx, idx, i)
2308 if jp_nkind(ctx, el) == ND_SPREAD { // [...a] : splice a's elements in
2309 let sb: *i64 = ev_cell()
2310 if js_eval(ctx, jp_na(ctx, el), env, genv, sb) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2311 if sb[0] == VAL_ARRAY {
2312 let src2: *i64 = (sb[1]) as *i64
2313 let slen: i64 = arr_len(src2)
2314 var si: i64 = 0
2315 while si < slen {
2316 let eb: *i64 = ev_cell()
2317 arr_get(src2, si, eb)
2318 arr_set(a, oi, eb[0], eb[1])
2319 oi = oi + 1
2320 si = si + 1
2321 }
2322 }
2323 } else {
2324 let vb: *i64 = ev_cell()
2325 if js_eval(ctx, el, env, genv, vb) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2326 arr_set(a, oi, vb[0], vb[1])
2327 oi = oi + 1
2328 }
2329 i = i + 1
2330 }
2331 ev_set(out, VAL_ARRAY, a as i64)
2332 return 0
2333 }
2334
2335 if k == ND_NEW {
2336 let obj: *i64 = obj_new()
2337 let thisv: *i64 = ev_cell()
2338 ev_set(thisv, VAL_OBJECT, obj as i64)
2339 var calleenode: i64 = 0
2340 var argsnode: i64 = 0 - 1
2341 if jp_nextra(ctx, idx) == 1 { calleenode = jp_na(ctx, idx) }
2342 else { let cn: i64 = jp_na(ctx, idx); calleenode = jp_na(ctx, cn); argsnode = cn }
2343 let cv: *i64 = ev_cell()
2344 if js_eval(ctx, calleenode, env, genv, cv) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2345 if cv[0] == VAL_GLOBALNS { if cv[1] == NS_OBJECT { ev_set(out, VAL_OBJECT, (obj_new()) as i64); return 0 } } // new Object() -> {}
2346 if cv[0] != VAL_NATIVE { if cv[0] != VAL_FUNC { ev_set(out, VAL_UNDEF, 0); return 1 } }
2347 var argc: i64 = 0
2348 let argbuf: *i64 = sys_mmap(8 * 2 * 256) as *i64
2349 if argsnode != (0 - 1) {
2350 let n: i64 = js_eval_args(ctx, argsnode, env, genv, argbuf) // post-spread-expansion count
2351 if n < 0 { ev_set(out, VAL_UNDEF, 0); return 1 }
2352 argc = n
2353 }
2354 if cv[0] == VAL_NATIVE { return js_construct_native_args(cv[1], argbuf, argc, out) } // new Array(n)/XHR
2355 obj_set_proto(obj, func_prototype(cv[1])) // link instance to Foo.prototype BEFORE the ctor runs
2356 let ret: *i64 = ev_cell()
2357 if js_call_core(ctx, (cv[1]) as *i64, argbuf, argc, thisv, genv, ret) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2358 if ret[0] == VAL_OBJECT { ev_copy(out, ret) } else { ev_set(out, VAL_OBJECT, obj as i64) }
2359 return 0
2360 }
2361 if k == ND_THIS {
2362 if env_get(env, jp_src(ctx), THIS_NS, 4, out) == 1 { ev_set(out, VAL_OBJECT, js_globalthis()) } // non-strict: unbound this -> globalThis
2363 return 0
2364 }
2365 if k == ND_TEMPLATE { return js_eval_template(ctx, idx, env, genv, out) }
2366 if k == ND_REGEX { return js_make_regex(ctx, idx, out) }
2367 if k == ND_SUPER { ev_set(out, VAL_UNDEF, 0); return 1 } // bare `super` (not a call/member) is a syntax error at eval
2368
2369 // MEMBER read: o.x (property child is an IDENT node; arr.length is special).
2370 if k == ND_MEMBER { return js_eval_member(ctx, idx, env, genv, out) }
2371 // INDEX read: o[i] / arr[i] (key is evaluated; array uses int index, object string key).
2372 if k == ND_INDEX { return js_eval_index(ctx, idx, env, genv, out) }
2373
2374 // Any OTHER node kind reaching eval is unrecognized/unimplemented -> ERROR, never a
2375 // silent success. (No blanket fall-through to undefined.)
2376 ev_set(out, VAL_UNDEF, 0); return 1
2377}
2378
2379// evaluate a CALL node. TWO call shapes:
2380// (1) METHOD CALL obj.method(args) -- callee is ND_MEMBER. Evaluate the object as
2381// `this`, resolve the method NAME against the receiver's type's builtin table, eval
2382// the args, and invoke the native (passing `this`). A user-FUNCTION stored as a
2383// property (o.f where o.f is a VAL_FUNC) is also called here, bound to its receiver's
2384// scope rules. A MISSING method = honest ERROR (rc=1), NEVER a silent undefined.
2385// (2) PLAIN CALL f(args) -- callee is an IDENT/closure resolving to a VAL_FUNC (or a
2386// VAL_NATIVE value, e.g. `var m = Math.max; m(...)`, with `this` = undefined).
2387// super(args) -- inside an `extends` class constructor: call the PARENT constructor with the
2388// SAME `this` (the instance under construction), so inherited fields get initialized on it.
2389// The `@super` binding (parent ctor VAL_FUNC) was installed by ND_CLASS in the class-body env;
2390// `this` is bound in the current call frame. super() itself evaluates to undefined.
2391func js_eval_super_ctor(ctx: *i64, idx: i64, env: *i64, genv: *i64, out: *i64) -> i64 {
2392 let sv: *i64 = ev_cell()
2393 if env_get(env, jp_src(ctx), SUPER_NS, 5, sv) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 } // super outside an extends class
2394 if sv[0] != VAL_FUNC { ev_set(out, VAL_UNDEF, 0); return 1 }
2395 let thisv: *i64 = ev_cell()
2396 if env_get(env, jp_src(ctx), THIS_NS, 4, thisv) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2397 let r: i64 = js_call_userfn(ctx, idx, env, genv, (sv[1]) as *i64, thisv, out)
2398 if r == 1 { return 1 }
2399 ev_set(out, VAL_UNDEF, 0)
2400 return 0
2401}
2402// super.method(args) -- invoke the PARENT prototype's `method` with the CURRENT `this`, so an
2403// overriding B.method can delegate up to A.method on the same instance. Resolves the method off
2404// the parent ctor's .prototype (walking its chain for grand-parent methods).
2405func js_eval_super_method(ctx: *i64, idx: i64, callee: i64, env: *i64, genv: *i64, out: *i64) -> i64 {
2406 let sv: *i64 = ev_cell()
2407 if env_get(env, jp_src(ctx), SUPER_NS, 5, sv) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2408 if sv[0] != VAL_FUNC { ev_set(out, VAL_UNDEF, 0); return 1 }
2409 let proto: *i64 = (func_prototype(sv[1])) as *i64
2410 let key: *i64 = ev_prop_key(ctx, jp_nb(ctx, callee))
2411 let mv: *i64 = ev_cell()
2412 var found: i64 = obj_get(proto, key, mv)
2413 if found == 0 { found = obj_proto_lookup(proto, key, mv) } // method defined on a grand-parent prototype
2414 if found == 0 { ev_set(out, VAL_UNDEF, 0); return 1 }
2415 if mv[0] != VAL_FUNC { ev_set(out, VAL_UNDEF, 0); return 1 }
2416 let thisv: *i64 = ev_cell()
2417 if env_get(env, jp_src(ctx), THIS_NS, 4, thisv) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2418 return js_call_userfn(ctx, idx, env, genv, (mv[1]) as *i64, thisv, out)
2419}
2420// Function.prototype.call / .apply: re-dispatch the receiver function with an EXPLICIT `this` (arg0) and
2421// arguments (call: arg1.. ; apply: the elements of arg1's array). fnclos = the receiver VAL_FUNC's closure
2422// record. Both are pervasive in real JS (inheritance via superConstructor.call, arguments-forwarding).
2423func js_fn_call_apply(ctx: *i64, idx: i64, env: *i64, genv: *i64, fnclos: *i64, is_apply: i64, out: *i64) -> i64 {
2424 let argbuf: *i64 = sys_mmap(8 * 2 * 256) as *i64
2425 let n: i64 = js_eval_args(ctx, idx, env, genv, argbuf)
2426 if n < 0 { ev_set(out, VAL_UNDEF, 0); return 1 }
2427 let thisv: *i64 = ev_cell()
2428 if n >= 1 { thisv[0] = ja_t(argbuf, 0); thisv[1] = ja_p(argbuf, 0) } else { ev_set(thisv, VAL_UNDEF, 0) }
2429 // SIZE the spread buffer to the ACTUAL arg count. fn.apply(this, arr) with a large arr must not
2430 // overflow a fixed 256-slot argbuf: jQuery's Sizzle does push.apply(results, getElementsByTagName(...))
2431 // spreading HUNDREDS of nodes -> a 256-cap silently wrote past the mmap page, corrupting the count so
2432 // $('div') on a 292-div page collapsed to 0. cap2 = spread length (apply) or n (call), + slack.
2433 var cap2: i64 = 4
2434 if is_apply == 1 { if n >= 2 { if ja_t(argbuf, 1) == VAL_ARRAY { cap2 = arr_len((ja_p(argbuf, 1)) as *i64) + 4 } } } else { cap2 = n + 4 }
2435 let argbuf2: *i64 = sys_mmap(8 * 2 * cap2) as *i64
2436 var argc2: i64 = 0
2437 if is_apply == 1 {
2438 if n >= 2 { if ja_t(argbuf, 1) == VAL_ARRAY {
2439 let arr: *i64 = (ja_p(argbuf, 1)) as *i64
2440 let al: i64 = arr_len(arr)
2441 var i: i64 = 0
2442 while i < al {
2443 let eb: *i64 = ev_cell()
2444 if arr_get(arr, i, eb) == 0 { ev_set(eb, VAL_UNDEF, 0) }
2445 argbuf2[argc2 * 2] = eb[0]; argbuf2[argc2 * 2 + 1] = eb[1]; argc2 = argc2 + 1; i = i + 1
2446 }
2447 } }
2448 } else {
2449 var i: i64 = 1
2450 while i < n { argbuf2[argc2 * 2] = ja_t(argbuf, i); argbuf2[argc2 * 2 + 1] = ja_p(argbuf, i); argc2 = argc2 + 1; i = i + 1 }
2451 }
2452 return js_call_core(ctx, fnclos, argbuf2, argc2, thisv, genv, out)
2453}
2454// Function.prototype.call / .apply on a NATIVE receiver (bid = the builtin id, e.g. BI_OBJ_TOSTRING).
2455// jQuery drives its whole type system through this: `toString.call(obj)`, `hasOwn.call(obj,key)`,
2456// `fnToString.call(Ctor)`. arg0 becomes the native's `this`; the rest are its arguments (apply spreads
2457// arg1's array). Mirrors js_fn_call_apply but re-enters js_native_apply instead of js_call_core.
2458func js_native_call_apply(ctx: *i64, idx: i64, env: *i64, genv: *i64, bid: i64, is_apply: i64, out: *i64) -> i64 {
2459 let argbuf: *i64 = sys_mmap(8 * 2 * 256) as *i64
2460 let n: i64 = js_eval_args(ctx, idx, env, genv, argbuf)
2461 if n < 0 { ev_set(out, VAL_UNDEF, 0); return 1 }
2462 let thisv: *i64 = ev_cell()
2463 if n >= 1 { thisv[0] = ja_t(argbuf, 0); thisv[1] = ja_p(argbuf, 0) } else { ev_set(thisv, VAL_UNDEF, 0) }
2464 // SIZE the spread buffer to the ACTUAL arg count (see js_fn_call_apply) -- the native path is exactly
2465 // what jQuery's push.apply(results, getElementsByTagName(...)) hits, so a fixed 256-cap here is the
2466 // one that collapsed $('div')/$('a') on large real pages. cap2 = spread length (apply) or n (call).
2467 var cap2: i64 = 4
2468 if is_apply == 1 { if n >= 2 { if ja_t(argbuf, 1) == VAL_ARRAY { cap2 = arr_len((ja_p(argbuf, 1)) as *i64) + 4 } } } else { cap2 = n + 4 }
2469 let argbuf2: *i64 = sys_mmap(8 * 2 * cap2) as *i64
2470 var argc2: i64 = 0
2471 if is_apply == 1 {
2472 if n >= 2 { if ja_t(argbuf, 1) == VAL_ARRAY {
2473 let arr: *i64 = (ja_p(argbuf, 1)) as *i64
2474 let al: i64 = arr_len(arr)
2475 var i: i64 = 0
2476 while i < al {
2477 let eb: *i64 = ev_cell()
2478 if arr_get(arr, i, eb) == 0 { ev_set(eb, VAL_UNDEF, 0) }
2479 argbuf2[argc2 * 2] = eb[0]; argbuf2[argc2 * 2 + 1] = eb[1]; argc2 = argc2 + 1; i = i + 1
2480 }
2481 } }
2482 } else {
2483 var i: i64 = 1
2484 while i < n { argbuf2[argc2 * 2] = ja_t(argbuf, i); argbuf2[argc2 * 2 + 1] = ja_p(argbuf, i); argc2 = argc2 + 1; i = i + 1 }
2485 }
2486 return js_native_apply(ctx, genv, bid, thisv, argbuf2, argc2, out)
2487}
2488func js_eval_call(ctx: *i64, idx: i64, env: *i64, genv: *i64, out: *i64) -> i64 {
2489 let callee: i64 = jp_na(ctx, idx)
2490 // OPTIONAL CALL a?.(args) (ND_CALL extra=1): if the callee is null/undefined, short-circuit to undefined
2491 // (no throw). vk: `window.CSS?.supports?.(...)` -> CSS undefined -> whole chain undefined. The callee-eval
2492 // itself short-circuits cleanly for `a?.b` optional members, so this is a safe pre-check.
2493 if jp_nextra(ctx, idx) == 1 {
2494 let ocv: *i64 = ev_cell()
2495 if js_eval(ctx, callee, env, genv, ocv) == 1 { ev_set(out, VAL_UNDEF, 0); return 0 }
2496 if ocv[0] == VAL_NULL { ev_set(out, VAL_UNDEF, 0); return 0 }
2497 if ocv[0] == VAL_UNDEF { ev_set(out, VAL_UNDEF, 0); return 0 }
2498 }
2499 // ---- (0) super calls (extends classes): super(...) chains the parent ctor onto the current
2500 // `this`; super.m(...) invokes the parent's prototype method with the current `this`. Both
2501 // resolve the `@super` binding (parent ctor) that ND_CLASS installed in the class-body env. ----
2502 if jp_nkind(ctx, callee) == ND_SUPER { return js_eval_super_ctor(ctx, idx, env, genv, out) }
2503 if jp_nkind(ctx, callee) == ND_MEMBER {
2504 if jp_nkind(ctx, jp_na(ctx, callee)) == ND_SUPER { return js_eval_super_method(ctx, idx, callee, env, genv, out) }
2505 }
2506 // ---- (1) method call: callee is a MEMBER access ----
2507 if jp_nkind(ctx, callee) == ND_MEMBER {
2508 let thisv: *i64 = ev_cell()
2509 if js_eval(ctx, jp_na(ctx, callee), env, genv, thisv) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2510 let key: *i64 = ev_prop_key(ctx, jp_nb(ctx, callee))
2511 // resolve a native method against the receiver's type.
2512 var bid: i64 = 0
2513 if thisv[0] == VAL_STR { bid = ev_native_str(key) }
2514 if thisv[0] == VAL_ARRAY { bid = ev_native_arr(key) }
2515 if thisv[0] == VAL_NUM { bid = ev_native_num(key) }
2516 if thisv[0] == VAL_FLOAT { bid = ev_native_num(key) }
2517 if thisv[0] == VAL_BOOL { bid = ev_native_num(key) }
2518 // user String.prototype / Array.prototype method (not a hardcoded builtin) -> call with this = receiver.
2519 if bid == 0 { if thisv[0] == VAL_STR { if js_strproto != 0 { let pm: *i64 = ev_cell(); if obj_get((js_strproto) as *i64, key, pm) == 1 { if pm[0] == VAL_FUNC { return js_call_userfn(ctx, idx, env, genv, (pm[1]) as *i64, thisv, out) } } } } }
2520 if bid == 0 { if thisv[0] == VAL_ARRAY { if js_arrproto != 0 { let pa: *i64 = ev_cell(); if obj_get((js_arrproto) as *i64, key, pa) == 1 { if pa[0] == VAL_FUNC { return js_call_userfn(ctx, idx, env, genv, (pa[1]) as *i64, thisv, out) } } } } }
2521 if thisv[0] == VAL_GLOBALNS { bid = ev_native_global(thisv[1], key) }
2522 if thisv[0] == VAL_PROMISE { bid = ev_native_promise(key) } // R-JS-PROMISE: p.then / p.catch
2523 if thisv[0] == VAL_RESPONSE { bid = ev_native_response(key) } // R-JS-FETCH: response.text()
2524 if thisv[0] == VAL_REGEX { bid = ev_native_regex(key) } // R-JS-REGEX: re.test(str)
2525 if thisv[0] == VAL_NATIVE { if thisv[1] == BI_STRING_CTOR { if ev_key_is(key, "fromCharCode\x00" as *u8) == 1 { bid = BI_STR_FROMCHARCODE } } } // String.fromCharCode static
2526 if thisv[0] == VAL_NATIVE { if thisv[1] == BI_ARRAY_CTOR { if ev_key_is(key, "isArray\x00" as *u8) == 1 { bid = BI_ARR_ISARRAY } } } // Array.isArray static
2527 // Function.prototype.call / .apply on a user function receiver.
2528 if thisv[0] == VAL_FUNC {
2529 if ev_key_is(key, "call\x00" as *u8) == 1 { return js_fn_call_apply(ctx, idx, env, genv, (thisv[1]) as *i64, 0, out) }
2530 if ev_key_is(key, "apply\x00" as *u8) == 1 { return js_fn_call_apply(ctx, idx, env, genv, (thisv[1]) as *i64, 1, out) }
2531 // a method on the function object itself (func_own) or inherited from Object.prototype
2532 // (e.g. `Ctor.inheritsFrom(shuper)`), called with `this` = the function.
2533 let pvf: *i64 = ev_cell()
2534 if ev_get_prop(thisv, key, pvf) == 0 {
2535 if pvf[0] == VAL_FUNC { return js_call_userfn(ctx, idx, env, genv, (pvf[1]) as *i64, thisv, out) }
2536 if pvf[0] == VAL_NATIVE { return js_invoke_native(ctx, idx, env, genv, pvf[1], thisv, out) }
2537 }
2538 }
2539 // Function.prototype.call / .apply on a NATIVE receiver (jQuery's type system: toString.call(obj),
2540 // hasOwn.call(o,k), fnToString.call(Ctor)). The native's OWN builtin id is thisv[1]; .call/.apply
2541 // re-dispatch it with arg0 as the new `this`. Other method names (.toString) resolve via ev_get_prop.
2542 if thisv[0] == VAL_NATIVE {
2543 if ev_key_is(key, "call\x00" as *u8) == 1 { return js_native_call_apply(ctx, idx, env, genv, thisv[1], 0, out) }
2544 if ev_key_is(key, "apply\x00" as *u8) == 1 { return js_native_call_apply(ctx, idx, env, genv, thisv[1], 1, out) }
2545 if bid == 0 {
2546 let pvn: *i64 = ev_cell()
2547 if ev_get_prop(thisv, key, pvn) == 0 {
2548 if pvn[0] == VAL_NATIVE { return js_invoke_native(ctx, idx, env, genv, pvn[1], thisv, out) }
2549 if pvn[0] == VAL_FUNC { return js_call_userfn(ctx, idx, env, genv, (pvn[1]) as *i64, thisv, out) }
2550 }
2551 }
2552 }
2553 // user statics on a NAMESPACE receiver (Object.extend(...)) -- win over the fixed natives.
2554 if thisv[0] == VAL_GLOBALNS {
2555 let pvg: *i64 = ev_cell()
2556 if obj_get(js_ns_statics(thisv[1]), key, pvg) == 1 {
2557 if pvg[0] == VAL_FUNC { return js_call_userfn(ctx, idx, env, genv, (pvg[1]) as *i64, thisv, out) }
2558 if pvg[0] == VAL_NATIVE { return js_invoke_native(ctx, idx, env, genv, pvg[1], thisv, out) }
2559 }
2560 }
2561 if bid != 0 { return js_invoke_native(ctx, idx, env, genv, bid, thisv, out) }
2562 // a user FUNCTION stored as an object/array property: o.f() / a.f().
2563 if thisv[0] == VAL_OBJECT {
2564 let pv: *i64 = ev_cell()
2565 if ev_get_prop(thisv, key, pv) == 0 {
2566 if pv[0] == VAL_FUNC { return js_call_userfn(ctx, idx, env, genv, (pv[1]) as *i64, thisv, out) }
2567 if pv[0] == VAL_NATIVE { return js_invoke_native(ctx, idx, env, genv, pv[1], thisv, out) }
2568 }
2569 }
2570 // missing method on the receiver -> honest ERROR (NOT silent undefined). This is the
2571 // tamper contract: `var o={}; o.nope()` and `(5).nope()` BOTH error (rc=1).
2572 if js_rt_dbg == 1 { sys_write(2, "CALL-ERR missing-method '" as *u8, 25); sys_write(2, ev_str_bytes(key), ev_str_len(key)); sys_write(2, "' recv-tag=" as *u8, 11); nx_dbg_num(thisv[0]); sys_write(2, "\n" as *u8, 1) }
2573 js_rt_mark(ctx, callee)
2574 ev_set(out, VAL_UNDEF, 0); return 1
2575 }
2576 // ---- (2) plain call: evaluate the callee to a function value ----
2577 let cv: *i64 = ev_cell()
2578 if js_eval(ctx, callee, env, genv, cv) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
2579 if cv[0] == VAL_FUNC { return js_call_userfn(ctx, idx, env, genv, (cv[1]) as *i64, 0 as *i64, out) }
2580 if cv[0] == VAL_NATIVE {
2581 let undv: *i64 = ev_cell()
2582 ev_set(undv, VAL_UNDEF, 0)
2583 return js_invoke_native(ctx, idx, env, genv, cv[1], undv, out)
2584 }
2585 if js_rt_dbg == 1 {
2586 sys_write(2, "CALL-ERR non-fn callee cv-tag=" as *u8, 30); nx_dbg_num(cv[0])
2587 if jp_nkind(ctx, callee) == ND_IDENT { sys_write(2, " name='" as *u8, 7); sys_write(2, ((jp_src(ctx) as i64) + ev_tok_start(ctx, callee)) as *u8, ev_tok_len(ctx, callee)); sys_write(2, "'" as *u8, 1) }
2588 sys_write(2, "\n" as *u8, 1)
2589 }
2590 js_rt_mark(ctx, callee)
2591 ev_set(out, VAL_UNDEF, 0); return 1 // calling a non-function -> error
2592}
2593
2594// CALL a USER function CLOSURE (clos = [fnode, defenv] record ptr). Opens a call frame
2595// parented at the closure's CAPTURED defining env (NOT the global env -- the rung-6 fix),
2596// binds params from the CALL node's args, runs the body, catches RETURN. Because the frame
2597// parents `defenv`, the body's free variables resolve up the LEXICAL chain to the enclosing
2598// function's locals (proper closures), and a top-level function's defenv IS genv (so globals
2599// + recursion still resolve). Extracted so the plain-call + property-method-call paths share
2600// one implementation. (`this` binding for user fns = OPEN; arrows would inherit it lexically.)
2601func js_call_userfn(ctx: *i64, idx: i64, env: *i64, genv: *i64, clos: *i64, thisval: *i64, out: *i64) -> i64 {
2602 // Evaluate the CALL node's AST argument expressions into a contiguous VALUE buffer (2 i64
2603 // per arg = [t,p]) -- then delegate to the shared closure-invoke core. This is the normal
2604 // call path; the array-iteration methods build their own argbuf and call js_call_core
2605 // directly with PRE-EVALUATED element/index values (the inverse direction).
2606 let argbuf: *i64 = sys_mmap(8 * 2 * 256) as *i64
2607 let n: i64 = js_eval_args(ctx, idx, env, genv, argbuf) // n = post-spread-expansion arg count
2608 if n < 0 { ev_set(out, VAL_UNDEF, 0); return 1 } // an argument expression errored
2609 let rr: i64 = js_call_core(ctx, clos, argbuf, n, thisval, genv, out)
2610 if rr == 1 { if js_rt_dbg == 1 {
2611 // gated CALL-TRACE: fires on each frame as an error unwinds -> innermost-first JS stack trace.
2612 // Anchor = the CALLEE node (exact for ident callees); excerpt shows the argument expressions.
2613 let cb2: i64 = ev_tok_start(ctx, jp_na(ctx, idx))
2614 sys_write(2, "CALL-TRACE @" as *u8, 12)
2615 nx_dbg_num(cb2)
2616 sys_write(2, " '" as *u8, 2)
2617 let sp4: *u8 = jp_src(ctx)
2618 var ti2: i64 = cb2
2619 let te2: i64 = cb2 + 56
2620 while ti2 < te2 { if sp4[ti2] == (0 as u8) { ti2 = te2 } else { sys_write(2, ((sp4 as i64) + ti2) as *u8, 1); ti2 = ti2 + 1 } }
2621 sys_write(2, "'\n" as *u8, 2)
2622 } }
2623 return rr
2624}
2625
2626// SHARED CLOSURE-INVOKE CORE (rung 7). Given a closure value (clos = [fnode, defenv] record)
2627// + an array of PRE-EVALUATED argument value-cells (argbuf, 2 i64 per arg) + argc, open a
2628// call frame parented at the closure's CAPTURED defenv (lexical scope -- rung 6), bind the
2629// params positionally from argbuf (missing args = undefined; extra args ignored), pre-hoist
2630// the body's nested function declarations, run the body, and catch RETURN. Returns 0 ok
2631// (value in out) or 1 ERROR. Used by BOTH the normal AST call (js_call_userfn) and the array
2632// iteration methods (forEach/map/filter/reduce/some/every) -- one call core, no duplication.
2633// Bind a destructuring pattern (ND_OBJ_PAT / ND_ARRAY_PAT) against a value into env. Shared by var-decls and
2634// FUNCTION PARAMS (vk: `function e({needRedirect:e})`). ND_OBJ_PAT child = ND_IDENT whose token is the source
2635// property KEY; its slot-a (jp_na) is the ALIAS binding node when renamed (`{key: alias}`), else -1 (`{x}`).
2636func js_is_pattern_kind(k: i64) -> i64 { if k == ND_ARRAY_PAT { return 1 } if k == ND_OBJ_PAT { return 1 } return 0 }
2637func js_bind_pattern(ctx: *i64, patnode: i64, vt: i64, vp: i64, env: *i64) -> i64 {
2638 let nk: i64 = jp_nkind(ctx, patnode)
2639 if nk == ND_ARRAY_PAT {
2640 let pc: i64 = jp_nb(ctx, patnode)
2641 var pi: i64 = 0
2642 while pi < pc {
2643 let nm: i64 = jp_child_at(ctx, patnode, pi)
2644 let eb: *i64 = ev_cell()
2645 ev_set(eb, VAL_UNDEF, 0)
2646 if vt == VAL_ARRAY { arr_get((vp) as *i64, pi, eb) }
2647 if js_is_pattern_kind(jp_nkind(ctx, nm)) == 1 { js_bind_pattern(ctx, nm, eb[0], eb[1], env) } // NESTED [a,[b,c]]
2648 else { env_define(env, jp_src(ctx), ev_tok_start(ctx, nm), ev_tok_len(ctx, nm), eb[0], eb[1]) }
2649 pi = pi + 1
2650 }
2651 return 0
2652 }
2653 if nk == ND_OBJ_PAT {
2654 let pc: i64 = jp_nb(ctx, patnode)
2655 var pi: i64 = 0
2656 while pi < pc {
2657 let nm: i64 = jp_child_at(ctx, patnode, pi)
2658 let na: i64 = jp_na(ctx, nm) // alias node (-1 if bare) -- IDENT alias OR a nested pattern
2659 let eb: *i64 = ev_cell()
2660 ev_set(eb, VAL_UNDEF, 0)
2661 if vt == VAL_OBJECT {
2662 let key: *i64 = je_str_range(jp_src(ctx), ev_tok_start(ctx, nm), ev_tok_len(ctx, nm))
2663 obj_get((vp) as *i64, key, eb)
2664 }
2665 if na >= 0 {
2666 if js_is_pattern_kind(jp_nkind(ctx, na)) == 1 { js_bind_pattern(ctx, na, eb[0], eb[1], env) } // {k: [a,b]}
2667 else { env_define(env, jp_src(ctx), ev_tok_start(ctx, na), ev_tok_len(ctx, na), eb[0], eb[1]) }
2668 } else { env_define(env, jp_src(ctx), ev_tok_start(ctx, nm), ev_tok_len(ctx, nm), eb[0], eb[1]) }
2669 pi = pi + 1
2670 }
2671 return 0
2672 }
2673 return 0
2674}
2675func js_call_core(ctx: *i64, clos: *i64, argbuf: *i64, argc: i64, thisval: *i64, genv: *i64, out: *i64) -> i64 {
2676 // VM RE-ENTRY (rung 5): a bytecode-VM closure arriving via a callback native / the event
2677 // loop routes back into the bound VM. Unbound hook -> honest error (never misread the record).
2678 if clos[0] == VMF_MAGIC {
2679 if js_vm_hook_addr == 0 { ev_set(out, VAL_UNDEF, 0); return 1 }
2680 let hf: func(i64, i64, i64, i64, i64, i64) -> i64 = (js_vm_hook_addr) as func(i64, i64, i64, i64, i64, i64) -> i64
2681 return hf(js_vm_vs_addr, clos as i64, argbuf as i64, argc, thisval as i64, out as i64)
2682 }
2683 let fnode: i64 = clos_fnode(clos)
2684 let defenv: *i64 = clos_defenv(clos)
2685 let params: i64 = jp_nb(ctx, fnode)
2686 let body: i64 = jp_nc(ctx, fnode)
2687 let pcount: i64 = jp_nb(ctx, params)
2688 let src: *u8 = jp_src(ctx)
2689
2690 let callenv: *i64 = env_child(defenv)
2691 if (thisval as i64) != 0 { env_define(callenv, jp_src(ctx), THIS_NS, 4, thisval[0], thisval[1]) } // bind `this` (method/new)
2692 var i: i64 = 0
2693 while i < pcount {
2694 let pnode: i64 = jp_child_at(ctx, params, i)
2695 if jp_nextra(ctx, pnode) == 1 { // rest param ...r -> array of the remaining args
2696 let ra: *i64 = arr_new()
2697 var ri: i64 = i
2698 var rk: i64 = 0
2699 while ri < argc { arr_set(ra, rk, argbuf[ri * 2 + 0], argbuf[ri * 2 + 1]); rk = rk + 1; ri = ri + 1 }
2700 env_define(callenv, src, ev_tok_start(ctx, pnode), ev_tok_len(ctx, pnode), VAL_ARRAY, ra as i64)
2701 i = pcount
2702 } else {
2703 let pk: i64 = jp_nkind(ctx, pnode)
2704 var vt: i64 = VAL_UNDEF
2705 var vp: i64 = 0
2706 if i < argc { vt = argbuf[i * 2 + 0]; vp = argbuf[i * 2 + 1] }
2707 if pk == ND_OBJ_PAT { js_bind_pattern(ctx, pnode, vt, vp, callenv) } // destructuring param {a,b:c}
2708 else { if pk == ND_ARRAY_PAT { js_bind_pattern(ctx, pnode, vt, vp, callenv) } // [a,b]
2709 else {
2710 if vt == VAL_UNDEF {
2711 let defx: i64 = jp_na(ctx, pnode)
2712 if defx != (0 - 1) {
2713 let db: *i64 = ev_cell()
2714 if js_eval(ctx, defx, callenv, genv, db) == 0 { vt = db[0]; vp = db[1] }
2715 }
2716 }
2717 env_define(callenv, src, ev_tok_start(ctx, pnode), ev_tok_len(ctx, pnode), vt, vp)
2718 } }
2719 i = i + 1
2720 }
2721 }
2722
2723 // `arguments`: array of ALL actual args (array-like: .length + [i]), bound per call frame. The name is
2724 // keyed by BYTE CONTENT (env_name_eq), so a static literal source works as the binding key.
2725 let argsarr: *i64 = arr_new()
2726 var aai: i64 = 0
2727 while aai < argc { arr_set(argsarr, aai, argbuf[aai * 2], argbuf[aai * 2 + 1]); aai = aai + 1 }
2728 env_define(callenv, "arguments\x00" as *u8, 0, 9, VAL_ARRAY, argsarr as i64)
2729
2730 // INTRA-BODY FUNCTION HOISTING (rung 7): pre-pass over the body block's DIRECT children,
2731 // binding each ND_FUNC_DECL as a CLOSURE capturing this call frame -- mirroring the
2732 // top-level hoist in js_run_source -- so a nested `function inner(){}` is callable BEFORE
2733 // its textual position. Re-binding when the decl is reached during execution is idempotent.
2734 js_hoist_body(ctx, body, callenv)
2735
2736 let st: i64 = js_exec_block(ctx, body, callenv, genv, out)
2737 if st == CS_ERROR { ev_set(out, VAL_UNDEF, 0); return 1 }
2738 if st == CS_RETURN { return 0 } // out holds the returned value
2739 // a break/continue that escaped to the function boundary was OUTSIDE any loop ->
2740 // NAMED error (real JS: Illegal break/continue statement), never silent.
2741 if st == CS_BREAK { ev_set(out, VAL_UNDEF, 0); return 1 }
2742 if st == CS_CONTINUE { ev_set(out, VAL_UNDEF, 0); return 1 }
2743 ev_set(out, VAL_UNDEF, 0); return 0 // fell off the end -> undefined
2744}
2745
2746// INTRA-BODY HOIST pre-pass: bind each ND_FUNC_DECL among `body` block's DIRECT children as a
2747// closure capturing `callenv`, so the decl is callable before its textual position. Mirrors
2748// the top-level hoist loop in js_run_source. Idempotent: the ND_FUNC_DECL exec case re-binds
2749// the same name to the same closure when reached.
2750func js_hoist_body(ctx: *i64, body: i64, callenv: *i64) -> i64 {
2751 if jp_nkind(ctx, body) != ND_BLOCK { return 0 }
2752 let cnt: i64 = jp_nb(ctx, body)
2753 var h: i64 = 0
2754 while h < cnt {
2755 let ch: i64 = jp_child_at(ctx, body, h)
2756 if jp_nkind(ctx, ch) == ND_FUNC_DECL {
2757 let nm: i64 = jp_na(ctx, ch)
2758 if nm >= 0 {
2759 let c: *i64 = clos_new(ch, callenv)
2760 env_define(callenv, jp_src(ctx), ev_tok_start(ctx, nm), ev_tok_len(ctx, nm), VAL_FUNC, c as i64)
2761 }
2762 }
2763 h = h + 1
2764 }
2765 return 0
2766}
2767
2768// ===================== native builtin invocation (R-JS-RUNTIME) =====================
2769// Evaluate the CALL node's argument expressions into a contiguous VALUE buffer (2 i64 per
2770// arg: [t,p]) and return the count. Returns -1 on an argument eval ERROR (propagated).
2771// Evaluate a CALL/NEW node's argument expressions into a flat [t,p] buffer. A `...expr` argument
2772// (ND_SPREAD) is SPLICED: its operand is evaluated as an array and each element becomes its own
2773// positional arg -- so `f(...xs)`, `f(a, ...xs, b)`, and the synthesized derived-ctor `super(...args)`
2774// all forward correctly. RETURNS the ACTUAL (post-expansion) arg count -- callers MUST use this, not
2775// the static child count (they diverge whenever a spread is present). Capped at 256 (buffer size).
2776func js_eval_args(ctx: *i64, idx: i64, env: *i64, genv: *i64, argbuf: *i64) -> i64 {
2777 let acount: i64 = jp_nc(ctx, idx)
2778 let argstart: i64 = jp_nb(ctx, idx)
2779 let children: *i64 = jp_children(ctx)
2780 var oi: i64 = 0
2781 var i: i64 = 0
2782 while i < acount {
2783 let anode: i64 = children[argstart + i]
2784 if jp_nkind(ctx, anode) == ND_SPREAD {
2785 let sb: *i64 = ev_cell()
2786 if js_eval(ctx, jp_na(ctx, anode), env, genv, sb) == 1 { return 0 - 1 }
2787 if sb[0] == VAL_ARRAY {
2788 let src2: *i64 = (sb[1]) as *i64
2789 let slen: i64 = arr_len(src2)
2790 var si: i64 = 0
2791 while si < slen {
2792 if oi < 256 {
2793 let eb: *i64 = ev_cell()
2794 arr_get(src2, si, eb)
2795 argbuf[oi * 2 + 0] = eb[0]
2796 argbuf[oi * 2 + 1] = eb[1]
2797 oi = oi + 1
2798 }
2799 si = si + 1
2800 }
2801 }
2802 } else {
2803 let av: *i64 = ev_cell()
2804 if js_eval(ctx, anode, env, genv, av) == 1 { return 0 - 1 }
2805 if oi < 256 { argbuf[oi * 2 + 0] = av[0]; argbuf[oi * 2 + 1] = av[1]; oi = oi + 1 }
2806 }
2807 i = i + 1
2808 }
2809 return oi
2810}
2811// accessor for arg k's [t,p] cell inside an evaluated arg buffer.
2812func ja_t(argbuf: *i64, k: i64) -> i64 { return argbuf[k * 2 + 0] }
2813func ja_p(argbuf: *i64, k: i64) -> i64 { return argbuf[k * 2 + 1] }
2814
2815// classify a builtin id: does it REQUIRE a string receiver (`this` = a VAL_STR)?
2816// Named against the BI_STR_* constants (not a magic range) so adding a string method
2817// here keeps the receiver guard correct.
2818func ev_bid_needs_str(bid: i64) -> i64 {
2819 if bid == BI_STR_CHARAT { return 1 }
2820 if bid == BI_STR_INDEXOF { return 1 }
2821 if bid == BI_STR_SLICE { return 1 }
2822 if bid == BI_STR_UPPER { return 1 }
2823 if bid == BI_STR_LOWER { return 1 }
2824 if bid == BI_STR_INCLUDES { return 1 }
2825 if bid == BI_STR_CHARCODEAT { return 1 }
2826 if bid == BI_STR_SUBSTRING { return 1 }
2827 if bid == BI_STR_SUBSTR { return 1 }
2828 if bid == BI_STR_CONCAT { return 1 }
2829 return 0
2830}
2831// does this builtin REQUIRE an array receiver (`this` = a VAL_ARRAY)?
2832func ev_bid_needs_arr(bid: i64) -> i64 {
2833 // BI_ARR_PUSH removed: bi_arr_push now writes a generic array-LIKE receiver (jQuery pushStack collections),
2834 // so push.apply(collection, nodes) -- Sizzle's result accumulation in .find() -- is no longer VAL_ARRAY-gated.
2835 if bid == BI_ARR_POP { return 1 }
2836 // BI_ARR_INDEXOF removed: bi_arr_indexof now reads a generic array-LIKE receiver (jQuery collections,
2837 // NodeLists, arguments), so indexOf.call(arrayLikeObject, x) must NOT be gated to VAL_ARRAY -- jQuery's
2838 // inArray = indexOf.call(collection, elem) is the hot path this unblocks.
2839 if bid == BI_ARR_JOIN { return 1 }
2840 // BI_ARR_SLICE removed: bi_arr_slice now reads an array-LIKE receiver -- jQuery's .get()=slice.call(this)
2841 // and .slice()=slice.apply(this,args) borrow Array.prototype.slice on the array-like jQuery collection.
2842 if bid == BI_ARR_REVERSE { return 1 }
2843 if bid == BI_ARR_SPLICE { return 1 }
2844 if bid == BI_ARR_SORT { return 1 }
2845 if bid == BI_ARR_SHIFT { return 1 }
2846 if bid == BI_ARR_UNSHIFT { return 1 }
2847 if bid == BI_ARR_CONCAT { return 1 }
2848 if bid == BI_ARR_FOREACH { return 1 }
2849 if bid == BI_ARR_MAP { return 1 }
2850 if bid == BI_ARR_FILTER { return 1 }
2851 if bid == BI_ARR_REDUCE { return 1 }
2852 if bid == BI_ARR_SOME { return 1 }
2853 if bid == BI_ARR_EVERY { return 1 }
2854 return 0
2855}
2856// INVOKE a native builtin `bid` with receiver `thisv` and the CALL node's evaluated args.
2857// Returns 0 ok (result in out) or 1 ERROR. This is the single dispatch hub: every starter
2858// builtin routes through here so the method-call path and the value-call path agree.
2859// ================= R-JS-EVENTLOOP + R-JS-PROMISE: event loop + promises =================
2860// genv[5] (lazy). MACRO queue = raw closure ptrs (setTimeout/rAF). MICRO queue = TYPED JOBS (kind0 = a
2861// plain queueMicrotask closure; kind1 = a promise reaction). The DRAIN (js_run_source_doc, after top-level):
2862// all microtasks, then EACH macrotask followed by a full microtask drain -- micro-before-next-macro BY
2863// CONSTRUCTION. Promises schedule .then reactions as microtasks, so promise order rides the same law.
2864func el_state(genv: *i64) -> *i64 {
2865 if genv[5] == 0 {
2866 let s: *i64 = sys_mmap(8 * (PEND_BASE + 1 + PEND_MAX * PEND_ENT)) as *i64
2867 s[0] = 0; s[1] = 0; s[2] = 0; s[3] = 0; s[PEND_BASE] = 0 // +pending count
2868 genv[5] = s as i64
2869 }
2870 return (genv[5]) as *i64
2871}
2872func el_push_job(genv: *i64, job: *i64) -> i64 { let s: *i64 = el_state(genv); if s[1] >= EL_MAXQ { return 1 } s[EL_HDR + s[1]] = job as i64; s[1] = s[1] + 1; return 0 }
2873// queueMicrotask(fn): wrap the closure in a kind-0 job.
2874func el_push_micro(genv: *i64, clos: i64) -> i64 { let j: *i64 = sys_mmap(8 * JOB_SZ) as *i64; j[0] = 0; j[1] = clos; return el_push_job(genv, j) }
2875func el_push_macro(genv: *i64, clos: i64) -> i64 { let s: *i64 = el_state(genv); if s[3] >= EL_MAXQ { return 1 } s[EL_HDR + EL_MAXQ + s[3]] = clos; s[3] = s[3] + 1; return 0 }
2876// schedule a promise REACTION as a microtask (kind-1 job). ptstate = the settled state (1 fulfil/2 reject),
2877// used for the no-handler pass-through case.
2878func el_push_reaction(genv: *i64, htag: i64, hpay: i64, vtag: i64, vpay: i64, resultprom: *i64, ptstate: i64) -> i64 {
2879 let j: *i64 = sys_mmap(8 * JOB_SZ) as *i64
2880 j[0] = 1; j[1] = htag; j[2] = hpay; j[3] = vtag; j[4] = vpay; j[5] = resultprom as i64; j[6] = ptstate
2881 return el_push_job(genv, j)
2882}
2883func prom_new() -> *i64 { let p: *i64 = sys_mmap(8 * (PROM_HDR + PROM_MAXR * PROM_REACT)) as *i64; p[0] = 0; p[1] = 0; p[2] = 0; p[3] = 0; return p }
2884// SETTLE a promise (idempotent): set state+value, then schedule every stored reaction as a microtask.
2885func prom_settle(ctx: *i64, genv: *i64, p: *i64, state: i64, vtag: i64, vpay: i64) -> i64 {
2886 if p[0] != 0 { return 0 } // already settled -> ignore (a promise resolves once)
2887 p[0] = state; p[1] = vtag; p[2] = vpay
2888 let n: i64 = p[3]
2889 var i: i64 = 0
2890 while i < n {
2891 let b: i64 = PROM_HDR + i * PROM_REACT
2892 var htag: i64 = 0; var hpay: i64 = 0
2893 if state == 1 { htag = p[b + 0]; hpay = p[b + 1] } else { htag = p[b + 2]; hpay = p[b + 3] }
2894 el_push_reaction(genv, htag, hpay, vtag, vpay, (p[b + 4]) as *i64, state)
2895 i = i + 1
2896 }
2897 p[3] = 0
2898 return 0
2899}
2900// attach a (onF,onR)->resultprom reaction. Pending -> store; already settled -> schedule now (still async).
2901func prom_add_reaction(ctx: *i64, genv: *i64, p: *i64, onFtag: i64, onFpay: i64, onRtag: i64, onRpay: i64, resultprom: *i64) -> i64 {
2902 if p[0] == 0 {
2903 let c: i64 = p[3]
2904 if c < PROM_MAXR {
2905 let b: i64 = PROM_HDR + c * PROM_REACT
2906 p[b + 0] = onFtag; p[b + 1] = onFpay; p[b + 2] = onRtag; p[b + 3] = onRpay; p[b + 4] = resultprom as i64
2907 p[3] = c + 1
2908 }
2909 return 0
2910 }
2911 var htag: i64 = 0; var hpay: i64 = 0
2912 if p[0] == 1 { htag = onFtag; hpay = onFpay } else { htag = onRtag; hpay = onRpay }
2913 el_push_reaction(genv, htag, hpay, p[1], p[2], resultprom, p[0])
2914 return 0
2915}
2916// ADOPT: make R settle when `inner` settles (used when a .then handler RETURNS a promise). The handlers are
2917// the internal VAL_RESOLVE/VAL_REJECT markers bound to R.
2918func prom_adopt(ctx: *i64, genv: *i64, R: *i64, inner: *i64) -> i64 {
2919 return prom_add_reaction(ctx, genv, inner, VAL_RESOLVE, R as i64, VAL_REJECT, R as i64, (0 as *i64))
2920}
2921// resolve R with a value: a promise value -> ADOPT it; anything else -> fulfil R with it.
2922func prom_resolve_value(ctx: *i64, genv: *i64, R: *i64, tag: i64, pay: i64) -> i64 {
2923 if tag == VAL_PROMISE { return prom_adopt(ctx, genv, R, (pay) as *i64) }
2924 return prom_settle(ctx, genv, R, 1, tag, pay)
2925}
2926// RUN one reaction microtask: apply the handler to the value, then settle the result promise.
2927func prom_run_reaction(ctx: *i64, genv: *i64, htag: i64, hpay: i64, vtag: i64, vpay: i64, resultprom: *i64, ptstate: i64) -> i64 {
2928 if htag == VAL_FUNC {
2929 let ab: *i64 = sys_mmap(8 * 2 * 2) as *i64
2930 ab[0] = vtag; ab[1] = vpay
2931 let res: *i64 = ev_cell()
2932 let undv: *i64 = ev_cell(); ev_set(undv, VAL_UNDEF, 0)
2933 let rc: i64 = js_call_core(ctx, (hpay) as *i64, ab, 1, undv, genv, res)
2934 if rc == 0 { prom_resolve_value(ctx, genv, resultprom, res[0], res[1]) }
2935 else { prom_settle(ctx, genv, resultprom, 2, VAL_UNDEF, 0) } // a throw in the handler -> reject (reason=undefined; named)
2936 return 0
2937 }
2938 if htag == VAL_RESOLVE { return prom_resolve_value(ctx, genv, (hpay) as *i64, vtag, vpay) }
2939 if htag == VAL_REJECT { return prom_settle(ctx, genv, (hpay) as *i64, 2, vtag, vpay) }
2940 // no handler (undefined) -> PASS THROUGH the settled value/state to the result promise.
2941 if ptstate == 1 { return prom_resolve_value(ctx, genv, resultprom, vtag, vpay) }
2942 return prom_settle(ctx, genv, resultprom, 2, vtag, vpay)
2943}
2944// run every queued MICROtask to exhaustion (both plain closures and promise reactions; a microtask may
2945// enqueue more -- real JS drains them all before the next macrotask).
2946func el_drain_micro(ctx: *i64, genv: *i64) -> i64 {
2947 let s: *i64 = el_state(genv)
2948 let ab: *i64 = sys_mmap(8 * 2 * 2) as *i64
2949 let tmp: *i64 = ev_cell()
2950 let undv: *i64 = ev_cell(); ev_set(undv, VAL_UNDEF, 0)
2951 var guard: i64 = 0
2952 while s[0] < s[1] {
2953 if guard > 2000000 { return 1 } // runaway-microtask backstop
2954 let job: *i64 = (s[EL_HDR + s[0]]) as *i64; s[0] = s[0] + 1
2955 if js_rt_dbg == 2 { if job[0] == 0 { sys_write(2, "MICRO fnode@" as *u8, 12); nx_dbg_num(ev_tok_start(ctx, clos_fnode((job[1]) as *i64))); sys_write(2, "\n" as *u8, 1) } }
2956 if job[0] == 0 { js_call_core(ctx, (job[1]) as *i64, ab, 0, undv, genv, tmp) }
2957 else { prom_run_reaction(ctx, genv, job[1], job[2], job[3], job[4], (job[5]) as *i64, job[6]) }
2958 guard = guard + 1
2959 }
2960 return 0
2961}
2962// the full event-loop drain: microtasks, then each macrotask followed by a microtask drain.
2963func el_drain(ctx: *i64, genv: *i64) -> i64 {
2964 if genv[5] == 0 { return 0 } // event loop never touched -> nothing to do (sync scripts unaffected)
2965 let s: *i64 = el_state(genv)
2966 let ab: *i64 = sys_mmap(8 * 2 * 2) as *i64
2967 let tmp: *i64 = ev_cell()
2968 let undv: *i64 = ev_cell(); ev_set(undv, VAL_UNDEF, 0)
2969 el_drain_micro(ctx, genv)
2970 var guard: i64 = 0
2971 while s[2] < s[3] {
2972 if guard > 2000000 { return 1 } // runaway-macrotask backstop
2973 let clos: i64 = s[EL_HDR + EL_MAXQ + s[2]]; s[2] = s[2] + 1
2974 if js_rt_dbg == 2 { sys_write(2, "MACRO fnode@" as *u8, 12); nx_dbg_num(ev_tok_start(ctx, clos_fnode((clos as *i64)))); sys_write(2, "\n" as *u8, 1) }
2975 js_call_core(ctx, (clos as *i64), ab, 0, undv, genv, tmp)
2976 el_drain_micro(ctx, genv)
2977 guard = guard + 1
2978 }
2979 return 0
2980}
2981// Promise.resolve(v): a promise value is returned as-is (idempotent); anything else -> a fulfilled promise.
2982func bi_prom_resolve(argbuf: *i64, argc: i64, out: *i64) -> i64 {
2983 if argc >= 1 { if argbuf[0] == VAL_PROMISE { ev_set(out, VAL_PROMISE, argbuf[1]); return 0 } }
2984 let p: *i64 = prom_new(); p[0] = 1
2985 if argc >= 1 { p[1] = argbuf[0]; p[2] = argbuf[1] } else { p[1] = VAL_UNDEF; p[2] = 0 }
2986 ev_set(out, VAL_PROMISE, p as i64); return 0
2987}
2988func bi_prom_reject(argbuf: *i64, argc: i64, out: *i64) -> i64 {
2989 let p: *i64 = prom_new(); p[0] = 2
2990 if argc >= 1 { p[1] = argbuf[0]; p[2] = argbuf[1] } else { p[1] = VAL_UNDEF; p[2] = 0 }
2991 ev_set(out, VAL_PROMISE, p as i64); return 0
2992}
2993// p.then(onF, onR) / p.catch(onR): attach handlers, return a NEW promise resolved by the handler's return.
2994func bi_prom_then(ctx: *i64, genv: *i64, thisv: *i64, argbuf: *i64, argc: i64, iscatch: i64, out: *i64) -> i64 {
2995 if thisv[0] != VAL_PROMISE { ev_set(out, VAL_UNDEF, 0); return 1 }
2996 let p: *i64 = (thisv[1]) as *i64
2997 var onFt: i64 = VAL_UNDEF; var onFp: i64 = 0
2998 var onRt: i64 = VAL_UNDEF; var onRp: i64 = 0
2999 if iscatch == 1 { if argc >= 1 { onRt = argbuf[0]; onRp = argbuf[1] } }
3000 else {
3001 if argc >= 1 { onFt = argbuf[0]; onFp = argbuf[1] }
3002 if argc >= 2 { onRt = argbuf[2]; onRp = argbuf[3] }
3003 }
3004 let R: *i64 = prom_new()
3005 prom_add_reaction(ctx, genv, p, onFt, onFp, onRt, onRp, R)
3006 ev_set(out, VAL_PROMISE, R as i64); return 0
3007}
3008// method resolver for a promise receiver (mirrors ev_native_str/arr).
3009func ev_native_promise(key: *i64) -> i64 {
3010 if ev_key_is(key, "then\x00" as *u8) == 1 { return BI_PROM_THEN }
3011 if ev_key_is(key, "catch\x00" as *u8) == 1 { return BI_PROM_CATCH }
3012 return 0
3013}
3014
3015// ================= R-JS-FETCH: fetch() + Response =================
3016// SETTLE a FRESH promise (no reactions yet -> just set state+value; no scheduling). Used by fetch/text
3017// which create a promise and immediately settle it.
3018func prom_settle_fresh(p: *i64, state: i64, vtag: i64, vpay: i64) -> i64 { p[0] = state; p[1] = vtag; p[2] = vpay; return 0 }
3019func resp_new(status: i64, btag: i64, bpay: i64) -> *i64 { let r: *i64 = sys_mmap(8 * RESP_HDR) as *i64; r[0] = status; r[1] = btag; r[2] = bpay; return r }
3020func js_str_from_bytes(b: *u8, n: i64) -> *i64 { let rec: *i64 = ev_str_new(n); let dst: *u8 = ev_str_bytes(rec); var i: i64 = 0; while i < n { dst[i] = b[i]; i = i + 1 } return rec }
3021// fetch(url) -> Promise<Response>. A data: URL ("data:...,<body>") resolves inline to a 200 Response; any
3022// other URL -> a REJECTED promise (the engine stays TLS-free; rung 5's consumer drives real network).
3023func bi_fetch(genv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
3024 let p: *i64 = prom_new()
3025 if argc < 1 { prom_settle_fresh(p, 2, VAL_UNDEF, 0); ev_set(out, VAL_PROMISE, p as i64); return 0 }
3026 if argbuf[0] != VAL_STR { prom_settle_fresh(p, 2, VAL_UNDEF, 0); ev_set(out, VAL_PROMISE, p as i64); return 0 }
3027 let urlrec: *i64 = (argbuf[1]) as *i64
3028 let ub: *u8 = ev_str_bytes(urlrec)
3029 let ul: i64 = ev_str_len(urlrec)
3030 if ul >= 6 {
3031 if (ub[0] & 0xff) == 100 { if (ub[1] & 0xff) == 97 { if (ub[2] & 0xff) == 116 { if (ub[3] & 0xff) == 97 { if (ub[4] & 0xff) == 58 {
3032 var ci: i64 = 5
3033 var found: i64 = 0
3034 while found == 0 { if ci >= ul { found = 2 } else { if (ub[ci] & 0xff) == 44 { found = 1 } else { ci = ci + 1 } } }
3035 if found == 1 {
3036 let bstart: i64 = ci + 1
3037 let blen: i64 = ul - bstart
3038 let brec: *i64 = js_str_from_bytes(((ub as i64) + bstart) as *u8, blen)
3039 let r: *i64 = resp_new(200, VAL_STR, brec as i64)
3040 prom_settle_fresh(p, 1, VAL_RESPONSE, r as i64)
3041 ev_set(out, VAL_PROMISE, p as i64); return 0
3042 }
3043 } } } } }
3044 }
3045 // non-data URL: RECORD as PENDING -> the consumer (headless render) services it over sovereign TLS and
3046 // re-drains. Returns a PENDING promise (state 0); .then reactions attach and fire once serviced.
3047 pend_record(genv, urlrec, p)
3048 ev_set(out, VAL_PROMISE, p as i64); return 0
3049}
3050// response.text() -> Promise<string> (the body). (.json() = a named follow-on: JSON.parse(await r.text()).)
3051func bi_resp_text(thisv: *i64, out: *i64) -> i64 {
3052 if thisv[0] != VAL_RESPONSE { ev_set(out, VAL_UNDEF, 0); return 1 }
3053 let r: *i64 = (thisv[1]) as *i64
3054 let p: *i64 = prom_new()
3055 prom_settle_fresh(p, 1, r[1], r[2])
3056 ev_set(out, VAL_PROMISE, p as i64); return 0
3057}
3058// response.json() -> Promise<parsed>. Reuses the sovereign JSON parser (bi_json_parse; forward-ref OK).
3059func bi_resp_json(thisv: *i64, out: *i64) -> i64 {
3060 if thisv[0] != VAL_RESPONSE { ev_set(out, VAL_UNDEF, 0); return 1 }
3061 let r: *i64 = (thisv[1]) as *i64
3062 let p: *i64 = prom_new()
3063 let ab: *i64 = sys_mmap(8 * 2 * 2) as *i64
3064 ab[0] = r[1]; ab[1] = r[2] // the body string as JSON.parse's arg0
3065 let parsed: *i64 = ev_cell()
3066 if bi_json_parse(ab, 1, parsed) == 0 { prom_settle_fresh(p, 1, parsed[0], parsed[1]) } else { prom_settle_fresh(p, 2, VAL_UNDEF, 0) }
3067 ev_set(out, VAL_PROMISE, p as i64); return 0
3068}
3069func ev_native_response(key: *i64) -> i64 {
3070 if ev_key_is(key, "text\x00" as *u8) == 1 { return BI_RESP_TEXT }
3071 if ev_key_is(key, "json\x00" as *u8) == 1 { return BI_RESP_JSON }
3072 return 0
3073}
3074
3075// ================= R-JS-XHR: XMLHttpRequest =================
3076// An XHR is a VAL_OBJECT: open/send/etc are VAL_NATIVE props (the object-method path dispatches them);
3077// status/readyState/responseText/onload are plain props. send() resolves a data: URL inline + fires
3078// onreadystatechange/onload as MICROtasks (async, like a real XHR).
3079func bi_xhr_new(out: *i64) -> i64 {
3080 let o: *i64 = obj_new()
3081 obj_set(o, ev_cstr("open\x00" as *u8), VAL_NATIVE, BI_XHR_OPEN)
3082 obj_set(o, ev_cstr("send\x00" as *u8), VAL_NATIVE, BI_XHR_SEND)
3083 obj_set(o, ev_cstr("setRequestHeader\x00" as *u8), VAL_NATIVE, BI_XHR_NOOP)
3084 obj_set(o, ev_cstr("getResponseHeader\x00" as *u8), VAL_NATIVE, BI_XHR_NOOP)
3085 obj_set(o, ev_cstr("abort\x00" as *u8), VAL_NATIVE, BI_XHR_NOOP)
3086 obj_set(o, ev_cstr("status\x00" as *u8), VAL_NUM, 0)
3087 obj_set(o, ev_cstr("readyState\x00" as *u8), VAL_NUM, 0)
3088 obj_set(o, ev_cstr("responseText\x00" as *u8), VAL_STR, (ev_cstr("\x00" as *u8)) as i64)
3089 ev_set(out, VAL_OBJECT, o as i64); return 0
3090}
3091// `new <native>()` -> construct (currently XHR). These constructors take no args.
3092func js_construct_native(bid: i64, out: *i64) -> i64 {
3093 if bid == BI_XHR_NEW { return bi_xhr_new(out) }
3094 ev_set(out, VAL_UNDEF, 0); return 1
3095}
3096// native constructor WITH evaluated args (argbuf: [tag,pay] pairs). new Array(n)=length-n array of
3097// undefined; new Array(a,b,..)=elements; Array()/new Array()=empty. Falls back to the arg-less form.
3098// Error-object core shared by Error/TypeError/RangeError (the caller stamps .name): a plain object with
3099// .message = ToString(arg) when present+non-undefined, else "" (V8-exact). 2 data args by design -- the
3100// 3-data-arg helper-call miscompile (see gotchas) forbids threading the name through here.
3101func bi_err_build(argbuf: *i64, argc: i64) -> *i64 {
3102 let o: *i64 = obj_new()
3103 var mrec: i64 = 0
3104 var have: i64 = 0
3105 if argc >= 1 { if argbuf[0] != VAL_UNDEF {
3106 if argbuf[0] == VAL_STR { mrec = argbuf[1] } else {
3107 let tb: *i64 = ev_cell()
3108 tb[0] = argbuf[0]
3109 tb[1] = argbuf[1]
3110 mrec = (ev_coerce_str(tb)) as i64
3111 }
3112 have = 1
3113 } }
3114 if have == 0 { mrec = (ev_cstr("\x00" as *u8)) as i64 } // explicit-NUL literal = reliable empty string (gotcha: bare "" aliases)
3115 obj_set(o, ev_cstr("message\x00" as *u8), VAL_STR, mrec)
3116 return o
3117}
3118func js_construct_native_args(bid: i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
3119 if bid == BI_ARRAY_CTOR {
3120 let a: *i64 = arr_new()
3121 if argc == 1 { if argbuf[0] == VAL_NUM {
3122 let n: i64 = argbuf[1]
3123 if n < 0 { ev_set(out, VAL_UNDEF, 0); return 1 } // RangeError in real JS
3124 var i: i64 = 0
3125 while i < n { arr_set(a, i, VAL_UNDEF, 0); i = i + 1 }
3126 ev_set(out, VAL_ARRAY, a as i64); return 0
3127 } }
3128 var j: i64 = 0
3129 while j < argc { arr_set(a, j, argbuf[j * 2], argbuf[j * 2 + 1]); j = j + 1 }
3130 ev_set(out, VAL_ARRAY, a as i64); return 0
3131 }
3132 // Error family: { message, name }. Distinct bids per subtype so .name matches V8. message follows the
3133 // spec: present+non-undefined -> ToString(arg); absent/undefined -> "" (V8: new Error().message === "").
3134 if bid == BI_ERROR_CTOR {
3135 let o: *i64 = bi_err_build(argbuf, argc)
3136 obj_set(o, ev_cstr("name\x00" as *u8), VAL_STR, (ev_cstr("Error\x00" as *u8)) as i64)
3137 ev_set(out, VAL_OBJECT, o as i64); return 0
3138 }
3139 if bid == BI_TYPEERR_CTOR {
3140 let o: *i64 = bi_err_build(argbuf, argc)
3141 obj_set(o, ev_cstr("name\x00" as *u8), VAL_STR, (ev_cstr("TypeError\x00" as *u8)) as i64)
3142 ev_set(out, VAL_OBJECT, o as i64); return 0
3143 }
3144 if bid == BI_RANGEERR_CTOR {
3145 let o: *i64 = bi_err_build(argbuf, argc)
3146 obj_set(o, ev_cstr("name\x00" as *u8), VAL_STR, (ev_cstr("RangeError\x00" as *u8)) as i64)
3147 ev_set(out, VAL_OBJECT, o as i64); return 0
3148 }
3149 if bid == BI_REGEX_CTOR { return bi_regex_ctor(argbuf, argc, out) } // new RegExp(pat,flags)
3150 return js_construct_native(bid, out)
3151}
3152func bi_xhr_open(thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
3153 if thisv[0] == VAL_OBJECT {
3154 let o: *i64 = (thisv[1]) as *i64
3155 if argc >= 2 { obj_set(o, ev_cstr("@xurl\x00" as *u8), argbuf[2], argbuf[3]) } // arg1 = url
3156 obj_set(o, ev_cstr("readyState\x00" as *u8), VAL_NUM, 1)
3157 }
3158 ev_set(out, VAL_UNDEF, 0); return 0
3159}
3160func bi_xhr_send(ctx: *i64, genv: *i64, thisv: *i64, out: *i64) -> i64 {
3161 if thisv[0] != VAL_OBJECT { ev_set(out, VAL_UNDEF, 0); return 0 }
3162 let o: *i64 = (thisv[1]) as *i64
3163 var status: i64 = 0
3164 var bodyp: i64 = (ev_cstr("\x00" as *u8)) as i64
3165 let ub: *i64 = ev_cell()
3166 if obj_get(o, ev_cstr("@xurl\x00" as *u8), ub) == 1 {
3167 if ub[0] == VAL_STR {
3168 let urlrec: *i64 = (ub[1]) as *i64
3169 let uu: *u8 = ev_str_bytes(urlrec); let ul: i64 = ev_str_len(urlrec)
3170 if ul >= 6 { if (uu[0] & 0xff) == 100 { if (uu[1] & 0xff) == 97 { if (uu[2] & 0xff) == 116 { if (uu[3] & 0xff) == 97 { if (uu[4] & 0xff) == 58 {
3171 var ci: i64 = 5; var found: i64 = 0
3172 while found == 0 { if ci >= ul { found = 2 } else { if (uu[ci] & 0xff) == 44 { found = 1 } else { ci = ci + 1 } } }
3173 if found == 1 { let bs: i64 = ci + 1; let bl: i64 = ul - bs; bodyp = (js_str_from_bytes(((uu as i64) + bs) as *u8, bl)) as i64; status = 200 }
3174 } } } } } }
3175 }
3176 }
3177 obj_set(o, ev_cstr("status\x00" as *u8), VAL_NUM, status)
3178 obj_set(o, ev_cstr("responseText\x00" as *u8), VAL_STR, bodyp)
3179 obj_set(o, ev_cstr("readyState\x00" as *u8), VAL_NUM, 4)
3180 let cb: *i64 = ev_cell()
3181 if obj_get(o, ev_cstr("onreadystatechange\x00" as *u8), cb) == 1 { if cb[0] == VAL_FUNC { el_push_micro(genv, cb[1]) } }
3182 if obj_get(o, ev_cstr("onload\x00" as *u8), cb) == 1 { if cb[0] == VAL_FUNC { el_push_micro(genv, cb[1]) } }
3183 ev_set(out, VAL_UNDEF, 0); return 0
3184}
3185
3186// ================= R-JS-PENDING-FETCH: record + service (the real-web loop) =================
3187// record a pending request: (url string, the promise to settle when serviced). 0 ok / 1 if full.
3188func pend_record(genv: *i64, urlrec: *i64, prom: *i64) -> i64 {
3189 let s: *i64 = el_state(genv)
3190 let c: i64 = s[PEND_BASE]
3191 if c >= PEND_MAX { return 1 }
3192 let b: i64 = PEND_BASE + 1 + c * PEND_ENT
3193 s[b + 0] = urlrec as i64; s[b + 1] = prom as i64; s[b + 2] = 0
3194 s[PEND_BASE] = c + 1
3195 return 0
3196}
3197// count of UNSERVICED pending requests (the consumer loop's termination test).
3198// ⚠NULL-GENV GUARD (2026-07-27): js_render_page_pending_begin returns 1 and leaves
3199// outbox[1]=0 on a PARSE ERROR, so a consumer that ignores the rc arrives here with
3200// genv=NULL. `genv[5]` then reads address 0x28 -- the exact SIGSEGV measured against
3201// real wikipedia. A browser must NEVER crash on unparseable page JS: guard genv
3202// itself before dereferencing it, in every pending accessor.
3203func js_pending_count(genv: *i64) -> i64 {
3204 if (genv as i64) == 0 { return 0 }
3205 if genv[5] == 0 { return 0 }
3206 let s: *i64 = (genv[5]) as *i64
3207 let c: i64 = s[PEND_BASE]
3208 var i: i64 = 0; var live: i64 = 0
3209 while i < c { if s[PEND_BASE + 1 + i * PEND_ENT + 2] == 0 { live = live + 1 } i = i + 1 }
3210 return live
3211}
3212// RAW count of recorded requests (serviced + unserviced) -- the consumer iterates 0..total each round.
3213func js_pending_total(genv: *i64) -> i64 { if (genv as i64) == 0 { return 0 } if genv[5] == 0 { return 0 } let s: *i64 = (genv[5]) as *i64; return s[PEND_BASE] } // two-step cast: inline `((genv[5]) as *i64)[k]` MISCOMPILES under nx_cc (returns garbage) -- see nx_cc-gotchas
3214// is pending entry i already serviced? (skip it in the consumer loop)
3215func js_pending_serviced(genv: *i64, i: i64) -> i64 {
3216 if (genv as i64) == 0 { return 1 }
3217 if genv[5] == 0 { return 1 }
3218 let s: *i64 = (genv[5]) as *i64
3219 if i < 0 { return 1 }
3220 if i >= s[PEND_BASE] { return 1 }
3221 return s[PEND_BASE + 1 + i * PEND_ENT + 2]
3222}
3223// write the URL C-string of pending entry `i` into buf; returns its length (0 if out of range).
3224func js_pending_url(genv: *i64, i: i64, buf: *u8, cap: i64) -> i64 {
3225 if (genv as i64) == 0 { return 0 }
3226 if genv[5] == 0 { return 0 }
3227 let s: *i64 = (genv[5]) as *i64
3228 if i < 0 { return 0 }
3229 if i >= s[PEND_BASE] { return 0 }
3230 let urlrec: *i64 = (s[PEND_BASE + 1 + i * PEND_ENT + 0]) as *i64
3231 let ub: *u8 = ev_str_bytes(urlrec); let ul: i64 = ev_str_len(urlrec)
3232 var n: i64 = ul; if n > (cap - 1) { n = cap - 1 }
3233 var k: i64 = 0; while k < n { buf[k] = ub[k]; k = k + 1 } buf[n] = 0 as u8
3234 return n
3235}
3236// SERVICE pending `i`: settle its promise with Response(status, body), mark serviced. Consumer then el_drains.
3237func js_pending_service(ctx: *i64, genv: *i64, i: i64, status: i64, body: *u8, bodylen: i64) -> i64 {
3238 if genv[5] == 0 { return 1 }
3239 let s: *i64 = (genv[5]) as *i64
3240 if i < 0 { return 1 }
3241 if i >= s[PEND_BASE] { return 1 }
3242 let b: i64 = PEND_BASE + 1 + i * PEND_ENT
3243 if s[b + 2] != 0 { return 0 }
3244 let brec: *i64 = js_str_from_bytes(body, bodylen)
3245 let r: *i64 = resp_new(status, VAL_STR, brec as i64)
3246 prom_settle(ctx, genv, (s[b + 1]) as *i64, 1, VAL_RESPONSE, r as i64)
3247 s[b + 2] = 1
3248 return 0
3249}
3250
3251func js_invoke_native(ctx: *i64, idx: i64, env: *i64, genv: *i64, bid: i64, thisv: *i64, out: *i64) -> i64 {
3252 // RECEIVER-TYPE GUARD (council BLOCKER fix): the string/array bi_* dereference
3253 // thisv[1] as a record pointer. A DETACHED method value called as a plain function
3254 // (`var f='hi'.charAt; f(0)` or `var p=[1].push; p(2)`) arrives here with thisv =
3255 // [VAL_UNDEF,0], so the deref would be a NULL crash (process exit 139). Require the
3256 // receiver to MATCH the method family BEFORE any deref; a mismatch is an honest
3257 // rc=1 error (exactly like the missing-method path, KAT39), never a crash. Math/
3258 // Object/console builtins ignore `this`, so they are not guarded. (Guard kept HERE,
3259 // before arg-eval, to preserve the original error ORDER; js_native_apply re-guards
3260 // for its direct callers -- idempotent, zero behavior change on this path.)
3261 if ev_bid_needs_str(bid) == 1 { if thisv[0] != VAL_STR { ev_set(out, VAL_UNDEF, 0); return 1 } }
3262 if ev_bid_needs_arr(bid) == 1 { if thisv[0] != VAL_ARRAY { ev_set(out, VAL_UNDEF, 0); return 1 } }
3263 let argbuf: *i64 = sys_mmap(8 * 2 * 256) as *i64
3264 let argc: i64 = js_eval_args(ctx, idx, env, genv, argbuf)
3265 if argc < 0 { ev_set(out, VAL_UNDEF, 0); return 1 } // an argument errored
3266 let nr: i64 = js_native_apply(ctx, genv, bid, thisv, argbuf, argc, out)
3267 if nr == 1 { js_rt_mark(ctx, idx) } // a builtin returned an honest error (unsupported feature) -> record where
3268 return nr
3269}
3270// VALUE-BASED NATIVE CORE (bytecode-VM rung 4b seam): dispatch builtin `bid` over
3271// PRE-EVALUATED argument cells (argbuf, 2 i64 per arg) + receiver `thisv`. No AST/env
3272// access -- callable by BOTH the tree-walker (js_invoke_native evals the CALL node's args
3273// then delegates here) and the bytecode VM (whose args are already on its operand stack).
3274// ctx/genv feed the callback/DOM/event-loop builtins (bi_arr_foreach's js_call_core,
3275// bi_doc_* page reads, setTimeout's queues). WARNING: the callback-taking builtins
3276// (forEach/map/filter/reduce/some/every, setTimeout/queueMicrotask/rAF, promise then)
3277// invoke a VAL_FUNC argument via js_call_core, which expects a TREE-WALKER closure record
3278// [fnode,defenv]; passing a VM closure into them is the rung-5 wire (VM re-entry), not yet valid.
3279// NUMBER-receiver method resolver (mirrors ev_native_str/ev_native_arr; used by all three tiers'
3280// method-call dispatch for VAL_NUM/VAL_FLOAT/VAL_BOOL receivers).
3281func ev_native_num(key: *i64) -> i64 {
3282 if ev_key_is(key, "toString\x00" as *u8) == 1 { return BI_NUM_TOSTRING }
3283 return 0
3284}
3285// (n).toString([radix]): radix 10 (or float/bool receiver) -> the same string '+'-coercion produces;
3286// integer receiver with radix 2..36 -> base-N digits, lowercase, V8-exact for integers.
3287func bi_num_tostring(thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
3288 var radix: i64 = 10
3289 if argc >= 1 { if argbuf[0] == VAL_NUM { radix = argbuf[1] } }
3290 if radix < 2 { radix = 10 }
3291 if radix > 36 { radix = 10 }
3292 if radix == 10 { ev_set(out, VAL_STR, (ev_coerce_str(thisv)) as i64); return 0 }
3293 if thisv[0] != VAL_NUM { ev_set(out, VAL_STR, (ev_coerce_str(thisv)) as i64); return 0 } // float+radix: decimal (named divergence)
3294 var m: i64 = thisv[1]
3295 var neg: i64 = 0
3296 if m < 0 { neg = 1; m = 0 - m }
3297 let tmp: *u8 = sys_mmap(80)
3298 var k: i64 = 0
3299 if m == 0 { tmp[0] = 48 as u8; k = 1 }
3300 while m > 0 {
3301 let d: i64 = m % radix
3302 var c: i64 = 48 + d
3303 if d >= 10 { c = 87 + d }
3304 tmp[k] = c as u8
3305 m = m / radix
3306 k = k + 1
3307 }
3308 var n: i64 = k
3309 if neg == 1 { n = n + 1 }
3310 let rec: *i64 = ev_str_new(n)
3311 let dst: *u8 = ev_str_bytes(rec)
3312 var w2: i64 = 0
3313 if neg == 1 { dst[0] = 45 as u8; w2 = 1 }
3314 var q: i64 = k - 1
3315 while q >= 0 { dst[w2] = tmp[q]; w2 = w2 + 1; q = q - 1 }
3316 ev_set(out, VAL_STR, rec as i64)
3317 return 0
3318}
3319func js_native_apply(ctx: *i64, genv: *i64, bid: i64, thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
3320 if ev_bid_needs_str(bid) == 1 { if thisv[0] != VAL_STR { ev_set(out, VAL_UNDEF, 0); return 1 } }
3321 if ev_bid_needs_arr(bid) == 1 { if thisv[0] != VAL_ARRAY { ev_set(out, VAL_UNDEF, 0); return 1 } }
3322 // R-JS-EVENTLOOP free functions: enqueue the callback closure (arg0 = a VAL_FUNC; argbuf[1]=clos ptr).
3323 // A non-function callback is ignored (real JS throws; foundation is lenient -> named open). setTimeout
3324 // returns a timer id (a number); queueMicrotask returns undefined; clearTimeout is a no-op.
3325 if bid == BI_SETTIMEOUT { if argc >= 1 { if argbuf[0] == VAL_FUNC { el_push_macro(genv, argbuf[1]) } } ev_set(out, VAL_NUM, 1); return 0 }
3326 if bid == BI_RAF { if argc >= 1 { if argbuf[0] == VAL_FUNC { el_push_macro(genv, argbuf[1]) } } ev_set(out, VAL_NUM, 1); return 0 }
3327 if bid == BI_QUEUEMICROTASK { if argc >= 1 { if argbuf[0] == VAL_FUNC { el_push_micro(genv, argbuf[1]) } } ev_set(out, VAL_UNDEF, 0); return 0 }
3328 if bid == BI_CLEARTIMEOUT { ev_set(out, VAL_UNDEF, 0); return 0 }
3329 // R-JS-PROMISE: Promise.resolve/reject (free) + p.then/p.catch (this = a promise).
3330 if bid == BI_PROM_RESOLVE { return bi_prom_resolve(argbuf, argc, out) }
3331 if bid == BI_PROM_REJECT { return bi_prom_reject(argbuf, argc, out) }
3332 if bid == BI_PROM_THEN { return bi_prom_then(ctx, genv, thisv, argbuf, argc, 0, out) }
3333 if bid == BI_PROM_CATCH { return bi_prom_then(ctx, genv, thisv, argbuf, argc, 1, out) }
3334 // R-JS-FETCH: fetch() (free) + response.text() (this = a Response).
3335 if bid == BI_FETCH { return bi_fetch(genv, argbuf, argc, out) }
3336 if bid == BI_RESP_TEXT { return bi_resp_text(thisv, out) }
3337 if bid == BI_RESP_JSON { return bi_resp_json(thisv, out) }
3338 if bid == BI_RE_TEST { return bi_re_test(thisv, argbuf, argc, out) } // R-JS-REGEX
3339 if bid == BI_RE_EXEC { return bi_re_exec(thisv, argbuf, argc, out) }
3340 if bid == BI_STR_SEARCH { return bi_str_search(thisv, argbuf, argc, out) }
3341 if bid == BI_STR_MATCH { return bi_str_match(thisv, argbuf, argc, out) }
3342 if bid == BI_STR_REPLACE { return bi_str_replace(thisv, argbuf, argc, out) }
3343 if bid == BI_STR_SPLIT { return bi_str_split(thisv, argbuf, argc, out) }
3344 if bid == BI_REGEX_CTOR { return bi_regex_ctor(argbuf, argc, out) } // RegExp(...) plain call
3345 if bid == BI_NUM_TOSTRING { return bi_num_tostring(thisv, argbuf, argc, out) } // (n).toString([radix])
3346 // Array/Error-family called WITHOUT `new` construct all the same (JS spec: Array(5) === new Array(5),
3347 // Error(m) === new Error(m)). Their bids are UNIQUE (111-114) so this dispatch can exist at all --
3348 // the old 101/102 bids collided with BI_XHR_OPEN/BI_XHR_SEND above and never reached construct.
3349 if bid == BI_ARRAY_CTOR { return js_construct_native_args(bid, argbuf, argc, out) }
3350 if bid == BI_ERROR_CTOR { return js_construct_native_args(bid, argbuf, argc, out) }
3351 if bid == BI_TYPEERR_CTOR { return js_construct_native_args(bid, argbuf, argc, out) }
3352 if bid == BI_RANGEERR_CTOR { return js_construct_native_args(bid, argbuf, argc, out) }
3353 if bid == BI_BOM_NOOP { ev_set(out, VAL_UNDEF, 0); return 0 } // R-JS-BOM: location.reload/history.pushState/...
3354 if bid == BI_WIN_ADD_LISTENER { return bi_win_add_listener(genv, argbuf, argc, out) }
3355 if bid == BI_STUB_NULL { ev_set(out, VAL_NULL, 0); return 0 } // headless element-stub method -> null
3356 if bid == BI_STUB_RETARG { if argc >= 1 { ev_set(out, ja_t(argbuf, 0), ja_p(argbuf, 0)) } else { ev_set(out, VAL_UNDEF, 0) } return 0 } // -> arg0 (chainable)
3357 if bid == BI_EL_CLONE { ev_set(out, VAL_OBJECT, (je_headless_el_stub()) as i64); return 0 } // cloneNode -> fresh stub
3358 if bid == BI_STUB_EMPTYARR { ev_set(out, VAL_ARRAY, (arr_new()) as i64); return 0 }
3359 if bid == BI_STUB_FALSE { ev_set(out, VAL_BOOL, 0); return 0 }
3360 if bid == BI_DATE_NOW { ev_set(out, VAL_NUM, sys_now_ms()); return 0 } // Date.now() -> ms timestamp
3361 if bid == BI_XHR_NEW { return bi_xhr_new(out) } // R-JS-XHR
3362 if bid == BI_XHR_OPEN { return bi_xhr_open(thisv, argbuf, argc, out) }
3363 if bid == BI_XHR_SEND { return bi_xhr_send(ctx, genv, thisv, out) }
3364 if bid == BI_XHR_NOOP { ev_set(out, VAL_UNDEF, 0); return 0 }
3365 // String methods (this = a string value).
3366 if bid == BI_STR_CHARAT { return bi_str_charat(thisv, argbuf, argc, out) }
3367 if bid == BI_STR_INDEXOF { return bi_str_indexof(thisv, argbuf, argc, out) }
3368 if bid == BI_STR_SLICE { return bi_str_slice(thisv, argbuf, argc, out) }
3369 if bid == BI_STR_UPPER { return bi_str_case(thisv, 1, out) }
3370 if bid == BI_STR_LOWER { return bi_str_case(thisv, 0, out) }
3371 if bid == BI_STR_INCLUDES { return bi_str_includes(thisv, argbuf, argc, out) }
3372 // Array methods (this = an array value).
3373 if bid == BI_ARR_PUSH { return bi_arr_push(thisv, argbuf, argc, out) }
3374 if bid == BI_ARR_POP { return bi_arr_pop(thisv, out) }
3375 if bid == BI_ARR_SHIFT { return bi_arr_shift(thisv, out) }
3376 if bid == BI_ARR_UNSHIFT { return bi_arr_unshift(thisv, argbuf, argc, out) }
3377 if bid == BI_ARR_CONCAT { return bi_arr_concat(thisv, argbuf, argc, out) }
3378 if bid == BI_STR_CONCAT { return bi_str_concat_m(thisv, argbuf, argc, out) }
3379 if bid == BI_ARR_ISARRAY { var iar: i64 = 0; if argc >= 1 { if ja_t(argbuf, 0) == VAL_ARRAY { iar = 1 } } ev_set(out, VAL_BOOL, iar); return 0 }
3380 if bid == BI_ARR_INDEXOF { return bi_arr_indexof(thisv, argbuf, argc, out) }
3381 if bid == BI_ARR_JOIN { return bi_arr_join(thisv, argbuf, argc, out) }
3382 if bid == BI_ARR_SLICE { return bi_arr_slice(thisv, argbuf, argc, out) }
3383 if bid == BI_ARR_REVERSE { return bi_arr_reverse(thisv, out) }
3384 if bid == BI_ARR_SPLICE { return bi_arr_splice(thisv, argbuf, argc, out) }
3385 // Array ITERATION methods (rung 7) -- they CALL BACK into the user closure (arg0), so they
3386 // need ctx + genv to invoke js_call_core. argbuf[0] = the callback, argbuf[1] = reduce init.
3387 if bid == BI_ARR_FOREACH { return bi_arr_foreach(ctx, genv, thisv, argbuf, argc, out) }
3388 if bid == BI_ARR_MAP { return bi_arr_map(ctx, genv, thisv, argbuf, argc, out) }
3389 if bid == BI_ARR_FILTER { return bi_arr_filter(ctx, genv, thisv, argbuf, argc, out) }
3390 if bid == BI_ARR_REDUCE { return bi_arr_reduce(ctx, genv, thisv, argbuf, argc, out) }
3391 if bid == BI_ARR_SOME { return bi_arr_some(ctx, genv, thisv, argbuf, argc, 0, out) }
3392 if bid == BI_ARR_EVERY { return bi_arr_some(ctx, genv, thisv, argbuf, argc, 1, out) }
3393 if bid == BI_ARR_SORT { return bi_arr_sort(ctx, genv, thisv, argbuf, argc, out) }
3394 // Object.* (operand = arg0).
3395 if bid == BI_OBJ_KEYS { return bi_obj_keys(argbuf, argc, out) }
3396 if bid == BI_OBJ_VALUES { return bi_obj_values(argbuf, argc, out) }
3397 if bid == BI_OBJ_DEFINEPROP { return bi_obj_defineprop(argbuf, argc, out) }
3398 // Math.* (free functions over args).
3399 if bid == BI_MATH_MAX { return bi_math_maxmin(argbuf, argc, 1, out) }
3400 if bid == BI_MATH_MIN { return bi_math_maxmin(argbuf, argc, 0, out) }
3401 if bid == BI_MATH_ABS { return bi_math_abs(argbuf, argc, out) }
3402 if bid == BI_MATH_FLOOR { return bi_math_floorceil(argbuf, argc, 0, out) }
3403 if bid == BI_MATH_CEIL { return bi_math_floorceil(argbuf, argc, 1, out) }
3404 if bid == BI_MATH_POW { return bi_math_pow(argbuf, argc, out) }
3405 if bid == BI_MATH_RANDOM { return bi_math_random(out) }
3406 if bid == BI_MATH_SQRT { return bi_math_sqrt(argbuf, argc, out) }
3407 if bid == BI_STR_CHARCODEAT { return bi_str_charcodeat(thisv, argbuf, argc, out) }
3408 if bid == BI_STR_FROMCHARCODE { return bi_str_fromcharcode(argbuf, argc, out) }
3409 if bid == BI_STR_SUBSTRING { return bi_str_substring(thisv, argbuf, argc, out) }
3410 if bid == BI_STR_SUBSTR { return bi_str_substr(thisv, argbuf, argc, out) }
3411 if bid == BI_PARSEINT { return bi_parseint(argbuf, argc, out) }
3412 if bid == BI_STRING_CTOR {
3413 if argc < 1 { ev_set(out, VAL_STR, (ev_cstr("\x00" as *u8)) as i64); return 0 }
3414 ev_set(out, VAL_STR, (ev_coerce_str_arg(argbuf, 0)) as i64)
3415 return 0
3416 }
3417 // console.* (page-observability primitive).
3418 if bid == BI_CONSOLE_LOG { return bi_console_log(argbuf, argc, out) }
3419 // document.* (DOM binding) -- reads the page HTML from the global env (genv[2]/genv[3]).
3420 if bid == BI_DOC_GET_BY_ID { return bi_doc_get_by_id(genv, argbuf, argc, out) }
3421 if bid == BI_DOC_QUERY_SELECTOR { return bi_doc_query_selector(genv, argbuf, argc, out) }
3422 if bid == BI_EL_GET_ATTR { return bi_el_get_attr(genv, thisv, argbuf, argc, out) }
3423 if bid == BI_DOC_QUERY_SELECTOR_ALL { return bi_doc_query_selector_all(genv, argbuf, argc, out) }
3424 if bid == BI_DOC_CREATE_ELEMENT { return bi_doc_create_element(genv, argbuf, argc, out) }
3425 if bid == BI_DOC_CREATE_FRAGMENT { return bi_doc_create_fragment(genv, out) }
3426 if bid == BI_DOC_CREATE_TEXT { return bi_doc_create_text(genv, argbuf, argc, out) }
3427 if bid == BI_DOC_GET_BY_TAG { return bi_doc_get_by_tag_class(genv, argbuf, argc, 3, out) }
3428 if bid == BI_DOC_GET_BY_CLASS { return bi_doc_get_by_tag_class(genv, argbuf, argc, 2, out) }
3429 if bid == BI_EL_GET_BY_TAG { return bi_el_get_by_tag(thisv, argbuf, argc, out) }
3430 if bid == BI_EL_GET_BY_CLASS { return bi_el_get_by_class(thisv, argbuf, argc, out) }
3431 if bid == BI_EL_QSA { return bi_el_qsa(thisv, argbuf, argc, out) }
3432 if bid == BI_EL_QS { return bi_el_qs(thisv, argbuf, argc, out) }
3433 if bid == BI_EL_MATCHES { return bi_el_matches(thisv, argbuf, argc, out) }
3434 if bid == BI_EL_GET_ATTR_NODE { return bi_el_get_attr_node(thisv, argbuf, argc, out) }
3435 if bid == BI_EL_REMOVE_CHILD { return bi_el_remove_child(thisv, argbuf, argc, out) }
3436 if bid == BI_EL_APPEND_CHILD { return bi_el_append_child(thisv, argbuf, argc, out) }
3437 if bid == BI_EL_CLONE_TREE { return bi_el_clone_tree(thisv, out) }
3438 if bid == BI_EL_CMPDOCPOS { return bi_el_cmpdocpos(thisv, argbuf, argc, out) }
3439 if bid == BI_EL_SET_ATTR { return bi_el_set_attr(thisv, argbuf, argc, out) }
3440 if bid == BI_EL_ADD_LISTENER { return bi_el_add_listener(genv, thisv, argbuf, argc, out) } // R-JS-EVENT
3441 if bid == BI_EL_CLICK { return bi_el_click(ctx, genv, thisv, out) }
3442 if bid == BI_CLS_TOGGLE { return bi_cls_do(thisv, argbuf, argc, out, 0) } // R-JS-CLASSLIST
3443 if bid == BI_CLS_ADD { return bi_cls_do(thisv, argbuf, argc, out, 1) }
3444 if bid == BI_CLS_REMOVE { return bi_cls_do(thisv, argbuf, argc, out, 2) }
3445 if bid == BI_CLS_CONTAINS { return bi_cls_do(thisv, argbuf, argc, out, 3) }
3446 if bid == BI_JSON_PARSE { return bi_json_parse(argbuf, argc, out) }
3447 // Object.prototype methods (this = the receiver) + Object.getPrototypeOf (operand = arg0).
3448 if bid == BI_OBJ_TOSTRING { return bi_obj_tostring(thisv, out) }
3449 if bid == BI_OBJ_HASOWN { return bi_obj_hasown(thisv, argbuf, argc, out) }
3450 if bid == BI_OBJ_VALUEOF { ev_set(out, thisv[0], thisv[1]); return 0 } // valueOf on an object = identity
3451 if bid == BI_OBJ_ISPROTOTYPEOF { return bi_obj_isprototypeof(thisv, argbuf, argc, out) }
3452 if bid == BI_FN_TOSTRING { return bi_fn_tostring(thisv, out) }
3453 if bid == BI_OBJ_GETPROTO { return bi_obj_getproto(argbuf, argc, out) }
3454 if bid == BI_OBJ_CREATE { return bi_obj_create(argbuf, argc, out) }
3455 if bid == BI_OBJ_ASSIGN { return bi_obj_assign(argbuf, argc, out) }
3456 ev_set(out, VAL_UNDEF, 0); return 1 // unknown builtin id -> honest error
3457}
3458
3459// ===================== starter builtin implementations (R-JS-RUNTIME) =====================
3460// Each `bi_*` receives the receiver `thisv` (where meaningful) + an evaluated arg buffer
3461// (2 i64 per arg) + argc. It writes a VALUE to out and returns 0 ok / 1 ERROR. Integer-number
3462// world: indices/lengths are i64; non-integer coercions stay HONEST OPEN (see header).
3463
3464// clamp a (possibly negative, possibly out-of-bounds) slice endpoint into [0,len].
3465// Negative endpoints count from the end (real JS slice): idx<0 -> len+idx, then clamp.
3466func ev_clamp_idx(i: i64, len: i64) -> i64 {
3467 var n: i64 = i
3468 if n < 0 { n = len + n }
3469 if n < 0 { n = 0 }
3470 if n > len { n = len }
3471 return n
3472}
3473// byte-substring search: index in `hay` where `needle` first occurs, or -1. Empty needle = 0.
3474func ev_str_find(hay: *i64, needle: *i64) -> i64 {
3475 let hl: i64 = ev_str_len(hay)
3476 let nl: i64 = ev_str_len(needle)
3477 if nl == 0 { return 0 }
3478 if nl > hl { return 0 - 1 }
3479 let hb: *u8 = ev_str_bytes(hay)
3480 let nb: *u8 = ev_str_bytes(needle)
3481 var i: i64 = 0
3482 let last: i64 = hl - nl
3483 while i <= last {
3484 var j: i64 = 0
3485 var ok: i64 = 1
3486 while j < nl {
3487 if (hb[i + j] & 0xff) != (nb[j] & 0xff) { ok = 0; j = nl } else { j = j + 1 }
3488 }
3489 if ok == 1 { return i }
3490 i = i + 1
3491 }
3492 return 0 - 1
3493}
3494// build a string value = bytes [a,b) of record `rec` (a,b already clamped, a<=b).
3495func ev_str_substr(rec: *i64, a: i64, b: i64, out: *i64) -> i64 {
3496 var len: i64 = b - a
3497 if len < 0 { len = 0 }
3498 let nr: *i64 = ev_str_new(len)
3499 let nd: *u8 = ev_str_bytes(nr)
3500 let sb: *u8 = ev_str_bytes(rec)
3501 var i: i64 = 0
3502 while i < len { nd[i] = sb[a + i]; i = i + 1 }
3503 ev_set(out, VAL_STR, nr as i64)
3504 return 0
3505}
3506
3507// ---- String methods ----
3508func bi_str_charat(thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
3509 let rec: *i64 = (thisv[1]) as *i64
3510 var i: i64 = 0
3511 if argc >= 1 { i = ja_p(argbuf, 0) }
3512 return ev_str_char_at(rec, i, out)
3513}
3514func bi_str_indexof(thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
3515 let rec: *i64 = (thisv[1]) as *i64
3516 if argc < 1 { ev_set(out, VAL_NUM, 0 - 1); return 0 }
3517 let sub: *i64 = ev_coerce_str_arg(argbuf, 0)
3518 ev_set(out, VAL_NUM, ev_str_find(rec, sub))
3519 return 0
3520}
3521func bi_str_includes(thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
3522 let rec: *i64 = (thisv[1]) as *i64
3523 if argc < 1 { ev_set(out, VAL_BOOL, 0); return 0 }
3524 let sub: *i64 = ev_coerce_str_arg(argbuf, 0)
3525 if ev_str_find(rec, sub) >= 0 { ev_set(out, VAL_BOOL, 1); return 0 }
3526 ev_set(out, VAL_BOOL, 0); return 0
3527}
3528func bi_str_slice(thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
3529 let rec: *i64 = (thisv[1]) as *i64
3530 let len: i64 = ev_str_len(rec)
3531 var a: i64 = 0
3532 var b: i64 = len
3533 if argc >= 1 { a = ja_p(argbuf, 0) }
3534 if argc >= 2 { b = ja_p(argbuf, 1) }
3535 let ca: i64 = ev_clamp_idx(a, len)
3536 let cb: i64 = ev_clamp_idx(b, len)
3537 return ev_str_substr(rec, ca, cb, out)
3538}
3539func bi_str_case(thisv: *i64, upper: i64, out: *i64) -> i64 {
3540 let rec: *i64 = (thisv[1]) as *i64
3541 let l: i64 = ev_str_len(rec)
3542 let nr: *i64 = ev_str_new(l)
3543 let nd: *u8 = ev_str_bytes(nr)
3544 let sb: *u8 = ev_str_bytes(rec)
3545 var i: i64 = 0
3546 while i < l {
3547 var c: i64 = sb[i] & 0xff
3548 if upper == 1 { if c >= 97 { if c <= 122 { c = c - 32 } } }
3549 if upper == 0 { if c >= 65 { if c <= 90 { c = c + 32 } } }
3550 nd[i] = c as u8
3551 i = i + 1
3552 }
3553 ev_set(out, VAL_STR, nr as i64)
3554 return 0
3555}
3556// coerce arg k (any value cell in argbuf) to a string record, for string-needle args.
3557func ev_coerce_str_arg(argbuf: *i64, k: i64) -> *i64 {
3558 let tmp: *i64 = ev_cell()
3559 tmp[0] = ja_t(argbuf, k)
3560 tmp[1] = ja_p(argbuf, k)
3561 return ev_coerce_str(tmp)
3562}
3563
3564// ---- Array methods ----
3565func bi_arr_push(thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
3566 var len: i64 = ev_arraylike_len(thisv) // array OR array-like object (jQuery pushStack collection)
3567 var k: i64 = 0
3568 while k < argc {
3569 if ev_arraylike_set(thisv, len, ja_t(argbuf, k), ja_p(argbuf, k)) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
3570 len = len + 1
3571 k = k + 1
3572 }
3573 ev_set(out, VAL_NUM, len) // push returns the NEW length
3574 return 0
3575}
3576func bi_arr_pop(thisv: *i64, out: *i64) -> i64 {
3577 let a: *i64 = (thisv[1]) as *i64
3578 let len: i64 = arr_len(a)
3579 if len == 0 { ev_set(out, VAL_UNDEF, 0); return 0 } // pop of empty -> undefined
3580 let last: i64 = len - 1
3581 arr_get(a, last, out)
3582 a[0] = last // shrink length (additive history not needed for the live array model)
3583 return 0
3584}
3585// arr.shift() -- remove+return arr[0], shift the rest down one slot, length-=1 (empty -> undefined).
3586func bi_arr_shift(thisv: *i64, out: *i64) -> i64 {
3587 let a: *i64 = (thisv[1]) as *i64
3588 let len: i64 = arr_len(a)
3589 if len == 0 { ev_set(out, VAL_UNDEF, 0); return 0 }
3590 arr_get(a, 0, out) // save element 0
3591 let d: *i64 = (a[2]) as *i64
3592 var i: i64 = 0
3593 while i < len - 1 {
3594 let b: i64 = i * ARR_ENT
3595 let nb: i64 = (i + 1) * ARR_ENT
3596 d[b + 0] = d[nb + 0]; d[b + 1] = d[nb + 1]
3597 i = i + 1
3598 }
3599 a[0] = len - 1
3600 return 0
3601}
3602// arr.unshift(...items) -- prepend items (existing shift up), return the new length. Grow first via arr_set
3603// (reallocs the backing block), THEN re-read d and move existing elements BACKWARDS to avoid clobbering.
3604func bi_arr_unshift(thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
3605 let a: *i64 = (thisv[1]) as *i64
3606 let len: i64 = arr_len(a)
3607 if argc == 0 { ev_set(out, VAL_NUM, len); return 0 }
3608 let newlen: i64 = len + argc
3609 if arr_set(a, newlen - 1, VAL_UNDEF, 0) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 } // grow + length=newlen
3610 let d: *i64 = (a[2]) as *i64
3611 var i: i64 = len - 1
3612 while i >= 0 {
3613 let src: i64 = i * ARR_ENT
3614 let dst: i64 = (i + argc) * ARR_ENT
3615 d[dst + 0] = d[src + 0]; d[dst + 1] = d[src + 1]
3616 i = i - 1
3617 }
3618 var k: i64 = 0
3619 while k < argc {
3620 let b: i64 = k * ARR_ENT
3621 d[b + 0] = ja_t(argbuf, k); d[b + 1] = ja_p(argbuf, k)
3622 k = k + 1
3623 }
3624 ev_set(out, VAL_NUM, newlen)
3625 return 0
3626}
3627// arr.concat(...items) -> NEW array = this's elements then each item (ARRAY items spread one level, else pushed).
3628func bi_arr_concat(thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
3629 let r: *i64 = arr_new()
3630 var ri: i64 = 0
3631 let a: *i64 = (thisv[1]) as *i64
3632 let al: i64 = arr_len(a)
3633 let eb: *i64 = ev_cell()
3634 var i: i64 = 0
3635 while i < al { if arr_get(a, i, eb) == 0 { ev_set(eb, VAL_UNDEF, 0) } arr_set(r, ri, eb[0], eb[1]); ri = ri + 1; i = i + 1 }
3636 var ai: i64 = 0
3637 while ai < argc {
3638 if ja_t(argbuf, ai) == VAL_ARRAY {
3639 let sa: *i64 = (ja_p(argbuf, ai)) as *i64
3640 let sl: i64 = arr_len(sa)
3641 var j: i64 = 0
3642 while j < sl { if arr_get(sa, j, eb) == 0 { ev_set(eb, VAL_UNDEF, 0) } arr_set(r, ri, eb[0], eb[1]); ri = ri + 1; j = j + 1 }
3643 } else {
3644 arr_set(r, ri, ja_t(argbuf, ai), ja_p(argbuf, ai)); ri = ri + 1
3645 }
3646 ai = ai + 1
3647 }
3648 ev_set(out, VAL_ARRAY, r as i64)
3649 return 0
3650}
3651// str.concat(...args) -> this followed by each arg coerced to a string.
3652func bi_str_concat_m(thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
3653 let accb: *i64 = ev_cell()
3654 ev_set(accb, VAL_STR, (ev_coerce_str(thisv)) as i64)
3655 var i: i64 = 0
3656 while i < argc {
3657 let ab: *i64 = ev_cell()
3658 ab[0] = ja_t(argbuf, i); ab[1] = ja_p(argbuf, i)
3659 let nb: *i64 = ev_cell()
3660 ev_str_concat((accb[1]) as *i64, ev_coerce_str(ab), nb)
3661 ev_copy(accb, nb)
3662 i = i + 1
3663 }
3664 ev_copy(out, accb)
3665 return 0
3666}
3667// LENGTH of an array OR array-like object. jQuery collections / NodeLists / `arguments` borrow
3668// Array.prototype methods via .call/.apply on a non-array `this` (e.g. inArray = indexOf.call(coll,x)),
3669// so these natives must read a generic array-like (numeric-indexed object with a `.length`), not
3670// assume VAL_ARRAY -- indexOf.call(jqObject,...) previously threw.
3671func ev_arraylike_len(v: *i64) -> i64 {
3672 if v[0] == VAL_ARRAY { return arr_len((v[1]) as *i64) }
3673 if v[0] == VAL_OBJECT {
3674 let lb: *i64 = ev_cell()
3675 if obj_get((v[1]) as *i64, ev_cstr("length\x00" as *u8), lb) == 1 { if lb[0] == VAL_NUM { return lb[1] } }
3676 }
3677 return 0
3678}
3679// element i of an array OR array-like object -> out (undefined if absent).
3680func ev_arraylike_get(v: *i64, i: i64, out: *i64) -> i64 {
3681 if v[0] == VAL_ARRAY { arr_get((v[1]) as *i64, i, out); return 0 }
3682 if v[0] == VAL_OBJECT { if obj_get((v[1]) as *i64, ev_num_to_str(i), out) == 1 { return 0 } }
3683 ev_set(out, VAL_UNDEF, 0)
3684 return 0
3685}
3686// SET element i of an array OR array-like object; for an object also grow its `.length` to i+1.
3687// This is what lets Array.prototype.push.apply(jqCollection, nodes) (Sizzle's result accumulation)
3688// mutate a jQuery collection (array-like object) rather than assuming a VAL_ARRAY receiver.
3689func ev_arraylike_set(v: *i64, i: i64, vt: i64, vp: i64) -> i64 {
3690 if v[0] == VAL_ARRAY { return arr_set((v[1]) as *i64, i, vt, vp) }
3691 if v[0] == VAL_OBJECT {
3692 let o: *i64 = (v[1]) as *i64
3693 obj_set(o, ev_num_to_str(i), vt, vp)
3694 let lb: *i64 = ev_cell()
3695 var cur: i64 = 0
3696 if obj_get(o, ev_cstr("length\x00" as *u8), lb) == 1 { if lb[0] == VAL_NUM { cur = lb[1] } }
3697 if (i + 1) > cur { obj_set(o, ev_cstr("length\x00" as *u8), VAL_NUM, i + 1) }
3698 return 0
3699 }
3700 return 0 // non-array-like receiver: no-op (don't abort the whole call)
3701}
3702func bi_arr_indexof(thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
3703 if argc < 1 { ev_set(out, VAL_NUM, 0 - 1); return 0 }
3704 let needle: *i64 = ev_cell()
3705 needle[0] = ja_t(argbuf, 0)
3706 needle[1] = ja_p(argbuf, 0)
3707 let len: i64 = ev_arraylike_len(thisv) // array OR jQuery-collection/NodeList (borrowed via .call)
3708 var i: i64 = 0
3709 while i < len {
3710 let el: *i64 = ev_cell()
3711 ev_arraylike_get(thisv, i, el)
3712 if ev_strict_eq(el, needle) == 1 { ev_set(out, VAL_NUM, i); return 0 }
3713 i = i + 1
3714 }
3715 ev_set(out, VAL_NUM, 0 - 1)
3716 return 0
3717}
3718func bi_arr_join(thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
3719 let a: *i64 = (thisv[1]) as *i64
3720 let len: i64 = arr_len(a)
3721 var sep: *i64 = ev_cstr(",\x00" as *u8) // default separator is ',' (real JS)
3722 if argc >= 1 { sep = ev_coerce_str_arg(argbuf, 0) }
3723 var acc: *i64 = ev_str_new(0)
3724 var i: i64 = 0
3725 while i < len {
3726 if i > 0 { let m: *i64 = ev_cell(); ev_str_concat(acc, sep, m); acc = (m[1]) as *i64 }
3727 let el: *i64 = ev_cell()
3728 arr_get(a, i, el)
3729 // undefined/null join as empty string (real JS); other values stringify.
3730 var es: *i64 = ev_str_new(0)
3731 if el[0] != VAL_UNDEF { if el[0] != VAL_NULL { es = ev_coerce_str(el) } }
3732 let m2: *i64 = ev_cell()
3733 ev_str_concat(acc, es, m2); acc = (m2[1]) as *i64
3734 i = i + 1
3735 }
3736 ev_set(out, VAL_STR, acc as i64)
3737 return 0
3738}
3739func bi_arr_slice(thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
3740 let len: i64 = ev_arraylike_len(thisv) // array OR array-like: jQuery's .get()=slice.call(this), .slice()=slice.apply(this,..)
3741 var sa: i64 = 0
3742 var sb: i64 = len
3743 if argc >= 1 { sa = ja_p(argbuf, 0) }
3744 if argc >= 2 { sb = ja_p(argbuf, 1) }
3745 let ca: i64 = ev_clamp_idx(sa, len)
3746 let cb: i64 = ev_clamp_idx(sb, len)
3747 let r: *i64 = arr_new()
3748 var i: i64 = ca
3749 var w: i64 = 0
3750 while i < cb {
3751 let el: *i64 = ev_cell()
3752 ev_arraylike_get(thisv, i, el)
3753 arr_set(r, w, el[0], el[1])
3754 w = w + 1
3755 i = i + 1
3756 }
3757 ev_set(out, VAL_ARRAY, r as i64)
3758 return 0
3759}
3760// arr.reverse() -- reverse IN PLACE, return the (same) array. A YouTube signature-decipher transform.
3761func bi_arr_reverse(thisv: *i64, out: *i64) -> i64 {
3762 let a: *i64 = (thisv[1]) as *i64
3763 let len: i64 = arr_len(a)
3764 var i: i64 = 0
3765 var j: i64 = len - 1
3766 while i < j {
3767 let ei: *i64 = ev_cell(); arr_get(a, i, ei)
3768 let ej: *i64 = ev_cell(); arr_get(a, j, ej)
3769 arr_set(a, i, ej[0], ej[1])
3770 arr_set(a, j, ei[0], ei[1])
3771 i = i + 1
3772 j = j - 1
3773 }
3774 ev_set(out, VAL_ARRAY, a as i64)
3775 return 0
3776}
3777// arr.splice(start, deleteCount, ...items) -- remove deleteCount at start (clamped; negative start = from end),
3778// insert items, MUTATE the array in place, return the removed elements as a NEW array. YouTube uses
3779// splice(0,b) to drop the first b chars of the signature. Rebuild-then-copy-back for correctness.
3780func bi_arr_splice(thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
3781 let a: *i64 = (thisv[1]) as *i64
3782 let len: i64 = arr_len(a)
3783 var start: i64 = 0
3784 if argc >= 1 { start = ja_p(argbuf, 0) }
3785 if start < 0 { start = len + start; if start < 0 { start = 0 } }
3786 if start > len { start = len }
3787 var delc: i64 = len - start // splice(start) with no count removes the rest
3788 if argc >= 2 { delc = ja_p(argbuf, 1); if delc < 0 { delc = 0 } if delc > (len - start) { delc = len - start } }
3789 let removed: *i64 = arr_new()
3790 var ri: i64 = 0
3791 while ri < delc { let el: *i64 = ev_cell(); arr_get(a, start + ri, el); arr_set(removed, ri, el[0], el[1]); ri = ri + 1 }
3792 var nitems: i64 = 0
3793 if argc > 2 { nitems = argc - 2 }
3794 let tmp: *i64 = arr_new() // [0..start) + items + [start+delc..len)
3795 var w: i64 = 0
3796 var k: i64 = 0
3797 while k < start { let el: *i64 = ev_cell(); arr_get(a, k, el); arr_set(tmp, w, el[0], el[1]); w = w + 1; k = k + 1 }
3798 var mi: i64 = 0
3799 while mi < nitems { arr_set(tmp, w, ja_t(argbuf, 2 + mi), ja_p(argbuf, 2 + mi)); w = w + 1; mi = mi + 1 }
3800 k = start + delc
3801 while k < len { let el: *i64 = ev_cell(); arr_get(a, k, el); arr_set(tmp, w, el[0], el[1]); w = w + 1; k = k + 1 }
3802 a[0] = 0 // reset + rebuild `a` from tmp (mutate in place)
3803 var p: i64 = 0
3804 while p < w { let el: *i64 = ev_cell(); arr_get(tmp, p, el); arr_set(a, p, el[0], el[1]); p = p + 1 }
3805 ev_set(out, VAL_ARRAY, removed as i64)
3806 return 0
3807}
3808
3809// ---- Array ITERATION methods (R-JS-CALLBACK, rung 7) ----
3810// Each `this` = an array (the receiver guard already verified VAL_ARRAY). arg0 = the callback,
3811// which MUST be a user CLOSURE/arrow (VAL_FUNC) -- a non-function callback is an HONEST ERROR
3812// (rc=1), never a silent no-op. They call back per element via js_call_core (the shared
3813// closure-invoke core), passing (element, index) as PRE-EVALUATED args. The callback's 3rd
3814// arg (the array itself) and thisArg are NAMED OPENS (not passed) -- see the header.
3815
3816// invoke the callback closure `cb` (a VAL_FUNC value cell) with (element, index) for array
3817// element i, writing the result to `res`. Returns 0 ok, 1 ERROR (propagated from the body).
3818func bi_cb_call_ei(ctx: *i64, genv: *i64, cb: *i64, a: *i64, i: i64, res: *i64) -> i64 {
3819 let clos: *i64 = (cb[1]) as *i64
3820 let cbargs: *i64 = sys_mmap(8 * 2 * 2) as *i64
3821 let el: *i64 = ev_cell()
3822 arr_get(a, i, el) // out-of-range -> el stays undefined (cells are dense here)
3823 cbargs[0] = el[0] // arg0 = element
3824 cbargs[1] = el[1]
3825 cbargs[2] = VAL_NUM // arg1 = index
3826 cbargs[3] = i
3827 return js_call_core(ctx, clos, cbargs, 2, 0 as *i64, genv, res)
3828}
3829// validate arg0 is a callback FUNCTION; returns its value-cell ptr, or 0 (null) if not a
3830// function -- the caller then errors. Centralizes the non-function-callback tamper contract.
3831func bi_cb_arg(argbuf: *i64, argc: i64) -> *i64 {
3832 if argc < 1 { return 0 as *i64 }
3833 if ja_t(argbuf, 0) != VAL_FUNC { return 0 as *i64 }
3834 let cb: *i64 = ev_cell()
3835 cb[0] = ja_t(argbuf, 0)
3836 cb[1] = ja_p(argbuf, 0)
3837 return cb
3838}
3839
3840func bi_arr_foreach(ctx: *i64, genv: *i64, thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
3841 let cb: *i64 = bi_cb_arg(argbuf, argc)
3842 if ev_isnull(cb) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 } // non-function callback -> ERROR
3843 let a: *i64 = (thisv[1]) as *i64
3844 let len: i64 = arr_len(a)
3845 var i: i64 = 0
3846 while i < len {
3847 let res: *i64 = ev_cell()
3848 if bi_cb_call_ei(ctx, genv, cb, a, i, res) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
3849 i = i + 1
3850 }
3851 ev_set(out, VAL_UNDEF, 0) // forEach returns undefined
3852 return 0
3853}
3854
3855func bi_arr_map(ctx: *i64, genv: *i64, thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
3856 let cb: *i64 = bi_cb_arg(argbuf, argc)
3857 if ev_isnull(cb) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
3858 let a: *i64 = (thisv[1]) as *i64
3859 let len: i64 = arr_len(a)
3860 let r: *i64 = arr_new()
3861 var i: i64 = 0
3862 while i < len {
3863 let res: *i64 = ev_cell()
3864 if bi_cb_call_ei(ctx, genv, cb, a, i, res) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
3865 arr_set(r, i, res[0], res[1]) // NEW array, same length
3866 i = i + 1
3867 }
3868 ev_set(out, VAL_ARRAY, r as i64)
3869 return 0
3870}
3871
3872func bi_arr_filter(ctx: *i64, genv: *i64, thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
3873 let cb: *i64 = bi_cb_arg(argbuf, argc)
3874 if ev_isnull(cb) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
3875 let a: *i64 = (thisv[1]) as *i64
3876 let len: i64 = arr_len(a)
3877 let r: *i64 = arr_new()
3878 var i: i64 = 0
3879 var w: i64 = 0
3880 while i < len {
3881 let res: *i64 = ev_cell()
3882 if bi_cb_call_ei(ctx, genv, cb, a, i, res) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
3883 if ev_truthy(res) == 1 {
3884 let el: *i64 = ev_cell()
3885 arr_get(a, i, el)
3886 arr_set(r, w, el[0], el[1]) // keep the ORIGINAL element when predicate truthy
3887 w = w + 1
3888 }
3889 i = i + 1
3890 }
3891 ev_set(out, VAL_ARRAY, r as i64)
3892 return 0
3893}
3894
3895func bi_arr_reduce(ctx: *i64, genv: *i64, thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
3896 let cb: *i64 = bi_cb_arg(argbuf, argc)
3897 if ev_isnull(cb) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
3898 let a: *i64 = (thisv[1]) as *i64
3899 let len: i64 = arr_len(a)
3900 let acc: *i64 = ev_cell()
3901 var start: i64 = 0
3902 // init form: arg1 present -> acc = init, fold from element 0. no-init form: acc = element 0,
3903 // fold from element 1. Empty array WITHOUT init = honest ERROR (real JS TypeError).
3904 if argc >= 2 {
3905 acc[0] = ja_t(argbuf, 1)
3906 acc[1] = ja_p(argbuf, 1)
3907 start = 0
3908 } else {
3909 if len == 0 { ev_set(out, VAL_UNDEF, 0); return 1 } // reduce of empty + no init -> ERROR
3910 arr_get(a, 0, acc)
3911 start = 1
3912 }
3913 var i: i64 = start
3914 while i < len {
3915 // fold: acc = cb(acc, element, index). Build the 3-arg buffer [acc, el, i].
3916 let clos: *i64 = (cb[1]) as *i64
3917 let cbargs: *i64 = sys_mmap(8 * 2 * 3) as *i64
3918 cbargs[0] = acc[0]
3919 cbargs[1] = acc[1]
3920 let el: *i64 = ev_cell()
3921 arr_get(a, i, el)
3922 cbargs[2] = el[0]
3923 cbargs[3] = el[1]
3924 cbargs[4] = VAL_NUM
3925 cbargs[5] = i
3926 let res: *i64 = ev_cell()
3927 if js_call_core(ctx, clos, cbargs, 3, 0 as *i64, genv, res) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
3928 acc[0] = res[0]
3929 acc[1] = res[1]
3930 i = i + 1
3931 }
3932 ev_copy(out, acc)
3933 return 0
3934}
3935
3936// some/every share one body (`want_all` = 0 for some, 1 for every) with SHORT-CIRCUIT:
3937// some -> true on the FIRST truthy callback (else false); stops early on the truthy hit.
3938// every -> false on the FIRST falsy callback (else true); stops early on the falsy hit.
3939func bi_arr_some(ctx: *i64, genv: *i64, thisv: *i64, argbuf: *i64, argc: i64, want_all: i64, out: *i64) -> i64 {
3940 let cb: *i64 = bi_cb_arg(argbuf, argc)
3941 if ev_isnull(cb) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
3942 let a: *i64 = (thisv[1]) as *i64
3943 let len: i64 = arr_len(a)
3944 var i: i64 = 0
3945 while i < len {
3946 let res: *i64 = ev_cell()
3947 if bi_cb_call_ei(ctx, genv, cb, a, i, res) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
3948 let t: i64 = ev_truthy(res)
3949 if want_all == 0 { if t == 1 { ev_set(out, VAL_BOOL, 1); return 0 } } // some: first truthy -> true
3950 if want_all == 1 { if t == 0 { ev_set(out, VAL_BOOL, 0); return 0 } } // every: first falsy -> false
3951 i = i + 1
3952 }
3953 if want_all == 0 { ev_set(out, VAL_BOOL, 0); return 0 } // some over empty / no-truthy -> false
3954 ev_set(out, VAL_BOOL, 1) // every over empty / all-truthy -> true
3955 return 0
3956}
3957// sort comparison of two VALUES: sign of cmp(x,y)'s numeric return if a comparator is given, else ToString
3958// (byte-lexicographic) order. cmp is a user closure -> js_call_core (thisval undefined). A comparator ERROR
3959// or non-numeric return counts as 0 (keep order) rather than aborting the sort.
3960func bi_sort_cmp(ctx: *i64, genv: *i64, cmp: *i64, x: *i64, y: *i64) -> i64 {
3961 if (cmp as i64) != 0 {
3962 let cbargs: *i64 = sys_mmap(8 * 2 * 2) as *i64
3963 cbargs[0] = x[0]; cbargs[1] = x[1]; cbargs[2] = y[0]; cbargs[3] = y[1]
3964 let res: *i64 = ev_cell()
3965 if js_call_core(ctx, (cmp[1]) as *i64, cbargs, 2, 0 as *i64, genv, res) == 1 { return 0 }
3966 if res[0] == VAL_FLOAT { if nx_f64_lt(res[1], 0) == 1 { return 0 - 1 } if nx_f64_gt(res[1], 0) == 1 { return 1 } return 0 }
3967 let n: i64 = ev_tonum(res)
3968 if n < 0 { return 0 - 1 }
3969 if n > 0 { return 1 }
3970 return 0
3971 }
3972 return ev_str_cmp(ev_coerce_str(x), ev_coerce_str(y))
3973}
3974// Array.prototype.sort([cmp]) -- STABLE in-place insertion sort (Sizzle's sortStable feature-detect requires a
3975// stable sort; a non-stable sort would make jQuery mis-classify + fall to a slower path or misorder). cmp is a
3976// user closure invoked via bi_sort_cmp; no cmp -> ToString order. Mutates thisv's cells, returns the array.
3977func bi_arr_sort(ctx: *i64, genv: *i64, thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
3978 if thisv[0] != VAL_ARRAY { ev_set(out, VAL_UNDEF, 0); return 1 }
3979 let a: *i64 = (thisv[1]) as *i64
3980 let len: i64 = arr_len(a)
3981 var cmp: *i64 = 0 as *i64
3982 if argc >= 1 { if ja_t(argbuf, 0) == VAL_FUNC { cmp = ev_cell(); cmp[0] = ja_t(argbuf, 0); cmp[1] = ja_p(argbuf, 0) } }
3983 let cur: *i64 = ev_cell()
3984 let prev: *i64 = ev_cell()
3985 var i: i64 = 1
3986 while i < len {
3987 if arr_get(a, i, cur) == 0 { ev_set(cur, VAL_UNDEF, 0) }
3988 var j: i64 = i - 1
3989 var placed: i64 = 0
3990 while placed == 0 {
3991 if j < 0 { placed = 1 } else {
3992 if arr_get(a, j, prev) == 0 { ev_set(prev, VAL_UNDEF, 0) }
3993 if bi_sort_cmp(ctx, genv, cmp, prev, cur) > 0 { arr_set(a, j + 1, prev[0], prev[1]); j = j - 1 } else { placed = 1 }
3994 }
3995 }
3996 arr_set(a, j + 1, cur[0], cur[1])
3997 i = i + 1
3998 }
3999 ev_set(out, VAL_ARRAY, a as i64)
4000 return 0
4001}
4002
4003// ---- Object.* free functions (operand object = arg0) ----
4004func bi_obj_keys(argbuf: *i64, argc: i64, out: *i64) -> i64 {
4005 if argc < 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
4006 if ja_t(argbuf, 0) != VAL_OBJECT { ev_set(out, VAL_UNDEF, 0); return 1 } // Object.keys(non-object) = open error
4007 let o: *i64 = (ja_p(argbuf, 0)) as *i64
4008 let c: i64 = obj_count(o)
4009 let r: *i64 = arr_new()
4010 var i: i64 = 0
4011 var j: i64 = 0
4012 while i < c {
4013 let kr: *i64 = obj_key(o, i)
4014 if (kr as i64) != 0 { arr_set(r, j, VAL_STR, kr as i64); j = j + 1 } // skip tombstoned (deleted) slots
4015 i = i + 1
4016 }
4017 ev_set(out, VAL_ARRAY, r as i64)
4018 return 0
4019}
4020// Object.defineProperty(target, key, descriptor): set target[key] = descriptor.value (the `value` form,
4021// which is what DeltaBlue's inheritsFrom install + typical polyfills use; get/set/enumerable/writable are a
4022// follow-on). target = VAL_OBJECT (e.g. Object.prototype). Returns target (real JS). Non-object target errors.
4023func bi_obj_defineprop(argbuf: *i64, argc: i64, out: *i64) -> i64 {
4024 if argc < 3 { ev_set(out, VAL_UNDEF, 0); return 1 }
4025 if ja_t(argbuf, 0) != VAL_OBJECT { ev_set(out, VAL_UNDEF, 0); return 1 }
4026 let target: *i64 = (ja_p(argbuf, 0)) as *i64
4027 let keyrec: *i64 = ev_coerce_str_arg(argbuf, 1)
4028 if ja_t(argbuf, 2) != VAL_OBJECT { ev_set(out, VAL_UNDEF, 0); return 1 }
4029 let desc: *i64 = (ja_p(argbuf, 2)) as *i64
4030 let vb: *i64 = ev_cell()
4031 if obj_get(desc, ev_cstr("value\x00" as *u8), vb) == 0 { ev_set(vb, VAL_UNDEF, 0) }
4032 if obj_set(target, keyrec, vb[0], vb[1]) == 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
4033 ev_set(out, VAL_OBJECT, target as i64)
4034 return 0
4035}
4036func bi_obj_values(argbuf: *i64, argc: i64, out: *i64) -> i64 {
4037 if argc < 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
4038 if ja_t(argbuf, 0) != VAL_OBJECT { ev_set(out, VAL_UNDEF, 0); return 1 }
4039 let o: *i64 = (ja_p(argbuf, 0)) as *i64
4040 let c: i64 = obj_count(o)
4041 let r: *i64 = arr_new()
4042 let d: *i64 = (o[OBJ_BACK]) as *i64
4043 var i: i64 = 0
4044 while i < c {
4045 let b: i64 = i * OBJ_ENT
4046 arr_set(r, i, d[b + 1], d[b + 2]) // entry value-tag / value-payload
4047 i = i + 1
4048 }
4049 ev_set(out, VAL_ARRAY, r as i64)
4050 return 0
4051}
4052
4053// Object.prototype.toString -> the "[object Type]" brand tag. This is jQuery's `toType` primitive
4054// (class2type[ toString.call(obj) ]) and the single most-called stdlib method in real page JS: every
4055// isArray/isFunction/type-switch routes through it. Brand is by the receiver's VALUE TAG (integer world).
4056func bi_obj_tostring(thisv: *i64, out: *i64) -> i64 {
4057 let t: i64 = thisv[0]
4058 if t == VAL_ARRAY { ev_set(out, VAL_STR, (ev_cstr("[object Array]\x00" as *u8)) as i64); return 0 }
4059 if t == VAL_FUNC { ev_set(out, VAL_STR, (ev_cstr("[object Function]\x00" as *u8)) as i64); return 0 }
4060 if t == VAL_NATIVE { ev_set(out, VAL_STR, (ev_cstr("[object Function]\x00" as *u8)) as i64); return 0 }
4061 if t == VAL_STR { ev_set(out, VAL_STR, (ev_cstr("[object String]\x00" as *u8)) as i64); return 0 }
4062 if t == VAL_NUM { ev_set(out, VAL_STR, (ev_cstr("[object Number]\x00" as *u8)) as i64); return 0 }
4063 if t == VAL_FLOAT { ev_set(out, VAL_STR, (ev_cstr("[object Number]\x00" as *u8)) as i64); return 0 }
4064 if t == VAL_BOOL { ev_set(out, VAL_STR, (ev_cstr("[object Boolean]\x00" as *u8)) as i64); return 0 }
4065 if t == VAL_UNDEF { ev_set(out, VAL_STR, (ev_cstr("[object Undefined]\x00" as *u8)) as i64); return 0 }
4066 if t == VAL_NULL { ev_set(out, VAL_STR, (ev_cstr("[object Null]\x00" as *u8)) as i64); return 0 }
4067 if t == VAL_REGEX { ev_set(out, VAL_STR, (ev_cstr("[object RegExp]\x00" as *u8)) as i64); return 0 }
4068 ev_set(out, VAL_STR, (ev_cstr("[object Object]\x00" as *u8)) as i64); return 0
4069}
4070// Object.prototype.hasOwnProperty(key) -> is `key` an OWN (not inherited) property of the receiver.
4071// jQuery calls it as hasOwn.call(obj,key); receiver arrives as thisv. Own-only = obj_get (proto walk is separate).
4072func bi_obj_hasown(thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
4073 if argc < 1 { ev_set(out, VAL_BOOL, 0); return 0 }
4074 if thisv[0] != VAL_OBJECT { ev_set(out, VAL_BOOL, 0); return 0 }
4075 let o: *i64 = (thisv[1]) as *i64
4076 let key: *i64 = ev_coerce_str_arg(argbuf, 0)
4077 let probe: *i64 = ev_cell()
4078 if obj_get(o, key, probe) == 1 { ev_set(out, VAL_BOOL, 1); return 0 }
4079 ev_set(out, VAL_BOOL, 0); return 0
4080}
4081// proto.isPrototypeOf(obj) -> is the receiver anywhere on obj's [[Prototype]] chain (depth-bounded).
4082func bi_obj_isprototypeof(thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
4083 if argc < 1 { ev_set(out, VAL_BOOL, 0); return 0 }
4084 if thisv[0] != VAL_OBJECT { ev_set(out, VAL_BOOL, 0); return 0 }
4085 if ja_t(argbuf, 0) != VAL_OBJECT { ev_set(out, VAL_BOOL, 0); return 0 }
4086 let target: i64 = thisv[1]
4087 var p: i64 = obj_proto((ja_p(argbuf, 0)) as *i64)
4088 var depth: i64 = 0
4089 while depth < 200 {
4090 if p == 0 { ev_set(out, VAL_BOOL, 0); return 0 }
4091 if p == target { ev_set(out, VAL_BOOL, 1); return 0 }
4092 p = obj_proto(p as *i64)
4093 depth = depth + 1
4094 }
4095 ev_set(out, VAL_BOOL, 0); return 0
4096}
4097// Function.prototype.toString -> a source-ish string. jQuery stores fnToString.call(Object) and only
4098// compares it in isPlainObject's DEEP path (which we short-circuit via a null [[Prototype]] on plain {}),
4099// so a stable native-code brand suffices to unblock the module-level read.
4100func bi_fn_tostring(thisv: *i64, out: *i64) -> i64 {
4101 ev_set(out, VAL_STR, (ev_cstr("function () { [native code] }\x00" as *u8)) as i64); return 0
4102}
4103// Object.getPrototypeOf(o) -> o's [[Prototype]] OBJECT, or null. A plain `{}` (obj_new) carries no
4104// explicit link (OBJ_PROTO==0) -- the implicit Object.prototype is applied at lookup time, not stored --
4105// so we honestly report null there. jQuery's isPlainObject reads `if(!proto)return true`, so plain
4106// objects classify correctly; `new Ctor()` instances (OBJ_PROTO set by `new`) return their real proto.
4107func bi_obj_getproto(argbuf: *i64, argc: i64, out: *i64) -> i64 {
4108 if argc < 1 { ev_set(out, VAL_NULL, 0); return 0 }
4109 if ja_t(argbuf, 0) == VAL_OBJECT {
4110 let p: i64 = obj_proto((ja_p(argbuf, 0)) as *i64)
4111 if p == 0 { ev_set(out, VAL_NULL, 0); return 0 }
4112 ev_set(out, VAL_OBJECT, p); return 0
4113 }
4114 ev_set(out, VAL_NULL, 0); return 0
4115}
4116// Object.create(proto): fresh object whose [[Prototype]] is proto (a plain empty obj for Object.create(null)).
4117// jQuery's event system does `elemData.events = Object.create(null)` for its handler map -> without this .on() threw.
4118func bi_obj_create(argbuf: *i64, argc: i64, out: *i64) -> i64 {
4119 let o: *i64 = obj_new()
4120 if argc >= 1 { if ja_t(argbuf, 0) == VAL_OBJECT { obj_set_proto(o, ja_p(argbuf, 0)) } }
4121 ev_set(out, VAL_OBJECT, o as i64)
4122 return 0
4123}
4124// Object.assign(target, ...sources): shallow-copy each source's OWN enumerable props into target; return target.
4125// Ubiquitous ES6 (vk + most modern frameworks use it for merging config/state/props).
4126func bi_obj_assign(argbuf: *i64, argc: i64, out: *i64) -> i64 {
4127 if argc < 1 { ev_set(out, VAL_UNDEF, 0); return 0 }
4128 if ja_t(argbuf, 0) != VAL_OBJECT { ev_set(out, ja_t(argbuf, 0), ja_p(argbuf, 0)); return 0 }
4129 let tgt: *i64 = (ja_p(argbuf, 0)) as *i64
4130 var s: i64 = 1
4131 while s < argc {
4132 if ja_t(argbuf, s) == VAL_OBJECT {
4133 let src: *i64 = (ja_p(argbuf, s)) as *i64
4134 let n: i64 = src[0]
4135 var i: i64 = 0
4136 while i < n { let kp: *i64 = obj_key(src, i); if (kp as i64) != 0 { let vb: *i64 = ev_cell(); obj_get(src, kp, vb); obj_set(tgt, kp, vb[0], vb[1]) } i = i + 1 }
4137 }
4138 s = s + 1
4139 }
4140 ev_set(out, VAL_OBJECT, tgt as i64)
4141 return 0
4142}
4143
4144// ---- Math.* free functions (integer-number world) ----
4145func bi_math_maxmin(argbuf: *i64, argc: i64, want_max: i64, out: *i64) -> i64 {
4146 // Math.max() = -Infinity / Math.min() = +Infinity in real JS; with no float this rung,
4147 // an empty call is an HONEST error rather than a fabricated sentinel.
4148 if argc < 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
4149 var best: i64 = ev_arg_num(argbuf, 0)
4150 var i: i64 = 1
4151 while i < argc {
4152 let v: i64 = ev_arg_num(argbuf, i)
4153 if want_max == 1 { if v > best { best = v } }
4154 if want_max == 0 { if v < best { best = v } }
4155 i = i + 1
4156 }
4157 ev_set(out, VAL_NUM, best)
4158 return 0
4159}
4160func bi_math_abs(argbuf: *i64, argc: i64, out: *i64) -> i64 {
4161 if argc < 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
4162 var n: i64 = ev_arg_num(argbuf, 0)
4163 if n < 0 { n = 0 - n }
4164 ev_set(out, VAL_NUM, n)
4165 return 0
4166}
4167func bi_math_identity(argbuf: *i64, argc: i64, out: *i64) -> i64 {
4168 if argc < 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
4169 ev_set(out, VAL_NUM, ev_arg_num(argbuf, 0)) // floor/ceil of an integer = the integer
4170 return 0
4171}
4172// Math.random() -> VAL_FLOAT in [0,1). xorshift64 (left-shifts ok; right-shift masked to be LOGICAL via the
4173// hex constant to dodge NishiLang's arithmetic >>). Mantissa = low 53 bits / 2^53 (exactly representable).
4174// Seeded deterministically (nonzero) -- non-standard vs V8's nondeterminism but valid + testable.
4175static rng_state: i64
4176func bi_math_random(out: *i64) -> i64 {
4177 if rng_state == 0 { rng_state = 88172645463325252 }
4178 var x: i64 = rng_state
4179 x = x ^ (x << 13)
4180 x = x ^ ((x >> 7) & 0x01FFFFFFFFFFFFFF)
4181 x = x ^ (x << 17)
4182 rng_state = x
4183 let m: i64 = x & 0x1FFFFFFFFFFFFF
4184 ev_set(out, VAL_FLOAT, nx_f64_div(ji2f(m), ji2f(9007199254740992)))
4185 return 0
4186}
4187// Math.floor / Math.ceil. Integer arg -> identity. Float arg -> round to -inf (floor) / +inf (ceil) via
4188// trunc-toward-zero + a one-step correction, using nx_f64_lt (handles ±0 / NaN correctly). Returns VAL_NUM.
4189func bi_math_floorceil(argbuf: *i64, argc: i64, is_ceil: i64, out: *i64) -> i64 {
4190 if argc < 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
4191 if ja_t(argbuf, 0) != VAL_FLOAT { ev_set(out, VAL_NUM, ev_arg_num(argbuf, 0)); return 0 }
4192 let raw: i64 = ja_p(argbuf, 0)
4193 var k: i64 = jf2i(raw) // trunc toward zero
4194 let back: i64 = ji2f(k) // f64 bits of trunc(f)
4195 if is_ceil == 1 { if nx_f64_lt(back, raw) == 1 { k = k + 1 } } // trunc<f -> ceil = trunc+1
4196 if is_ceil == 0 { if nx_f64_lt(raw, back) == 1 { k = k - 1 } } // f<trunc -> floor = trunc-1
4197 ev_set(out, VAL_NUM, k)
4198 return 0
4199}
4200// Math.sqrt(x) -> VAL_FLOAT. Int or float arg -> f64 bits -> soft-float sqrt (NaN for negatives).
4201func bi_math_sqrt(argbuf: *i64, argc: i64, out: *i64) -> i64 {
4202 if argc < 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
4203 var raw: i64 = 0
4204 if ja_t(argbuf, 0) == VAL_FLOAT { raw = ja_p(argbuf, 0) } else { raw = ji2f(ev_arg_num(argbuf, 0)) }
4205 ev_set(out, VAL_FLOAT, nx_f64_sqrt(raw))
4206 return 0
4207}
4208func bi_math_pow(argbuf: *i64, argc: i64, out: *i64) -> i64 {
4209 if argc < 2 { ev_set(out, VAL_UNDEF, 0); return 1 }
4210 // EXACT integer fast path: both operands are non-negative-exponent integers -> VAL_NUM (2^5=32 stays int).
4211 if ja_t(argbuf, 0) == VAL_NUM { if ja_t(argbuf, 1) == VAL_NUM {
4212 let base: i64 = ja_p(argbuf, 0)
4213 let exp: i64 = ja_p(argbuf, 1)
4214 if exp >= 0 { if exp <= 62 {
4215 var r: i64 = 1
4216 var e: i64 = 0
4217 while e < exp { r = r * base; e = e + 1 }
4218 ev_set(out, VAL_NUM, r)
4219 return 0
4220 } }
4221 } }
4222 // GENERAL Math.pow over reals (float base and/or float/negative exponent): x^y = fdlibm soft-float pow.
4223 let xc: *i64 = ev_cell()
4224 xc[0] = ja_t(argbuf, 0); xc[1] = ja_p(argbuf, 0)
4225 let yc: *i64 = ev_cell()
4226 yc[0] = ja_t(argbuf, 1); yc[1] = ja_p(argbuf, 1)
4227 ev_set(out, VAL_FLOAT, nx_f64_pow(ev_tof64(xc), ev_tof64(yc)))
4228 return 0
4229}
4230// coerce arg k to an i64 number (number/bool/null -> via ev_tonum; others -> 0 this rung).
4231func ev_arg_num(argbuf: *i64, k: i64) -> i64 {
4232 let tmp: *i64 = ev_cell()
4233 tmp[0] = ja_t(argbuf, k)
4234 tmp[1] = ja_p(argbuf, k)
4235 return ev_tonum(tmp)
4236}
4237
4238// ---- console.log: page-observability primitive ----
4239// Write each arg (space-separated) to stdout + a trailing newline; return undefined.
4240func bi_console_log(argbuf: *i64, argc: i64, out: *i64) -> i64 {
4241 var i: i64 = 0
4242 while i < argc {
4243 if i > 0 { sys_write(1, " " as *u8, 1) }
4244 let tmp: *i64 = ev_cell()
4245 tmp[0] = ja_t(argbuf, i)
4246 tmp[1] = ja_p(argbuf, i)
4247 let s: *i64 = ev_coerce_str(tmp)
4248 sys_write(1, ev_str_bytes(s), ev_str_len(s))
4249 i = i + 1
4250 }
4251 sys_write(1, "\n" as *u8, 1)
4252 ev_set(out, VAL_UNDEF, 0)
4253 return 0
4254}
4255
4256// ===================== member / index access (R-JS-OBJ) =====================
4257// "length" string record, lazily-cached, for arr.length / arr['length'].
4258func ev_is_length_key(key: *i64) -> i64 {
4259 let lit: *i64 = ev_cstr("length\x00" as *u8)
4260 return ev_str_eq(key, lit)
4261}
4262// look a property `key` (string record) up on a VALUE; write result to out.
4263// Returns 0 ok (out = value or undefined-when-absent), 1 ERROR (member on a primitive).
4264// Objects: property table lookup, absent -> undefined. Arrays: `.length` -> number,
4265// else a numeric-string key indexes the array (out-of-range -> undefined). Member/index
4266// on a number/bool/null/undefined is a NAMED OPEN -> ERROR (no prototype this rung).
4267func ev_get_prop(base: *i64, key: *i64, out: *i64) -> i64 {
4268 let t: i64 = base[0]
4269 if t == VAL_OBJECT {
4270 let o: *i64 = (base[1]) as *i64
4271 if obj_get(o, key, out) == 1 { return 0 }
4272 if obj_proto_lookup(o, key, out) == 1 { return 0 } // inherited from the [[Prototype]] chain
4273 if obj_get((js_object_prototype()) as *i64, key, out) == 1 { return 0 } // implicit Object.prototype (user-installed)
4274 let bmo: i64 = ev_native_obj(key) // implicit Object.prototype METHOD (toString/hasOwnProperty/valueOf/...)
4275 if bmo != 0 { ev_set(out, VAL_NATIVE, bmo); return 0 }
4276 if je_dom_node_nav(o, key, out) == 1 { return 0 } // tree-DOM node lazy navigation (firstChild/lastChild/nextSibling/parentNode/childNodes/checked)
4277 ev_set(out, VAL_UNDEF, 0); return 0 // absent property -> undefined (real JS)
4278 }
4279 // A FUNCTION is an object: `.prototype` is the shared instance prototype; any other key reads from the
4280 // function's own-property store (Foo.Node, Foo.count, ...), then the implicit Object.prototype chain.
4281 if t == VAL_FUNC {
4282 if ev_key_is(key, "prototype\x00" as *u8) == 1 { ev_set(out, VAL_OBJECT, func_prototype(base[1])); return 0 }
4283 if obj_get(func_own(base[1]), key, out) == 1 { return 0 }
4284 if obj_get((js_object_prototype()) as *i64, key, out) == 1 { return 0 } // functions inherit Object.prototype (user-installed)
4285 if ev_key_is(key, "toString\x00" as *u8) == 1 { ev_set(out, VAL_NATIVE, BI_FN_TOSTRING); return 0 } // Function.prototype.toString
4286 let bmf: i64 = ev_native_obj(key) // hasOwnProperty/valueOf/isPrototypeOf inherited from Object.prototype
4287 if bmf != 0 { ev_set(out, VAL_NATIVE, bmf); return 0 }
4288 ev_set(out, VAL_UNDEF, 0); return 0
4289 }
4290 if t == VAL_ARRAY {
4291 let a: *i64 = (base[1]) as *i64
4292 if ev_is_length_key(key) == 1 { ev_set(out, VAL_NUM, arr_len(a)); return 0 }
4293 // an array METHOD name (push/pop/...) reads as a native function value (real JS).
4294 let bm: i64 = ev_native_arr(key)
4295 if bm != 0 { ev_set(out, VAL_NATIVE, bm); return 0 }
4296 if js_arrproto != 0 { if obj_get((js_arrproto) as *i64, key, out) == 1 { return 0 } } // user Array.prototype method
4297 // numeric string key -> indexed element; non-numeric -> the array's NAMED-property expando (arrays are
4298 // objects: jQuery reads handlers.delegateCount etc.), else undefined.
4299 let ki: i64 = ev_str_to_index(key)
4300 if ki >= 0 { if arr_get(a, ki, out) == 1 { return 0 } ev_set(out, VAL_UNDEF, 0); return 0 }
4301 if a[3] != 0 { if obj_get((a[3]) as *i64, key, out) == 1 { return 0 } }
4302 ev_set(out, VAL_UNDEF, 0); return 0
4303 }
4304 // STRING member READ (R-JS-RUNTIME): `.length` -> number; a string METHOD name -> a native
4305 // function value; numeric index -> the 1-char substring; anything else -> undefined.
4306 if t == VAL_STR {
4307 let rec: *i64 = (base[1]) as *i64
4308 if ev_is_length_key(key) == 1 { ev_set(out, VAL_NUM, ev_str_len(rec)); return 0 }
4309 let bm: i64 = ev_native_str(key)
4310 if bm != 0 { ev_set(out, VAL_NATIVE, bm); return 0 }
4311 if js_strproto != 0 { if obj_get((js_strproto) as *i64, key, out) == 1 { return 0 } } // user String.prototype method
4312 let ki: i64 = ev_str_to_index(key)
4313 if ki >= 0 { ev_str_char_at(rec, ki, out); return 0 }
4314 ev_set(out, VAL_UNDEF, 0); return 0
4315 }
4316 // GLOBAL NAMESPACE member READ (Math/Object/console/JSON): a known method name -> a native
4317 // function value; an unknown name -> undefined (real JS, e.g. Math.zzz === undefined).
4318 if t == VAL_GLOBALNS {
4319 if base[1] == NS_OBJECT { if ev_key_is(key, "prototype\x00" as *u8) == 1 { ev_set(out, VAL_OBJECT, js_object_prototype()); return 0 } }
4320 if obj_get(js_ns_statics(base[1]), key, out) == 1 { return 0 } // user statics (Object.extend) win
4321 let bm: i64 = ev_native_global(base[1], key)
4322 if bm != 0 { ev_set(out, VAL_NATIVE, bm); return 0 }
4323 ev_set(out, VAL_UNDEF, 0); return 0
4324 }
4325 // member on a built-in CONSTRUCTOR native (String/Array): `.prototype` -> the writable proto object so user
4326 // code can extend it (`String.prototype.m = fn`). Other keys -> undefined (statics like fromCharcode are
4327 // resolved at the CALL site). This was the EarleyBoyer runtime blocker (String.prototype was an error).
4328 if t == VAL_NATIVE {
4329 if base[1] == BI_STRING_CTOR { if ev_key_is(key, "prototype\x00" as *u8) == 1 { ev_set(out, VAL_OBJECT, js_string_prototype()); return 0 } }
4330 if base[1] == BI_ARRAY_CTOR { if ev_key_is(key, "prototype\x00" as *u8) == 1 { ev_set(out, VAL_OBJECT, js_array_prototype()); return 0 } }
4331 if ev_key_is(key, "toString\x00" as *u8) == 1 { ev_set(out, VAL_NATIVE, BI_FN_TOSTRING); return 0 } // a native IS a function (jQuery: fnToString = hasOwn.toString)
4332 let bmn: i64 = ev_native_obj(key) // hasOwnProperty/valueOf/isPrototypeOf on the function object
4333 if bmn != 0 { ev_set(out, VAL_NATIVE, bmn); return 0 }
4334 ev_set(out, VAL_UNDEF, 0); return 0
4335 }
4336 if t == VAL_REGEX { // re.lastIndex / re.source / re.global; other props -> undefined (V8 object semantics)
4337 let rr: *i64 = (base[1]) as *i64
4338 if ev_key_is(key, "lastIndex\x00" as *u8) == 1 { ev_set(out, VAL_NUM, rr[4]); return 0 }
4339 if ev_key_is(key, "source\x00" as *u8) == 1 { ev_set(out, VAL_STR, (je_str_range((rr[1]) as *u8, 0, rr[2])) as i64); return 0 }
4340 if ev_key_is(key, "global\x00" as *u8) == 1 { var gb: i64 = 0; if (rr[3] & RXF_G) != 0 { gb = 1 } ev_set(out, VAL_BOOL, gb); return 0 }
4341 ev_set(out, VAL_UNDEF, 0); return 0
4342 }
4343 // property READ on a NUMBER/BOOL/FLOAT primitive -> undefined (V8: primitives wrap; a missing
4344 // prop reads undefined -- `(132).foo` is NOT an error; scheme2js feature-detects methods this way).
4345 // Methods that DO exist on numbers dispatch via ev_native_num in the call paths, not here.
4346 if t == VAL_NUM { ev_set(out, VAL_UNDEF, 0); return 0 }
4347 if t == VAL_FLOAT { ev_set(out, VAL_UNDEF, 0); return 0 }
4348 if t == VAL_BOOL { ev_set(out, VAL_UNDEF, 0); return 0 }
4349 // member on undefined/null (and anything else unhandled) IS an error (V8: TypeError).
4350 if js_rt_dbg == 1 { sys_write(2, "PROP-ERR base-tag=" as *u8, 18); nx_dbg_num(t); sys_write(2, " key='" as *u8, 6); sys_write(2, ev_str_bytes(key), ev_str_len(key)); sys_write(2, "'\n" as *u8, 2) }
4351 ev_set(out, VAL_UNDEF, 0); return 1
4352}
4353// compare a STRING-RECORD key against a NUL-terminated literal name; 1 iff equal bytes.
4354func ev_key_is(key: *i64, lit: *u8) -> i64 {
4355 let l: i64 = ev_str_len(key)
4356 var n: i64 = 0
4357 while lit[n] != (0 as u8) { n = n + 1 }
4358 if l != n { return 0 }
4359 let by: *u8 = ev_str_bytes(key)
4360 var i: i64 = 0
4361 while i < l { if (by[i] & 0xff) != (lit[i] & 0xff) { return 0 } i = i + 1 }
4362 return 1
4363}
4364// resolve a STRING method name -> its builtin id, or 0 if not a string method.
4365func ev_native_str(key: *i64) -> i64 {
4366 if ev_key_is(key, "toString\x00" as *u8) == 1 { return BI_NUM_TOSTRING } // "s".toString() = identity (radix-10 path coerces = self)
4367 if ev_key_is(key, "charAt\x00" as *u8) == 1 { return BI_STR_CHARAT }
4368 if ev_key_is(key, "indexOf\x00" as *u8) == 1 { return BI_STR_INDEXOF }
4369 if ev_key_is(key, "slice\x00" as *u8) == 1 { return BI_STR_SLICE }
4370 if ev_key_is(key, "concat\x00" as *u8) == 1 { return BI_STR_CONCAT }
4371 if ev_key_is(key, "toUpperCase\x00" as *u8) == 1 { return BI_STR_UPPER }
4372 if ev_key_is(key, "toLowerCase\x00" as *u8) == 1 { return BI_STR_LOWER }
4373 if ev_key_is(key, "includes\x00" as *u8) == 1 { return BI_STR_INCLUDES }
4374 if ev_key_is(key, "search\x00" as *u8) == 1 { return BI_STR_SEARCH } // R-JS-REGEX: str.search(re)
4375 if ev_key_is(key, "match\x00" as *u8) == 1 { return BI_STR_MATCH }
4376 if ev_key_is(key, "replace\x00" as *u8) == 1 { return BI_STR_REPLACE }
4377 if ev_key_is(key, "split\x00" as *u8) == 1 { return BI_STR_SPLIT }
4378 if ev_key_is(key, "charCodeAt\x00" as *u8) == 1 { return BI_STR_CHARCODEAT }
4379 if ev_key_is(key, "substring\x00" as *u8) == 1 { return BI_STR_SUBSTRING }
4380 if ev_key_is(key, "substr\x00" as *u8) == 1 { return BI_STR_SUBSTR }
4381 return 0
4382}
4383// s.charCodeAt(i): byte code unit (ASCII-exact for our byte strings); out-of-range -> NaN (real JS).
4384func bi_str_charcodeat(thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
4385 let srec: *i64 = (thisv[1]) as *i64
4386 let sl: i64 = ev_str_len(srec)
4387 var ix: i64 = 0
4388 if argc >= 1 { ix = ev_arg_num(argbuf, 0) }
4389 if ix < 0 { ev_set(out, VAL_FLOAT, nx_f64_div(ji2f(0), ji2f(0))); return 0 }
4390 if ix >= sl { ev_set(out, VAL_FLOAT, nx_f64_div(ji2f(0), ji2f(0))); return 0 }
4391 let b: *u8 = ev_str_bytes(srec)
4392 ev_set(out, VAL_NUM, b[ix] & 0xff)
4393 return 0
4394}
4395// String.fromCharCode(a,b,..): build a byte string from the low bytes (ASCII range; crypto/base64 use).
4396// LENGTH-based build (NOT ev_cstr, which NUL-scans): JS strings may contain \0 -- fromCharCode(0) is a
4397// 1-char string, and Octane RegExp's computeInputVariants inserts fromCharCode(0) mid-string.
4398func bi_str_fromcharcode(argbuf: *i64, argc: i64, out: *i64) -> i64 {
4399 let b: *u8 = sys_mmap(argc + 8)
4400 var i: i64 = 0
4401 while i < argc { b[i] = (ev_arg_num(argbuf, i) & 0xff) as u8; i = i + 1 }
4402 ev_set(out, VAL_STR, (je_buf_to_str(b, argc)) as i64)
4403 return 0
4404}
4405// s.substring(a,b?) with JS clamp-and-swap; s.substr(a,len?). LENGTH-based copy (NOT ev_cstr, which
4406// NUL-scans -> would truncate a substring that spans an interior \0; see bi_str_fromcharcode).
4407func bi_str_subcopy(srec: *i64, lo: i64, hi: i64, out: *i64) -> i64 {
4408 let sb: *u8 = ev_str_bytes(srec)
4409 let n: i64 = hi - lo
4410 ev_set(out, VAL_STR, (je_str_range(sb, lo, n)) as i64)
4411 return 0
4412}
4413func bi_str_substring(thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
4414 let srec: *i64 = (thisv[1]) as *i64
4415 let sl: i64 = ev_str_len(srec)
4416 var a: i64 = 0
4417 var b2: i64 = sl
4418 if argc >= 1 { a = ev_arg_num(argbuf, 0) }
4419 if argc >= 2 { b2 = ev_arg_num(argbuf, 1) }
4420 if a < 0 { a = 0 }
4421 if a > sl { a = sl }
4422 if b2 < 0 { b2 = 0 }
4423 if b2 > sl { b2 = sl }
4424 if a > b2 { let t: i64 = a; a = b2; b2 = t }
4425 return bi_str_subcopy(srec, a, b2, out)
4426}
4427func bi_str_substr(thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
4428 let srec: *i64 = (thisv[1]) as *i64
4429 let sl: i64 = ev_str_len(srec)
4430 var a: i64 = 0
4431 var ln: i64 = sl
4432 if argc >= 1 { a = ev_arg_num(argbuf, 0) }
4433 if argc >= 2 { ln = ev_arg_num(argbuf, 1) }
4434 if a < 0 { a = sl + a; if a < 0 { a = 0 } }
4435 if a > sl { a = sl }
4436 if ln < 0 { ln = 0 }
4437 var hi: i64 = a + ln
4438 if hi > sl { hi = sl }
4439 return bi_str_subcopy(srec, a, hi, out)
4440}
4441// parseInt(str, radix?): leading ws + optional sign + radix digits (0-9 a-z A-Z < radix); none -> NaN.
4442func bi_parseint(argbuf: *i64, argc: i64, out: *i64) -> i64 {
4443 if argc < 1 { ev_set(out, VAL_UNDEF, 0); return 1 }
4444 let srec: *i64 = ev_coerce_str_arg(argbuf, 0)
4445 let s: *u8 = ev_str_bytes(srec)
4446 let sl: i64 = ev_str_len(srec)
4447 var radix: i64 = 10
4448 if argc >= 2 { let r2: i64 = ev_arg_num(argbuf, 1); if r2 != 0 { radix = r2 } }
4449 var i: i64 = 0
4450 var ws: i64 = 1
4451 while ws == 1 { if i < sl { if s[i] == 32 { i = i + 1 } else { if s[i] == 9 { i = i + 1 } else { ws = 0 } } } else { ws = 0 } }
4452 var sign: i64 = 1
4453 if i < sl { if s[i] == 45 { sign = 0 - 1; i = i + 1 } else { if s[i] == 43 { i = i + 1 } } }
4454 var v: i64 = 0
4455 var got: i64 = 0
4456 var go: i64 = 1
4457 while go == 1 {
4458 if i >= sl { go = 0 } else {
4459 let c: i64 = s[i] & 0xff
4460 var d: i64 = 0 - 1
4461 if c >= 48 { if c <= 57 { d = c - 48 } }
4462 if c >= 97 { if c <= 122 { d = c - 87 } }
4463 if c >= 65 { if c <= 90 { d = c - 55 } }
4464 if d < 0 { go = 0 } else { if d >= radix { go = 0 } else { v = v * radix + d; got = 1; i = i + 1 } }
4465 }
4466 }
4467 if got == 0 { ev_set(out, VAL_FLOAT, nx_f64_div(ji2f(0), ji2f(0))); return 0 }
4468 ev_set(out, VAL_NUM, sign * v)
4469 return 0
4470}
4471// shared VAL_REGEX builder: compile pattern bytes + flag bytes -> a regex record. (patb persists as a heap
4472// string's bytes; the compiled *Regex is self-contained so match doesn't need patb.)
4473func js_make_regex_val(patb: *u8, patlen: i64, flb: *u8, fllen: i64, out: *i64) -> i64 {
4474 let fbits: i64 = nx_regex_flags(flb, fllen)
4475 let re: *Regex = nx_regex_compile(patb, patlen, fbits)
4476 let rec: *i64 = sys_mmap(5 * 8) as *i64
4477 rec[0] = re as i64; rec[1] = patb as i64; rec[2] = patlen; rec[3] = fbits; rec[4] = 0 // rec[4] = lastIndex
4478 ev_set(out, VAL_REGEX, rec as i64)
4479 return 0
4480}
4481// new RegExp(pat[,flags]) / RegExp(...): pat=string(compiled) or a regex(cloned); flags=optional string.
4482func bi_regex_ctor(argbuf: *i64, argc: i64, out: *i64) -> i64 {
4483 if argc == 0 { let e: *i64 = ev_str_new(0); return js_make_regex_val(ev_str_bytes(e), 0, ev_str_bytes(e), 0, out) }
4484 if argbuf[0] == VAL_REGEX { ev_set(out, VAL_REGEX, argbuf[1]); return 0 } // clone
4485 let patrec: *i64 = ev_coerce_str_arg(argbuf, 0)
4486 let e0: *i64 = ev_str_new(0)
4487 var flb: *u8 = ev_str_bytes(e0)
4488 var fllen: i64 = 0
4489 if argc >= 2 { let flrec: *i64 = ev_coerce_str_arg(argbuf, 1); flb = ev_str_bytes(flrec); fllen = ev_str_len(flrec) }
4490 return js_make_regex_val(ev_str_bytes(patrec), ev_str_len(patrec), flb, fllen, out)
4491}
4492// str.split(re): split on each (non-empty) regex match. zero-width matches are skipped (empty-sep = named open).
4493func bi_str_split(thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
4494 if thisv[0] != VAL_STR { ev_set(out, VAL_UNDEF, 0); return 1 }
4495 let srec: *i64 = (thisv[1]) as *i64
4496 let sb: *u8 = ev_str_bytes(srec); let sl: i64 = ev_str_len(srec)
4497 let a: *i64 = arr_new()
4498 if argc < 1 { arr_set(a, 0, VAL_STR, srec as i64); ev_set(out, VAL_ARRAY, a as i64); return 0 }
4499 // STRING separator: "a,b,c".split(",") -> ["a","b","c"] ("5,5".split(",") drives RayTrace's pixelSize).
4500 // Empty "" separator -> one element per character (real JS).
4501 if argbuf[0] == VAL_STR {
4502 let seprec: *i64 = (argbuf[1]) as *i64
4503 let pb: *u8 = ev_str_bytes(seprec)
4504 let pl: i64 = ev_str_len(seprec)
4505 if pl == 0 {
4506 var ci: i64 = 0
4507 while ci < sl { arr_set(a, ci, VAL_STR, (js_substr(sb, ci, ci + 1)) as i64); ci = ci + 1 }
4508 ev_set(out, VAL_ARRAY, a as i64); return 0
4509 }
4510 var start: i64 = 0
4511 var pos: i64 = 0
4512 var cnt: i64 = 0
4513 while (pos + pl) <= sl {
4514 var m: i64 = 1
4515 var k: i64 = 0
4516 while k < pl { if (sb[pos + k] & 0xff) != (pb[k] & 0xff) { m = 0; k = pl } else { k = k + 1 } }
4517 if m == 1 {
4518 arr_set(a, cnt, VAL_STR, (js_substr(sb, start, pos)) as i64); cnt = cnt + 1
4519 pos = pos + pl; start = pos
4520 } else { pos = pos + 1 }
4521 }
4522 arr_set(a, cnt, VAL_STR, (js_substr(sb, start, sl)) as i64)
4523 ev_set(out, VAL_ARRAY, a as i64); return 0
4524 }
4525 if argbuf[0] != VAL_REGEX { arr_set(a, 0, VAL_STR, srec as i64); ev_set(out, VAL_ARRAY, a as i64); return 0 }
4526 let rrec: *i64 = (argbuf[1]) as *i64
4527 let re: *Regex = (rrec[0]) as *Regex
4528 let saves: *i64 = sys_mmap(re.nsave * 8) as *i64
4529 var pos: i64 = 0; var start: i64 = 0; var cnt: i64 = 0; var go: i64 = 1
4530 while go == 1 {
4531 let ms: i64 = nx_regex_exec(re, sb, sl, pos, saves)
4532 if ms < 0 { go = 0 } else {
4533 let me: i64 = saves[1]
4534 if me == ms { pos = ms + 1; if pos > sl { go = 0 } }
4535 else { arr_set(a, cnt, VAL_STR, (js_substr(sb, start, ms)) as i64); cnt = cnt + 1; start = me; pos = me; if pos > sl { go = 0 } }
4536 }
4537 }
4538 arr_set(a, cnt, VAL_STR, (js_substr(sb, start, sl)) as i64)
4539 ev_set(out, VAL_ARRAY, a as i64)
4540 return 0
4541}
4542// substring sb[lo..hi) as a fresh string rec.
4543func js_substr(sb: *u8, lo: i64, hi: i64) -> *i64 {
4544 var n: i64 = hi - lo
4545 if n < 0 { n = 0 }
4546 let rec: *i64 = ev_str_new(n)
4547 let db: *u8 = ev_str_bytes(rec)
4548 var i: i64 = 0
4549 while i < n { db[i] = sb[lo + i]; i = i + 1 }
4550 return rec
4551}
4552// build a match-result array [fullmatch, g1, g2, ...] from `saves`; undefined for unmatched groups.
4553func js_regex_result(re: *Regex, sb: *u8, saves: *i64) -> *i64 {
4554 let a: *i64 = arr_new()
4555 let ng: i64 = re.ngroup
4556 var gi: i64 = 0
4557 while gi <= ng {
4558 let lo: i64 = saves[2 * gi]; let hi: i64 = saves[2 * gi + 1]
4559 if lo < 0 { arr_set(a, gi, VAL_UNDEF, 0) } else { arr_set(a, gi, VAL_STR, (js_substr(sb, lo, hi)) as i64) }
4560 gi = gi + 1
4561 }
4562 return a
4563}
4564// re.exec(str): [full,g1,..] or null. With the g flag, resumes from + advances the regex record's lastIndex
4565// (rec[4]) so `while((m=re.exec(s)))` walks every match; a null result resets lastIndex to 0.
4566func bi_re_exec(thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
4567 if thisv[0] != VAL_REGEX { ev_set(out, VAL_UNDEF, 0); return 1 }
4568 let rec: *i64 = (thisv[1]) as *i64
4569 let re: *Regex = (rec[0]) as *Regex
4570 if argc < 1 { ev_set(out, VAL_NULL, 0); return 0 }
4571 let srec: *i64 = ev_coerce_str_arg(argbuf, 0)
4572 let sb: *u8 = ev_str_bytes(srec); let sl: i64 = ev_str_len(srec)
4573 let g: i64 = rec[3] & RXF_G
4574 var start: i64 = 0
4575 if g != 0 { start = rec[4]; if start < 0 { start = 0 } }
4576 let saves: *i64 = sys_mmap(re.nsave * 8) as *i64
4577 let ms: i64 = nx_regex_exec(re, sb, sl, start, saves)
4578 if ms < 0 { if g != 0 { rec[4] = 0 } ev_set(out, VAL_NULL, 0); return 0 }
4579 let me: i64 = saves[1]
4580 if g != 0 { if me == ms { rec[4] = me + 1 } else { rec[4] = me } }
4581 ev_set(out, VAL_ARRAY, (js_regex_result(re, sb, saves)) as i64)
4582 return 0
4583}
4584// str.match(re): g-flag -> array of ALL full-match strings; else -> [full, g1, g2, ...]; no match -> null.
4585func bi_str_match(thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
4586 if thisv[0] != VAL_STR { ev_set(out, VAL_UNDEF, 0); return 1 }
4587 if argc < 1 { ev_set(out, VAL_NULL, 0); return 0 }
4588 if argbuf[0] != VAL_REGEX { ev_set(out, VAL_NULL, 0); return 0 }
4589 let srec: *i64 = (thisv[1]) as *i64
4590 let sb: *u8 = ev_str_bytes(srec); let sl: i64 = ev_str_len(srec)
4591 let rrec: *i64 = (argbuf[1]) as *i64
4592 let re: *Regex = (rrec[0]) as *Regex
4593 let saves: *i64 = sys_mmap(re.nsave * 8) as *i64
4594 if (rrec[3] & RXF_G) != 0 {
4595 let a: *i64 = arr_new()
4596 var pos: i64 = 0; var cnt: i64 = 0; var go: i64 = 1
4597 while go == 1 {
4598 let ms: i64 = nx_regex_exec(re, sb, sl, pos, saves)
4599 if ms < 0 { go = 0 } else {
4600 let me: i64 = saves[1]
4601 arr_set(a, cnt, VAL_STR, (js_substr(sb, ms, me)) as i64); cnt = cnt + 1
4602 if me == ms { pos = ms + 1 } else { pos = me }
4603 if pos > sl { go = 0 }
4604 }
4605 }
4606 if cnt == 0 { ev_set(out, VAL_NULL, 0); return 0 }
4607 ev_set(out, VAL_ARRAY, a as i64); return 0
4608 }
4609 let ms: i64 = nx_regex_exec(re, sb, sl, 0, saves)
4610 if ms < 0 { ev_set(out, VAL_NULL, 0); return 0 }
4611 let a: *i64 = arr_new()
4612 let ng: i64 = re.ngroup
4613 var gi: i64 = 0
4614 while gi <= ng {
4615 let lo: i64 = saves[2 * gi]; let hi: i64 = saves[2 * gi + 1]
4616 if lo < 0 { arr_set(a, gi, VAL_UNDEF, 0) } else { arr_set(a, gi, VAL_STR, (js_substr(sb, lo, hi)) as i64) }
4617 gi = gi + 1
4618 }
4619 ev_set(out, VAL_ARRAY, a as i64); return 0
4620}
4621// append a replacement template (literal + $& whole-match + $1..$9 groups + $$) into buf at bpp[0].
4622func js_repl_expand(rb: *u8, rl: i64, sb: *u8, saves: *i64, ng: i64, buf: *u8, bpp: *i64) -> i64 {
4623 var i: i64 = 0
4624 while i < rl {
4625 let c: i64 = (rb[i]) as i64
4626 if c == 36 { if i + 1 < rl {
4627 let n: i64 = (rb[i + 1]) as i64
4628 if n == 36 { buf[bpp[0]] = 36 as u8; bpp[0] = bpp[0] + 1; i = i + 2 }
4629 else { if n == 38 { let lo: i64 = saves[0]; let hi: i64 = saves[1]; var k: i64 = lo; while k < hi { buf[bpp[0]] = sb[k]; bpp[0] = bpp[0] + 1; k = k + 1 } i = i + 2 }
4630 else { var isg: i64 = 0; if n >= 49 { if n <= 57 { isg = 1 } }
4631 if isg == 1 { let g: i64 = n - 48; if g <= ng { let lo: i64 = saves[2 * g]; let hi: i64 = saves[2 * g + 1]; if lo >= 0 { var k: i64 = lo; while k < hi { buf[bpp[0]] = sb[k]; bpp[0] = bpp[0] + 1; k = k + 1 } } } i = i + 2 }
4632 else { buf[bpp[0]] = 36 as u8; bpp[0] = bpp[0] + 1; i = i + 1 } } }
4633 } else { buf[bpp[0]] = 36 as u8; bpp[0] = bpp[0] + 1; i = i + 1 } }
4634 else { buf[bpp[0]] = c as u8; bpp[0] = bpp[0] + 1; i = i + 1 }
4635 }
4636 return 0
4637}
4638// str.replace(re, repl): regex replace. g-flag -> all matches, else first. repl = string with $ substitution.
4639func bi_str_replace(thisv: *i64, argbuf: *i64, argc: i64, out: *i64) -> i64 {
4640 if thisv[0] != VAL_STR { ev_set(out, VAL_UNDEF, 0); return 1 }
4641 let srec: *i64 = (thisv[1]) as *i64
4642 let sb: *u8 = ev_str_bytes(srec);