jz 0.6.0 → 0.8.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 (63) hide show
  1. package/README.md +99 -72
  2. package/bench/README.md +253 -100
  3. package/bench/bench.svg +58 -81
  4. package/cli.js +85 -12
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6196 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +222 -35
  9. package/interop.js +240 -141
  10. package/layout.js +34 -18
  11. package/module/array.js +99 -111
  12. package/module/collection.js +124 -30
  13. package/module/console.js +1 -1
  14. package/module/core.js +163 -19
  15. package/module/json.js +3 -3
  16. package/module/math.js +162 -3
  17. package/module/number.js +268 -13
  18. package/module/object.js +37 -5
  19. package/module/regex.js +8 -7
  20. package/module/simd.js +37 -5
  21. package/module/string.js +203 -168
  22. package/module/typedarray.js +565 -112
  23. package/package.json +26 -7
  24. package/src/abi/string.js +29 -29
  25. package/src/ast.js +19 -2
  26. package/src/autoload.js +3 -0
  27. package/src/compile/analyze-scans.js +174 -11
  28. package/src/compile/analyze.js +101 -4
  29. package/src/compile/cse-load.js +200 -0
  30. package/src/compile/emit-assign.js +82 -20
  31. package/src/compile/emit.js +592 -51
  32. package/src/compile/index.js +504 -89
  33. package/src/compile/infer.js +36 -1
  34. package/src/compile/loop-divmod.js +109 -0
  35. package/src/compile/loop-model.js +91 -0
  36. package/src/compile/loop-square.js +102 -0
  37. package/src/compile/narrow.js +275 -41
  38. package/src/compile/peel-stencil.js +215 -0
  39. package/src/compile/plan/advise.js +55 -1
  40. package/src/compile/plan/common.js +29 -0
  41. package/src/compile/plan/index.js +8 -1
  42. package/src/compile/plan/inline.js +180 -22
  43. package/src/compile/plan/literals.js +313 -24
  44. package/src/compile/plan/scope.js +115 -39
  45. package/src/compile/program-facts.js +21 -2
  46. package/src/ctx.js +96 -19
  47. package/src/helper-counters.js +137 -0
  48. package/src/ir.js +157 -20
  49. package/src/kind-traits.js +34 -3
  50. package/src/kind.js +79 -4
  51. package/src/op-policy.js +5 -2
  52. package/src/ops.js +119 -0
  53. package/src/optimize/index.js +1274 -151
  54. package/src/optimize/recurse.js +182 -0
  55. package/src/optimize/vectorize.js +4187 -253
  56. package/src/prepare/index.js +75 -14
  57. package/src/prepare/lift-iife.js +149 -0
  58. package/src/reps.js +6 -2
  59. package/src/static.js +9 -0
  60. package/src/type.js +63 -51
  61. package/src/wat/assemble.js +286 -21
  62. package/src/widen.js +21 -0
  63. package/src/wat/optimize.js +0 -3760
package/module/string.js CHANGED
@@ -22,6 +22,38 @@ import { ssoBitI64Hex, sliceBitI64Hex, ptrNanHex, STR_INTERN_BIT } from '../layo
22
22
  const SSO_BIT_I64 = ssoBitI64Hex()
23
23
  const SLICE_BIT_I64 = sliceBitI64Hex()
24
24
 
25
+ // === SSO codec — single source of truth for the inline-string bit layout ===
26
+ // ASCII chars packed at 7 bits each into the NaN-box payload: char i at payload bit
27
+ // i*7 (uniform `(ptr >> i*7) & 0x7f`), length at payload bits 42-44, SSO_BIT at bit 46
28
+ // (SLICE_BIT at 45 stays 0). 6 chars fit (6*7=42, + 3-bit len). ASCII-only — a byte
29
+ // ≥0x80 falls back to a heap string. The 7-bit-uniform layout makes equal short strings
30
+ // content-bit-equal (so `op === 'tag'` is a bare i64.eq) and never touches memory.
31
+ const MAX_SSO = 6
32
+ const SSO_LEN_SHIFT = 10 // length occupies aux bits 10-12 (= payload bits 42-44)
33
+ // JS: ASCII string → { aux, offset }, or null when ineligible (too long / non-ASCII).
34
+ // BigInt-free (i32 ops only) so the self-hosted compiler — which runs this to encode
35
+ // every string literal — stays on jz's numeric core. offset = payload bits 0-31, aux =
36
+ // bits 32-46; char i at bit i*7 (chars 0-3 fit the offset, char 4 straddles, char 5 → aux).
37
+ const ssoEncode = (str) => {
38
+ if (str.length > MAX_SSO || !/^[\x00-\x7f]*$/.test(str)) return null
39
+ let offset = 0, auxChars = 0
40
+ for (let i = 0; i < str.length; i++) {
41
+ const c = str.charCodeAt(i), bit = i * 7
42
+ if (bit <= 24) offset |= c << bit // chars 0-3 (bits 0-27), wholly in offset
43
+ else if (bit < 32) { offset |= (c & 0xF) << 28; auxChars |= c >> 4 } // char 4: bits 28-34 straddle
44
+ else auxChars |= c << (bit - 32) // char 5: bits 35-41 → aux bits 3-9
45
+ }
46
+ return { aux: LAYOUT.SSO_BIT | (str.length << SSO_LEN_SHIFT) | auxChars, offset: offset >>> 0 }
47
+ }
48
+ // aux for an SSO string whose chars all fit in the offset (len ≤ 4: 4*7=28 ≤ 32 bits).
49
+ const ssoAux = (len) => LAYOUT.SSO_BIT | (len << SSO_LEN_SHIFT)
50
+ // WAT: char i (i32 expr) of SSO ptr (i64 expr) — 7-bit at payload bit i*7.
51
+ const ssoCharWat = (ptr, i) => `(i32.wrap_i64 (i64.and (i64.shr_u ${ptr} (i64.mul (i64.extend_i32_u ${i}) (i64.const 7))) (i64.const 0x7f)))`
52
+ // WAT: length (i32) of SSO ptr (i64 expr) — payload bits 42-44.
53
+ const ssoLenWat = (ptr) => `(i32.wrap_i64 (i64.and (i64.shr_u ${ptr} (i64.const 42)) (i64.const 7)))`
54
+ // WAT: length (i32) from an already-extracted aux (i32 expr).
55
+ const ssoLenFromAux = (aux) => `(i32.and (i32.shr_u ${aux} (i32.const ${SSO_LEN_SHIFT})) (i32.const 7))`
56
+
25
57
  // WAT (no-locals expression): byte length of a heap STRING given its raw $-local
26
58
  // names for the offset and the i64 ptr. A view (SLICE_BIT) carries its length in
27
59
  // aux[12:0]; an own heap string reads the i32 header at off-4. Callers must have
@@ -43,26 +75,28 @@ const heapLenExpr = (ptrLocal, offLocal) => `(if (result i32)
43
75
  // bit-equal (SSO content IS the bits). Bails to the caller's heap path on a
44
76
  // non-ASCII byte (SSO is ASCII-only). Locals: $sb scratch byte, $sp packed.
45
77
  const sliceSsoPackWat = () => `
46
- (if (i32.le_u (local.get $nlen) (i32.const 4))
78
+ (if (i32.le_u (local.get $nlen) (i32.const ${MAX_SSO}))
47
79
  (then (block $heap8
48
80
  (local.set $srcOff (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
49
81
  (local.set $isSso (i32.wrap_i64 (i64.shr_u
50
82
  (i64.and (local.get $ptr) (i64.const ${SSO_BIT_I64}))
51
83
  (i64.const ${LAYOUT.AUX_SHIFT}))))
52
- (local.set $i (i32.const 0)) (local.set $sp (i32.const 0))
53
- (loop $pk8
84
+ (local.set $i (i32.const 0)) (local.set $sp64 (i64.const 0))
85
+ (loop $pk7
54
86
  (if (i32.lt_u (local.get $i) (local.get $nlen))
55
87
  (then
56
88
  (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)))
89
+ (then ${ssoCharWat('(local.get $ptr)', '(i32.add (local.get $start) (local.get $i))')})
60
90
  (else (i32.load8_u (i32.add (i32.add (local.get $srcOff) (local.get $start)) (local.get $i))))))
61
91
  (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)))))
92
+ (local.set $sp64 (i64.or (local.get $sp64)
93
+ (i64.shl (i64.extend_i32_u (local.get $sb)) (i64.mul (i64.extend_i32_u (local.get $i)) (i64.const 7)))))
63
94
  (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))))))`
95
+ (br $pk7))))
96
+ (return (f64.reinterpret_i64 (i64.or (i64.or
97
+ (i64.const ${ptrNanHex(PTR.STRING, LAYOUT.SSO_BIT)})
98
+ (i64.shl (i64.extend_i32_u (local.get $nlen)) (i64.const 42)))
99
+ (local.get $sp64)))))))`
66
100
 
67
101
  // Probe the static intern index (buildInternTable, compile/index.js): a 5..32
68
102
  // byte slice whose content equals a static literal returns the CANONICAL
@@ -112,10 +146,15 @@ export default (ctx) => {
112
146
  __str_concat_raw: ['__str_byteLen', '__alloc', '__memgrow', '__mkptr', '__str_copy'],
113
147
  __str_append_byte: ['__str_byteLen', '__alloc', '__memgrow', '__mkptr', '__str_copy'],
114
148
  __str_copy: [],
115
- __str_slice: ['__str_byteLen', '__alloc'],
116
- __str_slice_view: ['__str_byteLen', '__mkptr', '__str_slice'],
117
- __str_indexof: ['__str_byteLen', '__to_str'],
118
- __str_lastindexof: ['__str_byteLen', '__to_str'],
149
+ // __str_slice/_view are FUNCTION templates: resolveIncludes' auto-dep scan realizes the
150
+ // factory (v()) to discover body calls, but that realization DIVERGES under self-host
151
+ // (jz.wasm) — so a body-called helper not also listed here is dropped from the kernel
152
+ // ("Unknown func $__clamp_idx" on `str.slice`). FN templates must declare body deps
153
+ // manually; pinned by test/selfhost-includes.js.
154
+ __str_slice: ['__str_byteLen', '__alloc', '__clamp_idx', '__mkptr'],
155
+ __str_slice_view: ['__str_byteLen', '__mkptr', '__str_slice', '__clamp_idx'],
156
+ __str_indexof: ['__str_byteLen'],
157
+ __str_lastindexof: ['__str_byteLen'],
119
158
  __wrap1: ['__alloc', '__mkptr'],
120
159
  __str_substring: ['__str_slice'],
121
160
  __str_startswith: ['__str_byteLen'],
@@ -128,15 +167,15 @@ export default (ctx) => {
128
167
  __str_replace: ['__str_indexof', '__str_slice', '__str_concat'],
129
168
  __str_replaceall: ['__str_indexof', '__str_slice', '__str_concat'],
130
169
  __str_split: ['__str_slice', '__str_byteLen', '__char_at', '__alloc'],
131
- __str_idx: [],
170
+ __str_idx: ['__char1byte'],
132
171
  __str_eq: ['__str_eq_cold'],
133
172
  __str_eq_cold: ['__char_at', '__str_byteLen'],
134
173
  __str_cmp: ['__char_at', '__str_byteLen'],
135
174
  __str_range_eq: ['__char_at', '__str_byteLen'],
136
175
  __str_substring_eq: ['__str_byteLen', '__str_range_eq'],
137
- __str_slice_eq: ['__str_byteLen', '__str_range_eq'],
176
+ __str_slice_eq: ['__str_byteLen', '__str_range_eq', '__clamp_idx'], // body-calls __clamp_idx; declare it (self-host auto-scan unreliable — test/selfhost-includes.js)
138
177
  __str_pad: ['__str_byteLen', '__str_copy', '__alloc'],
139
- __str_join: ['__str_concat', '__to_str', '__str_byteLen', '__len', '__ptr_offset'],
178
+ __str_join: ['__str_concat', '__to_str', '__str_byteLen', '__len', '__ptr_offset', '__mkptr'], // FN template: __mkptr body-called, must be manual (self-host auto-scan diverges)
140
179
  __str_encode: ['__str_byteLen', '__str_copy'],
141
180
  __encodeURIComponent: ['__to_str', '__str_byteLen', '__char_at', '__alloc', '__mkptr'],
142
181
  __decodeURIComponent: ['__to_str', '__str_byteLen', '__char_at', '__alloc', '__mkptr', '__uri_hex'],
@@ -145,16 +184,26 @@ export default (ctx) => {
145
184
  __str_byteLen: ['__ptr_type', '__ptr_aux', '__str_len'],
146
185
  })
147
186
 
187
+ // String *runtime* construction (concat/slice/split/`String(n)`/shared-memory
188
+ // pools) allocates and boxes pointers, so the module eagerly declares its core
189
+ // heap deps — same as object.js / typedarray.js / array.js. Several such helpers
190
+ // (`__str_slice`, `__str_split`, `__str_case`, …) call `$__mkptr`/`$__alloc` only
191
+ // through nested template thunks (sliceSsoPackWat/internProbeWat); resolveIncludes'
192
+ // auto-derivation realizes those thunks to recover the dep, but that re-realization
193
+ // is not reliable under self-host (the jz.wasm kernel re-runs the thunk late, with
194
+ // intern/heap state the native run never reaches), so a runtime string op would
195
+ // emit `call $__mkptr` with no definition. Declaring the deps explicitly here makes
196
+ // inclusion independent of thunk re-realization. Reachability pruning still drops
197
+ // both for a literals-only module (no `__mkptr`/`__alloc` call site → unreachable),
198
+ // so a heap-free program stays heap-free — no allocator, no memory, no exports.
148
199
  inc('__mkptr', '__alloc')
149
200
 
150
- // === String literal: "abc" → SSO if ≤4 ASCII, else heap ===
201
+ // === String literal: "abc" → SSO if ≤4 ASCII, else static data ===
151
202
 
152
203
  bind('str', (str) => {
153
- const MAX_SSO = 4
154
- if (ctx.features.sso && str.length <= MAX_SSO && /^[\x00-\x7f]*$/.test(str)) {
155
- let packed = 0
156
- for (let i = 0; i < str.length; i++) packed |= str.charCodeAt(i) << (i * 8)
157
- return mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | str.length, packed)
204
+ if (ctx.features.sso) {
205
+ const sso = ssoEncode(str)
206
+ if (sso) return mkPtrIR(PTR.STRING, sso.aux, sso.offset)
158
207
  }
159
208
  const bytes = new TextEncoder().encode(str)
160
209
  const len = bytes.length
@@ -211,11 +260,7 @@ export default (ctx) => {
211
260
  // SSO/STRING ptrs never have forwarding pointers (only ARRAY does), so we extract
212
261
  // the raw offset directly instead of paying the __ptr_offset function-call overhead.
213
262
  wat('__sso_char', `(func $__sso_char (param $ptr i64) (param $i i32) (result i32)
214
- (i32.and
215
- (i32.shr_u
216
- (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK})))
217
- (i32.mul (local.get $i) (i32.const 8)))
218
- (i32.const 0xFF)))`)
263
+ ${ssoCharWat('(local.get $ptr)', '(local.get $i)')})`)
219
264
 
220
265
  wat('__str_char', `(func $__str_char (param $ptr i64) (param $i i32) (result i32)
221
266
  (i32.load8_u (i32.add
@@ -242,16 +287,9 @@ export default (ctx) => {
242
287
  (i64.ne (i64.and (local.get $ptr) (i64.const ${SSO_BIT_I64})) (i64.const 0))
243
288
  (then
244
289
  (if (result i32)
245
- (i32.ge_u (local.get $i)
246
- (i32.and
247
- (i32.wrap_i64 (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.AUX_SHIFT})))
248
- (i32.const ${LAYOUT.SSO_BIT - 1})))
290
+ (i32.ge_u (local.get $i) ${ssoLenWat('(local.get $ptr)')})
249
291
  (then (i32.const 0))
250
- (else (i32.and
251
- (i32.shr_u
252
- (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK})))
253
- (i32.mul (local.get $i) (i32.const 8)))
254
- (i32.const 0xFF)))))
292
+ (else ${ssoCharWat('(local.get $ptr)', '(local.get $i)')})))
255
293
  (else
256
294
  (if (result i32)
257
295
  (i32.ge_u (local.get $i)
@@ -289,9 +327,7 @@ export default (ctx) => {
289
327
  (i32.const ${LAYOUT.SSO_BIT})))
290
328
  (local.set $len
291
329
  (if (result i32) (local.get $isSso)
292
- (then (i32.and
293
- (i32.wrap_i64 (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.AUX_SHIFT})))
294
- (i32.const ${LAYOUT.SSO_BIT - 1})))
330
+ (then ${ssoLenWat('(local.get $ptr)')})
295
331
  (else
296
332
  (if (result i32)
297
333
  (i64.ne (i64.and (local.get $ptr) (i64.const ${SLICE_BIT_I64})) (i64.const 0))
@@ -306,14 +342,15 @@ export default (ctx) => {
306
342
  (i32.or (i32.lt_s (local.get $i) (i32.const 0)) (i32.ge_u (local.get $i) (local.get $len)))
307
343
  (then (f64.const nan:${UNDEF_NAN}))
308
344
  (else
309
- (f64.reinterpret_i64
310
- (i64.or
311
- ;; mkptr(STRING, SSO_BIT|1, 0) = NAN_PREFIX | (STRING<<TAG_SHIFT) | ((SSO_BIT|1)<<AUX_SHIFT)
312
- (i64.const ${ptrNanHex(PTR.STRING, LAYOUT.SSO_BIT | 1)})
313
- (i64.extend_i32_u
314
- (if (result i32) (local.get $isSso)
315
- (then (i32.and (i32.shr_u (local.get $off) (i32.mul (local.get $i) (i32.const 8))) (i32.const 0xFF)))
316
- (else (i32.load8_u (i32.add (local.get $off) (local.get $i)))))))))))`)
345
+ (if (result f64) (local.get $isSso)
346
+ ;; SSO source: chars are ASCII (<0x80) → inline 1-char SSO (hot path, no branch).
347
+ (then (f64.reinterpret_i64
348
+ (i64.or
349
+ (i64.const ${ptrNanHex(PTR.STRING, ssoAux(1))})
350
+ (i64.extend_i32_u ${ssoCharWat('(local.get $ptr)', '(local.get $i)')}))))
351
+ ;; Heap source: the byte may be ≥0x80 (non-ASCII), which can't be a 7-bit SSO,
352
+ ;; so __char1byte routes those to a heap 1-byte string.
353
+ (else (call $__char1byte (i32.load8_u (i32.add (local.get $off) (local.get $i)))))))))`)
317
354
 
318
355
  // Hot: ~53M calls in watr self-host. Bit-eq covers identity. SSO/SSO with !bit-eq
319
356
  // guarantees content differs (high 32 bits encode type+len; both equal → low 32 differs
@@ -360,9 +397,12 @@ export default (ctx) => {
360
397
  (local $ta i32) (local $tb i32)
361
398
  (local $offA i32) (local $offB i32)
362
399
  (local $ssoA i32) (local $ssoB i32)
363
- (local $axA i32) (local $axB i32)
364
- (if (i64.eq (local.get $a) (local.get $b))
365
- (then (return (i32.const 1))))
400
+ ;; Sole caller is __str_eq, which already returned for bit-equal pointers, both-SSO
401
+ ;; (bit-ne content-ne), both-canonical-interned (bit-ne ⇒ content-ne) and both-plain-
402
+ ;; heap length mismatch. Re-testing them here is dead work on every cold call — skip to
403
+ ;; the byte walk. (__str_eq/__str_eq_cold are ~14% of self-host runtime; this trims the
404
+ ;; per-call fixed cost.) The heap/mixed paths below still decide every case correctly
405
+ ;; on their own, so this stays correct even if a future caller skips the prelude.
366
406
  (local.set $ta (i32.wrap_i64 (i64.and (i64.shr_u (local.get $a) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
367
407
  (local.set $tb (i32.wrap_i64 (i64.and (i64.shr_u (local.get $b) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
368
408
  (local.set $offA (i32.wrap_i64 (i64.and (local.get $a) (i64.const ${LAYOUT.OFFSET_MASK}))))
@@ -373,22 +413,6 @@ export default (ctx) => {
373
413
  (local.set $ssoB (i32.and
374
414
  (i32.wrap_i64 (i64.shr_u (local.get $b) (i64.const ${LAYOUT.AUX_SHIFT})))
375
415
  (i32.const ${LAYOUT.SSO_BIT})))
376
- ;; Both SSO with !bit-eq ⇒ content differs (high 32 bits hold tag+aux; both equal here).
377
- (if (i32.and (local.get $ssoA) (local.get $ssoB))
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))))
392
416
  ;; Both heap STRING fast path: inline len from header. Chunk by 4 bytes via unaligned
393
417
  ;; i32.load (wasm guarantees unaligned-OK), then byte-tail. Most string comparisons fail
394
418
  ;; early on the first 4-byte word, so this collapses the per-byte branch overhead into a
@@ -477,7 +501,7 @@ export default (ctx) => {
477
501
  (i32.wrap_i64 (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.AUX_SHIFT})))
478
502
  (i32.const ${LAYOUT.AUX_MASK})))
479
503
  (if (result i32) (i32.and (local.get $aux) (i32.const ${LAYOUT.SSO_BIT}))
480
- (then (i32.and (local.get $aux) (i32.const ${LAYOUT.SSO_BIT - 1})))
504
+ (then ${ssoLenFromAux('(local.get $aux)')})
481
505
  (else
482
506
  (if (result i32) (i32.and (local.get $aux) (i32.const ${LAYOUT.SLICE_BIT}))
483
507
  ;; view: length lives in aux[12:0], not a header.
@@ -495,21 +519,11 @@ export default (ctx) => {
495
519
  // (single bulk op instead of nlen × __char_at).
496
520
  wat('__str_slice', () => `(func $__str_slice (param $ptr i64) (param $start i32) (param $end i32) (result f64)
497
521
  (local $len i32) (local $nlen i32) (local $off i32) (local $i i32)
498
- (local $srcOff i32) (local $isSso i32) (local $sb i32) (local $sp i32)
522
+ (local $srcOff i32) (local $isSso i32) (local $sb i32) (local $sp i32) (local $sp64 i64)
499
523
  (local $ip i32) (local $h i32) (local $j i32) (local $slot i32) (local $cand i32) (local $k i32)
500
524
  (local.set $len (call $__str_byteLen (local.get $ptr)))
501
- (if (i32.lt_s (local.get $start) (i32.const 0))
502
- (then (local.set $start (i32.add (local.get $len) (local.get $start)))))
503
- (if (i32.lt_s (local.get $end) (i32.const 0))
504
- (then (local.set $end (i32.add (local.get $len) (local.get $end)))))
505
- (if (i32.lt_s (local.get $start) (i32.const 0))
506
- (then (local.set $start (i32.const 0))))
507
- (if (i32.gt_s (local.get $start) (local.get $len))
508
- (then (local.set $start (local.get $len))))
509
- (if (i32.lt_s (local.get $end) (i32.const 0))
510
- (then (local.set $end (i32.const 0))))
511
- (if (i32.gt_s (local.get $end) (local.get $len))
512
- (then (local.set $end (local.get $len))))
525
+ (local.set $start (call $__clamp_idx (local.get $start) (local.get $len)))
526
+ (local.set $end (call $__clamp_idx (local.get $end) (local.get $len)))
513
527
  (if (i32.ge_s (local.get $start) (local.get $end))
514
528
  (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
515
529
  (local.set $nlen (i32.sub (local.get $end) (local.get $start)))
@@ -548,18 +562,8 @@ export default (ctx) => {
548
562
  (local $len i32) (local $nlen i32) (local $srcOff i32) (local $tag i32)
549
563
  (local $ip i32) (local $h i32) (local $j i32) (local $slot i32) (local $cand i32) (local $k i32)
550
564
  (local.set $len (call $__str_byteLen (local.get $ptr)))
551
- (if (i32.lt_s (local.get $start) (i32.const 0))
552
- (then (local.set $start (i32.add (local.get $len) (local.get $start)))))
553
- (if (i32.lt_s (local.get $end) (i32.const 0))
554
- (then (local.set $end (i32.add (local.get $len) (local.get $end)))))
555
- (if (i32.lt_s (local.get $start) (i32.const 0))
556
- (then (local.set $start (i32.const 0))))
557
- (if (i32.gt_s (local.get $start) (local.get $len))
558
- (then (local.set $start (local.get $len))))
559
- (if (i32.lt_s (local.get $end) (i32.const 0))
560
- (then (local.set $end (i32.const 0))))
561
- (if (i32.gt_s (local.get $end) (local.get $len))
562
- (then (local.set $end (local.get $len))))
565
+ (local.set $start (call $__clamp_idx (local.get $start) (local.get $len)))
566
+ (local.set $end (call $__clamp_idx (local.get $end) (local.get $len)))
563
567
  (if (i32.ge_s (local.get $start) (local.get $end))
564
568
  (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
565
569
  (local.set $nlen (i32.sub (local.get $end) (local.get $start)))
@@ -664,18 +668,8 @@ export default (ctx) => {
664
668
  wat('__str_slice_eq', `(func $__str_slice_eq (param $ptr i64) (param $start i32) (param $end i32) (param $other i64) (result i32)
665
669
  (local $len i32)
666
670
  (local.set $len (call $__str_byteLen (local.get $ptr)))
667
- (if (i32.lt_s (local.get $start) (i32.const 0))
668
- (then (local.set $start (i32.add (local.get $len) (local.get $start)))))
669
- (if (i32.lt_s (local.get $end) (i32.const 0))
670
- (then (local.set $end (i32.add (local.get $len) (local.get $end)))))
671
- (if (i32.lt_s (local.get $start) (i32.const 0))
672
- (then (local.set $start (i32.const 0))))
673
- (if (i32.gt_s (local.get $start) (local.get $len))
674
- (then (local.set $start (local.get $len))))
675
- (if (i32.lt_s (local.get $end) (i32.const 0))
676
- (then (local.set $end (i32.const 0))))
677
- (if (i32.gt_s (local.get $end) (local.get $len))
678
- (then (local.set $end (local.get $len))))
671
+ (local.set $start (call $__clamp_idx (local.get $start) (local.get $len)))
672
+ (local.set $end (call $__clamp_idx (local.get $end) (local.get $len)))
679
673
  (call $__str_range_eq (local.get $ptr) (local.get $start) (local.get $end) (local.get $other)))`)
680
674
 
681
675
  // Hoist SSO/heap dispatch for hay and ndl out of the inner byte loop. Inner
@@ -684,8 +678,8 @@ export default (ctx) => {
684
678
  (local $hlen i32) (local $nlen i32) (local $i i32) (local $j i32) (local $match i32)
685
679
  (local $hoff i32) (local $noff i32)
686
680
  (local $hsso i32) (local $nsso i32) (local $hb i32) (local $nb i32) (local $k i32)
687
- ;; ToString the search value (21.1.3.9 step 4) coerces undefined/null/number/etc.
688
- (local.set $ndl (call $__to_str (local.get $ndl)))
681
+ ;; The search value is ToString'd at the call site (searchArg) per 21.1.3.9 step 4 —
682
+ ;; a known-string needle passes raw, so this helper carries no float-pulling __to_str.
689
683
  (local.set $hlen (call $__str_byteLen (local.get $hay)))
690
684
  (local.set $nlen (call $__str_byteLen (local.get $ndl)))
691
685
  (if (i32.eqz (local.get $nlen)) (then (return (local.get $from))))
@@ -707,10 +701,10 @@ export default (ctx) => {
707
701
  (br_if $nomatch (i32.ge_s (local.get $j) (local.get $nlen)))
708
702
  (local.set $k (i32.add (local.get $i) (local.get $j)))
709
703
  (local.set $hb (if (result i32) (local.get $hsso)
710
- (then (i32.and (i32.shr_u (local.get $hoff) (i32.shl (local.get $k) (i32.const 3))) (i32.const 0xFF)))
704
+ (then ${ssoCharWat('(local.get $hay)', '(local.get $k)')})
711
705
  (else (i32.load8_u (i32.add (local.get $hoff) (local.get $k))))))
712
706
  (local.set $nb (if (result i32) (local.get $nsso)
713
- (then (i32.and (i32.shr_u (local.get $noff) (i32.shl (local.get $j) (i32.const 3))) (i32.const 0xFF)))
707
+ (then ${ssoCharWat('(local.get $ndl)', '(local.get $j)')})
714
708
  (else (i32.load8_u (i32.add (local.get $noff) (local.get $j))))))
715
709
  (if (i32.ne (local.get $hb) (local.get $nb))
716
710
  (then (local.set $match (i32.const 0)) (br $nomatch)))
@@ -729,8 +723,7 @@ export default (ctx) => {
729
723
  (local $hlen i32) (local $nlen i32) (local $i i32) (local $j i32) (local $match i32)
730
724
  (local $hoff i32) (local $noff i32)
731
725
  (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)))
726
+ ;; The search value is ToString'd at the call site (searchArg) per 21.1.3.10 step 4.
734
727
  (local.set $hlen (call $__str_byteLen (local.get $hay)))
735
728
  (local.set $nlen (call $__str_byteLen (local.get $ndl)))
736
729
  ;; Empty needle always matches at the clamp(from,0,hlen) position
@@ -760,10 +753,10 @@ export default (ctx) => {
760
753
  (br_if $nomatch (i32.ge_s (local.get $j) (local.get $nlen)))
761
754
  (local.set $k (i32.add (local.get $i) (local.get $j)))
762
755
  (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)))
756
+ (then ${ssoCharWat('(local.get $hay)', '(local.get $k)')})
764
757
  (else (i32.load8_u (i32.add (local.get $hoff) (local.get $k))))))
765
758
  (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)))
759
+ (then ${ssoCharWat('(local.get $ndl)', '(local.get $j)')})
767
760
  (else (i32.load8_u (i32.add (local.get $noff) (local.get $j))))))
768
761
  (if (i32.ne (local.get $hb) (local.get $nb))
769
762
  (then (local.set $match (i32.const 0)) (br $nomatch)))
@@ -793,10 +786,10 @@ export default (ctx) => {
793
786
  (br_if $done (i32.ge_s (local.get $i) (local.get $plen)))
794
787
  (if (i32.ne
795
788
  (if (result i32) (local.get $ssso)
796
- (then (i32.and (i32.shr_u (local.get $soff) (i32.shl (local.get $i) (i32.const 3))) (i32.const 0xFF)))
789
+ (then ${ssoCharWat('(local.get $str)', '(local.get $i)')})
797
790
  (else (i32.load8_u (i32.add (local.get $soff) (local.get $i)))))
798
791
  (if (result i32) (local.get $psso)
799
- (then (i32.and (i32.shr_u (local.get $poff) (i32.shl (local.get $i) (i32.const 3))) (i32.const 0xFF)))
792
+ (then ${ssoCharWat('(local.get $pfx)', '(local.get $i)')})
800
793
  (else (i32.load8_u (i32.add (local.get $poff) (local.get $i))))))
801
794
  (then (return (i32.const 0))))
802
795
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
@@ -824,10 +817,10 @@ export default (ctx) => {
824
817
  (local.set $k (i32.add (local.get $off) (local.get $i)))
825
818
  (if (i32.ne
826
819
  (if (result i32) (local.get $ssso)
827
- (then (i32.and (i32.shr_u (local.get $soff) (i32.shl (local.get $k) (i32.const 3))) (i32.const 0xFF)))
820
+ (then ${ssoCharWat('(local.get $str)', '(local.get $k)')})
828
821
  (else (i32.load8_u (i32.add (local.get $soff) (local.get $k)))))
829
822
  (if (result i32) (local.get $fsso)
830
- (then (i32.and (i32.shr_u (local.get $foff) (i32.shl (local.get $i) (i32.const 3))) (i32.const 0xFF)))
823
+ (then ${ssoCharWat('(local.get $sfx)', '(local.get $i)')})
831
824
  (else (i32.load8_u (i32.add (local.get $foff) (local.get $i))))))
832
825
  (then (return (i32.const 0))))
833
826
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
@@ -849,9 +842,7 @@ export default (ctx) => {
849
842
  (then
850
843
  (block $dsso (loop $lsso
851
844
  (br_if $dsso (i32.ge_s (local.get $i) (local.get $len)))
852
- (local.set $c (i32.and
853
- (i32.shr_u (local.get $srcOff) (i32.shl (local.get $i) (i32.const 3)))
854
- (i32.const 0xFF)))
845
+ (local.set $c ${ssoCharWat('(local.get $ptr)', '(local.get $i)')})
855
846
  (if (i32.and (i32.ge_u (local.get $c) (local.get $lo)) (i32.le_u (local.get $c) (local.get $hi)))
856
847
  (then (local.set $c (i32.add (local.get $c) (local.get $delta)))))
857
848
  (i32.store8 (i32.add (local.get $off) (local.get $i)) (local.get $c))
@@ -953,7 +944,7 @@ export default (ctx) => {
953
944
  ;; Array (type=1) → join(",") like JS Array.toString()
954
945
  (if (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
955
946
  (then (return (i64.reinterpret_f64 (call $__str_join (local.get $val)
956
- (i64.reinterpret_f64 (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT | 1}) (i32.const 44))))))))
947
+ (i64.reinterpret_f64 (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${ssoAux(1)}) (i32.const 44))))))))
957
948
  (local.get $val))`)
958
949
 
959
950
  // Copy bytes of a string (SSO or heap) into memory at dst. Uses memory.copy for
@@ -962,18 +953,15 @@ export default (ctx) => {
962
953
  (local $w i32)
963
954
  (if (i64.ne (i64.and (local.get $src) (i64.const ${SSO_BIT_I64})) (i64.const 0))
964
955
  (then
965
- ;; SSO: up to 4 chars packed in low 32 bits (LE byte order). Unroll: write 1/2/3/4 bytes
966
- ;; depending on len. (len > 4 is rare/disallowed in practice fallback handles up to 4.)
967
- (local.set $w (i32.wrap_i64 (local.get $src)))
968
- (if (i32.ge_u (local.get $len) (i32.const 4))
969
- (then (i32.store (local.get $dst) (local.get $w)))
970
- (else
971
- (if (i32.eq (local.get $len) (i32.const 0)) (then (return)))
972
- (i32.store8 (local.get $dst) (local.get $w))
973
- (if (i32.eq (local.get $len) (i32.const 1)) (then (return)))
974
- (i32.store8 offset=1 (local.get $dst) (i32.shr_u (local.get $w) (i32.const 8)))
975
- (if (i32.eq (local.get $len) (i32.const 2)) (then (return)))
976
- (i32.store8 offset=2 (local.get $dst) (i32.shr_u (local.get $w) (i32.const 16))))))
956
+ ;; SSO: write $len ASCII chars, each 7-bit-packed at payload bit i*7 (chars 4-5
957
+ ;; span into aux, so read via the 7-bit codec, not the low 32 bits). $w = index.
958
+ (local.set $w (i32.const 0))
959
+ (loop $ssocp
960
+ (if (i32.lt_u (local.get $w) (local.get $len))
961
+ (then
962
+ (i32.store8 (i32.add (local.get $dst) (local.get $w)) ${ssoCharWat('(local.get $src)', '(local.get $w)')})
963
+ (local.set $w (i32.add (local.get $w) (i32.const 1)))
964
+ (br $ssocp)))))
977
965
  (else
978
966
  ;; Heap STRING: memory.copy directly from string data
979
967
  (memory.copy (local.get $dst)
@@ -1005,12 +993,12 @@ export default (ctx) => {
1005
993
  (then
1006
994
  (return (call $__mkptr
1007
995
  (i32.const ${PTR.STRING})
1008
- (i32.or (i32.const ${LAYOUT.SSO_BIT}) (local.get $total))
996
+ (i32.or (i32.const ${LAYOUT.SSO_BIT}) (i32.shl (local.get $total) (i32.const ${SSO_LEN_SHIFT})))
1009
997
  (i32.or
1010
998
  (i32.wrap_i64 (i64.and (local.get $a) (i64.const 0xFFFFFFFF)))
1011
999
  (i32.shl
1012
1000
  (i32.wrap_i64 (i64.and (local.get $b) (i64.const 0xFFFFFFFF)))
1013
- (i32.shl (local.get $alen) (i32.const 3))))))))`
1001
+ (i32.mul (local.get $alen) (i32.const 7))))))))`
1014
1002
 
1015
1003
  const concatFast = !ctx.memory.shared && ctx.transform.alloc !== false ? `
1016
1004
  (local.set $ta (i32.wrap_i64 (i64.and (i64.shr_u (local.get $a) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
@@ -1075,19 +1063,17 @@ export default (ctx) => {
1075
1063
  (i32.wrap_i64 (i64.shr_u (local.get $a) (i64.const ${LAYOUT.AUX_SHIFT})))
1076
1064
  (i32.const ${LAYOUT.SSO_BIT})))
1077
1065
  (then
1078
- (local.set $alen (i32.and
1079
- (i32.wrap_i64 (i64.shr_u (local.get $a) (i64.const ${LAYOUT.AUX_SHIFT})))
1080
- (i32.const ${LAYOUT.SSO_BIT - 1})))
1066
+ (local.set $alen ${ssoLenWat('(local.get $a)')})
1081
1067
  (if (i32.and
1082
1068
  (i32.lt_u (local.get $alen) (i32.const 4))
1083
1069
  (i32.lt_u (local.get $byte) (i32.const 0x80)))
1084
1070
  (then
1085
1071
  (return (call $__mkptr
1086
1072
  (i32.const ${PTR.STRING})
1087
- (i32.or (i32.const ${LAYOUT.SSO_BIT}) (i32.add (local.get $alen) (i32.const 1)))
1073
+ (i32.or (i32.const ${LAYOUT.SSO_BIT}) (i32.shl (i32.add (local.get $alen) (i32.const 1)) (i32.const ${SSO_LEN_SHIFT})))
1088
1074
  (i32.or
1089
1075
  (local.get $aoff)
1090
- (i32.shl (local.get $byte) (i32.shl (local.get $alen) (i32.const 3))))))))))
1076
+ (i32.shl (local.get $byte) (i32.mul (local.get $alen) (i32.const 7))))))))))
1091
1077
  ;; Slow path: allocate new heap STRING with original bytes + 1 new byte
1092
1078
  (local.set $alen (call $__str_byteLen (local.get $a)))
1093
1079
  (local.set $total (i32.add (local.get $alen) (i32.const 1)))
@@ -1368,11 +1354,11 @@ export default (ctx) => {
1368
1354
  (then
1369
1355
  (local.set $sb (i32.load8_u (i32.add (local.get $off) (local.get $i))))
1370
1356
  (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)))))
1357
+ (local.set $sp (i32.or (local.get $sp) (i32.shl (local.get $sb) (i32.mul (local.get $i) (i32.const 7)))))
1372
1358
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
1373
1359
  (br $pk))))
1374
1360
  (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))))))` : ''}
1361
+ (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.or (i32.const ${LAYOUT.SSO_BIT}) (i32.shl (local.get $target) (i32.const 10))) (local.get $sp))))))` : ''}
1376
1362
  (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
1377
1363
 
1378
1364
  // Base helpers (__sso_char/__str_char/__char_at/__str_byteLen) are referenced
@@ -1430,9 +1416,22 @@ export default (ctx) => {
1430
1416
  // covers string/number/null/undefined needles, but two cases need help here:
1431
1417
  // a BOOL rides the 0/1 carrier (→ "0"/"1" not "true"/"false"), and an OBJECT
1432
1418
  // needs compile-time ToPrimitive(string) (__to_str can't invoke user toString).
1433
- const searchArg = (search) =>
1434
- valTypeOf(search) === VAL.BOOL ? asI64(bool(search)) :
1435
- valTypeOf(search) === VAL.OBJECT ? toStrI64(search, emit(search)) : asI64(emit(search))
1419
+ // Coerce the search operand to a string AT THE CALL SITE (21.1.3.x ToString step),
1420
+ // so the `__str_indexof` family carries no embedded `__to_str`. A known-STRING arg
1421
+ // (the overwhelmingly common `s.indexOf("x")` / `s.indexOf(t)` shape) passes raw —
1422
+ // dropping the whole ToString → float-formatter dep tree (~4 KB) that an internal,
1423
+ // unconditional coercion forced into every search-method program. Mirrors
1424
+ // `stringSearchMethod` (startsWith/endsWith), which has always coerced here.
1425
+ const searchArg = (search) => {
1426
+ const vt = valTypeOf(search)
1427
+ if (vt === VAL.STRING) return asI64(emit(search))
1428
+ if (vt === VAL.BOOL) return asI64(bool(search))
1429
+ if (vt === VAL.OBJECT) return toStrI64(search, emit(search))
1430
+ inc('__to_str')
1431
+ return ['call', '$__to_str', asI64(emit(search))]
1432
+ }
1433
+ // Replacement operand of .replace/.replaceAll — same call-site ToString as searchArg.
1434
+ const strReplArg = searchArg
1436
1435
 
1437
1436
  bind('.string:indexOf', (str, search, from) => {
1438
1437
  inc('__str_indexof')
@@ -1573,9 +1572,14 @@ export default (ctx) => {
1573
1572
  asI64(tail)], 'f64')]]], 'f64')
1574
1573
  }
1575
1574
  inc('__str_replace')
1576
- return typed(['call', '$__str_replace', asI64(emit(str)), asI64(emit(search)), asI64(emit(repl))], 'f64')
1575
+ // search/repl ToString'd at the call site (searchArg) __str_replace's __str_indexof
1576
+ // no longer coerces internally, so a non-string search must be stringified here.
1577
+ return typed(['call', '$__str_replace', asI64(emit(str)), searchArg(search), strReplArg(repl)], 'f64')
1578
+ })
1579
+ bind('.replaceAll', (str, search, repl) => {
1580
+ inc('__str_replaceall')
1581
+ return typed(['call', '$__str_replaceall', asI64(emit(str)), searchArg(search), strReplArg(repl)], 'f64')
1577
1582
  })
1578
- bind('.replaceAll', method('__str_replaceall', 'III'))
1579
1583
 
1580
1584
  const caseMethod = (lo, hi, delta) => (str) => {
1581
1585
  inc('__str_case')
@@ -1592,7 +1596,7 @@ export default (ctx) => {
1592
1596
 
1593
1597
  const padMethod = (start) => (str, len, pad) => {
1594
1598
  inc('__str_pad')
1595
- const vpad = pad != null ? asI64(emit(pad)) : ['i64.reinterpret_f64', mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1, 32)]
1599
+ const vpad = pad != null ? asI64(emit(pad)) : ['i64.reinterpret_f64', mkPtrIR(PTR.STRING, ssoAux(1), 32)]
1596
1600
  return typed(['call', '$__str_pad', asI64(emit(str)), asI32(emit(len)), vpad, ['i32.const', start]], 'f64')
1597
1601
  }
1598
1602
  bind('.padStart', padMethod(1))
@@ -1659,14 +1663,17 @@ export default (ctx) => {
1659
1663
  // index; otherwise `oobIR`. Without the bounds check `__char_at` returns 0 for an
1660
1664
  // out-of-range index, which would wrap to a bogus `"\x00"` string (charAt → "",
1661
1665
  // at → undefined). `sLocal` is the receiver as an f64 local.
1662
- const charAtOr = (sLocal, iLocal, lenIR, oobIR) =>
1666
+ const charAtOr = (sLocal, iLocal, lenIR, oobIR) => (
1667
+ inc('__char1byte'),
1663
1668
  ['if', ['result', 'f64'],
1664
1669
  ['i32.and',
1665
1670
  ['i32.ge_s', ['local.get', iLocal], ['i32.const', 0]],
1666
1671
  ['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]]
1672
+ // __char_at yields a raw byte (≥0x80 for non-ASCII heap strings) → __char1byte
1673
+ // builds an SSO byte when ASCII, else a heap 1-byte string.
1674
+ ['then', ['call', '$__char1byte',
1675
+ ['call', '$__char_at', ['i64.reinterpret_f64', ['local.get', sLocal]], ['local.get', iLocal]]]],
1676
+ ['else', oobIR]])
1670
1677
  const emptyStr = mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT, 0)
1671
1678
 
1672
1679
  // .charAt(i) → 1-char string at index i, or "" when out of range (JS spec: no
@@ -1713,13 +1720,27 @@ export default (ctx) => {
1713
1720
  return typed(['f64.reinterpret_i64', toStrI64(value, emit(value))], 'f64')
1714
1721
  })
1715
1722
 
1723
+ // 1-byte string from a raw byte: SSO when ASCII (<0x80, fits the 7-bit codec),
1724
+ // else a heap 1-byte string (the 7-bit SSO can't represent bytes ≥0x80).
1725
+ wat('__char1byte', `(func $__char1byte (param $b i32) (result f64)
1726
+ (local $off i32)
1727
+ (local.set $b (i32.and (local.get $b) (i32.const 0xFF)))
1728
+ (if (result f64) (i32.lt_u (local.get $b) (i32.const 0x80))
1729
+ (then (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${ssoAux(1)}) (local.get $b)))
1730
+ (else
1731
+ (local.set $off (call $__alloc (i32.const 8)))
1732
+ (i32.store (local.get $off) (i32.const 1))
1733
+ (i32.store8 (i32.add (local.get $off) (i32.const 4)) (local.get $b))
1734
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (i32.add (local.get $off) (i32.const 4))))))`)
1735
+
1716
1736
  // String.fromCharCode(...codes) — variadic; each arg is ToUint16(ToNumber(code))
1717
1737
  // → a 1-byte string, concatenated left to right (mirrors String.fromCodePoint).
1718
1738
  bind('String.fromCharCode', (...codes) => {
1719
1739
  if (codes.length === 0) return emit(['str', ''])
1720
1740
  // ToUint16(ToNumber(code)): `toNumF64` performs ToPrimitive on an object
1721
- // argument, so a throwing valueOf/toString propagates per spec.
1722
- const one = (node) => mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1, asI32(toNumF64(node, emit(node))))
1741
+ // argument, so a throwing valueOf/toString propagates per spec. A byte ≥0x80
1742
+ // can't be a 7-bit-ASCII SSO, so __char1byte routes it to a heap 1-byte string.
1743
+ const one = (node) => { inc('__char1byte'); return typed(['call', '$__char1byte', asI32(toNumF64(node, emit(node)))], 'f64') }
1723
1744
  let r = one(codes[0])
1724
1745
  for (let i = 1; i < codes.length; i++) {
1725
1746
  inc('__str_concat_raw')
@@ -1742,27 +1763,41 @@ export default (ctx) => {
1742
1763
  (local.set $cp (i32.trunc_sat_f64_s (local.get $cpf)))
1743
1764
  ;; ASCII: 1 byte SSO
1744
1765
  (if (i32.lt_u (local.get $cp) (i32.const 128))
1745
- (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT | 1}) (local.get $cp)))))
1746
- ;; 2-byte: 0x80-0x7FF → SSO
1766
+ (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${ssoAux(1)}) (local.get $cp)))))
1767
+ ;; multi-byte (cp ≥ 0x80): UTF-8 bytes are ≥0x80 can't be 7-bit ASCII SSO, so
1768
+ ;; build a heap STRING (len header at $off, bytes at $off+4). Over-alloc to 8 so the
1769
+ ;; i32.store of the packed bytes never runs past the buffer; the len word bounds reads.
1770
+ ;; 2-byte: 0x80-0x7FF
1747
1771
  (if (i32.lt_u (local.get $cp) (i32.const 0x800))
1748
- (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT | 2})
1749
- (i32.or
1750
- (i32.or (i32.const 0xC0) (i32.shr_u (local.get $cp) (i32.const 6)))
1751
- (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 8)))))))
1752
- ;; 3-byte: 0x800-0xFFFF → SSO (3 bytes fits)
1772
+ (then
1773
+ (local.set $off (call $__alloc (i32.const 8)))
1774
+ (i32.store (local.get $off) (i32.const 2))
1775
+ (i32.store (i32.add (local.get $off) (i32.const 4))
1776
+ (i32.or
1777
+ (i32.or (i32.const 0xC0) (i32.shr_u (local.get $cp) (i32.const 6)))
1778
+ (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 8))))
1779
+ (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (i32.add (local.get $off) (i32.const 4))))))
1780
+ ;; 3-byte: 0x800-0xFFFF
1753
1781
  (if (i32.lt_u (local.get $cp) (i32.const 0x10000))
1754
- (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT | 3})
1755
- (i32.or (i32.or
1756
- (i32.or (i32.const 0xE0) (i32.shr_u (local.get $cp) (i32.const 12)))
1757
- (i32.shl (i32.or (i32.const 0x80) (i32.and (i32.shr_u (local.get $cp) (i32.const 6)) (i32.const 0x3F))) (i32.const 8)))
1758
- (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 16)))))))
1759
- ;; 4-byte: 0x10000-0x10FFFF SSO (4 bytes fits)
1760
- (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT | 4})
1782
+ (then
1783
+ (local.set $off (call $__alloc (i32.const 8)))
1784
+ (i32.store (local.get $off) (i32.const 3))
1785
+ (i32.store (i32.add (local.get $off) (i32.const 4))
1786
+ (i32.or (i32.or
1787
+ (i32.or (i32.const 0xE0) (i32.shr_u (local.get $cp) (i32.const 12)))
1788
+ (i32.shl (i32.or (i32.const 0x80) (i32.and (i32.shr_u (local.get $cp) (i32.const 6)) (i32.const 0x3F))) (i32.const 8)))
1789
+ (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 16))))
1790
+ (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (i32.add (local.get $off) (i32.const 4))))))
1791
+ ;; 4-byte: 0x10000-0x10FFFF
1792
+ (local.set $off (call $__alloc (i32.const 8)))
1793
+ (i32.store (local.get $off) (i32.const 4))
1794
+ (i32.store (i32.add (local.get $off) (i32.const 4))
1761
1795
  (i32.or (i32.or (i32.or
1762
1796
  (i32.or (i32.const 0xF0) (i32.shr_u (local.get $cp) (i32.const 18)))
1763
1797
  (i32.shl (i32.or (i32.const 0x80) (i32.and (i32.shr_u (local.get $cp) (i32.const 12)) (i32.const 0x3F))) (i32.const 8)))
1764
1798
  (i32.shl (i32.or (i32.const 0x80) (i32.and (i32.shr_u (local.get $cp) (i32.const 6)) (i32.const 0x3F))) (i32.const 16)))
1765
- (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 24))))))`)
1799
+ (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 24))))
1800
+ (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (i32.add (local.get $off) (i32.const 4)))))`)
1766
1801
 
1767
1802
  // String.fromCodePoint(...codePoints) — variadic; each arg is ToNumber-coerced
1768
1803
  // then validated/encoded by __fromCodePoint, results concatenated left to right.