jz 0.5.1 → 0.6.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.
Files changed (80) hide show
  1. package/README.md +288 -314
  2. package/bench/README.md +319 -0
  3. package/bench/bench.svg +112 -0
  4. package/cli.js +32 -24
  5. package/index.js +177 -55
  6. package/interop.js +88 -159
  7. package/jz.svg +5 -0
  8. package/jzify/arguments.js +97 -0
  9. package/jzify/bundler.js +382 -0
  10. package/jzify/classes.js +328 -0
  11. package/jzify/hoist-vars.js +177 -0
  12. package/jzify/index.js +51 -0
  13. package/jzify/names.js +37 -0
  14. package/jzify/switch.js +106 -0
  15. package/jzify/transform.js +349 -0
  16. package/layout.js +179 -0
  17. package/module/array.js +322 -153
  18. package/module/collection.js +603 -145
  19. package/module/console.js +55 -43
  20. package/module/core.js +266 -153
  21. package/module/date.js +15 -3
  22. package/module/function.js +73 -6
  23. package/module/index.js +2 -1
  24. package/module/json.js +226 -61
  25. package/module/math.js +414 -186
  26. package/module/number.js +306 -60
  27. package/module/object.js +448 -184
  28. package/module/regex.js +255 -25
  29. package/module/schema.js +24 -6
  30. package/module/simd.js +85 -0
  31. package/module/string.js +586 -220
  32. package/module/symbol.js +1 -1
  33. package/module/timer.js +9 -14
  34. package/module/typedarray.js +45 -48
  35. package/package.json +41 -12
  36. package/src/abi/index.js +39 -23
  37. package/src/abi/string.js +38 -41
  38. package/src/ast.js +460 -0
  39. package/src/autoload.js +26 -24
  40. package/src/bridge.js +111 -0
  41. package/src/compile/analyze-scans.js +661 -0
  42. package/src/compile/analyze.js +1565 -0
  43. package/src/compile/emit-assign.js +408 -0
  44. package/src/compile/emit.js +3201 -0
  45. package/src/compile/flow-types.js +103 -0
  46. package/src/{compile.js → compile/index.js} +497 -125
  47. package/src/{infer.js → compile/infer.js} +27 -98
  48. package/src/{narrow.js → compile/narrow.js} +302 -96
  49. package/src/compile/plan/advise.js +316 -0
  50. package/src/compile/plan/common.js +150 -0
  51. package/src/compile/plan/index.js +118 -0
  52. package/src/compile/plan/inline.js +679 -0
  53. package/src/compile/plan/literals.js +984 -0
  54. package/src/compile/plan/loops.js +472 -0
  55. package/src/compile/plan/scope.js +573 -0
  56. package/src/compile/program-facts.js +404 -0
  57. package/src/ctx.js +176 -58
  58. package/src/ir.js +540 -171
  59. package/src/kind-traits.js +105 -0
  60. package/src/kind.js +462 -0
  61. package/src/op-policy.js +57 -0
  62. package/src/{optimize.js → optimize/index.js} +1106 -446
  63. package/src/optimize/vectorize.js +1874 -0
  64. package/src/param-reps.js +65 -0
  65. package/src/parse.js +44 -0
  66. package/src/{prepare.js → prepare/index.js} +589 -202
  67. package/src/reps.js +115 -0
  68. package/src/resolve.js +12 -3
  69. package/src/static.js +199 -0
  70. package/src/type.js +647 -0
  71. package/src/{assemble.js → wat/assemble.js} +86 -48
  72. package/src/wat/optimize.js +3760 -0
  73. package/transform.js +21 -0
  74. package/wasi.js +47 -5
  75. package/src/analyze.js +0 -3818
  76. package/src/emit.js +0 -3040
  77. package/src/jzify.js +0 -1580
  78. package/src/plan.js +0 -2132
  79. package/src/vectorize.js +0 -1088
  80. /package/src/{codegen.js → wat/codegen.js} +0 -0
package/module/regex.js CHANGED
@@ -7,10 +7,11 @@
7
7
  * @module regex
8
8
  */
9
9
 
10
- import { typed, asF64, asI64, UNDEF_NAN, mkPtrIR, temp, tempI32 } from '../src/ir.js'
11
- import { emit } from '../src/emit.js'
12
- import { err, inc, PTR, LAYOUT } from '../src/ctx.js'
13
- import { includeModule } from '../src/autoload.js'
10
+ import { typed, asF64, asI64, UNDEF_NAN, NULL_NAN, mkPtrIR, temp, tempI32 } from '../src/ir.js'
11
+ import { emit, deps } from '../src/bridge.js'
12
+ import { ctx, err, inc, PTR, LAYOUT, getter, declGlobal } from '../src/ctx.js'
13
+ import { valTypeOf } from '../src/kind.js'
14
+ import { VAL } from '../src/reps.js'
14
15
 
15
16
  // Build IR that constructs a match array: [full, cap1, cap2, ...]
16
17
  // strLocal, msLocal, meLocal are local names (i32 for ms/me, f64 for str).
@@ -27,7 +28,7 @@ const buildMatchArr = (strLocal, msLocal, meLocal, nGroups, groupNames = []) =>
27
28
  if (groupNames[i]) named.push([i, groupNames[i]])
28
29
  }
29
30
  if (named.length) {
30
- includeModule('collection')
31
+ ctx.module.include('collection')
31
32
  inc('__hash_new_small', '__hash_set', '__dyn_set')
32
33
  }
33
34
  const captureValue = i => ['if', ['result', 'f64'],
@@ -223,7 +224,22 @@ const parseGroup = () => {
223
224
  const inner = parseAlt()
224
225
  cur() === RPAREN || perr('Unclosed ('); skip()
225
226
  if (groupName) groupNames[groupId] = groupName
226
- return groupId ? [type, inner, groupId] : [type, inner]
227
+ // Carry the capture name IN the group node (4th element) as well as the module-level
228
+ // groupNames array. The AST structure survives the parse→compile handoff intact (it
229
+ // drives codegen), whereas the self-host kernel drops mutations to the module-level
230
+ // groupNames array — so `.groups` was never built. compileRegexToStdlib reads names
231
+ // back via collectGroupNames(ast), which works in both legs.
232
+ return groupId ? (groupName ? [type, inner, groupId, groupName] : [type, inner, groupId]) : [type, inner]
233
+ }
234
+
235
+ // Walk a parsed regex AST and return a `groupId → name` array for named captures,
236
+ // sourced from the group nodes' 4th element (set by parseGroup). Kernel-safe: relies
237
+ // only on the surviving AST structure, not module-level parse state.
238
+ const collectGroupNames = (node, out = []) => {
239
+ if (!Array.isArray(node)) return out
240
+ if (node[0] === '()' && typeof node[2] === 'number' && typeof node[3] === 'string') out[node[2]] = node[3]
241
+ for (let i = 1; i < node.length; i++) collectGroupNames(node[i], out)
242
+ return out
227
243
  }
228
244
 
229
245
  const isGroupNameStart = c =>
@@ -251,7 +267,8 @@ const parseGroupName = () => {
251
267
  const CHAR_CLASS_WAT = {
252
268
  d: '(i32.and (i32.ge_u (local.get $char) (i32.const 48)) (i32.le_u (local.get $char) (i32.const 57)))',
253
269
  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))))',
254
- 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))))'
270
+ // SP(32) TAB(9) LF(10) CR(13) VT(11) FF(12); NBSP/Unicode-Zs are multibyte under UTF-8 out of scope
271
+ s: '(i32.or (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)))) (i32.or (i32.eq (local.get $char) (i32.const 11)) (i32.eq (local.get $char) (i32.const 12))))'
255
272
  }
256
273
 
257
274
  // 8-bit char load at $str + $pos
@@ -714,7 +731,7 @@ const patternMinLen = node => {
714
731
  // === Module init ===
715
732
 
716
733
  export default (ctx) => {
717
- Object.assign(ctx.core.stdlibDeps, {
734
+ deps({
718
735
  __str_to_buf: ['__str_byteLen', '__char_at'],
719
736
  })
720
737
 
@@ -750,12 +767,12 @@ export default (ctx) => {
750
767
  // Reserve mutable globals for capture group start/end (shared across regexes by index)
751
768
  for (let i = 1; i <= (ast.groups || 0); i++) {
752
769
  if (!ctx.scope.globals.has(`__re_g${i}_start`)) {
753
- ctx.scope.globals.set(`__re_g${i}_start`, `(global $__re_g${i}_start (mut i32) (i32.const -1))`)
754
- ctx.scope.globals.set(`__re_g${i}_end`, `(global $__re_g${i}_end (mut i32) (i32.const -1))`)
770
+ declGlobal(`__re_g${i}_start`, 'i32', -1)
771
+ declGlobal(`__re_g${i}_end`, 'i32', -1)
755
772
  }
756
773
  }
757
774
  ctx.runtime.regex.groups.set(id, ast.groups || 0)
758
- ctx.runtime.regex.groupNames.set(id, ast.groupNames || [])
775
+ ctx.runtime.regex.groupNames.set(id, collectGroupNames(ast))
759
776
  ctx.core.stdlib[funcName] = compileRegex(ast, funcName)
760
777
 
761
778
  // Search wrapper: tries match at each position, returns (match_start, match_end) via locals
@@ -774,7 +791,30 @@ export default (ctx) => {
774
791
  (br $next)))
775
792
  (i32.const -1) (i32.const -1))`
776
793
 
777
- inc(funcName, searchName, '__str_to_buf')
794
+ // search_from: like search but starts scanning at $fromPos (used by global exec)
795
+ const searchFromName = `__regex_search_from_${id}`
796
+ ctx.core.stdlib[searchFromName] = `(func $${searchFromName} (param $str i64) (param $fromPos i32) (result i32 i32)
797
+ (local $off i32) (local $len i32) (local $pos i32) (local $result i32)
798
+ (local.set $off (call $__str_to_buf (local.get $str)))
799
+ (local.set $len (call $__str_byteLen (local.get $str)))
800
+ (local.set $pos (local.get $fromPos))
801
+ (block $done (loop $next
802
+ (br_if $done (i32.gt_s (local.get $pos) (local.get $len)))
803
+ (local.set $result (call $${funcName} (local.get $off) (local.get $len) (local.get $pos)))
804
+ (if (i32.ge_s (local.get $result) (i32.const 0))
805
+ (then (return (local.get $pos) (local.get $result))))
806
+ (local.set $pos (i32.add (local.get $pos) (i32.const 1)))
807
+ (br $next)))
808
+ (i32.const -1) (i32.const -1))`
809
+
810
+ // lastIndex mutable global for /g or /y regexes — tracks position across exec() calls
811
+ if ((flags || '').includes('g') || (flags || '').includes('y')) {
812
+ const liGlobal = `__re_lastIndex_${id}`
813
+ if (!ctx.scope.globals.has(liGlobal))
814
+ declGlobal(liGlobal, 'i32')
815
+ }
816
+
817
+ inc(funcName, searchName, searchFromName, '__str_to_buf')
778
818
  ctx.runtime.regex.compiled.set(key, id)
779
819
  return id
780
820
  }
@@ -811,19 +851,46 @@ export default (ctx) => {
811
851
  ['else', ['f64.const', 0]]]], 'f64')
812
852
  }
813
853
 
814
- // regex.exec(str) → [match_text, cap1, ...] array or 0 (null)
854
+ // regex.exec(str) → [match_text, cap1, ...] array or null.
855
+ // Mirrors JS: returns null (NULL_NAN) on no-match so `!== null` and while-loop idioms work.
856
+ // For /g (and /y) regexes: stateful — reads lastIndex, advances on match, resets on miss.
815
857
  ctx.core.emit['.regex:exec'] = (obj, str) => {
816
858
  const id = resolveRegex(obj)
817
859
  if (id == null) err('regex.exec requires a known regex')
818
860
  const nGroups = ctx.runtime.regex.groups.get(id) || 0
819
861
  const groupNames = ctx.runtime.regex.groupNames.get(id) || []
862
+ const flags = flagsOf(obj)
863
+ const isGlobal = flags.includes('g') || flags.includes('y')
820
864
  const s = temp('re'), ms = tempI32('rems'), me = tempI32('reme')
865
+ const nullIR = ['f64.const', `nan:${NULL_NAN}`]
866
+ if (isGlobal) {
867
+ // Stateful path: read lastIndex, search from there, update/reset lastIndex.
868
+ const liGlobal = `$__re_lastIndex_${id}`
869
+ inc(`__regex_search_from_${id}`)
870
+ return typed(['block', ['result', 'f64'],
871
+ ['local.set', `$${s}`, asF64(emit(str))],
872
+ ['local.set', `$${ms}`, ['local.set', `$${me}`,
873
+ ['call', `$__regex_search_from_${id}`,
874
+ ['i64.reinterpret_f64', ['local.get', `$${s}`]],
875
+ ['global.get', liGlobal]]]],
876
+ ['if', ['result', 'f64'], ['i32.lt_s', ['local.get', `$${ms}`], ['i32.const', 0]],
877
+ // no match — reset lastIndex, return null
878
+ ['then', ['global.set', liGlobal, ['i32.const', 0]], nullIR],
879
+ // match — advance lastIndex past match end (bump by 1 for zero-length)
880
+ ['else',
881
+ ['global.set', liGlobal,
882
+ ['select',
883
+ ['i32.add', ['local.get', `$${me}`], ['i32.const', 1]],
884
+ ['local.get', `$${me}`],
885
+ ['i32.eq', ['local.get', `$${ms}`], ['local.get', `$${me}`]]]],
886
+ buildMatchArr(s, ms, me, nGroups, groupNames)]]], 'f64')
887
+ }
821
888
  return typed(['block', ['result', 'f64'],
822
889
  ['local.set', `$${s}`, asF64(emit(str))],
823
890
  ['local.set', `$${ms}`, ['local.set', `$${me}`,
824
891
  ['call', `$__regex_search_${id}`, ['i64.reinterpret_f64', ['local.get', `$${s}`]]]]],
825
892
  ['if', ['result', 'f64'], ['i32.lt_s', ['local.get', `$${ms}`], ['i32.const', 0]],
826
- ['then', ['f64.const', 0]],
893
+ ['then', nullIR],
827
894
  ['else', buildMatchArr(s, ms, me, nGroups, groupNames)]]], 'f64')
828
895
  }
829
896
 
@@ -844,26 +911,34 @@ export default (ctx) => {
844
911
  // RegExp.prototype.source — the pattern text. A literal stores it verbatim
845
912
  // (already grammar-escaped), so `/A/.source` is the 6-char "A".
846
913
  // An empty pattern serializes to "(?:)" so the result re-parses to a regex.
847
- ctx.core.emit['.regex:source'] = (obj) => {
914
+ ctx.core.emit['.regex:source'] = getter((obj) => {
848
915
  const a = regexAstOf(obj)
849
916
  return emit(['str', (a && a[1]) || '(?:)'])
850
- }
917
+ })
851
918
 
852
919
  // RegExp.prototype.flags — flag characters in canonical order (sec-get-regexp.prototype.flags).
853
920
  const FLAG_ORDER = 'dgimsvy'
854
- ctx.core.emit['.regex:flags'] = (obj) => {
921
+ ctx.core.emit['.regex:flags'] = getter((obj) => {
855
922
  const f = flagsOf(obj)
856
923
  return emit(['str', [...FLAG_ORDER].filter(c => f.includes(c)).join('')])
857
- }
924
+ })
858
925
 
859
926
  // Individual flag accessors → 1/0 (jz carries booleans as f64).
860
927
  for (const [prop, ch] of [
861
928
  ['global', 'g'], ['ignoreCase', 'i'], ['multiline', 'm'], ['dotAll', 's'],
862
929
  ['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')
930
+ ]) ctx.core.emit[`.regex:${prop}`] = getter((obj) => typed(['f64.const', flagsOf(obj).includes(ch) ? 1 : 0], 'f64'))
864
931
 
865
- // lastIndex — jz regexes are stateless; a freshly-evaluated regex reads 0.
866
- ctx.core.emit['.regex:lastIndex'] = () => typed(['f64.const', 0], 'f64')
932
+ // lastIndex — for /g and /y regexes, reads the mutable global; others always 0.
933
+ ctx.core.emit['.regex:lastIndex'] = getter((obj) => {
934
+ const id = resolveRegex(obj)
935
+ if (id != null) {
936
+ const flags = flagsOf(obj)
937
+ if (flags.includes('g') || flags.includes('y'))
938
+ return typed(['f64.convert_i32_u', ['global.get', `$__re_lastIndex_${id}`]], 'f64')
939
+ }
940
+ return typed(['f64.const', 0], 'f64')
941
+ })
867
942
 
868
943
  // str.search(/re/) → first match position or -1
869
944
  ctx.core.emit['.string:search'] = (str, search) => {
@@ -913,15 +988,116 @@ export default (ctx) => {
913
988
  ['else', buildMatchArr(s, ms, me, nGroups, groupNames)]]], 'f64')
914
989
  }
915
990
 
916
- // str.replace(/re/, repl) → replaced string
991
+ // str.replace(/re/, repl) → replaced string. With the `g` flag every match is
992
+ // replaced (a per-regex loop, mirroring split); otherwise only the first.
917
993
  ctx.core.emit['.string:replace'] = (str, search, repl) => {
918
994
  const id = resolveRegex(search)
995
+ const isFn = valTypeOf(repl) === VAL.CLOSURE && ctx.closure?.call
996
+ // ToString(fn(matchStr)) → i64 string. The closure value is hoisted into
997
+ // `fnL` once by the caller; each match passes its substring as the lone arg.
998
+ const callbackRepl = (fnL, matchStrIR) =>
999
+ ['call', '$__to_str', asI64(ctx.closure.call(typed(['local.get', `$${fnL}`], 'f64'), [matchStrIR]))]
919
1000
  if (id == null) {
1001
+ if (isFn) {
1002
+ // String search + callback: replace the FIRST occurrence (spec: a string
1003
+ // search matches once). Mirror string.js `.replace`'s callback path.
1004
+ inc('__str_indexof', '__str_slice', '__str_concat', '__str_byteLen', '__to_str')
1005
+ const s = temp('rps'), q = temp('rpq'), fnL = temp('rpf'), idx = tempI32('rpi'), mlen = tempI32('rpm')
1006
+ const sI64 = () => ['i64.reinterpret_f64', ['local.get', `$${s}`]]
1007
+ const match = typed(['call', '$__str_slice', sI64(), ['local.get', `$${idx}`],
1008
+ ['i32.add', ['local.get', `$${idx}`], ['local.get', `$${mlen}`]]], 'f64')
1009
+ return typed(['block', ['result', 'f64'],
1010
+ ['local.set', `$${s}`, asF64(emit(str))],
1011
+ ['local.set', `$${q}`, asF64(emit(search))],
1012
+ ['local.set', `$${fnL}`, asF64(emit(repl))],
1013
+ ['local.set', `$${mlen}`, ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${q}`]]]],
1014
+ ['local.set', `$${idx}`, ['call', '$__str_indexof', sI64(), ['i64.reinterpret_f64', ['local.get', `$${q}`]], ['i32.const', 0]]],
1015
+ ['if', ['result', 'f64'], ['i32.lt_s', ['local.get', `$${idx}`], ['i32.const', 0]],
1016
+ ['then', ['local.get', `$${s}`]],
1017
+ ['else', typed(['call', '$__str_concat',
1018
+ asI64(typed(['call', '$__str_concat',
1019
+ asI64(typed(['call', '$__str_slice', sI64(), ['i32.const', 0], ['local.get', `$${idx}`]], 'f64')),
1020
+ callbackRepl(fnL, match)], 'f64')),
1021
+ asI64(typed(['call', '$__str_slice', sI64(),
1022
+ ['i32.add', ['local.get', `$${idx}`], ['local.get', `$${mlen}`]],
1023
+ ['call', '$__str_byteLen', sI64()]], 'f64'))], 'f64')]]], 'f64')
1024
+ }
920
1025
  // Fall back to string replace
921
1026
  inc('__str_replace')
922
1027
  return typed(['call', '$__str_replace', asI64(emit(str)), asI64(emit(search)), asI64(emit(repl))], 'f64')
923
1028
  }
924
1029
  inc('__str_slice', '__str_concat', '__str_byteLen')
1030
+ // Regex + callback: walk matches in IR (a WAT helper can't call a closure).
1031
+ // One unified loop covers /g (all matches) and non-/g (break after the first).
1032
+ if (isFn) {
1033
+ const global = flagsOf(search).includes('g')
1034
+ inc('__str_to_buf', '__to_str', `__regex_${id}`)
1035
+ const s = temp('rcs'), fnL = temp('rcf'), acc = temp('rca')
1036
+ const off = tempI32('rco'), len = tempI32('rcl'), pos = tempI32('rcp')
1037
+ const res = tempI32('rcr'), ms = tempI32('rcms'), me = tempI32('rcme'), pe = tempI32('rcpe')
1038
+ const sI64 = () => ['i64.reinterpret_f64', ['local.get', `$${s}`]]
1039
+ const accI64 = () => ['i64.reinterpret_f64', ['local.get', `$${acc}`]]
1040
+ const slice = (a, b) => typed(['call', '$__str_slice', sI64(), a, b], 'f64')
1041
+ const matchStr = slice(['local.get', `$${ms}`], ['local.get', `$${me}`])
1042
+ const step = [
1043
+ ['local.set', `$${res}`, ['call', `$__regex_${id}`, ['local.get', `$${off}`], ['local.get', `$${len}`], ['local.get', `$${pos}`]]],
1044
+ ['if', ['i32.lt_s', ['local.get', `$${res}`], ['i32.const', 0]],
1045
+ ['then', ['local.set', `$${pos}`, ['i32.add', ['local.get', `$${pos}`], ['i32.const', 1]]], ['br', '$next']]],
1046
+ ['local.set', `$${ms}`, ['local.get', `$${pos}`]],
1047
+ ['local.set', `$${me}`, ['local.get', `$${res}`]],
1048
+ ['local.set', `$${acc}`, ['call', '$__str_concat', accI64(), asI64(slice(['local.get', `$${pe}`], ['local.get', `$${ms}`]))]],
1049
+ ['local.set', `$${acc}`, ['call', '$__str_concat', accI64(), callbackRepl(fnL, matchStr)]],
1050
+ ['local.set', `$${pe}`, ['local.get', `$${me}`]],
1051
+ ...(global ? [] : [['br', '$done']]),
1052
+ ['local.set', `$${pos}`, ['select', ['i32.add', ['local.get', `$${me}`], ['i32.const', 1]], ['local.get', `$${me}`], ['i32.eq', ['local.get', `$${ms}`], ['local.get', `$${me}`]]]],
1053
+ ['br', '$next'],
1054
+ ]
1055
+ return typed(['block', ['result', 'f64'],
1056
+ ['local.set', `$${s}`, asF64(emit(str))],
1057
+ ['local.set', `$${fnL}`, asF64(emit(repl))],
1058
+ ['local.set', `$${off}`, ['call', '$__str_to_buf', sI64()]],
1059
+ ['local.set', `$${len}`, ['call', '$__str_byteLen', sI64()]],
1060
+ ['local.set', `$${pe}`, ['i32.const', 0]],
1061
+ ['local.set', `$${pos}`, ['i32.const', 0]],
1062
+ ['local.set', `$${acc}`, slice(['i32.const', 0], ['i32.const', 0])],
1063
+ ['block', '$done', ['loop', '$next',
1064
+ ['br_if', '$done', ['i32.gt_s', ['local.get', `$${pos}`], ['local.get', `$${len}`]]],
1065
+ ...step]],
1066
+ ['call', '$__str_concat', accI64(), asI64(slice(['local.get', `$${pe}`], ['local.get', `$${len}`]))]], 'f64')
1067
+ }
1068
+ // Global replace: walk every match, accumulating slice(prevEnd,matchStart)+repl.
1069
+ // Empty seed via slice(str,0,0); zero-length matches advance by 1 (per split).
1070
+ if (flagsOf(search).includes('g')) {
1071
+ const replName = `__regex_replace_${id}`
1072
+ if (!ctx.core.stdlib[replName]) {
1073
+ inc('__str_to_buf')
1074
+ ctx.core.stdlib[replName] = `(func $${replName} (param $str i64) (param $repl i64) (result f64)
1075
+ (local $off i32) (local $len i32) (local $pos i32) (local $result i32)
1076
+ (local $mstart i32) (local $mend i32) (local $prevEnd i32) (local $acc f64)
1077
+ (local.set $off (call $__str_to_buf (local.get $str)))
1078
+ (local.set $len (call $__str_byteLen (local.get $str)))
1079
+ (local.set $prevEnd (i32.const 0))
1080
+ (local.set $pos (i32.const 0))
1081
+ (local.set $acc (call $__str_slice (local.get $str) (i32.const 0) (i32.const 0)))
1082
+ (block $done (loop $next
1083
+ (br_if $done (i32.gt_s (local.get $pos) (local.get $len)))
1084
+ (local.set $result (call $__regex_${id} (local.get $off) (local.get $len) (local.get $pos)))
1085
+ (if (i32.lt_s (local.get $result) (i32.const 0))
1086
+ (then (local.set $pos (i32.add (local.get $pos) (i32.const 1))) (br $next)))
1087
+ (local.set $mstart (local.get $pos))
1088
+ (local.set $mend (local.get $result))
1089
+ (local.set $acc (call $__str_concat (i64.reinterpret_f64 (local.get $acc))
1090
+ (i64.reinterpret_f64 (call $__str_slice (local.get $str) (local.get $prevEnd) (local.get $mstart)))))
1091
+ (local.set $acc (call $__str_concat (i64.reinterpret_f64 (local.get $acc)) (local.get $repl)))
1092
+ (local.set $prevEnd (local.get $mend))
1093
+ (local.set $pos (select (i32.add (local.get $mend) (i32.const 1)) (local.get $mend) (i32.eq (local.get $mstart) (local.get $mend))))
1094
+ (br $next)))
1095
+ (call $__str_concat (i64.reinterpret_f64 (local.get $acc))
1096
+ (i64.reinterpret_f64 (call $__str_slice (local.get $str) (local.get $prevEnd) (local.get $len)))))`
1097
+ inc(replName)
1098
+ }
1099
+ return typed(['call', `$${replName}`, asI64(emit(str)), asI64(emit(repl))], 'f64')
1100
+ }
925
1101
  const s = temp('sr'), r = temp('srr'), ms = tempI32('srms'), me = tempI32('srme')
926
1102
  return typed(['block', ['result', 'f64'],
927
1103
  ['local.set', `$${s}`, asF64(emit(str))],
@@ -939,13 +1115,67 @@ export default (ctx) => {
939
1115
  ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${s}`]]]]]]]]], 'f64')
940
1116
  }
941
1117
 
1118
+ // str.matchAll(/re/g) → array of match arrays (each like exec's result: full
1119
+ // match at [0], capture groups after, named groups under `.groups`). JS yields
1120
+ // a lazy iterator, but an array satisfies both `[...m]` and `for (const x of m)`.
1121
+ // Two-pass over the same anchored matcher: count, then fill a sized array.
1122
+ ctx.core.emit['.string:matchAll'] = (str, search) => {
1123
+ const id = resolveRegex(search)
1124
+ if (id == null) err('matchAll requires a regex argument')
1125
+ const nGroups = ctx.runtime.regex.groups.get(id) || 0
1126
+ const groupNames = ctx.runtime.regex.groupNames.get(id) || []
1127
+ inc('__str_to_buf', '__str_byteLen', '__alloc', '__mkptr', `__regex_${id}`)
1128
+ const s = temp('mas'), outArr = tempI32('mao')
1129
+ const off = tempI32('maof'), len = tempI32('maln'), pos = tempI32('maps')
1130
+ const res = tempI32('mars'), cnt = tempI32('macn'), wi = tempI32('mawi')
1131
+ const ms = tempI32('mams'), me = tempI32('mame')
1132
+ const sI64 = () => ['i64.reinterpret_f64', ['local.get', `$${s}`]]
1133
+ // Anchored-matcher scan: __regex_${id}(off,len,pos) returns match-end (>=0) or
1134
+ // -1; `body` runs per match with ms=pos, me=res; pos advances (+1 on a
1135
+ // zero-length match to make progress). `posInit` resets the cursor per pass.
1136
+ const scan = (body) => ['block', '$d', ['loop', '$n',
1137
+ ['br_if', '$d', ['i32.gt_s', ['local.get', `$${pos}`], ['local.get', `$${len}`]]],
1138
+ ['local.set', `$${res}`, ['call', `$__regex_${id}`, ['local.get', `$${off}`], ['local.get', `$${len}`], ['local.get', `$${pos}`]]],
1139
+ ['if', ['i32.lt_s', ['local.get', `$${res}`], ['i32.const', 0]],
1140
+ ['then', ['local.set', `$${pos}`, ['i32.add', ['local.get', `$${pos}`], ['i32.const', 1]]], ['br', '$n']]],
1141
+ ['local.set', `$${ms}`, ['local.get', `$${pos}`]],
1142
+ ['local.set', `$${me}`, ['local.get', `$${res}`]],
1143
+ ...body,
1144
+ ['local.set', `$${pos}`, ['select', ['i32.add', ['local.get', `$${me}`], ['i32.const', 1]], ['local.get', `$${me}`], ['i32.eq', ['local.get', `$${ms}`], ['local.get', `$${me}`]]]],
1145
+ ['br', '$n']]]
1146
+ // Re-running the matcher in the fill pass repopulates the $__re_g* capture
1147
+ // globals just before buildMatchArr reads them — correct per-match captures.
1148
+ const matchArr = buildMatchArr(s, ms, me, nGroups, groupNames)
1149
+ return typed(['block', ['result', 'f64'],
1150
+ ['local.set', `$${s}`, asF64(emit(str))],
1151
+ ['local.set', `$${off}`, ['call', '$__str_to_buf', sI64()]],
1152
+ ['local.set', `$${len}`, ['call', '$__str_byteLen', sI64()]],
1153
+ ['local.set', `$${cnt}`, ['i32.const', 0]],
1154
+ ['local.set', `$${pos}`, ['i32.const', 0]],
1155
+ scan([['local.set', `$${cnt}`, ['i32.add', ['local.get', `$${cnt}`], ['i32.const', 1]]]]),
1156
+ ['local.set', `$${outArr}`, ['call', '$__alloc', ['i32.add', ['i32.const', 8], ['i32.shl', ['local.get', `$${cnt}`], ['i32.const', 3]]]]],
1157
+ ['i32.store', ['local.get', `$${outArr}`], ['local.get', `$${cnt}`]],
1158
+ ['i32.store', ['i32.add', ['local.get', `$${outArr}`], ['i32.const', 4]], ['local.get', `$${cnt}`]],
1159
+ ['local.set', `$${outArr}`, ['i32.add', ['local.get', `$${outArr}`], ['i32.const', 8]]],
1160
+ ['local.set', `$${wi}`, ['i32.const', 0]],
1161
+ ['local.set', `$${pos}`, ['i32.const', 0]],
1162
+ scan([
1163
+ ['f64.store', ['i32.add', ['local.get', `$${outArr}`], ['i32.shl', ['local.get', `$${wi}`], ['i32.const', 3]]], matchArr],
1164
+ ['local.set', `$${wi}`, ['i32.add', ['local.get', `$${wi}`], ['i32.const', 1]]],
1165
+ ]),
1166
+ mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${outArr}`])], 'f64')
1167
+ }
1168
+
942
1169
  // str.split(/re/) → array of substrings
943
- ctx.core.emit['.string:split'] = (str, sep) => {
1170
+ ctx.core.emit['.string:split'] = (str, sep, limit) => {
944
1171
  const id = resolveRegex(sep)
945
1172
  if (id == null) {
946
- // Fall back to string split
1173
+ // Fall back to string split, forwarding the optional limit (0x7fffffff = no limit).
1174
+ // __str_split is 3-param (str, sep, limit); a 2-arg call here trips a wasm arity
1175
+ // error in any program with a known-string `.split` (e.g. the watr self-host).
947
1176
  inc('__str_split')
948
- return typed(['call', '$__str_split', asI64(emit(str)), asI64(emit(sep))], 'f64')
1177
+ const limitIR = limit == null ? ['i32.const', 0x7fffffff] : ['i32.trunc_sat_f64_u', asF64(emit(limit))]
1178
+ return typed(['call', '$__str_split', asI64(emit(str)), asI64(emit(sep)), limitIR], 'f64')
949
1179
  }
950
1180
 
951
1181
  // Generate a split-by-regex WAT function for this regex
package/module/schema.js CHANGED
@@ -8,8 +8,9 @@
8
8
  */
9
9
 
10
10
  import { typed, asF64 } from '../src/ir.js'
11
- import { emit } from '../src/emit.js'
12
- import { VAL, lookupValType, repOf } from '../src/analyze.js'
11
+ import { emit } from '../src/bridge.js'
12
+ import { valTypeOf } from '../src/kind.js'
13
+ import { VAL, lookupValType, repOf } from '../src/reps.js'
13
14
  import { err, inc } from '../src/ctx.js'
14
15
 
15
16
  /** Initialize schema helpers on ctx. Called once per compilation from core module. */
@@ -40,8 +41,12 @@ export function initSchema(ctx) {
40
41
 
41
42
  /** schemaId for a variable name: ValueRep first, then module-level ctx.schema.vars.
42
43
  * Both paths exist because vars covers names without a per-function ValueRep
43
- * (prepare-phase rest/destructure tracking, module-level autoboxes). */
44
- ctx.schema.idOf = (name) => repOf(name)?.schemaId ?? ctx.schema.vars.get(name)
44
+ * (prepare-phase rest/destructure tracking, module-level autoboxes).
45
+ * Poisoned names (shape-disagreeing assignments, see prepare's
46
+ * bindAssignSchema) resolve to NO schema regardless of store: a fixed-slot
47
+ * read against one literal's layout would misread the other sources. */
48
+ ctx.schema.idOf = (name) => ctx.schema.poisoned?.has(name) ? undefined
49
+ : repOf(name)?.schemaId ?? ctx.schema.vars.get(name)
45
50
 
46
51
  /** Resolve variable name to its schema props array, or null. */
47
52
  ctx.schema.resolve = (varName) => {
@@ -67,8 +72,16 @@ export function initSchema(ctx) {
67
72
  * 2. Receiver is a string variable, but its valType is unknown or not OBJECT
68
73
  * 3. Receiver is not a string variable (varName == null) — no type evidence
69
74
  * 4. Structural search finds the property at inconsistent offsets across schemas
70
- * Case 4 is a real ambiguity — the caller must route to runtime dispatch. */
71
- ctx.schema.find = (varName, prop) => {
75
+ * Case 4 is a real ambiguity — the caller must route to runtime dispatch.
76
+ *
77
+ * Named `slotOf`, not `find`: under self-host the compiler calls this as
78
+ * `ctx.schema.find(...)` on the statically-untyped `ctx.schema` receiver, and
79
+ * `find` collides with `Array.prototype.find` — the method-call dispatcher
80
+ * hijacks it into a bogus array `find` (predicate scan), returning null. Every
81
+ * schema-slot property read then mis-resolved through jz.wasm (e.g. a boxed
82
+ * `Object.assign(arr, {p}); arr.p`). A non-builtin name dispatches correctly.
83
+ * Mirrors the abi string `concat`→`cat` rename for the same root cause. */
84
+ ctx.schema.slotOf = (varName, prop) => {
72
85
  // Precise: variable has known schema
73
86
  const id = ctx.schema.idOf(varName)
74
87
  if (id != null) return ctx.schema.list[id]?.indexOf(prop) ?? -1
@@ -77,6 +90,11 @@ export function initSchema(ctx) {
77
90
  // can match HASH/ARRAY/etc. values as if they had OBJECT layout, producing
78
91
  // slot reads on unrelated memory. Funnel those through dynamic dispatch.
79
92
  if (typeof varName !== 'string') return -1
93
+ // Poisoned names hold objects of KNOWN-disagreeing shapes; the structural
94
+ // closed-world bet (receiver is one of the schemas containing the prop) is
95
+ // exactly wrong for them — its other shapes may lack the prop while another
96
+ // value occupies the would-be slot. Always dynamic.
97
+ if (ctx.schema.poisoned?.has(varName)) return -1
80
98
  const vt = lookupValType(varName)
81
99
  if (vt !== VAL.OBJECT) return -1
82
100
  // Structural subtyping: walk only schemas that contain this prop.
package/module/simd.js ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * SIMD module — source-level f32x4 / i32x4 intrinsics that lower 1:1 to wasm SIMD
3
+ * (v128). Lets a jz kernel process 4 lanes per instruction: the per-pixel-parallel
4
+ * kernels (mandelbrot, raymarcher) run 4 pixels/rays in masked lockstep and beat
5
+ * scalar V8 several-fold (validated: SIMD-4 mandelbrot ≈ 4.6× a warm-V8 scalar loop).
6
+ *
7
+ * A v128 value is just a local whose wasm type is `v128` (exprType returns 'v128'
8
+ * for these calls; emit passes it through without an f64/i32 coercion). Building
9
+ * blocks for the masked-lockstep idiom:
10
+ *
11
+ * f32x4.splat(x) broadcast a scalar to 4 f32 lanes
12
+ * f32x4.lanes(a,b,c,d) build a vector from 4 scalars (compile-time consts ok)
13
+ * f32x4.add/sub/mul/div(a,b)
14
+ * f32x4.sqrt/abs/neg/floor(a)
15
+ * f32x4.min/max(a,b)
16
+ * f32x4.le/lt/ge/gt/eq(a,b) → i32x4 lane mask (all-ones / all-zero per lane)
17
+ * f32x4.lane(v, k) extract lane k (0..3) as a number
18
+ * i32x4.splat(n) / add/sub(a,b) / lane(v,k)
19
+ * v128.and/or/xor(a,b) / not(a) / bitselect(t,f,mask)
20
+ * v128.anyTrue(v) / v128.allTrue(v) → i32 (loop-exit tests)
21
+ *
22
+ * @module simd
23
+ */
24
+ import { typed, asF64, asI32 } from '../src/ir.js'
25
+ import { emit } from '../src/bridge.js'
26
+ import { err } from '../src/ctx.js'
27
+
28
+ export default (ctx) => {
29
+ const e = ctx.core.emit
30
+ const V = (node) => typed(node, 'v128') // a v128-typed (lane vector) result
31
+ const I = (node) => typed(node, 'i32') // an i32 result (lane extract / any_true)
32
+ const F = (node) => typed(node, 'f64') // an f64 result (f32 lane → number)
33
+ const op = (a) => emit(a) // operand IR (v128 local.get, or a nested v128 expr) — NOT coerced to f64
34
+ const f32 = (a) => ['f32.demote_f64', asF64(emit(a))] // scalar → f32 for splat/lanes
35
+ // lane index must be a 0..3 literal — wasm extract_lane takes an immediate, so a
36
+ // runtime index can't lower (and would silently read lane 0). Fail loudly instead.
37
+ const laneIdx = (k) => {
38
+ const v = typeof k === 'number' ? k : (Array.isArray(k) && k.length === 2 && k[0] == null && typeof k[1] === 'number') ? k[1] : null
39
+ if (v == null || (v | 0) !== v || v < 0 || v > 3)
40
+ err(`SIMD lane index must be a 0..3 literal (got ${JSON.stringify(k)}) — wasm extract_lane needs a constant lane.`)
41
+ return v
42
+ }
43
+
44
+ // ── build / broadcast ──────────────────────────────────────────────────────
45
+ e['f32x4.splat'] = (a) => V(['f32x4.splat', f32(a)])
46
+ e['f32x4.lanes'] = (a, b, c, d) => V(['f32x4.replace_lane', 3,
47
+ ['f32x4.replace_lane', 2,
48
+ ['f32x4.replace_lane', 1, ['f32x4.splat', f32(a)], f32(b)], f32(c)], f32(d)])
49
+ e['i32x4.splat'] = (a) => V(['i32x4.splat', asI32(emit(a))])
50
+
51
+ // ── f32x4 arithmetic ─────────────────────────────────────────────────────────
52
+ for (const o of ['add', 'sub', 'mul', 'div', 'min', 'max'])
53
+ e[`f32x4.${o}`] = (a, b) => V([`f32x4.${o}`, op(a), op(b)])
54
+ for (const o of ['sqrt', 'abs', 'neg', 'floor', 'ceil', 'trunc', 'nearest'])
55
+ e[`f32x4.${o}`] = (a) => V([`f32x4.${o}`, op(a)])
56
+
57
+ // ── f32x4 comparisons → i32x4 lane mask (v128 of all-ones/all-zero lanes) ─────
58
+ for (const o of ['eq', 'ne', 'lt', 'le', 'gt', 'ge'])
59
+ e[`f32x4.${o}`] = (a, b) => V([`f32x4.${o}`, op(a), op(b)])
60
+
61
+ // ── i32x4 arithmetic (iteration counters, masks as ±1) ───────────────────────
62
+ for (const o of ['add', 'sub', 'mul'])
63
+ e[`i32x4.${o}`] = (a, b) => V([`i32x4.${o}`, op(a), op(b)])
64
+ // i32x4 comparisons → lane mask (signed). eq/ne have no suffix; the rest are _s.
65
+ for (const o of ['eq', 'ne', 'lt', 'le', 'gt', 'ge'])
66
+ e[`i32x4.${o}`] = (a, b) => V([`i32x4.${o}${o === 'eq' || o === 'ne' ? '' : '_s'}`, op(a), op(b)])
67
+ // shifts by a scalar count (for float-bit twiddling: exponent extract, etc.)
68
+ e['i32x4.shl'] = (a, n) => V(['i32x4.shl', op(a), asI32(emit(n))])
69
+ e['i32x4.shr'] = (a, n) => V(['i32x4.shr_s', op(a), asI32(emit(n))])
70
+ e['i32x4.shrU'] = (a, n) => V(['i32x4.shr_u', op(a), asI32(emit(n))])
71
+ // lane-wise int → float conversion (i32x4.convert) — the value, not a bit reinterpret
72
+ e['f32x4.convertI32'] = (a) => V(['f32x4.convert_i32x4_s', op(a)])
73
+
74
+ // ── v128 bitwise + select + reductions ───────────────────────────────────────
75
+ for (const o of ['and', 'or', 'xor'])
76
+ e[`v128.${o}`] = (a, b) => V([`v128.${o}`, op(a), op(b)])
77
+ e['v128.not'] = (a) => V(['v128.not', op(a)])
78
+ e['v128.bitselect'] = (t, f, m) => V(['v128.bitselect', op(t), op(f), op(m)])
79
+ e['v128.anyTrue'] = (a) => I(['v128.any_true', op(a)])
80
+ e['v128.allTrue'] = (a) => I(['i32x4.all_true', op(a)])
81
+
82
+ // ── lane extract (read a single lane back to a scalar) ───────────────────────
83
+ e['f32x4.lane'] = (v, k) => F(['f64.promote_f32', ['f32x4.extract_lane', laneIdx(k), op(v)]])
84
+ e['i32x4.lane'] = (v, k) => I(['i32x4.extract_lane', laneIdx(k), op(v)])
85
+ }