jz 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +288 -314
  2. package/bench/README.md +319 -0
  3. package/bench/bench.svg +112 -0
  4. package/cli.js +32 -24
  5. package/index.js +177 -55
  6. package/interop.js +88 -159
  7. package/jz.svg +5 -0
  8. package/jzify/arguments.js +97 -0
  9. package/jzify/bundler.js +382 -0
  10. package/jzify/classes.js +328 -0
  11. package/jzify/hoist-vars.js +177 -0
  12. package/jzify/index.js +51 -0
  13. package/jzify/names.js +37 -0
  14. package/jzify/switch.js +106 -0
  15. package/jzify/transform.js +349 -0
  16. package/layout.js +179 -0
  17. package/module/array.js +322 -153
  18. package/module/collection.js +603 -145
  19. package/module/console.js +55 -43
  20. package/module/core.js +266 -153
  21. package/module/date.js +15 -3
  22. package/module/function.js +73 -6
  23. package/module/index.js +2 -1
  24. package/module/json.js +226 -61
  25. package/module/math.js +414 -186
  26. package/module/number.js +306 -60
  27. package/module/object.js +448 -184
  28. package/module/regex.js +255 -25
  29. package/module/schema.js +24 -6
  30. package/module/simd.js +85 -0
  31. package/module/string.js +586 -220
  32. package/module/symbol.js +1 -1
  33. package/module/timer.js +9 -14
  34. package/module/typedarray.js +45 -48
  35. package/package.json +41 -12
  36. package/src/abi/index.js +39 -23
  37. package/src/abi/string.js +38 -41
  38. package/src/ast.js +460 -0
  39. package/src/autoload.js +26 -24
  40. package/src/bridge.js +111 -0
  41. package/src/compile/analyze-scans.js +661 -0
  42. package/src/compile/analyze.js +1565 -0
  43. package/src/compile/emit-assign.js +408 -0
  44. package/src/compile/emit.js +3201 -0
  45. package/src/compile/flow-types.js +103 -0
  46. package/src/{compile.js → compile/index.js} +497 -125
  47. package/src/{infer.js → compile/infer.js} +27 -98
  48. package/src/{narrow.js → compile/narrow.js} +302 -96
  49. package/src/compile/plan/advise.js +316 -0
  50. package/src/compile/plan/common.js +150 -0
  51. package/src/compile/plan/index.js +118 -0
  52. package/src/compile/plan/inline.js +679 -0
  53. package/src/compile/plan/literals.js +984 -0
  54. package/src/compile/plan/loops.js +472 -0
  55. package/src/compile/plan/scope.js +573 -0
  56. package/src/compile/program-facts.js +404 -0
  57. package/src/ctx.js +176 -58
  58. package/src/ir.js +540 -171
  59. package/src/kind-traits.js +105 -0
  60. package/src/kind.js +462 -0
  61. package/src/op-policy.js +57 -0
  62. package/src/{optimize.js → optimize/index.js} +1106 -446
  63. package/src/optimize/vectorize.js +1874 -0
  64. package/src/param-reps.js +65 -0
  65. package/src/parse.js +44 -0
  66. package/src/{prepare.js → prepare/index.js} +589 -202
  67. package/src/reps.js +115 -0
  68. package/src/resolve.js +12 -3
  69. package/src/static.js +199 -0
  70. package/src/type.js +647 -0
  71. package/src/{assemble.js → wat/assemble.js} +86 -48
  72. package/src/wat/optimize.js +3760 -0
  73. package/transform.js +21 -0
  74. package/wasi.js +47 -5
  75. package/src/analyze.js +0 -3818
  76. package/src/emit.js +0 -3040
  77. package/src/jzify.js +0 -1580
  78. package/src/plan.js +0 -2132
  79. package/src/vectorize.js +0 -1088
  80. /package/src/{codegen.js → wat/codegen.js} +0 -0
package/module/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
117
  __str_indexof: ['__str_byteLen', '__to_str'],
118
+ __str_lastindexof: ['__str_byteLen', '__to_str'],
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'],
@@ -78,7 +149,7 @@ export default (ctx) => {
78
149
 
79
150
  // === String literal: "abc" → SSO if ≤4 ASCII, else heap ===
80
151
 
81
- ctx.core.emit['str'] = (str) => {
152
+ bind('str', (str) => {
82
153
  const MAX_SSO = 4
83
154
  if (ctx.features.sso && str.length <= MAX_SSO && /^[\x00-\x7f]*$/.test(str)) {
84
155
  let packed = 0
@@ -88,16 +159,33 @@ export default (ctx) => {
88
159
  const bytes = new TextEncoder().encode(str)
89
160
  const len = bytes.length
90
161
  if (!ctx.memory.shared) {
91
- // Own memory: place in static data segment (no runtime allocation)
162
+ // Own memory: place in static data segment (no runtime allocation).
163
+ // Under internStrings the layout is [hash u32][len u32][bytes] and the
164
+ // pointer carries STR_INTERN_BIT: statics are CANONICAL (deduped), so
165
+ // unequal canonicals are bit-unequal (__str_eq short-circuit) and
166
+ // __str_hash loads the cached FNV at -8 instead of re-hashing. The len
167
+ // header stays at -4 either way — no other reader changes.
168
+ const interned = !!ctx.transform.optimize?.internStrings
169
+ const hdr = interned ? 8 : 4
170
+ const aux = interned ? STR_INTERN_BIT : 0
92
171
  if (!ctx.runtime.data) ctx.runtime.data = ''
93
172
  const prior = ctx.runtime.dataDedup.get(str)
94
- if (prior !== undefined) return mkPtrIR(PTR.STRING, 0, prior + 4)
173
+ if (prior !== undefined) return mkPtrIR(PTR.STRING, aux, prior + hdr)
95
174
  while (ctx.runtime.data.length % 4 !== 0) ctx.runtime.data += '\0'
96
175
  const offset = ctx.runtime.data.length
176
+ if (interned) {
177
+ // byte-FNV + clamp — must equal __str_hash's output exactly (it hashes
178
+ // UTF-8 bytes, then clamps ≤1 → +2 for the empty/tombstone sentinels)
179
+ let h = 0x811c9dc5 | 0
180
+ for (let i = 0; i < len; i++) h = Math.imul(h ^ bytes[i], 0x01000193) | 0
181
+ if (h <= 1) h = (h + 2) | 0
182
+ h = h >>> 0
183
+ ctx.runtime.data += String.fromCharCode(h & 0xFF, (h >> 8) & 0xFF, (h >> 16) & 0xFF, (h >> 24) & 0xFF)
184
+ }
97
185
  ctx.runtime.data += String.fromCharCode(len & 0xFF, (len >> 8) & 0xFF, (len >> 16) & 0xFF, (len >> 24) & 0xFF)
98
186
  for (let i = 0; i < len; i++) ctx.runtime.data += String.fromCharCode(bytes[i])
99
187
  ctx.runtime.dataDedup.set(str, offset)
100
- return mkPtrIR(PTR.STRING, 0, offset + 4)
188
+ return mkPtrIR(PTR.STRING, aux, offset + hdr)
101
189
  }
102
190
  // Shared memory: pack all string literals into one passive data segment with 4-byte
103
191
  // length prefixes. At __start, alloc the whole pool once and memory.init it in a single
@@ -105,7 +193,7 @@ export default (ctx) => {
105
193
  // use, independent of string length AND reused across uses.
106
194
  if (!ctx.runtime.strPool) {
107
195
  ctx.runtime.strPool = ''
108
- ctx.scope.globals.set('__strBase', '(global $__strBase (mut i32) (i32.const 0))')
196
+ declGlobal('__strBase', 'i32')
109
197
  }
110
198
  let off = ctx.runtime.strPoolDedup.get(str)
111
199
  if (off === undefined) {
@@ -116,23 +204,23 @@ export default (ctx) => {
116
204
  ctx.runtime.strPoolDedup.set(str, off)
117
205
  }
118
206
  return mkPtrIR(PTR.STRING, 0, ['i32.add', ['global.get', '$__strBase'], ['i32.const', off]])
119
- }
207
+ })
120
208
 
121
209
  // === WAT: char extraction ===
122
210
 
123
211
  // SSO/STRING ptrs never have forwarding pointers (only ARRAY does), so we extract
124
212
  // 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)
213
+ wat('__sso_char', `(func $__sso_char (param $ptr i64) (param $i i32) (result i32)
126
214
  (i32.and
127
215
  (i32.shr_u
128
216
  (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK})))
129
217
  (i32.mul (local.get $i) (i32.const 8)))
130
- (i32.const 0xFF)))`
218
+ (i32.const 0xFF)))`)
131
219
 
132
- ctx.core.stdlib['__str_char'] = `(func $__str_char (param $ptr i64) (param $i i32) (result i32)
220
+ wat('__str_char', `(func $__str_char (param $ptr i64) (param $i i32) (result i32)
133
221
  (i32.load8_u (i32.add
134
222
  (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK})))
135
- (local.get $i))))`
223
+ (local.get $i))))`)
136
224
 
137
225
  // Hot (~37M calls in watr self-host, ~40k/scan in tokenizer bench). Caller
138
226
  // guarantees $ptr is a STRING; SSO bit picks inline-byte-extract vs heap memory
@@ -149,7 +237,7 @@ export default (ctx) => {
149
237
  // load+bounds-check, beating call-site overhead. Repeated `i32.wrap_i64 +
150
238
  // i64.and OFFSET_MASK` subexpressions rely on CSE in the consumer; both
151
239
  // 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)
240
+ wat('__char_at', `(func $__char_at (param $ptr i64) (param $i i32) (result i32)
153
241
  (if (result i32)
154
242
  (i64.ne (i64.and (local.get $ptr) (i64.const ${SSO_BIT_I64})) (i64.const 0))
155
243
  (then
@@ -190,9 +278,9 @@ export default (ctx) => {
190
278
  (i32.load8_u
191
279
  (i32.add
192
280
  (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK})))
193
- (local.get $i)))))))))`
281
+ (local.get $i)))))))))`)
194
282
 
195
- ctx.core.stdlib['__str_idx'] = `(func $__str_idx (param $ptr i64) (param $i i32) (result f64)
283
+ wat('__str_idx', `(func $__str_idx (param $ptr i64) (param $i i32) (result f64)
196
284
  (local $t i32) (local $off i32) (local $len i32) (local $isSso i32)
197
285
  (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
286
  (local.set $off (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
@@ -221,21 +309,58 @@ export default (ctx) => {
221
309
  (f64.reinterpret_i64
222
310
  (i64.or
223
311
  ;; 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')})
312
+ (i64.const ${ptrNanHex(PTR.STRING, LAYOUT.SSO_BIT | 1)})
225
313
  (i64.extend_i32_u
226
314
  (if (result i32) (local.get $isSso)
227
315
  (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)))))))))))`
316
+ (else (i32.load8_u (i32.add (local.get $off) (local.get $i)))))))))))`)
229
317
 
230
318
  // Hot: ~53M calls in watr self-host. Bit-eq covers identity. SSO/SSO with !bit-eq
231
319
  // guarantees content differs (high 32 bits encode type+len; both equal → low 32 differs
232
320
  // ⇒ bytes differ). Heap/heap uses raw load8_u — no per-byte function calls.
233
321
  // 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)
322
+ // Hot/cold split: the prefix every comparison runs (bit-eq, both-SSO,
323
+ // both-canonical, heap length mismatch) is LOOP-FREE and small enough for
324
+ // the engine's wasm inliner — the call overhead disappears at every site
325
+ // while the byte-walk lives in __str_eq_cold. Mirrors the __ptr_offset_fwd
326
+ // split; a body containing a loop is excluded from V8's inliner.
327
+ wat('__str_eq', `(func $__str_eq (param $a i64) (param $b i64) (result i32)
328
+ (local $axA i32) (local $axB i32) (local $offA i32) (local $offB i32)
329
+ (if (i64.eq (local.get $a) (local.get $b))
330
+ (then (return (i32.const 1))))
331
+ (local.set $axA (i32.wrap_i64 (i64.shr_u (local.get $a) (i64.const ${LAYOUT.AUX_SHIFT}))))
332
+ (local.set $axB (i32.wrap_i64 (i64.shr_u (local.get $b) (i64.const ${LAYOUT.AUX_SHIFT}))))
333
+ ;; both SSO ⇒ bit-ne already decided content-ne
334
+ (if (i32.and
335
+ (i32.and (local.get $axA) (i32.const ${LAYOUT.SSO_BIT}))
336
+ (i32.and (local.get $axB) (i32.const ${LAYOUT.SSO_BIT})))
337
+ (then (return (i32.const 0))))
338
+ ;; both CANONICAL interned heap strings ⇒ bit-ne ⇒ content-ne
339
+ (if (i32.and
340
+ (i32.eq (i32.and (local.get $axA) (i32.const ${LAYOUT.SSO_BIT | LAYOUT.SLICE_BIT | 0x1})) (i32.const 0x1))
341
+ (i32.eq (i32.and (local.get $axB) (i32.const ${LAYOUT.SSO_BIT | LAYOUT.SLICE_BIT | 0x1})) (i32.const 0x1)))
342
+ (then (return (i32.const 0))))
343
+ ;; both PLAIN heap (not SSO, not slice): length mismatch exits without bytes
344
+ (if (i32.eqz (i32.or
345
+ (i32.and (i32.or (local.get $axA) (local.get $axB)) (i32.const ${LAYOUT.SSO_BIT | LAYOUT.SLICE_BIT}))
346
+ (i32.const 0)))
347
+ (then
348
+ (local.set $offA (i32.wrap_i64 (i64.and (local.get $a) (i64.const ${LAYOUT.OFFSET_MASK}))))
349
+ (local.set $offB (i32.wrap_i64 (i64.and (local.get $b) (i64.const ${LAYOUT.OFFSET_MASK}))))
350
+ (if (i32.and (i32.ge_u (local.get $offA) (i32.const 4)) (i32.ge_u (local.get $offB) (i32.const 4)))
351
+ (then
352
+ (if (i32.ne
353
+ (i32.load (i32.sub (local.get $offA) (i32.const 4)))
354
+ (i32.load (i32.sub (local.get $offB) (i32.const 4))))
355
+ (then (return (i32.const 0))))))))
356
+ (call $__str_eq_cold (local.get $a) (local.get $b)))`)
357
+
358
+ wat('__str_eq_cold', `(func $__str_eq_cold (param $a i64) (param $b i64) (result i32)
235
359
  (local $len i32) (local $lenB i32) (local $i i32)
236
360
  (local $ta i32) (local $tb i32)
237
361
  (local $offA i32) (local $offB i32)
238
362
  (local $ssoA i32) (local $ssoB i32)
363
+ (local $axA i32) (local $axB i32)
239
364
  (if (i64.eq (local.get $a) (local.get $b))
240
365
  (then (return (i32.const 1))))
241
366
  (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 +376,19 @@ export default (ctx) => {
251
376
  ;; Both SSO with !bit-eq ⇒ content differs (high 32 bits hold tag+aux; both equal here).
252
377
  (if (i32.and (local.get $ssoA) (local.get $ssoB))
253
378
  (then (return (i32.const 0))))
379
+ ;; Both CANONICAL interned heap strings (STR_INTERN_BIT, SSO/SLICE clear):
380
+ ;; canonicals are deduped, so bit-ne ⇒ content-ne — the V8 pointer-compare
381
+ ;; equivalent. This is the dominant unequal case in tag-dispatch chains
382
+ ;; (op === 'literal' ladders over static literals).
383
+ (local.set $axA (i32.wrap_i64 (i64.shr_u (local.get $a) (i64.const ${LAYOUT.AUX_SHIFT}))))
384
+ (local.set $axB (i32.wrap_i64 (i64.shr_u (local.get $b) (i64.const ${LAYOUT.AUX_SHIFT}))))
385
+ (if (i32.and
386
+ (i32.and (i32.eq (local.get $ta) (i32.const ${PTR.STRING}))
387
+ (i32.eq (local.get $tb) (i32.const ${PTR.STRING})))
388
+ (i32.and
389
+ (i32.eq (i32.and (local.get $axA) (i32.const ${LAYOUT.SSO_BIT | LAYOUT.SLICE_BIT | STR_INTERN_BIT})) (i32.const ${STR_INTERN_BIT}))
390
+ (i32.eq (i32.and (local.get $axB) (i32.const ${LAYOUT.SSO_BIT | LAYOUT.SLICE_BIT | STR_INTERN_BIT})) (i32.const ${STR_INTERN_BIT}))))
391
+ (then (return (i32.const 0))))
254
392
  ;; Both heap STRING fast path: inline len from header. Chunk by 4 bytes via unaligned
255
393
  ;; i32.load (wasm guarantees unaligned-OK), then byte-tail. Most string comparisons fail
256
394
  ;; early on the first 4-byte word, so this collapses the per-byte branch overhead into a
@@ -296,7 +434,7 @@ export default (ctx) => {
296
434
  (then (return (i32.const 0))))
297
435
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
298
436
  (br $lm)))
299
- (i32.const 1))`
437
+ (i32.const 1))`)
300
438
 
301
439
  // Three-way byte-wise compare: -1 if a < b, 0 if equal, +1 if a > b. Returns
302
440
  // i32 so callers can `i32.lt_s 0`, `i32.gt_s 0`, etc. without coercion.
@@ -305,7 +443,7 @@ export default (ctx) => {
305
443
  // codepoint order for code points < 0x80 and well-formed strings. NOT locale-
306
444
  // aware: this is the byte-wise variant suitable for sort-stability use cases,
307
445
  // not human-language collation.
308
- ctx.core.stdlib['__str_cmp'] = `(func $__str_cmp (param $a i64) (param $b i64) (result i32)
446
+ wat('__str_cmp', `(func $__str_cmp (param $a i64) (param $b i64) (result i32)
309
447
  (local $lenA i32) (local $lenB i32) (local $minLen i32) (local $i i32)
310
448
  (local $ca i32) (local $cb i32)
311
449
  ;; Bit-equal pointers (including same SSO inline form) ⇒ identical strings.
@@ -326,11 +464,11 @@ export default (ctx) => {
326
464
  ;; Common prefix matches — shorter string sorts first.
327
465
  (if (i32.lt_s (local.get $lenA) (local.get $lenB)) (then (return (i32.const -1))))
328
466
  (if (i32.gt_s (local.get $lenA) (local.get $lenB)) (then (return (i32.const 1))))
329
- (i32.const 0))`
467
+ (i32.const 0))`)
330
468
 
331
469
  // === WAT: unified byte length (SSO → aux low bits, heap → header) ===
332
470
 
333
- ctx.core.stdlib['__str_byteLen'] = `(func $__str_byteLen (param $ptr i64) (result i32)
471
+ wat('__str_byteLen', `(func $__str_byteLen (param $ptr i64) (result i32)
334
472
  (local $t i32) (local $off i32) (local $aux i32)
335
473
  (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
474
  (if (result i32) (i32.eq (local.get $t) (i32.const ${PTR.STRING}))
@@ -349,15 +487,16 @@ export default (ctx) => {
349
487
  (if (result i32) (i32.ge_u (local.get $off) (i32.const 4))
350
488
  (then (i32.load (i32.sub (local.get $off) (i32.const 4))))
351
489
  (else (i32.const 0))))))))
352
- (else (i32.const 0))))`
490
+ (else (i32.const 0))))`)
353
491
 
354
492
  // === WAT: string methods ===
355
493
 
356
494
  // SSO source uses an unrolled byte-extract loop (len ≤ 4); heap source uses memory.copy
357
495
  // (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)
496
+ wat('__str_slice', () => `(func $__str_slice (param $ptr i64) (param $start i32) (param $end i32) (result f64)
359
497
  (local $len i32) (local $nlen i32) (local $off i32) (local $i i32)
360
- (local $srcOff i32) (local $isSso i32)
498
+ (local $srcOff i32) (local $isSso i32) (local $sb i32) (local $sp i32)
499
+ (local $ip i32) (local $h i32) (local $j i32) (local $slot i32) (local $cand i32) (local $k i32)
361
500
  (local.set $len (call $__str_byteLen (local.get $ptr)))
362
501
  (if (i32.lt_s (local.get $start) (i32.const 0))
363
502
  (then (local.set $start (i32.add (local.get $len) (local.get $start)))))
@@ -374,6 +513,8 @@ export default (ctx) => {
374
513
  (if (i32.ge_s (local.get $start) (local.get $end))
375
514
  (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
376
515
  (local.set $nlen (i32.sub (local.get $end) (local.get $start)))
516
+ ${sliceSsoPackWat()}
517
+ ${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
518
  (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $nlen))))
378
519
  (i32.store (local.get $off) (local.get $nlen))
379
520
  (local.set $off (i32.add (local.get $off) (i32.const 4)))
@@ -393,7 +534,7 @@ export default (ctx) => {
393
534
  (br $loop))))
394
535
  (else
395
536
  (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)))`
537
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
397
538
 
398
539
  // No-copy slice: returns a VIEW into the receiver's buffer instead of copying
399
540
  // bytes. Only emitted when escape analysis proves the result never outlives the
@@ -403,8 +544,9 @@ export default (ctx) => {
403
544
  // (__str_slice) when the parent is SSO (no buffer to point into) or the result
404
545
  // is longer than SLICE_LEN_MASK (aux can't hold the length). Clamping mirrors
405
546
  // __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)
547
+ wat('__str_slice_view', () => `(func $__str_slice_view (param $ptr i64) (param $start i32) (param $end i32) (result f64)
407
548
  (local $len i32) (local $nlen i32) (local $srcOff i32) (local $tag i32)
549
+ (local $ip i32) (local $h i32) (local $j i32) (local $slot i32) (local $cand i32) (local $k i32)
408
550
  (local.set $len (call $__str_byteLen (local.get $ptr)))
409
551
  (if (i32.lt_s (local.get $start) (i32.const 0))
410
552
  (then (local.set $start (i32.add (local.get $len) (local.get $start)))))
@@ -422,22 +564,26 @@ export default (ctx) => {
422
564
  (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
423
565
  (local.set $nlen (i32.sub (local.get $end) (local.get $start)))
424
566
  (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].
567
+ ;; View-eligible: STRING parent, not SSO, length fits aux[12:0]. ≤4-byte
568
+ ;; results route to __str_slice's SSO pack instead (bit-equal short tokens).
426
569
  (if (i32.and
427
570
  (i32.and
428
- (i32.eq (local.get $tag) (i32.const ${PTR.STRING}))
571
+ (i32.and
572
+ (i32.eq (local.get $tag) (i32.const ${PTR.STRING}))
573
+ (i32.gt_u (local.get $nlen) (i32.const 4)))
429
574
  (i64.eqz (i64.and (local.get $ptr) (i64.const ${SSO_BIT_I64}))))
430
575
  (i32.le_u (local.get $nlen) (i32.const ${LAYOUT.SLICE_LEN_MASK})))
431
576
  (then
432
577
  (local.set $srcOff (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
578
+ ${internProbeWat('(i32.add (local.get $srcOff) (local.get $start))')}
433
579
  (return (call $__mkptr
434
580
  (i32.const ${PTR.STRING})
435
581
  (i32.or (i32.const ${LAYOUT.SLICE_BIT}) (local.get $nlen))
436
582
  (i32.add (local.get $srcOff) (local.get $start))))))
437
583
  ;; 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)))`
584
+ (call $__str_slice (local.get $ptr) (local.get $start) (local.get $end)))`)
439
585
 
440
- ctx.core.stdlib['__str_substring'] = `(func $__str_substring (param $ptr i64) (param $start i32) (param $end i32) (result f64)
586
+ wat('__str_substring', `(func $__str_substring (param $ptr i64) (param $start i32) (param $end i32) (result f64)
441
587
  (local $len i32) (local $tmp i32)
442
588
  (local.set $len (call $__str_byteLen (local.get $ptr)))
443
589
  (if (i32.lt_s (local.get $start) (i32.const 0))
@@ -453,7 +599,7 @@ export default (ctx) => {
453
599
  (local.set $tmp (local.get $start))
454
600
  (local.set $start (local.get $end))
455
601
  (local.set $end (local.get $tmp))))
456
- (call $__str_slice (local.get $ptr) (local.get $start) (local.get $end)))`
602
+ (call $__str_slice (local.get $ptr) (local.get $start) (local.get $end)))`)
457
603
 
458
604
  // === WAT: fused substring-equality ===
459
605
  //
@@ -468,7 +614,7 @@ export default (ctx) => {
468
614
  // returns one) and type-checks only `other`, mirroring __eq's STRING-vs-?
469
615
  // arm: a genuine number never equals a string, and a NaN-boxed non-STRING
470
616
  // 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)
617
+ wat('__str_range_eq', `(func $__str_range_eq (param $ptr i64) (param $start i32) (param $end i32) (param $other i64) (result i32)
472
618
  (local $n i32) (local $i i32) (local $fb f64)
473
619
  ;; A genuine number reinterprets to a non-NaN f64 (equals itself) — never a string.
474
620
  (local.set $fb (f64.reinterpret_i64 (local.get $other)))
@@ -492,10 +638,10 @@ export default (ctx) => {
492
638
  (then (return (i32.const 0))))
493
639
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
494
640
  (br $next)))
495
- (i32.const 1))`
641
+ (i32.const 1))`)
496
642
 
497
643
  // 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)
644
+ wat('__str_substring_eq', `(func $__str_substring_eq (param $ptr i64) (param $start i32) (param $end i32) (param $other i64) (result i32)
499
645
  (local $len i32) (local $tmp i32)
500
646
  (local.set $len (call $__str_byteLen (local.get $ptr)))
501
647
  (if (i32.lt_s (local.get $start) (i32.const 0))
@@ -511,11 +657,11 @@ export default (ctx) => {
511
657
  (local.set $tmp (local.get $start))
512
658
  (local.set $start (local.get $end))
513
659
  (local.set $end (local.get $tmp))))
514
- (call $__str_range_eq (local.get $ptr) (local.get $start) (local.get $end) (local.get $other)))`
660
+ (call $__str_range_eq (local.get $ptr) (local.get $start) (local.get $end) (local.get $other)))`)
515
661
 
516
662
  // Clamp mirrors __str_slice (negatives count from the end; __str_range_eq
517
663
  // 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)
664
+ wat('__str_slice_eq', `(func $__str_slice_eq (param $ptr i64) (param $start i32) (param $end i32) (param $other i64) (result i32)
519
665
  (local $len i32)
520
666
  (local.set $len (call $__str_byteLen (local.get $ptr)))
521
667
  (if (i32.lt_s (local.get $start) (i32.const 0))
@@ -530,11 +676,11 @@ export default (ctx) => {
530
676
  (then (local.set $end (i32.const 0))))
531
677
  (if (i32.gt_s (local.get $end) (local.get $len))
532
678
  (then (local.set $end (local.get $len))))
533
- (call $__str_range_eq (local.get $ptr) (local.get $start) (local.get $end) (local.get $other)))`
679
+ (call $__str_range_eq (local.get $ptr) (local.get $start) (local.get $end) (local.get $other)))`)
534
680
 
535
681
  // Hoist SSO/heap dispatch for hay and ndl out of the inner byte loop. Inner
536
682
  // 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)
683
+ wat('__str_indexof', `(func $__str_indexof (param $hay i64) (param $ndl i64) (param $from i32) (result i32)
538
684
  (local $hlen i32) (local $nlen i32) (local $i i32) (local $j i32) (local $match i32)
539
685
  (local $hoff i32) (local $noff i32)
540
686
  (local $hsso i32) (local $nsso i32) (local $hb i32) (local $nb i32) (local $k i32)
@@ -573,10 +719,63 @@ export default (ctx) => {
573
719
  (if (local.get $match) (then (return (local.get $i))))
574
720
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
575
721
  (br $outer)))
576
- (i32.const -1))`
722
+ (i32.const -1))`)
723
+
724
+ // Mirror of __str_indexof but searches from the end. Returns the last byte-offset
725
+ // of `ndl` in `hay` at or before `from` (-1 if not found). Per JS spec step 4,
726
+ // ToString(searchValue) is applied first. SSO/heap dispatch hoisted exactly as
727
+ // in __str_indexof so the inner loop is cheap byte-fetches.
728
+ wat('__str_lastindexof', `(func $__str_lastindexof (param $hay i64) (param $ndl i64) (param $from i32) (result i32)
729
+ (local $hlen i32) (local $nlen i32) (local $i i32) (local $j i32) (local $match i32)
730
+ (local $hoff i32) (local $noff i32)
731
+ (local $hsso i32) (local $nsso i32) (local $hb i32) (local $nb i32) (local $k i32)
732
+ ;; ToString the search value (21.1.3.10 step 4)
733
+ (local.set $ndl (call $__to_str (local.get $ndl)))
734
+ (local.set $hlen (call $__str_byteLen (local.get $hay)))
735
+ (local.set $nlen (call $__str_byteLen (local.get $ndl)))
736
+ ;; Empty needle always matches at the clamp(from,0,hlen) position
737
+ (if (i32.eqz (local.get $nlen))
738
+ (then (return (select
739
+ (select (local.get $from) (local.get $hlen) (i32.lt_s (local.get $from) (local.get $hlen)))
740
+ (i32.const 0)
741
+ (i32.ge_s (local.get $from) (i32.const 0))))))
742
+ (if (i32.gt_s (local.get $nlen) (local.get $hlen)) (then (return (i32.const -1))))
743
+ (local.set $hoff (i32.wrap_i64 (i64.and (local.get $hay) (i64.const ${LAYOUT.OFFSET_MASK}))))
744
+ (local.set $noff (i32.wrap_i64 (i64.and (local.get $ndl) (i64.const ${LAYOUT.OFFSET_MASK}))))
745
+ (local.set $hsso (i32.and
746
+ (i32.wrap_i64 (i64.shr_u (local.get $hay) (i64.const ${LAYOUT.AUX_SHIFT})))
747
+ (i32.const ${LAYOUT.SSO_BIT})))
748
+ (local.set $nsso (i32.and
749
+ (i32.wrap_i64 (i64.shr_u (local.get $ndl) (i64.const ${LAYOUT.AUX_SHIFT})))
750
+ (i32.const ${LAYOUT.SSO_BIT})))
751
+ ;; clamp start position: from defaults to hlen-nlen, capped at that ceiling
752
+ (local.set $i (i32.sub (local.get $hlen) (local.get $nlen)))
753
+ (if (i32.and (i32.ge_s (local.get $from) (i32.const 0)) (i32.lt_s (local.get $from) (local.get $i)))
754
+ (then (local.set $i (local.get $from))))
755
+ (block $done (loop $outer
756
+ (br_if $done (i32.lt_s (local.get $i) (i32.const 0)))
757
+ (local.set $match (i32.const 1))
758
+ (local.set $j (i32.const 0))
759
+ (block $nomatch (loop $inner
760
+ (br_if $nomatch (i32.ge_s (local.get $j) (local.get $nlen)))
761
+ (local.set $k (i32.add (local.get $i) (local.get $j)))
762
+ (local.set $hb (if (result i32) (local.get $hsso)
763
+ (then (i32.and (i32.shr_u (local.get $hoff) (i32.shl (local.get $k) (i32.const 3))) (i32.const 0xFF)))
764
+ (else (i32.load8_u (i32.add (local.get $hoff) (local.get $k))))))
765
+ (local.set $nb (if (result i32) (local.get $nsso)
766
+ (then (i32.and (i32.shr_u (local.get $noff) (i32.shl (local.get $j) (i32.const 3))) (i32.const 0xFF)))
767
+ (else (i32.load8_u (i32.add (local.get $noff) (local.get $j))))))
768
+ (if (i32.ne (local.get $hb) (local.get $nb))
769
+ (then (local.set $match (i32.const 0)) (br $nomatch)))
770
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
771
+ (br $inner)))
772
+ (if (local.get $match) (then (return (local.get $i))))
773
+ (local.set $i (i32.sub (local.get $i) (i32.const 1)))
774
+ (br $outer)))
775
+ (i32.const -1))`)
577
776
 
578
777
  // 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)
778
+ wat('__str_startswith', `(func $__str_startswith (param $str i64) (param $pfx i64) (result i32)
580
779
  (local $plen i32) (local $i i32)
581
780
  (local $soff i32) (local $poff i32) (local $ssso i32) (local $psso i32)
582
781
  (local.set $plen (call $__str_byteLen (local.get $pfx)))
@@ -602,9 +801,9 @@ export default (ctx) => {
602
801
  (then (return (i32.const 0))))
603
802
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
604
803
  (br $loop)))
605
- (i32.const 1))`
804
+ (i32.const 1))`)
606
805
 
607
- ctx.core.stdlib['__str_endswith'] = `(func $__str_endswith (param $str i64) (param $sfx i64) (result i32)
806
+ wat('__str_endswith', `(func $__str_endswith (param $str i64) (param $sfx i64) (result i32)
608
807
  (local $slen i32) (local $flen i32) (local $off i32) (local $i i32) (local $k i32)
609
808
  (local $soff i32) (local $foff i32) (local $ssso i32) (local $fsso i32)
610
809
  (local.set $slen (call $__str_byteLen (local.get $str)))
@@ -633,10 +832,10 @@ export default (ctx) => {
633
832
  (then (return (i32.const 0))))
634
833
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
635
834
  (br $loop)))
636
- (i32.const 1))`
835
+ (i32.const 1))`)
637
836
 
638
837
  // 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)
838
+ wat('__str_case', `(func $__str_case (param $ptr i64) (param $lo i32) (param $hi i32) (param $delta i32) (result f64)
640
839
  (local $len i32) (local $off i32) (local $i i32) (local $c i32)
641
840
  (local $srcOff i32)
642
841
  (local.set $len (call $__str_byteLen (local.get $ptr)))
@@ -667,9 +866,9 @@ export default (ctx) => {
667
866
  (i32.store8 (i32.add (local.get $off) (local.get $i)) (local.get $c))
668
867
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
669
868
  (br $lh)))))
670
- (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
869
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
671
870
 
672
- ctx.core.stdlib['__str_trim'] = `(func $__str_trim (param $ptr i64) (result f64)
871
+ wat('__str_trim', `(func $__str_trim (param $ptr i64) (result f64)
673
872
  (local $len i32) (local $start i32) (local $end i32)
674
873
  (local.set $len (call $__str_byteLen (local.get $ptr)))
675
874
  (local.set $start (i32.const 0))
@@ -684,9 +883,9 @@ export default (ctx) => {
684
883
  (br_if $d2 (i32.gt_u (call $__char_at (local.get $ptr) (i32.sub (local.get $end) (i32.const 1))) (i32.const 32)))
685
884
  (local.set $end (i32.sub (local.get $end) (i32.const 1)))
686
885
  (br $l2)))
687
- (call $__str_slice (local.get $ptr) (local.get $start) (local.get $end)))`
886
+ (call $__str_slice (local.get $ptr) (local.get $start) (local.get $end)))`)
688
887
 
689
- ctx.core.stdlib['__str_trimStart'] = `(func $__str_trimStart (param $ptr i64) (result f64)
888
+ wat('__str_trimStart', `(func $__str_trimStart (param $ptr i64) (result f64)
690
889
  (local $len i32) (local $start i32)
691
890
  (local.set $len (call $__str_byteLen (local.get $ptr)))
692
891
  (local.set $start (i32.const 0))
@@ -695,9 +894,9 @@ export default (ctx) => {
695
894
  (br_if $done (i32.gt_u (call $__char_at (local.get $ptr) (local.get $start)) (i32.const 32)))
696
895
  (local.set $start (i32.add (local.get $start) (i32.const 1)))
697
896
  (br $loop)))
698
- (call $__str_slice (local.get $ptr) (local.get $start) (local.get $len)))`
897
+ (call $__str_slice (local.get $ptr) (local.get $start) (local.get $len)))`)
699
898
 
700
- ctx.core.stdlib['__str_trimEnd'] = `(func $__str_trimEnd (param $ptr i64) (result f64)
899
+ wat('__str_trimEnd', `(func $__str_trimEnd (param $ptr i64) (result f64)
701
900
  (local $len i32) (local $end i32)
702
901
  (local.set $len (call $__str_byteLen (local.get $ptr)))
703
902
  (local.set $end (local.get $len))
@@ -706,11 +905,11 @@ export default (ctx) => {
706
905
  (br_if $done (i32.gt_u (call $__char_at (local.get $ptr) (i32.sub (local.get $end) (i32.const 1))) (i32.const 32)))
707
906
  (local.set $end (i32.sub (local.get $end) (i32.const 1)))
708
907
  (br $loop)))
709
- (call $__str_slice (local.get $ptr) (i32.const 0) (local.get $end)))`
908
+ (call $__str_slice (local.get $ptr) (i32.const 0) (local.get $end)))`)
710
909
 
711
910
  // Materialize source bytes once via __str_copy (handles SSO/heap), then memory.copy
712
911
  // 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)
912
+ wat('__str_repeat', `(func $__str_repeat (param $ptr i64) (param $n i32) (result f64)
714
913
  (local $len i32) (local $total i32) (local $off i32) (local $i i32)
715
914
  (local.set $len (call $__str_byteLen (local.get $ptr)))
716
915
  (if (i32.or (i32.eqz (local.get $n)) (i32.eqz (local.get $len)))
@@ -729,11 +928,11 @@ export default (ctx) => {
729
928
  (local.get $len))
730
929
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
731
930
  (br $loop)))
732
- (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
931
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
733
932
 
734
933
  // Coerce value to string: numbers → __ftoa, nullish → static strings,
735
934
  // 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)
935
+ wat('__to_str', `(func $__to_str (param $val i64) (result i64)
737
936
  (local $type i32) (local $f f64)
738
937
  (local.set $f (f64.reinterpret_i64 (local.get $val)))
739
938
  ;; Not NaN → number, convert
@@ -743,6 +942,10 @@ export default (ctx) => {
743
942
  (then (return (i64.reinterpret_f64 (call $__static_str (i32.const 5))))))
744
943
  (if (i64.eq (local.get $val) (i64.const ${UNDEF_NAN}))
745
944
  (then (return (i64.reinterpret_f64 (call $__static_str (i32.const 6))))))
945
+ (if (i64.eq (local.get $val) (i64.const ${FALSE_NAN}))
946
+ (then (return (i64.reinterpret_f64 (call $__static_str (i32.const 4))))))
947
+ (if (i64.eq (local.get $val) (i64.const ${TRUE_NAN}))
948
+ (then (return (i64.reinterpret_f64 (call $__static_str (i32.const 3))))))
746
949
  (local.set $type (call $__ptr_type (local.get $val)))
747
950
  ;; Plain NaN (type=0) → "NaN" string
748
951
  (if (i32.eqz (local.get $type))
@@ -751,11 +954,11 @@ export default (ctx) => {
751
954
  (if (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
752
955
  (then (return (i64.reinterpret_f64 (call $__str_join (local.get $val)
753
956
  (i64.reinterpret_f64 (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT | 1}) (i32.const 44))))))))
754
- (local.get $val))`
957
+ (local.get $val))`)
755
958
 
756
959
  // Copy bytes of a string (SSO or heap) into memory at dst. Uses memory.copy for
757
960
  // 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)
961
+ wat('__str_copy', `(func $__str_copy (param $src i64) (param $dst i32) (param $len i32)
759
962
  (local $w i32)
760
963
  (if (i64.ne (i64.and (local.get $src) (i64.const ${SSO_BIT_I64})) (i64.const 0))
761
964
  (then
@@ -775,7 +978,7 @@ export default (ctx) => {
775
978
  ;; Heap STRING: memory.copy directly from string data
776
979
  (memory.copy (local.get $dst)
777
980
  (i32.wrap_i64 (i64.and (local.get $src) (i64.const ${LAYOUT.OFFSET_MASK})))
778
- (local.get $len)))))`
981
+ (local.get $len)))))`)
779
982
 
780
983
  // Bump-extend fast path: when `a` is a heap STRING sitting at the top of the
781
984
  // bump allocator, extend its allocation in place instead of copying. Mutates
@@ -826,10 +1029,7 @@ export default (ctx) => {
826
1029
  (then
827
1030
  (local.set $newHeap
828
1031
  (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)))))
1032
+ (call $__memgrow (local.get $newHeap))
833
1033
  (call $__str_copy (local.get $b)
834
1034
  (i32.add (local.get $aoff) (local.get $alen))
835
1035
  (local.get $blen))
@@ -841,7 +1041,7 @@ export default (ctx) => {
841
1041
  // VAL.STRING and the rhs is a string-index. Skips __str_idx's 1-char SSO
842
1042
  // construction and __str_concat's type-dispatch — byte goes directly from
843
1043
  // __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)
1044
+ wat('__str_append_byte', `(func $__str_append_byte (param $a i64) (param $byte i32) (result f64)
845
1045
  (local $ta i32) (local $aoff i32) (local $alen i32)
846
1046
  (local $newHeap i32) (local $off i32) (local $total i32)
847
1047
  (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 +1063,7 @@ export default (ctx) => {
863
1063
  (then
864
1064
  (local.set $newHeap
865
1065
  (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)))))
1066
+ (call $__memgrow (local.get $newHeap))
870
1067
  (i32.store8 (i32.add (local.get $aoff) (local.get $alen)) (local.get $byte))
871
1068
  (i32.store (i32.sub (local.get $aoff) (i32.const 4)) (i32.add (local.get $alen) (i32.const 1)))
872
1069
  (global.set $__heap (local.get $newHeap))
@@ -899,9 +1096,9 @@ export default (ctx) => {
899
1096
  (local.set $off (i32.add (local.get $off) (i32.const 4)))
900
1097
  (call $__str_copy (local.get $a) (local.get $off) (local.get $alen))
901
1098
  (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)))`
1099
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
903
1100
 
904
- ctx.core.stdlib['__str_concat'] = `(func $__str_concat (param $a i64) (param $b i64) (result f64)
1101
+ wat('__str_concat', `(func $__str_concat (param $a i64) (param $b i64) (result f64)
905
1102
  (local $alen i32) (local $blen i32) (local $total i32) (local $off i32)
906
1103
  (local $ta i32) (local $aoff i32) (local $newHeap i32)
907
1104
  ;; Coerce operands to strings if needed
@@ -919,9 +1116,9 @@ export default (ctx) => {
919
1116
  (local.set $off (i32.add (local.get $off) (i32.const 4)))
920
1117
  (call $__str_copy (local.get $a) (local.get $off) (local.get $alen))
921
1118
  (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)))`
1119
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
923
1120
 
924
- ctx.core.stdlib['__str_concat_raw'] = `(func $__str_concat_raw (param $a i64) (param $b i64) (result f64)
1121
+ wat('__str_concat_raw', `(func $__str_concat_raw (param $a i64) (param $b i64) (result f64)
925
1122
  (local $alen i32) (local $blen i32) (local $total i32) (local $off i32)
926
1123
  (local $ta i32) (local $aoff i32) (local $newHeap i32)
927
1124
  (local.set $alen (call $__str_byteLen (local.get $a)))
@@ -936,9 +1133,9 @@ export default (ctx) => {
936
1133
  (local.set $off (i32.add (local.get $off) (i32.const 4)))
937
1134
  (call $__str_copy (local.get $a) (local.get $off) (local.get $alen))
938
1135
  (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)))`
1136
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
940
1137
 
941
- ctx.core.stdlib['__str_replace'] = `(func $__str_replace (param $str i64) (param $search i64) (param $repl i64) (result f64)
1138
+ wat('__str_replace', `(func $__str_replace (param $str i64) (param $search i64) (param $repl i64) (result f64)
942
1139
  (local $idx i32) (local $slen i32)
943
1140
  (local.set $idx (call $__str_indexof (local.get $str) (local.get $search) (i32.const 0)))
944
1141
  (if (result f64) (i32.lt_s (local.get $idx) (i32.const 0))
@@ -950,9 +1147,9 @@ export default (ctx) => {
950
1147
  (i64.reinterpret_f64 (call $__str_slice (local.get $str) (i32.const 0) (local.get $idx)))
951
1148
  (local.get $repl)))
952
1149
  (i64.reinterpret_f64 (call $__str_slice (local.get $str) (i32.add (local.get $idx) (local.get $slen))
953
- (call $__str_byteLen (local.get $str))))))))`
1150
+ (call $__str_byteLen (local.get $str))))))))`)
954
1151
 
955
- ctx.core.stdlib['__str_replaceall'] = `(func $__str_replaceall (param $str i64) (param $search i64) (param $repl i64) (result f64)
1152
+ wat('__str_replaceall', `(func $__str_replaceall (param $str i64) (param $search i64) (param $repl i64) (result f64)
956
1153
  (local $idx i32) (local $slen i32) (local $pos i32) (local $result i64)
957
1154
  (local.set $slen (call $__str_byteLen (local.get $search)))
958
1155
  (local.set $result (local.get $str))
@@ -968,33 +1165,48 @@ export default (ctx) => {
968
1165
  (call $__str_byteLen (local.get $result)))))))
969
1166
  (local.set $pos (i32.add (local.get $idx) (call $__str_byteLen (local.get $repl))))
970
1167
  (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)
1168
+ (f64.reinterpret_i64 (local.get $result)))`)
1169
+
1170
+ // $limit ≥ 0: honour JS's optional limit arg. 0x7fffffff = "no limit"
1171
+ // (sentinel passed by the no-limit call site). limit=0 []. limit=N → at
1172
+ // most N elements; the (N+1)th and later pieces are DISCARDED (not appended
1173
+ // as a remainder). Empty separator: split into individual byte-chars, up to
1174
+ // $limit chars ("abc".split("") → ["a","b","c"], "".split("") → []).
1175
+ wat('__str_split', `(func $__str_split (param $str i64) (param $sep i64) (param $limit i32) (result f64)
977
1176
  (local $slen i32) (local $plen i32) (local $count i32)
978
1177
  (local $i i32) (local $j i32) (local $match i32)
979
- (local $arr i32) (local $piece_start i32) (local $piece_idx i32)
1178
+ (local $arr i32) (local $piece_start i32) (local $piece_idx i32) (local $hitlim i32)
980
1179
  (local.set $slen (call $__str_byteLen (local.get $str)))
981
1180
  (local.set $plen (call $__str_byteLen (local.get $sep)))
1181
+ ;; limit=0 → empty array
1182
+ (if (i32.eqz (local.get $limit)) (then
1183
+ (local.set $arr (call $__alloc (i32.const 8)))
1184
+ (i32.store (local.get $arr) (i32.const 0))
1185
+ (i32.store (i32.add (local.get $arr) (i32.const 4)) (i32.const 0))
1186
+ (return (call $__mkptr (i32.const 1) (i32.const 0) (i32.add (local.get $arr) (i32.const 8))))))
982
1187
  (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))
1188
+ ;; Empty-separator: split into individual byte-chars, up to $limit
1189
+ (local.set $count (select (local.get $limit) (local.get $slen) (i32.lt_u (local.get $limit) (local.get $slen))))
1190
+ (local.set $arr (call $__alloc (i32.add (i32.const 8) (i32.shl (local.get $count) (i32.const 3)))))
1191
+ (i32.store (local.get $arr) (local.get $count))
1192
+ (i32.store (i32.add (local.get $arr) (i32.const 4)) (local.get $count))
986
1193
  (local.set $arr (i32.add (local.get $arr) (i32.const 8)))
987
1194
  (block $de (loop $le
988
- (br_if $de (i32.ge_s (local.get $i) (local.get $slen)))
1195
+ (br_if $de (i32.ge_s (local.get $i) (local.get $count)))
989
1196
  (f64.store (i32.add (local.get $arr) (i32.shl (local.get $i) (i32.const 3)))
990
1197
  (call $__str_slice (local.get $str) (local.get $i) (i32.add (local.get $i) (i32.const 1))))
991
1198
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
992
1199
  (br $le)))
993
1200
  (return (call $__mkptr (i32.const 1) (i32.const 0) (local.get $arr)))))
1201
+ ;; Count pass: tally pieces = separators+1, capped at $limit.
1202
+ ;; We stop incrementing count once count reaches $limit (last sep found
1203
+ ;; at that point produces piece #limit — a separator *after* piece limit-1,
1204
+ ;; meaning we've already found all limit pieces and don't count more).
994
1205
  (local.set $count (i32.const 1))
995
1206
  (local.set $i (i32.const 0))
996
1207
  (block $d1 (loop $l1
997
1208
  (br_if $d1 (i32.gt_s (local.get $i) (i32.sub (local.get $slen) (local.get $plen))))
1209
+ (br_if $d1 (i32.ge_u (local.get $count) (local.get $limit)))
998
1210
  (local.set $match (i32.const 1))
999
1211
  (local.set $j (i32.const 0))
1000
1212
  (block $n1 (loop $c1
@@ -1017,6 +1229,11 @@ export default (ctx) => {
1017
1229
  (local.set $piece_start (i32.const 0))
1018
1230
  (local.set $piece_idx (i32.const 0))
1019
1231
  (local.set $i (i32.const 0))
1232
+ ;; Fill pass: write pieces separated by $sep, up to $count total pieces.
1233
+ ;; When a separator is found and piece_idx+1 reaches count, write that
1234
+ ;; piece (before the sep) and exit WITHOUT appending the tail — this
1235
+ ;; correctly discards the remainder when $limit truncates the result.
1236
+ (local.set $hitlim (i32.const 0))
1020
1237
  (block $d2 (loop $l2
1021
1238
  (br_if $d2 (i32.gt_s (local.get $i) (i32.sub (local.get $slen) (local.get $plen))))
1022
1239
  (local.set $match (i32.const 1))
@@ -1034,14 +1251,27 @@ export default (ctx) => {
1034
1251
  (local.set $piece_idx (i32.add (local.get $piece_idx) (i32.const 1)))
1035
1252
  (local.set $i (i32.add (local.get $i) (local.get $plen)))
1036
1253
  (local.set $piece_start (local.get $i))
1254
+ ;; If we've emitted all $count pieces, mark limit-hit and stop (discard tail)
1255
+ (if (i32.ge_s (local.get $piece_idx) (local.get $count))
1256
+ (then (local.set $hitlim (i32.const 1)) (br $d2)))
1037
1257
  (br $l2)))
1038
1258
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
1039
1259
  (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)
1260
+ ;; Write tail only when the scan ended naturally (not truncated by limit)
1261
+ (if (i32.eqz (local.get $hitlim)) (then
1262
+ (f64.store (i32.add (local.get $arr) (i32.shl (local.get $piece_idx) (i32.const 3)))
1263
+ (call $__str_slice (local.get $str) (local.get $piece_start) (local.get $slen)))))
1264
+ (call $__mkptr (i32.const 1) (i32.const 0) (local.get $arr)))`)
1265
+
1266
+ // Array (type=1) → join(",") like JS Array.toString().
1267
+ // When the typedarray module is loaded, also handles PTR.TYPED (type=3) arrays:
1268
+ // promoteIntArrayLiterals rewrites [int,...] → new Int32Array([...]) internally,
1269
+ // so a.map(fn).join() may receive a PTR.TYPED result. Use __typed_idx to load
1270
+ // each element correctly (it returns f64 for any element type / stride).
1271
+ wat('__str_join', () => {
1272
+ if (!ctx.module.modules.typedarray) {
1273
+ // ARRAY-only fast path — no __typed_idx overhead.
1274
+ return `(func $__str_join (param $arr i64) (param $sep i64) (result f64)
1045
1275
  (local $off i32) (local $len i32) (local $i i32) (local $result f64)
1046
1276
  (local.set $off (call $__ptr_offset (local.get $arr)))
1047
1277
  (local.set $len (call $__len (local.get $arr)))
@@ -1057,13 +1287,48 @@ export default (ctx) => {
1057
1287
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
1058
1288
  (br $loop)))
1059
1289
  (local.get $result))`
1290
+ }
1291
+ // ARRAY + TYPED path: runtime dispatch on ptr type.
1292
+ // PTR.TYPED (type=3): elements have typed-array stride; __typed_idx reads correctly.
1293
+ // PTR.ARRAY (type=1): elements are 8-byte NaN-boxed f64 slots; i64.load is correct.
1294
+ return `(func $__str_join (param $arr i64) (param $sep i64) (result f64)
1295
+ (local $off i32) (local $len i32) (local $i i32) (local $result f64) (local $isTyped i32)
1296
+ (local.set $isTyped
1297
+ (i32.eq
1298
+ (i32.and (i32.wrap_i64 (i64.shr_u (local.get $arr) (i64.const ${LAYOUT.TAG_SHIFT})))
1299
+ (i32.const ${LAYOUT.TAG_MASK}))
1300
+ (i32.const ${PTR.TYPED})))
1301
+ (local.set $off (call $__ptr_offset (local.get $arr)))
1302
+ (local.set $len (call $__len (local.get $arr)))
1303
+ (if (i32.eqz (local.get $len))
1304
+ (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
1305
+ (local.set $result
1306
+ (f64.reinterpret_i64
1307
+ (call $__to_str
1308
+ (if (result i64) (local.get $isTyped)
1309
+ (then (i64.reinterpret_f64 (call $__typed_idx (local.get $arr) (i32.const 0))))
1310
+ (else (i64.load (local.get $off)))))))
1311
+ (local.set $i (i32.const 1))
1312
+ (block $done (loop $loop
1313
+ (br_if $done (i32.ge_s (local.get $i) (local.get $len)))
1314
+ (local.set $result (call $__str_concat (i64.reinterpret_f64 (local.get $result)) (local.get $sep)))
1315
+ (local.set $result
1316
+ (call $__str_concat
1317
+ (i64.reinterpret_f64 (local.get $result))
1318
+ (if (result i64) (local.get $isTyped)
1319
+ (then (i64.reinterpret_f64 (call $__typed_idx (local.get $arr) (local.get $i))))
1320
+ (else (i64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3))))))))
1321
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
1322
+ (br $loop)))
1323
+ (local.get $result))`
1324
+ })
1060
1325
 
1061
1326
  // Source string copied via __str_copy (handles SSO/heap with memory.copy where possible).
1062
1327
  // 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)
1328
+ wat('__str_pad', `(func $__str_pad (param $str i64) (param $target i32) (param $pad i64) (param $before i32) (result f64)
1064
1329
  (local $slen i32) (local $plen i32) (local $fill i32) (local $off i32) (local $i i32)
1065
1330
  (local $str_off i32) (local $pad_off i32)
1066
- (local $pbits i64) (local $poff i32) (local $psso i32)
1331
+ (local $pbits i64) (local $poff i32) (local $psso i32) (local $sp i32) (local $sb i32)
1067
1332
  (local.set $slen (call $__str_byteLen (local.get $str)))
1068
1333
  (if (i32.ge_s (local.get $slen) (local.get $target))
1069
1334
  (then (return (f64.reinterpret_i64 (local.get $str)))))
@@ -1090,7 +1355,25 @@ export default (ctx) => {
1090
1355
  (else (i32.load8_u (i32.add (local.get $poff) (i32.rem_u (local.get $i) (local.get $plen)))))))
1091
1356
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
1092
1357
  (br $l2)))
1093
- (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
1358
+ ${!ctx.memory.shared ? `
1359
+ ;; SSO + reclaim for ≤4 ASCII results: pack into the pointer and free this pad
1360
+ ;; allocation (at heap top) so padStart in a builder loop is allocation-neutral
1361
+ ;; — the accumulator stays at heap top and keeps bump-extending (O(n)).
1362
+ (if (i32.le_u (local.get $target) (i32.const 4))
1363
+ (then
1364
+ (block $heap
1365
+ (local.set $i (i32.const 0)) (local.set $sp (i32.const 0))
1366
+ (loop $pk
1367
+ (if (i32.lt_u (local.get $i) (local.get $target))
1368
+ (then
1369
+ (local.set $sb (i32.load8_u (i32.add (local.get $off) (local.get $i))))
1370
+ (br_if $heap (i32.ge_u (local.get $sb) (i32.const 0x80)))
1371
+ (local.set $sp (i32.or (local.get $sp) (i32.shl (local.get $sb) (i32.shl (local.get $i) (i32.const 3)))))
1372
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
1373
+ (br $pk))))
1374
+ (global.set $__heap (i32.sub (local.get $off) (i32.const 4)))
1375
+ (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.or (i32.const ${LAYOUT.SSO_BIT}) (local.get $target)) (local.get $sp))))))` : ''}
1376
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
1094
1377
 
1095
1378
  // Base helpers (__sso_char/__str_char/__char_at/__str_byteLen) are referenced
1096
1379
  // from other helpers' WAT bodies and from emit sites; their `stdlibDeps`
@@ -1103,16 +1386,16 @@ export default (ctx) => {
1103
1386
  // (21.1.3.27/28). Typed forms cover the static-string case; generic forms
1104
1387
  // pair with them so the dispatcher can pick a runtime ptr-type branch when
1105
1388
  // 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) => {
1389
+ bind('.string:toString', (str) => asF64(emit(str)))
1390
+ bind('.string:valueOf', (str) => asF64(emit(str)))
1391
+ bind('.toString', (val) => {
1109
1392
  inc('__to_str')
1110
1393
  return typed(['f64.reinterpret_i64', ['call', '$__to_str', asI64(emit(val))]], 'f64')
1111
- }
1394
+ })
1112
1395
  // Object.prototype.valueOf returns the receiver (per ES2024 20.1.3.7).
1113
1396
  // Array/Object inherit this; only primitive wrappers (Number/Boolean/String)
1114
1397
  // override to return the primitive — strings already covered by .string:valueOf.
1115
- ctx.core.emit['.valueOf'] = (val) => asF64(emit(val))
1398
+ bind('.valueOf', (val) => asF64(emit(val)))
1116
1399
 
1117
1400
  // `.slice` lowering, parametrised on the backing helper: __str_slice copies
1118
1401
  // bytes; __str_slice_view returns a no-copy SLICE_BIT view — used only when
@@ -1130,8 +1413,8 @@ export default (ctx) => {
1130
1413
  ['call', `$${fn}`, ['i64.reinterpret_f64', ['local.get', `$${t}`]], startIR,
1131
1414
  ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]], 'f64')
1132
1415
  }
1133
- ctx.core.emit['.string:slice'] = sliceEmitter('__str_slice')
1134
- ctx.core.emit['.string:slice#view'] = sliceEmitter('__str_slice_view')
1416
+ bind('.string:slice', sliceEmitter('__str_slice'))
1417
+ bind('.string:slice#view', sliceEmitter('__str_slice_view'))
1135
1418
 
1136
1419
  // ToIntegerOrInfinity for a string-method position argument: ToNumber (so
1137
1420
  // string / boolean / null / undefined positions coerce per spec) then trunc.
@@ -1148,14 +1431,25 @@ export default (ctx) => {
1148
1431
  // a BOOL rides the 0/1 carrier (→ "0"/"1" not "true"/"false"), and an OBJECT
1149
1432
  // needs compile-time ToPrimitive(string) (__to_str can't invoke user toString).
1150
1433
  const searchArg = (search) =>
1151
- valTypeOf(search) === VAL.BOOL ? asI64(emitBoolStr(search)) :
1434
+ valTypeOf(search) === VAL.BOOL ? asI64(bool(search)) :
1152
1435
  valTypeOf(search) === VAL.OBJECT ? toStrI64(search, emit(search)) : asI64(emit(search))
1153
1436
 
1154
- ctx.core.emit['.string:indexOf'] = (str, search, from) => {
1437
+ bind('.string:indexOf', (str, search, from) => {
1155
1438
  inc('__str_indexof')
1156
1439
  const hay = asI64(emit(str)), ndl = searchArg(search)
1157
1440
  return typed(['f64.convert_i32_s', ['call', '$__str_indexof', hay, ndl, posIndex(from)]], 'f64')
1158
- }
1441
+ })
1442
+
1443
+ // String.prototype.lastIndexOf: search from the end, returning the last
1444
+ // byte-offset of `search` in `str` at or before `fromIndex` (or -1).
1445
+ // Per spec the `from` default is +∞ (search from the very end), which we
1446
+ // map to 0x7fffffff — __str_lastindexof clamps it to hlen-nlen anyway.
1447
+ bind('.string:lastIndexOf', (str, search, from) => {
1448
+ inc('__str_lastindexof')
1449
+ const hay = asI64(emit(str)), ndl = searchArg(search)
1450
+ const fromIR = from == null ? ['i32.const', 0x7fffffff] : asI32(emit(from))
1451
+ return typed(['f64.convert_i32_s', ['call', '$__str_lastindexof', hay, ndl, fromIR]], 'f64')
1452
+ })
1159
1453
 
1160
1454
  // String.prototype.{includes,startsWith,endsWith} run IsRegExp(searchString)
1161
1455
  // and throw a TypeError when it is a RegExp. Detect a regex-typed search arg
@@ -1166,16 +1460,16 @@ export default (ctx) => {
1166
1460
  return typed(['block', ['result', 'f64'], ['throw', '$__jz_err', ['f64.const', 0]]], 'f64')
1167
1461
  }
1168
1462
 
1169
- ctx.core.emit['.string:includes'] = (str, search, from) => {
1463
+ bind('.string:includes', (str, search, from) => {
1170
1464
  const guard = regexpSearchGuard(search); if (guard) return guard
1171
1465
  inc('__str_indexof')
1172
1466
  const hay = asI64(emit(str)), ndl = searchArg(search)
1173
1467
  return typed(['f64.convert_i32_s',
1174
1468
  ['i32.ge_s', ['call', '$__str_indexof', hay, ndl, posIndex(from)], ['i32.const', 0]]], 'f64')
1175
- }
1469
+ })
1176
1470
 
1177
1471
  // Generic (no collision)
1178
- ctx.core.emit['.substring'] = (str, start, end) => {
1472
+ bind('.substring', (str, start, end) => {
1179
1473
  inc('__str_substring')
1180
1474
  if (end != null) return typed(['call', '$__str_substring', asI64(emit(str)), asI32(emit(start)), asI32(emit(end))], 'f64')
1181
1475
  const t = temp('t')
@@ -1183,13 +1477,13 @@ export default (ctx) => {
1183
1477
  ['local.set', `$${t}`, asF64(emit(str))],
1184
1478
  ['call', '$__str_substring', ['i64.reinterpret_f64', ['local.get', `$${t}`]], asI32(emit(start)),
1185
1479
  ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]], 'f64')
1186
- }
1480
+ })
1187
1481
 
1188
1482
  // .substr(start, length) — Annex B / legacy. Equivalent to substring(start, start+length).
1189
1483
  // __str_substring clamps end to byteLen and start/end to [0, byteLen], so negative
1190
1484
  // values are floored to 0 (matches v8 for length<0 → empty; for start<0 spec wants
1191
1485
  // max(0, len+start), which we don't implement — rare in practice).
1192
- ctx.core.emit['.substr'] = (str, start, length) => {
1486
+ bind('.substr', (str, start, length) => {
1193
1487
  inc('__str_substring')
1194
1488
  if (length != null) {
1195
1489
  const s = tempI32('substrS')
@@ -1205,15 +1499,6 @@ export default (ctx) => {
1205
1499
  ['local.set', `$${t}`, asF64(emit(str))],
1206
1500
  ['call', '$__str_substring', ['i64.reinterpret_f64', ['local.get', `$${t}`]], asI32(emit(start)),
1207
1501
  ['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
1502
  })
1218
1503
 
1219
1504
  // Search args go through ToString per spec — coerce non-string-typed args
@@ -1231,30 +1516,87 @@ export default (ctx) => {
1231
1516
  }
1232
1517
  return typed(['f64.convert_i32_s', ['call', `$${name}`, asI64(emit(str)), sfxArg]], 'f64')
1233
1518
  }
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
- }
1519
+ bind('.startsWith', stringSearchMethod('__str_startswith'))
1520
+ bind('.endsWith', stringSearchMethod('__str_endswith'))
1521
+ bind('.trim', method('__str_trim', 'I'))
1522
+ bind('.trimStart', method('__str_trimStart', 'I'))
1523
+ bind('.trimEnd', method('__str_trimEnd', 'I'))
1524
+ bind('.repeat', method('__str_repeat', 'Ii'))
1525
+ // split(sep, limit): both args are optional.
1526
+ // - No args (undefined sep) → [str]: JS spec step 3 treats undefined separator
1527
+ // as returning a single-element array of the whole string (not splitting at all).
1528
+ // - 1 arg → no limit (sentinel 0x7fffffff = MAX_I32).
1529
+ // - 2 args → honour limit: 0 → [], N at most N pieces.
1530
+ bind('.split', (str, sep, limit) => {
1531
+ if (sep === undefined) {
1532
+ // split() → [str]: wrap the whole string in a 1-element array
1533
+ inc('__wrap1')
1534
+ return typed(['call', '$__wrap1', asI64(emit(str))], 'f64')
1535
+ }
1536
+ inc('__str_split')
1537
+ const limitIR = limit === undefined
1538
+ ? ['i32.const', 0x7fffffff]
1539
+ : ['i32.trunc_sat_f64_u', asF64(emit(limit))]
1540
+ return typed(['call', '$__str_split', asI64(emit(str)), asI64(emit(sep)), limitIR], 'f64')
1541
+ })
1542
+
1543
+ // replace(search, replacement). When `replacement` is a function, replace the
1544
+ // FIRST occurrence of the (string) search with ToString(fn(match)) — per spec a
1545
+ // string search matches once and the callback receives the matched substring.
1546
+ // (A regex search routes to `.string:replace` in the regex module, which also
1547
+ // handles the /g loop.) Without a closure runtime we can't invoke the callback,
1548
+ // so fall back to a clear error rather than silent data loss.
1549
+ bind('.replace', (str, search, repl) => {
1550
+ if (valTypeOf(repl) === VAL.CLOSURE) {
1551
+ if (!ctx.closure?.call) err('.replace(search, fn): no closure runtime available for the callback form')
1552
+ inc('__str_indexof', '__str_slice', '__str_concat', '__str_byteLen', '__to_str')
1553
+ const s = temp('rps'), q = temp('rpq'), fnL = temp('rpf')
1554
+ const idx = tempI32('rpi'), mlen = tempI32('rpm')
1555
+ const sI64 = () => ['i64.reinterpret_f64', ['local.get', `$${s}`]]
1556
+ const match = typed(['call', '$__str_slice', sI64(), ['local.get', `$${idx}`],
1557
+ ['i32.add', ['local.get', `$${idx}`], ['local.get', `$${mlen}`]]], 'f64')
1558
+ const repIR = ['call', '$__to_str', asI64(ctx.closure.call(typed(['local.get', `$${fnL}`], 'f64'), [match]))]
1559
+ const head = typed(['call', '$__str_slice', sI64(), ['i32.const', 0], ['local.get', `$${idx}`]], 'f64')
1560
+ const tail = typed(['call', '$__str_slice', sI64(),
1561
+ ['i32.add', ['local.get', `$${idx}`], ['local.get', `$${mlen}`]],
1562
+ ['call', '$__str_byteLen', sI64()]], 'f64')
1563
+ return typed(['block', ['result', 'f64'],
1564
+ ['local.set', `$${s}`, asF64(emit(str))],
1565
+ ['local.set', `$${q}`, asF64(emit(search))],
1566
+ ['local.set', `$${fnL}`, asF64(emit(repl))],
1567
+ ['local.set', `$${mlen}`, ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${q}`]]]],
1568
+ ['local.set', `$${idx}`, ['call', '$__str_indexof', sI64(), ['i64.reinterpret_f64', ['local.get', `$${q}`]], ['i32.const', 0]]],
1569
+ ['if', ['result', 'f64'], ['i32.lt_s', ['local.get', `$${idx}`], ['i32.const', 0]],
1570
+ ['then', ['local.get', `$${s}`]],
1571
+ ['else', typed(['call', '$__str_concat',
1572
+ asI64(typed(['call', '$__str_concat', asI64(head), repIR], 'f64')),
1573
+ asI64(tail)], 'f64')]]], 'f64')
1574
+ }
1575
+ inc('__str_replace')
1576
+ return typed(['call', '$__str_replace', asI64(emit(str)), asI64(emit(search)), asI64(emit(repl))], 'f64')
1577
+ })
1578
+ bind('.replaceAll', method('__str_replaceall', 'III'))
1248
1579
 
1249
- ctx.core.emit['.toLowerCase'] = (str) => {
1580
+ const caseMethod = (lo, hi, delta) => (str) => {
1250
1581
  inc('__str_case')
1251
- return typed(['call', '$__str_case', asI64(emit(str)), ['i32.const', 65], ['i32.const', 90], ['i32.const', 32]], 'f64')
1582
+ return typed(['call', '$__str_case', asI64(emit(str)), ['i32.const', lo], ['i32.const', hi], ['i32.const', delta]], 'f64')
1252
1583
  }
1584
+ bind('.toUpperCase', caseMethod(97, 122, -32))
1585
+ const _toLowerCase = caseMethod(65, 90, 32)
1586
+ bind('.toLowerCase', _toLowerCase)
1253
1587
 
1254
1588
  // Locale-specific casing needs ICU/CLDR data. jz intentionally has no
1255
1589
  // runtime, so this follows the existing ASCII-only lowercase helper and
1256
1590
  // ignores optional locale arguments.
1257
- ctx.core.emit['.toLocaleLowerCase'] = ctx.core.emit['.toLowerCase']
1591
+ bind('.toLocaleLowerCase', _toLowerCase)
1592
+
1593
+ const padMethod = (start) => (str, len, pad) => {
1594
+ inc('__str_pad')
1595
+ const vpad = pad != null ? asI64(emit(pad)) : ['i64.reinterpret_f64', mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1, 32)]
1596
+ return typed(['call', '$__str_pad', asI64(emit(str)), asI32(emit(len)), vpad, ['i32.const', start]], 'f64')
1597
+ }
1598
+ bind('.padStart', padMethod(1))
1599
+ bind('.padEnd', padMethod(0))
1258
1600
 
1259
1601
  // Byte-wise variant of String.prototype.localeCompare. Returns -1/0/1 from
1260
1602
  // an unsigned byte-by-byte compare with shorter-string-sorts-first tiebreak.
@@ -1263,21 +1605,21 @@ export default (ctx) => {
1263
1605
  // the spec exactly; for non-ASCII it follows UTF-8 byte order, which is
1264
1606
  // codepoint order for well-formed strings — close enough for sort-stability
1265
1607
  // use cases, wrong for human-language collation.
1266
- ctx.core.emit['.localeCompare'] = method('__str_cmp', 'II', 'i32')
1608
+ bind('.localeCompare', method('__str_cmp', 'II', 'i32'))
1267
1609
 
1268
- ctx.core.emit['.string:concat'] = (str, ...others) => {
1610
+ bind('.string:concat', (str, ...others) => {
1269
1611
  inc('__str_concat')
1270
1612
  let result = asF64(emit(str))
1271
1613
  for (const other of others) result = typed(['call', '$__str_concat', ['i64.reinterpret_f64', result], asI64(emit(other))], 'f64')
1272
1614
  return result
1273
- }
1615
+ })
1274
1616
 
1275
1617
  // 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
1618
+ // bool selects the interned "true"/"false" literal (constant-folded
1277
1619
  // 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))
1620
+ const partStrI64 = (p) => valTypeOf(p) === VAL.BOOL ? asI64(bool(p)) : toStrI64(p, emit(p))
1279
1621
 
1280
- ctx.core.emit['strcat'] = (...parts) => {
1622
+ bind('strcat', (...parts) => {
1281
1623
  inc('__to_str', '__str_byteLen', '__alloc', '__mkptr', '__str_copy')
1282
1624
  if (!parts.length) return mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT, 0)
1283
1625
  if (parts.length === 1) return typed(['f64.reinterpret_i64', partStrI64(parts[0])], 'f64')
@@ -1311,29 +1653,33 @@ export default (ctx) => {
1311
1653
  ['then', mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT, 0)],
1312
1654
  ['else', ['block', ['result', 'f64'], ...alloc]]])
1313
1655
  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
- }
1656
+ })
1327
1657
 
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
1658
+ // Shared: `$iLocal` in `[0, lenIR)`a 1-byte SSO string of the char at that
1659
+ // index; otherwise `oobIR`. Without the bounds check `__char_at` returns 0 for an
1660
+ // out-of-range index, which would wrap to a bogus `"\x00"` string (charAt → "",
1661
+ // at undefined). `sLocal` is the receiver as an f64 local.
1662
+ const charAtOr = (sLocal, iLocal, lenIR, oobIR) =>
1663
+ ['if', ['result', 'f64'],
1664
+ ['i32.and',
1665
+ ['i32.ge_s', ['local.get', iLocal], ['i32.const', 0]],
1666
+ ['i32.lt_s', ['local.get', iLocal], lenIR]],
1667
+ ['then', mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1,
1668
+ ['call', '$__char_at', ['i64.reinterpret_f64', ['local.get', sLocal]], ['local.get', iLocal]])],
1669
+ ['else', oobIR]]
1670
+ const emptyStr = mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT, 0)
1671
+
1672
+ // .charAt(i) → 1-char string at index i, or "" when out of range (JS spec: no
1673
+ // negative-index wraparound — a negative or >=length index yields "").
1674
+ bind('.charAt', (str, idx) => {
1675
+ inc('__char_at', '__str_byteLen')
1676
+ const s = temp('cs'), i = tempI32('ci')
1333
1677
  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
- }
1678
+ ['local.set', `$${s}`, asF64(emit(str))],
1679
+ ['local.set', `$${i}`, asI32(emit(idx))],
1680
+ charAtOr(`$${s}`, `$${i}`,
1681
+ ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${s}`]]], emptyStr)], 'f64')
1682
+ })
1337
1683
 
1338
1684
  // .charCodeAt(i) → JS-spec char code: the UTF-16 code unit at `i`, or NaN
1339
1685
  // when `i` is out of range (`i < 0 || i >= length`). Result is f64 because
@@ -1342,32 +1688,50 @@ export default (ctx) => {
1342
1688
  // loop `while ((cc = s.charCodeAt(i++)) <= 32) {}` would never terminate
1343
1689
  // (`0 <= 32` is true, `NaN <= 32` is false). The narrower may re-narrow the
1344
1690
  // 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')
1691
+ bind('.charCodeAt', (str, idx) =>
1692
+ typed(ctx.abi.string.ops.charCodeAt(asF64(emit(str)), asI32(emit(idx)), ctx, true), 'f64'))
1693
+
1694
+ // String.prototype.codePointAt(i) — byte-indexed.
1695
+ // jz strings are UTF-8 byte-arrays; `i` is a byte offset, not a UTF-16 code-unit
1696
+ // index. For ASCII inputs (U+0000..U+007F) the result is the exact Unicode code
1697
+ // point. For multi-byte sequences the result is the value of the leading byte only
1698
+ // (not a full code-point decode). Out-of-range → undefined (NaN-boxed). This is a
1699
+ // documented byte-semantics limitation; it keeps the implementation allocation-free
1700
+ // and consistent with jz's byte-indexed string model throughout.
1701
+ bind('.codePointAt', (str, idx) =>
1702
+ typed(ctx.abi.string.ops.charCodeAt(asF64(emit(str)), asI32(emit(idx)), ctx, true), 'f64'))
1347
1703
 
1348
1704
  // String.fromCharCode(code) → 1-char SSO string
1349
- ctx.core.emit['String'] = (value) => {
1705
+ bind('String', (value) => {
1350
1706
  if (value === undefined) return emit(['str', ''])
1351
1707
  if (valTypeOf(value) === VAL.STRING) return emit(value)
1352
- if (valTypeOf(value) === VAL.BOOL) return emitBoolStr(value)
1708
+ if (valTypeOf(value) === VAL.BOOL) return bool(value)
1353
1709
  if (valTypeOf(value) === VAL.NUMBER) {
1354
1710
  inc('__ftoa')
1355
1711
  return typed(['call', '$__ftoa', asF64(emit(value)), ['i32.const', 0], ['i32.const', 0]], 'f64')
1356
1712
  }
1357
1713
  return typed(['f64.reinterpret_i64', toStrI64(value, emit(value))], 'f64')
1358
- }
1714
+ })
1359
1715
 
1360
- ctx.core.emit['String.fromCharCode'] = (code) => {
1361
- if (code === undefined) return emit(['str', ''])
1716
+ // String.fromCharCode(...codes) variadic; each arg is ToUint16(ToNumber(code))
1717
+ // a 1-byte string, concatenated left to right (mirrors String.fromCodePoint).
1718
+ bind('String.fromCharCode', (...codes) => {
1719
+ if (codes.length === 0) return emit(['str', ''])
1362
1720
  // ToUint16(ToNumber(code)): `toNumF64` performs ToPrimitive on an object
1363
1721
  // argument, so a throwing valueOf/toString propagates per spec.
1364
- return mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1, asI32(toNumF64(code, emit(code))))
1365
- }
1722
+ const one = (node) => mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1, asI32(toNumF64(node, emit(node))))
1723
+ let r = one(codes[0])
1724
+ for (let i = 1; i < codes.length; i++) {
1725
+ inc('__str_concat_raw')
1726
+ r = typed(['call', '$__str_concat_raw', asI64(r), asI64(one(codes[i]))], 'f64')
1727
+ }
1728
+ return r
1729
+ })
1366
1730
 
1367
1731
  // String.fromCodePoint(cp) → UTF-8 encoded string for one code point.
1368
1732
  // Param is f64 (already ToNumber-coerced); throws RangeError ($__jz_err) when
1369
1733
  // 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)
1734
+ wat('__fromCodePoint', `(func $__fromCodePoint (param $cpf f64) (result f64)
1371
1735
  (local $cp i32) (local $off i32) (local $len i32)
1372
1736
  (if (i32.or
1373
1737
  (i32.or
@@ -1398,11 +1762,11 @@ export default (ctx) => {
1398
1762
  (i32.or (i32.const 0xF0) (i32.shr_u (local.get $cp) (i32.const 18)))
1399
1763
  (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
1764
  (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))))))`
1765
+ (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 24))))))`)
1402
1766
 
1403
1767
  // String.fromCodePoint(...codePoints) — variadic; each arg is ToNumber-coerced
1404
1768
  // then validated/encoded by __fromCodePoint, results concatenated left to right.
1405
- ctx.core.emit['String.fromCodePoint'] = (...codes) => {
1769
+ bind('String.fromCodePoint', (...codes) => {
1406
1770
  if (codes.length === 0) return emit(['str', ''])
1407
1771
  ctx.runtime.throws = true
1408
1772
  inc('__fromCodePoint')
@@ -1414,9 +1778,9 @@ export default (ctx) => {
1414
1778
  r = typed(['call', '$__str_concat_raw', asI64(r), asI64(one(codes[i]))], 'f64')
1415
1779
  }
1416
1780
  return r
1417
- }
1781
+ })
1418
1782
 
1419
- ctx.core.stdlib['__encodeURIComponent'] = `(func $__encodeURIComponent (param $val i64) (result f64)
1783
+ wat('__encodeURIComponent', `(func $__encodeURIComponent (param $val i64) (result f64)
1420
1784
  (local $str i64) (local $slen i32) (local $base i32) (local $out i32)
1421
1785
  (local $i i32) (local $j i32) (local $c i32) (local $hi i32) (local $lo i32)
1422
1786
  (local.set $str (call $__to_str (local.get $val)))
@@ -1458,24 +1822,24 @@ export default (ctx) => {
1458
1822
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
1459
1823
  (br $loop)))
1460
1824
  (i32.store (local.get $base) (local.get $j))
1461
- (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $out)))`
1825
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $out)))`)
1462
1826
 
1463
- ctx.core.emit['encodeURIComponent'] = (value) => {
1827
+ bind('encodeURIComponent', (value) => {
1464
1828
  inc('__encodeURIComponent')
1465
1829
  const input = value === undefined ? ['i64.const', UNDEF_NAN] : asI64(emit(value))
1466
1830
  return typed(['call', '$__encodeURIComponent', input], 'f64')
1467
- }
1831
+ })
1468
1832
 
1469
- ctx.core.stdlib['__uri_hex'] = `(func $__uri_hex (param $c i32) (result i32)
1833
+ wat('__uri_hex', `(func $__uri_hex (param $c i32) (result i32)
1470
1834
  (if (result i32) (i32.and (i32.ge_u (local.get $c) (i32.const 48)) (i32.le_u (local.get $c) (i32.const 57)))
1471
1835
  (then (i32.sub (local.get $c) (i32.const 48)))
1472
1836
  (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
1837
  (then (i32.sub (local.get $c) (i32.const 55)))
1474
1838
  (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
1839
  (then (i32.sub (local.get $c) (i32.const 87)))
1476
- (else (i32.const -1))))))))`
1840
+ (else (i32.const -1))))))))`)
1477
1841
 
1478
- ctx.core.stdlib['__decodeURIComponent'] = `(func $__decodeURIComponent (param $v i64) (result f64)
1842
+ wat('__decodeURIComponent', `(func $__decodeURIComponent (param $v i64) (result f64)
1479
1843
  (local $s i64) (local $len i32) (local $i i32)
1480
1844
  (local $base i32) (local $dst i32) (local $outLen i32)
1481
1845
  (local $c i32) (local $hi i32) (local $lo i32)
@@ -1551,38 +1915,40 @@ export default (ctx) => {
1551
1915
  (local.set $outLen (i32.add (local.get $outLen) (i32.const 1)))))
1552
1916
  (br $loop)))
1553
1917
  (i32.store (local.get $base) (local.get $outLen))
1554
- (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $dst)))`
1918
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $dst)))`)
1555
1919
 
1556
- ctx.core.emit['decodeURIComponent'] = (value) => {
1920
+ bind('decodeURIComponent', (value) => {
1557
1921
  ctx.runtime.throws = true
1558
1922
  inc('__decodeURIComponent')
1559
1923
  return typed(['call', '$__decodeURIComponent',
1560
1924
  value === undefined ? ['i64.const', UNDEF_NAN] : asI64(emit(value))], 'f64')
1561
- }
1925
+ })
1562
1926
 
1563
- // .at(i) → charAt with negative index support
1564
- ctx.core.emit['.string:at'] = (str, idx) => {
1927
+ // .at(i) → 1-char string at index i with negative-index support, or undefined
1928
+ // when out of range (JS spec: `i += length` for negative, then OOB → undefined).
1929
+ bind('.string:at', (str, idx) => {
1565
1930
  inc('__char_at', '__str_byteLen')
1566
- const t = tempI32('at'), s = temp('as')
1931
+ const t = tempI32('at'), s = temp('as'), len = tempI32('al')
1567
1932
  return typed(['block', ['result', 'f64'],
1568
1933
  ['local.set', `$${s}`, asF64(emit(str))],
1934
+ ['local.set', `$${len}`, ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${s}`]]]],
1569
1935
  ['local.set', `$${t}`, asI32(emit(idx))],
1570
1936
  // Negative index: t += length
1571
1937
  ['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
- }
1938
+ ['then', ['local.set', `$${t}`, ['i32.add', ['local.get', `$${t}`], ['local.get', `$${len}`]]]]],
1939
+ charAtOr(`$${s}`, `$${t}`, ['local.get', `$${len}`],
1940
+ ['f64.reinterpret_i64', ['i64.const', UNDEF_NAN]])], 'f64')
1941
+ })
1576
1942
 
1577
1943
  // .search(str) → indexOf (same as indexOf for string args)
1578
- ctx.core.emit['.search'] = (str, search) => {
1944
+ bind('.search', (str, search) => {
1579
1945
  inc('__str_indexof')
1580
1946
  return typed(['f64.convert_i32_s', ['call', '$__str_indexof', asI64(emit(str)), asI64(emit(search)), ['i32.const', 0]]], 'f64')
1581
- }
1947
+ })
1582
1948
 
1583
1949
  // .match(str) → [match] array if found, or 0 (null) if not
1584
1950
  // For string args, returns single-element array with the matched substring
1585
- ctx.core.emit['.match'] = (str, search) => {
1951
+ bind('.match', (str, search) => {
1586
1952
  inc('__str_indexof', '__str_slice', '__wrap1')
1587
1953
  const s = temp('ms'), q = temp('mq'), idx = tempI32('mi')
1588
1954
  // indexOf, then if >= 0, create 1-element array with the match slice
@@ -1599,24 +1965,24 @@ export default (ctx) => {
1599
1965
  ['call', '$__str_slice', ['i64.reinterpret_f64', ['local.get', `$${s}`]],
1600
1966
  ['local.get', `$${idx}`],
1601
1967
  ['i32.add', ['local.get', `$${idx}`], ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${q}`]]]]]]]]]], 'f64')
1602
- }
1968
+ })
1603
1969
 
1604
1970
  // __wrap1(val: i64) → f64 — create 1-element array [val]
1605
- ctx.core.stdlib['__wrap1'] = `(func $__wrap1 (param $val i64) (result f64)
1971
+ wat('__wrap1', `(func $__wrap1 (param $val i64) (result f64)
1606
1972
  (local $ptr i32)
1607
1973
  (local.set $ptr (call $__alloc (i32.const 16)))
1608
1974
  (i32.store (local.get $ptr) (i32.const 1))
1609
1975
  (i32.store (i32.add (local.get $ptr) (i32.const 4)) (i32.const 1))
1610
1976
  (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))))`
1977
+ (call $__mkptr (i32.const 1) (i32.const 0) (i32.add (local.get $ptr) (i32.const 8))))`)
1612
1978
 
1613
1979
  // 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')
1980
+ bind('TextEncoder', () => typed(['f64.const', 1], 'f64'))
1981
+ bind('TextDecoder', () => typed(['f64.const', 2], 'f64'))
1616
1982
 
1617
1983
  // .encode(str) → Uint8Array of string's UTF-8 bytes
1618
1984
  // 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)
1985
+ wat('__str_encode', `(func $__str_encode (param $str i64) (result f64)
1620
1986
  (local $len i32) (local $dst i32)
1621
1987
  (local.set $len (call $__str_byteLen (local.get $str)))
1622
1988
  (local.set $dst (call $__alloc (i32.add (i32.const 8) (local.get $len))))
@@ -1624,9 +1990,9 @@ export default (ctx) => {
1624
1990
  (i32.store (i32.add (local.get $dst) (i32.const 4)) (local.get $len))
1625
1991
  (local.set $dst (i32.add (local.get $dst) (i32.const 8)))
1626
1992
  (call $__str_copy (local.get $str) (local.get $dst) (local.get $len))
1627
- (call $__mkptr (i32.const 3) (i32.const 1) (local.get $dst)))`
1993
+ (call $__mkptr (i32.const 3) (i32.const 1) (local.get $dst)))`)
1628
1994
 
1629
- ctx.core.emit['.encode'] = (obj, str) => {
1995
+ bind('.encode', (obj, str) => {
1630
1996
  inc('__str_encode')
1631
1997
  // .encode() yields a runtime PTR.TYPED/u8 array (see __mkptr above). Downstream
1632
1998
  // indexing/spread dispatch through __typed_idx, whose element-unaware fallback
@@ -1634,10 +2000,10 @@ export default (ctx) => {
1634
2000
  // the feature pulls the element-aware variant — same invariant `.length` follows.
1635
2001
  ctx.features.typedarray = true
1636
2002
  return typed(['call', '$__str_encode', asI64(emit(str))], 'f64')
1637
- }
2003
+ })
1638
2004
 
1639
2005
  // .decode(uint8arr) → string from byte data
1640
- ctx.core.stdlib['__bytes_decode'] = `(func $__bytes_decode (param $arr i64) (result f64)
2006
+ wat('__bytes_decode', `(func $__bytes_decode (param $arr i64) (result f64)
1641
2007
  (local $off i32) (local $len i32) (local $dst i32)
1642
2008
  (local.set $off (call $__ptr_offset (local.get $arr)))
1643
2009
  (local.set $len (call $__len (local.get $arr)))
@@ -1645,10 +2011,10 @@ export default (ctx) => {
1645
2011
  (i32.store (local.get $dst) (local.get $len))
1646
2012
  (local.set $dst (i32.add (local.get $dst) (i32.const 4)))
1647
2013
  (memory.copy (local.get $dst) (local.get $off) (local.get $len))
1648
- (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $dst)))`
2014
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $dst)))`)
1649
2015
 
1650
- ctx.core.emit['.decode'] = (obj, arr) => {
2016
+ bind('.decode', (obj, arr) => {
1651
2017
  inc('__bytes_decode')
1652
2018
  return typed(['call', '$__bytes_decode', asI64(emit(arr))], 'f64')
1653
- }
2019
+ })
1654
2020
  }