nx_js_lex.nx source
↩ module page · 653 lines · 33002 B
1// nx_js_lex.nx -- R-JS-LEX (WB-JS-001 rung 0): the ECMAScript TOKENIZER, the
2// hardware-up floor of the sovereign JS engine (browser-engine-exceed roadmap §2).
3// Founds R-JS-PARSE above it (no-floating law: the lexer is built+gated before any
4// parser rides it). Shape = STATE_MACHINE/STRUCT_WALK over source bytes -- an
5// EXISTING emitter shape, so team-authorable now (NOT blocked on X-AUT-006c).
6//
7// Scans ECMAScript source into a flat token stream (3 i64 slots per token:
8// kind, start-offset, byte-length). Covers the lexical core real JS uses:
9// - whitespace + line-terminators (skipped; ASI is a PARSER concern, rung 1)
10// - // line comments and /* block */ comments (skipped)
11// - identifiers + keywords ($ and _ legal; keyword set classified)
12// - numeric literals: decimal int/float, exponent, 0x/0o/0b prefixes
13// - string literals ' and " with backslash-escape handling (\" \' \\ \n ...)
14// - punctuators incl. multi-char operators (=== !== >>>= => ?. ?? ** etc.)
15// - unterminated string -> ERROR token (honest, never silently swallowed)
16//
17// HONEST RUNG-0 SCOPE (named OPEN, NOT faked): template literals (`...${}`) and
18// regex-literal disambiguation need PARSER context (is `/` a divide or a regex?),
19// so they are R-JS-LEX-0b, built once the parser can feed back context. BigInt `n`
20// suffix + full Unicode IdentifierStart are also 0b. Rung 0 tokenizes ASCII JS.
21//
22// GATE (main): 7 KATs assert the exact token stream (kind + lexeme) on real JS
23// snippets + TAMPER (unterminated string must be ERROR, never a fabricated STRING).
24// Self-validating; exit 0 iff all pass; appends knowledge/status/js_engine.log.
25//
26// license_tier: ORIGINAL (tutor-bootstrap scaffold; team re-authors from the
27// R-JS-LEX data spec via author=organ -- (B)-debt, mirror the h2 capstone note.)
28import "nx_syscalls.nx"
29import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
30
31const JS_TOK_EOF: i64 = 0
32const JS_TOK_IDENT: i64 = 1
33const JS_TOK_KEYWORD: i64 = 2
34const JS_TOK_NUMBER: i64 = 3
35const JS_TOK_STRING: i64 = 4
36const JS_TOK_PUNCT: i64 = 5
37const JS_TOK_ERROR: i64 = 8
38const JS_TOK_TEMPLATE: i64 = 9 // template literal: whole span incl both backticks; interior re-lexed at eval
39const JS_TOK_REGEX: i64 = 10 // regex literal /pattern/flags: whole span; pattern+flags extracted at eval
40
41// ---- byte classification ----
42func js_is_digit(c: i64) -> i64 { if c >= 48 { if c <= 57 { return 1 } } return 0 }
43func js_is_alpha(c: i64) -> i64 {
44 if c >= 65 { if c <= 90 { return 1 } }
45 if c >= 97 { if c <= 122 { return 1 } }
46 return 0
47}
48// ECMAScript identifiers admit Unicode letters, not just ASCII. This lexer is BYTE
49// oriented, so every byte of a UTF-8 multi-byte sequence (0x80..0xFF) counts as an
50// identifier byte. MEASURED MOTIVATION (2026-07-27): netflix's 3.9 MB bundle fails to
51// parse at a lodash deburr map -- `{À:"A",Á:"A",à:"a",...}` -- bare non-ASCII identifier
52// KEYS, which are near-universal in bundled apps. Isolated with controls by
53// nx_js_unicode_probe (quoted non-ASCII keys already worked; bare ones did not).
54// This is deliberately PERMISSIVE: it accepts some code points real JS reserves for
55// punctuation, which only ever makes us accept MORE programs -- it can never change the
56// meaning of a valid one.
57// ⚠RESIDUAL RISK (named, not hidden): Unicode SPACE characters are also >=0x80 (NBSP
58// U+00A0 = C2 A0, U+2028/9, BOM U+FEFF). Used as a separator ADJACENT to an identifier
59// they would now be absorbed into it. Minified bundles -- the entire point of this
60// change -- separate with ASCII space, so this does not arise in practice; a proper fix
61// is a UTF-8 decode + Unicode ID_Start/ID_Continue table, a separate bounded rung.
62func js_is_idstart(c: i64) -> i64 {
63 if js_is_alpha(c) == 1 { return 1 }
64 if c == 95 { return 1 } // _
65 if c == 36 { return 1 } // $
66 if c >= 128 { return 1 } // UTF-8 lead/continuation byte = part of a Unicode letter
67 return 0
68}
69func js_is_idpart(c: i64) -> i64 {
70 if js_is_idstart(c) == 1 { return 1 }
71 if js_is_digit(c) == 1 { return 1 }
72 return 0
73}
74func js_is_hex(c: i64) -> i64 {
75 if js_is_digit(c) == 1 { return 1 }
76 if c >= 65 { if c <= 70 { return 1 } } // A-F
77 if c >= 97 { if c <= 102 { return 1 } } // a-f
78 return 0
79}
80func js_is_space(c: i64) -> i64 {
81 if c == 32 { return 1 } // space
82 if c == 9 { return 1 } // tab
83 if c == 10 { return 1 } // \n
84 if c == 13 { return 1 } // \r
85 if c == 11 { return 1 } // \v
86 if c == 12 { return 1 } // \f
87 return 0
88}
89
90// ---- lexeme compare: src[start..start+len) == NUL-terminated lit ? ----
91func js_lexeme_eq(src: *u8, start: i64, len: i64, lit: *u8) -> i64 {
92 var i: i64 = 0
93 while i < len {
94 if (lit[i] & 0xff) == 0 { return 0 } // lit shorter than len
95 if (src[start + i] & 0xff) != (lit[i] & 0xff) { return 0 }
96 i = i + 1
97 }
98 if (lit[len] & 0xff) != 0 { return 0 } // lit longer than len
99 return 1
100}
101
102// ---- keyword classification (ECMAScript reserved + common contextual) ----
103func js_is_keyword(src: *u8, start: i64, len: i64) -> i64 {
104 if js_lexeme_eq(src, start, len, "var\x00" as *u8) == 1 { return 1 }
105 if js_lexeme_eq(src, start, len, "let\x00" as *u8) == 1 { return 1 }
106 if js_lexeme_eq(src, start, len, "const\x00" as *u8) == 1 { return 1 }
107 if js_lexeme_eq(src, start, len, "function\x00" as *u8) == 1 { return 1 }
108 if js_lexeme_eq(src, start, len, "return\x00" as *u8) == 1 { return 1 }
109 if js_lexeme_eq(src, start, len, "if\x00" as *u8) == 1 { return 1 }
110 if js_lexeme_eq(src, start, len, "else\x00" as *u8) == 1 { return 1 }
111 if js_lexeme_eq(src, start, len, "for\x00" as *u8) == 1 { return 1 }
112 if js_lexeme_eq(src, start, len, "while\x00" as *u8) == 1 { return 1 }
113 if js_lexeme_eq(src, start, len, "do\x00" as *u8) == 1 { return 1 }
114 if js_lexeme_eq(src, start, len, "break\x00" as *u8) == 1 { return 1 }
115 if js_lexeme_eq(src, start, len, "continue\x00" as *u8) == 1 { return 1 }
116 if js_lexeme_eq(src, start, len, "switch\x00" as *u8) == 1 { return 1 }
117 if js_lexeme_eq(src, start, len, "case\x00" as *u8) == 1 { return 1 }
118 if js_lexeme_eq(src, start, len, "default\x00" as *u8) == 1 { return 1 }
119 if js_lexeme_eq(src, start, len, "new\x00" as *u8) == 1 { return 1 }
120 if js_lexeme_eq(src, start, len, "delete\x00" as *u8) == 1 { return 1 }
121 if js_lexeme_eq(src, start, len, "typeof\x00" as *u8) == 1 { return 1 }
122 if js_lexeme_eq(src, start, len, "instanceof\x00" as *u8) == 1 { return 1 }
123 if js_lexeme_eq(src, start, len, "in\x00" as *u8) == 1 { return 1 }
124 if js_lexeme_eq(src, start, len, "of\x00" as *u8) == 1 { return 1 }
125 if js_lexeme_eq(src, start, len, "this\x00" as *u8) == 1 { return 1 }
126 if js_lexeme_eq(src, start, len, "null\x00" as *u8) == 1 { return 1 }
127 if js_lexeme_eq(src, start, len, "true\x00" as *u8) == 1 { return 1 }
128 if js_lexeme_eq(src, start, len, "false\x00" as *u8) == 1 { return 1 }
129 if js_lexeme_eq(src, start, len, "void\x00" as *u8) == 1 { return 1 }
130 if js_lexeme_eq(src, start, len, "try\x00" as *u8) == 1 { return 1 }
131 if js_lexeme_eq(src, start, len, "catch\x00" as *u8) == 1 { return 1 }
132 if js_lexeme_eq(src, start, len, "finally\x00" as *u8) == 1 { return 1 }
133 if js_lexeme_eq(src, start, len, "throw\x00" as *u8) == 1 { return 1 }
134 if js_lexeme_eq(src, start, len, "class\x00" as *u8) == 1 { return 1 }
135 if js_lexeme_eq(src, start, len, "extends\x00" as *u8) == 1 { return 1 }
136 if js_lexeme_eq(src, start, len, "super\x00" as *u8) == 1 { return 1 }
137 if js_lexeme_eq(src, start, len, "import\x00" as *u8) == 1 { return 1 }
138 if js_lexeme_eq(src, start, len, "export\x00" as *u8) == 1 { return 1 }
139 if js_lexeme_eq(src, start, len, "async\x00" as *u8) == 1 { return 1 }
140 if js_lexeme_eq(src, start, len, "await\x00" as *u8) == 1 { return 1 }
141 if js_lexeme_eq(src, start, len, "yield\x00" as *u8) == 1 { return 1 }
142 return 0
143}
144
145// ---- scanners (each returns the END index just past the token) ----
146func js_scan_ident(src: *u8, srclen: i64, pos: i64) -> i64 {
147 var e: i64 = pos + 1
148 var k: i64 = 1
149 while k == 1 {
150 if e >= srclen { k = 0 }
151 if k == 1 { if js_is_idpart(src[e] & 0xff) == 1 { e = e + 1 } else { k = 0 } }
152 }
153 return e
154}
155func js_scan_while_digit(src: *u8, srclen: i64, pos: i64) -> i64 {
156 var e: i64 = pos
157 var k: i64 = 1
158 while k == 1 {
159 if e >= srclen { k = 0 }
160 if k == 1 { if js_is_digit(src[e] & 0xff) == 1 { e = e + 1 } else { k = 0 } }
161 }
162 return e
163}
164func js_scan_while_hex(src: *u8, srclen: i64, pos: i64) -> i64 {
165 var e: i64 = pos
166 var k: i64 = 1
167 while k == 1 {
168 if e >= srclen { k = 0 }
169 if k == 1 { if js_is_hex(src[e] & 0xff) == 1 { e = e + 1 } else { k = 0 } }
170 }
171 return e
172}
173func js_scan_exp(src: *u8, srclen: i64, pos: i64) -> i64 {
174 var e: i64 = pos + 1 // skip e/E
175 if e < srclen {
176 let s: i64 = src[e] & 0xff
177 if s == 43 { e = e + 1 } // +
178 if s == 45 { e = e + 1 } // -
179 }
180 e = js_scan_while_digit(src, srclen, e)
181 return e
182}
183func js_scan_number(src: *u8, srclen: i64, pos: i64) -> i64 {
184 var e: i64 = pos
185 if (src[e] & 0xff) == 48 { // leading '0' -> maybe radix prefix
186 if e + 1 < srclen {
187 let n1: i64 = src[e + 1] & 0xff
188 if n1 == 120 { return js_scan_while_hex(src, srclen, e + 2) } // 0x
189 if n1 == 88 { return js_scan_while_hex(src, srclen, e + 2) } // 0X
190 if n1 == 98 { return js_scan_while_digit(src, srclen, e + 2) } // 0b
191 if n1 == 66 { return js_scan_while_digit(src, srclen, e + 2) } // 0B
192 if n1 == 111 { return js_scan_while_digit(src, srclen, e + 2) } // 0o
193 if n1 == 79 { return js_scan_while_digit(src, srclen, e + 2) } // 0O
194 }
195 }
196 e = js_scan_while_digit(src, srclen, e) // integer part
197 if e < srclen { if (src[e] & 0xff) == 46 { // '.' fraction
198 e = js_scan_while_digit(src, srclen, e + 1)
199 } }
200 if e < srclen {
201 let ec: i64 = src[e] & 0xff
202 if ec == 101 { e = js_scan_exp(src, srclen, e) } // e
203 if ec == 69 { e = js_scan_exp(src, srclen, e) } // E
204 }
205 return e
206}
207// string: returns END (past close quote) if closed, else 0 - END if unterminated.
208func js_scan_string(src: *u8, srclen: i64, pos: i64) -> i64 {
209 let q: i64 = src[pos] & 0xff
210 var e: i64 = pos + 1
211 var k: i64 = 1
212 var closed: i64 = 0
213 while k == 1 {
214 if e >= srclen { k = 0 }
215 if k == 1 {
216 let c: i64 = src[e] & 0xff
217 if c == 92 { e = e + 2 } // backslash escape -> skip next char
218 else {
219 if c == q { e = e + 1; closed = 1; k = 0 }
220 else { e = e + 1 }
221 }
222 }
223 }
224 if closed == 1 { return e }
225 return 0 - e
226}
227
228// punctuator length at pos (1..4), or 0 if pos is not a punctuator.
229func js_punct_len(src: *u8, srclen: i64, pos: i64) -> i64 {
230 let c: i64 = src[pos] & 0xff
231 var c1: i64 = 0
232 var c2: i64 = 0
233 var c3: i64 = 0
234 if pos + 1 < srclen { c1 = src[pos + 1] & 0xff }
235 if pos + 2 < srclen { c2 = src[pos + 2] & 0xff }
236 if pos + 3 < srclen { c3 = src[pos + 3] & 0xff }
237 if c == 62 { // >
238 if c1 == 62 { // >>
239 if c2 == 62 { if c3 == 61 { return 4 } return 3 } // >>> , >>>=
240 if c2 == 61 { return 3 } // >>=
241 return 2
242 }
243 if c1 == 61 { return 2 } // >=
244 return 1
245 }
246 if c == 60 { // <
247 if c1 == 60 { if c2 == 61 { return 3 } return 2 } // << , <<=
248 if c1 == 61 { return 2 } // <=
249 return 1
250 }
251 if c == 61 { // =
252 if c1 == 61 { if c2 == 61 { return 3 } return 2 } // == , ===
253 if c1 == 62 { return 2 } // =>
254 return 1
255 }
256 if c == 33 { // !
257 if c1 == 61 { if c2 == 61 { return 3 } return 2 } // != , !==
258 return 1
259 }
260 if c == 38 { // &
261 if c1 == 38 { if c2 == 61 { return 3 } return 2 } // && , &&=
262 if c1 == 61 { return 2 } // &=
263 return 1
264 }
265 if c == 124 { // |
266 if c1 == 124 { if c2 == 61 { return 3 } return 2 } // || , ||=
267 if c1 == 61 { return 2 } // |=
268 return 1
269 }
270 if c == 63 { // ?
271 if c1 == 63 { if c2 == 61 { return 3 } return 2 } // ?? , ??=
272 // `?.` is OPTIONAL CHAINING only when NOT followed by a decimal digit
273 // (ECMAScript: OptionalChainingPunctuator :: ?. [lookahead is not DecimalDigit]).
274 // Otherwise it is a TERNARY whose consequent is a leading-dot number:
275 // `e.disabledStyleProp?.5:1` == `e.disabledStyleProp ? .5 : 1`. Minifiers emit
276 // `.5` for `0.5`, so greedily taking `?.` broke real bundles -- MEASURED in
277 // netflix's bundle 2026-07-27; isolated by nx_js_bundle_probe S6/S7 (`.5` alone
278 // and `a?.b` alone both already passed, only the combination failed).
279 if c1 == 46 { if js_is_digit(c2) == 1 { return 1 } return 2 } // ?. vs ? .5
280 return 1
281 }
282 if c == 42 { // *
283 if c1 == 42 { if c2 == 61 { return 3 } return 2 } // ** , **=
284 if c1 == 61 { return 2 } // *=
285 return 1
286 }
287 if c == 43 { if c1 == 43 { return 2 } if c1 == 61 { return 2 } return 1 } // ++ += +
288 if c == 45 { if c1 == 45 { return 2 } if c1 == 61 { return 2 } return 1 } // -- -= -
289 if c == 47 { if c1 == 61 { return 2 } return 1 } // /= / (// /* handled in trivia)
290 if c == 37 { if c1 == 61 { return 2 } return 1 } // %= %
291 if c == 94 { if c1 == 61 { return 2 } return 1 } // ^= ^
292 if c == 46 { if c1 == 46 { if c2 == 46 { return 3 } } return 1 } // ... .
293 if c == 123 { return 1 } // {
294 if c == 125 { return 1 } // }
295 if c == 40 { return 1 } // (
296 if c == 41 { return 1 } // )
297 if c == 91 { return 1 } // [
298 if c == 93 { return 1 } // ]
299 if c == 59 { return 1 } // ;
300 if c == 44 { return 1 } // ,
301 if c == 58 { return 1 } // :
302 if c == 126 { return 1 } // ~
303 return 0
304}
305
306// skip whitespace + // line and /* block */ comments; return first real-token pos.
307func js_skip_trivia(src: *u8, srclen: i64, pos: i64) -> i64 {
308 var p: i64 = pos
309 var go: i64 = 1
310 while go == 1 {
311 var k: i64 = 1
312 while k == 1 {
313 if p >= srclen { k = 0 } else { if js_is_space(src[p] & 0xff) == 1 { p = p + 1 } else { k = 0 } }
314 }
315 var did: i64 = 0
316 if p + 1 < srclen { if (src[p] & 0xff) == 47 { if (src[p + 1] & 0xff) == 47 { // //
317 p = p + 2
318 var k2: i64 = 1
319 while k2 == 1 {
320 if p >= srclen { k2 = 0 } else { if (src[p] & 0xff) == 10 { k2 = 0 } else { p = p + 1 } }
321 }
322 did = 1
323 } } }
324 if did == 0 { if p + 1 < srclen { if (src[p] & 0xff) == 47 { if (src[p + 1] & 0xff) == 42 { // /*
325 p = p + 2
326 var k3: i64 = 1
327 while k3 == 1 {
328 if p + 1 >= srclen { p = srclen; k3 = 0 }
329 else {
330 if (src[p] & 0xff) == 42 { if (src[p + 1] & 0xff) == 47 { p = p + 2; k3 = 0 } else { p = p + 1 } }
331 else { p = p + 1 }
332 }
333 }
334 did = 1
335 } } } }
336 if did == 0 { go = 0 }
337 }
338 return p
339}
340
341func js_emit(toks: *i64, ntok_box: *i64, kind: i64, start: i64, len: i64) -> i64 {
342 let i: i64 = ntok_box[0]
343 toks[i * 3 + 0] = kind
344 toks[i * 3 + 1] = start
345 toks[i * 3 + 2] = len
346 ntok_box[0] = i + 1
347 return 0
348}
349
350func js_num_starts(src: *u8, srclen: i64, pos: i64) -> i64 {
351 let c: i64 = src[pos] & 0xff
352 if js_is_digit(c) == 1 { return 1 }
353 if c == 46 { if pos + 1 < srclen { if js_is_digit(src[pos + 1] & 0xff) == 1 { return 1 } } } // .5
354 return 0
355}
356
357func js_lex_string(src: *u8, srclen: i64, pos: i64, toks: *i64, ntok_box: *i64) -> i64 {
358 let r: i64 = js_scan_string(src, srclen, pos)
359 if r > 0 { js_emit(toks, ntok_box, JS_TOK_STRING, pos, r - pos); return r }
360 let e: i64 = 0 - r
361 js_emit(toks, ntok_box, JS_TOK_ERROR, pos, e - pos) // unterminated -> ERROR
362 return e
363}
364
365// scan a template literal `...${expr}...` : emit ONE JS_TOK_TEMPLATE token spanning both backticks.
366// Handles \-escapes and ${ ... } interior brace-nesting (so a `}` inside ${} does not end it early).
367// The interior (literal runs + ${expr}) is decoded at EVAL time. Nested templates/strings-with-braces
368// inside ${} are a NAMED OPEN (brace-depth only, no string awareness inside interpolation).
369func js_lex_template(src: *u8, srclen: i64, pos: i64, toks: *i64, ntok_box: *i64) -> i64 {
370 var p: i64 = pos + 1
371 var go: i64 = 1
372 while go == 1 {
373 if p >= srclen { js_emit(toks, ntok_box, JS_TOK_ERROR, pos, p - pos); return p } // unterminated
374 let ch: i64 = src[p] & 0xff
375 if ch == 92 { p = p + 2 } // \-escape: skip 2
376 else {
377 if ch == 96 { go = 0 } // closing `
378 else {
379 var handled: i64 = 0
380 if ch == 36 { if p + 1 < srclen { if (src[p + 1] & 0xff) == 123 { // ${
381 p = p + 2
382 var depth: i64 = 1
383 var g2: i64 = 1
384 while g2 == 1 {
385 if p >= srclen { g2 = 0 }
386 else {
387 let c2: i64 = src[p] & 0xff
388 if c2 == 123 { depth = depth + 1; p = p + 1 }
389 else { if c2 == 125 { depth = depth - 1; p = p + 1; if depth == 0 { g2 = 0 } } else { p = p + 1 } }
390 }
391 }
392 handled = 1
393 } } }
394 if handled == 0 { p = p + 1 }
395 }
396 }
397 }
398 js_emit(toks, ntok_box, JS_TOK_TEMPLATE, pos, (p + 1) - pos)
399 return p + 1
400}
401// R-JS-LEX-0b: can a `/` at this point START A REGEX (vs be division)? Decided by the PREVIOUS token.
402// After a VALUE (ident/number/string/template/`)`/`]`/`}`/this/true/false/null/super) => division.
403// After anything else (operators, ( [ { , ; = return typeof new ... , or start-of-input) => regex.
404// (`}` is treated as a value here -- the one classic ambiguity; block-`}` followed by /re/ is vanishingly
405// rare in real/minified code. Noted.)
406func js_regex_context(toks: *i64, ntok: i64, src: *u8) -> i64 {
407 if ntok == 0 { return 1 }
408 let b: i64 = (ntok - 1) * 3
409 let pk: i64 = toks[b]
410 let ps: i64 = toks[b + 1]
411 let pl: i64 = toks[b + 2]
412 if pk == JS_TOK_NUMBER { return 0 }
413 if pk == JS_TOK_STRING { return 0 }
414 if pk == JS_TOK_TEMPLATE { return 0 }
415 if pk == JS_TOK_IDENT { return 0 }
416 if pk == JS_TOK_KEYWORD {
417 if js_lexeme_eq(src, ps, pl, "this\x00" as *u8) == 1 { return 0 }
418 if js_lexeme_eq(src, ps, pl, "true\x00" as *u8) == 1 { return 0 }
419 if js_lexeme_eq(src, ps, pl, "false\x00" as *u8) == 1 { return 0 }
420 if js_lexeme_eq(src, ps, pl, "null\x00" as *u8) == 1 { return 0 }
421 if js_lexeme_eq(src, ps, pl, "super\x00" as *u8) == 1 { return 0 }
422 return 1
423 }
424 if pk == JS_TOK_PUNCT {
425 if js_lexeme_eq(src, ps, pl, ")\x00" as *u8) == 1 { return 0 }
426 if js_lexeme_eq(src, ps, pl, "]\x00" as *u8) == 1 { return 0 }
427 if js_lexeme_eq(src, ps, pl, "}\x00" as *u8) == 1 { return 0 }
428 return 1
429 }
430 return 1
431}
432// scan a regex literal /pattern/flags -> ONE JS_TOK_REGEX token. `/` closes only OUTSIDE a `[..]` class;
433// `\` escapes the next char; an unescaped newline (or EOF) before the close = ERROR (never a fabricated regex).
434func js_lex_regex(src: *u8, srclen: i64, pos: i64, toks: *i64, ntok_box: *i64) -> i64 {
435 var p: i64 = pos + 1
436 var inclass: i64 = 0
437 var go: i64 = 1
438 while go == 1 {
439 if p >= srclen { js_emit(toks, ntok_box, JS_TOK_ERROR, pos, p - pos); return p }
440 let ch: i64 = src[p] & 0xff
441 if ch == 92 { p = p + 2 }
442 else {
443 if ch == 10 { js_emit(toks, ntok_box, JS_TOK_ERROR, pos, p - pos); return p }
444 else { if ch == 91 { inclass = 1; p = p + 1 }
445 else { if ch == 93 { inclass = 0; p = p + 1 }
446 else { if ch == 47 { if inclass == 0 { go = 0 } else { p = p + 1 } }
447 else { p = p + 1 } } } }
448 }
449 }
450 p = p + 1 // consume closing '/'
451 var gf: i64 = 1
452 while gf == 1 { if p < srclen { let fc: i64 = src[p] & 0xff; if fc >= 97 { if fc <= 122 { p = p + 1 } else { gf = 0 } } else { gf = 0 } } else { gf = 0 } }
453 js_emit(toks, ntok_box, JS_TOK_REGEX, pos, p - pos)
454 return p
455}
456// lex exactly ONE token at pos (pos already past trivia); return END index.
457func js_lex_one(src: *u8, srclen: i64, pos: i64, toks: *i64, ntok_box: *i64) -> i64 {
458 let c: i64 = src[pos] & 0xff
459 if js_is_idstart(c) == 1 {
460 let e: i64 = js_scan_ident(src, srclen, pos)
461 var kind: i64 = JS_TOK_IDENT
462 if js_is_keyword(src, pos, e - pos) == 1 { kind = JS_TOK_KEYWORD }
463 js_emit(toks, ntok_box, kind, pos, e - pos)
464 return e
465 }
466 if js_num_starts(src, srclen, pos) == 1 {
467 let e: i64 = js_scan_number(src, srclen, pos)
468 js_emit(toks, ntok_box, JS_TOK_NUMBER, pos, e - pos)
469 return e
470 }
471 if c == 34 { return js_lex_string(src, srclen, pos, toks, ntok_box) } // "
472 if c == 39 { return js_lex_string(src, srclen, pos, toks, ntok_box) } // '
473 if c == 96 { return js_lex_template(src, srclen, pos, toks, ntok_box) } // ` template literal
474 if c == 47 { if js_regex_context(toks, ntok_box[0], src) == 1 { return js_lex_regex(src, srclen, pos, toks, ntok_box) } } // '/' regex vs divide
475 let pl: i64 = js_punct_len(src, srclen, pos)
476 if pl > 0 { js_emit(toks, ntok_box, JS_TOK_PUNCT, pos, pl); return pos + pl }
477 js_emit(toks, ntok_box, JS_TOK_ERROR, pos, 1) // unknown byte
478 return pos + 1
479}
480
481// PUBLIC: tokenize src[0..srclen) into toks (3 i64/token); returns token count.
482func js_lex(src: *u8, srclen: i64, toks: *i64, maxtoks: i64, ntok_box: *i64) -> i64 {
483 var pos: i64 = 0
484 ntok_box[0] = 0
485 var go: i64 = 1
486 while go == 1 {
487 pos = js_skip_trivia(src, srclen, pos)
488 if pos >= srclen { go = 0 }
489 if go == 1 { if ntok_box[0] >= maxtoks { go = 0 } }
490 if go == 1 { pos = js_lex_one(src, srclen, pos, toks, ntok_box) }
491 }
492 return ntok_box[0]
493}
494
495// ===================== GATE =====================
496func jl_puts(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
497// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
498// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
499// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
500// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
501func jl_putn(v: i64) -> i64 { nxi_out(v); return 0 }
502func jl_strlen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
503
504func js_expect(toks: *i64, ntok: i64, idx: i64, kind: i64, src: *u8, lit: *u8) -> i64 {
505 if idx >= ntok { return 0 }
506 if toks[idx * 3 + 0] != kind { return 0 }
507 return js_lexeme_eq(src, toks[idx * 3 + 1], toks[idx * 3 + 2], lit)
508}
509
510func main() -> i64 {
511 let toks: *i64 = sys_mmap(256 * 3 * 8) as *i64
512 let ntb: *i64 = sys_mmap(16) as *i64
513 var pass: i64 = 0
514 var tot: i64 = 0
515 jl_puts("nx_js_lex gate (R-JS-LEX, WB-JS-001 rung 0)\n" as *u8)
516
517 // ---- KAT 1: keywords/idents/number/punct/dot ----
518 let s1: *u8 = "var x = 42 + foo.bar\x00" as *u8
519 let n1: i64 = js_lex(s1, jl_strlen(s1), toks, 256, ntb)
520 var r1: i64 = 1
521 if n1 != 8 { r1 = 0 }
522 r1 = r1 * js_expect(toks, n1, 0, JS_TOK_KEYWORD, s1, "var\x00" as *u8)
523 r1 = r1 * js_expect(toks, n1, 1, JS_TOK_IDENT, s1, "x\x00" as *u8)
524 r1 = r1 * js_expect(toks, n1, 2, JS_TOK_PUNCT, s1, "=\x00" as *u8)
525 r1 = r1 * js_expect(toks, n1, 3, JS_TOK_NUMBER, s1, "42\x00" as *u8)
526 r1 = r1 * js_expect(toks, n1, 4, JS_TOK_PUNCT, s1, "+\x00" as *u8)
527 r1 = r1 * js_expect(toks, n1, 5, JS_TOK_IDENT, s1, "foo\x00" as *u8)
528 r1 = r1 * js_expect(toks, n1, 6, JS_TOK_PUNCT, s1, ".\x00" as *u8)
529 r1 = r1 * js_expect(toks, n1, 7, JS_TOK_IDENT, s1, "bar\x00" as *u8)
530 if r1 == 1 { jl_puts(" PASS KAT1 var/ident/number/dot (8 toks)\n" as *u8); pass = pass + 1 } else { jl_puts(" FAIL KAT1 ntok=" as *u8); jl_putn(n1); jl_puts("\n" as *u8) }
531 tot = tot + 1
532
533 // ---- KAT 2: multi-char operators incl >>>= ----
534 let s2: *u8 = "a===b!==c>>>=d\x00" as *u8
535 let n2: i64 = js_lex(s2, jl_strlen(s2), toks, 256, ntb)
536 var r2: i64 = 1
537 if n2 != 7 { r2 = 0 }
538 r2 = r2 * js_expect(toks, n2, 0, JS_TOK_IDENT, s2, "a\x00" as *u8)
539 r2 = r2 * js_expect(toks, n2, 1, JS_TOK_PUNCT, s2, "===\x00" as *u8)
540 r2 = r2 * js_expect(toks, n2, 2, JS_TOK_IDENT, s2, "b\x00" as *u8)
541 r2 = r2 * js_expect(toks, n2, 3, JS_TOK_PUNCT, s2, "!==\x00" as *u8)
542 r2 = r2 * js_expect(toks, n2, 4, JS_TOK_IDENT, s2, "c\x00" as *u8)
543 r2 = r2 * js_expect(toks, n2, 5, JS_TOK_PUNCT, s2, ">>>=\x00" as *u8)
544 r2 = r2 * js_expect(toks, n2, 6, JS_TOK_IDENT, s2, "d\x00" as *u8)
545 if r2 == 1 { jl_puts(" PASS KAT2 operators ===/!==/>>>= (7 toks)\n" as *u8); pass = pass + 1 } else { jl_puts(" FAIL KAT2 ntok=" as *u8); jl_putn(n2); jl_puts("\n" as *u8) }
546 tot = tot + 1
547
548 // ---- KAT 3: hex + float-exponent + comments skipped ----
549 let s3: *u8 = "let n = 0xFF; /* c */ z = 3.14e2 // end\x00" as *u8
550 let n3: i64 = js_lex(s3, jl_strlen(s3), toks, 256, ntb)
551 var r3: i64 = 1
552 if n3 != 8 { r3 = 0 }
553 r3 = r3 * js_expect(toks, n3, 0, JS_TOK_KEYWORD, s3, "let\x00" as *u8)
554 r3 = r3 * js_expect(toks, n3, 3, JS_TOK_NUMBER, s3, "0xFF\x00" as *u8)
555 r3 = r3 * js_expect(toks, n3, 4, JS_TOK_PUNCT, s3, ";\x00" as *u8)
556 r3 = r3 * js_expect(toks, n3, 5, JS_TOK_IDENT, s3, "z\x00" as *u8)
557 r3 = r3 * js_expect(toks, n3, 7, JS_TOK_NUMBER, s3, "3.14e2\x00" as *u8)
558 if r3 == 1 { jl_puts(" PASS KAT3 hex/float-exp + comments skipped (8 toks)\n" as *u8); pass = pass + 1 } else { jl_puts(" FAIL KAT3 ntok=" as *u8); jl_putn(n3); jl_puts("\n" as *u8) }
559 tot = tot + 1
560
561 // ---- KAT 4: string literal "hi" (byte-built to avoid source-escape ambiguity) ----
562 let s4: *u8 = sys_mmap(8)
563 s4[0] = 34 as u8; s4[1] = 104 as u8; s4[2] = 105 as u8; s4[3] = 34 as u8 // "hi"
564 let n4: i64 = js_lex(s4, 4, toks, 256, ntb)
565 var r4: i64 = 1
566 if n4 != 1 { r4 = 0 }
567 if toks[0] != JS_TOK_STRING { r4 = 0 }
568 if toks[2] != 4 { r4 = 0 }
569 if r4 == 1 { jl_puts(" PASS KAT4 string \"hi\" (1 STRING, len 4)\n" as *u8); pass = pass + 1 } else { jl_puts(" FAIL KAT4 ntok=" as *u8); jl_putn(n4); jl_puts(" kind=" as *u8); jl_putn(toks[0]); jl_puts("\n" as *u8) }
570 tot = tot + 1
571
572 // ---- KAT 5 (TAMPER): unterminated string -> ERROR, never a fabricated STRING ----
573 let s5: *u8 = sys_mmap(8)
574 s5[0] = 34 as u8; s5[1] = 97 as u8; s5[2] = 98 as u8; s5[3] = 99 as u8 // "abc
575 let n5: i64 = js_lex(s5, 4, toks, 256, ntb)
576 var r5: i64 = 1
577 if n5 != 1 { r5 = 0 }
578 if toks[0] != JS_TOK_ERROR { r5 = 0 }
579 if r5 == 1 { jl_puts(" PASS KAT5 tamper: unterminated string -> ERROR (not STRING)\n" as *u8); pass = pass + 1 } else { jl_puts(" FAIL KAT5 ntok=" as *u8); jl_putn(n5); jl_puts(" kind=" as *u8); jl_putn(toks[0]); jl_puts("\n" as *u8) }
580 tot = tot + 1
581
582 // ---- KAT 6: function decl punctuation ----
583 let s6: *u8 = "function f(){return 1}\x00" as *u8
584 let n6: i64 = js_lex(s6, jl_strlen(s6), toks, 256, ntb)
585 var r6: i64 = 1
586 if n6 != 8 { r6 = 0 }
587 r6 = r6 * js_expect(toks, n6, 0, JS_TOK_KEYWORD, s6, "function\x00" as *u8)
588 r6 = r6 * js_expect(toks, n6, 1, JS_TOK_IDENT, s6, "f\x00" as *u8)
589 r6 = r6 * js_expect(toks, n6, 2, JS_TOK_PUNCT, s6, "(\x00" as *u8)
590 r6 = r6 * js_expect(toks, n6, 5, JS_TOK_KEYWORD, s6, "return\x00" as *u8)
591 r6 = r6 * js_expect(toks, n6, 6, JS_TOK_NUMBER, s6, "1\x00" as *u8)
592 r6 = r6 * js_expect(toks, n6, 7, JS_TOK_PUNCT, s6, "}\x00" as *u8)
593 if r6 == 1 { jl_puts(" PASS KAT6 function decl (8 toks)\n" as *u8); pass = pass + 1 } else { jl_puts(" FAIL KAT6 ntok=" as *u8); jl_putn(n6); jl_puts("\n" as *u8) }
594 tot = tot + 1
595
596 // ---- KAT 7: backslash escape inside string does NOT terminate ('a\nb', literal backslash) ----
597 let s7: *u8 = sys_mmap(8)
598 s7[0] = 39 as u8; s7[1] = 97 as u8; s7[2] = 92 as u8; s7[3] = 110 as u8; s7[4] = 98 as u8; s7[5] = 39 as u8 // 'a\nb'
599 let n7: i64 = js_lex(s7, 6, toks, 256, ntb)
600 var r7: i64 = 1
601 if n7 != 1 { r7 = 0 }
602 if toks[0] != JS_TOK_STRING { r7 = 0 }
603 if toks[2] != 6 { r7 = 0 }
604 if r7 == 1 { jl_puts(" PASS KAT7 escaped quote/backslash handled (1 STRING, len 6)\n" as *u8); pass = pass + 1 } else { jl_puts(" FAIL KAT7 ntok=" as *u8); jl_putn(n7); jl_puts(" len=" as *u8); jl_putn(toks[2]); jl_puts("\n" as *u8) }
605 tot = tot + 1
606
607 // ---- KAT8: regex literal after '=' (expression context) ----
608 let s8: *u8 = "x = /ab+c/gi\x00" as *u8
609 let n8: i64 = js_lex(s8, jl_strlen(s8), toks, 256, ntb)
610 var r8: i64 = 1
611 if n8 != 3 { r8 = 0 }
612 r8 = r8 * js_expect(toks, n8, 0, JS_TOK_IDENT, s8, "x\x00" as *u8)
613 r8 = r8 * js_expect(toks, n8, 1, JS_TOK_PUNCT, s8, "=\x00" as *u8)
614 r8 = r8 * js_expect(toks, n8, 2, JS_TOK_REGEX, s8, "/ab+c/gi\x00" as *u8)
615 if r8 == 1 { jl_puts(" PASS KAT8 regex /ab+c/gi after '='\n" as *u8); pass = pass + 1 } else { jl_puts(" FAIL KAT8 ntok=" as *u8); jl_putn(n8); jl_puts("\n" as *u8) }
616 tot = tot + 1
617 // ---- KAT9: division after ident stays PUNCT '/' (NOT regex) ----
618 let s9: *u8 = "a / b\x00" as *u8
619 let n9: i64 = js_lex(s9, jl_strlen(s9), toks, 256, ntb)
620 var r9: i64 = 1
621 if n9 != 3 { r9 = 0 }
622 r9 = r9 * js_expect(toks, n9, 0, JS_TOK_IDENT, s9, "a\x00" as *u8)
623 r9 = r9 * js_expect(toks, n9, 1, JS_TOK_PUNCT, s9, "/\x00" as *u8)
624 r9 = r9 * js_expect(toks, n9, 2, JS_TOK_IDENT, s9, "b\x00" as *u8)
625 if r9 == 1 { jl_puts(" PASS KAT9 division 'a / b' -> PUNCT '/'\n" as *u8); pass = pass + 1 } else { jl_puts(" FAIL KAT9 ntok=" as *u8); jl_putn(n9); jl_puts("\n" as *u8) }
626 tot = tot + 1
627 // ---- KAT10: regex with '/' inside a [..] class does not close early ----
628 let sA: *u8 = "/[/]x/\x00" as *u8
629 let nA: i64 = js_lex(sA, jl_strlen(sA), toks, 256, ntb)
630 var rA: i64 = 1
631 if nA != 1 { rA = 0 }
632 rA = rA * js_expect(toks, nA, 0, JS_TOK_REGEX, sA, "/[/]x/\x00" as *u8)
633 if rA == 1 { jl_puts(" PASS KAT10 regex '/[/]x/' (/ inside [] does not close)\n" as *u8); pass = pass + 1 } else { jl_puts(" FAIL KAT10 ntok=" as *u8); jl_putn(nA); jl_puts("\n" as *u8) }
634 tot = tot + 1
635 jl_puts("---- nx_js_lex gate: passed " as *u8); jl_putn(pass); jl_puts(" / " as *u8); jl_putn(tot); jl_puts("\n" as *u8)
636 let lfd: i64 = sys_openat_append("knowledge/status/js_engine.log\x00" as *u8, 0x1a4)
637 if lfd >= 0 {
638 sys_write(lfd, "R-JS-LEX organ=nx_js_lex kats=" as *u8, 30)
639 jl_fdn(lfd, pass); sys_write(lfd, "/" as *u8, 1); jl_fdn(lfd, tot)
640 if pass == tot { sys_write(lfd, " tamper=ok verdict=GREEN\n" as *u8, 25) }
641 if pass != tot { sys_write(lfd, " tamper=?? verdict=RED\n" as *u8, 23) }
642 sys_close(lfd)
643 }
644 if pass == tot { sys_exit(0); return 0 }
645 sys_exit(1)
646 return 1
647}
648
649// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
650// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
651// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
652// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
653func jl_fdn(fd: i64, v: i64) -> i64 { nxi_fd(fd, v); return 0 }