jz 0.1.1 → 0.2.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.
package/module/string.js CHANGED
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * String module — literals, char access, and string methods.
3
3
  *
4
- * Type=4 (STRING): heap-allocated, length in header [-4:len].
5
- * Type=5 (STRING_SSO): ≤4 ASCII chars packed in pointer offset (no memory).
4
+ * Type=4 (STRING) covers both encodings; aux bit LAYOUT.SSO_BIT discriminates:
5
+ * bit clear: heap-allocated, length in header [-4:len], offset bytes.
6
+ * bit set: inline ≤4 ASCII chars packed in offset (no memory),
7
+ * length in aux low bits (0..4).
6
8
  *
7
9
  * Methods use type-qualified keys (.string:slice) for array-colliding names,
8
10
  * generic keys (.toUpperCase) for non-colliding ones.
@@ -10,16 +12,21 @@
10
12
  * @module string
11
13
  */
12
14
 
13
- import { typed, asF64, asI32, NULL_NAN, UNDEF_NAN, mkPtrIR, temp, tempI32 } from '../src/ir.js'
15
+ import { typed, asF64, asI32, asI64, NULL_NAN, UNDEF_NAN, mkPtrIR, temp, tempI32 } from '../src/ir.js'
14
16
  import { emit } from '../src/emit.js'
15
17
  import { valTypeOf, VAL } from '../src/analyze.js'
16
- import { inc, PTR } from '../src/ctx.js'
18
+ import { inc, PTR, LAYOUT } from '../src/ctx.js'
19
+
20
+ // SSO discriminator bit pre-shifted to its slot in the full i64 ptr (bit 46).
21
+ // Used as `i64.and ptr SSO_BIT_I64` for branch-without-extracting-aux.
22
+ const SSO_BIT_I64 = '0x' + (BigInt(LAYOUT.SSO_BIT) << BigInt(LAYOUT.AUX_SHIFT)).toString(16).toUpperCase().padStart(16, '0')
17
23
 
18
24
 
19
25
  export default (ctx) => {
20
26
  Object.assign(ctx.core.stdlibDeps, {
21
27
  __str_concat: ['__to_str', '__str_byteLen', '__alloc', '__mkptr', '__str_copy'],
22
28
  __str_concat_raw: ['__str_byteLen', '__alloc', '__mkptr', '__str_copy'],
29
+ __str_append_byte: ['__str_byteLen', '__alloc', '__mkptr', '__str_copy'],
23
30
  __str_copy: [],
24
31
  __str_slice: ['__str_byteLen', '__alloc'],
25
32
  __str_indexof: ['__str_byteLen'],
@@ -34,8 +41,9 @@ export default (ctx) => {
34
41
  __str_replace: ['__str_indexof', '__str_slice', '__str_concat'],
35
42
  __str_replaceall: ['__str_indexof', '__str_slice', '__str_concat'],
36
43
  __str_split: ['__str_slice', '__str_byteLen', '__char_at', '__alloc'],
37
- __str_idx: ['__str_byteLen', '__char_at', '__mkptr'],
44
+ __str_idx: [],
38
45
  __str_eq: ['__char_at'],
46
+ __str_cmp: ['__char_at', '__str_byteLen'],
39
47
  __str_pad: ['__str_byteLen', '__str_copy', '__alloc'],
40
48
  __str_join: ['__str_concat', '__to_str', '__str_byteLen', '__len', '__ptr_offset'],
41
49
  __str_encode: ['__str_byteLen', '__str_copy'],
@@ -52,7 +60,7 @@ export default (ctx) => {
52
60
  if (ctx.features.sso && str.length <= MAX_SSO && /^[\x00-\x7f]*$/.test(str)) {
53
61
  let packed = 0
54
62
  for (let i = 0; i < str.length; i++) packed |= str.charCodeAt(i) << (i * 8)
55
- return mkPtrIR(PTR.SSO, str.length, packed)
63
+ return mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | str.length, packed)
56
64
  }
57
65
  const bytes = new TextEncoder().encode(str)
58
66
  const len = bytes.length
@@ -91,28 +99,25 @@ export default (ctx) => {
91
99
 
92
100
  // SSO/STRING ptrs never have forwarding pointers (only ARRAY does), so we extract
93
101
  // the raw offset directly instead of paying the __ptr_offset function-call overhead.
94
- ctx.core.stdlib['__sso_char'] = `(func $__sso_char (param $ptr f64) (param $i i32) (result i32)
102
+ ctx.core.stdlib['__sso_char'] = `(func $__sso_char (param $ptr i64) (param $i i32) (result i32)
95
103
  (i32.and
96
104
  (i32.shr_u
97
- (i32.wrap_i64 (i64.and (i64.reinterpret_f64 (local.get $ptr)) (i64.const 0xFFFFFFFF)))
105
+ (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK})))
98
106
  (i32.mul (local.get $i) (i32.const 8)))
99
107
  (i32.const 0xFF)))`
100
108
 
101
- ctx.core.stdlib['__str_char'] = `(func $__str_char (param $ptr f64) (param $i i32) (result i32)
109
+ ctx.core.stdlib['__str_char'] = `(func $__str_char (param $ptr i64) (param $i i32) (result i32)
102
110
  (i32.load8_u (i32.add
103
- (i32.wrap_i64 (i64.and (i64.reinterpret_f64 (local.get $ptr)) (i64.const 0xFFFFFFFF)))
111
+ (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK})))
104
112
  (local.get $i))))`
105
113
 
106
- // Hot (~37M calls in watr self-host). Type+offset extracted once from $bits;
107
- // SSO/STRING bodies merged inline to skip 2 function calls per char fetch.
108
- ctx.core.stdlib['__char_at'] = `(func $__char_at (param $ptr f64) (param $i i32) (result i32)
109
- (local $bits i64) (local $off i32)
110
- (local.set $bits (i64.reinterpret_f64 (local.get $ptr)))
111
- (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
114
+ // Hot (~37M calls in watr self-host). Caller guarantees $ptr is a STRING;
115
+ // SSO bit picks inline-byte-extract vs heap memory load.
116
+ ctx.core.stdlib['__char_at'] = `(func $__char_at (param $ptr i64) (param $i i32) (result i32)
117
+ (local $off i32)
118
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
112
119
  (if (result i32)
113
- (i32.eq
114
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF)))
115
- (i32.const ${PTR.SSO}))
120
+ (i64.ne (i64.and (local.get $ptr) (i64.const ${SSO_BIT_I64})) (i64.const 0))
116
121
  (then
117
122
  (i32.and
118
123
  (i32.shr_u (local.get $off) (i32.mul (local.get $i) (i32.const 8)))
@@ -120,44 +125,66 @@ export default (ctx) => {
120
125
  (else
121
126
  (i32.load8_u (i32.add (local.get $off) (local.get $i))))))`
122
127
 
123
- ctx.core.stdlib['__str_idx'] = `(func $__str_idx (param $ptr f64) (param $i i32) (result f64)
124
- (local $len i32)
125
- (local.set $len (call $__str_byteLen (local.get $ptr)))
128
+ ctx.core.stdlib['__str_idx'] = `(func $__str_idx (param $ptr i64) (param $i i32) (result f64)
129
+ (local $t i32) (local $off i32) (local $len i32) (local $isSso i32)
130
+ (local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
131
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
132
+ (local.set $isSso (i32.and
133
+ (i32.wrap_i64 (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.AUX_SHIFT})))
134
+ (i32.const ${LAYOUT.SSO_BIT})))
135
+ (local.set $len
136
+ (if (result i32) (local.get $isSso)
137
+ (then (i32.and
138
+ (i32.wrap_i64 (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.AUX_SHIFT})))
139
+ (i32.const ${LAYOUT.SSO_BIT - 1})))
140
+ (else
141
+ (if (result i32) (i32.and (i32.eq (local.get $t) (i32.const ${PTR.STRING})) (i32.ge_u (local.get $off) (i32.const 4)))
142
+ (then (i32.load (i32.sub (local.get $off) (i32.const 4))))
143
+ (else (i32.const 0))))))
126
144
  (if (result f64)
127
- (i32.or
128
- (i32.lt_s (local.get $i) (i32.const 0))
129
- (i32.ge_u (local.get $i) (local.get $len)))
145
+ (i32.or (i32.lt_s (local.get $i) (i32.const 0)) (i32.ge_u (local.get $i) (local.get $len)))
130
146
  (then (f64.const nan:${UNDEF_NAN}))
131
147
  (else
132
- (call $__mkptr
133
- (i32.const ${PTR.SSO})
134
- (i32.const 1)
135
- (call $__char_at (local.get $ptr) (local.get $i))))))`
148
+ (f64.reinterpret_i64
149
+ (i64.or
150
+ ;; mkptr(STRING, SSO_BIT|1, 0) = NAN_PREFIX | (STRING<<TAG_SHIFT) | ((SSO_BIT|1)<<AUX_SHIFT)
151
+ (i64.const ${'0x' + (LAYOUT.NAN_PREFIX_BITS | (BigInt(PTR.STRING) << BigInt(LAYOUT.TAG_SHIFT)) | ((BigInt(LAYOUT.SSO_BIT) | 1n) << BigInt(LAYOUT.AUX_SHIFT))).toString(16).toUpperCase().padStart(16, '0')})
152
+ (i64.extend_i32_u
153
+ (if (result i32) (local.get $isSso)
154
+ (then (i32.and (i32.shr_u (local.get $off) (i32.mul (local.get $i) (i32.const 8))) (i32.const 0xFF)))
155
+ (else (i32.load8_u (i32.add (local.get $off) (local.get $i)))))))))))`
136
156
 
137
157
  // Hot: ~53M calls in watr self-host. Bit-eq covers identity. SSO/SSO with !bit-eq
138
158
  // guarantees content differs (high 32 bits encode type+len; both equal → low 32 differs
139
- // ⇒ bytes differ). STRING/STRING uses raw load8_u — no per-byte function calls.
140
- // Mixed SSO×STRING is rare; falls back to __char_at.
141
- ctx.core.stdlib['__str_eq'] = `(func $__str_eq (param $a f64) (param $b f64) (result i32)
159
+ // ⇒ bytes differ). Heap/heap uses raw load8_u — no per-byte function calls.
160
+ // Mixed SSO×heap is rare; falls back to __char_at.
161
+ ctx.core.stdlib['__str_eq'] = `(func $__str_eq (param $a i64) (param $b i64) (result i32)
142
162
  (local $len i32) (local $lenB i32) (local $i i32)
143
- (local $ba i64) (local $bb i64) (local $ta i32) (local $tb i32)
163
+ (local $ta i32) (local $tb i32)
144
164
  (local $offA i32) (local $offB i32)
145
- (local.set $ba (i64.reinterpret_f64 (local.get $a)))
146
- (local.set $bb (i64.reinterpret_f64 (local.get $b)))
147
- (if (i64.eq (local.get $ba) (local.get $bb))
165
+ (local $ssoA i32) (local $ssoB i32)
166
+ (if (i64.eq (local.get $a) (local.get $b))
148
167
  (then (return (i32.const 1))))
149
- (local.set $ta (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ba) (i64.const 47)) (i64.const 0xF))))
150
- (local.set $tb (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bb) (i64.const 47)) (i64.const 0xF))))
151
- (local.set $offA (i32.wrap_i64 (i64.and (local.get $ba) (i64.const 0xFFFFFFFF))))
152
- (local.set $offB (i32.wrap_i64 (i64.and (local.get $bb) (i64.const 0xFFFFFFFF))))
153
- ;; Both SSO with !bit-eq ⇒ content differs (high 32 bits hold type+len; both equal here).
154
- (if (i32.and (i32.eq (local.get $ta) (i32.const ${PTR.SSO})) (i32.eq (local.get $tb) (i32.const ${PTR.SSO})))
168
+ (local.set $ta (i32.wrap_i64 (i64.and (i64.shr_u (local.get $a) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
169
+ (local.set $tb (i32.wrap_i64 (i64.and (i64.shr_u (local.get $b) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
170
+ (local.set $offA (i32.wrap_i64 (i64.and (local.get $a) (i64.const ${LAYOUT.OFFSET_MASK}))))
171
+ (local.set $offB (i32.wrap_i64 (i64.and (local.get $b) (i64.const ${LAYOUT.OFFSET_MASK}))))
172
+ (local.set $ssoA (i32.and
173
+ (i32.wrap_i64 (i64.shr_u (local.get $a) (i64.const ${LAYOUT.AUX_SHIFT})))
174
+ (i32.const ${LAYOUT.SSO_BIT})))
175
+ (local.set $ssoB (i32.and
176
+ (i32.wrap_i64 (i64.shr_u (local.get $b) (i64.const ${LAYOUT.AUX_SHIFT})))
177
+ (i32.const ${LAYOUT.SSO_BIT})))
178
+ ;; Both SSO with !bit-eq ⇒ content differs (high 32 bits hold tag+aux; both equal here).
179
+ (if (i32.and (local.get $ssoA) (local.get $ssoB))
155
180
  (then (return (i32.const 0))))
156
- ;; Both STRING fast path: inline len from header. Chunk by 4 bytes via unaligned i32.load
157
- ;; (wasm guarantees unaligned-OK), then byte-tail. Most string comparisons fail early on
158
- ;; the first 4-byte word, so this collapses the per-byte branch overhead into a single
159
- ;; 32-bit equality.
160
- (if (i32.and (i32.eq (local.get $ta) (i32.const ${PTR.STRING})) (i32.eq (local.get $tb) (i32.const ${PTR.STRING})))
181
+ ;; Both heap STRING fast path: inline len from header. Chunk by 4 bytes via unaligned
182
+ ;; i32.load (wasm guarantees unaligned-OK), then byte-tail. Most string comparisons fail
183
+ ;; early on the first 4-byte word, so this collapses the per-byte branch overhead into a
184
+ ;; single 32-bit equality.
185
+ (if (i32.and
186
+ (i32.and (i32.eq (local.get $ta) (i32.const ${PTR.STRING})) (i32.eqz (local.get $ssoA)))
187
+ (i32.and (i32.eq (local.get $tb) (i32.const ${PTR.STRING})) (i32.eqz (local.get $ssoB))))
161
188
  (then
162
189
  (if (i32.or (i32.lt_u (local.get $offA) (i32.const 4)) (i32.lt_u (local.get $offB) (i32.const 4)))
163
190
  (then (return (i32.const 0))))
@@ -183,14 +210,18 @@ export default (ctx) => {
183
210
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
184
211
  (br $lh)))
185
212
  (return (i32.const 1))))
186
- ;; Mixed (SSO×STRING) or anything else: compute len per side then per-byte via __char_at.
187
- (if (i32.eq (local.get $ta) (i32.const ${PTR.SSO}))
188
- (then (local.set $len (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ba) (i64.const 32)) (i64.const 0x7FFF)))))
213
+ ;; Mixed (SSO×heap) or anything else: compute len per side then per-byte via __char_at.
214
+ (if (local.get $ssoA)
215
+ (then (local.set $len (i32.and
216
+ (i32.wrap_i64 (i64.shr_u (local.get $a) (i64.const ${LAYOUT.AUX_SHIFT})))
217
+ (i32.const ${LAYOUT.SSO_BIT - 1}))))
189
218
  (else
190
219
  (if (i32.and (i32.eq (local.get $ta) (i32.const ${PTR.STRING})) (i32.ge_u (local.get $offA) (i32.const 4)))
191
220
  (then (local.set $len (i32.load (i32.sub (local.get $offA) (i32.const 4))))))))
192
- (if (i32.eq (local.get $tb) (i32.const ${PTR.SSO}))
193
- (then (local.set $lenB (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bb) (i64.const 32)) (i64.const 0x7FFF)))))
221
+ (if (local.get $ssoB)
222
+ (then (local.set $lenB (i32.and
223
+ (i32.wrap_i64 (i64.shr_u (local.get $b) (i64.const ${LAYOUT.AUX_SHIFT})))
224
+ (i32.const ${LAYOUT.SSO_BIT - 1}))))
194
225
  (else
195
226
  (if (i32.and (i32.eq (local.get $tb) (i32.const ${PTR.STRING})) (i32.ge_u (local.get $offB) (i32.const 4)))
196
227
  (then (local.set $lenB (i32.load (i32.sub (local.get $offB) (i32.const 4))))))))
@@ -205,28 +236,62 @@ export default (ctx) => {
205
236
  (br $lm)))
206
237
  (i32.const 1))`
207
238
 
208
- // === WAT: unified byte length (SSO aux, heap header) ===
239
+ // Three-way byte-wise compare: -1 if a < b, 0 if equal, +1 if a > b. Returns
240
+ // i32 so callers can `i32.lt_s 0`, `i32.gt_s 0`, etc. without coercion.
241
+ // Comparison is unsigned (i32.load8_u via __char_at) — matches JS spec for
242
+ // ASCII; for non-ASCII it's a UTF-8 byte order, which collates the same as
243
+ // codepoint order for code points < 0x80 and well-formed strings. NOT locale-
244
+ // aware: this is the byte-wise variant suitable for sort-stability use cases,
245
+ // not human-language collation.
246
+ ctx.core.stdlib['__str_cmp'] = `(func $__str_cmp (param $a i64) (param $b i64) (result i32)
247
+ (local $lenA i32) (local $lenB i32) (local $minLen i32) (local $i i32)
248
+ (local $ca i32) (local $cb i32)
249
+ ;; Bit-equal pointers (including same SSO inline form) ⇒ identical strings.
250
+ (if (i64.eq (local.get $a) (local.get $b))
251
+ (then (return (i32.const 0))))
252
+ (local.set $lenA (call $__str_byteLen (local.get $a)))
253
+ (local.set $lenB (call $__str_byteLen (local.get $b)))
254
+ (local.set $minLen (select (local.get $lenA) (local.get $lenB)
255
+ (i32.lt_s (local.get $lenA) (local.get $lenB))))
256
+ (block $done (loop $next
257
+ (br_if $done (i32.ge_s (local.get $i) (local.get $minLen)))
258
+ (local.set $ca (call $__char_at (local.get $a) (local.get $i)))
259
+ (local.set $cb (call $__char_at (local.get $b) (local.get $i)))
260
+ (if (i32.lt_u (local.get $ca) (local.get $cb)) (then (return (i32.const -1))))
261
+ (if (i32.gt_u (local.get $ca) (local.get $cb)) (then (return (i32.const 1))))
262
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
263
+ (br $next)))
264
+ ;; Common prefix matches — shorter string sorts first.
265
+ (if (i32.lt_s (local.get $lenA) (local.get $lenB)) (then (return (i32.const -1))))
266
+ (if (i32.gt_s (local.get $lenA) (local.get $lenB)) (then (return (i32.const 1))))
267
+ (i32.const 0))`
268
+
269
+ // === WAT: unified byte length (SSO → aux low bits, heap → header) ===
209
270
 
210
- ctx.core.stdlib['__str_byteLen'] = `(func $__str_byteLen (param $ptr f64) (result i32)
211
- (local $bits i64) (local $t i32) (local $off i32)
212
- (local.set $bits (i64.reinterpret_f64 (local.get $ptr)))
213
- (local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF))))
214
- (if (result i32) (i32.eq (local.get $t) (i32.const ${PTR.SSO}))
215
- (then (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 32)) (i64.const 0x7FFF))))
216
- (else
217
- (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
218
- (if (result i32)
219
- (i32.and (i32.eq (local.get $t) (i32.const ${PTR.STRING})) (i32.ge_u (local.get $off) (i32.const 4)))
220
- (then (i32.load (i32.sub (local.get $off) (i32.const 4))))
221
- (else (i32.const 0))))))`
271
+ ctx.core.stdlib['__str_byteLen'] = `(func $__str_byteLen (param $ptr i64) (result i32)
272
+ (local $t i32) (local $off i32) (local $aux i32)
273
+ (local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
274
+ (if (result i32) (i32.eq (local.get $t) (i32.const ${PTR.STRING}))
275
+ (then
276
+ (local.set $aux (i32.and
277
+ (i32.wrap_i64 (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.AUX_SHIFT})))
278
+ (i32.const ${LAYOUT.AUX_MASK})))
279
+ (if (result i32) (i32.and (local.get $aux) (i32.const ${LAYOUT.SSO_BIT}))
280
+ (then (i32.and (local.get $aux) (i32.const ${LAYOUT.SSO_BIT - 1})))
281
+ (else
282
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
283
+ (if (result i32) (i32.ge_u (local.get $off) (i32.const 4))
284
+ (then (i32.load (i32.sub (local.get $off) (i32.const 4))))
285
+ (else (i32.const 0))))))
286
+ (else (i32.const 0))))`
222
287
 
223
288
  // === WAT: string methods ===
224
289
 
225
290
  // SSO source uses an unrolled byte-extract loop (len ≤ 4); heap source uses memory.copy
226
291
  // (single bulk op instead of nlen × __char_at).
227
- ctx.core.stdlib['__str_slice'] = `(func $__str_slice (param $ptr f64) (param $start i32) (param $end i32) (result f64)
292
+ ctx.core.stdlib['__str_slice'] = `(func $__str_slice (param $ptr i64) (param $start i32) (param $end i32) (result f64)
228
293
  (local $len i32) (local $nlen i32) (local $off i32) (local $i i32)
229
- (local $bits i64) (local $srcOff i32) (local $isSso i32)
294
+ (local $srcOff i32) (local $isSso i32)
230
295
  (local.set $len (call $__str_byteLen (local.get $ptr)))
231
296
  (if (i32.lt_s (local.get $start) (i32.const 0))
232
297
  (then (local.set $start (i32.add (local.get $len) (local.get $start)))))
@@ -241,16 +306,15 @@ export default (ctx) => {
241
306
  (if (i32.gt_s (local.get $end) (local.get $len))
242
307
  (then (local.set $end (local.get $len))))
243
308
  (if (i32.ge_s (local.get $start) (local.get $end))
244
- (then (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 0) (i32.const 0)))))
309
+ (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
245
310
  (local.set $nlen (i32.sub (local.get $end) (local.get $start)))
246
311
  (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $nlen))))
247
312
  (i32.store (local.get $off) (local.get $nlen))
248
313
  (local.set $off (i32.add (local.get $off) (i32.const 4)))
249
- (local.set $bits (i64.reinterpret_f64 (local.get $ptr)))
250
- (local.set $srcOff (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
251
- (local.set $isSso (i32.eq
252
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF)))
253
- (i32.const ${PTR.SSO})))
314
+ (local.set $srcOff (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
315
+ (local.set $isSso (i32.wrap_i64 (i64.shr_u
316
+ (i64.and (local.get $ptr) (i64.const ${SSO_BIT_I64}))
317
+ (i64.const ${LAYOUT.AUX_SHIFT}))))
254
318
  (if (local.get $isSso)
255
319
  (then
256
320
  (block $done (loop $loop
@@ -265,7 +329,7 @@ export default (ctx) => {
265
329
  (memory.copy (local.get $off) (i32.add (local.get $srcOff) (local.get $start)) (local.get $nlen))))
266
330
  (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
267
331
 
268
- ctx.core.stdlib['__str_substring'] = `(func $__str_substring (param $ptr f64) (param $start i32) (param $end i32) (result f64)
332
+ ctx.core.stdlib['__str_substring'] = `(func $__str_substring (param $ptr i64) (param $start i32) (param $end i32) (result f64)
269
333
  (local $len i32) (local $tmp i32)
270
334
  (local.set $len (call $__str_byteLen (local.get $ptr)))
271
335
  (if (i32.lt_s (local.get $start) (i32.const 0))
@@ -285,24 +349,22 @@ export default (ctx) => {
285
349
 
286
350
  // Hoist SSO/heap dispatch for hay and ndl out of the inner byte loop. Inner
287
351
  // loop becomes (load8_u OR sso byte-extract) per side — no per-byte calls.
288
- ctx.core.stdlib['__str_indexof'] = `(func $__str_indexof (param $hay f64) (param $ndl f64) (param $from i32) (result i32)
352
+ ctx.core.stdlib['__str_indexof'] = `(func $__str_indexof (param $hay i64) (param $ndl i64) (param $from i32) (result i32)
289
353
  (local $hlen i32) (local $nlen i32) (local $i i32) (local $j i32) (local $match i32)
290
- (local $hbits i64) (local $nbits i64) (local $hoff i32) (local $noff i32)
354
+ (local $hoff i32) (local $noff i32)
291
355
  (local $hsso i32) (local $nsso i32) (local $hb i32) (local $nb i32) (local $k i32)
292
356
  (local.set $hlen (call $__str_byteLen (local.get $hay)))
293
357
  (local.set $nlen (call $__str_byteLen (local.get $ndl)))
294
358
  (if (i32.eqz (local.get $nlen)) (then (return (local.get $from))))
295
359
  (if (i32.gt_s (local.get $nlen) (local.get $hlen)) (then (return (i32.const -1))))
296
- (local.set $hbits (i64.reinterpret_f64 (local.get $hay)))
297
- (local.set $nbits (i64.reinterpret_f64 (local.get $ndl)))
298
- (local.set $hoff (i32.wrap_i64 (i64.and (local.get $hbits) (i64.const 0xFFFFFFFF))))
299
- (local.set $noff (i32.wrap_i64 (i64.and (local.get $nbits) (i64.const 0xFFFFFFFF))))
300
- (local.set $hsso (i32.eq
301
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $hbits) (i64.const 47)) (i64.const 0xF)))
302
- (i32.const ${PTR.SSO})))
303
- (local.set $nsso (i32.eq
304
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $nbits) (i64.const 47)) (i64.const 0xF)))
305
- (i32.const ${PTR.SSO})))
360
+ (local.set $hoff (i32.wrap_i64 (i64.and (local.get $hay) (i64.const ${LAYOUT.OFFSET_MASK}))))
361
+ (local.set $noff (i32.wrap_i64 (i64.and (local.get $ndl) (i64.const ${LAYOUT.OFFSET_MASK}))))
362
+ (local.set $hsso (i32.and
363
+ (i32.wrap_i64 (i64.shr_u (local.get $hay) (i64.const ${LAYOUT.AUX_SHIFT})))
364
+ (i32.const ${LAYOUT.SSO_BIT})))
365
+ (local.set $nsso (i32.and
366
+ (i32.wrap_i64 (i64.shr_u (local.get $ndl) (i64.const ${LAYOUT.AUX_SHIFT})))
367
+ (i32.const ${LAYOUT.SSO_BIT})))
306
368
  (local.set $i (if (result i32) (i32.gt_s (local.get $from) (i32.const 0)) (then (local.get $from)) (else (i32.const 0))))
307
369
  (block $done (loop $outer
308
370
  (br_if $done (i32.gt_s (local.get $i) (i32.sub (local.get $hlen) (local.get $nlen))))
@@ -327,22 +389,20 @@ export default (ctx) => {
327
389
  (i32.const -1))`
328
390
 
329
391
  // SSO/heap dispatch hoisted; inner loop is two inlined byte-fetches and a compare.
330
- ctx.core.stdlib['__str_startswith'] = `(func $__str_startswith (param $str f64) (param $pfx f64) (result i32)
392
+ ctx.core.stdlib['__str_startswith'] = `(func $__str_startswith (param $str i64) (param $pfx i64) (result i32)
331
393
  (local $plen i32) (local $i i32)
332
- (local $sbits i64) (local $pbits i64) (local $soff i32) (local $poff i32) (local $ssso i32) (local $psso i32)
394
+ (local $soff i32) (local $poff i32) (local $ssso i32) (local $psso i32)
333
395
  (local.set $plen (call $__str_byteLen (local.get $pfx)))
334
396
  (if (i32.gt_s (local.get $plen) (call $__str_byteLen (local.get $str)))
335
397
  (then (return (i32.const 0))))
336
- (local.set $sbits (i64.reinterpret_f64 (local.get $str)))
337
- (local.set $pbits (i64.reinterpret_f64 (local.get $pfx)))
338
- (local.set $soff (i32.wrap_i64 (i64.and (local.get $sbits) (i64.const 0xFFFFFFFF))))
339
- (local.set $poff (i32.wrap_i64 (i64.and (local.get $pbits) (i64.const 0xFFFFFFFF))))
340
- (local.set $ssso (i32.eq
341
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $sbits) (i64.const 47)) (i64.const 0xF)))
342
- (i32.const ${PTR.SSO})))
343
- (local.set $psso (i32.eq
344
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $pbits) (i64.const 47)) (i64.const 0xF)))
345
- (i32.const ${PTR.SSO})))
398
+ (local.set $soff (i32.wrap_i64 (i64.and (local.get $str) (i64.const ${LAYOUT.OFFSET_MASK}))))
399
+ (local.set $poff (i32.wrap_i64 (i64.and (local.get $pfx) (i64.const ${LAYOUT.OFFSET_MASK}))))
400
+ (local.set $ssso (i32.and
401
+ (i32.wrap_i64 (i64.shr_u (local.get $str) (i64.const ${LAYOUT.AUX_SHIFT})))
402
+ (i32.const ${LAYOUT.SSO_BIT})))
403
+ (local.set $psso (i32.and
404
+ (i32.wrap_i64 (i64.shr_u (local.get $pfx) (i64.const ${LAYOUT.AUX_SHIFT})))
405
+ (i32.const ${LAYOUT.SSO_BIT})))
346
406
  (block $done (loop $loop
347
407
  (br_if $done (i32.ge_s (local.get $i) (local.get $plen)))
348
408
  (if (i32.ne
@@ -357,24 +417,22 @@ export default (ctx) => {
357
417
  (br $loop)))
358
418
  (i32.const 1))`
359
419
 
360
- ctx.core.stdlib['__str_endswith'] = `(func $__str_endswith (param $str f64) (param $sfx f64) (result i32)
420
+ ctx.core.stdlib['__str_endswith'] = `(func $__str_endswith (param $str i64) (param $sfx i64) (result i32)
361
421
  (local $slen i32) (local $flen i32) (local $off i32) (local $i i32) (local $k i32)
362
- (local $sbits i64) (local $fbits i64) (local $soff i32) (local $foff i32) (local $ssso i32) (local $fsso i32)
422
+ (local $soff i32) (local $foff i32) (local $ssso i32) (local $fsso i32)
363
423
  (local.set $slen (call $__str_byteLen (local.get $str)))
364
424
  (local.set $flen (call $__str_byteLen (local.get $sfx)))
365
425
  (if (i32.gt_s (local.get $flen) (local.get $slen))
366
426
  (then (return (i32.const 0))))
367
427
  (local.set $off (i32.sub (local.get $slen) (local.get $flen)))
368
- (local.set $sbits (i64.reinterpret_f64 (local.get $str)))
369
- (local.set $fbits (i64.reinterpret_f64 (local.get $sfx)))
370
- (local.set $soff (i32.wrap_i64 (i64.and (local.get $sbits) (i64.const 0xFFFFFFFF))))
371
- (local.set $foff (i32.wrap_i64 (i64.and (local.get $fbits) (i64.const 0xFFFFFFFF))))
372
- (local.set $ssso (i32.eq
373
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $sbits) (i64.const 47)) (i64.const 0xF)))
374
- (i32.const ${PTR.SSO})))
375
- (local.set $fsso (i32.eq
376
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $fbits) (i64.const 47)) (i64.const 0xF)))
377
- (i32.const ${PTR.SSO})))
428
+ (local.set $soff (i32.wrap_i64 (i64.and (local.get $str) (i64.const ${LAYOUT.OFFSET_MASK}))))
429
+ (local.set $foff (i32.wrap_i64 (i64.and (local.get $sfx) (i64.const ${LAYOUT.OFFSET_MASK}))))
430
+ (local.set $ssso (i32.and
431
+ (i32.wrap_i64 (i64.shr_u (local.get $str) (i64.const ${LAYOUT.AUX_SHIFT})))
432
+ (i32.const ${LAYOUT.SSO_BIT})))
433
+ (local.set $fsso (i32.and
434
+ (i32.wrap_i64 (i64.shr_u (local.get $sfx) (i64.const ${LAYOUT.AUX_SHIFT})))
435
+ (i32.const ${LAYOUT.SSO_BIT})))
378
436
  (block $done (loop $loop
379
437
  (br_if $done (i32.ge_s (local.get $i) (local.get $flen)))
380
438
  (local.set $k (i32.add (local.get $off) (local.get $i)))
@@ -391,20 +449,17 @@ export default (ctx) => {
391
449
  (i32.const 1))`
392
450
 
393
451
  // Source SSO/heap dispatch hoisted out of the byte loop (was a per-byte __char_at).
394
- ctx.core.stdlib['__str_case'] = `(func $__str_case (param $ptr f64) (param $lo i32) (param $hi i32) (param $delta i32) (result f64)
452
+ ctx.core.stdlib['__str_case'] = `(func $__str_case (param $ptr i64) (param $lo i32) (param $hi i32) (param $delta i32) (result f64)
395
453
  (local $len i32) (local $off i32) (local $i i32) (local $c i32)
396
- (local $bits i64) (local $srcOff i32)
454
+ (local $srcOff i32)
397
455
  (local.set $len (call $__str_byteLen (local.get $ptr)))
398
456
  (if (i32.eqz (local.get $len))
399
- (then (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 0) (i32.const 0)))))
457
+ (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
400
458
  (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $len))))
401
459
  (i32.store (local.get $off) (local.get $len))
402
460
  (local.set $off (i32.add (local.get $off) (i32.const 4)))
403
- (local.set $bits (i64.reinterpret_f64 (local.get $ptr)))
404
- (local.set $srcOff (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
405
- (if (i32.eq
406
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF)))
407
- (i32.const ${PTR.SSO}))
461
+ (local.set $srcOff (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
462
+ (if (i64.ne (i64.and (local.get $ptr) (i64.const ${SSO_BIT_I64})) (i64.const 0))
408
463
  (then
409
464
  (block $dsso (loop $lsso
410
465
  (br_if $dsso (i32.ge_s (local.get $i) (local.get $len)))
@@ -427,7 +482,7 @@ export default (ctx) => {
427
482
  (br $lh)))))
428
483
  (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
429
484
 
430
- ctx.core.stdlib['__str_trim'] = `(func $__str_trim (param $ptr f64) (result f64)
485
+ ctx.core.stdlib['__str_trim'] = `(func $__str_trim (param $ptr i64) (result f64)
431
486
  (local $len i32) (local $start i32) (local $end i32)
432
487
  (local.set $len (call $__str_byteLen (local.get $ptr)))
433
488
  (local.set $start (i32.const 0))
@@ -444,7 +499,7 @@ export default (ctx) => {
444
499
  (br $l2)))
445
500
  (call $__str_slice (local.get $ptr) (local.get $start) (local.get $end)))`
446
501
 
447
- ctx.core.stdlib['__str_trimStart'] = `(func $__str_trimStart (param $ptr f64) (result f64)
502
+ ctx.core.stdlib['__str_trimStart'] = `(func $__str_trimStart (param $ptr i64) (result f64)
448
503
  (local $len i32) (local $start i32)
449
504
  (local.set $len (call $__str_byteLen (local.get $ptr)))
450
505
  (local.set $start (i32.const 0))
@@ -455,7 +510,7 @@ export default (ctx) => {
455
510
  (br $loop)))
456
511
  (call $__str_slice (local.get $ptr) (local.get $start) (local.get $len)))`
457
512
 
458
- ctx.core.stdlib['__str_trimEnd'] = `(func $__str_trimEnd (param $ptr f64) (result f64)
513
+ ctx.core.stdlib['__str_trimEnd'] = `(func $__str_trimEnd (param $ptr i64) (result f64)
459
514
  (local $len i32) (local $end i32)
460
515
  (local.set $len (call $__str_byteLen (local.get $ptr)))
461
516
  (local.set $end (local.get $len))
@@ -468,11 +523,11 @@ export default (ctx) => {
468
523
 
469
524
  // Materialize source bytes once via __str_copy (handles SSO/heap), then memory.copy
470
525
  // each subsequent repetition (single bulk op vs len byte stores per copy).
471
- ctx.core.stdlib['__str_repeat'] = `(func $__str_repeat (param $ptr f64) (param $n i32) (result f64)
526
+ ctx.core.stdlib['__str_repeat'] = `(func $__str_repeat (param $ptr i64) (param $n i32) (result f64)
472
527
  (local $len i32) (local $total i32) (local $off i32) (local $i i32)
473
528
  (local.set $len (call $__str_byteLen (local.get $ptr)))
474
529
  (if (i32.or (i32.eqz (local.get $n)) (i32.eqz (local.get $len)))
475
- (then (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 0) (i32.const 0)))))
530
+ (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
476
531
  (local.set $total (i32.mul (local.get $len) (local.get $n)))
477
532
  (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $total))))
478
533
  (i32.store (local.get $off) (local.get $total))
@@ -491,39 +546,35 @@ export default (ctx) => {
491
546
 
492
547
  // Coerce value to string: numbers → __ftoa, nullish → static strings,
493
548
  // plain NaN → "NaN", arrays → join(","), other string-like pointers pass through.
494
- ctx.core.stdlib['__to_str'] = `(func $__to_str (param $val f64) (result f64)
495
- (local $type i32)
496
- (local $bits i64)
549
+ ctx.core.stdlib['__to_str'] = `(func $__to_str (param $val i64) (result i64)
550
+ (local $type i32) (local $f f64)
551
+ (local.set $f (f64.reinterpret_i64 (local.get $val)))
497
552
  ;; Not NaN → number, convert
498
- (if (f64.eq (local.get $val) (local.get $val))
499
- (then (return (call $__ftoa (local.get $val) (i32.const 0) (i32.const 0)))))
500
- (local.set $bits (i64.reinterpret_f64 (local.get $val)))
501
- (if (i64.eq (local.get $bits) (i64.const ${NULL_NAN}))
502
- (then (return (call $__static_str (i32.const 5)))))
503
- (if (i64.eq (local.get $bits) (i64.const ${UNDEF_NAN}))
504
- (then (return (call $__static_str (i32.const 6)))))
553
+ (if (f64.eq (local.get $f) (local.get $f))
554
+ (then (return (i64.reinterpret_f64 (call $__ftoa (local.get $f) (i32.const 0) (i32.const 0))))))
555
+ (if (i64.eq (local.get $val) (i64.const ${NULL_NAN}))
556
+ (then (return (i64.reinterpret_f64 (call $__static_str (i32.const 5))))))
557
+ (if (i64.eq (local.get $val) (i64.const ${UNDEF_NAN}))
558
+ (then (return (i64.reinterpret_f64 (call $__static_str (i32.const 6))))))
505
559
  (local.set $type (call $__ptr_type (local.get $val)))
506
560
  ;; Plain NaN (type=0) → "NaN" string
507
561
  (if (i32.eqz (local.get $type))
508
- (then (return (call $__static_str (i32.const 0)))))
562
+ (then (return (i64.reinterpret_f64 (call $__static_str (i32.const 0))))))
509
563
  ;; Array (type=1) → join(",") like JS Array.toString()
510
564
  (if (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
511
- (then (return (call $__str_join (local.get $val)
512
- (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 1) (i32.const 44))))))
565
+ (then (return (i64.reinterpret_f64 (call $__str_join (local.get $val)
566
+ (i64.reinterpret_f64 (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT | 1}) (i32.const 44))))))))
513
567
  (local.get $val))`
514
568
 
515
569
  // Copy bytes of a string (SSO or heap) into memory at dst. Uses memory.copy for
516
- // heap strings (single native op); unpacks SSO aux-packed bytes inline.
517
- ctx.core.stdlib['__str_copy'] = `(func $__str_copy (param $src f64) (param $dst i32) (param $len i32)
518
- (local $bits i64) (local $w i32)
519
- (local.set $bits (i64.reinterpret_f64 (local.get $src)))
520
- (if (i32.eq
521
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF)))
522
- (i32.const ${PTR.SSO}))
570
+ // heap strings (single native op); unpacks SSO offset-packed bytes inline.
571
+ ctx.core.stdlib['__str_copy'] = `(func $__str_copy (param $src i64) (param $dst i32) (param $len i32)
572
+ (local $w i32)
573
+ (if (i64.ne (i64.and (local.get $src) (i64.const ${SSO_BIT_I64})) (i64.const 0))
523
574
  (then
524
575
  ;; SSO: up to 4 chars packed in low 32 bits (LE byte order). Unroll: write 1/2/3/4 bytes
525
576
  ;; depending on len. (len > 4 is rare/disallowed in practice — fallback handles up to 4.)
526
- (local.set $w (i32.wrap_i64 (local.get $bits)))
577
+ (local.set $w (i32.wrap_i64 (local.get $src)))
527
578
  (if (i32.ge_u (local.get $len) (i32.const 4))
528
579
  (then (i32.store (local.get $dst) (local.get $w)))
529
580
  (else
@@ -536,11 +587,107 @@ export default (ctx) => {
536
587
  (else
537
588
  ;; Heap STRING: memory.copy directly from string data
538
589
  (memory.copy (local.get $dst)
539
- (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF)))
590
+ (i32.wrap_i64 (i64.and (local.get $src) (i64.const ${LAYOUT.OFFSET_MASK})))
540
591
  (local.get $len)))))`
541
592
 
542
- ctx.core.stdlib['__str_concat'] = `(func $__str_concat (param $a f64) (param $b f64) (result f64)
593
+ // Bump-extend fast path: when `a` is a heap STRING sitting at the top of the
594
+ // bump allocator, extend its allocation in place instead of copying. Mutates
595
+ // memory[a.off-4] to the new length and bumps __heap. This makes the canonical
596
+ // `buf += char` build pattern O(N) instead of O(N²) — closing the asymptotic
597
+ // gap with V8's cons-strings. Tradeoff: aliased refs to `a` see the larger
598
+ // length too, so this departs from strict JS string immutability for the rare
599
+ // `let b = a; a += x` aliasing case. The fast path can't trigger when other
600
+ // allocations have happened since `a` was created (it's no longer at heap top).
601
+ // Only emitted for own-memory mode; shared memory falls back to slow path.
602
+ const concatFast = !ctx.memory.shared ? `
603
+ (local.set $ta (i32.wrap_i64 (i64.and (i64.shr_u (local.get $a) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
604
+ (local.set $aoff (i32.wrap_i64 (i64.and (local.get $a) (i64.const ${LAYOUT.OFFSET_MASK}))))
605
+ ;; Bump-extend requires heap STRING (not SSO — its offset holds packed bytes).
606
+ (if (i32.and
607
+ (i32.and
608
+ (i32.eq (local.get $ta) (i32.const ${PTR.STRING}))
609
+ (i64.eqz (i64.and (local.get $a) (i64.const ${SSO_BIT_I64}))))
610
+ (i32.eq
611
+ (i32.and (i32.add (i32.add (local.get $aoff) (local.get $alen)) (i32.const 7)) (i32.const -8))
612
+ (global.get $__heap)))
613
+ (then
614
+ (local.set $newHeap
615
+ (i32.and (i32.add (i32.add (local.get $aoff) (local.get $total)) (i32.const 7)) (i32.const -8)))
616
+ (if (i32.gt_u (local.get $newHeap) (i32.mul (memory.size) (i32.const 65536)))
617
+ (then (if (i32.eq (memory.grow
618
+ (i32.shr_u (i32.add (i32.sub (local.get $newHeap) (i32.mul (memory.size) (i32.const 65536))) (i32.const 65535)) (i32.const 16)))
619
+ (i32.const -1)) (then (unreachable)))))
620
+ (call $__str_copy (local.get $b)
621
+ (i32.add (local.get $aoff) (local.get $alen))
622
+ (local.get $blen))
623
+ (i32.store (i32.sub (local.get $aoff) (i32.const 4)) (local.get $total))
624
+ (global.set $__heap (local.get $newHeap))
625
+ (return (f64.reinterpret_i64 (local.get $a)))))` : ''
626
+
627
+ // Fused single-byte append: `buf += str[i]` lowers to this when both sides are
628
+ // VAL.STRING and the rhs is a string-index. Skips __str_idx's 1-char SSO
629
+ // construction and __str_concat's type-dispatch — byte goes directly from
630
+ // __char_at to memory. Bump-extends in place when `a` is at heap top.
631
+ ctx.core.stdlib['__str_append_byte'] = `(func $__str_append_byte (param $a i64) (param $byte i32) (result f64)
632
+ (local $ta i32) (local $aoff i32) (local $alen i32)
633
+ (local $newHeap i32) (local $off i32) (local $total i32)
634
+ (local.set $ta (i32.wrap_i64 (i64.and (i64.shr_u (local.get $a) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
635
+ (local.set $aoff (i32.wrap_i64 (i64.and (local.get $a) (i64.const ${LAYOUT.OFFSET_MASK}))))
636
+ ;; Heap STRING at heap top: bump-extend by 1 byte (own-memory mode only).
637
+ ;; Gate on STRING tag AND !SSO_BIT — for SSO, $aoff holds packed bytes (not a heap addr).
638
+ ${!ctx.memory.shared ? `
639
+ (if (i32.and
640
+ (i32.eq (local.get $ta) (i32.const ${PTR.STRING}))
641
+ (i64.eqz (i64.and (local.get $a) (i64.const ${SSO_BIT_I64}))))
642
+ (then
643
+ (local.set $alen (i32.load (i32.sub (local.get $aoff) (i32.const 4))))
644
+ (if (i32.eq
645
+ (i32.and (i32.add (i32.add (local.get $aoff) (local.get $alen)) (i32.const 7)) (i32.const -8))
646
+ (global.get $__heap))
647
+ (then
648
+ (local.set $newHeap
649
+ (i32.and (i32.add (i32.add (local.get $aoff) (local.get $alen)) (i32.const 8)) (i32.const -8)))
650
+ (if (i32.gt_u (local.get $newHeap) (i32.mul (memory.size) (i32.const 65536)))
651
+ (then (if (i32.eq (memory.grow
652
+ (i32.shr_u (i32.add (i32.sub (local.get $newHeap) (i32.mul (memory.size) (i32.const 65536))) (i32.const 65535)) (i32.const 16)))
653
+ (i32.const -1)) (then (unreachable)))))
654
+ (i32.store8 (i32.add (local.get $aoff) (local.get $alen)) (local.get $byte))
655
+ (i32.store (i32.sub (local.get $aoff) (i32.const 4)) (i32.add (local.get $alen) (i32.const 1)))
656
+ (global.set $__heap (local.get $newHeap))
657
+ (return (f64.reinterpret_i64 (local.get $a)))))))` : ''}
658
+ ;; SSO (STRING with SSO bit) with len < 4 and ASCII byte: pack into SSO without allocation
659
+ (if (i32.and
660
+ (i32.eq (local.get $ta) (i32.const ${PTR.STRING}))
661
+ (i32.and
662
+ (i32.wrap_i64 (i64.shr_u (local.get $a) (i64.const ${LAYOUT.AUX_SHIFT})))
663
+ (i32.const ${LAYOUT.SSO_BIT})))
664
+ (then
665
+ (local.set $alen (i32.and
666
+ (i32.wrap_i64 (i64.shr_u (local.get $a) (i64.const ${LAYOUT.AUX_SHIFT})))
667
+ (i32.const ${LAYOUT.SSO_BIT - 1})))
668
+ (if (i32.and
669
+ (i32.lt_u (local.get $alen) (i32.const 4))
670
+ (i32.lt_u (local.get $byte) (i32.const 0x80)))
671
+ (then
672
+ (return (call $__mkptr
673
+ (i32.const ${PTR.STRING})
674
+ (i32.or (i32.const ${LAYOUT.SSO_BIT}) (i32.add (local.get $alen) (i32.const 1)))
675
+ (i32.or
676
+ (local.get $aoff)
677
+ (i32.shl (local.get $byte) (i32.shl (local.get $alen) (i32.const 3))))))))))
678
+ ;; Slow path: allocate new heap STRING with original bytes + 1 new byte
679
+ (local.set $alen (call $__str_byteLen (local.get $a)))
680
+ (local.set $total (i32.add (local.get $alen) (i32.const 1)))
681
+ (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $total))))
682
+ (i32.store (local.get $off) (local.get $total))
683
+ (local.set $off (i32.add (local.get $off) (i32.const 4)))
684
+ (call $__str_copy (local.get $a) (local.get $off) (local.get $alen))
685
+ (i32.store8 (i32.add (local.get $off) (local.get $alen)) (local.get $byte))
686
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
687
+
688
+ ctx.core.stdlib['__str_concat'] = `(func $__str_concat (param $a i64) (param $b i64) (result f64)
543
689
  (local $alen i32) (local $blen i32) (local $total i32) (local $off i32)
690
+ (local $ta i32) (local $aoff i32) (local $newHeap i32)
544
691
  ;; Coerce operands to strings if needed
545
692
  (local.set $a (call $__to_str (local.get $a)))
546
693
  (local.set $b (call $__to_str (local.get $b)))
@@ -548,7 +695,8 @@ export default (ctx) => {
548
695
  (local.set $blen (call $__str_byteLen (local.get $b)))
549
696
  (local.set $total (i32.add (local.get $alen) (local.get $blen)))
550
697
  (if (i32.eqz (local.get $total))
551
- (then (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 0) (i32.const 0)))))
698
+ (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
699
+ ${concatFast}
552
700
  (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $total))))
553
701
  (i32.store (local.get $off) (local.get $total))
554
702
  (local.set $off (i32.add (local.get $off) (i32.const 4)))
@@ -556,13 +704,15 @@ export default (ctx) => {
556
704
  (call $__str_copy (local.get $b) (i32.add (local.get $off) (local.get $alen)) (local.get $blen))
557
705
  (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
558
706
 
559
- ctx.core.stdlib['__str_concat_raw'] = `(func $__str_concat_raw (param $a f64) (param $b f64) (result f64)
707
+ ctx.core.stdlib['__str_concat_raw'] = `(func $__str_concat_raw (param $a i64) (param $b i64) (result f64)
560
708
  (local $alen i32) (local $blen i32) (local $total i32) (local $off i32)
709
+ (local $ta i32) (local $aoff i32) (local $newHeap i32)
561
710
  (local.set $alen (call $__str_byteLen (local.get $a)))
562
711
  (local.set $blen (call $__str_byteLen (local.get $b)))
563
712
  (local.set $total (i32.add (local.get $alen) (local.get $blen)))
564
713
  (if (i32.eqz (local.get $total))
565
- (then (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 0) (i32.const 0)))))
714
+ (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
715
+ ${concatFast}
566
716
  (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $total))))
567
717
  (i32.store (local.get $off) (local.get $total))
568
718
  (local.set $off (i32.add (local.get $off) (i32.const 4)))
@@ -570,39 +720,39 @@ export default (ctx) => {
570
720
  (call $__str_copy (local.get $b) (i32.add (local.get $off) (local.get $alen)) (local.get $blen))
571
721
  (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
572
722
 
573
- ctx.core.stdlib['__str_replace'] = `(func $__str_replace (param $str f64) (param $search f64) (param $repl f64) (result f64)
723
+ ctx.core.stdlib['__str_replace'] = `(func $__str_replace (param $str i64) (param $search i64) (param $repl i64) (result f64)
574
724
  (local $idx i32) (local $slen i32)
575
725
  (local.set $idx (call $__str_indexof (local.get $str) (local.get $search) (i32.const 0)))
576
726
  (if (result f64) (i32.lt_s (local.get $idx) (i32.const 0))
577
- (then (local.get $str))
727
+ (then (f64.reinterpret_i64 (local.get $str)))
578
728
  (else
579
729
  (local.set $slen (call $__str_byteLen (local.get $search)))
580
730
  (call $__str_concat
581
- (call $__str_concat
582
- (call $__str_slice (local.get $str) (i32.const 0) (local.get $idx))
583
- (local.get $repl))
584
- (call $__str_slice (local.get $str) (i32.add (local.get $idx) (local.get $slen))
585
- (call $__str_byteLen (local.get $str)))))))`
586
-
587
- ctx.core.stdlib['__str_replaceall'] = `(func $__str_replaceall (param $str f64) (param $search f64) (param $repl f64) (result f64)
588
- (local $idx i32) (local $slen i32) (local $pos i32) (local $result f64)
731
+ (i64.reinterpret_f64 (call $__str_concat
732
+ (i64.reinterpret_f64 (call $__str_slice (local.get $str) (i32.const 0) (local.get $idx)))
733
+ (local.get $repl)))
734
+ (i64.reinterpret_f64 (call $__str_slice (local.get $str) (i32.add (local.get $idx) (local.get $slen))
735
+ (call $__str_byteLen (local.get $str))))))))`
736
+
737
+ ctx.core.stdlib['__str_replaceall'] = `(func $__str_replaceall (param $str i64) (param $search i64) (param $repl i64) (result f64)
738
+ (local $idx i32) (local $slen i32) (local $pos i32) (local $result i64)
589
739
  (local.set $slen (call $__str_byteLen (local.get $search)))
590
740
  (local.set $result (local.get $str))
591
741
  (local.set $pos (i32.const 0))
592
742
  (block $done (loop $next
593
743
  (local.set $idx (call $__str_indexof (local.get $result) (local.get $search) (local.get $pos)))
594
744
  (br_if $done (i32.lt_s (local.get $idx) (i32.const 0)))
595
- (local.set $result (call $__str_concat
596
- (call $__str_concat
597
- (call $__str_slice (local.get $result) (i32.const 0) (local.get $idx))
598
- (local.get $repl))
599
- (call $__str_slice (local.get $result) (i32.add (local.get $idx) (local.get $slen))
600
- (call $__str_byteLen (local.get $result)))))
745
+ (local.set $result (i64.reinterpret_f64 (call $__str_concat
746
+ (i64.reinterpret_f64 (call $__str_concat
747
+ (i64.reinterpret_f64 (call $__str_slice (local.get $result) (i32.const 0) (local.get $idx)))
748
+ (local.get $repl)))
749
+ (i64.reinterpret_f64 (call $__str_slice (local.get $result) (i32.add (local.get $idx) (local.get $slen))
750
+ (call $__str_byteLen (local.get $result)))))))
601
751
  (local.set $pos (i32.add (local.get $idx) (call $__str_byteLen (local.get $repl))))
602
752
  (br $next)))
603
- (local.get $result))`
753
+ (f64.reinterpret_i64 (local.get $result)))`
604
754
 
605
- ctx.core.stdlib['__str_split'] = `(func $__str_split (param $str f64) (param $sep f64) (result f64)
755
+ ctx.core.stdlib['__str_split'] = `(func $__str_split (param $str i64) (param $sep i64) (result f64)
606
756
  (local $slen i32) (local $plen i32) (local $count i32)
607
757
  (local $i i32) (local $j i32) (local $match i32)
608
758
  (local $arr i32) (local $piece_start i32) (local $piece_idx i32)
@@ -658,32 +808,32 @@ export default (ctx) => {
658
808
  (call $__str_slice (local.get $str) (local.get $piece_start) (local.get $slen)))
659
809
  (call $__mkptr (i32.const 1) (i32.const 0) (local.get $arr)))`
660
810
 
661
- ctx.core.stdlib['__str_join'] = `(func $__str_join (param $arr f64) (param $sep f64) (result f64)
811
+ ctx.core.stdlib['__str_join'] = `(func $__str_join (param $arr i64) (param $sep i64) (result f64)
662
812
  (local $off i32) (local $len i32) (local $i i32) (local $result f64)
663
813
  (local.set $off (call $__ptr_offset (local.get $arr)))
664
814
  (local.set $len (call $__len (local.get $arr)))
665
815
  (if (i32.eqz (local.get $len))
666
- (then (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 0) (i32.const 0)))))
667
- (local.set $result (f64.load (local.get $off)))
816
+ (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
817
+ (local.set $result (f64.reinterpret_i64 (call $__to_str (i64.load (local.get $off)))))
668
818
  (local.set $i (i32.const 1))
669
819
  (block $done (loop $loop
670
820
  (br_if $done (i32.ge_s (local.get $i) (local.get $len)))
671
- (local.set $result (call $__str_concat (local.get $result) (local.get $sep)))
672
- (local.set $result (call $__str_concat (local.get $result)
673
- (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3))))))
821
+ (local.set $result (call $__str_concat (i64.reinterpret_f64 (local.get $result)) (local.get $sep)))
822
+ (local.set $result (call $__str_concat (i64.reinterpret_f64 (local.get $result))
823
+ (i64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3))))))
674
824
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
675
825
  (br $loop)))
676
826
  (local.get $result))`
677
827
 
678
828
  // Source string copied via __str_copy (handles SSO/heap with memory.copy where possible).
679
829
  // Pad fill loops a single tile of pad bytes — hoist pad dispatch out of the byte loop.
680
- ctx.core.stdlib['__str_pad'] = `(func $__str_pad (param $str f64) (param $target i32) (param $pad f64) (param $before i32) (result f64)
830
+ ctx.core.stdlib['__str_pad'] = `(func $__str_pad (param $str i64) (param $target i32) (param $pad i64) (param $before i32) (result f64)
681
831
  (local $slen i32) (local $plen i32) (local $fill i32) (local $off i32) (local $i i32)
682
832
  (local $str_off i32) (local $pad_off i32)
683
833
  (local $pbits i64) (local $poff i32) (local $psso i32)
684
834
  (local.set $slen (call $__str_byteLen (local.get $str)))
685
835
  (if (i32.ge_s (local.get $slen) (local.get $target))
686
- (then (return (local.get $str))))
836
+ (then (return (f64.reinterpret_i64 (local.get $str)))))
687
837
  (local.set $plen (call $__str_byteLen (local.get $pad)))
688
838
  (local.set $fill (i32.sub (local.get $target) (local.get $slen)))
689
839
  (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $target))))
@@ -692,11 +842,11 @@ export default (ctx) => {
692
842
  (local.set $str_off (select (local.get $fill) (i32.const 0) (local.get $before)))
693
843
  (local.set $pad_off (select (i32.const 0) (local.get $slen) (local.get $before)))
694
844
  (call $__str_copy (local.get $str) (i32.add (local.get $off) (local.get $str_off)) (local.get $slen))
695
- (local.set $pbits (i64.reinterpret_f64 (local.get $pad)))
696
- (local.set $poff (i32.wrap_i64 (i64.and (local.get $pbits) (i64.const 0xFFFFFFFF))))
697
- (local.set $psso (i32.eq
698
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $pbits) (i64.const 47)) (i64.const 0xF)))
699
- (i32.const ${PTR.SSO})))
845
+ (local.set $pbits (local.get $pad))
846
+ (local.set $poff (i32.wrap_i64 (i64.and (local.get $pbits) (i64.const ${LAYOUT.OFFSET_MASK}))))
847
+ (local.set $psso (i32.and
848
+ (i32.wrap_i64 (i64.shr_u (local.get $pbits) (i64.const ${LAYOUT.AUX_SHIFT})))
849
+ (i32.const ${LAYOUT.SSO_BIT})))
700
850
  (block $d2 (loop $l2
701
851
  (br_if $d2 (i32.ge_s (local.get $i) (local.get $fill)))
702
852
  (i32.store8 (i32.add (local.get $off) (i32.add (local.get $pad_off) (local.get $i)))
@@ -716,36 +866,51 @@ export default (ctx) => {
716
866
  // === Method emitters ===
717
867
 
718
868
  // Type-qualified (collide with array: slice, indexOf, includes)
869
+ // String.prototype.toString / .valueOf — both return the receiver per spec
870
+ // (21.1.3.27/28). Typed forms cover the static-string case; generic forms
871
+ // pair with them so the dispatcher can pick a runtime ptr-type branch when
872
+ // the receiver type can't be statically inferred (e.g. a callback param).
873
+ ctx.core.emit['.string:toString'] = (str) => asF64(emit(str))
874
+ ctx.core.emit['.string:valueOf'] = (str) => asF64(emit(str))
875
+ ctx.core.emit['.toString'] = (val) => {
876
+ inc('__to_str')
877
+ return typed(['f64.reinterpret_i64', ['call', '$__to_str', asI64(emit(val))]], 'f64')
878
+ }
879
+ // Object.prototype.valueOf returns the receiver (per ES2024 20.1.3.7).
880
+ // Array/Object inherit this; only primitive wrappers (Number/Boolean/String)
881
+ // override to return the primitive — strings already covered by .string:valueOf.
882
+ ctx.core.emit['.valueOf'] = (val) => asF64(emit(val))
883
+
719
884
  ctx.core.emit['.string:slice'] = (str, start, end) => {
720
885
  inc('__str_slice')
721
- if (end != null) return typed(['call', '$__str_slice', asF64(emit(str)), asI32(emit(start)), asI32(emit(end))], 'f64')
886
+ if (end != null) return typed(['call', '$__str_slice', asI64(emit(str)), asI32(emit(start)), asI32(emit(end))], 'f64')
722
887
  const t = temp('t')
723
888
  return typed(['block', ['result', 'f64'],
724
889
  ['local.set', `$${t}`, asF64(emit(str))],
725
- ['call', '$__str_slice', ['local.get', `$${t}`], asI32(emit(start)),
726
- ['call', '$__str_byteLen', ['local.get', `$${t}`]]]], 'f64')
890
+ ['call', '$__str_slice', ['i64.reinterpret_f64', ['local.get', `$${t}`]], asI32(emit(start)),
891
+ ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]], 'f64')
727
892
  }
728
893
 
729
894
  ctx.core.emit['.string:indexOf'] = (str, search, from) => {
730
895
  inc('__str_indexof')
731
- return typed(['f64.convert_i32_s', ['call', '$__str_indexof', asF64(emit(str)), asF64(emit(search)), from ? asI32(emit(from)) : ['i32.const', 0]]], 'f64')
896
+ return typed(['f64.convert_i32_s', ['call', '$__str_indexof', asI64(emit(str)), asI64(emit(search)), from ? asI32(emit(from)) : ['i32.const', 0]]], 'f64')
732
897
  }
733
898
 
734
899
  ctx.core.emit['.string:includes'] = (str, search) => {
735
900
  inc('__str_indexof')
736
901
  return typed(['f64.convert_i32_s',
737
- ['i32.ge_s', ['call', '$__str_indexof', asF64(emit(str)), asF64(emit(search)), ['i32.const', 0]], ['i32.const', 0]]], 'f64')
902
+ ['i32.ge_s', ['call', '$__str_indexof', asI64(emit(str)), asI64(emit(search)), ['i32.const', 0]], ['i32.const', 0]]], 'f64')
738
903
  }
739
904
 
740
905
  // Generic (no collision)
741
906
  ctx.core.emit['.substring'] = (str, start, end) => {
742
907
  inc('__str_substring')
743
- if (end != null) return typed(['call', '$__str_substring', asF64(emit(str)), asI32(emit(start)), asI32(emit(end))], 'f64')
908
+ if (end != null) return typed(['call', '$__str_substring', asI64(emit(str)), asI32(emit(start)), asI32(emit(end))], 'f64')
744
909
  const t = temp('t')
745
910
  return typed(['block', ['result', 'f64'],
746
911
  ['local.set', `$${t}`, asF64(emit(str))],
747
- ['call', '$__str_substring', ['local.get', `$${t}`], asI32(emit(start)),
748
- ['call', '$__str_byteLen', ['local.get', `$${t}`]]]], 'f64')
912
+ ['call', '$__str_substring', ['i64.reinterpret_f64', ['local.get', `$${t}`]], asI32(emit(start)),
913
+ ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]], 'f64')
749
914
  }
750
915
 
751
916
  // Factory for simple str→call patterns: [emitKey, stdlibName, argCoercions, i32Result?]
@@ -756,38 +921,67 @@ export default (ctx) => {
756
921
  return typed(i32Result ? ['f64.convert_i32_s', call] : call, 'f64')
757
922
  }
758
923
 
759
- // Simple str methods: [emitKey, stdlibName, argCoercions, i32Result?]
760
- ctx.core.emit['.startsWith'] = strMethod('__str_startswith', ['f'], true)
761
- ctx.core.emit['.endsWith'] = strMethod('__str_endswith', ['f'], true)
762
- ctx.core.emit['.trim'] = strMethod('__str_trim', [])
763
- ctx.core.emit['.trimStart'] = strMethod('__str_trimStart', [])
764
- ctx.core.emit['.trimEnd'] = strMethod('__str_trimEnd', [])
765
- ctx.core.emit['.repeat'] = strMethod('__str_repeat', ['i'])
766
- ctx.core.emit['.split'] = strMethod('__str_split', ['f'])
767
- ctx.core.emit['.replace'] = strMethod('__str_replace', ['f', 'f'])
768
- ctx.core.emit['.replaceAll'] = strMethod('__str_replaceall', ['f', 'f'])
924
+ // Search args go through ToString per spec — coerce non-string-typed args
925
+ // via __to_str so the underlying byte-compare receives an actual string.
926
+ const stringSearchMethod = (name) => (str, sfx) => {
927
+ inc(name)
928
+ let sfxArg = asI64(emit(sfx))
929
+ if (valTypeOf(sfx) !== VAL.STRING) {
930
+ inc('__to_str')
931
+ sfxArg = ['call', '$__to_str', sfxArg]
932
+ }
933
+ return typed(['f64.convert_i32_s', ['call', `$${name}`, asI64(emit(str)), sfxArg]], 'f64')
934
+ }
935
+ ctx.core.emit['.startsWith'] = stringSearchMethod('__str_startswith')
936
+ ctx.core.emit['.endsWith'] = stringSearchMethod('__str_endswith')
937
+ ctx.core.emit['.trim'] = (str) => (inc('__str_trim'),
938
+ typed(['call', '$__str_trim', asI64(emit(str))], 'f64'))
939
+ ctx.core.emit['.trimStart'] = (str) => (inc('__str_trimStart'),
940
+ typed(['call', '$__str_trimStart', asI64(emit(str))], 'f64'))
941
+ ctx.core.emit['.trimEnd'] = (str) => (inc('__str_trimEnd'),
942
+ typed(['call', '$__str_trimEnd', asI64(emit(str))], 'f64'))
943
+ ctx.core.emit['.repeat'] = (str, n) => (inc('__str_repeat'),
944
+ typed(['call', '$__str_repeat', asI64(emit(str)), asI32(emit(n))], 'f64'))
945
+ ctx.core.emit['.split'] = (str, sep) => (inc('__str_split'),
946
+ typed(['call', '$__str_split', asI64(emit(str)), asI64(emit(sep))], 'f64'))
947
+ ctx.core.emit['.replace'] = (str, search, repl) => (inc('__str_replace'),
948
+ typed(['call', '$__str_replace', asI64(emit(str)), asI64(emit(search)), asI64(emit(repl))], 'f64'))
949
+ ctx.core.emit['.replaceAll'] = (str, search, repl) => (inc('__str_replaceall'),
950
+ typed(['call', '$__str_replaceall', asI64(emit(str)), asI64(emit(search)), asI64(emit(repl))], 'f64'))
769
951
 
770
952
  ctx.core.emit['.toUpperCase'] = (str) => {
771
953
  inc('__str_case')
772
- return typed(['call', '$__str_case', asF64(emit(str)), ['i32.const', 97], ['i32.const', 122], ['i32.const', -32]], 'f64')
954
+ return typed(['call', '$__str_case', asI64(emit(str)), ['i32.const', 97], ['i32.const', 122], ['i32.const', -32]], 'f64')
773
955
  }
774
956
 
775
957
  ctx.core.emit['.toLowerCase'] = (str) => {
776
958
  inc('__str_case')
777
- return typed(['call', '$__str_case', asF64(emit(str)), ['i32.const', 65], ['i32.const', 90], ['i32.const', 32]], 'f64')
959
+ return typed(['call', '$__str_case', asI64(emit(str)), ['i32.const', 65], ['i32.const', 90], ['i32.const', 32]], 'f64')
960
+ }
961
+
962
+ // Byte-wise variant of String.prototype.localeCompare. Returns -1/0/1 from
963
+ // an unsigned byte-by-byte compare with shorter-string-sorts-first tiebreak.
964
+ // NOT locale-aware: real localeCompare is ICU-driven (CLDR collation, case
965
+ // folding, accent ordering). For ASCII inputs the byte-wise result matches
966
+ // the spec exactly; for non-ASCII it follows UTF-8 byte order, which is
967
+ // codepoint order for well-formed strings — close enough for sort-stability
968
+ // use cases, wrong for human-language collation.
969
+ ctx.core.emit['.localeCompare'] = (str, other) => {
970
+ inc('__str_cmp')
971
+ return typed(['f64.convert_i32_s', ['call', '$__str_cmp', asI64(emit(str)), asI64(emit(other))]], 'f64')
778
972
  }
779
973
 
780
974
  ctx.core.emit['.string:concat'] = (str, ...others) => {
781
975
  inc('__str_concat')
782
976
  let result = asF64(emit(str))
783
- for (const other of others) result = typed(['call', '$__str_concat', result, asF64(emit(other))], 'f64')
977
+ for (const other of others) result = typed(['call', '$__str_concat', ['i64.reinterpret_f64', result], asI64(emit(other))], 'f64')
784
978
  return result
785
979
  }
786
980
 
787
981
  ctx.core.emit['strcat'] = (...parts) => {
788
982
  inc('__to_str', '__str_byteLen', '__alloc', '__mkptr', '__str_copy')
789
- if (!parts.length) return mkPtrIR(PTR.SSO, 0, 0)
790
- if (parts.length === 1) return typed(['call', '$__to_str', asF64(emit(parts[0]))], 'f64')
983
+ if (!parts.length) return mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT, 0)
984
+ if (parts.length === 1) return typed(['f64.reinterpret_i64', ['call', '$__to_str', asI64(emit(parts[0]))]], 'f64')
791
985
 
792
986
  const vals = parts.map(() => temp('s'))
793
987
  const lens = parts.map(() => tempI32('sl'))
@@ -797,8 +991,8 @@ export default (ctx) => {
797
991
  const ir = []
798
992
 
799
993
  for (let i = 0; i < parts.length; i++) {
800
- ir.push(['local.set', `$${vals[i]}`, ['call', '$__to_str', asF64(emit(parts[i]))]])
801
- ir.push(['local.set', `$${lens[i]}`, ['call', '$__str_byteLen', ['local.get', `$${vals[i]}`]]])
994
+ ir.push(['local.set', `$${vals[i]}`, ['f64.reinterpret_i64', ['call', '$__to_str', asI64(emit(parts[i]))]]])
995
+ ir.push(['local.set', `$${lens[i]}`, ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${vals[i]}`]]]])
802
996
  }
803
997
  ir.push(['local.set', `$${total}`, ['i32.const', 0]])
804
998
  for (const len of lens)
@@ -810,26 +1004,26 @@ export default (ctx) => {
810
1004
  ['local.set', `$${dst}`, ['local.get', `$${off}`]],
811
1005
  ]
812
1006
  for (let i = 0; i < parts.length; i++) {
813
- alloc.push(['call', '$__str_copy', ['local.get', `$${vals[i]}`], ['local.get', `$${dst}`], ['local.get', `$${lens[i]}`]])
1007
+ alloc.push(['call', '$__str_copy', ['i64.reinterpret_f64', ['local.get', `$${vals[i]}`]], ['local.get', `$${dst}`], ['local.get', `$${lens[i]}`]])
814
1008
  alloc.push(['local.set', `$${dst}`, ['i32.add', ['local.get', `$${dst}`], ['local.get', `$${lens[i]}`]]])
815
1009
  }
816
1010
  alloc.push(['call', '$__mkptr', ['i32.const', PTR.STRING], ['i32.const', 0], ['local.get', `$${off}`]])
817
1011
  ir.push(['if', ['result', 'f64'], ['i32.eqz', ['local.get', `$${total}`]],
818
- ['then', mkPtrIR(PTR.SSO, 0, 0)],
1012
+ ['then', mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT, 0)],
819
1013
  ['else', ['block', ['result', 'f64'], ...alloc]]])
820
1014
  return typed(['block', ['result', 'f64'], ...ir], 'f64')
821
1015
  }
822
1016
 
823
1017
  ctx.core.emit['.padStart'] = (str, len, pad) => {
824
1018
  inc('__str_pad')
825
- const vpad = pad != null ? asF64(emit(pad)) : mkPtrIR(PTR.SSO, 1, 32)
826
- return typed(['call', '$__str_pad', asF64(emit(str)), asI32(emit(len)), vpad, ['i32.const', 1]], 'f64')
1019
+ const vpad = pad != null ? asI64(emit(pad)) : ['i64.reinterpret_f64', mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1, 32)]
1020
+ return typed(['call', '$__str_pad', asI64(emit(str)), asI32(emit(len)), vpad, ['i32.const', 1]], 'f64')
827
1021
  }
828
1022
 
829
1023
  ctx.core.emit['.padEnd'] = (str, len, pad) => {
830
1024
  inc('__str_pad')
831
- const vpad = pad != null ? asF64(emit(pad)) : mkPtrIR(PTR.SSO, 1, 32)
832
- return typed(['call', '$__str_pad', asF64(emit(str)), asI32(emit(len)), vpad, ['i32.const', 0]], 'f64')
1025
+ const vpad = pad != null ? asI64(emit(pad)) : ['i64.reinterpret_f64', mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1, 32)]
1026
+ return typed(['call', '$__str_pad', asI64(emit(str)), asI32(emit(len)), vpad, ['i32.const', 0]], 'f64')
833
1027
  }
834
1028
 
835
1029
  // .charAt(i) → 1-char string from char code at index i
@@ -838,8 +1032,8 @@ export default (ctx) => {
838
1032
  const t = tempI32('ch')
839
1033
  // Get char code, create SSO string with 1 byte
840
1034
  return typed(['block', ['result', 'f64'],
841
- ['local.set', `$${t}`, ['call', '$__char_at', asF64(emit(str)), asI32(emit(idx))]],
842
- mkPtrIR(PTR.SSO, 1, ['local.get', `$${t}`])], 'f64')
1035
+ ['local.set', `$${t}`, ['call', '$__char_at', asI64(emit(str)), asI32(emit(idx))]],
1036
+ mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1, ['local.get', `$${t}`])], 'f64')
843
1037
  }
844
1038
 
845
1039
  // .charCodeAt(i) → integer char code (0..255 for ASCII bytes — unsigned, always
@@ -848,7 +1042,7 @@ export default (ctx) => {
848
1042
  // `c - 48` arithmetic skip the per-iteration f64 widen + i32 trunc round-trip.
849
1043
  ctx.core.emit['.charCodeAt'] = (str, idx) => {
850
1044
  inc('__char_at')
851
- return typed(['call', '$__char_at', asF64(emit(str)), asI32(emit(idx))], 'i32')
1045
+ return typed(['call', '$__char_at', asI64(emit(str)), asI32(emit(idx))], 'i32')
852
1046
  }
853
1047
 
854
1048
  // String.fromCharCode(code) → 1-char SSO string
@@ -860,32 +1054,35 @@ export default (ctx) => {
860
1054
  return typed(['call', '$__ftoa', asF64(emit(value)), ['i32.const', 0], ['i32.const', 0]], 'f64')
861
1055
  }
862
1056
  inc('__to_str')
863
- return typed(['call', '$__to_str', asF64(emit(value))], 'f64')
1057
+ return typed(['f64.reinterpret_i64', ['call', '$__to_str', asI64(emit(value))]], 'f64')
864
1058
  }
865
1059
 
866
- ctx.core.emit['String.fromCharCode'] = (code) => mkPtrIR(PTR.SSO, 1, asI32(emit(code)))
1060
+ ctx.core.emit['String.fromCharCode'] = (code) => {
1061
+ if (code === undefined) return emit(['str', ''])
1062
+ return mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1, asI32(emit(code)))
1063
+ }
867
1064
 
868
1065
  // String.fromCodePoint(cp) → UTF-8 encoded string
869
1066
  ctx.core.stdlib['__fromCodePoint'] = `(func $__fromCodePoint (param $cp i32) (result f64)
870
1067
  (local $off i32) (local $len i32)
871
1068
  ;; ASCII: 1 byte SSO
872
1069
  (if (i32.lt_u (local.get $cp) (i32.const 128))
873
- (then (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 1) (local.get $cp)))))
1070
+ (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT | 1}) (local.get $cp)))))
874
1071
  ;; 2-byte: 0x80-0x7FF → SSO
875
1072
  (if (i32.lt_u (local.get $cp) (i32.const 0x800))
876
- (then (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 2)
1073
+ (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT | 2})
877
1074
  (i32.or
878
1075
  (i32.or (i32.const 0xC0) (i32.shr_u (local.get $cp) (i32.const 6)))
879
1076
  (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 8)))))))
880
1077
  ;; 3-byte: 0x800-0xFFFF → SSO (3 bytes fits)
881
1078
  (if (i32.lt_u (local.get $cp) (i32.const 0x10000))
882
- (then (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 3)
1079
+ (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT | 3})
883
1080
  (i32.or (i32.or
884
1081
  (i32.or (i32.const 0xE0) (i32.shr_u (local.get $cp) (i32.const 12)))
885
1082
  (i32.shl (i32.or (i32.const 0x80) (i32.and (i32.shr_u (local.get $cp) (i32.const 6)) (i32.const 0x3F))) (i32.const 8)))
886
1083
  (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 16)))))))
887
1084
  ;; 4-byte: 0x10000-0x10FFFF → SSO (4 bytes fits)
888
- (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 4)
1085
+ (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT | 4})
889
1086
  (i32.or (i32.or (i32.or
890
1087
  (i32.or (i32.const 0xF0) (i32.shr_u (local.get $cp) (i32.const 18)))
891
1088
  (i32.shl (i32.or (i32.const 0x80) (i32.and (i32.shr_u (local.get $cp) (i32.const 12)) (i32.const 0x3F))) (i32.const 8)))
@@ -893,12 +1090,13 @@ export default (ctx) => {
893
1090
  (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 24))))))`
894
1091
 
895
1092
  ctx.core.emit['String.fromCodePoint'] = (code) => {
1093
+ if (code === undefined) return emit(['str', ''])
896
1094
  inc('__fromCodePoint')
897
1095
  return typed(['call', '$__fromCodePoint', asI32(emit(code))], 'f64')
898
1096
  }
899
1097
 
900
1098
  // .at(i) → charAt with negative index support
901
- ctx.core.emit['.at'] = (str, idx) => {
1099
+ ctx.core.emit['.string:at'] = (str, idx) => {
902
1100
  inc('__char_at', '__str_byteLen')
903
1101
  const t = tempI32('at'), s = temp('as')
904
1102
  return typed(['block', ['result', 'f64'],
@@ -907,14 +1105,14 @@ export default (ctx) => {
907
1105
  // Negative index: t += length
908
1106
  ['if', ['i32.lt_s', ['local.get', `$${t}`], ['i32.const', 0]],
909
1107
  ['then', ['local.set', `$${t}`, ['i32.add', ['local.get', `$${t}`],
910
- ['call', '$__str_byteLen', ['local.get', `$${s}`]]]]]],
911
- mkPtrIR(PTR.SSO, 1, ['call', '$__char_at', ['local.get', `$${s}`], ['local.get', `$${t}`]])], 'f64')
1108
+ ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${s}`]]]]]]],
1109
+ mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1, ['call', '$__char_at', ['i64.reinterpret_f64', ['local.get', `$${s}`]], ['local.get', `$${t}`]])], 'f64')
912
1110
  }
913
1111
 
914
1112
  // .search(str) → indexOf (same as indexOf for string args)
915
1113
  ctx.core.emit['.search'] = (str, search) => {
916
1114
  inc('__str_indexof')
917
- return typed(['f64.convert_i32_s', ['call', '$__str_indexof', asF64(emit(str)), asF64(emit(search)), ['i32.const', 0]]], 'f64')
1115
+ return typed(['f64.convert_i32_s', ['call', '$__str_indexof', asI64(emit(str)), asI64(emit(search)), ['i32.const', 0]]], 'f64')
918
1116
  }
919
1117
 
920
1118
  // .match(str) → [match] array if found, or 0 (null) if not
@@ -926,24 +1124,25 @@ export default (ctx) => {
926
1124
  return typed(['block', ['result', 'f64'],
927
1125
  ['local.set', `$${s}`, asF64(emit(str))],
928
1126
  ['local.set', `$${q}`, asF64(emit(search))],
929
- ['local.set', `$${idx}`, ['call', '$__str_indexof', ['local.get', `$${s}`], ['local.get', `$${q}`], ['i32.const', 0]]],
1127
+ ['local.set', `$${idx}`, ['call', '$__str_indexof', ['i64.reinterpret_f64', ['local.get', `$${s}`]], ['i64.reinterpret_f64', ['local.get', `$${q}`]], ['i32.const', 0]]],
930
1128
  ['if', ['result', 'f64'], ['i32.lt_s', ['local.get', `$${idx}`], ['i32.const', 0]],
931
1129
  ['then', ['f64.const', 0]], // null
932
1130
  ['else',
933
1131
  // Build 1-element array containing the search string
934
1132
  ['call', '$__wrap1',
935
- ['call', '$__str_slice', ['local.get', `$${s}`],
936
- ['local.get', `$${idx}`],
937
- ['i32.add', ['local.get', `$${idx}`], ['call', '$__str_byteLen', ['local.get', `$${q}`]]]]]]]], 'f64')
1133
+ ['i64.reinterpret_f64',
1134
+ ['call', '$__str_slice', ['i64.reinterpret_f64', ['local.get', `$${s}`]],
1135
+ ['local.get', `$${idx}`],
1136
+ ['i32.add', ['local.get', `$${idx}`], ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${q}`]]]]]]]]]], 'f64')
938
1137
  }
939
1138
 
940
- // __wrap1(val: f64) → f64 — create 1-element array [val]
941
- ctx.core.stdlib['__wrap1'] = `(func $__wrap1 (param $val f64) (result f64)
1139
+ // __wrap1(val: i64) → f64 — create 1-element array [val]
1140
+ ctx.core.stdlib['__wrap1'] = `(func $__wrap1 (param $val i64) (result f64)
942
1141
  (local $ptr i32)
943
1142
  (local.set $ptr (call $__alloc (i32.const 16)))
944
1143
  (i32.store (local.get $ptr) (i32.const 1))
945
1144
  (i32.store (i32.add (local.get $ptr) (i32.const 4)) (i32.const 1))
946
- (f64.store (i32.add (local.get $ptr) (i32.const 8)) (local.get $val))
1145
+ (i64.store (i32.add (local.get $ptr) (i32.const 8)) (local.get $val))
947
1146
  (call $__mkptr (i32.const 1) (i32.const 0) (i32.add (local.get $ptr) (i32.const 8))))`
948
1147
 
949
1148
  // TextEncoder() / TextDecoder() → dummy values (methods do the work)
@@ -952,7 +1151,7 @@ export default (ctx) => {
952
1151
 
953
1152
  // .encode(str) → Uint8Array of string's UTF-8 bytes
954
1153
  // Copies bytes from string (SSO or heap) into a new Uint8Array
955
- ctx.core.stdlib['__str_encode'] = `(func $__str_encode (param $str f64) (result f64)
1154
+ ctx.core.stdlib['__str_encode'] = `(func $__str_encode (param $str i64) (result f64)
956
1155
  (local $len i32) (local $dst i32)
957
1156
  (local.set $len (call $__str_byteLen (local.get $str)))
958
1157
  (local.set $dst (call $__alloc (i32.add (i32.const 8) (local.get $len))))
@@ -964,11 +1163,11 @@ export default (ctx) => {
964
1163
 
965
1164
  ctx.core.emit['.encode'] = (obj, str) => {
966
1165
  inc('__str_encode')
967
- return typed(['call', '$__str_encode', asF64(emit(str))], 'f64')
1166
+ return typed(['call', '$__str_encode', asI64(emit(str))], 'f64')
968
1167
  }
969
1168
 
970
1169
  // .decode(uint8arr) → string from byte data
971
- ctx.core.stdlib['__bytes_decode'] = `(func $__bytes_decode (param $arr f64) (result f64)
1170
+ ctx.core.stdlib['__bytes_decode'] = `(func $__bytes_decode (param $arr i64) (result f64)
972
1171
  (local $off i32) (local $len i32) (local $dst i32)
973
1172
  (local.set $off (call $__ptr_offset (local.get $arr)))
974
1173
  (local.set $len (call $__len (local.get $arr)))
@@ -980,6 +1179,6 @@ export default (ctx) => {
980
1179
 
981
1180
  ctx.core.emit['.decode'] = (obj, arr) => {
982
1181
  inc('__bytes_decode')
983
- return typed(['call', '$__bytes_decode', asF64(emit(arr))], 'f64')
1182
+ return typed(['call', '$__bytes_decode', asI64(emit(arr))], 'f64')
984
1183
  }
985
1184
  }