jz 0.0.0 → 0.1.0

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