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.
- package/LICENSE +21 -0
- package/README.md +373 -0
- package/cli.js +163 -0
- package/index.js +247 -0
- package/module/array.js +1322 -0
- package/module/collection.js +793 -0
- package/module/console.js +192 -0
- package/module/core.js +644 -0
- package/module/function.js +181 -0
- package/module/index.js +15 -0
- package/module/json.js +506 -0
- package/module/math.js +390 -0
- package/module/number.js +601 -0
- package/module/object.js +359 -0
- package/module/regex.js +914 -0
- package/module/schema.js +106 -0
- package/module/string.js +985 -0
- package/module/symbol.js +55 -0
- package/module/timer.js +253 -0
- package/module/typedarray.js +713 -0
- package/package.json +56 -5
- package/src/analyze.js +1927 -0
- package/src/autoload.js +175 -0
- package/src/compile.js +1211 -0
- package/src/ctx.js +244 -0
- package/src/emit.js +2105 -0
- package/src/host.js +536 -0
- package/src/ir.js +649 -0
- package/src/jzify.js +418 -0
- package/src/key.js +73 -0
- package/src/narrow.js +928 -0
- package/src/optimize.js +1352 -0
- package/src/plan.js +105 -0
- package/src/prepare.js +1534 -0
- package/src/source.js +76 -0
- package/wasi.js +80 -0
package/module/regex.js
ADDED
|
@@ -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
|
+
}
|