jz 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +37 -33
  2. package/bench/README.md +176 -73
  3. package/bench/bench.svg +58 -71
  4. package/cli.js +12 -5
  5. package/dist/interop.js +1 -1
  6. package/dist/jz.js +1366 -1101
  7. package/index.js +40 -9
  8. package/interop.js +193 -138
  9. package/layout.js +29 -18
  10. package/module/array.js +49 -73
  11. package/module/collection.js +83 -25
  12. package/module/console.js +1 -1
  13. package/module/core.js +161 -15
  14. package/module/json.js +3 -3
  15. package/module/math.js +167 -117
  16. package/module/number.js +247 -13
  17. package/module/object.js +11 -5
  18. package/module/regex.js +8 -7
  19. package/module/string.js +295 -171
  20. package/module/typedarray.js +169 -105
  21. package/package.json +7 -3
  22. package/src/abi/string.js +40 -35
  23. package/src/ast.js +19 -2
  24. package/src/compile/analyze.js +64 -2
  25. package/src/compile/cse-load.js +200 -0
  26. package/src/compile/emit-assign.js +73 -14
  27. package/src/compile/emit.js +324 -34
  28. package/src/compile/index.js +204 -61
  29. package/src/compile/infer.js +8 -1
  30. package/src/compile/loop-divmod.js +12 -58
  31. package/src/compile/loop-model.js +91 -0
  32. package/src/compile/loop-recurrence.js +167 -0
  33. package/src/compile/loop-square.js +102 -0
  34. package/src/compile/narrow.js +180 -34
  35. package/src/compile/peel-stencil.js +18 -64
  36. package/src/compile/plan/common.js +29 -0
  37. package/src/compile/plan/index.js +4 -1
  38. package/src/compile/plan/inline.js +176 -21
  39. package/src/compile/plan/literals.js +93 -19
  40. package/src/ctx.js +51 -12
  41. package/src/helper-counters.js +137 -0
  42. package/src/ir.js +102 -13
  43. package/src/kind-traits.js +7 -3
  44. package/src/kind.js +14 -1
  45. package/src/op-policy.js +5 -2
  46. package/src/ops.js +119 -0
  47. package/src/optimize/index.js +1125 -136
  48. package/src/optimize/recurse.js +182 -0
  49. package/src/optimize/vectorize.js +1302 -144
  50. package/src/prepare/index.js +29 -12
  51. package/src/reps.js +4 -1
  52. package/src/type.js +53 -45
  53. package/src/wat/assemble.js +92 -9
  54. package/src/widen.js +21 -0
  55. package/src/wat/optimize.js +0 -3938
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
@@ -110,10 +144,17 @@ export default (ctx) => {
110
144
  deps({
111
145
  __str_concat: ['__to_str', '__str_byteLen', '__alloc', '__memgrow', '__mkptr', '__str_copy'],
112
146
  __str_concat_raw: ['__str_byteLen', '__alloc', '__memgrow', '__mkptr', '__str_copy'],
147
+ __str_concat_fresh: ['__to_str', '__str_byteLen', '__alloc', '__mkptr', '__str_copy'],
148
+ __str_concat_raw_fresh: ['__str_byteLen', '__alloc', '__mkptr', '__str_copy'],
113
149
  __str_append_byte: ['__str_byteLen', '__alloc', '__memgrow', '__mkptr', '__str_copy'],
114
150
  __str_copy: [],
115
- __str_slice: ['__str_byteLen', '__alloc'],
116
- __str_slice_view: ['__str_byteLen', '__mkptr', '__str_slice'],
151
+ // __str_slice/_view are FUNCTION templates: resolveIncludes' auto-dep scan realizes the
152
+ // factory (v()) to discover body calls, but that realization DIVERGES under self-host
153
+ // (jz.wasm) — so a body-called helper not also listed here is dropped from the kernel
154
+ // ("Unknown func $__clamp_idx" on `str.slice`). FN templates must declare body deps
155
+ // manually; pinned by test/selfhost-includes.js.
156
+ __str_slice: ['__str_byteLen', '__alloc', '__clamp_idx', '__mkptr'],
157
+ __str_slice_view: ['__str_byteLen', '__mkptr', '__str_slice', '__clamp_idx'],
117
158
  __str_indexof: ['__str_byteLen'],
118
159
  __str_lastindexof: ['__str_byteLen'],
119
160
  __wrap1: ['__alloc', '__mkptr'],
@@ -128,15 +169,15 @@ export default (ctx) => {
128
169
  __str_replace: ['__str_indexof', '__str_slice', '__str_concat'],
129
170
  __str_replaceall: ['__str_indexof', '__str_slice', '__str_concat'],
130
171
  __str_split: ['__str_slice', '__str_byteLen', '__char_at', '__alloc'],
131
- __str_idx: [],
172
+ __str_idx: ['__char1byte'],
132
173
  __str_eq: ['__str_eq_cold'],
133
174
  __str_eq_cold: ['__char_at', '__str_byteLen'],
134
175
  __str_cmp: ['__char_at', '__str_byteLen'],
135
176
  __str_range_eq: ['__char_at', '__str_byteLen'],
136
177
  __str_substring_eq: ['__str_byteLen', '__str_range_eq'],
137
- __str_slice_eq: ['__str_byteLen', '__str_range_eq'],
178
+ __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
179
  __str_pad: ['__str_byteLen', '__str_copy', '__alloc'],
139
- __str_join: ['__str_concat', '__to_str', '__str_byteLen', '__len', '__ptr_offset'],
180
+ __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
181
  __str_encode: ['__str_byteLen', '__str_copy'],
141
182
  __encodeURIComponent: ['__to_str', '__str_byteLen', '__char_at', '__alloc', '__mkptr'],
142
183
  __decodeURIComponent: ['__to_str', '__str_byteLen', '__char_at', '__alloc', '__mkptr', '__uri_hex'],
@@ -162,11 +203,9 @@ export default (ctx) => {
162
203
  // === String literal: "abc" → SSO if ≤4 ASCII, else static data ===
163
204
 
164
205
  bind('str', (str) => {
165
- const MAX_SSO = 4
166
- if (ctx.features.sso && str.length <= MAX_SSO && /^[\x00-\x7f]*$/.test(str)) {
167
- let packed = 0
168
- for (let i = 0; i < str.length; i++) packed |= str.charCodeAt(i) << (i * 8)
169
- return mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | str.length, packed)
206
+ if (ctx.features.sso) {
207
+ const sso = ssoEncode(str)
208
+ if (sso) return mkPtrIR(PTR.STRING, sso.aux, sso.offset)
170
209
  }
171
210
  const bytes = new TextEncoder().encode(str)
172
211
  const len = bytes.length
@@ -223,11 +262,7 @@ export default (ctx) => {
223
262
  // SSO/STRING ptrs never have forwarding pointers (only ARRAY does), so we extract
224
263
  // the raw offset directly instead of paying the __ptr_offset function-call overhead.
225
264
  wat('__sso_char', `(func $__sso_char (param $ptr i64) (param $i i32) (result i32)
226
- (i32.and
227
- (i32.shr_u
228
- (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK})))
229
- (i32.mul (local.get $i) (i32.const 8)))
230
- (i32.const 0xFF)))`)
265
+ ${ssoCharWat('(local.get $ptr)', '(local.get $i)')})`)
231
266
 
232
267
  wat('__str_char', `(func $__str_char (param $ptr i64) (param $i i32) (result i32)
233
268
  (i32.load8_u (i32.add
@@ -254,16 +289,9 @@ export default (ctx) => {
254
289
  (i64.ne (i64.and (local.get $ptr) (i64.const ${SSO_BIT_I64})) (i64.const 0))
255
290
  (then
256
291
  (if (result i32)
257
- (i32.ge_u (local.get $i)
258
- (i32.and
259
- (i32.wrap_i64 (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.AUX_SHIFT})))
260
- (i32.const ${LAYOUT.SSO_BIT - 1})))
292
+ (i32.ge_u (local.get $i) ${ssoLenWat('(local.get $ptr)')})
261
293
  (then (i32.const 0))
262
- (else (i32.and
263
- (i32.shr_u
264
- (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK})))
265
- (i32.mul (local.get $i) (i32.const 8)))
266
- (i32.const 0xFF)))))
294
+ (else ${ssoCharWat('(local.get $ptr)', '(local.get $i)')})))
267
295
  (else
268
296
  (if (result i32)
269
297
  (i32.ge_u (local.get $i)
@@ -301,9 +329,7 @@ export default (ctx) => {
301
329
  (i32.const ${LAYOUT.SSO_BIT})))
302
330
  (local.set $len
303
331
  (if (result i32) (local.get $isSso)
304
- (then (i32.and
305
- (i32.wrap_i64 (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.AUX_SHIFT})))
306
- (i32.const ${LAYOUT.SSO_BIT - 1})))
332
+ (then ${ssoLenWat('(local.get $ptr)')})
307
333
  (else
308
334
  (if (result i32)
309
335
  (i64.ne (i64.and (local.get $ptr) (i64.const ${SLICE_BIT_I64})) (i64.const 0))
@@ -318,14 +344,15 @@ export default (ctx) => {
318
344
  (i32.or (i32.lt_s (local.get $i) (i32.const 0)) (i32.ge_u (local.get $i) (local.get $len)))
319
345
  (then (f64.const nan:${UNDEF_NAN}))
320
346
  (else
321
- (f64.reinterpret_i64
322
- (i64.or
323
- ;; mkptr(STRING, SSO_BIT|1, 0) = NAN_PREFIX | (STRING<<TAG_SHIFT) | ((SSO_BIT|1)<<AUX_SHIFT)
324
- (i64.const ${ptrNanHex(PTR.STRING, LAYOUT.SSO_BIT | 1)})
325
- (i64.extend_i32_u
326
- (if (result i32) (local.get $isSso)
327
- (then (i32.and (i32.shr_u (local.get $off) (i32.mul (local.get $i) (i32.const 8))) (i32.const 0xFF)))
328
- (else (i32.load8_u (i32.add (local.get $off) (local.get $i)))))))))))`)
347
+ (if (result f64) (local.get $isSso)
348
+ ;; SSO source: chars are ASCII (<0x80) → inline 1-char SSO (hot path, no branch).
349
+ (then (f64.reinterpret_i64
350
+ (i64.or
351
+ (i64.const ${ptrNanHex(PTR.STRING, ssoAux(1))})
352
+ (i64.extend_i32_u ${ssoCharWat('(local.get $ptr)', '(local.get $i)')}))))
353
+ ;; Heap source: the byte may be ≥0x80 (non-ASCII), which can't be a 7-bit SSO,
354
+ ;; so __char1byte routes those to a heap 1-byte string.
355
+ (else (call $__char1byte (i32.load8_u (i32.add (local.get $off) (local.get $i)))))))))`)
329
356
 
330
357
  // Hot: ~53M calls in watr self-host. Bit-eq covers identity. SSO/SSO with !bit-eq
331
358
  // guarantees content differs (high 32 bits encode type+len; both equal → low 32 differs
@@ -372,9 +399,12 @@ export default (ctx) => {
372
399
  (local $ta i32) (local $tb i32)
373
400
  (local $offA i32) (local $offB i32)
374
401
  (local $ssoA i32) (local $ssoB i32)
375
- (local $axA i32) (local $axB i32)
376
- (if (i64.eq (local.get $a) (local.get $b))
377
- (then (return (i32.const 1))))
402
+ ;; Sole caller is __str_eq, which already returned for bit-equal pointers, both-SSO
403
+ ;; (bit-ne content-ne), both-canonical-interned (bit-ne ⇒ content-ne) and both-plain-
404
+ ;; heap length mismatch. Re-testing them here is dead work on every cold call — skip to
405
+ ;; the byte walk. (__str_eq/__str_eq_cold are ~14% of self-host runtime; this trims the
406
+ ;; per-call fixed cost.) The heap/mixed paths below still decide every case correctly
407
+ ;; on their own, so this stays correct even if a future caller skips the prelude.
378
408
  (local.set $ta (i32.wrap_i64 (i64.and (i64.shr_u (local.get $a) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
379
409
  (local.set $tb (i32.wrap_i64 (i64.and (i64.shr_u (local.get $b) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
380
410
  (local.set $offA (i32.wrap_i64 (i64.and (local.get $a) (i64.const ${LAYOUT.OFFSET_MASK}))))
@@ -385,22 +415,6 @@ export default (ctx) => {
385
415
  (local.set $ssoB (i32.and
386
416
  (i32.wrap_i64 (i64.shr_u (local.get $b) (i64.const ${LAYOUT.AUX_SHIFT})))
387
417
  (i32.const ${LAYOUT.SSO_BIT})))
388
- ;; Both SSO with !bit-eq ⇒ content differs (high 32 bits hold tag+aux; both equal here).
389
- (if (i32.and (local.get $ssoA) (local.get $ssoB))
390
- (then (return (i32.const 0))))
391
- ;; Both CANONICAL interned heap strings (STR_INTERN_BIT, SSO/SLICE clear):
392
- ;; canonicals are deduped, so bit-ne ⇒ content-ne — the V8 pointer-compare
393
- ;; equivalent. This is the dominant unequal case in tag-dispatch chains
394
- ;; (op === 'literal' ladders over static literals).
395
- (local.set $axA (i32.wrap_i64 (i64.shr_u (local.get $a) (i64.const ${LAYOUT.AUX_SHIFT}))))
396
- (local.set $axB (i32.wrap_i64 (i64.shr_u (local.get $b) (i64.const ${LAYOUT.AUX_SHIFT}))))
397
- (if (i32.and
398
- (i32.and (i32.eq (local.get $ta) (i32.const ${PTR.STRING}))
399
- (i32.eq (local.get $tb) (i32.const ${PTR.STRING})))
400
- (i32.and
401
- (i32.eq (i32.and (local.get $axA) (i32.const ${LAYOUT.SSO_BIT | LAYOUT.SLICE_BIT | STR_INTERN_BIT})) (i32.const ${STR_INTERN_BIT}))
402
- (i32.eq (i32.and (local.get $axB) (i32.const ${LAYOUT.SSO_BIT | LAYOUT.SLICE_BIT | STR_INTERN_BIT})) (i32.const ${STR_INTERN_BIT}))))
403
- (then (return (i32.const 0))))
404
418
  ;; Both heap STRING fast path: inline len from header. Chunk by 4 bytes via unaligned
405
419
  ;; i32.load (wasm guarantees unaligned-OK), then byte-tail. Most string comparisons fail
406
420
  ;; early on the first 4-byte word, so this collapses the per-byte branch overhead into a
@@ -489,7 +503,7 @@ export default (ctx) => {
489
503
  (i32.wrap_i64 (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.AUX_SHIFT})))
490
504
  (i32.const ${LAYOUT.AUX_MASK})))
491
505
  (if (result i32) (i32.and (local.get $aux) (i32.const ${LAYOUT.SSO_BIT}))
492
- (then (i32.and (local.get $aux) (i32.const ${LAYOUT.SSO_BIT - 1})))
506
+ (then ${ssoLenFromAux('(local.get $aux)')})
493
507
  (else
494
508
  (if (result i32) (i32.and (local.get $aux) (i32.const ${LAYOUT.SLICE_BIT}))
495
509
  ;; view: length lives in aux[12:0], not a header.
@@ -507,21 +521,11 @@ export default (ctx) => {
507
521
  // (single bulk op instead of nlen × __char_at).
508
522
  wat('__str_slice', () => `(func $__str_slice (param $ptr i64) (param $start i32) (param $end i32) (result f64)
509
523
  (local $len i32) (local $nlen i32) (local $off i32) (local $i i32)
510
- (local $srcOff i32) (local $isSso i32) (local $sb i32) (local $sp i32)
524
+ (local $srcOff i32) (local $isSso i32) (local $sb i32) (local $sp i32) (local $sp64 i64)
511
525
  (local $ip i32) (local $h i32) (local $j i32) (local $slot i32) (local $cand i32) (local $k i32)
512
526
  (local.set $len (call $__str_byteLen (local.get $ptr)))
513
- (if (i32.lt_s (local.get $start) (i32.const 0))
514
- (then (local.set $start (i32.add (local.get $len) (local.get $start)))))
515
- (if (i32.lt_s (local.get $end) (i32.const 0))
516
- (then (local.set $end (i32.add (local.get $len) (local.get $end)))))
517
- (if (i32.lt_s (local.get $start) (i32.const 0))
518
- (then (local.set $start (i32.const 0))))
519
- (if (i32.gt_s (local.get $start) (local.get $len))
520
- (then (local.set $start (local.get $len))))
521
- (if (i32.lt_s (local.get $end) (i32.const 0))
522
- (then (local.set $end (i32.const 0))))
523
- (if (i32.gt_s (local.get $end) (local.get $len))
524
- (then (local.set $end (local.get $len))))
527
+ (local.set $start (call $__clamp_idx (local.get $start) (local.get $len)))
528
+ (local.set $end (call $__clamp_idx (local.get $end) (local.get $len)))
525
529
  (if (i32.ge_s (local.get $start) (local.get $end))
526
530
  (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
527
531
  (local.set $nlen (i32.sub (local.get $end) (local.get $start)))
@@ -560,18 +564,8 @@ export default (ctx) => {
560
564
  (local $len i32) (local $nlen i32) (local $srcOff i32) (local $tag i32)
561
565
  (local $ip i32) (local $h i32) (local $j i32) (local $slot i32) (local $cand i32) (local $k i32)
562
566
  (local.set $len (call $__str_byteLen (local.get $ptr)))
563
- (if (i32.lt_s (local.get $start) (i32.const 0))
564
- (then (local.set $start (i32.add (local.get $len) (local.get $start)))))
565
- (if (i32.lt_s (local.get $end) (i32.const 0))
566
- (then (local.set $end (i32.add (local.get $len) (local.get $end)))))
567
- (if (i32.lt_s (local.get $start) (i32.const 0))
568
- (then (local.set $start (i32.const 0))))
569
- (if (i32.gt_s (local.get $start) (local.get $len))
570
- (then (local.set $start (local.get $len))))
571
- (if (i32.lt_s (local.get $end) (i32.const 0))
572
- (then (local.set $end (i32.const 0))))
573
- (if (i32.gt_s (local.get $end) (local.get $len))
574
- (then (local.set $end (local.get $len))))
567
+ (local.set $start (call $__clamp_idx (local.get $start) (local.get $len)))
568
+ (local.set $end (call $__clamp_idx (local.get $end) (local.get $len)))
575
569
  (if (i32.ge_s (local.get $start) (local.get $end))
576
570
  (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
577
571
  (local.set $nlen (i32.sub (local.get $end) (local.get $start)))
@@ -676,31 +670,33 @@ export default (ctx) => {
676
670
  wat('__str_slice_eq', `(func $__str_slice_eq (param $ptr i64) (param $start i32) (param $end i32) (param $other i64) (result i32)
677
671
  (local $len i32)
678
672
  (local.set $len (call $__str_byteLen (local.get $ptr)))
679
- (if (i32.lt_s (local.get $start) (i32.const 0))
680
- (then (local.set $start (i32.add (local.get $len) (local.get $start)))))
681
- (if (i32.lt_s (local.get $end) (i32.const 0))
682
- (then (local.set $end (i32.add (local.get $len) (local.get $end)))))
683
- (if (i32.lt_s (local.get $start) (i32.const 0))
684
- (then (local.set $start (i32.const 0))))
685
- (if (i32.gt_s (local.get $start) (local.get $len))
686
- (then (local.set $start (local.get $len))))
687
- (if (i32.lt_s (local.get $end) (i32.const 0))
688
- (then (local.set $end (i32.const 0))))
689
- (if (i32.gt_s (local.get $end) (local.get $len))
690
- (then (local.set $end (local.get $len))))
673
+ (local.set $start (call $__clamp_idx (local.get $start) (local.get $len)))
674
+ (local.set $end (call $__clamp_idx (local.get $end) (local.get $len)))
691
675
  (call $__str_range_eq (local.get $ptr) (local.get $start) (local.get $end) (local.get $other)))`)
692
676
 
693
677
  // Hoist SSO/heap dispatch for hay and ndl out of the inner byte loop. Inner
694
678
  // loop becomes (load8_u OR sso byte-extract) per side — no per-byte calls.
679
+ // Needle byte j, SSO-aware (packed-bits extract vs heap load) — lets the SIMD
680
+ // haystack scan serve SSO needles too (the common short "," / "://" needle),
681
+ // not just heap×heap. `$nsso` is loop-invariant so the branch predicts away.
682
+ const nByte = (j) => `(if (result i32) (local.get $nsso)
683
+ (then ${ssoCharWat('(local.get $ndl)', j)})
684
+ (else (i32.load8_u (i32.add (local.get $noff) ${j}))))`
695
685
  wat('__str_indexof', `(func $__str_indexof (param $hay i64) (param $ndl i64) (param $from i32) (result i32)
696
686
  (local $hlen i32) (local $nlen i32) (local $i i32) (local $j i32) (local $match i32)
697
687
  (local $hoff i32) (local $noff i32)
698
688
  (local $hsso i32) (local $nsso i32) (local $hb i32) (local $nb i32) (local $k i32)
689
+ (local $splat v128) (local $mask i32) (local $scanEnd i32)
699
690
  ;; The search value is ToString'd at the call site (searchArg) per 21.1.3.9 step 4 —
700
691
  ;; a known-string needle passes raw, so this helper carries no float-pulling __to_str.
701
692
  (local.set $hlen (call $__str_byteLen (local.get $hay)))
702
693
  (local.set $nlen (call $__str_byteLen (local.get $ndl)))
703
- (if (i32.eqz (local.get $nlen)) (then (return (local.get $from))))
694
+ ;; Empty needle matches at clamp(from, 0, hlen) per 21.1.3.9 step 6 (Min(Max(pos,0),len)).
695
+ (if (i32.eqz (local.get $nlen))
696
+ (then (return (select
697
+ (select (local.get $from) (local.get $hlen) (i32.lt_s (local.get $from) (local.get $hlen)))
698
+ (i32.const 0)
699
+ (i32.ge_s (local.get $from) (i32.const 0))))))
704
700
  (if (i32.gt_s (local.get $nlen) (local.get $hlen)) (then (return (i32.const -1))))
705
701
  (local.set $hoff (i32.wrap_i64 (i64.and (local.get $hay) (i64.const ${LAYOUT.OFFSET_MASK}))))
706
702
  (local.set $noff (i32.wrap_i64 (i64.and (local.get $ndl) (i64.const ${LAYOUT.OFFSET_MASK}))))
@@ -711,6 +707,83 @@ export default (ctx) => {
711
707
  (i32.wrap_i64 (i64.shr_u (local.get $ndl) (i64.const ${LAYOUT.AUX_SHIFT})))
712
708
  (i32.const ${LAYOUT.SSO_BIT})))
713
709
  (local.set $i (if (result i32) (i32.gt_s (local.get $from) (i32.const 0)) (then (local.get $from)) (else (i32.const 0))))
710
+ ;; Single-byte needle (the common s.indexOf('/') / s.includes(' ')): scan for one byte without
711
+ ;; the per-position match/j/k bookkeeping. Heap haystack gets a branchless load8_u loop (no
712
+ ;; per-byte SSO test); a 16-wide SIMD scan here would only add splat/window setup that short
713
+ ;; strings — the bulk of single-char searches — never amortize, so the scalar scan stays.
714
+ (if (i32.eq (local.get $nlen) (i32.const 1))
715
+ (then
716
+ (local.set $nb ${nByte('(i32.const 0)')})
717
+ (if (i32.eqz (local.get $hsso))
718
+ (then
719
+ (block $sd (loop $ss
720
+ (br_if $sd (i32.ge_s (local.get $i) (local.get $hlen)))
721
+ (if (i32.eq (i32.load8_u (i32.add (local.get $hoff) (local.get $i))) (local.get $nb))
722
+ (then (return (local.get $i))))
723
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
724
+ (br $ss)))
725
+ (return (i32.const -1)))
726
+ (else
727
+ (block $sd2 (loop $ss2
728
+ (br_if $sd2 (i32.ge_s (local.get $i) (local.get $hlen)))
729
+ (if (i32.eq ${ssoCharWat('(local.get $hay)', '(local.get $i)')} (local.get $nb))
730
+ (then (return (local.get $i))))
731
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
732
+ (br $ss2)))
733
+ (return (i32.const -1))))))
734
+ ;; Multi-byte needle, HEAP haystack (needle SSO or heap): a SIMD first-byte memchr — broadcast
735
+ ;; needle[0] across 16 lanes, i8x16.eq a 16-byte haystack window, and read the match-bitmask.
736
+ ;; Only positions whose first byte matches reach the branchless verify; the rare match (V8's own
737
+ ;; StringIndexOf is SIMD here) is what the scalar path lost on. The SIMD load only touches the
738
+ ;; haystack, so an SSO needle (the common short "," / "://" / "TARGET") rides this path too —
739
+ ;; its bytes are fetched SSO-aware via nByte(). scanEnd = hlen-nlen is the last viable start; the
740
+ ;; window stops at hlen-16 (a full 16-byte load stays inside the string), a scalar tail covers
741
+ ;; the rest. Only an SSO haystack (≤ MAX_SSO bytes — too short to scan 16-wide) falls through.
742
+ (if (i32.eqz (local.get $hsso))
743
+ (then
744
+ (local.set $nb ${nByte('(i32.const 0)')})
745
+ (local.set $scanEnd (i32.sub (local.get $hlen) (local.get $nlen)))
746
+ (local.set $splat (i8x16.splat (local.get $nb)))
747
+ (block $vd (loop $vo
748
+ (br_if $vd (i32.gt_s (local.get $i) (i32.sub (local.get $hlen) (i32.const 16))))
749
+ (local.set $mask (i8x16.bitmask
750
+ (i8x16.eq (v128.load (i32.add (local.get $hoff) (local.get $i))) (local.get $splat))))
751
+ (block $bd (loop $bo
752
+ (br_if $bd (i32.eqz (local.get $mask)))
753
+ (local.set $k (i32.add (local.get $i) (i32.ctz (local.get $mask)))) ;; candidate start
754
+ (br_if $bd (i32.gt_s (local.get $k) (local.get $scanEnd))) ;; positions only grow
755
+ (local.set $match (i32.const 1))
756
+ (local.set $j (i32.const 1))
757
+ (block $mn (loop $mi
758
+ (br_if $mn (i32.ge_s (local.get $j) (local.get $nlen)))
759
+ (if (i32.ne (i32.load8_u (i32.add (i32.add (local.get $hoff) (local.get $k)) (local.get $j)))
760
+ ${nByte('(local.get $j)')})
761
+ (then (local.set $match (i32.const 0)) (br $mn)))
762
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
763
+ (br $mi)))
764
+ (if (local.get $match) (then (return (local.get $k))))
765
+ (local.set $mask (i32.and (local.get $mask) (i32.sub (local.get $mask) (i32.const 1)))) ;; clear lowest match
766
+ (br $bo)))
767
+ (local.set $i (i32.add (local.get $i) (i32.const 16)))
768
+ (br $vo)))
769
+ ;; scalar tail: positions [i, scanEnd] the SIMD window couldn't cover
770
+ (block $md (loop $mo
771
+ (br_if $md (i32.gt_s (local.get $i) (local.get $scanEnd)))
772
+ (if (i32.eq (i32.load8_u (i32.add (local.get $hoff) (local.get $i))) (local.get $nb))
773
+ (then
774
+ (local.set $match (i32.const 1))
775
+ (local.set $j (i32.const 1))
776
+ (block $mn2 (loop $mi2
777
+ (br_if $mn2 (i32.ge_s (local.get $j) (local.get $nlen)))
778
+ (if (i32.ne (i32.load8_u (i32.add (i32.add (local.get $hoff) (local.get $i)) (local.get $j)))
779
+ ${nByte('(local.get $j)')})
780
+ (then (local.set $match (i32.const 0)) (br $mn2)))
781
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
782
+ (br $mi2)))
783
+ (if (local.get $match) (then (return (local.get $i))))))
784
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
785
+ (br $mo)))
786
+ (return (i32.const -1))))
714
787
  (block $done (loop $outer
715
788
  (br_if $done (i32.gt_s (local.get $i) (i32.sub (local.get $hlen) (local.get $nlen))))
716
789
  (local.set $match (i32.const 1))
@@ -719,10 +792,10 @@ export default (ctx) => {
719
792
  (br_if $nomatch (i32.ge_s (local.get $j) (local.get $nlen)))
720
793
  (local.set $k (i32.add (local.get $i) (local.get $j)))
721
794
  (local.set $hb (if (result i32) (local.get $hsso)
722
- (then (i32.and (i32.shr_u (local.get $hoff) (i32.shl (local.get $k) (i32.const 3))) (i32.const 0xFF)))
795
+ (then ${ssoCharWat('(local.get $hay)', '(local.get $k)')})
723
796
  (else (i32.load8_u (i32.add (local.get $hoff) (local.get $k))))))
724
797
  (local.set $nb (if (result i32) (local.get $nsso)
725
- (then (i32.and (i32.shr_u (local.get $noff) (i32.shl (local.get $j) (i32.const 3))) (i32.const 0xFF)))
798
+ (then ${ssoCharWat('(local.get $ndl)', '(local.get $j)')})
726
799
  (else (i32.load8_u (i32.add (local.get $noff) (local.get $j))))))
727
800
  (if (i32.ne (local.get $hb) (local.get $nb))
728
801
  (then (local.set $match (i32.const 0)) (br $nomatch)))
@@ -771,10 +844,10 @@ export default (ctx) => {
771
844
  (br_if $nomatch (i32.ge_s (local.get $j) (local.get $nlen)))
772
845
  (local.set $k (i32.add (local.get $i) (local.get $j)))
773
846
  (local.set $hb (if (result i32) (local.get $hsso)
774
- (then (i32.and (i32.shr_u (local.get $hoff) (i32.shl (local.get $k) (i32.const 3))) (i32.const 0xFF)))
847
+ (then ${ssoCharWat('(local.get $hay)', '(local.get $k)')})
775
848
  (else (i32.load8_u (i32.add (local.get $hoff) (local.get $k))))))
776
849
  (local.set $nb (if (result i32) (local.get $nsso)
777
- (then (i32.and (i32.shr_u (local.get $noff) (i32.shl (local.get $j) (i32.const 3))) (i32.const 0xFF)))
850
+ (then ${ssoCharWat('(local.get $ndl)', '(local.get $j)')})
778
851
  (else (i32.load8_u (i32.add (local.get $noff) (local.get $j))))))
779
852
  (if (i32.ne (local.get $hb) (local.get $nb))
780
853
  (then (local.set $match (i32.const 0)) (br $nomatch)))
@@ -804,10 +877,10 @@ export default (ctx) => {
804
877
  (br_if $done (i32.ge_s (local.get $i) (local.get $plen)))
805
878
  (if (i32.ne
806
879
  (if (result i32) (local.get $ssso)
807
- (then (i32.and (i32.shr_u (local.get $soff) (i32.shl (local.get $i) (i32.const 3))) (i32.const 0xFF)))
880
+ (then ${ssoCharWat('(local.get $str)', '(local.get $i)')})
808
881
  (else (i32.load8_u (i32.add (local.get $soff) (local.get $i)))))
809
882
  (if (result i32) (local.get $psso)
810
- (then (i32.and (i32.shr_u (local.get $poff) (i32.shl (local.get $i) (i32.const 3))) (i32.const 0xFF)))
883
+ (then ${ssoCharWat('(local.get $pfx)', '(local.get $i)')})
811
884
  (else (i32.load8_u (i32.add (local.get $poff) (local.get $i))))))
812
885
  (then (return (i32.const 0))))
813
886
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
@@ -835,10 +908,10 @@ export default (ctx) => {
835
908
  (local.set $k (i32.add (local.get $off) (local.get $i)))
836
909
  (if (i32.ne
837
910
  (if (result i32) (local.get $ssso)
838
- (then (i32.and (i32.shr_u (local.get $soff) (i32.shl (local.get $k) (i32.const 3))) (i32.const 0xFF)))
911
+ (then ${ssoCharWat('(local.get $str)', '(local.get $k)')})
839
912
  (else (i32.load8_u (i32.add (local.get $soff) (local.get $k)))))
840
913
  (if (result i32) (local.get $fsso)
841
- (then (i32.and (i32.shr_u (local.get $foff) (i32.shl (local.get $i) (i32.const 3))) (i32.const 0xFF)))
914
+ (then ${ssoCharWat('(local.get $sfx)', '(local.get $i)')})
842
915
  (else (i32.load8_u (i32.add (local.get $foff) (local.get $i))))))
843
916
  (then (return (i32.const 0))))
844
917
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
@@ -860,9 +933,7 @@ export default (ctx) => {
860
933
  (then
861
934
  (block $dsso (loop $lsso
862
935
  (br_if $dsso (i32.ge_s (local.get $i) (local.get $len)))
863
- (local.set $c (i32.and
864
- (i32.shr_u (local.get $srcOff) (i32.shl (local.get $i) (i32.const 3)))
865
- (i32.const 0xFF)))
936
+ (local.set $c ${ssoCharWat('(local.get $ptr)', '(local.get $i)')})
866
937
  (if (i32.and (i32.ge_u (local.get $c) (local.get $lo)) (i32.le_u (local.get $c) (local.get $hi)))
867
938
  (then (local.set $c (i32.add (local.get $c) (local.get $delta)))))
868
939
  (i32.store8 (i32.add (local.get $off) (local.get $i)) (local.get $c))
@@ -964,7 +1035,7 @@ export default (ctx) => {
964
1035
  ;; Array (type=1) → join(",") like JS Array.toString()
965
1036
  (if (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
966
1037
  (then (return (i64.reinterpret_f64 (call $__str_join (local.get $val)
967
- (i64.reinterpret_f64 (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT | 1}) (i32.const 44))))))))
1038
+ (i64.reinterpret_f64 (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${ssoAux(1)}) (i32.const 44))))))))
968
1039
  (local.get $val))`)
969
1040
 
970
1041
  // Copy bytes of a string (SSO or heap) into memory at dst. Uses memory.copy for
@@ -973,18 +1044,15 @@ export default (ctx) => {
973
1044
  (local $w i32)
974
1045
  (if (i64.ne (i64.and (local.get $src) (i64.const ${SSO_BIT_I64})) (i64.const 0))
975
1046
  (then
976
- ;; SSO: up to 4 chars packed in low 32 bits (LE byte order). Unroll: write 1/2/3/4 bytes
977
- ;; depending on len. (len > 4 is rare/disallowed in practice fallback handles up to 4.)
978
- (local.set $w (i32.wrap_i64 (local.get $src)))
979
- (if (i32.ge_u (local.get $len) (i32.const 4))
980
- (then (i32.store (local.get $dst) (local.get $w)))
981
- (else
982
- (if (i32.eq (local.get $len) (i32.const 0)) (then (return)))
983
- (i32.store8 (local.get $dst) (local.get $w))
984
- (if (i32.eq (local.get $len) (i32.const 1)) (then (return)))
985
- (i32.store8 offset=1 (local.get $dst) (i32.shr_u (local.get $w) (i32.const 8)))
986
- (if (i32.eq (local.get $len) (i32.const 2)) (then (return)))
987
- (i32.store8 offset=2 (local.get $dst) (i32.shr_u (local.get $w) (i32.const 16))))))
1047
+ ;; SSO: write $len ASCII chars, each 7-bit-packed at payload bit i*7 (chars 4-5
1048
+ ;; span into aux, so read via the 7-bit codec, not the low 32 bits). $w = index.
1049
+ (local.set $w (i32.const 0))
1050
+ (loop $ssocp
1051
+ (if (i32.lt_u (local.get $w) (local.get $len))
1052
+ (then
1053
+ (i32.store8 (i32.add (local.get $dst) (local.get $w)) ${ssoCharWat('(local.get $src)', '(local.get $w)')})
1054
+ (local.set $w (i32.add (local.get $w) (i32.const 1)))
1055
+ (br $ssocp)))))
988
1056
  (else
989
1057
  ;; Heap STRING: memory.copy directly from string data
990
1058
  (memory.copy (local.get $dst)
@@ -1016,12 +1084,12 @@ export default (ctx) => {
1016
1084
  (then
1017
1085
  (return (call $__mkptr
1018
1086
  (i32.const ${PTR.STRING})
1019
- (i32.or (i32.const ${LAYOUT.SSO_BIT}) (local.get $total))
1087
+ (i32.or (i32.const ${LAYOUT.SSO_BIT}) (i32.shl (local.get $total) (i32.const ${SSO_LEN_SHIFT})))
1020
1088
  (i32.or
1021
1089
  (i32.wrap_i64 (i64.and (local.get $a) (i64.const 0xFFFFFFFF)))
1022
1090
  (i32.shl
1023
1091
  (i32.wrap_i64 (i64.and (local.get $b) (i64.const 0xFFFFFFFF)))
1024
- (i32.shl (local.get $alen) (i32.const 3))))))))`
1092
+ (i32.mul (local.get $alen) (i32.const 7))))))))`
1025
1093
 
1026
1094
  const concatFast = !ctx.memory.shared && ctx.transform.alloc !== false ? `
1027
1095
  (local.set $ta (i32.wrap_i64 (i64.and (i64.shr_u (local.get $a) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
@@ -1048,6 +1116,18 @@ export default (ctx) => {
1048
1116
  (global.set $__heap (local.get $newHeap))
1049
1117
  (return (f64.reinterpret_i64 (local.get $a)))))` : ''
1050
1118
 
1119
+ // Always-fresh tail: allocate a new buffer and copy both operands. Shared by the
1120
+ // bump-extend concats (reached when `a` is NOT heap-top) and the `_fresh` variants
1121
+ // (which never bump-extend at all — emit routes a NON-self-accumulating `t = s + x`
1122
+ // here so it can't mutate the live `s` the way the heap-top extend would).
1123
+ const allocCopyTail = `
1124
+ (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $total))))
1125
+ (i32.store (local.get $off) (local.get $total))
1126
+ (local.set $off (i32.add (local.get $off) (i32.const 4)))
1127
+ (call $__str_copy (local.get $a) (local.get $off) (local.get $alen))
1128
+ (call $__str_copy (local.get $b) (i32.add (local.get $off) (local.get $alen)) (local.get $blen))
1129
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
1130
+
1051
1131
  // Fused single-byte append: `buf += str[i]` lowers to this when both sides are
1052
1132
  // VAL.STRING and the rhs is a string-index. Skips __str_idx's 1-char SSO
1053
1133
  // construction and __str_concat's type-dispatch — byte goes directly from
@@ -1086,19 +1166,17 @@ export default (ctx) => {
1086
1166
  (i32.wrap_i64 (i64.shr_u (local.get $a) (i64.const ${LAYOUT.AUX_SHIFT})))
1087
1167
  (i32.const ${LAYOUT.SSO_BIT})))
1088
1168
  (then
1089
- (local.set $alen (i32.and
1090
- (i32.wrap_i64 (i64.shr_u (local.get $a) (i64.const ${LAYOUT.AUX_SHIFT})))
1091
- (i32.const ${LAYOUT.SSO_BIT - 1})))
1169
+ (local.set $alen ${ssoLenWat('(local.get $a)')})
1092
1170
  (if (i32.and
1093
1171
  (i32.lt_u (local.get $alen) (i32.const 4))
1094
1172
  (i32.lt_u (local.get $byte) (i32.const 0x80)))
1095
1173
  (then
1096
1174
  (return (call $__mkptr
1097
1175
  (i32.const ${PTR.STRING})
1098
- (i32.or (i32.const ${LAYOUT.SSO_BIT}) (i32.add (local.get $alen) (i32.const 1)))
1176
+ (i32.or (i32.const ${LAYOUT.SSO_BIT}) (i32.shl (i32.add (local.get $alen) (i32.const 1)) (i32.const ${SSO_LEN_SHIFT})))
1099
1177
  (i32.or
1100
1178
  (local.get $aoff)
1101
- (i32.shl (local.get $byte) (i32.shl (local.get $alen) (i32.const 3))))))))))
1179
+ (i32.shl (local.get $byte) (i32.mul (local.get $alen) (i32.const 7))))))))))
1102
1180
  ;; Slow path: allocate new heap STRING with original bytes + 1 new byte
1103
1181
  (local.set $alen (call $__str_byteLen (local.get $a)))
1104
1182
  (local.set $total (i32.add (local.get $alen) (i32.const 1)))
@@ -1109,6 +1187,10 @@ export default (ctx) => {
1109
1187
  (i32.store8 (i32.add (local.get $off) (local.get $alen)) (local.get $byte))
1110
1188
  (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
1111
1189
 
1190
+ // __str_concat / __str_concat_raw bump-EXTEND `a` in place when it is the heap-top own string
1191
+ // (the O(N) accumulator path). Emit calls these ONLY when the source is a self-accumulation
1192
+ // `x = x + …` (so the mutated `a` is dead-after-reassign) or when `a` is a provably-fresh
1193
+ // module-internal temporary; a plain `t = s + x` over a live `s` routes to the _fresh twins.
1112
1194
  wat('__str_concat', `(func $__str_concat (param $a i64) (param $b i64) (result f64)
1113
1195
  (local $alen i32) (local $blen i32) (local $total i32) (local $off i32)
1114
1196
  (local $ta i32) (local $aoff i32) (local $newHeap i32)
@@ -1121,13 +1203,7 @@ export default (ctx) => {
1121
1203
  (if (i32.eqz (local.get $total))
1122
1204
  (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
1123
1205
  ${ssoResultFast}
1124
- ${concatFast}
1125
- (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $total))))
1126
- (i32.store (local.get $off) (local.get $total))
1127
- (local.set $off (i32.add (local.get $off) (i32.const 4)))
1128
- (call $__str_copy (local.get $a) (local.get $off) (local.get $alen))
1129
- (call $__str_copy (local.get $b) (i32.add (local.get $off) (local.get $alen)) (local.get $blen))
1130
- (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
1206
+ ${concatFast}${allocCopyTail}`)
1131
1207
 
1132
1208
  wat('__str_concat_raw', `(func $__str_concat_raw (param $a i64) (param $b i64) (result f64)
1133
1209
  (local $alen i32) (local $blen i32) (local $total i32) (local $off i32)
@@ -1138,13 +1214,30 @@ export default (ctx) => {
1138
1214
  (if (i32.eqz (local.get $total))
1139
1215
  (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
1140
1216
  ${ssoResultFast}
1141
- ${concatFast}
1142
- (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $total))))
1143
- (i32.store (local.get $off) (local.get $total))
1144
- (local.set $off (i32.add (local.get $off) (i32.const 4)))
1145
- (call $__str_copy (local.get $a) (local.get $off) (local.get $alen))
1146
- (call $__str_copy (local.get $b) (i32.add (local.get $off) (local.get $alen)) (local.get $blen))
1147
- (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
1217
+ ${concatFast}${allocCopyTail}`)
1218
+
1219
+ // Non-mutating twins: same SSO-pair fast path, but NEVER bump-extend — always alloc+copy a fresh
1220
+ // buffer, leaving `a` untouched. The default for emit's user-level `+` (any `t = s + x` where the
1221
+ // result is not assigned straight back to `s`), so string immutability holds for live operands.
1222
+ wat('__str_concat_fresh', `(func $__str_concat_fresh (param $a i64) (param $b i64) (result f64)
1223
+ (local $alen i32) (local $blen i32) (local $total i32) (local $off i32)
1224
+ (local.set $a (call $__to_str (local.get $a)))
1225
+ (local.set $b (call $__to_str (local.get $b)))
1226
+ (local.set $alen (call $__str_byteLen (local.get $a)))
1227
+ (local.set $blen (call $__str_byteLen (local.get $b)))
1228
+ (local.set $total (i32.add (local.get $alen) (local.get $blen)))
1229
+ (if (i32.eqz (local.get $total))
1230
+ (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
1231
+ ${ssoResultFast}${allocCopyTail}`)
1232
+
1233
+ wat('__str_concat_raw_fresh', `(func $__str_concat_raw_fresh (param $a i64) (param $b i64) (result f64)
1234
+ (local $alen i32) (local $blen i32) (local $total i32) (local $off i32)
1235
+ (local.set $alen (call $__str_byteLen (local.get $a)))
1236
+ (local.set $blen (call $__str_byteLen (local.get $b)))
1237
+ (local.set $total (i32.add (local.get $alen) (local.get $blen)))
1238
+ (if (i32.eqz (local.get $total))
1239
+ (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
1240
+ ${ssoResultFast}${allocCopyTail}`)
1148
1241
 
1149
1242
  wat('__str_replace', `(func $__str_replace (param $str i64) (param $search i64) (param $repl i64) (result f64)
1150
1243
  (local $idx i32) (local $slen i32)
@@ -1379,11 +1472,11 @@ export default (ctx) => {
1379
1472
  (then
1380
1473
  (local.set $sb (i32.load8_u (i32.add (local.get $off) (local.get $i))))
1381
1474
  (br_if $heap (i32.ge_u (local.get $sb) (i32.const 0x80)))
1382
- (local.set $sp (i32.or (local.get $sp) (i32.shl (local.get $sb) (i32.shl (local.get $i) (i32.const 3)))))
1475
+ (local.set $sp (i32.or (local.get $sp) (i32.shl (local.get $sb) (i32.mul (local.get $i) (i32.const 7)))))
1383
1476
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
1384
1477
  (br $pk))))
1385
1478
  (global.set $__heap (i32.sub (local.get $off) (i32.const 4)))
1386
- (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.or (i32.const ${LAYOUT.SSO_BIT}) (local.get $target)) (local.get $sp))))))` : ''}
1479
+ (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))))))` : ''}
1387
1480
  (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
1388
1481
 
1389
1482
  // Base helpers (__sso_char/__str_char/__char_at/__str_byteLen) are referenced
@@ -1621,7 +1714,7 @@ export default (ctx) => {
1621
1714
 
1622
1715
  const padMethod = (start) => (str, len, pad) => {
1623
1716
  inc('__str_pad')
1624
- const vpad = pad != null ? asI64(emit(pad)) : ['i64.reinterpret_f64', mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1, 32)]
1717
+ const vpad = pad != null ? asI64(emit(pad)) : ['i64.reinterpret_f64', mkPtrIR(PTR.STRING, ssoAux(1), 32)]
1625
1718
  return typed(['call', '$__str_pad', asI64(emit(str)), asI32(emit(len)), vpad, ['i32.const', start]], 'f64')
1626
1719
  }
1627
1720
  bind('.padStart', padMethod(1))
@@ -1688,14 +1781,17 @@ export default (ctx) => {
1688
1781
  // index; otherwise `oobIR`. Without the bounds check `__char_at` returns 0 for an
1689
1782
  // out-of-range index, which would wrap to a bogus `"\x00"` string (charAt → "",
1690
1783
  // at → undefined). `sLocal` is the receiver as an f64 local.
1691
- const charAtOr = (sLocal, iLocal, lenIR, oobIR) =>
1784
+ const charAtOr = (sLocal, iLocal, lenIR, oobIR) => (
1785
+ inc('__char1byte'),
1692
1786
  ['if', ['result', 'f64'],
1693
1787
  ['i32.and',
1694
1788
  ['i32.ge_s', ['local.get', iLocal], ['i32.const', 0]],
1695
1789
  ['i32.lt_s', ['local.get', iLocal], lenIR]],
1696
- ['then', mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1,
1697
- ['call', '$__char_at', ['i64.reinterpret_f64', ['local.get', sLocal]], ['local.get', iLocal]])],
1698
- ['else', oobIR]]
1790
+ // __char_at yields a raw byte (≥0x80 for non-ASCII heap strings) → __char1byte
1791
+ // builds an SSO byte when ASCII, else a heap 1-byte string.
1792
+ ['then', ['call', '$__char1byte',
1793
+ ['call', '$__char_at', ['i64.reinterpret_f64', ['local.get', sLocal]], ['local.get', iLocal]]]],
1794
+ ['else', oobIR]])
1699
1795
  const emptyStr = mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT, 0)
1700
1796
 
1701
1797
  // .charAt(i) → 1-char string at index i, or "" when out of range (JS spec: no
@@ -1742,13 +1838,27 @@ export default (ctx) => {
1742
1838
  return typed(['f64.reinterpret_i64', toStrI64(value, emit(value))], 'f64')
1743
1839
  })
1744
1840
 
1841
+ // 1-byte string from a raw byte: SSO when ASCII (<0x80, fits the 7-bit codec),
1842
+ // else a heap 1-byte string (the 7-bit SSO can't represent bytes ≥0x80).
1843
+ wat('__char1byte', `(func $__char1byte (param $b i32) (result f64)
1844
+ (local $off i32)
1845
+ (local.set $b (i32.and (local.get $b) (i32.const 0xFF)))
1846
+ (if (result f64) (i32.lt_u (local.get $b) (i32.const 0x80))
1847
+ (then (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${ssoAux(1)}) (local.get $b)))
1848
+ (else
1849
+ (local.set $off (call $__alloc (i32.const 8)))
1850
+ (i32.store (local.get $off) (i32.const 1))
1851
+ (i32.store8 (i32.add (local.get $off) (i32.const 4)) (local.get $b))
1852
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (i32.add (local.get $off) (i32.const 4))))))`)
1853
+
1745
1854
  // String.fromCharCode(...codes) — variadic; each arg is ToUint16(ToNumber(code))
1746
1855
  // → a 1-byte string, concatenated left to right (mirrors String.fromCodePoint).
1747
1856
  bind('String.fromCharCode', (...codes) => {
1748
1857
  if (codes.length === 0) return emit(['str', ''])
1749
1858
  // ToUint16(ToNumber(code)): `toNumF64` performs ToPrimitive on an object
1750
- // argument, so a throwing valueOf/toString propagates per spec.
1751
- const one = (node) => mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1, asI32(toNumF64(node, emit(node))))
1859
+ // argument, so a throwing valueOf/toString propagates per spec. A byte ≥0x80
1860
+ // can't be a 7-bit-ASCII SSO, so __char1byte routes it to a heap 1-byte string.
1861
+ const one = (node) => { inc('__char1byte'); return typed(['call', '$__char1byte', asI32(toNumF64(node, emit(node)))], 'f64') }
1752
1862
  let r = one(codes[0])
1753
1863
  for (let i = 1; i < codes.length; i++) {
1754
1864
  inc('__str_concat_raw')
@@ -1771,27 +1881,41 @@ export default (ctx) => {
1771
1881
  (local.set $cp (i32.trunc_sat_f64_s (local.get $cpf)))
1772
1882
  ;; ASCII: 1 byte SSO
1773
1883
  (if (i32.lt_u (local.get $cp) (i32.const 128))
1774
- (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT | 1}) (local.get $cp)))))
1775
- ;; 2-byte: 0x80-0x7FF → SSO
1884
+ (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${ssoAux(1)}) (local.get $cp)))))
1885
+ ;; multi-byte (cp ≥ 0x80): UTF-8 bytes are ≥0x80 can't be 7-bit ASCII SSO, so
1886
+ ;; build a heap STRING (len header at $off, bytes at $off+4). Over-alloc to 8 so the
1887
+ ;; i32.store of the packed bytes never runs past the buffer; the len word bounds reads.
1888
+ ;; 2-byte: 0x80-0x7FF
1776
1889
  (if (i32.lt_u (local.get $cp) (i32.const 0x800))
1777
- (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT | 2})
1778
- (i32.or
1779
- (i32.or (i32.const 0xC0) (i32.shr_u (local.get $cp) (i32.const 6)))
1780
- (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 8)))))))
1781
- ;; 3-byte: 0x800-0xFFFF → SSO (3 bytes fits)
1890
+ (then
1891
+ (local.set $off (call $__alloc (i32.const 8)))
1892
+ (i32.store (local.get $off) (i32.const 2))
1893
+ (i32.store (i32.add (local.get $off) (i32.const 4))
1894
+ (i32.or
1895
+ (i32.or (i32.const 0xC0) (i32.shr_u (local.get $cp) (i32.const 6)))
1896
+ (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 8))))
1897
+ (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (i32.add (local.get $off) (i32.const 4))))))
1898
+ ;; 3-byte: 0x800-0xFFFF
1782
1899
  (if (i32.lt_u (local.get $cp) (i32.const 0x10000))
1783
- (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT | 3})
1784
- (i32.or (i32.or
1785
- (i32.or (i32.const 0xE0) (i32.shr_u (local.get $cp) (i32.const 12)))
1786
- (i32.shl (i32.or (i32.const 0x80) (i32.and (i32.shr_u (local.get $cp) (i32.const 6)) (i32.const 0x3F))) (i32.const 8)))
1787
- (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 16)))))))
1788
- ;; 4-byte: 0x10000-0x10FFFF SSO (4 bytes fits)
1789
- (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT | 4})
1900
+ (then
1901
+ (local.set $off (call $__alloc (i32.const 8)))
1902
+ (i32.store (local.get $off) (i32.const 3))
1903
+ (i32.store (i32.add (local.get $off) (i32.const 4))
1904
+ (i32.or (i32.or
1905
+ (i32.or (i32.const 0xE0) (i32.shr_u (local.get $cp) (i32.const 12)))
1906
+ (i32.shl (i32.or (i32.const 0x80) (i32.and (i32.shr_u (local.get $cp) (i32.const 6)) (i32.const 0x3F))) (i32.const 8)))
1907
+ (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 16))))
1908
+ (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (i32.add (local.get $off) (i32.const 4))))))
1909
+ ;; 4-byte: 0x10000-0x10FFFF
1910
+ (local.set $off (call $__alloc (i32.const 8)))
1911
+ (i32.store (local.get $off) (i32.const 4))
1912
+ (i32.store (i32.add (local.get $off) (i32.const 4))
1790
1913
  (i32.or (i32.or (i32.or
1791
1914
  (i32.or (i32.const 0xF0) (i32.shr_u (local.get $cp) (i32.const 18)))
1792
1915
  (i32.shl (i32.or (i32.const 0x80) (i32.and (i32.shr_u (local.get $cp) (i32.const 12)) (i32.const 0x3F))) (i32.const 8)))
1793
1916
  (i32.shl (i32.or (i32.const 0x80) (i32.and (i32.shr_u (local.get $cp) (i32.const 6)) (i32.const 0x3F))) (i32.const 16)))
1794
- (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 24))))))`)
1917
+ (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 24))))
1918
+ (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (i32.add (local.get $off) (i32.const 4)))))`)
1795
1919
 
1796
1920
  // String.fromCodePoint(...codePoints) — variadic; each arg is ToNumber-coerced
1797
1921
  // then validated/encoded by __fromCodePoint, results concatenated left to right.