jz 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,914 @@
1
+ /**
2
+ * Regex module — parser, WAT codegen, and integration.
3
+ *
4
+ * Parses regex patterns into lispy AST, compiles to WASM matching functions.
5
+ * Regex literals become compile-time WASM functions, methods dispatch statically.
6
+ *
7
+ * @module regex
8
+ */
9
+
10
+ import { typed, asF64, UNDEF_NAN, mkPtrIR, temp, tempI32 } from '../src/ir.js'
11
+ import { emit } from '../src/emit.js'
12
+ import { err, inc, PTR } from '../src/ctx.js'
13
+
14
+ // Build IR that constructs a match array: [full, cap1, cap2, ...]
15
+ // strLocal, msLocal, meLocal are local names (i32 for ms/me, f64 for str).
16
+ // Captures read from globals $__re_g${i}_start / _end. -1 → undefined.
17
+ const buildMatchArr = (strLocal, msLocal, meLocal, nGroups) => {
18
+ const N = nGroups + 1
19
+ inc('__alloc', '__mkptr', '__str_slice')
20
+ const arr = tempI32('mka')
21
+ const stmts = [
22
+ ['local.set', `$${arr}`, ['call', '$__alloc', ['i32.const', 8 + N * 8]]],
23
+ ['i32.store', ['local.get', `$${arr}`], ['i32.const', N]],
24
+ ['i32.store', ['i32.add', ['local.get', `$${arr}`], ['i32.const', 4]], ['i32.const', N]],
25
+ ['f64.store', ['i32.add', ['local.get', `$${arr}`], ['i32.const', 8]],
26
+ ['call', '$__str_slice', ['local.get', `$${strLocal}`],
27
+ ['local.get', `$${msLocal}`], ['local.get', `$${meLocal}`]]],
28
+ ]
29
+ for (let i = 1; i <= nGroups; i++) {
30
+ stmts.push(['f64.store', ['i32.add', ['local.get', `$${arr}`], ['i32.const', 8 + i * 8]],
31
+ ['if', ['result', 'f64'],
32
+ ['i32.lt_s', ['global.get', `$__re_g${i}_start`], ['i32.const', 0]],
33
+ ['then', ['f64.const', `nan:${UNDEF_NAN}`]],
34
+ ['else', ['call', '$__str_slice', ['local.get', `$${strLocal}`],
35
+ ['global.get', `$__re_g${i}_start`], ['global.get', `$__re_g${i}_end`]]]]])
36
+ }
37
+ stmts.push(mkPtrIR(PTR.ARRAY, 0, ['i32.add', ['local.get', `$${arr}`], ['i32.const', 8]]))
38
+ return ['block', ['result', 'f64'], ...stmts]
39
+ }
40
+
41
+ // === Parser ===
42
+
43
+ const PIPE = 124, STAR = 42, PLUS = 43, QUEST = 63, DOT = 46,
44
+ LBRACK = 91, RBRACK = 93, LPAREN = 40, RPAREN = 41,
45
+ LBRACE = 123, RBRACE = 125, CARET = 94, DOLLAR = 36,
46
+ BSLASH = 92, DASH = 45, COLON = 58, EQUAL = 61, EXCL = 33, LT = 60
47
+
48
+ let src, idx, groupNum
49
+
50
+ const cur = () => src.charCodeAt(idx),
51
+ peek = () => src[idx],
52
+ skip = (n = 1) => (idx += n, src[idx - n]),
53
+ eof = () => idx >= src.length,
54
+ perr = msg => { throw SyntaxError(`Regex: ${msg} at ${idx}`) }
55
+
56
+ /** Parse regex pattern → AST */
57
+ export const parseRegex = (pattern, flags = '') => {
58
+ src = pattern; idx = 0; groupNum = 0
59
+ let ast = parseAlt()
60
+ if (!eof()) perr('Unexpected ' + peek())
61
+ if (typeof ast === 'string') ast = ['seq', ast]
62
+ if (flags) ast.flags = flags
63
+ ast.groups = groupNum
64
+ return ast
65
+ }
66
+
67
+ const parseAlt = () => {
68
+ const alts = [parseSeq()]
69
+ while (cur() === PIPE) { skip(); alts.push(parseSeq()) }
70
+ return alts.length === 1 ? alts[0] : ['|', ...alts]
71
+ }
72
+
73
+ const parseSeq = () => {
74
+ const items = []
75
+ while (!eof() && cur() !== PIPE && cur() !== RPAREN) items.push(parseQuantified())
76
+ if (items.length === 0) return ['seq']
77
+ if (items.length === 1) return items[0]
78
+ return ['seq', ...items]
79
+ }
80
+
81
+ const parseQuantified = () => {
82
+ let node = parseAtom()
83
+ while (true) {
84
+ const c = cur()
85
+ if (c === STAR) { skip(); node = ['*', node] }
86
+ else if (c === PLUS) { skip(); node = ['+', node] }
87
+ else if (c === QUEST) { skip(); node = ['?', node] }
88
+ else if (c === LBRACE) { node = parseRepeat(node) }
89
+ else break
90
+ if (cur() === QUEST) { skip(); node[0] += '?' }
91
+ }
92
+ return node
93
+ }
94
+
95
+ const parseRepeat = node => {
96
+ skip() // {
97
+ let min = parseNum(), max = min
98
+ if (cur() === 44) { skip(); max = cur() === RBRACE ? Infinity : parseNum() }
99
+ cur() === RBRACE || perr('Expected }'); skip()
100
+ return ['{}', node, min, max]
101
+ }
102
+
103
+ const parseNum = () => {
104
+ let n = 0
105
+ while (cur() >= 48 && cur() <= 57) { n = n * 10 + (cur() - 48); skip() }
106
+ return n
107
+ }
108
+
109
+ const parseAtom = () => {
110
+ const c = cur()
111
+ if (c === CARET) { skip(); return ['^'] }
112
+ if (c === DOLLAR) { skip(); return ['$'] }
113
+ if (c === DOT) { skip(); return ['.'] }
114
+ if (c === LBRACK) return parseClass()
115
+ if (c === LPAREN) return parseGroup()
116
+ if (c === BSLASH) return parseEscape()
117
+ return skip()
118
+ }
119
+
120
+ const parseClass = () => {
121
+ skip() // [
122
+ const negated = cur() === CARET; if (negated) skip()
123
+ const items = []
124
+ while (cur() !== RBRACK && !eof()) {
125
+ const c = parseClassChar()
126
+ if (cur() === DASH && src.charCodeAt(idx + 1) !== RBRACK) { skip(); items.push(['-', c, parseClassChar()]) }
127
+ else items.push(c)
128
+ }
129
+ cur() === RBRACK || perr('Unclosed ['); skip()
130
+ return [negated ? '[^]' : '[]', ...items]
131
+ }
132
+
133
+ const parseClassChar = () => {
134
+ if (cur() === BSLASH) {
135
+ skip(); const c = peek()
136
+ if ('dDwWsS'.includes(c)) { skip(); return ['\\' + c] }
137
+ return parseEscapeChar()
138
+ }
139
+ return skip()
140
+ }
141
+
142
+ const parseEscape = () => {
143
+ skip()
144
+ const c = peek()
145
+ if (c >= '1' && c <= '9') { skip(); return ['\\' + c] }
146
+ if ('dDwWsS'.includes(c)) { skip(); return ['\\' + c] }
147
+ if (c === 'b' || c === 'B') { skip(); return ['\\' + c] }
148
+ return parseEscapeChar()
149
+ }
150
+
151
+ const parseEscapeChar = () => {
152
+ const c = skip()
153
+ if (c === 'n') return '\n'
154
+ if (c === 'r') return '\r'
155
+ if (c === 't') return '\t'
156
+ if (c === '0') return '\0'
157
+ if (c === 'x') { const h = src.slice(idx, idx + 2); idx += 2; return String.fromCharCode(parseInt(h, 16)) }
158
+ if (c === 'u') { const h = src.slice(idx, idx + 4); idx += 4; return String.fromCharCode(parseInt(h, 16)) }
159
+ return c
160
+ }
161
+
162
+ const parseGroup = () => {
163
+ skip()
164
+ let type = '()', groupId = null
165
+ if (cur() === QUEST) {
166
+ skip(); const c = cur()
167
+ if (c === COLON) { skip(); type = '(?:)' }
168
+ else if (c === EQUAL) { skip(); type = '(?=)' }
169
+ else if (c === EXCL) { skip(); type = '(?!)' }
170
+ else if (c === LT) {
171
+ skip(); const c2 = cur()
172
+ if (c2 === EQUAL) { skip(); type = '(?<=)' }
173
+ else if (c2 === EXCL) { skip(); type = '(?<!)' }
174
+ else perr('Invalid group syntax')
175
+ } else perr('Invalid group syntax')
176
+ } else groupId = ++groupNum
177
+ const inner = parseAlt()
178
+ cur() === RPAREN || perr('Unclosed ('); skip()
179
+ return groupId ? [type, inner, groupId] : [type, inner]
180
+ }
181
+
182
+
183
+ // === WAT Codegen ===
184
+
185
+ const CHAR_CLASS_WAT = {
186
+ d: '(i32.and (i32.ge_u (local.get $char) (i32.const 48)) (i32.le_u (local.get $char) (i32.const 57)))',
187
+ w: '(i32.or (i32.or (i32.and (i32.ge_u (local.get $char) (i32.const 97)) (i32.le_u (local.get $char) (i32.const 122))) (i32.and (i32.ge_u (local.get $char) (i32.const 65)) (i32.le_u (local.get $char) (i32.const 90)))) (i32.or (i32.and (i32.ge_u (local.get $char) (i32.const 48)) (i32.le_u (local.get $char) (i32.const 57))) (i32.eq (local.get $char) (i32.const 95))))',
188
+ s: '(i32.or (i32.or (i32.eq (local.get $char) (i32.const 32)) (i32.eq (local.get $char) (i32.const 9))) (i32.or (i32.eq (local.get $char) (i32.const 10)) (i32.eq (local.get $char) (i32.const 13))))'
189
+ }
190
+
191
+ // 8-bit char load at $str + $pos
192
+ const LOAD_CHAR = '(local.set $char (i32.load8_u (i32.add (local.get $str) (local.get $pos))))'
193
+
194
+ /**
195
+ * Compile regex AST → WAT matching function.
196
+ * Generated: (func $name (param $str i32) (param $len i32) (param $start i32) (result i32))
197
+ * Returns end position of match, or -1 on failure.
198
+ */
199
+ export const compileRegex = (ast, name = 'regex_match') => {
200
+ const groups = ast.groups || 0
201
+ const flags = ast.flags || ''
202
+ const ignoreCase = flags.includes('i'), dotAll = flags.includes('s')
203
+
204
+ const locals = ['$pos i32', '$save i32', '$char i32', '$match i32']
205
+ for (let i = 1; i <= groups; i++) locals.push(`$g${i}_start i32`, `$g${i}_end i32`)
206
+
207
+ const rctx = { ignoreCase, dotAll, groups, labelId: 0, code: [], failLabel: null }
208
+ rctx.code.push('(local.set $pos (local.get $start))')
209
+ // Init capture locals to -1 (unmatched / undefined)
210
+ for (let i = 1; i <= groups; i++) {
211
+ rctx.code.push(`(local.set $g${i}_start (i32.const -1))`)
212
+ rctx.code.push(`(local.set $g${i}_end (i32.const -1))`)
213
+ }
214
+ compileNode(ast, rctx)
215
+ // On success, publish captures to module globals (read by .string:match / .regex:exec)
216
+ for (let i = 1; i <= groups; i++) {
217
+ rctx.code.push(`(global.set $__re_g${i}_start (local.get $g${i}_start))`)
218
+ rctx.code.push(`(global.set $__re_g${i}_end (local.get $g${i}_end))`)
219
+ }
220
+ rctx.code.push('(local.get $pos)')
221
+
222
+ return `(func $${name} (param $str i32) (param $len i32) (param $start i32) (result i32)
223
+ (local ${locals.join(') (local ')})
224
+ ${rctx.code.join('\n ')}
225
+ )`
226
+ }
227
+
228
+ const GREEDY_OPS = new Set(['*', '+', '?', '{}'])
229
+ const LAZY_OPS = new Set(['*?', '+?', '??', '{}?'])
230
+
231
+ const compileSeq = (items, c) => {
232
+ for (let i = 0; i < items.length; i++) {
233
+ const item = items[i]
234
+ if (!Array.isArray(item) || i >= items.length - 1) { compileNode(item, c); continue }
235
+ // Greedy quantifier followed by more items → needs backtracking
236
+ if (GREEDY_OPS.has(item[0])) {
237
+ compileGreedyBacktrack(item, items.slice(i + 1), c)
238
+ return
239
+ }
240
+ // Lazy quantifier followed by more items → expand-on-fail
241
+ if (LAZY_OPS.has(item[0])) {
242
+ compileLazyBacktrack(item, items.slice(i + 1), c)
243
+ return
244
+ }
245
+ compileNode(item, c)
246
+ }
247
+ }
248
+
249
+ /** Compile greedy quantifier + rest of sequence with proper backtracking. */
250
+ const compileGreedyBacktrack = (quant, rest, c) => {
251
+ const [op, node, ...qargs] = quant
252
+ const min = op === '+' ? 1 : op === '{}' ? qargs[0] : 0
253
+ const max = op === '?' ? 1 : op === '{}' ? qargs[1] : Infinity
254
+
255
+ // Save position before greedy matching
256
+ const saveL = `$gbt_${c.labelId++}`
257
+ const okL = `$gbt_ok_${c.labelId++}`
258
+ c.code.unshift(`(local ${saveL} i32)`)
259
+ c.code.unshift(`(local ${okL} i32)`)
260
+ c.code.push(`(local.set ${saveL} (local.get $pos))`)
261
+
262
+ // Greedy loop: match as many as possible
263
+ compileRepeatN(node, min, max, true, c)
264
+
265
+ // pos is now at max greedy match end
266
+ // Backtrack loop: try rest, on fail give back one char and retry
267
+ const btLoop = `$bt_${c.labelId++}`
268
+ const btEnd = `$bt_end_${c.labelId++}`
269
+ const btFail = `$bt_fail_${c.labelId++}`
270
+ const btSave = `$bt_sv_${c.labelId++}`
271
+ c.code.unshift(`(local ${btSave} i32)`)
272
+
273
+ c.code.push(`(local.set ${okL} (i32.const 0))`)
274
+ c.code.push(`(block ${btEnd}`)
275
+ c.code.push(`(loop ${btLoop}`)
276
+ // Check min constraint: pos - greedyStart >= min
277
+ if (min > 0) {
278
+ c.code.push(`(br_if ${btEnd} (i32.lt_s (i32.sub (local.get $pos) (local.get ${saveL})) (i32.const ${min})))`)
279
+ } else {
280
+ c.code.push(`(br_if ${btEnd} (i32.lt_s (local.get $pos) (local.get ${saveL})))`)
281
+ }
282
+ // Save pos for restore on failure
283
+ c.code.push(`(local.set ${btSave} (local.get $pos))`)
284
+ // Try rest of sequence
285
+ c.code.push(`(block ${btFail}`)
286
+ const saved = c.failLabel; c.failLabel = btFail
287
+ compileSeq(rest, c)
288
+ c.failLabel = saved
289
+ // Rest succeeded
290
+ c.code.push(`(local.set ${okL} (i32.const 1))`)
291
+ c.code.push(`(br ${btEnd})`)
292
+ c.code.push(')') // end btFail block
293
+ // Rest failed — restore pos and give back one match (backtrack by pattern width)
294
+ c.code.push(`(local.set $pos (i32.sub (local.get ${btSave}) (i32.const ${patternMinLen(node)})))`)
295
+ c.code.push(`(br ${btLoop})`)
296
+ c.code.push(')') // end loop
297
+ c.code.push(')') // end block
298
+
299
+ // Check if backtracking succeeded
300
+ c.code.push(`(if (i32.eqz (local.get ${okL}))`)
301
+ emitFail(c)
302
+ c.code.push(')')
303
+ }
304
+
305
+ /** Compile lazy quantifier + rest with expand-on-fail backtracking. */
306
+ const compileLazyBacktrack = (quant, rest, c) => {
307
+ const [op, node, ...qargs] = quant
308
+ const min = op === '+?' ? 1 : op === '{}?' ? qargs[0] : 0
309
+ const max = op === '??' ? 1 : op === '{}?' ? qargs[1] : Infinity
310
+
311
+ // Match minimum required
312
+ for (let i = 0; i < min; i++) compileNode(node, c)
313
+
314
+ // Lazy expand loop: try rest first, on fail match one more and retry
315
+ const okL = `$lz_ok_${c.labelId++}`
316
+ const ltLoop = `$lz_${c.labelId++}`
317
+ const ltEnd = `$lz_end_${c.labelId++}`
318
+ const ltFail = `$lz_fail_${c.labelId++}`
319
+ const ltSave = `$lz_sv_${c.labelId++}`
320
+ const countL = `$lz_n_${c.labelId++}`
321
+ c.code.unshift(`(local ${okL} i32)`)
322
+ c.code.unshift(`(local ${ltSave} i32)`)
323
+ c.code.unshift(`(local ${countL} i32)`)
324
+
325
+ c.code.push(`(local.set ${okL} (i32.const 0))`)
326
+ c.code.push(`(local.set ${countL} (i32.const 0))`)
327
+ c.code.push(`(block ${ltEnd}`)
328
+ c.code.push(`(loop ${ltLoop}`)
329
+ // Check max constraint
330
+ if (max !== Infinity) {
331
+ c.code.push(`(br_if ${ltEnd} (i32.ge_u (local.get ${countL}) (i32.const ${max - min})))`)
332
+ }
333
+ // Save pos before trying rest
334
+ c.code.push(`(local.set ${ltSave} (local.get $pos))`)
335
+ // Try rest of sequence
336
+ c.code.push(`(block ${ltFail}`)
337
+ const saved = c.failLabel; c.failLabel = ltFail
338
+ compileSeq(rest, c)
339
+ c.failLabel = saved
340
+ // Rest succeeded
341
+ c.code.push(`(local.set ${okL} (i32.const 1))`)
342
+ c.code.push(`(br ${ltEnd})`)
343
+ c.code.push(')') // end ltFail block
344
+ // Rest failed — restore pos, try matching one more
345
+ c.code.push(`(local.set $pos (local.get ${ltSave}))`)
346
+ // Try to match one more instance of the quantified node
347
+ const tryMore = `$lz_try_${c.labelId++}`
348
+ c.code.push(`(block ${tryMore}`)
349
+ const saved2 = c.failLabel; c.failLabel = tryMore
350
+ compileNode(node, c)
351
+ c.failLabel = saved2
352
+ c.code.push(`(local.set ${countL} (i32.add (local.get ${countL}) (i32.const 1)))`)
353
+ c.code.push(`(br ${ltLoop})`)
354
+ c.code.push(')') // end tryMore block
355
+ // Can't match more — fail entirely
356
+ c.code.push(')') // end loop
357
+ c.code.push(')') // end block
358
+
359
+ c.code.push(`(if (i32.eqz (local.get ${okL}))`)
360
+ emitFail(c)
361
+ c.code.push(')')
362
+ }
363
+
364
+ const compileNode = (node, c) => {
365
+ if (typeof node === 'string') { compileLiteral(node, c); return }
366
+ if (!Array.isArray(node)) return
367
+ const [op, ...args] = node
368
+ switch (op) {
369
+ case 'seq': compileSeq(args, c); break
370
+ case '|': compileAlt(args, c); break
371
+ case '*': compileRepeatN(args[0], 0, Infinity, true, c); break
372
+ case '+': compileRepeatN(args[0], 1, Infinity, true, c); break
373
+ case '?': compileRepeatN(args[0], 0, 1, true, c); break
374
+ case '*?': compileRepeatN(args[0], 0, Infinity, false, c); break
375
+ case '+?': compileRepeatN(args[0], 1, Infinity, false, c); break
376
+ case '??': compileRepeatN(args[0], 0, 1, false, c); break
377
+ case '{}': compileRepeatN(args[0], args[1], args[2], true, c); break
378
+ case '{}?': compileRepeatN(args[0], args[1], args[2], false, c); break
379
+ case '[]': compileClassN(args, false, c); break
380
+ case '[^]': compileClassN(args, true, c); break
381
+ case '.': compileDot(c); break
382
+ case '^': compileAnchorStart(c); break
383
+ case '$': compileAnchorEnd(c); break
384
+ case '()': compileCapture(args[0], args[1], c); break
385
+ case '(?:)': compileNode(args[0], c); break
386
+ case '(?=)': compileLookahead(args[0], true, c); break
387
+ case '(?!)': compileLookahead(args[0], false, c); break
388
+ case '(?<=)': compileLookbehind(args[0], true, c); break
389
+ case '(?<!)': compileLookbehind(args[0], false, c); break
390
+ case '\\d': compileCharClassN('d', false, c); break
391
+ case '\\D': compileCharClassN('d', true, c); break
392
+ case '\\w': compileCharClassN('w', false, c); break
393
+ case '\\W': compileCharClassN('w', true, c); break
394
+ case '\\s': compileCharClassN('s', false, c); break
395
+ case '\\S': compileCharClassN('s', true, c); break
396
+ case '\\b': compileWordBoundary(false, c); break
397
+ case '\\B': compileWordBoundary(true, c); break
398
+ case '\\1': case '\\2': case '\\3': case '\\4': case '\\5':
399
+ case '\\6': case '\\7': case '\\8': case '\\9':
400
+ compileBackref(parseInt(op[1]), c); break
401
+ }
402
+ }
403
+
404
+ const emitFail = c => {
405
+ if (c.failLabel) c.code.push(`(then (br ${c.failLabel}))`)
406
+ else c.code.push('(then (return (i32.const -1)))')
407
+ }
408
+
409
+ const compileLiteral = (ch, c) => {
410
+ const code = ch.charCodeAt(0)
411
+ c.code.push('(if (i32.ge_u (local.get $pos) (local.get $len))'); emitFail(c); c.code.push(')')
412
+ c.code.push(LOAD_CHAR)
413
+ if (c.ignoreCase && ((code >= 65 && code <= 90) || (code >= 97 && code <= 122))) {
414
+ const lo = code | 32, hi = lo - 32
415
+ c.code.push(`(if (i32.and (i32.ne (local.get $char) (i32.const ${lo})) (i32.ne (local.get $char) (i32.const ${hi})))`)
416
+ } else {
417
+ c.code.push(`(if (i32.ne (local.get $char) (i32.const ${code}))`)
418
+ }
419
+ emitFail(c); c.code.push(')')
420
+ c.code.push('(local.set $pos (i32.add (local.get $pos) (i32.const 1)))')
421
+ }
422
+
423
+ const compileAlt = (branches, c) => {
424
+ const endLabel = `$alt_end_${c.labelId++}`
425
+ c.code.push(`(block ${endLabel}`)
426
+ for (let i = 0; i < branches.length; i++) {
427
+ const isLast = i === branches.length - 1
428
+ const tryLabel = `$alt_try_${c.labelId++}`
429
+ if (!isLast) { c.code.push(`(block ${tryLabel}`); c.code.push('(local.set $save (local.get $pos))') }
430
+ const saved = c.failLabel
431
+ if (!isLast) c.failLabel = tryLabel
432
+ compileNode(branches[i], c)
433
+ c.failLabel = saved
434
+ if (!isLast) {
435
+ c.code.push(`(br ${endLabel})`); c.code.push(')') // end try block
436
+ c.code.push('(local.set $pos (local.get $save))')
437
+ }
438
+ }
439
+ c.code.push(')')
440
+ }
441
+
442
+ const compileRepeatN = (node, min, max, greedy, c) => {
443
+ const loopLabel = `$rep_loop_${c.labelId++}`, endLabel = `$rep_end_${c.labelId++}`
444
+ const countLocal = `$count_${c.labelId++}`
445
+ c.code.unshift(`(local ${countLocal} i32)`)
446
+ c.code.push(`(local.set ${countLocal} (i32.const 0))`)
447
+
448
+ if (greedy) {
449
+ c.code.push(`(block ${endLabel}`); c.code.push(`(loop ${loopLabel}`)
450
+ if (max !== Infinity) c.code.push(`(br_if ${endLabel} (i32.ge_u (local.get ${countLocal}) (i32.const ${max})))`)
451
+ c.code.push('(local.set $save (local.get $pos))')
452
+ const tryLabel = `$rep_try_${c.labelId++}`
453
+ c.code.push(`(block ${tryLabel}`)
454
+ const saved = c.failLabel; c.failLabel = tryLabel
455
+ compileNode(node, c); c.failLabel = saved
456
+ c.code.push(`(local.set ${countLocal} (i32.add (local.get ${countLocal}) (i32.const 1)))`)
457
+ c.code.push(`(br ${loopLabel})`); c.code.push(')') // end try
458
+ c.code.push('(local.set $pos (local.get $save))')
459
+ c.code.push(')'); c.code.push(')') // end loop, block
460
+ } else {
461
+ for (let i = 0; i < min; i++) compileNode(node, c)
462
+ if (max > min) {
463
+ c.code.push(`(block ${endLabel}`); c.code.push(`(loop ${loopLabel}`)
464
+ if (max !== Infinity) c.code.push(`(br_if ${endLabel} (i32.ge_u (local.get ${countLocal}) (i32.const ${max - min})))`)
465
+ c.code.push('(local.set $save (local.get $pos))')
466
+ const tryLabel = `$rep_try_${c.labelId++}`
467
+ c.code.push(`(block ${tryLabel}`)
468
+ const saved = c.failLabel; c.failLabel = tryLabel
469
+ compileNode(node, c); c.failLabel = saved
470
+ c.code.push(`(local.set ${countLocal} (i32.add (local.get ${countLocal}) (i32.const 1)))`)
471
+ c.code.push(`(br ${loopLabel})`); c.code.push(')')
472
+ c.code.push('(local.set $pos (local.get $save))')
473
+ c.code.push(')'); c.code.push(')')
474
+ }
475
+ }
476
+
477
+ if (min > 0 && greedy) {
478
+ c.code.push(`(if (i32.lt_u (local.get ${countLocal}) (i32.const ${min}))`)
479
+ emitFail(c); c.code.push(')')
480
+ }
481
+ }
482
+
483
+ const compileClassItem = (item, c) => {
484
+ if (typeof item === 'string') {
485
+ const code = item.charCodeAt(0)
486
+ if (c.ignoreCase && ((code >= 65 && code <= 90) || (code >= 97 && code <= 122))) {
487
+ const lo = code | 32, hi = lo - 32
488
+ return `(i32.or (i32.eq (local.get $char) (i32.const ${lo})) (i32.eq (local.get $char) (i32.const ${hi})))`
489
+ }
490
+ return `(i32.eq (local.get $char) (i32.const ${code}))`
491
+ }
492
+ if (Array.isArray(item)) {
493
+ if (item[0] === '-') {
494
+ const lo = item[1].charCodeAt(0), hi = item[2].charCodeAt(0)
495
+ if (c.ignoreCase && lo >= 65 && hi <= 122) {
496
+ const loL = lo | 32, loU = lo & ~32, hiL = hi | 32, hiU = hi & ~32
497
+ return `(i32.or (i32.and (i32.ge_u (local.get $char) (i32.const ${loL})) (i32.le_u (local.get $char) (i32.const ${hiL}))) (i32.and (i32.ge_u (local.get $char) (i32.const ${loU})) (i32.le_u (local.get $char) (i32.const ${hiU}))))`
498
+ }
499
+ return `(i32.and (i32.ge_u (local.get $char) (i32.const ${lo})) (i32.le_u (local.get $char) (i32.const ${hi})))`
500
+ }
501
+ if (item[0] === '\\d') return CHAR_CLASS_WAT.d
502
+ if (item[0] === '\\w') return CHAR_CLASS_WAT.w
503
+ if (item[0] === '\\s') return CHAR_CLASS_WAT.s
504
+ }
505
+ return null
506
+ }
507
+
508
+ const compileClassN = (items, negated, c) => {
509
+ c.code.push('(if (i32.ge_u (local.get $pos) (local.get $len))'); emitFail(c); c.code.push(')')
510
+ c.code.push(LOAD_CHAR)
511
+ const tests = items.map(i => compileClassItem(i, c)).filter(Boolean)
512
+ const condition = tests.length === 1 ? tests[0] : tests.reduce((a, b) => `(i32.or ${a} ${b})`)
513
+ const check = negated ? `(i32.eqz ${condition})` : condition
514
+ c.code.push(`(if (i32.eqz ${check})`); emitFail(c); c.code.push(')')
515
+ c.code.push('(local.set $pos (i32.add (local.get $pos) (i32.const 1)))')
516
+ }
517
+
518
+ const compileCharClassN = (cls, negated, c) => {
519
+ c.code.push('(if (i32.ge_u (local.get $pos) (local.get $len))'); emitFail(c); c.code.push(')')
520
+ c.code.push(LOAD_CHAR)
521
+ const condition = CHAR_CLASS_WAT[cls]
522
+ const check = negated ? condition : `(i32.eqz ${condition})`
523
+ c.code.push(`(if ${check}`); emitFail(c); c.code.push(')')
524
+ c.code.push('(local.set $pos (i32.add (local.get $pos) (i32.const 1)))')
525
+ }
526
+
527
+ const compileDot = c => {
528
+ c.code.push('(if (i32.ge_u (local.get $pos) (local.get $len))'); emitFail(c); c.code.push(')')
529
+ if (!c.dotAll) {
530
+ c.code.push(LOAD_CHAR)
531
+ c.code.push('(if (i32.eq (local.get $char) (i32.const 10))'); emitFail(c); c.code.push(')')
532
+ }
533
+ c.code.push('(local.set $pos (i32.add (local.get $pos) (i32.const 1)))')
534
+ }
535
+
536
+ const compileAnchorStart = c => {
537
+ c.code.push('(if (i32.ne (local.get $pos) (i32.const 0))'); emitFail(c); c.code.push(')')
538
+ }
539
+
540
+ const compileAnchorEnd = c => {
541
+ c.code.push('(if (i32.ne (local.get $pos) (local.get $len))'); emitFail(c); c.code.push(')')
542
+ }
543
+
544
+ const compileCapture = (inner, groupId, c) => {
545
+ c.code.push(`(local.set $g${groupId}_start (local.get $pos))`)
546
+ compileNode(inner, c)
547
+ c.code.push(`(local.set $g${groupId}_end (local.get $pos))`)
548
+ }
549
+
550
+ const compileLookahead = (inner, positive, c) => {
551
+ c.code.push('(local.set $save (local.get $pos))')
552
+ const label = `$look_${c.labelId++}`
553
+ c.code.push(`(block ${label}`)
554
+ const saved = c.failLabel; c.failLabel = label
555
+ compileNode(inner, c); c.failLabel = saved
556
+ c.code.push('(local.set $match (i32.const 1))')
557
+ c.code.push(`(br ${label})`); c.code.push(')')
558
+ c.code.push('(local.set $pos (local.get $save))')
559
+ if (positive) { c.code.push('(if (i32.eqz (local.get $match))'); emitFail(c); c.code.push(')') }
560
+ else { c.code.push('(if (local.get $match)'); emitFail(c); c.code.push(')') }
561
+ c.code.push('(local.set $match (i32.const 0))')
562
+ }
563
+
564
+ const compileLookbehind = (inner, positive, c) => {
565
+ c.code.push('(local.set $save (local.get $pos))')
566
+ const len = patternMinLen(inner)
567
+ if (len > 0) {
568
+ c.code.push(`(if (i32.lt_u (local.get $pos) (i32.const ${len}))`)
569
+ if (positive) { emitFail(c); c.code.push(')') }
570
+ else c.code.push('(then (nop)))')
571
+ c.code.push(`(local.set $pos (i32.sub (local.get $pos) (i32.const ${len})))`)
572
+ const label = `$lookb_${c.labelId++}`
573
+ c.code.push(`(block ${label}`)
574
+ const saved = c.failLabel; c.failLabel = label
575
+ compileNode(inner, c); c.failLabel = saved
576
+ c.code.push('(local.set $match (i32.const 1))')
577
+ c.code.push(`(br ${label})`); c.code.push(')')
578
+ c.code.push('(local.set $pos (local.get $save))')
579
+ if (positive) { c.code.push('(if (i32.eqz (local.get $match))'); emitFail(c); c.code.push(')') }
580
+ else { c.code.push('(if (local.get $match)'); emitFail(c); c.code.push(')') }
581
+ c.code.push('(local.set $match (i32.const 0))')
582
+ }
583
+ }
584
+
585
+ const compileWordBoundary = (negated, c) => {
586
+ const isWord = CHAR_CLASS_WAT.w
587
+ c.code.push('(local.set $match (i32.const 0))')
588
+ c.code.push('(if (i32.gt_u (local.get $pos) (i32.const 0))')
589
+ c.code.push('(then')
590
+ c.code.push('(local.set $char (i32.load8_u (i32.add (local.get $str) (i32.sub (local.get $pos) (i32.const 1)))))')
591
+ c.code.push(`(local.set $match ${isWord})`)
592
+ c.code.push('))')
593
+ c.code.push('(local.set $save (local.get $match))')
594
+ c.code.push('(local.set $match (i32.const 0))')
595
+ c.code.push('(if (i32.lt_u (local.get $pos) (local.get $len))')
596
+ c.code.push('(then')
597
+ c.code.push(LOAD_CHAR)
598
+ c.code.push(`(local.set $match ${isWord})`)
599
+ c.code.push('))')
600
+ c.code.push('(local.set $match (i32.xor (local.get $save) (local.get $match)))')
601
+ if (negated) c.code.push('(if (local.get $match)')
602
+ else c.code.push('(if (i32.eqz (local.get $match))')
603
+ emitFail(c); c.code.push(')')
604
+ }
605
+
606
+ const compileBackref = (n, c) => {
607
+ const sL = `$g${n}_start`, eL = `$g${n}_end`
608
+ const loopL = `$backref_${c.labelId++}`, endL = `$backref_end_${c.labelId++}`
609
+ const iL = `$br_i_${c.labelId++}`
610
+ c.code.unshift(`(local ${iL} i32)`)
611
+ c.code.push(`(local.set ${iL} (local.get ${sL}))`)
612
+ c.code.push(`(block ${endL}`); c.code.push(`(loop ${loopL}`)
613
+ c.code.push(`(br_if ${endL} (i32.ge_u (local.get ${iL}) (local.get ${eL})))`)
614
+ c.code.push('(if (i32.ge_u (local.get $pos) (local.get $len))'); emitFail(c); c.code.push(')')
615
+ c.code.push(`(local.set $char (i32.load8_u (i32.add (local.get $str) (local.get ${iL}))))`)
616
+ c.code.push(`(local.set $save (i32.load8_u (i32.add (local.get $str) (local.get $pos))))`)
617
+ if (c.ignoreCase) {
618
+ c.code.push('(if (i32.and (i32.ne (i32.or (local.get $char) (i32.const 32)) (i32.or (local.get $save) (i32.const 32))) (i32.or (i32.lt_u (local.get $char) (i32.const 65)) (i32.gt_u (local.get $char) (i32.const 122))))')
619
+ } else {
620
+ c.code.push('(if (i32.ne (local.get $char) (local.get $save))')
621
+ }
622
+ emitFail(c); c.code.push(')')
623
+ c.code.push(`(local.set ${iL} (i32.add (local.get ${iL}) (i32.const 1)))`)
624
+ c.code.push('(local.set $pos (i32.add (local.get $pos) (i32.const 1)))')
625
+ c.code.push(`(br ${loopL})`); c.code.push(')'); c.code.push(')')
626
+ }
627
+
628
+ const patternMinLen = node => {
629
+ if (typeof node === 'string') return 1
630
+ if (!Array.isArray(node)) return 0
631
+ const [op, ...args] = node
632
+ switch (op) {
633
+ case 'seq': return args.reduce((s, a) => s + patternMinLen(a), 0)
634
+ case '|': return Math.min(...args.map(patternMinLen))
635
+ case '*': case '*?': case '?': case '??': return 0
636
+ case '+': case '+?': return patternMinLen(args[0])
637
+ case '{}': case '{}?': return args[1] * patternMinLen(args[0])
638
+ case '[]': case '[^]': case '.': return 1
639
+ case '\\d': case '\\D': case '\\w': case '\\W': case '\\s': case '\\S': return 1
640
+ case '()': case '(?:)': return patternMinLen(args[0])
641
+ case '(?=)': case '(?!)': case '(?<=)': case '(?<!)': return 0
642
+ case '^': case '$': case '\\b': case '\\B': return 0
643
+ default: return 0
644
+ }
645
+ }
646
+
647
+
648
+ // === Module init ===
649
+
650
+ export default (ctx) => {
651
+ Object.assign(ctx.core.stdlibDeps, {
652
+ __str_to_buf: ['__str_byteLen', '__char_at'],
653
+ })
654
+
655
+ ctx.runtime.regex = { count: 0, vars: new Map(), compiled: new Map(), groups: new Map() }
656
+
657
+ // SSO → heap normalizer: returns data offset (i32) for direct byte access
658
+ ctx.core.stdlib['__str_to_buf'] = `(func $__str_to_buf (param $ptr f64) (result i32)
659
+ (local $type i32) (local $off i32) (local $len i32) (local $buf i32) (local $i i32)
660
+ (local.set $type (call $__ptr_type (local.get $ptr)))
661
+ (if (i32.eq (local.get $type) (i32.const 4))
662
+ (then (return (call $__ptr_offset (local.get $ptr)))))
663
+ (local.set $off (call $__ptr_offset (local.get $ptr)))
664
+ (local.set $len (call $__ptr_aux (local.get $ptr)))
665
+ (local.set $buf (call $__alloc (local.get $len)))
666
+ (local.set $i (i32.const 0))
667
+ (block $done (loop $next
668
+ (br_if $done (i32.ge_u (local.get $i) (local.get $len)))
669
+ (i32.store8 (i32.add (local.get $buf) (local.get $i))
670
+ (i32.and (i32.shr_u (local.get $off) (i32.mul (local.get $i) (i32.const 8))) (i32.const 0xFF)))
671
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
672
+ (br $next)))
673
+ (local.get $buf))`
674
+
675
+ /** Compile regex pattern to WASM function, return regex ID */
676
+ const compileRegexToStdlib = (pattern, flags) => {
677
+ const key = pattern + ':' + (flags || '')
678
+ if (ctx.runtime.regex.compiled.has(key)) return ctx.runtime.regex.compiled.get(key)
679
+ const id = ctx.runtime.regex.count++
680
+ const ast = parseRegex(pattern, flags)
681
+ const funcName = `__regex_${id}`
682
+ // Reserve mutable globals for capture group start/end (shared across regexes by index)
683
+ for (let i = 1; i <= (ast.groups || 0); i++) {
684
+ if (!ctx.scope.globals.has(`__re_g${i}_start`)) {
685
+ ctx.scope.globals.set(`__re_g${i}_start`, `(global $__re_g${i}_start (mut i32) (i32.const -1))`)
686
+ ctx.scope.globals.set(`__re_g${i}_end`, `(global $__re_g${i}_end (mut i32) (i32.const -1))`)
687
+ }
688
+ }
689
+ ctx.runtime.regex.groups.set(id, ast.groups || 0)
690
+ ctx.core.stdlib[funcName] = compileRegex(ast, funcName)
691
+
692
+ // Search wrapper: tries match at each position, returns (match_start, match_end) via locals
693
+ const searchName = `__regex_search_${id}`
694
+ ctx.core.stdlib[searchName] = `(func $${searchName} (param $str f64) (result i32 i32)
695
+ (local $off i32) (local $len i32) (local $pos i32) (local $result i32)
696
+ (local.set $off (call $__str_to_buf (local.get $str)))
697
+ (local.set $len (call $__str_byteLen (local.get $str)))
698
+ (local.set $pos (i32.const 0))
699
+ (block $done (loop $next
700
+ (br_if $done (i32.gt_s (local.get $pos) (local.get $len)))
701
+ (local.set $result (call $${funcName} (local.get $off) (local.get $len) (local.get $pos)))
702
+ (if (i32.ge_s (local.get $result) (i32.const 0))
703
+ (then (return (local.get $pos) (local.get $result))))
704
+ (local.set $pos (i32.add (local.get $pos) (i32.const 1)))
705
+ (br $next)))
706
+ (i32.const -1) (i32.const -1))`
707
+
708
+ inc(funcName, searchName, '__str_to_buf')
709
+ ctx.runtime.regex.compiled.set(key, id)
710
+ return id
711
+ }
712
+
713
+ /** Resolve regex ID from AST node (inline regex or variable) */
714
+ const resolveRegex = (obj) => {
715
+ if (Array.isArray(obj) && obj[0] === '//') return compileRegexToStdlib(obj[1], obj[2])
716
+ if (typeof obj === 'string' && ctx.runtime.regex.vars.has(obj)) {
717
+ const ast = ctx.runtime.regex.vars.get(obj)
718
+ return compileRegexToStdlib(ast[1], ast[2])
719
+ }
720
+ return null
721
+ }
722
+
723
+ // Regex literal: ['//','pattern','flags?'] → compile + store
724
+ ctx.core.emit['//'] = (pattern, flags) => {
725
+ const id = compileRegexToStdlib(pattern, flags)
726
+ ctx.runtime.regex._lastId = id // for variable tracking
727
+ return typed(['i32.const', id], 'i32')
728
+ }
729
+
730
+ // regex.test(str) → search, return 1/0
731
+ ctx.core.emit['.regex:test'] = (obj, str) => {
732
+ const id = resolveRegex(obj)
733
+ if (id == null) err('regex.test requires a known regex')
734
+ const s = temp('rt'), mstart = tempI32('rms'), mend = tempI32('rme')
735
+ return typed(['block', ['result', 'f64'],
736
+ ['local.set', `$${s}`, asF64(emit(str))],
737
+ ['local.set', `$${mstart}`, ['local.set', `$${mend}`,
738
+ ['call', `$__regex_search_${id}`, ['local.get', `$${s}`]]]],
739
+ // search returns (start, end) multi-value; capture both
740
+ ['if', ['result', 'f64'], ['i32.ge_s', ['local.get', `$${mstart}`], ['i32.const', 0]],
741
+ ['then', ['f64.const', 1]],
742
+ ['else', ['f64.const', 0]]]], 'f64')
743
+ }
744
+
745
+ // regex.exec(str) → [match_text, cap1, ...] array or 0 (null)
746
+ ctx.core.emit['.regex:exec'] = (obj, str) => {
747
+ const id = resolveRegex(obj)
748
+ if (id == null) err('regex.exec requires a known regex')
749
+ const nGroups = ctx.runtime.regex.groups.get(id) || 0
750
+ const s = temp('re'), ms = tempI32('rems'), me = tempI32('reme')
751
+ return typed(['block', ['result', 'f64'],
752
+ ['local.set', `$${s}`, asF64(emit(str))],
753
+ ['local.set', `$${ms}`, ['local.set', `$${me}`,
754
+ ['call', `$__regex_search_${id}`, ['local.get', `$${s}`]]]],
755
+ ['if', ['result', 'f64'], ['i32.lt_s', ['local.get', `$${ms}`], ['i32.const', 0]],
756
+ ['then', ['f64.const', 0]],
757
+ ['else', buildMatchArr(s, ms, me, nGroups)]]], 'f64')
758
+ }
759
+
760
+ // str.search(/re/) → first match position or -1
761
+ ctx.core.emit['.string:search'] = (str, search) => {
762
+ const id = resolveRegex(search)
763
+ if (id == null) {
764
+ // Fall back to string search (indexOf)
765
+ inc('__str_indexof')
766
+ return typed(['f64.convert_i32_s', ['call', '$__str_indexof', asF64(emit(str)), asF64(emit(search))]], 'f64')
767
+ }
768
+ const s = temp('ss'), ms = tempI32('ssms'), me = tempI32('ssme')
769
+ return typed(['block', ['result', 'f64'],
770
+ ['local.set', `$${s}`, asF64(emit(str))],
771
+ ['local.set', `$${ms}`, ['local.set', `$${me}`,
772
+ ['call', `$__regex_search_${id}`, ['local.get', `$${s}`]]]],
773
+ ['f64.convert_i32_s', ['local.get', `$${ms}`]]], 'f64')
774
+ }
775
+
776
+ // str.match(/re/) → [match_text] or 0
777
+ ctx.core.emit['.string:match'] = (str, search) => {
778
+ const id = resolveRegex(search)
779
+ if (id == null) {
780
+ // Fall back to string match
781
+ inc('__str_indexof', '__str_slice', '__wrap1', '__str_byteLen')
782
+ const s = temp('ms'), q = temp('mq'), idx = tempI32('mi')
783
+ return typed(['block', ['result', 'f64'],
784
+ ['local.set', `$${s}`, asF64(emit(str))],
785
+ ['local.set', `$${q}`, asF64(emit(search))],
786
+ ['local.set', `$${idx}`, ['call', '$__str_indexof', ['local.get', `$${s}`], ['local.get', `$${q}`]]],
787
+ ['if', ['result', 'f64'], ['i32.lt_s', ['local.get', `$${idx}`], ['i32.const', 0]],
788
+ ['then', ['f64.const', 0]],
789
+ ['else',
790
+ ['call', '$__wrap1',
791
+ ['call', '$__str_slice', ['local.get', `$${s}`],
792
+ ['local.get', `$${idx}`],
793
+ ['i32.add', ['local.get', `$${idx}`], ['call', '$__str_byteLen', ['local.get', `$${q}`]]]]]]]], 'f64')
794
+ }
795
+ const nGroups = ctx.runtime.regex.groups.get(id) || 0
796
+ const s = temp('sm'), ms = tempI32('smms'), me = tempI32('smme')
797
+ return typed(['block', ['result', 'f64'],
798
+ ['local.set', `$${s}`, asF64(emit(str))],
799
+ ['local.set', `$${ms}`, ['local.set', `$${me}`,
800
+ ['call', `$__regex_search_${id}`, ['local.get', `$${s}`]]]],
801
+ ['if', ['result', 'f64'], ['i32.lt_s', ['local.get', `$${ms}`], ['i32.const', 0]],
802
+ ['then', ['f64.const', 0]],
803
+ ['else', buildMatchArr(s, ms, me, nGroups)]]], 'f64')
804
+ }
805
+
806
+ // str.replace(/re/, repl) → replaced string
807
+ ctx.core.emit['.string:replace'] = (str, search, repl) => {
808
+ const id = resolveRegex(search)
809
+ if (id == null) {
810
+ // Fall back to string replace
811
+ inc('__str_replace')
812
+ return typed(['call', '$__str_replace', asF64(emit(str)), asF64(emit(search)), asF64(emit(repl))], 'f64')
813
+ }
814
+ inc('__str_slice', '__str_concat', '__str_byteLen')
815
+ const s = temp('sr'), r = temp('srr'), ms = tempI32('srms'), me = tempI32('srme')
816
+ return typed(['block', ['result', 'f64'],
817
+ ['local.set', `$${s}`, asF64(emit(str))],
818
+ ['local.set', `$${r}`, asF64(emit(repl))],
819
+ ['local.set', `$${ms}`, ['local.set', `$${me}`,
820
+ ['call', `$__regex_search_${id}`, ['local.get', `$${s}`]]]],
821
+ ['if', ['result', 'f64'], ['i32.lt_s', ['local.get', `$${ms}`], ['i32.const', 0]],
822
+ ['then', ['local.get', `$${s}`]],
823
+ ['else',
824
+ ['call', '$__str_concat',
825
+ ['call', '$__str_concat',
826
+ ['call', '$__str_slice', ['local.get', `$${s}`], ['i32.const', 0], ['local.get', `$${ms}`]],
827
+ ['local.get', `$${r}`]],
828
+ ['call', '$__str_slice', ['local.get', `$${s}`], ['local.get', `$${me}`],
829
+ ['call', '$__str_byteLen', ['local.get', `$${s}`]]]]]]], 'f64')
830
+ }
831
+
832
+ // str.split(/re/) → array of substrings
833
+ ctx.core.emit['.string:split'] = (str, sep) => {
834
+ const id = resolveRegex(sep)
835
+ if (id == null) {
836
+ // Fall back to string split
837
+ inc('__str_split')
838
+ return typed(['call', '$__str_split', asF64(emit(str)), asF64(emit(sep))], 'f64')
839
+ }
840
+
841
+ // Generate a split-by-regex WAT function for this regex
842
+ const splitName = `__regex_split_${id}`
843
+ if (!ctx.core.stdlib[splitName]) {
844
+ inc('__str_to_buf', '__str_slice', '__alloc')
845
+ ctx.core.stdlib[splitName] = `(func $${splitName} (param $str f64) (result f64)
846
+ (local $off i32) (local $len i32) (local $pos i32) (local $result i32)
847
+ (local $mstart i32) (local $mend i32) (local $prevEnd i32)
848
+ (local $arrOff i32) (local $count i32) (local $cap i32)
849
+ (local $newArr i32) (local $j i32)
850
+ (local.set $off (call $__str_to_buf (local.get $str)))
851
+ (local.set $len (call $__str_byteLen (local.get $str)))
852
+ ;; Alloc result array (cap=8 initially)
853
+ (local.set $cap (i32.const 8))
854
+ (local.set $arrOff (call $__alloc (i32.add (i32.const 8) (i32.mul (local.get $cap) (i32.const 8)))))
855
+ (local.set $prevEnd (i32.const 0))
856
+ (local.set $count (i32.const 0))
857
+ (local.set $pos (i32.const 0))
858
+ (block $done (loop $next
859
+ (br_if $done (i32.gt_s (local.get $pos) (local.get $len)))
860
+ (local.set $result (call $__regex_${id} (local.get $off) (local.get $len) (local.get $pos)))
861
+ (if (i32.lt_s (local.get $result) (i32.const 0))
862
+ (then
863
+ ;; No match at this position — advance and try next
864
+ (local.set $pos (i32.add (local.get $pos) (i32.const 1)))
865
+ (br $next)))
866
+ ;; Found match at $pos..$result — slice prevEnd..pos into array
867
+ (local.set $mstart (local.get $pos))
868
+ (local.set $mend (local.get $result))
869
+ ;; Grow array if at capacity
870
+ (if (i32.ge_u (local.get $count) (local.get $cap))
871
+ (then
872
+ (local.set $cap (i32.shl (local.get $cap) (i32.const 1)))
873
+ (local.set $newArr (call $__alloc (i32.add (i32.const 8) (i32.mul (local.get $cap) (i32.const 8)))))
874
+ (local.set $j (i32.const 0))
875
+ (block $cd (loop $cl
876
+ (br_if $cd (i32.ge_s (local.get $j) (local.get $count)))
877
+ (f64.store (i32.add (i32.add (local.get $newArr) (i32.const 8)) (i32.shl (local.get $j) (i32.const 3)))
878
+ (f64.load (i32.add (i32.add (local.get $arrOff) (i32.const 8)) (i32.shl (local.get $j) (i32.const 3)))))
879
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
880
+ (br $cl)))
881
+ (local.set $arrOff (local.get $newArr))))
882
+ (f64.store (i32.add (i32.add (local.get $arrOff) (i32.const 8)) (i32.mul (local.get $count) (i32.const 8)))
883
+ (call $__str_slice (local.get $str) (local.get $prevEnd) (local.get $mstart)))
884
+ (local.set $count (i32.add (local.get $count) (i32.const 1)))
885
+ (local.set $prevEnd (local.get $mend))
886
+ ;; Advance past match (at least 1 to avoid infinite loop on zero-length match)
887
+ (local.set $pos (select (i32.add (local.get $mend) (i32.const 1)) (local.get $mend) (i32.eq (local.get $mstart) (local.get $mend))))
888
+ (br $next)))
889
+ ;; Final segment: prevEnd..len — grow if needed
890
+ (if (i32.ge_u (local.get $count) (local.get $cap))
891
+ (then
892
+ (local.set $cap (i32.shl (local.get $cap) (i32.const 1)))
893
+ (local.set $newArr (call $__alloc (i32.add (i32.const 8) (i32.mul (local.get $cap) (i32.const 8)))))
894
+ (local.set $j (i32.const 0))
895
+ (block $cd2 (loop $cl2
896
+ (br_if $cd2 (i32.ge_s (local.get $j) (local.get $count)))
897
+ (f64.store (i32.add (i32.add (local.get $newArr) (i32.const 8)) (i32.shl (local.get $j) (i32.const 3)))
898
+ (f64.load (i32.add (i32.add (local.get $arrOff) (i32.const 8)) (i32.shl (local.get $j) (i32.const 3)))))
899
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
900
+ (br $cl2)))
901
+ (local.set $arrOff (local.get $newArr))))
902
+ (f64.store (i32.add (i32.add (local.get $arrOff) (i32.const 8)) (i32.mul (local.get $count) (i32.const 8)))
903
+ (call $__str_slice (local.get $str) (local.get $prevEnd) (local.get $len)))
904
+ (local.set $count (i32.add (local.get $count) (i32.const 1)))
905
+ ;; Write array header (len + cap at arrOff)
906
+ (i32.store (local.get $arrOff) (local.get $count))
907
+ (i32.store (i32.add (local.get $arrOff) (i32.const 4)) (local.get $cap))
908
+ (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (i32.add (local.get $arrOff) (i32.const 8))))`
909
+ inc(splitName)
910
+ }
911
+
912
+ return typed(['call', `$${splitName}`, asF64(emit(str))], 'f64')
913
+ }
914
+ }