jz 0.7.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 (53) 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 +1156 -984
  7. package/index.js +40 -9
  8. package/interop.js +193 -138
  9. package/layout.js +29 -18
  10. package/module/array.js +29 -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 +26 -3
  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 +162 -156
  20. package/module/typedarray.js +169 -105
  21. package/package.json +7 -3
  22. package/src/abi/string.js +29 -29
  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 +253 -23
  28. package/src/compile/index.js +198 -61
  29. package/src/compile/loop-divmod.js +12 -58
  30. package/src/compile/loop-model.js +91 -0
  31. package/src/compile/loop-square.js +102 -0
  32. package/src/compile/narrow.js +169 -32
  33. package/src/compile/peel-stencil.js +18 -64
  34. package/src/compile/plan/common.js +29 -0
  35. package/src/compile/plan/index.js +4 -1
  36. package/src/compile/plan/inline.js +176 -21
  37. package/src/compile/plan/literals.js +93 -19
  38. package/src/ctx.js +51 -12
  39. package/src/helper-counters.js +137 -0
  40. package/src/ir.js +102 -13
  41. package/src/kind-traits.js +7 -3
  42. package/src/kind.js +14 -1
  43. package/src/op-policy.js +5 -2
  44. package/src/ops.js +119 -0
  45. package/src/optimize/index.js +960 -136
  46. package/src/optimize/recurse.js +182 -0
  47. package/src/optimize/vectorize.js +1292 -144
  48. package/src/prepare/index.js +11 -7
  49. package/src/reps.js +4 -1
  50. package/src/type.js +53 -45
  51. package/src/wat/assemble.js +92 -9
  52. package/src/widen.js +21 -0
  53. 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
@@ -112,8 +146,13 @@ 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'],
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'],
117
156
  __str_indexof: ['__str_byteLen'],
118
157
  __str_lastindexof: ['__str_byteLen'],
119
158
  __wrap1: ['__alloc', '__mkptr'],
@@ -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'],
@@ -162,11 +201,9 @@ export default (ctx) => {
162
201
  // === String literal: "abc" → SSO if ≤4 ASCII, else static data ===
163
202
 
164
203
  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)
204
+ if (ctx.features.sso) {
205
+ const sso = ssoEncode(str)
206
+ if (sso) return mkPtrIR(PTR.STRING, sso.aux, sso.offset)
170
207
  }
171
208
  const bytes = new TextEncoder().encode(str)
172
209
  const len = bytes.length
@@ -223,11 +260,7 @@ export default (ctx) => {
223
260
  // SSO/STRING ptrs never have forwarding pointers (only ARRAY does), so we extract
224
261
  // the raw offset directly instead of paying the __ptr_offset function-call overhead.
225
262
  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)))`)
263
+ ${ssoCharWat('(local.get $ptr)', '(local.get $i)')})`)
231
264
 
232
265
  wat('__str_char', `(func $__str_char (param $ptr i64) (param $i i32) (result i32)
233
266
  (i32.load8_u (i32.add
@@ -254,16 +287,9 @@ export default (ctx) => {
254
287
  (i64.ne (i64.and (local.get $ptr) (i64.const ${SSO_BIT_I64})) (i64.const 0))
255
288
  (then
256
289
  (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})))
290
+ (i32.ge_u (local.get $i) ${ssoLenWat('(local.get $ptr)')})
261
291
  (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)))))
292
+ (else ${ssoCharWat('(local.get $ptr)', '(local.get $i)')})))
267
293
  (else
268
294
  (if (result i32)
269
295
  (i32.ge_u (local.get $i)
@@ -301,9 +327,7 @@ export default (ctx) => {
301
327
  (i32.const ${LAYOUT.SSO_BIT})))
302
328
  (local.set $len
303
329
  (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})))
330
+ (then ${ssoLenWat('(local.get $ptr)')})
307
331
  (else
308
332
  (if (result i32)
309
333
  (i64.ne (i64.and (local.get $ptr) (i64.const ${SLICE_BIT_I64})) (i64.const 0))
@@ -318,14 +342,15 @@ export default (ctx) => {
318
342
  (i32.or (i32.lt_s (local.get $i) (i32.const 0)) (i32.ge_u (local.get $i) (local.get $len)))
319
343
  (then (f64.const nan:${UNDEF_NAN}))
320
344
  (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)))))))))))`)
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)))))))))`)
329
354
 
330
355
  // Hot: ~53M calls in watr self-host. Bit-eq covers identity. SSO/SSO with !bit-eq
331
356
  // guarantees content differs (high 32 bits encode type+len; both equal → low 32 differs
@@ -372,9 +397,12 @@ export default (ctx) => {
372
397
  (local $ta i32) (local $tb i32)
373
398
  (local $offA i32) (local $offB i32)
374
399
  (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))))
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.
378
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}))))
379
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}))))
380
408
  (local.set $offA (i32.wrap_i64 (i64.and (local.get $a) (i64.const ${LAYOUT.OFFSET_MASK}))))
@@ -385,22 +413,6 @@ export default (ctx) => {
385
413
  (local.set $ssoB (i32.and
386
414
  (i32.wrap_i64 (i64.shr_u (local.get $b) (i64.const ${LAYOUT.AUX_SHIFT})))
387
415
  (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
416
  ;; Both heap STRING fast path: inline len from header. Chunk by 4 bytes via unaligned
405
417
  ;; i32.load (wasm guarantees unaligned-OK), then byte-tail. Most string comparisons fail
406
418
  ;; early on the first 4-byte word, so this collapses the per-byte branch overhead into a
@@ -489,7 +501,7 @@ export default (ctx) => {
489
501
  (i32.wrap_i64 (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.AUX_SHIFT})))
490
502
  (i32.const ${LAYOUT.AUX_MASK})))
491
503
  (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})))
504
+ (then ${ssoLenFromAux('(local.get $aux)')})
493
505
  (else
494
506
  (if (result i32) (i32.and (local.get $aux) (i32.const ${LAYOUT.SLICE_BIT}))
495
507
  ;; view: length lives in aux[12:0], not a header.
@@ -507,21 +519,11 @@ export default (ctx) => {
507
519
  // (single bulk op instead of nlen × __char_at).
508
520
  wat('__str_slice', () => `(func $__str_slice (param $ptr i64) (param $start i32) (param $end i32) (result f64)
509
521
  (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)
522
+ (local $srcOff i32) (local $isSso i32) (local $sb i32) (local $sp i32) (local $sp64 i64)
511
523
  (local $ip i32) (local $h i32) (local $j i32) (local $slot i32) (local $cand i32) (local $k i32)
512
524
  (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))))
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)))
525
527
  (if (i32.ge_s (local.get $start) (local.get $end))
526
528
  (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
527
529
  (local.set $nlen (i32.sub (local.get $end) (local.get $start)))
@@ -560,18 +562,8 @@ export default (ctx) => {
560
562
  (local $len i32) (local $nlen i32) (local $srcOff i32) (local $tag i32)
561
563
  (local $ip i32) (local $h i32) (local $j i32) (local $slot i32) (local $cand i32) (local $k i32)
562
564
  (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))))
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)))
575
567
  (if (i32.ge_s (local.get $start) (local.get $end))
576
568
  (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
577
569
  (local.set $nlen (i32.sub (local.get $end) (local.get $start)))
@@ -676,18 +668,8 @@ export default (ctx) => {
676
668
  wat('__str_slice_eq', `(func $__str_slice_eq (param $ptr i64) (param $start i32) (param $end i32) (param $other i64) (result i32)
677
669
  (local $len i32)
678
670
  (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))))
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)))
691
673
  (call $__str_range_eq (local.get $ptr) (local.get $start) (local.get $end) (local.get $other)))`)
692
674
 
693
675
  // Hoist SSO/heap dispatch for hay and ndl out of the inner byte loop. Inner
@@ -719,10 +701,10 @@ export default (ctx) => {
719
701
  (br_if $nomatch (i32.ge_s (local.get $j) (local.get $nlen)))
720
702
  (local.set $k (i32.add (local.get $i) (local.get $j)))
721
703
  (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)))
704
+ (then ${ssoCharWat('(local.get $hay)', '(local.get $k)')})
723
705
  (else (i32.load8_u (i32.add (local.get $hoff) (local.get $k))))))
724
706
  (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)))
707
+ (then ${ssoCharWat('(local.get $ndl)', '(local.get $j)')})
726
708
  (else (i32.load8_u (i32.add (local.get $noff) (local.get $j))))))
727
709
  (if (i32.ne (local.get $hb) (local.get $nb))
728
710
  (then (local.set $match (i32.const 0)) (br $nomatch)))
@@ -771,10 +753,10 @@ export default (ctx) => {
771
753
  (br_if $nomatch (i32.ge_s (local.get $j) (local.get $nlen)))
772
754
  (local.set $k (i32.add (local.get $i) (local.get $j)))
773
755
  (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)))
756
+ (then ${ssoCharWat('(local.get $hay)', '(local.get $k)')})
775
757
  (else (i32.load8_u (i32.add (local.get $hoff) (local.get $k))))))
776
758
  (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)))
759
+ (then ${ssoCharWat('(local.get $ndl)', '(local.get $j)')})
778
760
  (else (i32.load8_u (i32.add (local.get $noff) (local.get $j))))))
779
761
  (if (i32.ne (local.get $hb) (local.get $nb))
780
762
  (then (local.set $match (i32.const 0)) (br $nomatch)))
@@ -804,10 +786,10 @@ export default (ctx) => {
804
786
  (br_if $done (i32.ge_s (local.get $i) (local.get $plen)))
805
787
  (if (i32.ne
806
788
  (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)))
789
+ (then ${ssoCharWat('(local.get $str)', '(local.get $i)')})
808
790
  (else (i32.load8_u (i32.add (local.get $soff) (local.get $i)))))
809
791
  (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)))
792
+ (then ${ssoCharWat('(local.get $pfx)', '(local.get $i)')})
811
793
  (else (i32.load8_u (i32.add (local.get $poff) (local.get $i))))))
812
794
  (then (return (i32.const 0))))
813
795
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
@@ -835,10 +817,10 @@ export default (ctx) => {
835
817
  (local.set $k (i32.add (local.get $off) (local.get $i)))
836
818
  (if (i32.ne
837
819
  (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)))
820
+ (then ${ssoCharWat('(local.get $str)', '(local.get $k)')})
839
821
  (else (i32.load8_u (i32.add (local.get $soff) (local.get $k)))))
840
822
  (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)))
823
+ (then ${ssoCharWat('(local.get $sfx)', '(local.get $i)')})
842
824
  (else (i32.load8_u (i32.add (local.get $foff) (local.get $i))))))
843
825
  (then (return (i32.const 0))))
844
826
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
@@ -860,9 +842,7 @@ export default (ctx) => {
860
842
  (then
861
843
  (block $dsso (loop $lsso
862
844
  (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)))
845
+ (local.set $c ${ssoCharWat('(local.get $ptr)', '(local.get $i)')})
866
846
  (if (i32.and (i32.ge_u (local.get $c) (local.get $lo)) (i32.le_u (local.get $c) (local.get $hi)))
867
847
  (then (local.set $c (i32.add (local.get $c) (local.get $delta)))))
868
848
  (i32.store8 (i32.add (local.get $off) (local.get $i)) (local.get $c))
@@ -964,7 +944,7 @@ export default (ctx) => {
964
944
  ;; Array (type=1) → join(",") like JS Array.toString()
965
945
  (if (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
966
946
  (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))))))))
947
+ (i64.reinterpret_f64 (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${ssoAux(1)}) (i32.const 44))))))))
968
948
  (local.get $val))`)
969
949
 
970
950
  // Copy bytes of a string (SSO or heap) into memory at dst. Uses memory.copy for
@@ -973,18 +953,15 @@ export default (ctx) => {
973
953
  (local $w i32)
974
954
  (if (i64.ne (i64.and (local.get $src) (i64.const ${SSO_BIT_I64})) (i64.const 0))
975
955
  (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))))))
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)))))
988
965
  (else
989
966
  ;; Heap STRING: memory.copy directly from string data
990
967
  (memory.copy (local.get $dst)
@@ -1016,12 +993,12 @@ export default (ctx) => {
1016
993
  (then
1017
994
  (return (call $__mkptr
1018
995
  (i32.const ${PTR.STRING})
1019
- (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})))
1020
997
  (i32.or
1021
998
  (i32.wrap_i64 (i64.and (local.get $a) (i64.const 0xFFFFFFFF)))
1022
999
  (i32.shl
1023
1000
  (i32.wrap_i64 (i64.and (local.get $b) (i64.const 0xFFFFFFFF)))
1024
- (i32.shl (local.get $alen) (i32.const 3))))))))`
1001
+ (i32.mul (local.get $alen) (i32.const 7))))))))`
1025
1002
 
1026
1003
  const concatFast = !ctx.memory.shared && ctx.transform.alloc !== false ? `
1027
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}))))
@@ -1086,19 +1063,17 @@ export default (ctx) => {
1086
1063
  (i32.wrap_i64 (i64.shr_u (local.get $a) (i64.const ${LAYOUT.AUX_SHIFT})))
1087
1064
  (i32.const ${LAYOUT.SSO_BIT})))
1088
1065
  (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})))
1066
+ (local.set $alen ${ssoLenWat('(local.get $a)')})
1092
1067
  (if (i32.and
1093
1068
  (i32.lt_u (local.get $alen) (i32.const 4))
1094
1069
  (i32.lt_u (local.get $byte) (i32.const 0x80)))
1095
1070
  (then
1096
1071
  (return (call $__mkptr
1097
1072
  (i32.const ${PTR.STRING})
1098
- (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})))
1099
1074
  (i32.or
1100
1075
  (local.get $aoff)
1101
- (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))))))))))
1102
1077
  ;; Slow path: allocate new heap STRING with original bytes + 1 new byte
1103
1078
  (local.set $alen (call $__str_byteLen (local.get $a)))
1104
1079
  (local.set $total (i32.add (local.get $alen) (i32.const 1)))
@@ -1379,11 +1354,11 @@ export default (ctx) => {
1379
1354
  (then
1380
1355
  (local.set $sb (i32.load8_u (i32.add (local.get $off) (local.get $i))))
1381
1356
  (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)))))
1357
+ (local.set $sp (i32.or (local.get $sp) (i32.shl (local.get $sb) (i32.mul (local.get $i) (i32.const 7)))))
1383
1358
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
1384
1359
  (br $pk))))
1385
1360
  (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))))))` : ''}
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))))))` : ''}
1387
1362
  (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
1388
1363
 
1389
1364
  // Base helpers (__sso_char/__str_char/__char_at/__str_byteLen) are referenced
@@ -1621,7 +1596,7 @@ export default (ctx) => {
1621
1596
 
1622
1597
  const padMethod = (start) => (str, len, pad) => {
1623
1598
  inc('__str_pad')
1624
- 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)]
1625
1600
  return typed(['call', '$__str_pad', asI64(emit(str)), asI32(emit(len)), vpad, ['i32.const', start]], 'f64')
1626
1601
  }
1627
1602
  bind('.padStart', padMethod(1))
@@ -1688,14 +1663,17 @@ export default (ctx) => {
1688
1663
  // index; otherwise `oobIR`. Without the bounds check `__char_at` returns 0 for an
1689
1664
  // out-of-range index, which would wrap to a bogus `"\x00"` string (charAt → "",
1690
1665
  // at → undefined). `sLocal` is the receiver as an f64 local.
1691
- const charAtOr = (sLocal, iLocal, lenIR, oobIR) =>
1666
+ const charAtOr = (sLocal, iLocal, lenIR, oobIR) => (
1667
+ inc('__char1byte'),
1692
1668
  ['if', ['result', 'f64'],
1693
1669
  ['i32.and',
1694
1670
  ['i32.ge_s', ['local.get', iLocal], ['i32.const', 0]],
1695
1671
  ['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]]
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]])
1699
1677
  const emptyStr = mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT, 0)
1700
1678
 
1701
1679
  // .charAt(i) → 1-char string at index i, or "" when out of range (JS spec: no
@@ -1742,13 +1720,27 @@ export default (ctx) => {
1742
1720
  return typed(['f64.reinterpret_i64', toStrI64(value, emit(value))], 'f64')
1743
1721
  })
1744
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
+
1745
1736
  // String.fromCharCode(...codes) — variadic; each arg is ToUint16(ToNumber(code))
1746
1737
  // → a 1-byte string, concatenated left to right (mirrors String.fromCodePoint).
1747
1738
  bind('String.fromCharCode', (...codes) => {
1748
1739
  if (codes.length === 0) return emit(['str', ''])
1749
1740
  // 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))))
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') }
1752
1744
  let r = one(codes[0])
1753
1745
  for (let i = 1; i < codes.length; i++) {
1754
1746
  inc('__str_concat_raw')
@@ -1771,27 +1763,41 @@ export default (ctx) => {
1771
1763
  (local.set $cp (i32.trunc_sat_f64_s (local.get $cpf)))
1772
1764
  ;; ASCII: 1 byte SSO
1773
1765
  (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
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
1776
1771
  (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)
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
1782
1781
  (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})
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))
1790
1795
  (i32.or (i32.or (i32.or
1791
1796
  (i32.or (i32.const 0xF0) (i32.shr_u (local.get $cp) (i32.const 18)))
1792
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)))
1793
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)))
1794
- (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)))))`)
1795
1801
 
1796
1802
  // String.fromCodePoint(...codePoints) — variadic; each arg is ToNumber-coerced
1797
1803
  // then validated/encoded by __fromCodePoint, results concatenated left to right.