jz 0.4.0 → 0.5.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/module/regex.js CHANGED
@@ -10,14 +10,31 @@
10
10
  import { typed, asF64, asI64, UNDEF_NAN, mkPtrIR, temp, tempI32 } from '../src/ir.js'
11
11
  import { emit } from '../src/emit.js'
12
12
  import { err, inc, PTR, LAYOUT } from '../src/ctx.js'
13
+ import { includeModule } from '../src/autoload.js'
13
14
 
14
15
  // Build IR that constructs a match array: [full, cap1, cap2, ...]
15
16
  // strLocal, msLocal, meLocal are local names (i32 for ms/me, f64 for str).
16
17
  // Captures read from globals $__re_g${i}_start / _end. -1 → undefined.
17
- const buildMatchArr = (strLocal, msLocal, meLocal, nGroups) => {
18
+ const buildMatchArr = (strLocal, msLocal, meLocal, nGroups, groupNames = []) => {
18
19
  const N = nGroups + 1
19
20
  inc('__alloc', '__mkptr', '__str_slice')
20
21
  const arr = tempI32('mka')
22
+ const arrPtr = temp('mkap')
23
+ const captures = []
24
+ const named = []
25
+ for (let i = 1; i <= nGroups; i++) {
26
+ captures[i] = [tempI32('mkgs'), tempI32('mkge')]
27
+ if (groupNames[i]) named.push([i, groupNames[i]])
28
+ }
29
+ if (named.length) {
30
+ includeModule('collection')
31
+ inc('__hash_new_small', '__hash_set', '__dyn_set')
32
+ }
33
+ const captureValue = i => ['if', ['result', 'f64'],
34
+ ['i32.lt_s', ['local.get', `$${captures[i][0]}`], ['i32.const', 0]],
35
+ ['then', ['f64.const', `nan:${UNDEF_NAN}`]],
36
+ ['else', ['call', '$__str_slice', ['i64.reinterpret_f64', ['local.get', `$${strLocal}`]],
37
+ ['local.get', `$${captures[i][0]}`], ['local.get', `$${captures[i][1]}`]]]]
21
38
  const stmts = [
22
39
  ['local.set', `$${arr}`, ['call', '$__alloc', ['i32.const', 8 + N * 8]]],
23
40
  ['i32.store', ['local.get', `$${arr}`], ['i32.const', N]],
@@ -26,15 +43,31 @@ const buildMatchArr = (strLocal, msLocal, meLocal, nGroups) => {
26
43
  ['call', '$__str_slice', ['i64.reinterpret_f64', ['local.get', `$${strLocal}`]],
27
44
  ['local.get', `$${msLocal}`], ['local.get', `$${meLocal}`]]],
28
45
  ]
46
+ for (let i = 1; i <= nGroups; i++) {
47
+ stmts.push(['local.set', `$${captures[i][0]}`, ['global.get', `$__re_g${i}_start`]])
48
+ stmts.push(['local.set', `$${captures[i][1]}`, ['global.get', `$__re_g${i}_end`]])
49
+ }
29
50
  for (let i = 1; i <= nGroups; i++) {
30
51
  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', ['i64.reinterpret_f64', ['local.get', `$${strLocal}`]],
35
- ['global.get', `$__re_g${i}_start`], ['global.get', `$__re_g${i}_end`]]]]])
52
+ captureValue(i)])
53
+ }
54
+ stmts.push(['local.set', `$${arrPtr}`, mkPtrIR(PTR.ARRAY, 0, ['i32.add', ['local.get', `$${arr}`], ['i32.const', 8]])])
55
+ if (named.length) {
56
+ const groups = temp('mkg')
57
+ stmts.push(['local.set', `$${groups}`, ['call', '$__hash_new_small']])
58
+ for (const [i, name] of named) {
59
+ stmts.push(['local.set', `$${groups}`,
60
+ ['f64.reinterpret_i64', ['call', '$__hash_set',
61
+ ['i64.reinterpret_f64', ['local.get', `$${groups}`]],
62
+ asI64(emit(['str', name])),
63
+ ['i64.reinterpret_f64', captureValue(i)]]]])
64
+ }
65
+ stmts.push(['drop', ['call', '$__dyn_set',
66
+ ['i64.reinterpret_f64', ['local.get', `$${arrPtr}`]],
67
+ asI64(emit(['str', 'groups'])),
68
+ ['i64.reinterpret_f64', ['local.get', `$${groups}`]]]])
36
69
  }
37
- stmts.push(mkPtrIR(PTR.ARRAY, 0, ['i32.add', ['local.get', `$${arr}`], ['i32.const', 8]]))
70
+ stmts.push(['local.get', `$${arrPtr}`])
38
71
  return ['block', ['result', 'f64'], ...stmts]
39
72
  }
40
73
 
@@ -43,9 +76,9 @@ const buildMatchArr = (strLocal, msLocal, meLocal, nGroups) => {
43
76
  const PIPE = 124, STAR = 42, PLUS = 43, QUEST = 63, DOT = 46,
44
77
  LBRACK = 91, RBRACK = 93, LPAREN = 40, RPAREN = 41,
45
78
  LBRACE = 123, RBRACE = 125, CARET = 94, DOLLAR = 36,
46
- BSLASH = 92, DASH = 45, COLON = 58, EQUAL = 61, EXCL = 33, LT = 60
79
+ BSLASH = 92, DASH = 45, COLON = 58, EQUAL = 61, EXCL = 33, LT = 60, GT = 62
47
80
 
48
- let src, idx, groupNum
81
+ let src, idx, groupNum, groupNames
49
82
 
50
83
  const cur = () => src.charCodeAt(idx),
51
84
  peek = () => src[idx],
@@ -55,12 +88,13 @@ const cur = () => src.charCodeAt(idx),
55
88
 
56
89
  /** Parse regex pattern → AST */
57
90
  export const parseRegex = (pattern, flags = '') => {
58
- src = pattern; idx = 0; groupNum = 0
91
+ src = pattern; idx = 0; groupNum = 0; groupNames = []
59
92
  let ast = parseAlt()
60
93
  if (!eof()) perr('Unexpected ' + peek())
61
94
  if (typeof ast === 'string') ast = ['seq', ast]
62
95
  if (flags) ast.flags = flags
63
96
  ast.groups = groupNum
97
+ if (groupNames.length) ast.groupNames = groupNames
64
98
  return ast
65
99
  }
66
100
 
@@ -85,7 +119,7 @@ const parseQuantified = () => {
85
119
  if (c === STAR) { skip(); node = ['*', node] }
86
120
  else if (c === PLUS) { skip(); node = ['+', node] }
87
121
  else if (c === QUEST) { skip(); node = ['?', node] }
88
- else if (c === LBRACE) { node = parseRepeat(node) }
122
+ else if (c === LBRACE && isRepeatStart()) { node = parseRepeat(node) }
89
123
  else break
90
124
  if (cur() === QUEST) { skip(); node[0] += '?' }
91
125
  }
@@ -100,6 +134,17 @@ const parseRepeat = node => {
100
134
  return ['{}', node, min, max]
101
135
  }
102
136
 
137
+ const isRepeatStart = () => {
138
+ let i = idx + 1
139
+ if (src.charCodeAt(i) < 48 || src.charCodeAt(i) > 57) return false
140
+ while (src.charCodeAt(i) >= 48 && src.charCodeAt(i) <= 57) i++
141
+ if (src.charCodeAt(i) === RBRACE) return true
142
+ if (src.charCodeAt(i) !== 44) return false
143
+ i++
144
+ while (src.charCodeAt(i) >= 48 && src.charCodeAt(i) <= 57) i++
145
+ return src.charCodeAt(i) === RBRACE
146
+ }
147
+
103
148
  const parseNum = () => {
104
149
  let n = 0
105
150
  while (cur() >= 48 && cur() <= 57) { n = n * 10 + (cur() - 48); skip() }
@@ -143,6 +188,7 @@ const parseEscape = () => {
143
188
  skip()
144
189
  const c = peek()
145
190
  if (c >= '1' && c <= '9') { skip(); return ['\\' + c] }
191
+ if (c === 'k' && src.charCodeAt(idx + 1) === LT) perr('Named backreference unsupported')
146
192
  if ('dDwWsS'.includes(c)) { skip(); return ['\\' + c] }
147
193
  if (c === 'b' || c === 'B') { skip(); return ['\\' + c] }
148
194
  return parseEscapeChar()
@@ -161,7 +207,7 @@ const parseEscapeChar = () => {
161
207
 
162
208
  const parseGroup = () => {
163
209
  skip()
164
- let type = '()', groupId = null
210
+ let type = '()', groupId = null, groupName = null
165
211
  if (cur() === QUEST) {
166
212
  skip(); const c = cur()
167
213
  if (c === COLON) { skip(); type = '(?:)' }
@@ -171,14 +217,34 @@ const parseGroup = () => {
171
217
  skip(); const c2 = cur()
172
218
  if (c2 === EQUAL) { skip(); type = '(?<=)' }
173
219
  else if (c2 === EXCL) { skip(); type = '(?<!)' }
174
- else perr('Invalid group syntax')
220
+ else { groupName = parseGroupName(); groupId = ++groupNum }
175
221
  } else perr('Invalid group syntax')
176
222
  } else groupId = ++groupNum
177
223
  const inner = parseAlt()
178
224
  cur() === RPAREN || perr('Unclosed ('); skip()
225
+ if (groupName) groupNames[groupId] = groupName
179
226
  return groupId ? [type, inner, groupId] : [type, inner]
180
227
  }
181
228
 
229
+ const isGroupNameStart = c =>
230
+ (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c === 36 || c === 95
231
+
232
+ const isGroupNameContinue = c => isGroupNameStart(c) || (c >= 48 && c <= 57)
233
+
234
+ const parseGroupName = () => {
235
+ const start = idx
236
+ isGroupNameStart(cur()) || perr('Invalid group name')
237
+ skip()
238
+ while (!eof() && cur() !== GT) {
239
+ isGroupNameContinue(cur()) || perr('Invalid group name')
240
+ skip()
241
+ }
242
+ cur() === GT || perr('Unclosed group name')
243
+ const name = src.slice(start, idx)
244
+ skip()
245
+ return name
246
+ }
247
+
182
248
 
183
249
  // === WAT Codegen ===
184
250
 
@@ -652,7 +718,7 @@ export default (ctx) => {
652
718
  __str_to_buf: ['__str_byteLen', '__char_at'],
653
719
  })
654
720
 
655
- ctx.runtime.regex = { count: 0, vars: new Map(), compiled: new Map(), groups: new Map() }
721
+ ctx.runtime.regex = { count: 0, vars: new Map(), compiled: new Map(), groups: new Map(), groupNames: new Map() }
656
722
 
657
723
  // SSO → heap normalizer: returns data offset (i32) for direct byte access.
658
724
  // Heap STRING: aux bit SSO_BIT is 0 → offset already points at bytes.
@@ -689,6 +755,7 @@ export default (ctx) => {
689
755
  }
690
756
  }
691
757
  ctx.runtime.regex.groups.set(id, ast.groups || 0)
758
+ ctx.runtime.regex.groupNames.set(id, ast.groupNames || [])
692
759
  ctx.core.stdlib[funcName] = compileRegex(ast, funcName)
693
760
 
694
761
  // Search wrapper: tries match at each position, returns (match_start, match_end) via locals
@@ -749,6 +816,7 @@ export default (ctx) => {
749
816
  const id = resolveRegex(obj)
750
817
  if (id == null) err('regex.exec requires a known regex')
751
818
  const nGroups = ctx.runtime.regex.groups.get(id) || 0
819
+ const groupNames = ctx.runtime.regex.groupNames.get(id) || []
752
820
  const s = temp('re'), ms = tempI32('rems'), me = tempI32('reme')
753
821
  return typed(['block', ['result', 'f64'],
754
822
  ['local.set', `$${s}`, asF64(emit(str))],
@@ -756,9 +824,47 @@ export default (ctx) => {
756
824
  ['call', `$__regex_search_${id}`, ['i64.reinterpret_f64', ['local.get', `$${s}`]]]]],
757
825
  ['if', ['result', 'f64'], ['i32.lt_s', ['local.get', `$${ms}`], ['i32.const', 0]],
758
826
  ['then', ['f64.const', 0]],
759
- ['else', buildMatchArr(s, ms, me, nGroups)]]], 'f64')
827
+ ['else', buildMatchArr(s, ms, me, nGroups, groupNames)]]], 'f64')
828
+ }
829
+
830
+ // === Regex instance properties ===
831
+ // A regex value is a compile-time id; pattern + flags live in the literal AST
832
+ // (`['//', pattern, flags]`) or in the regex-var table. Every property below
833
+ // resolves entirely at compile time. Routed by the `.regex:<prop>` dispatch
834
+ // added to core's `.` handler — registered with arity ≤ 1 (receiver only).
835
+
836
+ /** Resolve a regex-typed operand to its `['//', pattern, flags]` literal AST. */
837
+ const regexAstOf = (obj) => {
838
+ if (Array.isArray(obj) && obj[0] === '//') return obj
839
+ if (typeof obj === 'string' && ctx.runtime.regex.vars.has(obj)) return ctx.runtime.regex.vars.get(obj)
840
+ return null
841
+ }
842
+ const flagsOf = (obj) => { const a = regexAstOf(obj); return (a && a[2]) || '' }
843
+
844
+ // RegExp.prototype.source — the pattern text. A literal stores it verbatim
845
+ // (already grammar-escaped), so `/A/.source` is the 6-char "A".
846
+ // An empty pattern serializes to "(?:)" so the result re-parses to a regex.
847
+ ctx.core.emit['.regex:source'] = (obj) => {
848
+ const a = regexAstOf(obj)
849
+ return emit(['str', (a && a[1]) || '(?:)'])
850
+ }
851
+
852
+ // RegExp.prototype.flags — flag characters in canonical order (sec-get-regexp.prototype.flags).
853
+ const FLAG_ORDER = 'dgimsvy'
854
+ ctx.core.emit['.regex:flags'] = (obj) => {
855
+ const f = flagsOf(obj)
856
+ return emit(['str', [...FLAG_ORDER].filter(c => f.includes(c)).join('')])
760
857
  }
761
858
 
859
+ // Individual flag accessors → 1/0 (jz carries booleans as f64).
860
+ for (const [prop, ch] of [
861
+ ['global', 'g'], ['ignoreCase', 'i'], ['multiline', 'm'], ['dotAll', 's'],
862
+ ['unicode', 'u'], ['sticky', 'y'], ['hasIndices', 'd'], ['unicodeSets', 'v'],
863
+ ]) ctx.core.emit[`.regex:${prop}`] = (obj) => typed(['f64.const', flagsOf(obj).includes(ch) ? 1 : 0], 'f64')
864
+
865
+ // lastIndex — jz regexes are stateless; a freshly-evaluated regex reads 0.
866
+ ctx.core.emit['.regex:lastIndex'] = () => typed(['f64.const', 0], 'f64')
867
+
762
868
  // str.search(/re/) → first match position or -1
763
869
  ctx.core.emit['.string:search'] = (str, search) => {
764
870
  const id = resolveRegex(search)
@@ -796,6 +902,7 @@ export default (ctx) => {
796
902
  ['i32.add', ['local.get', `$${idx}`], ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${q}`]]]]]]]]]], 'f64')
797
903
  }
798
904
  const nGroups = ctx.runtime.regex.groups.get(id) || 0
905
+ const groupNames = ctx.runtime.regex.groupNames.get(id) || []
799
906
  const s = temp('sm'), ms = tempI32('smms'), me = tempI32('smme')
800
907
  return typed(['block', ['result', 'f64'],
801
908
  ['local.set', `$${s}`, asF64(emit(str))],
@@ -803,7 +910,7 @@ export default (ctx) => {
803
910
  ['call', `$__regex_search_${id}`, ['i64.reinterpret_f64', ['local.get', `$${s}`]]]]],
804
911
  ['if', ['result', 'f64'], ['i32.lt_s', ['local.get', `$${ms}`], ['i32.const', 0]],
805
912
  ['then', ['f64.const', 0]],
806
- ['else', buildMatchArr(s, ms, me, nGroups)]]], 'f64')
913
+ ['else', buildMatchArr(s, ms, me, nGroups, groupNames)]]], 'f64')
807
914
  }
808
915
 
809
916
  // str.replace(/re/, repl) → replaced string
package/module/schema.js CHANGED
@@ -22,7 +22,9 @@ export function initSchema(ctx) {
22
22
  ctx.schema._byProp = byProp
23
23
 
24
24
  ctx.schema.register = (props) => {
25
- const key = props.join('\x01')
25
+ // Length prefix disambiguates [] from [''] (both join to '') and any
26
+ // shorter prop list from a longer one whose extra entries are empty.
27
+ const key = props.length + '\x01' + props.join('\x01')
26
28
  const existing = byKey.get(key)
27
29
  if (existing != null) return existing
28
30
  const id = ctx.schema.list.push(props) - 1
@@ -100,4 +102,16 @@ export function initSchema(ctx) {
100
102
  const idx = ctx.schema.list[id]?.indexOf(prop)
101
103
  return idx >= 0 ? (ctx.schema.slotTypes.get(id)?.[idx] ?? null) : null
102
104
  }
105
+
106
+ /** Resolve per-slot intCertain: returns true iff every observed write to
107
+ * `varName.prop` is integer-shaped. Precise path only — requires `varName`
108
+ * to have a bound `schemaId`. Consumers (Math.floor elision, toNumF64 skip,
109
+ * intIndexIR) treat `false`/`null` identically (no narrowing). */
110
+ ctx.schema.slotIntCertainAt = (varName, prop) => {
111
+ const id = ctx.schema.idOf(varName)
112
+ if (id == null) return false
113
+ const idx = ctx.schema.list[id]?.indexOf(prop)
114
+ if (idx < 0) return false
115
+ return ctx.schema.slotIntCertain.get(id)?.[idx] === true
116
+ }
103
117
  }