code wiki / (root) / nx_rxfull.nx

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}