jz 0.5.1 → 0.7.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 (86) hide show
  1. package/README.md +315 -318
  2. package/bench/README.md +369 -0
  3. package/bench/bench.svg +102 -0
  4. package/cli.js +104 -30
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6024 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +362 -84
  9. package/interop.js +159 -186
  10. package/jz.svg +5 -0
  11. package/jzify/arguments.js +97 -0
  12. package/jzify/bundler.js +382 -0
  13. package/jzify/classes.js +328 -0
  14. package/jzify/hoist-vars.js +177 -0
  15. package/jzify/index.js +51 -0
  16. package/jzify/names.js +37 -0
  17. package/jzify/switch.js +106 -0
  18. package/jzify/transform.js +349 -0
  19. package/layout.js +184 -0
  20. package/module/array.js +377 -176
  21. package/module/collection.js +639 -145
  22. package/module/console.js +55 -43
  23. package/module/core.js +264 -153
  24. package/module/date.js +15 -3
  25. package/module/function.js +73 -6
  26. package/module/index.js +2 -1
  27. package/module/json.js +226 -61
  28. package/module/math.js +551 -187
  29. package/module/number.js +327 -60
  30. package/module/object.js +474 -184
  31. package/module/regex.js +255 -25
  32. package/module/schema.js +24 -6
  33. package/module/simd.js +117 -0
  34. package/module/string.js +621 -226
  35. package/module/symbol.js +1 -1
  36. package/module/timer.js +9 -14
  37. package/module/typedarray.js +459 -73
  38. package/package.json +58 -14
  39. package/src/abi/index.js +39 -23
  40. package/src/abi/string.js +38 -41
  41. package/src/ast.js +460 -0
  42. package/src/autoload.js +29 -24
  43. package/src/bridge.js +111 -0
  44. package/src/compile/analyze-scans.js +824 -0
  45. package/src/compile/analyze.js +1600 -0
  46. package/src/compile/emit-assign.js +411 -0
  47. package/src/compile/emit.js +3512 -0
  48. package/src/compile/flow-types.js +103 -0
  49. package/src/{compile.js → compile/index.js} +778 -128
  50. package/src/{infer.js → compile/infer.js} +62 -98
  51. package/src/compile/loop-divmod.js +155 -0
  52. package/src/{narrow.js → compile/narrow.js} +409 -106
  53. package/src/compile/peel-stencil.js +261 -0
  54. package/src/compile/plan/advise.js +370 -0
  55. package/src/compile/plan/common.js +150 -0
  56. package/src/compile/plan/index.js +122 -0
  57. package/src/compile/plan/inline.js +682 -0
  58. package/src/compile/plan/literals.js +1199 -0
  59. package/src/compile/plan/loops.js +472 -0
  60. package/src/compile/plan/scope.js +649 -0
  61. package/src/compile/program-facts.js +423 -0
  62. package/src/ctx.js +220 -64
  63. package/src/ir.js +589 -172
  64. package/src/kind-traits.js +132 -0
  65. package/src/kind.js +524 -0
  66. package/src/op-policy.js +57 -0
  67. package/src/{optimize.js → optimize/index.js} +1432 -473
  68. package/src/optimize/vectorize.js +4660 -0
  69. package/src/param-reps.js +65 -0
  70. package/src/parse.js +44 -0
  71. package/src/{prepare.js → prepare/index.js} +649 -205
  72. package/src/prepare/lift-iife.js +149 -0
  73. package/src/reps.js +116 -0
  74. package/src/resolve.js +12 -3
  75. package/src/static.js +208 -0
  76. package/src/type.js +651 -0
  77. package/src/{assemble.js → wat/assemble.js} +275 -55
  78. package/src/wat/optimize.js +3938 -0
  79. package/transform.js +21 -0
  80. package/wasi.js +47 -5
  81. package/src/analyze.js +0 -3818
  82. package/src/emit.js +0 -3040
  83. package/src/jzify.js +0 -1580
  84. package/src/plan.js +0 -2132
  85. package/src/vectorize.js +0 -1088
  86. /package/src/{codegen.js → wat/codegen.js} +0 -0
package/module/string.js CHANGED
@@ -12,16 +12,15 @@
12
12
  * @module string
13
13
  */
14
14
 
15
- import { typed, asF64, asI32, asI64, NULL_NAN, UNDEF_NAN, mkPtrIR, temp, tempI32, toNumF64, toStrI64 } from '../src/ir.js'
16
- import { emit, emitBoolStr } from '../src/emit.js'
17
- import { valTypeOf, VAL } from '../src/analyze.js'
18
- import { inc, emitter, PTR, LAYOUT } from '../src/ctx.js'
15
+ import { typed, asF64, asI32, asI64, NULL_NAN, UNDEF_NAN, FALSE_NAN, TRUE_NAN, mkPtrIR, temp, tempI32, toNumF64, toStrI64 } from '../src/ir.js'
16
+ import { emit, bool, method, deps, wat, bind } from '../src/bridge.js'
17
+ import { valTypeOf } from '../src/kind.js'
18
+ import { VAL } from '../src/reps.js'
19
+ import { ctx, inc, PTR, LAYOUT, err, declGlobal } from '../src/ctx.js'
20
+ import { ssoBitI64Hex, sliceBitI64Hex, ptrNanHex, STR_INTERN_BIT } from '../layout.js'
19
21
 
20
- // SSO discriminator bit pre-shifted to its slot in the full i64 ptr (bit 46).
21
- // Used as `i64.and ptr SSO_BIT_I64` for branch-without-extracting-aux.
22
- const SSO_BIT_I64 = '0x' + (BigInt(LAYOUT.SSO_BIT) << BigInt(LAYOUT.AUX_SHIFT)).toString(16).toUpperCase().padStart(16, '0')
23
- // Slice/view discriminator bit, pre-shifted to bit 45 — same branch-without-aux trick.
24
- const SLICE_BIT_I64 = '0x' + (BigInt(LAYOUT.SLICE_BIT) << BigInt(LAYOUT.AUX_SHIFT)).toString(16).toUpperCase().padStart(16, '0')
22
+ const SSO_BIT_I64 = ssoBitI64Hex()
23
+ const SLICE_BIT_I64 = sliceBitI64Hex()
25
24
 
26
25
  // WAT (no-locals expression): byte length of a heap STRING given its raw $-local
27
26
  // names for the offset and the i64 ptr. A view (SLICE_BIT) carries its length in
@@ -38,15 +37,86 @@ const heapLenExpr = (ptrLocal, offLocal) => `(if (result i32)
38
37
  (else (i32.const 0)))))`
39
38
 
40
39
 
40
+
41
+ // SSO-pack a ≤4-byte slice straight from the source bytes (both SSO and
42
+ // memory-backed parents) — no allocation, and equal short tokens become
43
+ // bit-equal (SSO content IS the bits). Bails to the caller's heap path on a
44
+ // non-ASCII byte (SSO is ASCII-only). Locals: $sb scratch byte, $sp packed.
45
+ const sliceSsoPackWat = () => `
46
+ (if (i32.le_u (local.get $nlen) (i32.const 4))
47
+ (then (block $heap8
48
+ (local.set $srcOff (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
49
+ (local.set $isSso (i32.wrap_i64 (i64.shr_u
50
+ (i64.and (local.get $ptr) (i64.const ${SSO_BIT_I64}))
51
+ (i64.const ${LAYOUT.AUX_SHIFT}))))
52
+ (local.set $i (i32.const 0)) (local.set $sp (i32.const 0))
53
+ (loop $pk8
54
+ (if (i32.lt_u (local.get $i) (local.get $nlen))
55
+ (then
56
+ (local.set $sb (if (result i32) (local.get $isSso)
57
+ (then (i32.and
58
+ (i32.shr_u (local.get $srcOff) (i32.shl (i32.add (local.get $start) (local.get $i)) (i32.const 3)))
59
+ (i32.const 0xFF)))
60
+ (else (i32.load8_u (i32.add (i32.add (local.get $srcOff) (local.get $start)) (local.get $i))))))
61
+ (br_if $heap8 (i32.ge_u (local.get $sb) (i32.const 0x80)))
62
+ (local.set $sp (i32.or (local.get $sp) (i32.shl (local.get $sb) (i32.shl (local.get $i) (i32.const 3)))))
63
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
64
+ (br $pk8))))
65
+ (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.or (i32.const ${LAYOUT.SSO_BIT}) (local.get $nlen)) (local.get $sp))))))`
66
+
67
+ // Probe the static intern index (buildInternTable, compile/index.js): a 5..32
68
+ // byte slice whose content equals a static literal returns the CANONICAL
69
+ // static pointer, making every later comparison a bit-eq hit. Emitted only
70
+ // when the table exists ($__internBase declared — the stdlib body is a thunk
71
+ // evaluated at pullStdlib, after buildInternTable). FNV-1a must match
72
+ // __str_hash's heap branch. Locals: $ip src ptr, $h, $j, $slot, $cand, $k.
73
+ const internProbeWat = (ipExpr, guard = '(i32.const 1)') => ctx.scope.globals.has('__internBase') ? `
74
+ (if (i32.and (i32.le_u (local.get $nlen) (i32.const 32)) ${guard})
75
+ (then
76
+ (local.set $ip ${ipExpr})
77
+ (local.set $h (i32.const 0x811c9dc5))
78
+ (local.set $j (i32.const 0))
79
+ (block $hd (loop $hl
80
+ (br_if $hd (i32.ge_u (local.get $j) (local.get $nlen)))
81
+ (local.set $h (i32.mul
82
+ (i32.xor (local.get $h) (i32.load8_u (i32.add (local.get $ip) (local.get $j))))
83
+ (i32.const 0x01000193)))
84
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
85
+ (br $hl)))
86
+ (if (i32.le_s (local.get $h) (i32.const 1))
87
+ (then (local.set $h (i32.add (local.get $h) (i32.const 2)))))
88
+ (local.set $j (i32.and (local.get $h) (global.get $__internMask)))
89
+ (block $missI (loop $plI
90
+ (block $nextI
91
+ (local.set $slot (i32.add (global.get $__internBase) (i32.shl (local.get $j) (i32.const 3))))
92
+ (local.set $cand (i32.load (i32.add (local.get $slot) (i32.const 4))))
93
+ (br_if $missI (i32.eqz (local.get $cand)))
94
+ (br_if $nextI (i32.ne (i32.load (local.get $slot)) (local.get $h)))
95
+ (br_if $nextI (i32.ne (i32.load (i32.sub (local.get $cand) (i32.const 4))) (local.get $nlen)))
96
+ (local.set $k (i32.const 0))
97
+ (block $vd (loop $vl
98
+ (br_if $vd (i32.ge_u (local.get $k) (local.get $nlen)))
99
+ (br_if $nextI (i32.ne
100
+ (i32.load8_u (i32.add (local.get $ip) (local.get $k)))
101
+ (i32.load8_u (i32.add (local.get $cand) (local.get $k)))))
102
+ (local.set $k (i32.add (local.get $k) (i32.const 1)))
103
+ (br $vl)))
104
+ (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${STR_INTERN_BIT}) (local.get $cand))))
105
+ (local.set $j (i32.and (i32.add (local.get $j) (i32.const 1)) (global.get $__internMask)))
106
+ (br $plI)))))` : ''
107
+
108
+
41
109
  export default (ctx) => {
42
- Object.assign(ctx.core.stdlibDeps, {
43
- __str_concat: ['__to_str', '__str_byteLen', '__alloc', '__mkptr', '__str_copy'],
44
- __str_concat_raw: ['__str_byteLen', '__alloc', '__mkptr', '__str_copy'],
45
- __str_append_byte: ['__str_byteLen', '__alloc', '__mkptr', '__str_copy'],
110
+ deps({
111
+ __str_concat: ['__to_str', '__str_byteLen', '__alloc', '__memgrow', '__mkptr', '__str_copy'],
112
+ __str_concat_raw: ['__str_byteLen', '__alloc', '__memgrow', '__mkptr', '__str_copy'],
113
+ __str_append_byte: ['__str_byteLen', '__alloc', '__memgrow', '__mkptr', '__str_copy'],
46
114
  __str_copy: [],
47
115
  __str_slice: ['__str_byteLen', '__alloc'],
48
116
  __str_slice_view: ['__str_byteLen', '__mkptr', '__str_slice'],
49
- __str_indexof: ['__str_byteLen', '__to_str'],
117
+ __str_indexof: ['__str_byteLen'],
118
+ __str_lastindexof: ['__str_byteLen'],
119
+ __wrap1: ['__alloc', '__mkptr'],
50
120
  __str_substring: ['__str_slice'],
51
121
  __str_startswith: ['__str_byteLen'],
52
122
  __str_endswith: ['__str_byteLen'],
@@ -59,7 +129,8 @@ export default (ctx) => {
59
129
  __str_replaceall: ['__str_indexof', '__str_slice', '__str_concat'],
60
130
  __str_split: ['__str_slice', '__str_byteLen', '__char_at', '__alloc'],
61
131
  __str_idx: [],
62
- __str_eq: ['__char_at', '__str_byteLen'],
132
+ __str_eq: ['__str_eq_cold'],
133
+ __str_eq_cold: ['__char_at', '__str_byteLen'],
63
134
  __str_cmp: ['__char_at', '__str_byteLen'],
64
135
  __str_range_eq: ['__char_at', '__str_byteLen'],
65
136
  __str_substring_eq: ['__str_byteLen', '__str_range_eq'],
@@ -74,11 +145,23 @@ export default (ctx) => {
74
145
  __str_byteLen: ['__ptr_type', '__ptr_aux', '__str_len'],
75
146
  })
76
147
 
148
+ // String *runtime* construction (concat/slice/split/`String(n)`/shared-memory
149
+ // pools) allocates and boxes pointers, so the module eagerly declares its core
150
+ // heap deps — same as object.js / typedarray.js / array.js. Several such helpers
151
+ // (`__str_slice`, `__str_split`, `__str_case`, …) call `$__mkptr`/`$__alloc` only
152
+ // through nested template thunks (sliceSsoPackWat/internProbeWat); resolveIncludes'
153
+ // auto-derivation realizes those thunks to recover the dep, but that re-realization
154
+ // is not reliable under self-host (the jz.wasm kernel re-runs the thunk late, with
155
+ // intern/heap state the native run never reaches), so a runtime string op would
156
+ // emit `call $__mkptr` with no definition. Declaring the deps explicitly here makes
157
+ // inclusion independent of thunk re-realization. Reachability pruning still drops
158
+ // both for a literals-only module (no `__mkptr`/`__alloc` call site → unreachable),
159
+ // so a heap-free program stays heap-free — no allocator, no memory, no exports.
77
160
  inc('__mkptr', '__alloc')
78
161
 
79
- // === String literal: "abc" → SSO if ≤4 ASCII, else heap ===
162
+ // === String literal: "abc" → SSO if ≤4 ASCII, else static data ===
80
163
 
81
- ctx.core.emit['str'] = (str) => {
164
+ bind('str', (str) => {
82
165
  const MAX_SSO = 4
83
166
  if (ctx.features.sso && str.length <= MAX_SSO && /^[\x00-\x7f]*$/.test(str)) {
84
167
  let packed = 0
@@ -88,16 +171,33 @@ export default (ctx) => {
88
171
  const bytes = new TextEncoder().encode(str)
89
172
  const len = bytes.length
90
173
  if (!ctx.memory.shared) {
91
- // Own memory: place in static data segment (no runtime allocation)
174
+ // Own memory: place in static data segment (no runtime allocation).
175
+ // Under internStrings the layout is [hash u32][len u32][bytes] and the
176
+ // pointer carries STR_INTERN_BIT: statics are CANONICAL (deduped), so
177
+ // unequal canonicals are bit-unequal (__str_eq short-circuit) and
178
+ // __str_hash loads the cached FNV at -8 instead of re-hashing. The len
179
+ // header stays at -4 either way — no other reader changes.
180
+ const interned = !!ctx.transform.optimize?.internStrings
181
+ const hdr = interned ? 8 : 4
182
+ const aux = interned ? STR_INTERN_BIT : 0
92
183
  if (!ctx.runtime.data) ctx.runtime.data = ''
93
184
  const prior = ctx.runtime.dataDedup.get(str)
94
- if (prior !== undefined) return mkPtrIR(PTR.STRING, 0, prior + 4)
185
+ if (prior !== undefined) return mkPtrIR(PTR.STRING, aux, prior + hdr)
95
186
  while (ctx.runtime.data.length % 4 !== 0) ctx.runtime.data += '\0'
96
187
  const offset = ctx.runtime.data.length
188
+ if (interned) {
189
+ // byte-FNV + clamp — must equal __str_hash's output exactly (it hashes
190
+ // UTF-8 bytes, then clamps ≤1 → +2 for the empty/tombstone sentinels)
191
+ let h = 0x811c9dc5 | 0
192
+ for (let i = 0; i < len; i++) h = Math.imul(h ^ bytes[i], 0x01000193) | 0
193
+ if (h <= 1) h = (h + 2) | 0
194
+ h = h >>> 0
195
+ ctx.runtime.data += String.fromCharCode(h & 0xFF, (h >> 8) & 0xFF, (h >> 16) & 0xFF, (h >> 24) & 0xFF)
196
+ }
97
197
  ctx.runtime.data += String.fromCharCode(len & 0xFF, (len >> 8) & 0xFF, (len >> 16) & 0xFF, (len >> 24) & 0xFF)
98
198
  for (let i = 0; i < len; i++) ctx.runtime.data += String.fromCharCode(bytes[i])
99
199
  ctx.runtime.dataDedup.set(str, offset)
100
- return mkPtrIR(PTR.STRING, 0, offset + 4)
200
+ return mkPtrIR(PTR.STRING, aux, offset + hdr)
101
201
  }
102
202
  // Shared memory: pack all string literals into one passive data segment with 4-byte
103
203
  // length prefixes. At __start, alloc the whole pool once and memory.init it in a single
@@ -105,7 +205,7 @@ export default (ctx) => {
105
205
  // use, independent of string length AND reused across uses.
106
206
  if (!ctx.runtime.strPool) {
107
207
  ctx.runtime.strPool = ''
108
- ctx.scope.globals.set('__strBase', '(global $__strBase (mut i32) (i32.const 0))')
208
+ declGlobal('__strBase', 'i32')
109
209
  }
110
210
  let off = ctx.runtime.strPoolDedup.get(str)
111
211
  if (off === undefined) {
@@ -116,23 +216,23 @@ export default (ctx) => {
116
216
  ctx.runtime.strPoolDedup.set(str, off)
117
217
  }
118
218
  return mkPtrIR(PTR.STRING, 0, ['i32.add', ['global.get', '$__strBase'], ['i32.const', off]])
119
- }
219
+ })
120
220
 
121
221
  // === WAT: char extraction ===
122
222
 
123
223
  // SSO/STRING ptrs never have forwarding pointers (only ARRAY does), so we extract
124
224
  // the raw offset directly instead of paying the __ptr_offset function-call overhead.
125
- ctx.core.stdlib['__sso_char'] = `(func $__sso_char (param $ptr i64) (param $i i32) (result i32)
225
+ wat('__sso_char', `(func $__sso_char (param $ptr i64) (param $i i32) (result i32)
126
226
  (i32.and
127
227
  (i32.shr_u
128
228
  (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK})))
129
229
  (i32.mul (local.get $i) (i32.const 8)))
130
- (i32.const 0xFF)))`
230
+ (i32.const 0xFF)))`)
131
231
 
132
- ctx.core.stdlib['__str_char'] = `(func $__str_char (param $ptr i64) (param $i i32) (result i32)
232
+ wat('__str_char', `(func $__str_char (param $ptr i64) (param $i i32) (result i32)
133
233
  (i32.load8_u (i32.add
134
234
  (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK})))
135
- (local.get $i))))`
235
+ (local.get $i))))`)
136
236
 
137
237
  // Hot (~37M calls in watr self-host, ~40k/scan in tokenizer bench). Caller
138
238
  // guarantees $ptr is a STRING; SSO bit picks inline-byte-extract vs heap memory
@@ -149,7 +249,7 @@ export default (ctx) => {
149
249
  // load+bounds-check, beating call-site overhead. Repeated `i32.wrap_i64 +
150
250
  // i64.and OFFSET_MASK` subexpressions rely on CSE in the consumer; both
151
251
  // V8/TurboFan and watr's own propagate pass handle this.
152
- ctx.core.stdlib['__char_at'] = `(func $__char_at (param $ptr i64) (param $i i32) (result i32)
252
+ wat('__char_at', `(func $__char_at (param $ptr i64) (param $i i32) (result i32)
153
253
  (if (result i32)
154
254
  (i64.ne (i64.and (local.get $ptr) (i64.const ${SSO_BIT_I64})) (i64.const 0))
155
255
  (then
@@ -190,9 +290,9 @@ export default (ctx) => {
190
290
  (i32.load8_u
191
291
  (i32.add
192
292
  (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK})))
193
- (local.get $i)))))))))`
293
+ (local.get $i)))))))))`)
194
294
 
195
- ctx.core.stdlib['__str_idx'] = `(func $__str_idx (param $ptr i64) (param $i i32) (result f64)
295
+ wat('__str_idx', `(func $__str_idx (param $ptr i64) (param $i i32) (result f64)
196
296
  (local $t i32) (local $off i32) (local $len i32) (local $isSso i32)
197
297
  (local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
198
298
  (local.set $off (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
@@ -221,21 +321,58 @@ export default (ctx) => {
221
321
  (f64.reinterpret_i64
222
322
  (i64.or
223
323
  ;; mkptr(STRING, SSO_BIT|1, 0) = NAN_PREFIX | (STRING<<TAG_SHIFT) | ((SSO_BIT|1)<<AUX_SHIFT)
224
- (i64.const ${'0x' + (LAYOUT.NAN_PREFIX_BITS | (BigInt(PTR.STRING) << BigInt(LAYOUT.TAG_SHIFT)) | ((BigInt(LAYOUT.SSO_BIT) | 1n) << BigInt(LAYOUT.AUX_SHIFT))).toString(16).toUpperCase().padStart(16, '0')})
324
+ (i64.const ${ptrNanHex(PTR.STRING, LAYOUT.SSO_BIT | 1)})
225
325
  (i64.extend_i32_u
226
326
  (if (result i32) (local.get $isSso)
227
327
  (then (i32.and (i32.shr_u (local.get $off) (i32.mul (local.get $i) (i32.const 8))) (i32.const 0xFF)))
228
- (else (i32.load8_u (i32.add (local.get $off) (local.get $i)))))))))))`
328
+ (else (i32.load8_u (i32.add (local.get $off) (local.get $i)))))))))))`)
229
329
 
230
330
  // Hot: ~53M calls in watr self-host. Bit-eq covers identity. SSO/SSO with !bit-eq
231
331
  // guarantees content differs (high 32 bits encode type+len; both equal → low 32 differs
232
332
  // ⇒ bytes differ). Heap/heap uses raw load8_u — no per-byte function calls.
233
333
  // Mixed SSO×heap is rare; falls back to __char_at.
234
- ctx.core.stdlib['__str_eq'] = `(func $__str_eq (param $a i64) (param $b i64) (result i32)
334
+ // Hot/cold split: the prefix every comparison runs (bit-eq, both-SSO,
335
+ // both-canonical, heap length mismatch) is LOOP-FREE and small enough for
336
+ // the engine's wasm inliner — the call overhead disappears at every site
337
+ // while the byte-walk lives in __str_eq_cold. Mirrors the __ptr_offset_fwd
338
+ // split; a body containing a loop is excluded from V8's inliner.
339
+ wat('__str_eq', `(func $__str_eq (param $a i64) (param $b i64) (result i32)
340
+ (local $axA i32) (local $axB i32) (local $offA i32) (local $offB i32)
341
+ (if (i64.eq (local.get $a) (local.get $b))
342
+ (then (return (i32.const 1))))
343
+ (local.set $axA (i32.wrap_i64 (i64.shr_u (local.get $a) (i64.const ${LAYOUT.AUX_SHIFT}))))
344
+ (local.set $axB (i32.wrap_i64 (i64.shr_u (local.get $b) (i64.const ${LAYOUT.AUX_SHIFT}))))
345
+ ;; both SSO ⇒ bit-ne already decided content-ne
346
+ (if (i32.and
347
+ (i32.and (local.get $axA) (i32.const ${LAYOUT.SSO_BIT}))
348
+ (i32.and (local.get $axB) (i32.const ${LAYOUT.SSO_BIT})))
349
+ (then (return (i32.const 0))))
350
+ ;; both CANONICAL interned heap strings ⇒ bit-ne ⇒ content-ne
351
+ (if (i32.and
352
+ (i32.eq (i32.and (local.get $axA) (i32.const ${LAYOUT.SSO_BIT | LAYOUT.SLICE_BIT | 0x1})) (i32.const 0x1))
353
+ (i32.eq (i32.and (local.get $axB) (i32.const ${LAYOUT.SSO_BIT | LAYOUT.SLICE_BIT | 0x1})) (i32.const 0x1)))
354
+ (then (return (i32.const 0))))
355
+ ;; both PLAIN heap (not SSO, not slice): length mismatch exits without bytes
356
+ (if (i32.eqz (i32.or
357
+ (i32.and (i32.or (local.get $axA) (local.get $axB)) (i32.const ${LAYOUT.SSO_BIT | LAYOUT.SLICE_BIT}))
358
+ (i32.const 0)))
359
+ (then
360
+ (local.set $offA (i32.wrap_i64 (i64.and (local.get $a) (i64.const ${LAYOUT.OFFSET_MASK}))))
361
+ (local.set $offB (i32.wrap_i64 (i64.and (local.get $b) (i64.const ${LAYOUT.OFFSET_MASK}))))
362
+ (if (i32.and (i32.ge_u (local.get $offA) (i32.const 4)) (i32.ge_u (local.get $offB) (i32.const 4)))
363
+ (then
364
+ (if (i32.ne
365
+ (i32.load (i32.sub (local.get $offA) (i32.const 4)))
366
+ (i32.load (i32.sub (local.get $offB) (i32.const 4))))
367
+ (then (return (i32.const 0))))))))
368
+ (call $__str_eq_cold (local.get $a) (local.get $b)))`)
369
+
370
+ wat('__str_eq_cold', `(func $__str_eq_cold (param $a i64) (param $b i64) (result i32)
235
371
  (local $len i32) (local $lenB i32) (local $i i32)
236
372
  (local $ta i32) (local $tb i32)
237
373
  (local $offA i32) (local $offB i32)
238
374
  (local $ssoA i32) (local $ssoB i32)
375
+ (local $axA i32) (local $axB i32)
239
376
  (if (i64.eq (local.get $a) (local.get $b))
240
377
  (then (return (i32.const 1))))
241
378
  (local.set $ta (i32.wrap_i64 (i64.and (i64.shr_u (local.get $a) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
@@ -251,6 +388,19 @@ export default (ctx) => {
251
388
  ;; Both SSO with !bit-eq ⇒ content differs (high 32 bits hold tag+aux; both equal here).
252
389
  (if (i32.and (local.get $ssoA) (local.get $ssoB))
253
390
  (then (return (i32.const 0))))
391
+ ;; Both CANONICAL interned heap strings (STR_INTERN_BIT, SSO/SLICE clear):
392
+ ;; canonicals are deduped, so bit-ne ⇒ content-ne — the V8 pointer-compare
393
+ ;; equivalent. This is the dominant unequal case in tag-dispatch chains
394
+ ;; (op === 'literal' ladders over static literals).
395
+ (local.set $axA (i32.wrap_i64 (i64.shr_u (local.get $a) (i64.const ${LAYOUT.AUX_SHIFT}))))
396
+ (local.set $axB (i32.wrap_i64 (i64.shr_u (local.get $b) (i64.const ${LAYOUT.AUX_SHIFT}))))
397
+ (if (i32.and
398
+ (i32.and (i32.eq (local.get $ta) (i32.const ${PTR.STRING}))
399
+ (i32.eq (local.get $tb) (i32.const ${PTR.STRING})))
400
+ (i32.and
401
+ (i32.eq (i32.and (local.get $axA) (i32.const ${LAYOUT.SSO_BIT | LAYOUT.SLICE_BIT | STR_INTERN_BIT})) (i32.const ${STR_INTERN_BIT}))
402
+ (i32.eq (i32.and (local.get $axB) (i32.const ${LAYOUT.SSO_BIT | LAYOUT.SLICE_BIT | STR_INTERN_BIT})) (i32.const ${STR_INTERN_BIT}))))
403
+ (then (return (i32.const 0))))
254
404
  ;; Both heap STRING fast path: inline len from header. Chunk by 4 bytes via unaligned
255
405
  ;; i32.load (wasm guarantees unaligned-OK), then byte-tail. Most string comparisons fail
256
406
  ;; early on the first 4-byte word, so this collapses the per-byte branch overhead into a
@@ -296,7 +446,7 @@ export default (ctx) => {
296
446
  (then (return (i32.const 0))))
297
447
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
298
448
  (br $lm)))
299
- (i32.const 1))`
449
+ (i32.const 1))`)
300
450
 
301
451
  // Three-way byte-wise compare: -1 if a < b, 0 if equal, +1 if a > b. Returns
302
452
  // i32 so callers can `i32.lt_s 0`, `i32.gt_s 0`, etc. without coercion.
@@ -305,7 +455,7 @@ export default (ctx) => {
305
455
  // codepoint order for code points < 0x80 and well-formed strings. NOT locale-
306
456
  // aware: this is the byte-wise variant suitable for sort-stability use cases,
307
457
  // not human-language collation.
308
- ctx.core.stdlib['__str_cmp'] = `(func $__str_cmp (param $a i64) (param $b i64) (result i32)
458
+ wat('__str_cmp', `(func $__str_cmp (param $a i64) (param $b i64) (result i32)
309
459
  (local $lenA i32) (local $lenB i32) (local $minLen i32) (local $i i32)
310
460
  (local $ca i32) (local $cb i32)
311
461
  ;; Bit-equal pointers (including same SSO inline form) ⇒ identical strings.
@@ -326,11 +476,11 @@ export default (ctx) => {
326
476
  ;; Common prefix matches — shorter string sorts first.
327
477
  (if (i32.lt_s (local.get $lenA) (local.get $lenB)) (then (return (i32.const -1))))
328
478
  (if (i32.gt_s (local.get $lenA) (local.get $lenB)) (then (return (i32.const 1))))
329
- (i32.const 0))`
479
+ (i32.const 0))`)
330
480
 
331
481
  // === WAT: unified byte length (SSO → aux low bits, heap → header) ===
332
482
 
333
- ctx.core.stdlib['__str_byteLen'] = `(func $__str_byteLen (param $ptr i64) (result i32)
483
+ wat('__str_byteLen', `(func $__str_byteLen (param $ptr i64) (result i32)
334
484
  (local $t i32) (local $off i32) (local $aux i32)
335
485
  (local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
336
486
  (if (result i32) (i32.eq (local.get $t) (i32.const ${PTR.STRING}))
@@ -349,15 +499,16 @@ export default (ctx) => {
349
499
  (if (result i32) (i32.ge_u (local.get $off) (i32.const 4))
350
500
  (then (i32.load (i32.sub (local.get $off) (i32.const 4))))
351
501
  (else (i32.const 0))))))))
352
- (else (i32.const 0))))`
502
+ (else (i32.const 0))))`)
353
503
 
354
504
  // === WAT: string methods ===
355
505
 
356
506
  // SSO source uses an unrolled byte-extract loop (len ≤ 4); heap source uses memory.copy
357
507
  // (single bulk op instead of nlen × __char_at).
358
- ctx.core.stdlib['__str_slice'] = `(func $__str_slice (param $ptr i64) (param $start i32) (param $end i32) (result f64)
508
+ wat('__str_slice', () => `(func $__str_slice (param $ptr i64) (param $start i32) (param $end i32) (result f64)
359
509
  (local $len i32) (local $nlen i32) (local $off i32) (local $i i32)
360
- (local $srcOff i32) (local $isSso i32)
510
+ (local $srcOff i32) (local $isSso i32) (local $sb i32) (local $sp i32)
511
+ (local $ip i32) (local $h i32) (local $j i32) (local $slot i32) (local $cand i32) (local $k i32)
361
512
  (local.set $len (call $__str_byteLen (local.get $ptr)))
362
513
  (if (i32.lt_s (local.get $start) (i32.const 0))
363
514
  (then (local.set $start (i32.add (local.get $len) (local.get $start)))))
@@ -374,6 +525,8 @@ export default (ctx) => {
374
525
  (if (i32.ge_s (local.get $start) (local.get $end))
375
526
  (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
376
527
  (local.set $nlen (i32.sub (local.get $end) (local.get $start)))
528
+ ${sliceSsoPackWat()}
529
+ ${internProbeWat('(i32.add (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ' + LAYOUT.OFFSET_MASK + '))) (local.get $start))', '(i32.eqz (local.get $isSso))')}
377
530
  (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $nlen))))
378
531
  (i32.store (local.get $off) (local.get $nlen))
379
532
  (local.set $off (i32.add (local.get $off) (i32.const 4)))
@@ -393,7 +546,7 @@ export default (ctx) => {
393
546
  (br $loop))))
394
547
  (else
395
548
  (memory.copy (local.get $off) (i32.add (local.get $srcOff) (local.get $start)) (local.get $nlen))))
396
- (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
549
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
397
550
 
398
551
  // No-copy slice: returns a VIEW into the receiver's buffer instead of copying
399
552
  // bytes. Only emitted when escape analysis proves the result never outlives the
@@ -403,8 +556,9 @@ export default (ctx) => {
403
556
  // (__str_slice) when the parent is SSO (no buffer to point into) or the result
404
557
  // is longer than SLICE_LEN_MASK (aux can't hold the length). Clamping mirrors
405
558
  // __str_slice; the fallback re-clamps idempotently.
406
- ctx.core.stdlib['__str_slice_view'] = `(func $__str_slice_view (param $ptr i64) (param $start i32) (param $end i32) (result f64)
559
+ wat('__str_slice_view', () => `(func $__str_slice_view (param $ptr i64) (param $start i32) (param $end i32) (result f64)
407
560
  (local $len i32) (local $nlen i32) (local $srcOff i32) (local $tag i32)
561
+ (local $ip i32) (local $h i32) (local $j i32) (local $slot i32) (local $cand i32) (local $k i32)
408
562
  (local.set $len (call $__str_byteLen (local.get $ptr)))
409
563
  (if (i32.lt_s (local.get $start) (i32.const 0))
410
564
  (then (local.set $start (i32.add (local.get $len) (local.get $start)))))
@@ -422,22 +576,26 @@ export default (ctx) => {
422
576
  (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
423
577
  (local.set $nlen (i32.sub (local.get $end) (local.get $start)))
424
578
  (local.set $tag (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
425
- ;; View-eligible: STRING parent, not SSO, length fits aux[12:0].
579
+ ;; View-eligible: STRING parent, not SSO, length fits aux[12:0]. ≤4-byte
580
+ ;; results route to __str_slice's SSO pack instead (bit-equal short tokens).
426
581
  (if (i32.and
427
582
  (i32.and
428
- (i32.eq (local.get $tag) (i32.const ${PTR.STRING}))
583
+ (i32.and
584
+ (i32.eq (local.get $tag) (i32.const ${PTR.STRING}))
585
+ (i32.gt_u (local.get $nlen) (i32.const 4)))
429
586
  (i64.eqz (i64.and (local.get $ptr) (i64.const ${SSO_BIT_I64}))))
430
587
  (i32.le_u (local.get $nlen) (i32.const ${LAYOUT.SLICE_LEN_MASK})))
431
588
  (then
432
589
  (local.set $srcOff (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
590
+ ${internProbeWat('(i32.add (local.get $srcOff) (local.get $start))')}
433
591
  (return (call $__mkptr
434
592
  (i32.const ${PTR.STRING})
435
593
  (i32.or (i32.const ${LAYOUT.SLICE_BIT}) (local.get $nlen))
436
594
  (i32.add (local.get $srcOff) (local.get $start))))))
437
595
  ;; Fallback: copy (SSO parent, or slice too long for the aux length field).
438
- (call $__str_slice (local.get $ptr) (local.get $start) (local.get $end)))`
596
+ (call $__str_slice (local.get $ptr) (local.get $start) (local.get $end)))`)
439
597
 
440
- ctx.core.stdlib['__str_substring'] = `(func $__str_substring (param $ptr i64) (param $start i32) (param $end i32) (result f64)
598
+ wat('__str_substring', `(func $__str_substring (param $ptr i64) (param $start i32) (param $end i32) (result f64)
441
599
  (local $len i32) (local $tmp i32)
442
600
  (local.set $len (call $__str_byteLen (local.get $ptr)))
443
601
  (if (i32.lt_s (local.get $start) (i32.const 0))
@@ -453,7 +611,7 @@ export default (ctx) => {
453
611
  (local.set $tmp (local.get $start))
454
612
  (local.set $start (local.get $end))
455
613
  (local.set $end (local.get $tmp))))
456
- (call $__str_slice (local.get $ptr) (local.get $start) (local.get $end)))`
614
+ (call $__str_slice (local.get $ptr) (local.get $start) (local.get $end)))`)
457
615
 
458
616
  // === WAT: fused substring-equality ===
459
617
  //
@@ -468,7 +626,7 @@ export default (ctx) => {
468
626
  // returns one) and type-checks only `other`, mirroring __eq's STRING-vs-?
469
627
  // arm: a genuine number never equals a string, and a NaN-boxed non-STRING
470
628
  // never does either (jz `==` is strict).
471
- ctx.core.stdlib['__str_range_eq'] = `(func $__str_range_eq (param $ptr i64) (param $start i32) (param $end i32) (param $other i64) (result i32)
629
+ wat('__str_range_eq', `(func $__str_range_eq (param $ptr i64) (param $start i32) (param $end i32) (param $other i64) (result i32)
472
630
  (local $n i32) (local $i i32) (local $fb f64)
473
631
  ;; A genuine number reinterprets to a non-NaN f64 (equals itself) — never a string.
474
632
  (local.set $fb (f64.reinterpret_i64 (local.get $other)))
@@ -492,10 +650,10 @@ export default (ctx) => {
492
650
  (then (return (i32.const 0))))
493
651
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
494
652
  (br $next)))
495
- (i32.const 1))`
653
+ (i32.const 1))`)
496
654
 
497
655
  // Clamp mirrors __str_substring (negatives floor to 0, swap when start>end).
498
- ctx.core.stdlib['__str_substring_eq'] = `(func $__str_substring_eq (param $ptr i64) (param $start i32) (param $end i32) (param $other i64) (result i32)
656
+ wat('__str_substring_eq', `(func $__str_substring_eq (param $ptr i64) (param $start i32) (param $end i32) (param $other i64) (result i32)
499
657
  (local $len i32) (local $tmp i32)
500
658
  (local.set $len (call $__str_byteLen (local.get $ptr)))
501
659
  (if (i32.lt_s (local.get $start) (i32.const 0))
@@ -511,11 +669,11 @@ export default (ctx) => {
511
669
  (local.set $tmp (local.get $start))
512
670
  (local.set $start (local.get $end))
513
671
  (local.set $end (local.get $tmp))))
514
- (call $__str_range_eq (local.get $ptr) (local.get $start) (local.get $end) (local.get $other)))`
672
+ (call $__str_range_eq (local.get $ptr) (local.get $start) (local.get $end) (local.get $other)))`)
515
673
 
516
674
  // Clamp mirrors __str_slice (negatives count from the end; __str_range_eq
517
675
  // floors a negative span to an empty match).
518
- ctx.core.stdlib['__str_slice_eq'] = `(func $__str_slice_eq (param $ptr i64) (param $start i32) (param $end i32) (param $other i64) (result i32)
676
+ wat('__str_slice_eq', `(func $__str_slice_eq (param $ptr i64) (param $start i32) (param $end i32) (param $other i64) (result i32)
519
677
  (local $len i32)
520
678
  (local.set $len (call $__str_byteLen (local.get $ptr)))
521
679
  (if (i32.lt_s (local.get $start) (i32.const 0))
@@ -530,16 +688,16 @@ export default (ctx) => {
530
688
  (then (local.set $end (i32.const 0))))
531
689
  (if (i32.gt_s (local.get $end) (local.get $len))
532
690
  (then (local.set $end (local.get $len))))
533
- (call $__str_range_eq (local.get $ptr) (local.get $start) (local.get $end) (local.get $other)))`
691
+ (call $__str_range_eq (local.get $ptr) (local.get $start) (local.get $end) (local.get $other)))`)
534
692
 
535
693
  // Hoist SSO/heap dispatch for hay and ndl out of the inner byte loop. Inner
536
694
  // loop becomes (load8_u OR sso byte-extract) per side — no per-byte calls.
537
- ctx.core.stdlib['__str_indexof'] = `(func $__str_indexof (param $hay i64) (param $ndl i64) (param $from i32) (result i32)
695
+ wat('__str_indexof', `(func $__str_indexof (param $hay i64) (param $ndl i64) (param $from i32) (result i32)
538
696
  (local $hlen i32) (local $nlen i32) (local $i i32) (local $j i32) (local $match i32)
539
697
  (local $hoff i32) (local $noff i32)
540
698
  (local $hsso i32) (local $nsso i32) (local $hb i32) (local $nb i32) (local $k i32)
541
- ;; ToString the search value (21.1.3.9 step 4) coerces undefined/null/number/etc.
542
- (local.set $ndl (call $__to_str (local.get $ndl)))
699
+ ;; The search value is ToString'd at the call site (searchArg) per 21.1.3.9 step 4 —
700
+ ;; a known-string needle passes raw, so this helper carries no float-pulling __to_str.
543
701
  (local.set $hlen (call $__str_byteLen (local.get $hay)))
544
702
  (local.set $nlen (call $__str_byteLen (local.get $ndl)))
545
703
  (if (i32.eqz (local.get $nlen)) (then (return (local.get $from))))
@@ -573,10 +731,62 @@ export default (ctx) => {
573
731
  (if (local.get $match) (then (return (local.get $i))))
574
732
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
575
733
  (br $outer)))
576
- (i32.const -1))`
734
+ (i32.const -1))`)
735
+
736
+ // Mirror of __str_indexof but searches from the end. Returns the last byte-offset
737
+ // of `ndl` in `hay` at or before `from` (-1 if not found). Per JS spec step 4,
738
+ // ToString(searchValue) is applied first. SSO/heap dispatch hoisted exactly as
739
+ // in __str_indexof so the inner loop is cheap byte-fetches.
740
+ wat('__str_lastindexof', `(func $__str_lastindexof (param $hay i64) (param $ndl i64) (param $from i32) (result i32)
741
+ (local $hlen i32) (local $nlen i32) (local $i i32) (local $j i32) (local $match i32)
742
+ (local $hoff i32) (local $noff i32)
743
+ (local $hsso i32) (local $nsso i32) (local $hb i32) (local $nb i32) (local $k i32)
744
+ ;; The search value is ToString'd at the call site (searchArg) per 21.1.3.10 step 4.
745
+ (local.set $hlen (call $__str_byteLen (local.get $hay)))
746
+ (local.set $nlen (call $__str_byteLen (local.get $ndl)))
747
+ ;; Empty needle always matches at the clamp(from,0,hlen) position
748
+ (if (i32.eqz (local.get $nlen))
749
+ (then (return (select
750
+ (select (local.get $from) (local.get $hlen) (i32.lt_s (local.get $from) (local.get $hlen)))
751
+ (i32.const 0)
752
+ (i32.ge_s (local.get $from) (i32.const 0))))))
753
+ (if (i32.gt_s (local.get $nlen) (local.get $hlen)) (then (return (i32.const -1))))
754
+ (local.set $hoff (i32.wrap_i64 (i64.and (local.get $hay) (i64.const ${LAYOUT.OFFSET_MASK}))))
755
+ (local.set $noff (i32.wrap_i64 (i64.and (local.get $ndl) (i64.const ${LAYOUT.OFFSET_MASK}))))
756
+ (local.set $hsso (i32.and
757
+ (i32.wrap_i64 (i64.shr_u (local.get $hay) (i64.const ${LAYOUT.AUX_SHIFT})))
758
+ (i32.const ${LAYOUT.SSO_BIT})))
759
+ (local.set $nsso (i32.and
760
+ (i32.wrap_i64 (i64.shr_u (local.get $ndl) (i64.const ${LAYOUT.AUX_SHIFT})))
761
+ (i32.const ${LAYOUT.SSO_BIT})))
762
+ ;; clamp start position: from defaults to hlen-nlen, capped at that ceiling
763
+ (local.set $i (i32.sub (local.get $hlen) (local.get $nlen)))
764
+ (if (i32.and (i32.ge_s (local.get $from) (i32.const 0)) (i32.lt_s (local.get $from) (local.get $i)))
765
+ (then (local.set $i (local.get $from))))
766
+ (block $done (loop $outer
767
+ (br_if $done (i32.lt_s (local.get $i) (i32.const 0)))
768
+ (local.set $match (i32.const 1))
769
+ (local.set $j (i32.const 0))
770
+ (block $nomatch (loop $inner
771
+ (br_if $nomatch (i32.ge_s (local.get $j) (local.get $nlen)))
772
+ (local.set $k (i32.add (local.get $i) (local.get $j)))
773
+ (local.set $hb (if (result i32) (local.get $hsso)
774
+ (then (i32.and (i32.shr_u (local.get $hoff) (i32.shl (local.get $k) (i32.const 3))) (i32.const 0xFF)))
775
+ (else (i32.load8_u (i32.add (local.get $hoff) (local.get $k))))))
776
+ (local.set $nb (if (result i32) (local.get $nsso)
777
+ (then (i32.and (i32.shr_u (local.get $noff) (i32.shl (local.get $j) (i32.const 3))) (i32.const 0xFF)))
778
+ (else (i32.load8_u (i32.add (local.get $noff) (local.get $j))))))
779
+ (if (i32.ne (local.get $hb) (local.get $nb))
780
+ (then (local.set $match (i32.const 0)) (br $nomatch)))
781
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
782
+ (br $inner)))
783
+ (if (local.get $match) (then (return (local.get $i))))
784
+ (local.set $i (i32.sub (local.get $i) (i32.const 1)))
785
+ (br $outer)))
786
+ (i32.const -1))`)
577
787
 
578
788
  // SSO/heap dispatch hoisted; inner loop is two inlined byte-fetches and a compare.
579
- ctx.core.stdlib['__str_startswith'] = `(func $__str_startswith (param $str i64) (param $pfx i64) (result i32)
789
+ wat('__str_startswith', `(func $__str_startswith (param $str i64) (param $pfx i64) (result i32)
580
790
  (local $plen i32) (local $i i32)
581
791
  (local $soff i32) (local $poff i32) (local $ssso i32) (local $psso i32)
582
792
  (local.set $plen (call $__str_byteLen (local.get $pfx)))
@@ -602,9 +812,9 @@ export default (ctx) => {
602
812
  (then (return (i32.const 0))))
603
813
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
604
814
  (br $loop)))
605
- (i32.const 1))`
815
+ (i32.const 1))`)
606
816
 
607
- ctx.core.stdlib['__str_endswith'] = `(func $__str_endswith (param $str i64) (param $sfx i64) (result i32)
817
+ wat('__str_endswith', `(func $__str_endswith (param $str i64) (param $sfx i64) (result i32)
608
818
  (local $slen i32) (local $flen i32) (local $off i32) (local $i i32) (local $k i32)
609
819
  (local $soff i32) (local $foff i32) (local $ssso i32) (local $fsso i32)
610
820
  (local.set $slen (call $__str_byteLen (local.get $str)))
@@ -633,10 +843,10 @@ export default (ctx) => {
633
843
  (then (return (i32.const 0))))
634
844
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
635
845
  (br $loop)))
636
- (i32.const 1))`
846
+ (i32.const 1))`)
637
847
 
638
848
  // Source SSO/heap dispatch hoisted out of the byte loop (was a per-byte __char_at).
639
- ctx.core.stdlib['__str_case'] = `(func $__str_case (param $ptr i64) (param $lo i32) (param $hi i32) (param $delta i32) (result f64)
849
+ wat('__str_case', `(func $__str_case (param $ptr i64) (param $lo i32) (param $hi i32) (param $delta i32) (result f64)
640
850
  (local $len i32) (local $off i32) (local $i i32) (local $c i32)
641
851
  (local $srcOff i32)
642
852
  (local.set $len (call $__str_byteLen (local.get $ptr)))
@@ -667,9 +877,9 @@ export default (ctx) => {
667
877
  (i32.store8 (i32.add (local.get $off) (local.get $i)) (local.get $c))
668
878
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
669
879
  (br $lh)))))
670
- (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
880
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
671
881
 
672
- ctx.core.stdlib['__str_trim'] = `(func $__str_trim (param $ptr i64) (result f64)
882
+ wat('__str_trim', `(func $__str_trim (param $ptr i64) (result f64)
673
883
  (local $len i32) (local $start i32) (local $end i32)
674
884
  (local.set $len (call $__str_byteLen (local.get $ptr)))
675
885
  (local.set $start (i32.const 0))
@@ -684,9 +894,9 @@ export default (ctx) => {
684
894
  (br_if $d2 (i32.gt_u (call $__char_at (local.get $ptr) (i32.sub (local.get $end) (i32.const 1))) (i32.const 32)))
685
895
  (local.set $end (i32.sub (local.get $end) (i32.const 1)))
686
896
  (br $l2)))
687
- (call $__str_slice (local.get $ptr) (local.get $start) (local.get $end)))`
897
+ (call $__str_slice (local.get $ptr) (local.get $start) (local.get $end)))`)
688
898
 
689
- ctx.core.stdlib['__str_trimStart'] = `(func $__str_trimStart (param $ptr i64) (result f64)
899
+ wat('__str_trimStart', `(func $__str_trimStart (param $ptr i64) (result f64)
690
900
  (local $len i32) (local $start i32)
691
901
  (local.set $len (call $__str_byteLen (local.get $ptr)))
692
902
  (local.set $start (i32.const 0))
@@ -695,9 +905,9 @@ export default (ctx) => {
695
905
  (br_if $done (i32.gt_u (call $__char_at (local.get $ptr) (local.get $start)) (i32.const 32)))
696
906
  (local.set $start (i32.add (local.get $start) (i32.const 1)))
697
907
  (br $loop)))
698
- (call $__str_slice (local.get $ptr) (local.get $start) (local.get $len)))`
908
+ (call $__str_slice (local.get $ptr) (local.get $start) (local.get $len)))`)
699
909
 
700
- ctx.core.stdlib['__str_trimEnd'] = `(func $__str_trimEnd (param $ptr i64) (result f64)
910
+ wat('__str_trimEnd', `(func $__str_trimEnd (param $ptr i64) (result f64)
701
911
  (local $len i32) (local $end i32)
702
912
  (local.set $len (call $__str_byteLen (local.get $ptr)))
703
913
  (local.set $end (local.get $len))
@@ -706,11 +916,11 @@ export default (ctx) => {
706
916
  (br_if $done (i32.gt_u (call $__char_at (local.get $ptr) (i32.sub (local.get $end) (i32.const 1))) (i32.const 32)))
707
917
  (local.set $end (i32.sub (local.get $end) (i32.const 1)))
708
918
  (br $loop)))
709
- (call $__str_slice (local.get $ptr) (i32.const 0) (local.get $end)))`
919
+ (call $__str_slice (local.get $ptr) (i32.const 0) (local.get $end)))`)
710
920
 
711
921
  // Materialize source bytes once via __str_copy (handles SSO/heap), then memory.copy
712
922
  // each subsequent repetition (single bulk op vs len byte stores per copy).
713
- ctx.core.stdlib['__str_repeat'] = `(func $__str_repeat (param $ptr i64) (param $n i32) (result f64)
923
+ wat('__str_repeat', `(func $__str_repeat (param $ptr i64) (param $n i32) (result f64)
714
924
  (local $len i32) (local $total i32) (local $off i32) (local $i i32)
715
925
  (local.set $len (call $__str_byteLen (local.get $ptr)))
716
926
  (if (i32.or (i32.eqz (local.get $n)) (i32.eqz (local.get $len)))
@@ -729,11 +939,11 @@ export default (ctx) => {
729
939
  (local.get $len))
730
940
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
731
941
  (br $loop)))
732
- (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
942
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
733
943
 
734
944
  // Coerce value to string: numbers → __ftoa, nullish → static strings,
735
945
  // plain NaN → "NaN", arrays → join(","), other string-like pointers pass through.
736
- ctx.core.stdlib['__to_str'] = `(func $__to_str (param $val i64) (result i64)
946
+ wat('__to_str', `(func $__to_str (param $val i64) (result i64)
737
947
  (local $type i32) (local $f f64)
738
948
  (local.set $f (f64.reinterpret_i64 (local.get $val)))
739
949
  ;; Not NaN → number, convert
@@ -743,6 +953,10 @@ export default (ctx) => {
743
953
  (then (return (i64.reinterpret_f64 (call $__static_str (i32.const 5))))))
744
954
  (if (i64.eq (local.get $val) (i64.const ${UNDEF_NAN}))
745
955
  (then (return (i64.reinterpret_f64 (call $__static_str (i32.const 6))))))
956
+ (if (i64.eq (local.get $val) (i64.const ${FALSE_NAN}))
957
+ (then (return (i64.reinterpret_f64 (call $__static_str (i32.const 4))))))
958
+ (if (i64.eq (local.get $val) (i64.const ${TRUE_NAN}))
959
+ (then (return (i64.reinterpret_f64 (call $__static_str (i32.const 3))))))
746
960
  (local.set $type (call $__ptr_type (local.get $val)))
747
961
  ;; Plain NaN (type=0) → "NaN" string
748
962
  (if (i32.eqz (local.get $type))
@@ -751,11 +965,11 @@ export default (ctx) => {
751
965
  (if (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
752
966
  (then (return (i64.reinterpret_f64 (call $__str_join (local.get $val)
753
967
  (i64.reinterpret_f64 (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT | 1}) (i32.const 44))))))))
754
- (local.get $val))`
968
+ (local.get $val))`)
755
969
 
756
970
  // Copy bytes of a string (SSO or heap) into memory at dst. Uses memory.copy for
757
971
  // heap strings (single native op); unpacks SSO offset-packed bytes inline.
758
- ctx.core.stdlib['__str_copy'] = `(func $__str_copy (param $src i64) (param $dst i32) (param $len i32)
972
+ wat('__str_copy', `(func $__str_copy (param $src i64) (param $dst i32) (param $len i32)
759
973
  (local $w i32)
760
974
  (if (i64.ne (i64.and (local.get $src) (i64.const ${SSO_BIT_I64})) (i64.const 0))
761
975
  (then
@@ -775,7 +989,7 @@ export default (ctx) => {
775
989
  ;; Heap STRING: memory.copy directly from string data
776
990
  (memory.copy (local.get $dst)
777
991
  (i32.wrap_i64 (i64.and (local.get $src) (i64.const ${LAYOUT.OFFSET_MASK})))
778
- (local.get $len)))))`
992
+ (local.get $len)))))`)
779
993
 
780
994
  // Bump-extend fast path: when `a` is a heap STRING sitting at the top of the
781
995
  // bump allocator, extend its allocation in place instead of copying. Mutates
@@ -826,10 +1040,7 @@ export default (ctx) => {
826
1040
  (then
827
1041
  (local.set $newHeap
828
1042
  (i32.and (i32.add (i32.add (local.get $aoff) (local.get $total)) (i32.const 7)) (i32.const -8)))
829
- (if (i32.gt_u (local.get $newHeap) (i32.mul (memory.size) (i32.const 65536)))
830
- (then (if (i32.eq (memory.grow
831
- (i32.shr_u (i32.add (i32.sub (local.get $newHeap) (i32.mul (memory.size) (i32.const 65536))) (i32.const 65535)) (i32.const 16)))
832
- (i32.const -1)) (then (unreachable)))))
1043
+ (call $__memgrow (local.get $newHeap))
833
1044
  (call $__str_copy (local.get $b)
834
1045
  (i32.add (local.get $aoff) (local.get $alen))
835
1046
  (local.get $blen))
@@ -841,7 +1052,7 @@ export default (ctx) => {
841
1052
  // VAL.STRING and the rhs is a string-index. Skips __str_idx's 1-char SSO
842
1053
  // construction and __str_concat's type-dispatch — byte goes directly from
843
1054
  // __char_at to memory. Bump-extends in place when `a` is at heap top.
844
- ctx.core.stdlib['__str_append_byte'] = `(func $__str_append_byte (param $a i64) (param $byte i32) (result f64)
1055
+ wat('__str_append_byte', `(func $__str_append_byte (param $a i64) (param $byte i32) (result f64)
845
1056
  (local $ta i32) (local $aoff i32) (local $alen i32)
846
1057
  (local $newHeap i32) (local $off i32) (local $total i32)
847
1058
  (local.set $ta (i32.wrap_i64 (i64.and (i64.shr_u (local.get $a) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
@@ -863,10 +1074,7 @@ export default (ctx) => {
863
1074
  (then
864
1075
  (local.set $newHeap
865
1076
  (i32.and (i32.add (i32.add (local.get $aoff) (local.get $alen)) (i32.const 8)) (i32.const -8)))
866
- (if (i32.gt_u (local.get $newHeap) (i32.mul (memory.size) (i32.const 65536)))
867
- (then (if (i32.eq (memory.grow
868
- (i32.shr_u (i32.add (i32.sub (local.get $newHeap) (i32.mul (memory.size) (i32.const 65536))) (i32.const 65535)) (i32.const 16)))
869
- (i32.const -1)) (then (unreachable)))))
1077
+ (call $__memgrow (local.get $newHeap))
870
1078
  (i32.store8 (i32.add (local.get $aoff) (local.get $alen)) (local.get $byte))
871
1079
  (i32.store (i32.sub (local.get $aoff) (i32.const 4)) (i32.add (local.get $alen) (i32.const 1)))
872
1080
  (global.set $__heap (local.get $newHeap))
@@ -899,9 +1107,9 @@ export default (ctx) => {
899
1107
  (local.set $off (i32.add (local.get $off) (i32.const 4)))
900
1108
  (call $__str_copy (local.get $a) (local.get $off) (local.get $alen))
901
1109
  (i32.store8 (i32.add (local.get $off) (local.get $alen)) (local.get $byte))
902
- (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
1110
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
903
1111
 
904
- ctx.core.stdlib['__str_concat'] = `(func $__str_concat (param $a i64) (param $b i64) (result f64)
1112
+ wat('__str_concat', `(func $__str_concat (param $a i64) (param $b i64) (result f64)
905
1113
  (local $alen i32) (local $blen i32) (local $total i32) (local $off i32)
906
1114
  (local $ta i32) (local $aoff i32) (local $newHeap i32)
907
1115
  ;; Coerce operands to strings if needed
@@ -919,9 +1127,9 @@ export default (ctx) => {
919
1127
  (local.set $off (i32.add (local.get $off) (i32.const 4)))
920
1128
  (call $__str_copy (local.get $a) (local.get $off) (local.get $alen))
921
1129
  (call $__str_copy (local.get $b) (i32.add (local.get $off) (local.get $alen)) (local.get $blen))
922
- (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
1130
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
923
1131
 
924
- ctx.core.stdlib['__str_concat_raw'] = `(func $__str_concat_raw (param $a i64) (param $b i64) (result f64)
1132
+ wat('__str_concat_raw', `(func $__str_concat_raw (param $a i64) (param $b i64) (result f64)
925
1133
  (local $alen i32) (local $blen i32) (local $total i32) (local $off i32)
926
1134
  (local $ta i32) (local $aoff i32) (local $newHeap i32)
927
1135
  (local.set $alen (call $__str_byteLen (local.get $a)))
@@ -936,9 +1144,9 @@ export default (ctx) => {
936
1144
  (local.set $off (i32.add (local.get $off) (i32.const 4)))
937
1145
  (call $__str_copy (local.get $a) (local.get $off) (local.get $alen))
938
1146
  (call $__str_copy (local.get $b) (i32.add (local.get $off) (local.get $alen)) (local.get $blen))
939
- (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
1147
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
940
1148
 
941
- ctx.core.stdlib['__str_replace'] = `(func $__str_replace (param $str i64) (param $search i64) (param $repl i64) (result f64)
1149
+ wat('__str_replace', `(func $__str_replace (param $str i64) (param $search i64) (param $repl i64) (result f64)
942
1150
  (local $idx i32) (local $slen i32)
943
1151
  (local.set $idx (call $__str_indexof (local.get $str) (local.get $search) (i32.const 0)))
944
1152
  (if (result f64) (i32.lt_s (local.get $idx) (i32.const 0))
@@ -950,9 +1158,9 @@ export default (ctx) => {
950
1158
  (i64.reinterpret_f64 (call $__str_slice (local.get $str) (i32.const 0) (local.get $idx)))
951
1159
  (local.get $repl)))
952
1160
  (i64.reinterpret_f64 (call $__str_slice (local.get $str) (i32.add (local.get $idx) (local.get $slen))
953
- (call $__str_byteLen (local.get $str))))))))`
1161
+ (call $__str_byteLen (local.get $str))))))))`)
954
1162
 
955
- ctx.core.stdlib['__str_replaceall'] = `(func $__str_replaceall (param $str i64) (param $search i64) (param $repl i64) (result f64)
1163
+ wat('__str_replaceall', `(func $__str_replaceall (param $str i64) (param $search i64) (param $repl i64) (result f64)
956
1164
  (local $idx i32) (local $slen i32) (local $pos i32) (local $result i64)
957
1165
  (local.set $slen (call $__str_byteLen (local.get $search)))
958
1166
  (local.set $result (local.get $str))
@@ -968,33 +1176,48 @@ export default (ctx) => {
968
1176
  (call $__str_byteLen (local.get $result)))))))
969
1177
  (local.set $pos (i32.add (local.get $idx) (call $__str_byteLen (local.get $repl))))
970
1178
  (br $next)))
971
- (f64.reinterpret_i64 (local.get $result)))`
972
-
973
- // Empty separator: per JS spec, split into individual byte-chars
974
- // ("abc".split("") -> ["a","b","c"], "".split("") -> []). Without this
975
- // guard the main loop advances by plen=0 and spins forever.
976
- ctx.core.stdlib['__str_split'] = `(func $__str_split (param $str i64) (param $sep i64) (result f64)
1179
+ (f64.reinterpret_i64 (local.get $result)))`)
1180
+
1181
+ // $limit ≥ 0: honour JS's optional limit arg. 0x7fffffff = "no limit"
1182
+ // (sentinel passed by the no-limit call site). limit=0 []. limit=N → at
1183
+ // most N elements; the (N+1)th and later pieces are DISCARDED (not appended
1184
+ // as a remainder). Empty separator: split into individual byte-chars, up to
1185
+ // $limit chars ("abc".split("") → ["a","b","c"], "".split("") → []).
1186
+ wat('__str_split', `(func $__str_split (param $str i64) (param $sep i64) (param $limit i32) (result f64)
977
1187
  (local $slen i32) (local $plen i32) (local $count i32)
978
1188
  (local $i i32) (local $j i32) (local $match i32)
979
- (local $arr i32) (local $piece_start i32) (local $piece_idx i32)
1189
+ (local $arr i32) (local $piece_start i32) (local $piece_idx i32) (local $hitlim i32)
980
1190
  (local.set $slen (call $__str_byteLen (local.get $str)))
981
1191
  (local.set $plen (call $__str_byteLen (local.get $sep)))
1192
+ ;; limit=0 → empty array
1193
+ (if (i32.eqz (local.get $limit)) (then
1194
+ (local.set $arr (call $__alloc (i32.const 8)))
1195
+ (i32.store (local.get $arr) (i32.const 0))
1196
+ (i32.store (i32.add (local.get $arr) (i32.const 4)) (i32.const 0))
1197
+ (return (call $__mkptr (i32.const 1) (i32.const 0) (i32.add (local.get $arr) (i32.const 8))))))
982
1198
  (if (i32.eqz (local.get $plen)) (then
983
- (local.set $arr (call $__alloc (i32.add (i32.const 8) (i32.shl (local.get $slen) (i32.const 3)))))
984
- (i32.store (local.get $arr) (local.get $slen))
985
- (i32.store (i32.add (local.get $arr) (i32.const 4)) (local.get $slen))
1199
+ ;; Empty-separator: split into individual byte-chars, up to $limit
1200
+ (local.set $count (select (local.get $limit) (local.get $slen) (i32.lt_u (local.get $limit) (local.get $slen))))
1201
+ (local.set $arr (call $__alloc (i32.add (i32.const 8) (i32.shl (local.get $count) (i32.const 3)))))
1202
+ (i32.store (local.get $arr) (local.get $count))
1203
+ (i32.store (i32.add (local.get $arr) (i32.const 4)) (local.get $count))
986
1204
  (local.set $arr (i32.add (local.get $arr) (i32.const 8)))
987
1205
  (block $de (loop $le
988
- (br_if $de (i32.ge_s (local.get $i) (local.get $slen)))
1206
+ (br_if $de (i32.ge_s (local.get $i) (local.get $count)))
989
1207
  (f64.store (i32.add (local.get $arr) (i32.shl (local.get $i) (i32.const 3)))
990
1208
  (call $__str_slice (local.get $str) (local.get $i) (i32.add (local.get $i) (i32.const 1))))
991
1209
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
992
1210
  (br $le)))
993
1211
  (return (call $__mkptr (i32.const 1) (i32.const 0) (local.get $arr)))))
1212
+ ;; Count pass: tally pieces = separators+1, capped at $limit.
1213
+ ;; We stop incrementing count once count reaches $limit (last sep found
1214
+ ;; at that point produces piece #limit — a separator *after* piece limit-1,
1215
+ ;; meaning we've already found all limit pieces and don't count more).
994
1216
  (local.set $count (i32.const 1))
995
1217
  (local.set $i (i32.const 0))
996
1218
  (block $d1 (loop $l1
997
1219
  (br_if $d1 (i32.gt_s (local.get $i) (i32.sub (local.get $slen) (local.get $plen))))
1220
+ (br_if $d1 (i32.ge_u (local.get $count) (local.get $limit)))
998
1221
  (local.set $match (i32.const 1))
999
1222
  (local.set $j (i32.const 0))
1000
1223
  (block $n1 (loop $c1
@@ -1017,6 +1240,11 @@ export default (ctx) => {
1017
1240
  (local.set $piece_start (i32.const 0))
1018
1241
  (local.set $piece_idx (i32.const 0))
1019
1242
  (local.set $i (i32.const 0))
1243
+ ;; Fill pass: write pieces separated by $sep, up to $count total pieces.
1244
+ ;; When a separator is found and piece_idx+1 reaches count, write that
1245
+ ;; piece (before the sep) and exit WITHOUT appending the tail — this
1246
+ ;; correctly discards the remainder when $limit truncates the result.
1247
+ (local.set $hitlim (i32.const 0))
1020
1248
  (block $d2 (loop $l2
1021
1249
  (br_if $d2 (i32.gt_s (local.get $i) (i32.sub (local.get $slen) (local.get $plen))))
1022
1250
  (local.set $match (i32.const 1))
@@ -1034,14 +1262,27 @@ export default (ctx) => {
1034
1262
  (local.set $piece_idx (i32.add (local.get $piece_idx) (i32.const 1)))
1035
1263
  (local.set $i (i32.add (local.get $i) (local.get $plen)))
1036
1264
  (local.set $piece_start (local.get $i))
1265
+ ;; If we've emitted all $count pieces, mark limit-hit and stop (discard tail)
1266
+ (if (i32.ge_s (local.get $piece_idx) (local.get $count))
1267
+ (then (local.set $hitlim (i32.const 1)) (br $d2)))
1037
1268
  (br $l2)))
1038
1269
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
1039
1270
  (br $l2)))
1040
- (f64.store (i32.add (local.get $arr) (i32.shl (local.get $piece_idx) (i32.const 3)))
1041
- (call $__str_slice (local.get $str) (local.get $piece_start) (local.get $slen)))
1042
- (call $__mkptr (i32.const 1) (i32.const 0) (local.get $arr)))`
1043
-
1044
- ctx.core.stdlib['__str_join'] = `(func $__str_join (param $arr i64) (param $sep i64) (result f64)
1271
+ ;; Write tail only when the scan ended naturally (not truncated by limit)
1272
+ (if (i32.eqz (local.get $hitlim)) (then
1273
+ (f64.store (i32.add (local.get $arr) (i32.shl (local.get $piece_idx) (i32.const 3)))
1274
+ (call $__str_slice (local.get $str) (local.get $piece_start) (local.get $slen)))))
1275
+ (call $__mkptr (i32.const 1) (i32.const 0) (local.get $arr)))`)
1276
+
1277
+ // Array (type=1) → join(",") like JS Array.toString().
1278
+ // When the typedarray module is loaded, also handles PTR.TYPED (type=3) arrays:
1279
+ // promoteIntArrayLiterals rewrites [int,...] → new Int32Array([...]) internally,
1280
+ // so a.map(fn).join() may receive a PTR.TYPED result. Use __typed_idx to load
1281
+ // each element correctly (it returns f64 for any element type / stride).
1282
+ wat('__str_join', () => {
1283
+ if (!ctx.module.modules.typedarray) {
1284
+ // ARRAY-only fast path — no __typed_idx overhead.
1285
+ return `(func $__str_join (param $arr i64) (param $sep i64) (result f64)
1045
1286
  (local $off i32) (local $len i32) (local $i i32) (local $result f64)
1046
1287
  (local.set $off (call $__ptr_offset (local.get $arr)))
1047
1288
  (local.set $len (call $__len (local.get $arr)))
@@ -1057,13 +1298,48 @@ export default (ctx) => {
1057
1298
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
1058
1299
  (br $loop)))
1059
1300
  (local.get $result))`
1301
+ }
1302
+ // ARRAY + TYPED path: runtime dispatch on ptr type.
1303
+ // PTR.TYPED (type=3): elements have typed-array stride; __typed_idx reads correctly.
1304
+ // PTR.ARRAY (type=1): elements are 8-byte NaN-boxed f64 slots; i64.load is correct.
1305
+ return `(func $__str_join (param $arr i64) (param $sep i64) (result f64)
1306
+ (local $off i32) (local $len i32) (local $i i32) (local $result f64) (local $isTyped i32)
1307
+ (local.set $isTyped
1308
+ (i32.eq
1309
+ (i32.and (i32.wrap_i64 (i64.shr_u (local.get $arr) (i64.const ${LAYOUT.TAG_SHIFT})))
1310
+ (i32.const ${LAYOUT.TAG_MASK}))
1311
+ (i32.const ${PTR.TYPED})))
1312
+ (local.set $off (call $__ptr_offset (local.get $arr)))
1313
+ (local.set $len (call $__len (local.get $arr)))
1314
+ (if (i32.eqz (local.get $len))
1315
+ (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
1316
+ (local.set $result
1317
+ (f64.reinterpret_i64
1318
+ (call $__to_str
1319
+ (if (result i64) (local.get $isTyped)
1320
+ (then (i64.reinterpret_f64 (call $__typed_idx (local.get $arr) (i32.const 0))))
1321
+ (else (i64.load (local.get $off)))))))
1322
+ (local.set $i (i32.const 1))
1323
+ (block $done (loop $loop
1324
+ (br_if $done (i32.ge_s (local.get $i) (local.get $len)))
1325
+ (local.set $result (call $__str_concat (i64.reinterpret_f64 (local.get $result)) (local.get $sep)))
1326
+ (local.set $result
1327
+ (call $__str_concat
1328
+ (i64.reinterpret_f64 (local.get $result))
1329
+ (if (result i64) (local.get $isTyped)
1330
+ (then (i64.reinterpret_f64 (call $__typed_idx (local.get $arr) (local.get $i))))
1331
+ (else (i64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3))))))))
1332
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
1333
+ (br $loop)))
1334
+ (local.get $result))`
1335
+ })
1060
1336
 
1061
1337
  // Source string copied via __str_copy (handles SSO/heap with memory.copy where possible).
1062
1338
  // Pad fill loops a single tile of pad bytes — hoist pad dispatch out of the byte loop.
1063
- ctx.core.stdlib['__str_pad'] = `(func $__str_pad (param $str i64) (param $target i32) (param $pad i64) (param $before i32) (result f64)
1339
+ wat('__str_pad', `(func $__str_pad (param $str i64) (param $target i32) (param $pad i64) (param $before i32) (result f64)
1064
1340
  (local $slen i32) (local $plen i32) (local $fill i32) (local $off i32) (local $i i32)
1065
1341
  (local $str_off i32) (local $pad_off i32)
1066
- (local $pbits i64) (local $poff i32) (local $psso i32)
1342
+ (local $pbits i64) (local $poff i32) (local $psso i32) (local $sp i32) (local $sb i32)
1067
1343
  (local.set $slen (call $__str_byteLen (local.get $str)))
1068
1344
  (if (i32.ge_s (local.get $slen) (local.get $target))
1069
1345
  (then (return (f64.reinterpret_i64 (local.get $str)))))
@@ -1090,7 +1366,25 @@ export default (ctx) => {
1090
1366
  (else (i32.load8_u (i32.add (local.get $poff) (i32.rem_u (local.get $i) (local.get $plen)))))))
1091
1367
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
1092
1368
  (br $l2)))
1093
- (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
1369
+ ${!ctx.memory.shared ? `
1370
+ ;; SSO + reclaim for ≤4 ASCII results: pack into the pointer and free this pad
1371
+ ;; allocation (at heap top) so padStart in a builder loop is allocation-neutral
1372
+ ;; — the accumulator stays at heap top and keeps bump-extending (O(n)).
1373
+ (if (i32.le_u (local.get $target) (i32.const 4))
1374
+ (then
1375
+ (block $heap
1376
+ (local.set $i (i32.const 0)) (local.set $sp (i32.const 0))
1377
+ (loop $pk
1378
+ (if (i32.lt_u (local.get $i) (local.get $target))
1379
+ (then
1380
+ (local.set $sb (i32.load8_u (i32.add (local.get $off) (local.get $i))))
1381
+ (br_if $heap (i32.ge_u (local.get $sb) (i32.const 0x80)))
1382
+ (local.set $sp (i32.or (local.get $sp) (i32.shl (local.get $sb) (i32.shl (local.get $i) (i32.const 3)))))
1383
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
1384
+ (br $pk))))
1385
+ (global.set $__heap (i32.sub (local.get $off) (i32.const 4)))
1386
+ (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.or (i32.const ${LAYOUT.SSO_BIT}) (local.get $target)) (local.get $sp))))))` : ''}
1387
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
1094
1388
 
1095
1389
  // Base helpers (__sso_char/__str_char/__char_at/__str_byteLen) are referenced
1096
1390
  // from other helpers' WAT bodies and from emit sites; their `stdlibDeps`
@@ -1103,16 +1397,16 @@ export default (ctx) => {
1103
1397
  // (21.1.3.27/28). Typed forms cover the static-string case; generic forms
1104
1398
  // pair with them so the dispatcher can pick a runtime ptr-type branch when
1105
1399
  // the receiver type can't be statically inferred (e.g. a callback param).
1106
- ctx.core.emit['.string:toString'] = (str) => asF64(emit(str))
1107
- ctx.core.emit['.string:valueOf'] = (str) => asF64(emit(str))
1108
- ctx.core.emit['.toString'] = (val) => {
1400
+ bind('.string:toString', (str) => asF64(emit(str)))
1401
+ bind('.string:valueOf', (str) => asF64(emit(str)))
1402
+ bind('.toString', (val) => {
1109
1403
  inc('__to_str')
1110
1404
  return typed(['f64.reinterpret_i64', ['call', '$__to_str', asI64(emit(val))]], 'f64')
1111
- }
1405
+ })
1112
1406
  // Object.prototype.valueOf returns the receiver (per ES2024 20.1.3.7).
1113
1407
  // Array/Object inherit this; only primitive wrappers (Number/Boolean/String)
1114
1408
  // override to return the primitive — strings already covered by .string:valueOf.
1115
- ctx.core.emit['.valueOf'] = (val) => asF64(emit(val))
1409
+ bind('.valueOf', (val) => asF64(emit(val)))
1116
1410
 
1117
1411
  // `.slice` lowering, parametrised on the backing helper: __str_slice copies
1118
1412
  // bytes; __str_slice_view returns a no-copy SLICE_BIT view — used only when
@@ -1130,8 +1424,8 @@ export default (ctx) => {
1130
1424
  ['call', `$${fn}`, ['i64.reinterpret_f64', ['local.get', `$${t}`]], startIR,
1131
1425
  ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]], 'f64')
1132
1426
  }
1133
- ctx.core.emit['.string:slice'] = sliceEmitter('__str_slice')
1134
- ctx.core.emit['.string:slice#view'] = sliceEmitter('__str_slice_view')
1427
+ bind('.string:slice', sliceEmitter('__str_slice'))
1428
+ bind('.string:slice#view', sliceEmitter('__str_slice_view'))
1135
1429
 
1136
1430
  // ToIntegerOrInfinity for a string-method position argument: ToNumber (so
1137
1431
  // string / boolean / null / undefined positions coerce per spec) then trunc.
@@ -1147,15 +1441,39 @@ export default (ctx) => {
1147
1441
  // covers string/number/null/undefined needles, but two cases need help here:
1148
1442
  // a BOOL rides the 0/1 carrier (→ "0"/"1" not "true"/"false"), and an OBJECT
1149
1443
  // needs compile-time ToPrimitive(string) (__to_str can't invoke user toString).
1150
- const searchArg = (search) =>
1151
- valTypeOf(search) === VAL.BOOL ? asI64(emitBoolStr(search)) :
1152
- valTypeOf(search) === VAL.OBJECT ? toStrI64(search, emit(search)) : asI64(emit(search))
1444
+ // Coerce the search operand to a string AT THE CALL SITE (21.1.3.x ToString step),
1445
+ // so the `__str_indexof` family carries no embedded `__to_str`. A known-STRING arg
1446
+ // (the overwhelmingly common `s.indexOf("x")` / `s.indexOf(t)` shape) passes raw —
1447
+ // dropping the whole ToString → float-formatter dep tree (~4 KB) that an internal,
1448
+ // unconditional coercion forced into every search-method program. Mirrors
1449
+ // `stringSearchMethod` (startsWith/endsWith), which has always coerced here.
1450
+ const searchArg = (search) => {
1451
+ const vt = valTypeOf(search)
1452
+ if (vt === VAL.STRING) return asI64(emit(search))
1453
+ if (vt === VAL.BOOL) return asI64(bool(search))
1454
+ if (vt === VAL.OBJECT) return toStrI64(search, emit(search))
1455
+ inc('__to_str')
1456
+ return ['call', '$__to_str', asI64(emit(search))]
1457
+ }
1458
+ // Replacement operand of .replace/.replaceAll — same call-site ToString as searchArg.
1459
+ const strReplArg = searchArg
1153
1460
 
1154
- ctx.core.emit['.string:indexOf'] = (str, search, from) => {
1461
+ bind('.string:indexOf', (str, search, from) => {
1155
1462
  inc('__str_indexof')
1156
1463
  const hay = asI64(emit(str)), ndl = searchArg(search)
1157
1464
  return typed(['f64.convert_i32_s', ['call', '$__str_indexof', hay, ndl, posIndex(from)]], 'f64')
1158
- }
1465
+ })
1466
+
1467
+ // String.prototype.lastIndexOf: search from the end, returning the last
1468
+ // byte-offset of `search` in `str` at or before `fromIndex` (or -1).
1469
+ // Per spec the `from` default is +∞ (search from the very end), which we
1470
+ // map to 0x7fffffff — __str_lastindexof clamps it to hlen-nlen anyway.
1471
+ bind('.string:lastIndexOf', (str, search, from) => {
1472
+ inc('__str_lastindexof')
1473
+ const hay = asI64(emit(str)), ndl = searchArg(search)
1474
+ const fromIR = from == null ? ['i32.const', 0x7fffffff] : asI32(emit(from))
1475
+ return typed(['f64.convert_i32_s', ['call', '$__str_lastindexof', hay, ndl, fromIR]], 'f64')
1476
+ })
1159
1477
 
1160
1478
  // String.prototype.{includes,startsWith,endsWith} run IsRegExp(searchString)
1161
1479
  // and throw a TypeError when it is a RegExp. Detect a regex-typed search arg
@@ -1166,16 +1484,16 @@ export default (ctx) => {
1166
1484
  return typed(['block', ['result', 'f64'], ['throw', '$__jz_err', ['f64.const', 0]]], 'f64')
1167
1485
  }
1168
1486
 
1169
- ctx.core.emit['.string:includes'] = (str, search, from) => {
1487
+ bind('.string:includes', (str, search, from) => {
1170
1488
  const guard = regexpSearchGuard(search); if (guard) return guard
1171
1489
  inc('__str_indexof')
1172
1490
  const hay = asI64(emit(str)), ndl = searchArg(search)
1173
1491
  return typed(['f64.convert_i32_s',
1174
1492
  ['i32.ge_s', ['call', '$__str_indexof', hay, ndl, posIndex(from)], ['i32.const', 0]]], 'f64')
1175
- }
1493
+ })
1176
1494
 
1177
1495
  // Generic (no collision)
1178
- ctx.core.emit['.substring'] = (str, start, end) => {
1496
+ bind('.substring', (str, start, end) => {
1179
1497
  inc('__str_substring')
1180
1498
  if (end != null) return typed(['call', '$__str_substring', asI64(emit(str)), asI32(emit(start)), asI32(emit(end))], 'f64')
1181
1499
  const t = temp('t')
@@ -1183,13 +1501,13 @@ export default (ctx) => {
1183
1501
  ['local.set', `$${t}`, asF64(emit(str))],
1184
1502
  ['call', '$__str_substring', ['i64.reinterpret_f64', ['local.get', `$${t}`]], asI32(emit(start)),
1185
1503
  ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]], 'f64')
1186
- }
1504
+ })
1187
1505
 
1188
1506
  // .substr(start, length) — Annex B / legacy. Equivalent to substring(start, start+length).
1189
1507
  // __str_substring clamps end to byteLen and start/end to [0, byteLen], so negative
1190
1508
  // values are floored to 0 (matches v8 for length<0 → empty; for start<0 spec wants
1191
1509
  // max(0, len+start), which we don't implement — rare in practice).
1192
- ctx.core.emit['.substr'] = (str, start, length) => {
1510
+ bind('.substr', (str, start, length) => {
1193
1511
  inc('__str_substring')
1194
1512
  if (length != null) {
1195
1513
  const s = tempI32('substrS')
@@ -1205,15 +1523,6 @@ export default (ctx) => {
1205
1523
  ['local.set', `$${t}`, asF64(emit(str))],
1206
1524
  ['call', '$__str_substring', ['i64.reinterpret_f64', ['local.get', `$${t}`]], asI32(emit(start)),
1207
1525
  ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]], 'f64')
1208
- }
1209
-
1210
- // Declarative emitter for regular `(recv, ...args) → call($stdlib, coerced...)` methods.
1211
- // `coerce` is a per-argument code string ('I' i64 · 'F' f64 · 'i' i32); index 0 is the receiver.
1212
- // `ret` is the helper's result type — 'f64' is returned directly, 'i32' is widened to the f64 ABI.
1213
- const CO = { I: asI64, F: asF64, i: asI32 }
1214
- const method = (stdlib, coerce, ret = 'f64') => emitter([stdlib], (...nodes) => {
1215
- const c = ['call', `$${stdlib}`, ...nodes.map((n, i) => CO[coerce[i]](emit(n)))]
1216
- return typed(ret === 'i32' ? ['f64.convert_i32_s', c] : c, 'f64')
1217
1526
  })
1218
1527
 
1219
1528
  // Search args go through ToString per spec — coerce non-string-typed args
@@ -1231,30 +1540,92 @@ export default (ctx) => {
1231
1540
  }
1232
1541
  return typed(['f64.convert_i32_s', ['call', `$${name}`, asI64(emit(str)), sfxArg]], 'f64')
1233
1542
  }
1234
- ctx.core.emit['.startsWith'] = stringSearchMethod('__str_startswith')
1235
- ctx.core.emit['.endsWith'] = stringSearchMethod('__str_endswith')
1236
- ctx.core.emit['.trim'] = method('__str_trim', 'I')
1237
- ctx.core.emit['.trimStart'] = method('__str_trimStart', 'I')
1238
- ctx.core.emit['.trimEnd'] = method('__str_trimEnd', 'I')
1239
- ctx.core.emit['.repeat'] = method('__str_repeat', 'Ii')
1240
- ctx.core.emit['.split'] = method('__str_split', 'II')
1241
- ctx.core.emit['.replace'] = method('__str_replace', 'III')
1242
- ctx.core.emit['.replaceAll'] = method('__str_replaceall', 'III')
1243
-
1244
- ctx.core.emit['.toUpperCase'] = (str) => {
1245
- inc('__str_case')
1246
- return typed(['call', '$__str_case', asI64(emit(str)), ['i32.const', 97], ['i32.const', 122], ['i32.const', -32]], 'f64')
1247
- }
1543
+ bind('.startsWith', stringSearchMethod('__str_startswith'))
1544
+ bind('.endsWith', stringSearchMethod('__str_endswith'))
1545
+ bind('.trim', method('__str_trim', 'I'))
1546
+ bind('.trimStart', method('__str_trimStart', 'I'))
1547
+ bind('.trimEnd', method('__str_trimEnd', 'I'))
1548
+ bind('.repeat', method('__str_repeat', 'Ii'))
1549
+ // split(sep, limit): both args are optional.
1550
+ // - No args (undefined sep) → [str]: JS spec step 3 treats undefined separator
1551
+ // as returning a single-element array of the whole string (not splitting at all).
1552
+ // - 1 arg → no limit (sentinel 0x7fffffff = MAX_I32).
1553
+ // - 2 args → honour limit: 0 → [], N at most N pieces.
1554
+ bind('.split', (str, sep, limit) => {
1555
+ if (sep === undefined) {
1556
+ // split() → [str]: wrap the whole string in a 1-element array
1557
+ inc('__wrap1')
1558
+ return typed(['call', '$__wrap1', asI64(emit(str))], 'f64')
1559
+ }
1560
+ inc('__str_split')
1561
+ const limitIR = limit === undefined
1562
+ ? ['i32.const', 0x7fffffff]
1563
+ : ['i32.trunc_sat_f64_u', asF64(emit(limit))]
1564
+ return typed(['call', '$__str_split', asI64(emit(str)), asI64(emit(sep)), limitIR], 'f64')
1565
+ })
1248
1566
 
1249
- ctx.core.emit['.toLowerCase'] = (str) => {
1567
+ // replace(search, replacement). When `replacement` is a function, replace the
1568
+ // FIRST occurrence of the (string) search with ToString(fn(match)) — per spec a
1569
+ // string search matches once and the callback receives the matched substring.
1570
+ // (A regex search routes to `.string:replace` in the regex module, which also
1571
+ // handles the /g loop.) Without a closure runtime we can't invoke the callback,
1572
+ // so fall back to a clear error rather than silent data loss.
1573
+ bind('.replace', (str, search, repl) => {
1574
+ if (valTypeOf(repl) === VAL.CLOSURE) {
1575
+ if (!ctx.closure?.call) err('.replace(search, fn): no closure runtime available for the callback form')
1576
+ inc('__str_indexof', '__str_slice', '__str_concat', '__str_byteLen', '__to_str')
1577
+ const s = temp('rps'), q = temp('rpq'), fnL = temp('rpf')
1578
+ const idx = tempI32('rpi'), mlen = tempI32('rpm')
1579
+ const sI64 = () => ['i64.reinterpret_f64', ['local.get', `$${s}`]]
1580
+ const match = typed(['call', '$__str_slice', sI64(), ['local.get', `$${idx}`],
1581
+ ['i32.add', ['local.get', `$${idx}`], ['local.get', `$${mlen}`]]], 'f64')
1582
+ const repIR = ['call', '$__to_str', asI64(ctx.closure.call(typed(['local.get', `$${fnL}`], 'f64'), [match]))]
1583
+ const head = typed(['call', '$__str_slice', sI64(), ['i32.const', 0], ['local.get', `$${idx}`]], 'f64')
1584
+ const tail = typed(['call', '$__str_slice', sI64(),
1585
+ ['i32.add', ['local.get', `$${idx}`], ['local.get', `$${mlen}`]],
1586
+ ['call', '$__str_byteLen', sI64()]], 'f64')
1587
+ return typed(['block', ['result', 'f64'],
1588
+ ['local.set', `$${s}`, asF64(emit(str))],
1589
+ ['local.set', `$${q}`, asF64(emit(search))],
1590
+ ['local.set', `$${fnL}`, asF64(emit(repl))],
1591
+ ['local.set', `$${mlen}`, ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${q}`]]]],
1592
+ ['local.set', `$${idx}`, ['call', '$__str_indexof', sI64(), ['i64.reinterpret_f64', ['local.get', `$${q}`]], ['i32.const', 0]]],
1593
+ ['if', ['result', 'f64'], ['i32.lt_s', ['local.get', `$${idx}`], ['i32.const', 0]],
1594
+ ['then', ['local.get', `$${s}`]],
1595
+ ['else', typed(['call', '$__str_concat',
1596
+ asI64(typed(['call', '$__str_concat', asI64(head), repIR], 'f64')),
1597
+ asI64(tail)], 'f64')]]], 'f64')
1598
+ }
1599
+ inc('__str_replace')
1600
+ // search/repl ToString'd at the call site (searchArg) — __str_replace's __str_indexof
1601
+ // no longer coerces internally, so a non-string search must be stringified here.
1602
+ return typed(['call', '$__str_replace', asI64(emit(str)), searchArg(search), strReplArg(repl)], 'f64')
1603
+ })
1604
+ bind('.replaceAll', (str, search, repl) => {
1605
+ inc('__str_replaceall')
1606
+ return typed(['call', '$__str_replaceall', asI64(emit(str)), searchArg(search), strReplArg(repl)], 'f64')
1607
+ })
1608
+
1609
+ const caseMethod = (lo, hi, delta) => (str) => {
1250
1610
  inc('__str_case')
1251
- return typed(['call', '$__str_case', asI64(emit(str)), ['i32.const', 65], ['i32.const', 90], ['i32.const', 32]], 'f64')
1611
+ return typed(['call', '$__str_case', asI64(emit(str)), ['i32.const', lo], ['i32.const', hi], ['i32.const', delta]], 'f64')
1252
1612
  }
1613
+ bind('.toUpperCase', caseMethod(97, 122, -32))
1614
+ const _toLowerCase = caseMethod(65, 90, 32)
1615
+ bind('.toLowerCase', _toLowerCase)
1253
1616
 
1254
1617
  // Locale-specific casing needs ICU/CLDR data. jz intentionally has no
1255
1618
  // runtime, so this follows the existing ASCII-only lowercase helper and
1256
1619
  // ignores optional locale arguments.
1257
- ctx.core.emit['.toLocaleLowerCase'] = ctx.core.emit['.toLowerCase']
1620
+ bind('.toLocaleLowerCase', _toLowerCase)
1621
+
1622
+ const padMethod = (start) => (str, len, pad) => {
1623
+ inc('__str_pad')
1624
+ const vpad = pad != null ? asI64(emit(pad)) : ['i64.reinterpret_f64', mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1, 32)]
1625
+ return typed(['call', '$__str_pad', asI64(emit(str)), asI32(emit(len)), vpad, ['i32.const', start]], 'f64')
1626
+ }
1627
+ bind('.padStart', padMethod(1))
1628
+ bind('.padEnd', padMethod(0))
1258
1629
 
1259
1630
  // Byte-wise variant of String.prototype.localeCompare. Returns -1/0/1 from
1260
1631
  // an unsigned byte-by-byte compare with shorter-string-sorts-first tiebreak.
@@ -1263,21 +1634,21 @@ export default (ctx) => {
1263
1634
  // the spec exactly; for non-ASCII it follows UTF-8 byte order, which is
1264
1635
  // codepoint order for well-formed strings — close enough for sort-stability
1265
1636
  // use cases, wrong for human-language collation.
1266
- ctx.core.emit['.localeCompare'] = method('__str_cmp', 'II', 'i32')
1637
+ bind('.localeCompare', method('__str_cmp', 'II', 'i32'))
1267
1638
 
1268
- ctx.core.emit['.string:concat'] = (str, ...others) => {
1639
+ bind('.string:concat', (str, ...others) => {
1269
1640
  inc('__str_concat')
1270
1641
  let result = asF64(emit(str))
1271
1642
  for (const other of others) result = typed(['call', '$__str_concat', ['i64.reinterpret_f64', result], asI64(emit(other))], 'f64')
1272
1643
  return result
1273
- }
1644
+ })
1274
1645
 
1275
1646
  // A VAL.BOOL part rides the 0/1 carrier, so __to_str would render "1"/"0".
1276
- // emitBoolStr selects the interned "true"/"false" literal (constant-folded
1647
+ // bool selects the interned "true"/"false" literal (constant-folded
1277
1648
  // when the operand is known); every other part goes through __to_str.
1278
- const partStrI64 = (p) => valTypeOf(p) === VAL.BOOL ? asI64(emitBoolStr(p)) : toStrI64(p, emit(p))
1649
+ const partStrI64 = (p) => valTypeOf(p) === VAL.BOOL ? asI64(bool(p)) : toStrI64(p, emit(p))
1279
1650
 
1280
- ctx.core.emit['strcat'] = (...parts) => {
1651
+ bind('strcat', (...parts) => {
1281
1652
  inc('__to_str', '__str_byteLen', '__alloc', '__mkptr', '__str_copy')
1282
1653
  if (!parts.length) return mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT, 0)
1283
1654
  if (parts.length === 1) return typed(['f64.reinterpret_i64', partStrI64(parts[0])], 'f64')
@@ -1311,29 +1682,33 @@ export default (ctx) => {
1311
1682
  ['then', mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT, 0)],
1312
1683
  ['else', ['block', ['result', 'f64'], ...alloc]]])
1313
1684
  return typed(['block', ['result', 'f64'], ...ir], 'f64')
1314
- }
1315
-
1316
- ctx.core.emit['.padStart'] = (str, len, pad) => {
1317
- inc('__str_pad')
1318
- const vpad = pad != null ? asI64(emit(pad)) : ['i64.reinterpret_f64', mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1, 32)]
1319
- return typed(['call', '$__str_pad', asI64(emit(str)), asI32(emit(len)), vpad, ['i32.const', 1]], 'f64')
1320
- }
1321
-
1322
- ctx.core.emit['.padEnd'] = (str, len, pad) => {
1323
- inc('__str_pad')
1324
- const vpad = pad != null ? asI64(emit(pad)) : ['i64.reinterpret_f64', mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1, 32)]
1325
- return typed(['call', '$__str_pad', asI64(emit(str)), asI32(emit(len)), vpad, ['i32.const', 0]], 'f64')
1326
- }
1685
+ })
1327
1686
 
1328
- // .charAt(i) → 1-char string from char code at index i
1329
- ctx.core.emit['.charAt'] = (str, idx) => {
1330
- inc('__char_at')
1331
- const t = tempI32('ch')
1332
- // Get char code, create SSO string with 1 byte
1687
+ // Shared: `$iLocal` in `[0, lenIR)`a 1-byte SSO string of the char at that
1688
+ // index; otherwise `oobIR`. Without the bounds check `__char_at` returns 0 for an
1689
+ // out-of-range index, which would wrap to a bogus `"\x00"` string (charAt → "",
1690
+ // at undefined). `sLocal` is the receiver as an f64 local.
1691
+ const charAtOr = (sLocal, iLocal, lenIR, oobIR) =>
1692
+ ['if', ['result', 'f64'],
1693
+ ['i32.and',
1694
+ ['i32.ge_s', ['local.get', iLocal], ['i32.const', 0]],
1695
+ ['i32.lt_s', ['local.get', iLocal], lenIR]],
1696
+ ['then', mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1,
1697
+ ['call', '$__char_at', ['i64.reinterpret_f64', ['local.get', sLocal]], ['local.get', iLocal]])],
1698
+ ['else', oobIR]]
1699
+ const emptyStr = mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT, 0)
1700
+
1701
+ // .charAt(i) → 1-char string at index i, or "" when out of range (JS spec: no
1702
+ // negative-index wraparound — a negative or >=length index yields "").
1703
+ bind('.charAt', (str, idx) => {
1704
+ inc('__char_at', '__str_byteLen')
1705
+ const s = temp('cs'), i = tempI32('ci')
1333
1706
  return typed(['block', ['result', 'f64'],
1334
- ['local.set', `$${t}`, ['call', '$__char_at', asI64(emit(str)), asI32(emit(idx))]],
1335
- mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1, ['local.get', `$${t}`])], 'f64')
1336
- }
1707
+ ['local.set', `$${s}`, asF64(emit(str))],
1708
+ ['local.set', `$${i}`, asI32(emit(idx))],
1709
+ charAtOr(`$${s}`, `$${i}`,
1710
+ ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${s}`]]], emptyStr)], 'f64')
1711
+ })
1337
1712
 
1338
1713
  // .charCodeAt(i) → JS-spec char code: the UTF-16 code unit at `i`, or NaN
1339
1714
  // when `i` is out of range (`i < 0 || i >= length`). Result is f64 because
@@ -1342,32 +1717,50 @@ export default (ctx) => {
1342
1717
  // loop `while ((cc = s.charCodeAt(i++)) <= 32) {}` would never terminate
1343
1718
  // (`0 <= 32` is true, `NaN <= 32` is false). The narrower may re-narrow the
1344
1719
  // result to i32 where it can prove the index in-bounds.
1345
- ctx.core.emit['.charCodeAt'] = (str, idx) =>
1346
- typed(ctx.abi.string.ops.charCodeAt(asF64(emit(str)), asI32(emit(idx)), ctx, true), 'f64')
1720
+ bind('.charCodeAt', (str, idx) =>
1721
+ typed(ctx.abi.string.ops.charCodeAt(asF64(emit(str)), asI32(emit(idx)), ctx, true), 'f64'))
1722
+
1723
+ // String.prototype.codePointAt(i) — byte-indexed.
1724
+ // jz strings are UTF-8 byte-arrays; `i` is a byte offset, not a UTF-16 code-unit
1725
+ // index. For ASCII inputs (U+0000..U+007F) the result is the exact Unicode code
1726
+ // point. For multi-byte sequences the result is the value of the leading byte only
1727
+ // (not a full code-point decode). Out-of-range → undefined (NaN-boxed). This is a
1728
+ // documented byte-semantics limitation; it keeps the implementation allocation-free
1729
+ // and consistent with jz's byte-indexed string model throughout.
1730
+ bind('.codePointAt', (str, idx) =>
1731
+ typed(ctx.abi.string.ops.charCodeAt(asF64(emit(str)), asI32(emit(idx)), ctx, true), 'f64'))
1347
1732
 
1348
1733
  // String.fromCharCode(code) → 1-char SSO string
1349
- ctx.core.emit['String'] = (value) => {
1734
+ bind('String', (value) => {
1350
1735
  if (value === undefined) return emit(['str', ''])
1351
1736
  if (valTypeOf(value) === VAL.STRING) return emit(value)
1352
- if (valTypeOf(value) === VAL.BOOL) return emitBoolStr(value)
1737
+ if (valTypeOf(value) === VAL.BOOL) return bool(value)
1353
1738
  if (valTypeOf(value) === VAL.NUMBER) {
1354
1739
  inc('__ftoa')
1355
1740
  return typed(['call', '$__ftoa', asF64(emit(value)), ['i32.const', 0], ['i32.const', 0]], 'f64')
1356
1741
  }
1357
1742
  return typed(['f64.reinterpret_i64', toStrI64(value, emit(value))], 'f64')
1358
- }
1743
+ })
1359
1744
 
1360
- ctx.core.emit['String.fromCharCode'] = (code) => {
1361
- if (code === undefined) return emit(['str', ''])
1745
+ // String.fromCharCode(...codes) variadic; each arg is ToUint16(ToNumber(code))
1746
+ // a 1-byte string, concatenated left to right (mirrors String.fromCodePoint).
1747
+ bind('String.fromCharCode', (...codes) => {
1748
+ if (codes.length === 0) return emit(['str', ''])
1362
1749
  // ToUint16(ToNumber(code)): `toNumF64` performs ToPrimitive on an object
1363
1750
  // argument, so a throwing valueOf/toString propagates per spec.
1364
- return mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1, asI32(toNumF64(code, emit(code))))
1365
- }
1751
+ const one = (node) => mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1, asI32(toNumF64(node, emit(node))))
1752
+ let r = one(codes[0])
1753
+ for (let i = 1; i < codes.length; i++) {
1754
+ inc('__str_concat_raw')
1755
+ r = typed(['call', '$__str_concat_raw', asI64(r), asI64(one(codes[i]))], 'f64')
1756
+ }
1757
+ return r
1758
+ })
1366
1759
 
1367
1760
  // String.fromCodePoint(cp) → UTF-8 encoded string for one code point.
1368
1761
  // Param is f64 (already ToNumber-coerced); throws RangeError ($__jz_err) when
1369
1762
  // the value is not an integer in [0, 0x10FFFF] (22.1.2.2 step 5.d).
1370
- ctx.core.stdlib['__fromCodePoint'] = `(func $__fromCodePoint (param $cpf f64) (result f64)
1763
+ wat('__fromCodePoint', `(func $__fromCodePoint (param $cpf f64) (result f64)
1371
1764
  (local $cp i32) (local $off i32) (local $len i32)
1372
1765
  (if (i32.or
1373
1766
  (i32.or
@@ -1398,11 +1791,11 @@ export default (ctx) => {
1398
1791
  (i32.or (i32.const 0xF0) (i32.shr_u (local.get $cp) (i32.const 18)))
1399
1792
  (i32.shl (i32.or (i32.const 0x80) (i32.and (i32.shr_u (local.get $cp) (i32.const 12)) (i32.const 0x3F))) (i32.const 8)))
1400
1793
  (i32.shl (i32.or (i32.const 0x80) (i32.and (i32.shr_u (local.get $cp) (i32.const 6)) (i32.const 0x3F))) (i32.const 16)))
1401
- (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 24))))))`
1794
+ (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 24))))))`)
1402
1795
 
1403
1796
  // String.fromCodePoint(...codePoints) — variadic; each arg is ToNumber-coerced
1404
1797
  // then validated/encoded by __fromCodePoint, results concatenated left to right.
1405
- ctx.core.emit['String.fromCodePoint'] = (...codes) => {
1798
+ bind('String.fromCodePoint', (...codes) => {
1406
1799
  if (codes.length === 0) return emit(['str', ''])
1407
1800
  ctx.runtime.throws = true
1408
1801
  inc('__fromCodePoint')
@@ -1414,9 +1807,9 @@ export default (ctx) => {
1414
1807
  r = typed(['call', '$__str_concat_raw', asI64(r), asI64(one(codes[i]))], 'f64')
1415
1808
  }
1416
1809
  return r
1417
- }
1810
+ })
1418
1811
 
1419
- ctx.core.stdlib['__encodeURIComponent'] = `(func $__encodeURIComponent (param $val i64) (result f64)
1812
+ wat('__encodeURIComponent', `(func $__encodeURIComponent (param $val i64) (result f64)
1420
1813
  (local $str i64) (local $slen i32) (local $base i32) (local $out i32)
1421
1814
  (local $i i32) (local $j i32) (local $c i32) (local $hi i32) (local $lo i32)
1422
1815
  (local.set $str (call $__to_str (local.get $val)))
@@ -1458,24 +1851,24 @@ export default (ctx) => {
1458
1851
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
1459
1852
  (br $loop)))
1460
1853
  (i32.store (local.get $base) (local.get $j))
1461
- (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $out)))`
1854
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $out)))`)
1462
1855
 
1463
- ctx.core.emit['encodeURIComponent'] = (value) => {
1856
+ bind('encodeURIComponent', (value) => {
1464
1857
  inc('__encodeURIComponent')
1465
1858
  const input = value === undefined ? ['i64.const', UNDEF_NAN] : asI64(emit(value))
1466
1859
  return typed(['call', '$__encodeURIComponent', input], 'f64')
1467
- }
1860
+ })
1468
1861
 
1469
- ctx.core.stdlib['__uri_hex'] = `(func $__uri_hex (param $c i32) (result i32)
1862
+ wat('__uri_hex', `(func $__uri_hex (param $c i32) (result i32)
1470
1863
  (if (result i32) (i32.and (i32.ge_u (local.get $c) (i32.const 48)) (i32.le_u (local.get $c) (i32.const 57)))
1471
1864
  (then (i32.sub (local.get $c) (i32.const 48)))
1472
1865
  (else (if (result i32) (i32.and (i32.ge_u (local.get $c) (i32.const 65)) (i32.le_u (local.get $c) (i32.const 70)))
1473
1866
  (then (i32.sub (local.get $c) (i32.const 55)))
1474
1867
  (else (if (result i32) (i32.and (i32.ge_u (local.get $c) (i32.const 97)) (i32.le_u (local.get $c) (i32.const 102)))
1475
1868
  (then (i32.sub (local.get $c) (i32.const 87)))
1476
- (else (i32.const -1))))))))`
1869
+ (else (i32.const -1))))))))`)
1477
1870
 
1478
- ctx.core.stdlib['__decodeURIComponent'] = `(func $__decodeURIComponent (param $v i64) (result f64)
1871
+ wat('__decodeURIComponent', `(func $__decodeURIComponent (param $v i64) (result f64)
1479
1872
  (local $s i64) (local $len i32) (local $i i32)
1480
1873
  (local $base i32) (local $dst i32) (local $outLen i32)
1481
1874
  (local $c i32) (local $hi i32) (local $lo i32)
@@ -1551,38 +1944,40 @@ export default (ctx) => {
1551
1944
  (local.set $outLen (i32.add (local.get $outLen) (i32.const 1)))))
1552
1945
  (br $loop)))
1553
1946
  (i32.store (local.get $base) (local.get $outLen))
1554
- (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $dst)))`
1947
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $dst)))`)
1555
1948
 
1556
- ctx.core.emit['decodeURIComponent'] = (value) => {
1949
+ bind('decodeURIComponent', (value) => {
1557
1950
  ctx.runtime.throws = true
1558
1951
  inc('__decodeURIComponent')
1559
1952
  return typed(['call', '$__decodeURIComponent',
1560
1953
  value === undefined ? ['i64.const', UNDEF_NAN] : asI64(emit(value))], 'f64')
1561
- }
1954
+ })
1562
1955
 
1563
- // .at(i) → charAt with negative index support
1564
- ctx.core.emit['.string:at'] = (str, idx) => {
1956
+ // .at(i) → 1-char string at index i with negative-index support, or undefined
1957
+ // when out of range (JS spec: `i += length` for negative, then OOB → undefined).
1958
+ bind('.string:at', (str, idx) => {
1565
1959
  inc('__char_at', '__str_byteLen')
1566
- const t = tempI32('at'), s = temp('as')
1960
+ const t = tempI32('at'), s = temp('as'), len = tempI32('al')
1567
1961
  return typed(['block', ['result', 'f64'],
1568
1962
  ['local.set', `$${s}`, asF64(emit(str))],
1963
+ ['local.set', `$${len}`, ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${s}`]]]],
1569
1964
  ['local.set', `$${t}`, asI32(emit(idx))],
1570
1965
  // Negative index: t += length
1571
1966
  ['if', ['i32.lt_s', ['local.get', `$${t}`], ['i32.const', 0]],
1572
- ['then', ['local.set', `$${t}`, ['i32.add', ['local.get', `$${t}`],
1573
- ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${s}`]]]]]]],
1574
- mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1, ['call', '$__char_at', ['i64.reinterpret_f64', ['local.get', `$${s}`]], ['local.get', `$${t}`]])], 'f64')
1575
- }
1967
+ ['then', ['local.set', `$${t}`, ['i32.add', ['local.get', `$${t}`], ['local.get', `$${len}`]]]]],
1968
+ charAtOr(`$${s}`, `$${t}`, ['local.get', `$${len}`],
1969
+ ['f64.reinterpret_i64', ['i64.const', UNDEF_NAN]])], 'f64')
1970
+ })
1576
1971
 
1577
1972
  // .search(str) → indexOf (same as indexOf for string args)
1578
- ctx.core.emit['.search'] = (str, search) => {
1973
+ bind('.search', (str, search) => {
1579
1974
  inc('__str_indexof')
1580
1975
  return typed(['f64.convert_i32_s', ['call', '$__str_indexof', asI64(emit(str)), asI64(emit(search)), ['i32.const', 0]]], 'f64')
1581
- }
1976
+ })
1582
1977
 
1583
1978
  // .match(str) → [match] array if found, or 0 (null) if not
1584
1979
  // For string args, returns single-element array with the matched substring
1585
- ctx.core.emit['.match'] = (str, search) => {
1980
+ bind('.match', (str, search) => {
1586
1981
  inc('__str_indexof', '__str_slice', '__wrap1')
1587
1982
  const s = temp('ms'), q = temp('mq'), idx = tempI32('mi')
1588
1983
  // indexOf, then if >= 0, create 1-element array with the match slice
@@ -1599,24 +1994,24 @@ export default (ctx) => {
1599
1994
  ['call', '$__str_slice', ['i64.reinterpret_f64', ['local.get', `$${s}`]],
1600
1995
  ['local.get', `$${idx}`],
1601
1996
  ['i32.add', ['local.get', `$${idx}`], ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${q}`]]]]]]]]]], 'f64')
1602
- }
1997
+ })
1603
1998
 
1604
1999
  // __wrap1(val: i64) → f64 — create 1-element array [val]
1605
- ctx.core.stdlib['__wrap1'] = `(func $__wrap1 (param $val i64) (result f64)
2000
+ wat('__wrap1', `(func $__wrap1 (param $val i64) (result f64)
1606
2001
  (local $ptr i32)
1607
2002
  (local.set $ptr (call $__alloc (i32.const 16)))
1608
2003
  (i32.store (local.get $ptr) (i32.const 1))
1609
2004
  (i32.store (i32.add (local.get $ptr) (i32.const 4)) (i32.const 1))
1610
2005
  (i64.store (i32.add (local.get $ptr) (i32.const 8)) (local.get $val))
1611
- (call $__mkptr (i32.const 1) (i32.const 0) (i32.add (local.get $ptr) (i32.const 8))))`
2006
+ (call $__mkptr (i32.const 1) (i32.const 0) (i32.add (local.get $ptr) (i32.const 8))))`)
1612
2007
 
1613
2008
  // TextEncoder() / TextDecoder() → dummy values (methods do the work)
1614
- ctx.core.emit['TextEncoder'] = () => typed(['f64.const', 1], 'f64')
1615
- ctx.core.emit['TextDecoder'] = () => typed(['f64.const', 2], 'f64')
2009
+ bind('TextEncoder', () => typed(['f64.const', 1], 'f64'))
2010
+ bind('TextDecoder', () => typed(['f64.const', 2], 'f64'))
1616
2011
 
1617
2012
  // .encode(str) → Uint8Array of string's UTF-8 bytes
1618
2013
  // Copies bytes from string (SSO or heap) into a new Uint8Array
1619
- ctx.core.stdlib['__str_encode'] = `(func $__str_encode (param $str i64) (result f64)
2014
+ wat('__str_encode', `(func $__str_encode (param $str i64) (result f64)
1620
2015
  (local $len i32) (local $dst i32)
1621
2016
  (local.set $len (call $__str_byteLen (local.get $str)))
1622
2017
  (local.set $dst (call $__alloc (i32.add (i32.const 8) (local.get $len))))
@@ -1624,9 +2019,9 @@ export default (ctx) => {
1624
2019
  (i32.store (i32.add (local.get $dst) (i32.const 4)) (local.get $len))
1625
2020
  (local.set $dst (i32.add (local.get $dst) (i32.const 8)))
1626
2021
  (call $__str_copy (local.get $str) (local.get $dst) (local.get $len))
1627
- (call $__mkptr (i32.const 3) (i32.const 1) (local.get $dst)))`
2022
+ (call $__mkptr (i32.const 3) (i32.const 1) (local.get $dst)))`)
1628
2023
 
1629
- ctx.core.emit['.encode'] = (obj, str) => {
2024
+ bind('.encode', (obj, str) => {
1630
2025
  inc('__str_encode')
1631
2026
  // .encode() yields a runtime PTR.TYPED/u8 array (see __mkptr above). Downstream
1632
2027
  // indexing/spread dispatch through __typed_idx, whose element-unaware fallback
@@ -1634,10 +2029,10 @@ export default (ctx) => {
1634
2029
  // the feature pulls the element-aware variant — same invariant `.length` follows.
1635
2030
  ctx.features.typedarray = true
1636
2031
  return typed(['call', '$__str_encode', asI64(emit(str))], 'f64')
1637
- }
2032
+ })
1638
2033
 
1639
2034
  // .decode(uint8arr) → string from byte data
1640
- ctx.core.stdlib['__bytes_decode'] = `(func $__bytes_decode (param $arr i64) (result f64)
2035
+ wat('__bytes_decode', `(func $__bytes_decode (param $arr i64) (result f64)
1641
2036
  (local $off i32) (local $len i32) (local $dst i32)
1642
2037
  (local.set $off (call $__ptr_offset (local.get $arr)))
1643
2038
  (local.set $len (call $__len (local.get $arr)))
@@ -1645,10 +2040,10 @@ export default (ctx) => {
1645
2040
  (i32.store (local.get $dst) (local.get $len))
1646
2041
  (local.set $dst (i32.add (local.get $dst) (i32.const 4)))
1647
2042
  (memory.copy (local.get $dst) (local.get $off) (local.get $len))
1648
- (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $dst)))`
2043
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $dst)))`)
1649
2044
 
1650
- ctx.core.emit['.decode'] = (obj, arr) => {
2045
+ bind('.decode', (obj, arr) => {
1651
2046
  inc('__bytes_decode')
1652
2047
  return typed(['call', '$__bytes_decode', asI64(emit(arr))], 'f64')
1653
- }
2048
+ })
1654
2049
  }