nx_rxfull.nx source
↩ module page · 768 lines · 35338 B
1// nx_regex.nx -- SOVEREIGN regex engine (operator 2026-07-08/09: push the JS engine to run modern bundles;
2// regex is the last bundle-blocker + a PARSE-level one). Standalone + self-gated so it is proven before wiring
3// into the JS lexer/RegExp. Architecture = Cox's "regex VM" (compile the pattern to a flat INSTRUCTION PROGRAM,
4// then a backtracking matcher over it) -- flat dispatch dodges nx_cc's deep-else-nest miscompile, and it makes
5// captures + step-bounding clean. STEP + DEPTH bounded => a hostile bundle regex can NEVER hang/crash the crawler
6// (fails safe to no-match). Covers: literals, ., char classes [..] (ranges/negation/predef), \d\w\s\D\W\S,
7// anchors ^ $, quantifiers * + ? and {n} {n,} {n,m} (greedy + lazy ?), groups (capturing + (?:..)), alternation
8// |, escapes, \b \B word-boundary, flags i (icase) m (multiline) s (dotall). Leftmost match + capture groups.
9// license_tier: ORIGINAL
10import "nx_syscalls.nx"
11
12// ---- instruction opcodes (program stride 3: [op, a, b]) ----
13const RE_CHAR: i64 = 1 // a = charcode (lowered if icase)
14const RE_ANY: i64 = 2 // any char (not newline unless dotall)
15const RE_CLASS: i64 = 3 // a = class-table base
16const RE_SPLIT: i64 = 4 // a,b = two pcs to try (a first)
17const RE_JMP: i64 = 5 // a = pc
18const RE_SAVE: i64 = 6 // a = capture slot
19const RE_MATCH: i64 = 7
20const RE_BOL: i64 = 8 // ^
21const RE_EOL: i64 = 9 // $
22const RE_WORDB: i64 = 10 // \b
23const RE_NWORDB: i64 = 11 // \B
24const RE_AHEAD: i64 = 12 // a=negate(0 pos/1 neg) b=after-pc; ZERO-WIDTH sub-match of [p+1 .. RE_AEND)
25const RE_AEND: i64 = 13 // terminates a lookahead sub-match program (acts like MATCH for the sub-run)
26const RE_BREF: i64 = 14 // a=groupnum; re-match the text a prior capture group matched (JS \1..\9)
27const RE_MARK: i64 = 15 // a=slot; record current input pos into marks[slot] (loop-entry stamp)
28const RE_BACK: i64 = 16 // a=slot b=target; take the loop back-edge to `target` ONLY if pos advanced since
29 // marks[slot] -- a ZERO-WIDTH iteration FAILS (ES RepeatMatcher), killing `(a*)*`
30 // catastrophic-empty-loop recursion (was a native-stack SEGV before the depth cap).
31const RE_LOOPA: i64 = 17 // a=min(0 star/1 plus) b=greedy; the SINGLE consuming atom insn sits at p+1 and the
32 // continuation at p+2. Matches the atom ITERATIVELY (count run, then try counts
33 // longest-first/shortest-first) -> O(1) recursion depth per loop instead of one
34 // SPLIT frame PER CHARACTER -- \s+ \S* [^x]* .* on KB-scale strings stay flat.
35
36// ---- AST node kinds (node stride 6: [kind, a, b, c, d, e]) ----
37const RX_CHAR: i64 = 1 // a=code
38const RX_ANY: i64 = 2
39const RX_CLASS: i64 = 3 // a=class base
40const RX_CONCAT: i64 = 4 // a=left b=right
41const RX_ALT: i64 = 5 // a=left b=right
42const RX_STAR: i64 = 6 // a=child b=greedy
43const RX_PLUS: i64 = 7 // a=child b=greedy
44const RX_QUEST: i64 = 8 // a=child b=greedy
45const RX_REPEAT: i64 = 9 // a=child b=min c=max(-1=inf) d=greedy
46const RX_GROUP: i64 = 10 // a=child b=capturing c=groupnum
47const RX_BOL: i64 = 11
48const RX_EOL: i64 = 12
49const RX_WORDB: i64 = 13
50const RX_NWORDB: i64 = 14
51const RX_EMPTY: i64 = 15
52const RX_LOOKAHEAD: i64 = 16 // a=child b=negate(0 pos/1 neg) -- (?=..) / (?!..)
53const RX_BACKREF: i64 = 17 // a=groupnum -- \1..\9
54
55// ---- flags ----
56const RXF_I: i64 = 1
57const RXF_M: i64 = 2
58const RXF_S: i64 = 4
59const RXF_G: i64 = 8
60
61// ---- caps (fail-safe, never silent-corrupt) ----
62const RX_MAXNODE: i64 = 8192
63const RX_MAXPROG: i64 = 16384
64const RX_MAXCLASS: i64 = 8192
65const RX_MAXREP: i64 = 1000 // {n,m} expansion clamp
66const RX_MAXDEPTH: i64 = 6000 // backtracking recursion cap (stack-overflow guard)
67const RX_FUEL: i64 = 5000000 // total backtracking steps (catastrophic-backtrack guard)
68
69struct Regex { prog: *i64, nprog: i64, classes: *i64, ngroup: i64, flags: i64, nsave: i64, ok: i64, nmark: i64 }
70const REGEX_BYTES: i64 = 64
71
72struct RxP { pat: *u8, plen: i64, pos: i64, nodes: *i64, nn: i64, classes: *i64, nc: i64, ngroup: i64, flags: i64, err: i64 }
73const RXP_BYTES: i64 = 80
74
75struct Rx { s: *u8, slen: i64, prog: *i64, classes: *i64, flags: i64, saves: *i64, fuel: *i64, marks: *i64 }
76const RX_BYTES: i64 = 64
77
78// ---------- small char helpers ----------
79func rx_lc(c: i64) -> i64 { if c >= 65 { if c <= 90 { return c + 32 } } return c }
80func rx_swapcase(c: i64) -> i64 { if c >= 65 { if c <= 90 { return c + 32 } } if c >= 97 { if c <= 122 { return c - 32 } } return c }
81func rx_isword(c: i64) -> i64 { if c >= 48 { if c <= 57 { return 1 } } if c >= 65 { if c <= 90 { return 1 } } if c >= 97 { if c <= 122 { return 1 } } if c == 95 { return 1 } return 0 }
82
83// ---------- parser cursor ----------
84func rp_peek(p: *RxP) -> i64 { if p.pos < p.plen { return (p.pat[p.pos]) as i64 } return 0 - 1 }
85func rp_at(p: *RxP, off: i64) -> i64 { let q: i64 = p.pos + off; if q < p.plen { return (p.pat[q]) as i64 } return 0 - 1 }
86func rp_adv(p: *RxP) -> i64 { p.pos = p.pos + 1; return 0 }
87// create a node (kind,a,b,c); d/e default 0. returns node index.
88func rp_node(p: *RxP, kind: i64, a: i64, b: i64, c: i64) -> i64 {
89 let i: i64 = p.nn
90 if i >= RX_MAXNODE { p.err = 1; return 0 }
91 let base: i64 = i * 6
92 let nd: *i64 = p.nodes
93 nd[base] = kind; nd[base + 1] = a; nd[base + 2] = b; nd[base + 3] = c; nd[base + 4] = 0; nd[base + 5] = 0
94 p.nn = i + 1
95 return i
96}
97
98// append a [lo,hi] range to the class arena (used while building a class)
99func rp_add_range(p: *RxP, lo: i64, hi: i64) -> i64 { let cl: *i64 = p.classes; if p.nc + 2 > RX_MAXCLASS { p.err = 1; return 0 } cl[p.nc] = lo; cl[p.nc + 1] = hi; p.nc = p.nc + 2; return 0 }
100// add the ranges for a predefined class into the current build (which: 0 digit / 1 word / 2 space)
101func rp_add_predef(p: *RxP, which: i64) -> i64 {
102 if which == 0 { rp_add_range(p, 48, 57); return 0 }
103 if which == 1 { rp_add_range(p, 48, 57); rp_add_range(p, 65, 90); rp_add_range(p, 97, 122); rp_add_range(p, 95, 95); return 0 }
104 rp_add_range(p, 9, 13); rp_add_range(p, 32, 32); return 0
105}
106// build a standalone one-class from a predefined set (neg=1 for \D\W\S) -> RX_CLASS node
107func rp_predef_node(p: *RxP, which: i64, neg: i64) -> i64 {
108 let cl: *i64 = p.classes
109 let base: i64 = p.nc
110 if base + 2 > RX_MAXCLASS { p.err = 1; return 0 }
111 cl[base] = neg; cl[base + 1] = 0; p.nc = base + 2
112 rp_add_predef(p, which)
113 cl[base + 1] = (p.nc - base - 2) / 2
114 return rp_node(p, RX_CLASS, base, 0, 0)
115}
116
117// read ONE class-escape LITERAL value (the '\' is consumed by the caller; \d\w\s predefs handled separately).
118// Handles \xHH (byte) and \uHHHH (FULL code point -- values >0xFF land in a range that matches NO byte, which
119// is exactly right for a byte-string engine: `Ĩ-` matches nothing in ASCII, per V8). \n\t\r etc via
120// rx_esc_char. Consumes the escape char + any hex digits.
121func rp_class_esc_val(p: *RxP) -> i64 {
122 let e: i64 = rp_peek(p); rp_adv(p)
123 if e == 120 { // \xHH
124 let h1: i64 = rx_hexval(rp_peek(p))
125 let h2: i64 = rx_hexval(rp_at(p, 1))
126 if h1 >= 0 { if h2 >= 0 { rp_adv(p); rp_adv(p); return h1 * 16 + h2 } }
127 return 120
128 }
129 if e == 117 { // \uHHHH -> full code point (may exceed byte range)
130 let u1: i64 = rx_hexval(rp_peek(p))
131 let u2: i64 = rx_hexval(rp_at(p, 1))
132 let u3: i64 = rx_hexval(rp_at(p, 2))
133 let u4: i64 = rx_hexval(rp_at(p, 3))
134 if u1 >= 0 { if u2 >= 0 { if u3 >= 0 { if u4 >= 0 {
135 rp_adv(p); rp_adv(p); rp_adv(p); rp_adv(p)
136 return ((u1 * 16 + u2) * 16 + u3) * 16 + u4
137 } } } }
138 return 117
139 }
140 return rx_esc_char(e)
141}
142// parse a [...] class -> RX_CLASS node
143func rp_class(p: *RxP) -> i64 {
144 rp_adv(p) // consume '['
145 let cl: *i64 = p.classes
146 let base: i64 = p.nc
147 if base + 2 > RX_MAXCLASS { p.err = 1; return 0 }
148 var neg: i64 = 0
149 if rp_peek(p) == 94 { neg = 1; rp_adv(p) } // ^
150 cl[base] = neg; cl[base + 1] = 0; p.nc = base + 2
151 var go: i64 = 1
152 while go == 1 {
153 let c: i64 = rp_peek(p)
154 if c < 0 { p.err = 1; go = 0 }
155 else {
156 if c == 93 { rp_adv(p); go = 0 } // ']'
157 else {
158 if c == 92 { // '\' escape inside class
159 rp_adv(p) // consume backslash
160 let e: i64 = rp_peek(p)
161 if e == 100 { rp_adv(p); rp_add_predef(p, 0) }
162 else { if e == 119 { rp_adv(p); rp_add_predef(p, 1) }
163 else { if e == 115 { rp_adv(p); rp_add_predef(p, 2) }
164 else { let lit: i64 = rp_class_esc_val(p); rp_class_lit(p, lit) } } } // \x \u \n .. + range
165 } else {
166 rp_adv(p)
167 rp_class_lit(p, c)
168 }
169 }
170 }
171 }
172 if p.err == 1 { return 0 }
173 cl[base + 1] = (p.nc - base - 2) / 2
174 return rp_node(p, RX_CLASS, base, 0, 0)
175}
176// a literal class member `lo` -- maybe a range `lo-hi` (peek at p.pos which is AFTER lo was consumed).
177func rp_class_lit(p: *RxP, lo: i64) -> i64 {
178 if rp_peek(p) == 45 { if rp_at(p, 1) != 93 { if rp_at(p, 1) >= 0 { // '-' and next isn't ']'
179 rp_adv(p) // consume '-'
180 var hi: i64 = rp_peek(p)
181 if hi == 92 { rp_adv(p); hi = rp_class_esc_val(p) } // \x/\u/\n.. hi bound (consumes esc + hex)
182 else { rp_adv(p) } // literal hi char
183 rp_add_range(p, lo, hi)
184 return 0
185 } } }
186 rp_add_range(p, lo, lo)
187 return 0
188}
189
190// hex digit -> value (0-15) or -1.
191func rx_hexval(c: i64) -> i64 {
192 if c >= 48 { if c <= 57 { return c - 48 } }
193 if c >= 97 { if c <= 102 { return c - 87 } }
194 if c >= 65 { if c <= 70 { return c - 55 } }
195 return 0 - 1
196}
197// decode a backslash-escape char (the byte AFTER '\') to its literal code.
198func rx_esc_char(e: i64) -> i64 {
199 if e == 110 { return 10 } // \n
200 if e == 116 { return 9 } // \t
201 if e == 114 { return 13 } // \r
202 if e == 102 { return 12 } // \f
203 if e == 118 { return 11 } // \v
204 if e == 48 { return 0 } // \0
205 return e // \\ \. \/ \[ ... -> the literal char
206}
207
208// parse an escape in ATOM context -> a node (class / boundary / char)
209func rp_escape(p: *RxP) -> i64 {
210 rp_adv(p) // consume '\'
211 let e: i64 = rp_peek(p); rp_adv(p)
212 if e == 100 { return rp_predef_node(p, 0, 0) } // \d
213 if e == 68 { return rp_predef_node(p, 0, 1) } // \D
214 if e == 119 { return rp_predef_node(p, 1, 0) } // \w
215 if e == 87 { return rp_predef_node(p, 1, 1) } // \W
216 if e == 115 { return rp_predef_node(p, 2, 0) } // \s
217 if e == 83 { return rp_predef_node(p, 2, 1) } // \S
218 if e == 98 { return rp_node(p, RX_WORDB, 0, 0, 0) } // \b
219 if e == 66 { return rp_node(p, RX_NWORDB, 0, 0, 0) } // \B
220 if e == 49 { return rp_backref_node(p, 1) } // \1 .. \9 backreference
221 if e == 50 { return rp_backref_node(p, 2) }
222 if e == 51 { return rp_backref_node(p, 3) }
223 if e == 52 { return rp_backref_node(p, 4) }
224 if e == 53 { return rp_backref_node(p, 5) }
225 if e == 54 { return rp_backref_node(p, 6) }
226 if e == 55 { return rp_backref_node(p, 7) }
227 if e == 56 { return rp_backref_node(p, 8) }
228 if e == 57 { return rp_backref_node(p, 9) }
229 if e == 120 { // \xHH -> literal byte
230 let h1: i64 = rx_hexval(rp_peek(p))
231 let h2: i64 = rx_hexval(rp_at(p, 1))
232 if h1 >= 0 { if h2 >= 0 {
233 rp_adv(p); rp_adv(p)
234 var xc: i64 = h1 * 16 + h2
235 if (p.flags & RXF_I) != 0 { xc = rx_lc(xc) }
236 return rp_node(p, RX_CHAR, xc, 0, 0)
237 } }
238 }
239 if e == 117 { // \uHHHH -> FULL code point (NOT low-byte: a value
240 let u1: i64 = rx_hexval(rp_peek(p)) // >0xFF then matches NO byte, which is correct for
241 let u2: i64 = rx_hexval(rp_at(p, 1)) // a byte-string engine -- `
` must NOT alias '('.
242 let u3: i64 = rx_hexval(rp_at(p, 2))
243 let u4: i64 = rx_hexval(rp_at(p, 3))
244 if u1 >= 0 { if u2 >= 0 { if u3 >= 0 { if u4 >= 0 {
245 rp_adv(p); rp_adv(p); rp_adv(p); rp_adv(p)
246 var uc: i64 = ((u1 * 16 + u2) * 16 + u3) * 16 + u4
247 if (p.flags & RXF_I) != 0 { uc = rx_lc(uc) }
248 return rp_node(p, RX_CHAR, uc, 0, 0)
249 } } } }
250 }
251 var lit: i64 = rx_esc_char(e)
252 if (p.flags & RXF_I) != 0 { lit = rx_lc(lit) }
253 return rp_node(p, RX_CHAR, lit, 0, 0)
254}
255// backref node; a group number that hasn't been opened YET (forward ref) is a JS octal/empty edge -> if the
256// referenced group index exceeds the total this parse will see, the matcher treats an unset span as empty.
257func rp_backref_node(p: *RxP, g: i64) -> i64 { return rp_node(p, RX_BACKREF, g, 0, 0) }
258
259// atom := group | class | . | ^ | $ | \esc | literal
260func rp_atom(p: *RxP) -> i64 {
261 let c: i64 = rp_peek(p)
262 if c == 40 { // '('
263 rp_adv(p)
264 var capturing: i64 = 1
265 var gnum: i64 = 0
266 if rp_peek(p) == 63 { // (?
267 let c2: i64 = rp_at(p, 1)
268 if c2 == 58 { capturing = 0; rp_adv(p); rp_adv(p) } // (?:
269 else { if c2 == 61 { // (?= positive lookahead
270 rp_adv(p); rp_adv(p)
271 let la: i64 = rp_alt(p)
272 if rp_peek(p) != 41 { p.err = 1; return 0 }
273 rp_adv(p)
274 return rp_node(p, RX_LOOKAHEAD, la, 0, 0)
275 }
276 else { if c2 == 33 { // (?! negative lookahead
277 rp_adv(p); rp_adv(p)
278 let ln: i64 = rp_alt(p)
279 if rp_peek(p) != 41 { p.err = 1; return 0 }
280 rp_adv(p)
281 return rp_node(p, RX_LOOKAHEAD, ln, 1, 0)
282 }
283 else { p.err = 1; return 0 } } } // (?< lookbehind still unsupported -> decline
284 }
285 if capturing == 1 { p.ngroup = p.ngroup + 1; gnum = p.ngroup }
286 let child: i64 = rp_alt(p)
287 if rp_peek(p) != 41 { p.err = 1; return 0 }
288 rp_adv(p)
289 return rp_node(p, RX_GROUP, child, capturing, gnum)
290 }
291 if c == 91 { return rp_class(p) } // '['
292 if c == 46 { rp_adv(p); return rp_node(p, RX_ANY, 0, 0, 0) } // '.'
293 if c == 94 { rp_adv(p); return rp_node(p, RX_BOL, 0, 0, 0) } // '^'
294 if c == 36 { rp_adv(p); return rp_node(p, RX_EOL, 0, 0, 0) } // '$'
295 if c == 92 { return rp_escape(p) } // '\'
296 if c < 0 { return rp_node(p, RX_EMPTY, 0, 0, 0) }
297 rp_adv(p)
298 var lit: i64 = c
299 if (p.flags & RXF_I) != 0 { lit = rx_lc(lit) }
300 return rp_node(p, RX_CHAR, lit, 0, 0)
301}
302
303// parse an optional {n} {n,} {n,m}. On success returns 1 and fills box[0]=min box[1]=max(-1 inf).
304// On not-a-quantifier restores pos and returns 0. (a bare '{' is then a literal.)
305func rp_brace(p: *RxP, box: *i64) -> i64 {
306 let save: i64 = p.pos
307 rp_adv(p) // consume '{'
308 var haven: i64 = 0
309 var n: i64 = 0
310 var d: i64 = rp_peek(p)
311 while d >= 48 { if d <= 57 { n = n * 10 + (d - 48); haven = 1; rp_adv(p); d = rp_peek(p) } else { d = 0 - 1 } }
312 if haven == 0 { p.pos = save; return 0 }
313 var mx: i64 = n
314 if rp_peek(p) == 44 { // ','
315 rp_adv(p)
316 if rp_peek(p) == 125 { mx = 0 - 1 } // {n,}
317 else {
318 var m: i64 = 0; var havem: i64 = 0
319 var d2: i64 = rp_peek(p)
320 while d2 >= 48 { if d2 <= 57 { m = m * 10 + (d2 - 48); havem = 1; rp_adv(p); d2 = rp_peek(p) } else { d2 = 0 - 1 } }
321 if havem == 0 { p.pos = save; return 0 }
322 mx = m
323 }
324 }
325 if rp_peek(p) != 125 { p.pos = save; return 0 }
326 rp_adv(p) // consume '}'
327 box[0] = n; box[1] = mx
328 return 1
329}
330
331// repeat := atom quantifier? (quantifier: * + ? {n,m}, each with optional lazy '?')
332func rp_repeat(p: *RxP) -> i64 {
333 let atom: i64 = rp_atom(p)
334 if p.err == 1 { return atom }
335 let c: i64 = rp_peek(p)
336 if c == 42 { rp_adv(p); let g: i64 = rp_lazy(p); return rp_wrap(p, RX_STAR, atom, g, 0, 0) } // *
337 if c == 43 { rp_adv(p); let g: i64 = rp_lazy(p); return rp_wrap(p, RX_PLUS, atom, g, 0, 0) } // +
338 if c == 63 { rp_adv(p); let g: i64 = rp_lazy(p); return rp_wrap(p, RX_QUEST, atom, g, 0, 0) } // ?
339 if c == 123 { // '{'
340 let box: *i64 = sys_mmap(16) as *i64
341 if rp_brace(p, box) == 1 {
342 let g: i64 = rp_lazy(p)
343 return rp_wrap(p, RX_REPEAT, atom, box[0], box[1], g)
344 }
345 }
346 return atom
347}
348// consume an optional lazy '?' after a quantifier -> greedy(1) / lazy(0)
349func rp_lazy(p: *RxP) -> i64 { if rp_peek(p) == 63 { rp_adv(p); return 0 } return 1 }
350// build a quantifier wrapper node; RX_REPEAT stores greedy in slot d (nd[idx*6+4]).
351func rp_wrap(p: *RxP, kind: i64, child: i64, x: i64, y: i64, g: i64) -> i64 {
352 if kind == RX_REPEAT {
353 let idx: i64 = rp_node(p, RX_REPEAT, child, x, y)
354 p.nodes[idx * 6 + 4] = g
355 return idx
356 }
357 return rp_node(p, kind, child, x, 0) // STAR/PLUS/QUEST: b=greedy
358}
359
360// concat := repeat* (until '|' ')' or end)
361func rp_concat(p: *RxP) -> i64 {
362 var acc: i64 = 0 - 1
363 var go: i64 = 1
364 while go == 1 {
365 let c: i64 = rp_peek(p)
366 if c < 0 { go = 0 }
367 else { if c == 124 { go = 0 } else { if c == 41 { go = 0 } else {
368 let r: i64 = rp_repeat(p)
369 if p.err == 1 { go = 0 } else {
370 if acc < 0 { acc = r } else { acc = rp_node(p, RX_CONCAT, acc, r, 0) }
371 }
372 } } }
373 }
374 if acc < 0 { acc = rp_node(p, RX_EMPTY, 0, 0, 0) }
375 return acc
376}
377
378// alt := concat ('|' concat)*
379func rp_alt(p: *RxP) -> i64 {
380 var left: i64 = rp_concat(p)
381 var go: i64 = 1
382 while go == 1 {
383 if rp_peek(p) == 124 { // '|'
384 rp_adv(p)
385 let right: i64 = rp_concat(p)
386 left = rp_node(p, RX_ALT, left, right, 0)
387 if p.err == 1 { go = 0 }
388 } else { go = 0 }
389 }
390 return left
391}
392
393// ---------- compile AST -> program ----------
394func rc_emit(prog: *i64, npp: *i64, op: i64, a: i64, b: i64) -> i64 {
395 let i: i64 = npp[0]
396 if i >= RX_MAXPROG { return i }
397 prog[i * 3] = op; prog[i * 3 + 1] = a; prog[i * 3 + 2] = b
398 npp[0] = i + 1
399 return i
400}
401// is the node ONE consuming atom (compiles to exactly one CHAR/CLASS/ANY insn)? -> RE_LOOPA flattenable.
402func rx_single_atom(nodes: *i64, node: i64) -> i64 {
403 let k: i64 = nodes[node * 6]
404 if k == RX_CHAR { return 1 }
405 if k == RX_CLASS { return 1 }
406 if k == RX_ANY { return 1 }
407 return 0
408}
409// emit a greedy/lazy star loop over child-node `child`. Single-atom bodies flatten to RE_LOOPA (iterative,
410// O(1) depth). Composite bodies: npp[1] = the running MARK-slot counter (empty-loop guard): the back-edge is
411// RE_BACK (progress-checked) not an unconditional RE_JMP, so a zero-width iteration fails per the ES spec.
412func rc_star(nodes: *i64, child: i64, prog: *i64, npp: *i64, greedy: i64) -> i64 {
413 if rx_single_atom(nodes, child) == 1 {
414 rc_emit(prog, npp, RE_LOOPA, 0, greedy)
415 rc(nodes, child, prog, npp) // exactly one atom insn
416 return 0
417 }
418 let slot: i64 = npp[1]; npp[1] = slot + 1
419 let l1: i64 = npp[0]
420 let s: i64 = rc_emit(prog, npp, RE_SPLIT, 0, 0)
421 let cs: i64 = npp[0]
422 rc_emit(prog, npp, RE_MARK, slot, 0)
423 rc(nodes, child, prog, npp)
424 rc_emit(prog, npp, RE_BACK, slot, l1) // progress -> back to l1 (re-SPLIT); empty -> iteration fails
425 let af: i64 = npp[0]
426 if greedy == 1 { prog[s * 3 + 1] = cs; prog[s * 3 + 2] = af } else { prog[s * 3 + 1] = af; prog[s * 3 + 2] = cs }
427 return 0
428}
429func rc(nodes: *i64, node: i64, prog: *i64, npp: *i64) -> i64 {
430 let base: i64 = node * 6
431 let k: i64 = nodes[base]
432 let a: i64 = nodes[base + 1]
433 let b: i64 = nodes[base + 2]
434 let c: i64 = nodes[base + 3]
435 let d: i64 = nodes[base + 4]
436 if k == RX_CHAR { rc_emit(prog, npp, RE_CHAR, a, 0); return 0 }
437 if k == RX_ANY { rc_emit(prog, npp, RE_ANY, 0, 0); return 0 }
438 if k == RX_CLASS { rc_emit(prog, npp, RE_CLASS, a, 0); return 0 }
439 if k == RX_BOL { rc_emit(prog, npp, RE_BOL, 0, 0); return 0 }
440 if k == RX_EOL { rc_emit(prog, npp, RE_EOL, 0, 0); return 0 }
441 if k == RX_WORDB { rc_emit(prog, npp, RE_WORDB, 0, 0); return 0 }
442 if k == RX_NWORDB { rc_emit(prog, npp, RE_NWORDB, 0, 0); return 0 }
443 if k == RX_EMPTY { return 0 }
444 if k == RX_CONCAT { rc(nodes, a, prog, npp); rc(nodes, b, prog, npp); return 0 }
445 if k == RX_ALT {
446 let s: i64 = rc_emit(prog, npp, RE_SPLIT, 0, 0)
447 prog[s * 3 + 1] = npp[0]
448 rc(nodes, a, prog, npp)
449 let j: i64 = rc_emit(prog, npp, RE_JMP, 0, 0)
450 prog[s * 3 + 2] = npp[0]
451 rc(nodes, b, prog, npp)
452 prog[j * 3 + 1] = npp[0]
453 return 0
454 }
455 if k == RX_STAR { rc_star(nodes, a, prog, npp, b); return 0 }
456 if k == RX_PLUS {
457 if rx_single_atom(nodes, a) == 1 { // flatten: one mandatory + iterative tail
458 rc_emit(prog, npp, RE_LOOPA, 1, b)
459 rc(nodes, a, prog, npp)
460 return 0
461 }
462 let slot: i64 = npp[1]; npp[1] = slot + 1
463 let l1: i64 = npp[0]
464 rc_emit(prog, npp, RE_MARK, slot, 0)
465 rc(nodes, a, prog, npp)
466 let s: i64 = rc_emit(prog, npp, RE_SPLIT, 0, 0)
467 let bk: i64 = npp[0]
468 rc_emit(prog, npp, RE_BACK, slot, l1) // greedy loop arm: re-enter only if the body advanced
469 let af: i64 = npp[0]
470 if b == 1 { prog[s * 3 + 1] = bk; prog[s * 3 + 2] = af } else { prog[s * 3 + 1] = af; prog[s * 3 + 2] = bk }
471 return 0
472 }
473 if k == RX_QUEST {
474 let s: i64 = rc_emit(prog, npp, RE_SPLIT, 0, 0)
475 let cs: i64 = npp[0]
476 rc(nodes, a, prog, npp)
477 let af: i64 = npp[0]
478 if b == 1 { prog[s * 3 + 1] = cs; prog[s * 3 + 2] = af } else { prog[s * 3 + 1] = af; prog[s * 3 + 2] = cs }
479 return 0
480 }
481 if k == RX_GROUP {
482 if b == 1 {
483 rc_emit(prog, npp, RE_SAVE, 2 * c, 0)
484 rc(nodes, a, prog, npp)
485 rc_emit(prog, npp, RE_SAVE, 2 * c + 1, 0)
486 } else { rc(nodes, a, prog, npp) }
487 return 0
488 }
489 if k == RX_BACKREF { rc_emit(prog, npp, RE_BREF, a, 0); return 0 }
490 if k == RX_LOOKAHEAD {
491 // RE_AHEAD(neg, after) ; <body> ; RE_AEND -- the matcher runs the body as an isolated ZERO-WIDTH
492 // sub-match (RE_AEND ends it), then continues at `after` with the input position UNCHANGED.
493 let sh: i64 = rc_emit(prog, npp, RE_AHEAD, b, 0)
494 rc(nodes, a, prog, npp)
495 rc_emit(prog, npp, RE_AEND, 0, 0)
496 prog[sh * 3 + 2] = npp[0] // after = pc past RE_AEND
497 return 0
498 }
499 if k == RX_REPEAT {
500 var i: i64 = 0
501 while i < b { rc(nodes, a, prog, npp); i = i + 1 } // min mandatory copies
502 if c < 0 { rc_star(nodes, a, prog, npp, d); return 0 } // {n,} -> star tail
503 var opt: i64 = c - b
504 if opt > RX_MAXREP { opt = RX_MAXREP }
505 let sidx: *i64 = sys_mmap(8 * (opt + 1)) as *i64
506 var oi: i64 = 0
507 while oi < opt { // (max-min) optional copies
508 let s: i64 = rc_emit(prog, npp, RE_SPLIT, 0, 0)
509 sidx[oi] = s
510 prog[s * 3 + 1] = npp[0] // greedy default: try child (fall-through)
511 rc(nodes, a, prog, npp)
512 oi = oi + 1
513 }
514 let endpc: i64 = npp[0]
515 oi = 0
516 while oi < opt {
517 let s: i64 = sidx[oi]
518 if d == 1 { prog[s * 3 + 2] = endpc } else { let cs: i64 = prog[s * 3 + 1]; prog[s * 3 + 1] = endpc; prog[s * 3 + 2] = cs }
519 oi = oi + 1
520 }
521 return 0
522 }
523 return 0
524}
525
526// ---------- matcher ----------
527func rx_class_match(rx: *Rx, cbase: i64, ch: i64) -> i64 {
528 let cl: *i64 = rx.classes
529 let neg: i64 = cl[cbase]
530 let nr: i64 = cl[cbase + 1]
531 var m: i64 = 0
532 var k: i64 = 0
533 while k < nr { let lo: i64 = cl[cbase + 2 + k * 2]; let hi: i64 = cl[cbase + 2 + k * 2 + 1]; if ch >= lo { if ch <= hi { m = 1 } } k = k + 1 }
534 if m == 0 { if (rx.flags & RXF_I) != 0 {
535 let c2: i64 = rx_swapcase(ch)
536 var k2: i64 = 0
537 while k2 < nr { let lo: i64 = cl[cbase + 2 + k2 * 2]; let hi: i64 = cl[cbase + 2 + k2 * 2 + 1]; if c2 >= lo { if c2 <= hi { m = 1 } } k2 = k2 + 1 }
538 } }
539 if neg == 1 { return 1 - m }
540 return m
541}
542func rx_wordb(rx: *Rx, i: i64) -> i64 {
543 var before: i64 = 0
544 if i > 0 { if rx_isword((rx.s[i - 1]) as i64) == 1 { before = 1 } }
545 var after: i64 = 0
546 if i < rx.slen { if rx_isword((rx.s[i]) as i64) == 1 { after = 1 } }
547 if before != after { return 1 }
548 return 0
549}
550// backtracking matcher: return end position (>=0) on success (saves filled), -1 on fail/abort.
551// FLAT dispatch (each branch returns or updates p/i and re-loops) -- dodges nx_cc deep-else miscompile.
552func re_bt(rx: *Rx, pc: i64, sp: i64, depth: i64) -> i64 {
553 if depth > RX_MAXDEPTH { return 0 - 1 }
554 let prog: *i64 = rx.prog
555 let s: *u8 = rx.s
556 let slen: i64 = rx.slen
557 let fuel: *i64 = rx.fuel
558 var p: i64 = pc
559 var i: i64 = sp
560 var run: i64 = 1
561 while run == 1 {
562 if fuel[0] <= 0 { return 0 - 1 }
563 fuel[0] = fuel[0] - 1
564 let base: i64 = p * 3
565 let op: i64 = prog[base]
566 let a: i64 = prog[base + 1]
567 let b: i64 = prog[base + 2]
568 if op == RE_MATCH { return i }
569 if op == RE_CHAR {
570 if i >= slen { return 0 - 1 }
571 var ch: i64 = (s[i]) as i64
572 if (rx.flags & RXF_I) != 0 { ch = rx_lc(ch) }
573 if ch != a { return 0 - 1 }
574 p = p + 1; i = i + 1
575 }
576 if op == RE_ANY {
577 if i >= slen { return 0 - 1 }
578 if (rx.flags & RXF_S) == 0 { let dc: i64 = (s[i]) as i64; if dc == 10 { return 0 - 1 } if dc == 13 { return 0 - 1 } } // `.` excludes LF+CR (V8)
579 p = p + 1; i = i + 1
580 }
581 if op == RE_CLASS {
582 if i >= slen { return 0 - 1 }
583 if rx_class_match(rx, a, (s[i]) as i64) == 0 { return 0 - 1 }
584 p = p + 1; i = i + 1
585 }
586 if op == RE_JMP { p = a }
587 if op == RE_SPLIT {
588 let r: i64 = re_bt(rx, a, i, depth + 1)
589 if r >= 0 { return r }
590 p = b
591 }
592 if op == RE_SAVE {
593 let sv: *i64 = rx.saves
594 let old: i64 = sv[a]
595 sv[a] = i
596 let r2: i64 = re_bt(rx, p + 1, i, depth + 1)
597 if r2 >= 0 { return r2 }
598 sv[a] = old
599 return 0 - 1
600 }
601 if op == RE_BOL {
602 var okb: i64 = 0
603 if i == 0 { okb = 1 }
604 if okb == 0 { if (rx.flags & RXF_M) != 0 { if ((s[i - 1]) as i64) == 10 { okb = 1 } } }
605 if okb == 0 { return 0 - 1 }
606 p = p + 1
607 }
608 if op == RE_EOL {
609 var oke: i64 = 0
610 if i == slen { oke = 1 }
611 if oke == 0 { if (rx.flags & RXF_M) != 0 { if ((s[i]) as i64) == 10 { oke = 1 } } }
612 if oke == 0 { return 0 - 1 }
613 p = p + 1
614 }
615 if op == RE_WORDB { if rx_wordb(rx, i) == 0 { return 0 - 1 } p = p + 1 }
616 if op == RE_NWORDB { if rx_wordb(rx, i) == 1 { return 0 - 1 } p = p + 1 }
617 if op == RE_AEND { return i } // lookahead sub-match terminator: success at current pos
618 if op == RE_AHEAD { // a=neg b=after; run [p+1..RE_AEND) zero-width
619 let sub: i64 = re_bt(rx, p + 1, i, depth + 1)
620 var matched: i64 = 0
621 if sub >= 0 { matched = 1 }
622 if a == 0 { if matched == 0 { return 0 - 1 } } // positive: body MUST match here
623 if a == 1 { if matched == 1 { return 0 - 1 } } // negative: body must NOT match here
624 p = b // continue after the assertion; i UNCHANGED (zero-width)
625 }
626 if op == RE_LOOPA { // a=min b=greedy; atom insn at p+1, continuation at p+2.
627 let ap: i64 = (p + 1) * 3 // count the atom's maximal run ITERATIVELY (no recursion),
628 let akind: i64 = prog[ap] // then try lengths longest-first (greedy) / shortest-first
629 let aa: i64 = prog[ap + 1] // (lazy); each candidate recurses the continuation ONCE.
630 var cnt: i64 = 0
631 var j: i64 = i
632 var scan: i64 = 1
633 while scan == 1 {
634 if j >= slen { scan = 0 } else {
635 let ch0: i64 = (s[j]) as i64
636 var okc: i64 = 0
637 if akind == RE_CHAR { var c2: i64 = ch0; if (rx.flags & RXF_I) != 0 { c2 = rx_lc(c2) } if c2 == aa { okc = 1 } }
638 if akind == RE_CLASS { okc = rx_class_match(rx, aa, ch0) }
639 if akind == RE_ANY { okc = 1; if (rx.flags & RXF_S) == 0 { if ch0 == 10 { okc = 0 } if ch0 == 13 { okc = 0 } } }
640 if okc == 1 { j = j + 1; cnt = cnt + 1 } else { scan = 0 }
641 }
642 }
643 if cnt < a { return 0 - 1 } // fewer than min -> no match here
644 if b == 1 {
645 var c3: i64 = cnt
646 while c3 >= a {
647 if fuel[0] <= 0 { return 0 - 1 }
648 fuel[0] = fuel[0] - 1
649 let r3: i64 = re_bt(rx, p + 2, i + c3, depth + 1)
650 if r3 >= 0 { return r3 }
651 c3 = c3 - 1
652 }
653 return 0 - 1
654 }
655 var c4: i64 = a
656 while c4 <= cnt {
657 if fuel[0] <= 0 { return 0 - 1 }
658 fuel[0] = fuel[0] - 1
659 let r4: i64 = re_bt(rx, p + 2, i + c4, depth + 1)
660 if r4 >= 0 { return r4 }
661 c4 = c4 + 1
662 }
663 return 0 - 1
664 }
665 if op == RE_MARK { // stamp loop-entry pos; save-restore discipline (like RE_SAVE)
666 let mk: *i64 = rx.marks // so backtracking into an EARLIER iteration sees ITS stamp.
667 let mold: i64 = mk[a]
668 mk[a] = i
669 let mr: i64 = re_bt(rx, p + 1, i, depth + 1)
670 if mr >= 0 { return mr }
671 mk[a] = mold
672 return 0 - 1
673 }
674 if op == RE_BACK { // ES RepeatMatcher: an EMPTY iteration FAILS (captures restored
675 if i > rx.marks[a] { p = b } else { return 0 - 1 } // via the SAVE/MARK unwind); progress -> loop.
676 }
677 if op == RE_BREF { // a=groupnum; re-match the captured span
678 let sv: *i64 = rx.saves
679 let gs: i64 = sv[2 * a]
680 let ge: i64 = sv[2 * a + 1]
681 if gs >= 0 { if ge >= gs { // group matched -> require the same bytes here
682 let glen: i64 = ge - gs
683 if i + glen > slen { return 0 - 1 }
684 var bi: i64 = 0
685 while bi < glen {
686 var c1: i64 = (s[gs + bi]) as i64
687 var c2: i64 = (s[i + bi]) as i64
688 if (rx.flags & RXF_I) != 0 { c1 = rx_lc(c1); c2 = rx_lc(c2) }
689 if c1 != c2 { return 0 - 1 }
690 bi = bi + 1
691 }
692 i = i + glen
693 } }
694 p = p + 1 // unset group (gs<0) -> matches empty (JS semantics)
695 }
696 }
697 return 0 - 1
698}
699
700// ---------- public API ----------
701// compile a pattern (bytes+len) with flag bits -> *Regex (re.ok==0 on parse error/decline).
702func nx_regex_compile(pat: *u8, plen: i64, flags: i64) -> *Regex {
703 let p: *RxP = sys_mmap(RXP_BYTES) as *RxP
704 p.pat = pat; p.plen = plen; p.pos = 0
705 p.nodes = sys_mmap(RX_MAXNODE * 6 * 8) as *i64; p.nn = 0
706 p.classes = sys_mmap(RX_MAXCLASS * 8) as *i64; p.nc = 0
707 p.ngroup = 0; p.flags = flags; p.err = 0
708 let root: i64 = rp_alt(p)
709 let re: *Regex = sys_mmap(REGEX_BYTES) as *Regex
710 re.classes = p.classes; re.ngroup = p.ngroup; re.flags = flags
711 re.nsave = 2 * (p.ngroup + 1)
712 if p.err == 1 { re.ok = 0; re.prog = 0 as *i64; re.nprog = 0; return re }
713 if p.pos < p.plen { re.ok = 0; re.prog = 0 as *i64; re.nprog = 0; return re } // trailing junk (e.g. stray ')')
714 let prog: *i64 = sys_mmap(RX_MAXPROG * 3 * 8) as *i64
715 let npp: *i64 = sys_mmap(16) as *i64; npp[0] = 0; npp[1] = 0 // [1] = MARK-slot counter (one per loop)
716 rc_emit(prog, npp, RE_SAVE, 0, 0) // save[0] = match start
717 rc(p.nodes, root, prog, npp)
718 rc_emit(prog, npp, RE_SAVE, 1, 0) // save[1] = match end
719 rc_emit(prog, npp, RE_MATCH, 0, 0)
720 re.prog = prog; re.nprog = npp[0]; re.ok = 1; re.nmark = npp[1]
721 return re
722}
723// leftmost search from `start`. saves (caller-sized re.nsave) filled with [start,end, g1s,g1e, ...]; -1 = unset.
724// returns match-start position (>=0) or -1 (no match). Total work is fuel-bounded => never hangs.
725func nx_regex_exec(re: *Regex, s: *u8, slen: i64, start: i64, saves: *i64) -> i64 {
726 if re.ok == 0 { return 0 - 1 }
727 let rx: *Rx = sys_mmap(RX_BYTES) as *Rx
728 rx.s = s; rx.slen = slen; rx.prog = re.prog; rx.classes = re.classes; rx.flags = re.flags; rx.saves = saves
729 let fuel: *i64 = sys_mmap(8) as *i64; fuel[0] = RX_FUEL
730 rx.fuel = fuel
731 var nmk: i64 = re.nmark
732 if nmk < 1 { nmk = 1 }
733 let mks: *i64 = sys_mmap(nmk * 8) as *i64 // loop-entry stamps (zero-init; MARK stamps before any BACK reads)
734 var mi: i64 = 0
735 while mi < nmk { mks[mi] = 0 - 1; mi = mi + 1 }
736 rx.marks = mks
737 let nsave: i64 = re.nsave
738 var i: i64 = start
739 while i <= slen {
740 var k: i64 = 0
741 while k < nsave { saves[k] = 0 - 1; k = k + 1 }
742 let r: i64 = re_bt(rx, 0, i, 0)
743 if r >= 0 { return saves[0] }
744 if fuel[0] <= 0 { return 0 - 1 }
745 i = i + 1
746 }
747 return 0 - 1
748}
749// convenience: 1 if the pattern matches anywhere in s, else 0.
750func nx_regex_test(re: *Regex, s: *u8, slen: i64) -> i64 {
751 let saves: *i64 = sys_mmap(re.nsave * 8) as *i64
752 if nx_regex_exec(re, s, slen, 0, saves) >= 0 { return 1 }
753 return 0
754}
755// parse a JS flags string ("gimsuy") into bits (unknown flags ignored).
756func nx_regex_flags(fs: *u8, flen: i64) -> i64 {
757 var f: i64 = 0
758 var i: i64 = 0
759 while i < flen {
760 let c: i64 = (fs[i]) as i64
761 if c == 105 { f = f | RXF_I } // i
762 if c == 109 { f = f | RXF_M } // m
763 if c == 115 { f = f | RXF_S } // s
764 if c == 103 { f = f | RXF_G } // g
765 i = i + 1
766 }
767 return f
768}