jz 0.1.0 → 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,14 +12,21 @@
10
12
  * @module string
11
13
  */
12
14
 
13
- import { emit, typed, asF64, asI32, UNDEF_NAN, mkPtrIR, temp, tempI32 } from '../src/compile.js'
14
- import { inc, PTR } from '../src/ctx.js'
15
+ import { typed, asF64, asI32, asI64, NULL_NAN, UNDEF_NAN, mkPtrIR, temp, tempI32 } from '../src/ir.js'
16
+ import { emit } from '../src/emit.js'
17
+ import { valTypeOf, VAL } from '../src/analyze.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')
15
23
 
16
24
 
17
25
  export default (ctx) => {
18
26
  Object.assign(ctx.core.stdlibDeps, {
19
27
  __str_concat: ['__to_str', '__str_byteLen', '__alloc', '__mkptr', '__str_copy'],
20
28
  __str_concat_raw: ['__str_byteLen', '__alloc', '__mkptr', '__str_copy'],
29
+ __str_append_byte: ['__str_byteLen', '__alloc', '__mkptr', '__str_copy'],
21
30
  __str_copy: [],
22
31
  __str_slice: ['__str_byteLen', '__alloc'],
23
32
  __str_indexof: ['__str_byteLen'],
@@ -32,8 +41,9 @@ export default (ctx) => {
32
41
  __str_replace: ['__str_indexof', '__str_slice', '__str_concat'],
33
42
  __str_replaceall: ['__str_indexof', '__str_slice', '__str_concat'],
34
43
  __str_split: ['__str_slice', '__str_byteLen', '__char_at', '__alloc'],
35
- __str_idx: ['__str_byteLen', '__char_at', '__mkptr'],
44
+ __str_idx: [],
36
45
  __str_eq: ['__char_at'],
46
+ __str_cmp: ['__char_at', '__str_byteLen'],
37
47
  __str_pad: ['__str_byteLen', '__str_copy', '__alloc'],
38
48
  __str_join: ['__str_concat', '__to_str', '__str_byteLen', '__len', '__ptr_offset'],
39
49
  __str_encode: ['__str_byteLen', '__str_copy'],
@@ -50,9 +60,10 @@ export default (ctx) => {
50
60
  if (ctx.features.sso && str.length <= MAX_SSO && /^[\x00-\x7f]*$/.test(str)) {
51
61
  let packed = 0
52
62
  for (let i = 0; i < str.length; i++) packed |= str.charCodeAt(i) << (i * 8)
53
- return mkPtrIR(PTR.SSO, str.length, packed)
63
+ return mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | str.length, packed)
54
64
  }
55
- const len = str.length
65
+ const bytes = new TextEncoder().encode(str)
66
+ const len = bytes.length
56
67
  if (!ctx.memory.shared) {
57
68
  // Own memory: place in static data segment (no runtime allocation)
58
69
  if (!ctx.runtime.data) ctx.runtime.data = ''
@@ -61,7 +72,7 @@ export default (ctx) => {
61
72
  while (ctx.runtime.data.length % 4 !== 0) ctx.runtime.data += '\0'
62
73
  const offset = ctx.runtime.data.length
63
74
  ctx.runtime.data += String.fromCharCode(len & 0xFF, (len >> 8) & 0xFF, (len >> 16) & 0xFF, (len >> 24) & 0xFF)
64
- for (let i = 0; i < len; i++) ctx.runtime.data += String.fromCharCode(str.charCodeAt(i))
75
+ for (let i = 0; i < len; i++) ctx.runtime.data += String.fromCharCode(bytes[i])
65
76
  ctx.runtime.dataDedup.set(str, offset)
66
77
  return mkPtrIR(PTR.STRING, 0, offset + 4)
67
78
  }
@@ -75,10 +86,10 @@ export default (ctx) => {
75
86
  }
76
87
  let off = ctx.runtime.strPoolDedup.get(str)
77
88
  if (off === undefined) {
78
- // Pack length header then bytes; offset points PAST the length (at the data).
89
+ // Pack length header then UTF-8 bytes; offset points PAST the length (at the data).
79
90
  ctx.runtime.strPool += String.fromCharCode(len & 0xFF, (len >> 8) & 0xFF, (len >> 16) & 0xFF, (len >> 24) & 0xFF)
80
91
  off = ctx.runtime.strPool.length
81
- ctx.runtime.strPool += str
92
+ for (let i = 0; i < len; i++) ctx.runtime.strPool += String.fromCharCode(bytes[i])
82
93
  ctx.runtime.strPoolDedup.set(str, off)
83
94
  }
84
95
  return mkPtrIR(PTR.STRING, 0, ['i32.add', ['global.get', '$__strBase'], ['i32.const', off]])
@@ -88,28 +99,25 @@ export default (ctx) => {
88
99
 
89
100
  // SSO/STRING ptrs never have forwarding pointers (only ARRAY does), so we extract
90
101
  // the raw offset directly instead of paying the __ptr_offset function-call overhead.
91
- 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)
92
103
  (i32.and
93
104
  (i32.shr_u
94
- (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})))
95
106
  (i32.mul (local.get $i) (i32.const 8)))
96
107
  (i32.const 0xFF)))`
97
108
 
98
- 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)
99
110
  (i32.load8_u (i32.add
100
- (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})))
101
112
  (local.get $i))))`
102
113
 
103
- // Hot (~37M calls in watr self-host). Type+offset extracted once from $bits;
104
- // SSO/STRING bodies merged inline to skip 2 function calls per char fetch.
105
- ctx.core.stdlib['__char_at'] = `(func $__char_at (param $ptr f64) (param $i i32) (result i32)
106
- (local $bits i64) (local $off i32)
107
- (local.set $bits (i64.reinterpret_f64 (local.get $ptr)))
108
- (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}))))
109
119
  (if (result i32)
110
- (i32.eq
111
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF)))
112
- (i32.const ${PTR.SSO}))
120
+ (i64.ne (i64.and (local.get $ptr) (i64.const ${SSO_BIT_I64})) (i64.const 0))
113
121
  (then
114
122
  (i32.and
115
123
  (i32.shr_u (local.get $off) (i32.mul (local.get $i) (i32.const 8)))
@@ -117,44 +125,66 @@ export default (ctx) => {
117
125
  (else
118
126
  (i32.load8_u (i32.add (local.get $off) (local.get $i))))))`
119
127
 
120
- ctx.core.stdlib['__str_idx'] = `(func $__str_idx (param $ptr f64) (param $i i32) (result f64)
121
- (local $len i32)
122
- (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))))))
123
144
  (if (result f64)
124
- (i32.or
125
- (i32.lt_s (local.get $i) (i32.const 0))
126
- (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)))
127
146
  (then (f64.const nan:${UNDEF_NAN}))
128
147
  (else
129
- (call $__mkptr
130
- (i32.const ${PTR.SSO})
131
- (i32.const 1)
132
- (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)))))))))))`
133
156
 
134
157
  // Hot: ~53M calls in watr self-host. Bit-eq covers identity. SSO/SSO with !bit-eq
135
158
  // guarantees content differs (high 32 bits encode type+len; both equal → low 32 differs
136
- // ⇒ bytes differ). STRING/STRING uses raw load8_u — no per-byte function calls.
137
- // Mixed SSO×STRING is rare; falls back to __char_at.
138
- 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)
139
162
  (local $len i32) (local $lenB i32) (local $i i32)
140
- (local $ba i64) (local $bb i64) (local $ta i32) (local $tb i32)
163
+ (local $ta i32) (local $tb i32)
141
164
  (local $offA i32) (local $offB i32)
142
- (local.set $ba (i64.reinterpret_f64 (local.get $a)))
143
- (local.set $bb (i64.reinterpret_f64 (local.get $b)))
144
- (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))
145
167
  (then (return (i32.const 1))))
146
- (local.set $ta (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ba) (i64.const 47)) (i64.const 0xF))))
147
- (local.set $tb (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bb) (i64.const 47)) (i64.const 0xF))))
148
- (local.set $offA (i32.wrap_i64 (i64.and (local.get $ba) (i64.const 0xFFFFFFFF))))
149
- (local.set $offB (i32.wrap_i64 (i64.and (local.get $bb) (i64.const 0xFFFFFFFF))))
150
- ;; Both SSO with !bit-eq ⇒ content differs (high 32 bits hold type+len; both equal here).
151
- (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))
152
180
  (then (return (i32.const 0))))
153
- ;; Both STRING fast path: inline len from header. Chunk by 4 bytes via unaligned i32.load
154
- ;; (wasm guarantees unaligned-OK), then byte-tail. Most string comparisons fail early on
155
- ;; the first 4-byte word, so this collapses the per-byte branch overhead into a single
156
- ;; 32-bit equality.
157
- (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))))
158
188
  (then
159
189
  (if (i32.or (i32.lt_u (local.get $offA) (i32.const 4)) (i32.lt_u (local.get $offB) (i32.const 4)))
160
190
  (then (return (i32.const 0))))
@@ -180,14 +210,18 @@ export default (ctx) => {
180
210
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
181
211
  (br $lh)))
182
212
  (return (i32.const 1))))
183
- ;; Mixed (SSO×STRING) or anything else: compute len per side then per-byte via __char_at.
184
- (if (i32.eq (local.get $ta) (i32.const ${PTR.SSO}))
185
- (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}))))
186
218
  (else
187
219
  (if (i32.and (i32.eq (local.get $ta) (i32.const ${PTR.STRING})) (i32.ge_u (local.get $offA) (i32.const 4)))
188
220
  (then (local.set $len (i32.load (i32.sub (local.get $offA) (i32.const 4))))))))
189
- (if (i32.eq (local.get $tb) (i32.const ${PTR.SSO}))
190
- (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}))))
191
225
  (else
192
226
  (if (i32.and (i32.eq (local.get $tb) (i32.const ${PTR.STRING})) (i32.ge_u (local.get $offB) (i32.const 4)))
193
227
  (then (local.set $lenB (i32.load (i32.sub (local.get $offB) (i32.const 4))))))))
@@ -202,28 +236,62 @@ export default (ctx) => {
202
236
  (br $lm)))
203
237
  (i32.const 1))`
204
238
 
205
- // === 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))`
206
268
 
207
- ctx.core.stdlib['__str_byteLen'] = `(func $__str_byteLen (param $ptr f64) (result i32)
208
- (local $bits i64) (local $t i32) (local $off i32)
209
- (local.set $bits (i64.reinterpret_f64 (local.get $ptr)))
210
- (local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF))))
211
- (if (result i32) (i32.eq (local.get $t) (i32.const ${PTR.SSO}))
212
- (then (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 32)) (i64.const 0x7FFF))))
213
- (else
214
- (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
215
- (if (result i32)
216
- (i32.and (i32.eq (local.get $t) (i32.const ${PTR.STRING})) (i32.ge_u (local.get $off) (i32.const 4)))
217
- (then (i32.load (i32.sub (local.get $off) (i32.const 4))))
218
- (else (i32.const 0))))))`
269
+ // === WAT: unified byte length (SSO aux low bits, heap → header) ===
270
+
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))))`
219
287
 
220
288
  // === WAT: string methods ===
221
289
 
222
290
  // SSO source uses an unrolled byte-extract loop (len ≤ 4); heap source uses memory.copy
223
291
  // (single bulk op instead of nlen × __char_at).
224
- 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)
225
293
  (local $len i32) (local $nlen i32) (local $off i32) (local $i i32)
226
- (local $bits i64) (local $srcOff i32) (local $isSso i32)
294
+ (local $srcOff i32) (local $isSso i32)
227
295
  (local.set $len (call $__str_byteLen (local.get $ptr)))
228
296
  (if (i32.lt_s (local.get $start) (i32.const 0))
229
297
  (then (local.set $start (i32.add (local.get $len) (local.get $start)))))
@@ -238,16 +306,15 @@ export default (ctx) => {
238
306
  (if (i32.gt_s (local.get $end) (local.get $len))
239
307
  (then (local.set $end (local.get $len))))
240
308
  (if (i32.ge_s (local.get $start) (local.get $end))
241
- (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)))))
242
310
  (local.set $nlen (i32.sub (local.get $end) (local.get $start)))
243
311
  (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $nlen))))
244
312
  (i32.store (local.get $off) (local.get $nlen))
245
313
  (local.set $off (i32.add (local.get $off) (i32.const 4)))
246
- (local.set $bits (i64.reinterpret_f64 (local.get $ptr)))
247
- (local.set $srcOff (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
248
- (local.set $isSso (i32.eq
249
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF)))
250
- (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}))))
251
318
  (if (local.get $isSso)
252
319
  (then
253
320
  (block $done (loop $loop
@@ -262,7 +329,7 @@ export default (ctx) => {
262
329
  (memory.copy (local.get $off) (i32.add (local.get $srcOff) (local.get $start)) (local.get $nlen))))
263
330
  (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
264
331
 
265
- 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)
266
333
  (local $len i32) (local $tmp i32)
267
334
  (local.set $len (call $__str_byteLen (local.get $ptr)))
268
335
  (if (i32.lt_s (local.get $start) (i32.const 0))
@@ -282,24 +349,22 @@ export default (ctx) => {
282
349
 
283
350
  // Hoist SSO/heap dispatch for hay and ndl out of the inner byte loop. Inner
284
351
  // loop becomes (load8_u OR sso byte-extract) per side — no per-byte calls.
285
- 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)
286
353
  (local $hlen i32) (local $nlen i32) (local $i i32) (local $j i32) (local $match i32)
287
- (local $hbits i64) (local $nbits i64) (local $hoff i32) (local $noff i32)
354
+ (local $hoff i32) (local $noff i32)
288
355
  (local $hsso i32) (local $nsso i32) (local $hb i32) (local $nb i32) (local $k i32)
289
356
  (local.set $hlen (call $__str_byteLen (local.get $hay)))
290
357
  (local.set $nlen (call $__str_byteLen (local.get $ndl)))
291
358
  (if (i32.eqz (local.get $nlen)) (then (return (local.get $from))))
292
359
  (if (i32.gt_s (local.get $nlen) (local.get $hlen)) (then (return (i32.const -1))))
293
- (local.set $hbits (i64.reinterpret_f64 (local.get $hay)))
294
- (local.set $nbits (i64.reinterpret_f64 (local.get $ndl)))
295
- (local.set $hoff (i32.wrap_i64 (i64.and (local.get $hbits) (i64.const 0xFFFFFFFF))))
296
- (local.set $noff (i32.wrap_i64 (i64.and (local.get $nbits) (i64.const 0xFFFFFFFF))))
297
- (local.set $hsso (i32.eq
298
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $hbits) (i64.const 47)) (i64.const 0xF)))
299
- (i32.const ${PTR.SSO})))
300
- (local.set $nsso (i32.eq
301
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $nbits) (i64.const 47)) (i64.const 0xF)))
302
- (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})))
303
368
  (local.set $i (if (result i32) (i32.gt_s (local.get $from) (i32.const 0)) (then (local.get $from)) (else (i32.const 0))))
304
369
  (block $done (loop $outer
305
370
  (br_if $done (i32.gt_s (local.get $i) (i32.sub (local.get $hlen) (local.get $nlen))))
@@ -324,22 +389,20 @@ export default (ctx) => {
324
389
  (i32.const -1))`
325
390
 
326
391
  // SSO/heap dispatch hoisted; inner loop is two inlined byte-fetches and a compare.
327
- 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)
328
393
  (local $plen i32) (local $i i32)
329
- (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)
330
395
  (local.set $plen (call $__str_byteLen (local.get $pfx)))
331
396
  (if (i32.gt_s (local.get $plen) (call $__str_byteLen (local.get $str)))
332
397
  (then (return (i32.const 0))))
333
- (local.set $sbits (i64.reinterpret_f64 (local.get $str)))
334
- (local.set $pbits (i64.reinterpret_f64 (local.get $pfx)))
335
- (local.set $soff (i32.wrap_i64 (i64.and (local.get $sbits) (i64.const 0xFFFFFFFF))))
336
- (local.set $poff (i32.wrap_i64 (i64.and (local.get $pbits) (i64.const 0xFFFFFFFF))))
337
- (local.set $ssso (i32.eq
338
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $sbits) (i64.const 47)) (i64.const 0xF)))
339
- (i32.const ${PTR.SSO})))
340
- (local.set $psso (i32.eq
341
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $pbits) (i64.const 47)) (i64.const 0xF)))
342
- (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})))
343
406
  (block $done (loop $loop
344
407
  (br_if $done (i32.ge_s (local.get $i) (local.get $plen)))
345
408
  (if (i32.ne
@@ -354,24 +417,22 @@ export default (ctx) => {
354
417
  (br $loop)))
355
418
  (i32.const 1))`
356
419
 
357
- 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)
358
421
  (local $slen i32) (local $flen i32) (local $off i32) (local $i i32) (local $k i32)
359
- (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)
360
423
  (local.set $slen (call $__str_byteLen (local.get $str)))
361
424
  (local.set $flen (call $__str_byteLen (local.get $sfx)))
362
425
  (if (i32.gt_s (local.get $flen) (local.get $slen))
363
426
  (then (return (i32.const 0))))
364
427
  (local.set $off (i32.sub (local.get $slen) (local.get $flen)))
365
- (local.set $sbits (i64.reinterpret_f64 (local.get $str)))
366
- (local.set $fbits (i64.reinterpret_f64 (local.get $sfx)))
367
- (local.set $soff (i32.wrap_i64 (i64.and (local.get $sbits) (i64.const 0xFFFFFFFF))))
368
- (local.set $foff (i32.wrap_i64 (i64.and (local.get $fbits) (i64.const 0xFFFFFFFF))))
369
- (local.set $ssso (i32.eq
370
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $sbits) (i64.const 47)) (i64.const 0xF)))
371
- (i32.const ${PTR.SSO})))
372
- (local.set $fsso (i32.eq
373
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $fbits) (i64.const 47)) (i64.const 0xF)))
374
- (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})))
375
436
  (block $done (loop $loop
376
437
  (br_if $done (i32.ge_s (local.get $i) (local.get $flen)))
377
438
  (local.set $k (i32.add (local.get $off) (local.get $i)))
@@ -388,20 +449,17 @@ export default (ctx) => {
388
449
  (i32.const 1))`
389
450
 
390
451
  // Source SSO/heap dispatch hoisted out of the byte loop (was a per-byte __char_at).
391
- 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)
392
453
  (local $len i32) (local $off i32) (local $i i32) (local $c i32)
393
- (local $bits i64) (local $srcOff i32)
454
+ (local $srcOff i32)
394
455
  (local.set $len (call $__str_byteLen (local.get $ptr)))
395
456
  (if (i32.eqz (local.get $len))
396
- (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)))))
397
458
  (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $len))))
398
459
  (i32.store (local.get $off) (local.get $len))
399
460
  (local.set $off (i32.add (local.get $off) (i32.const 4)))
400
- (local.set $bits (i64.reinterpret_f64 (local.get $ptr)))
401
- (local.set $srcOff (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
402
- (if (i32.eq
403
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF)))
404
- (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))
405
463
  (then
406
464
  (block $dsso (loop $lsso
407
465
  (br_if $dsso (i32.ge_s (local.get $i) (local.get $len)))
@@ -424,7 +482,7 @@ export default (ctx) => {
424
482
  (br $lh)))))
425
483
  (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
426
484
 
427
- 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)
428
486
  (local $len i32) (local $start i32) (local $end i32)
429
487
  (local.set $len (call $__str_byteLen (local.get $ptr)))
430
488
  (local.set $start (i32.const 0))
@@ -441,7 +499,7 @@ export default (ctx) => {
441
499
  (br $l2)))
442
500
  (call $__str_slice (local.get $ptr) (local.get $start) (local.get $end)))`
443
501
 
444
- 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)
445
503
  (local $len i32) (local $start i32)
446
504
  (local.set $len (call $__str_byteLen (local.get $ptr)))
447
505
  (local.set $start (i32.const 0))
@@ -452,7 +510,7 @@ export default (ctx) => {
452
510
  (br $loop)))
453
511
  (call $__str_slice (local.get $ptr) (local.get $start) (local.get $len)))`
454
512
 
455
- 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)
456
514
  (local $len i32) (local $end i32)
457
515
  (local.set $len (call $__str_byteLen (local.get $ptr)))
458
516
  (local.set $end (local.get $len))
@@ -465,11 +523,11 @@ export default (ctx) => {
465
523
 
466
524
  // Materialize source bytes once via __str_copy (handles SSO/heap), then memory.copy
467
525
  // each subsequent repetition (single bulk op vs len byte stores per copy).
468
- 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)
469
527
  (local $len i32) (local $total i32) (local $off i32) (local $i i32)
470
528
  (local.set $len (call $__str_byteLen (local.get $ptr)))
471
529
  (if (i32.or (i32.eqz (local.get $n)) (i32.eqz (local.get $len)))
472
- (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)))))
473
531
  (local.set $total (i32.mul (local.get $len) (local.get $n)))
474
532
  (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $total))))
475
533
  (i32.store (local.get $off) (local.get $total))
@@ -486,34 +544,37 @@ export default (ctx) => {
486
544
  (br $loop)))
487
545
  (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
488
546
 
489
- // Coerce value to string: numbers → __ftoa, plain NaN "NaN", arrays → join(","), other pointers pass through
490
- ctx.core.stdlib['__to_str'] = `(func $__to_str (param $val f64) (result f64)
491
- (local $type i32)
547
+ // Coerce value to string: numbers → __ftoa, nullishstatic strings,
548
+ // plain NaN "NaN", arrays join(","), other string-like pointers pass through.
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)))
492
552
  ;; Not NaN → number, convert
493
- (if (f64.eq (local.get $val) (local.get $val))
494
- (then (return (call $__ftoa (local.get $val) (i32.const 0) (i32.const 0)))))
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))))))
495
559
  (local.set $type (call $__ptr_type (local.get $val)))
496
560
  ;; Plain NaN (type=0) → "NaN" string
497
561
  (if (i32.eqz (local.get $type))
498
- (then (return (call $__static_str (i32.const 0)))))
562
+ (then (return (i64.reinterpret_f64 (call $__static_str (i32.const 0))))))
499
563
  ;; Array (type=1) → join(",") like JS Array.toString()
500
564
  (if (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
501
- (then (return (call $__str_join (local.get $val)
502
- (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))))))))
503
567
  (local.get $val))`
504
568
 
505
569
  // Copy bytes of a string (SSO or heap) into memory at dst. Uses memory.copy for
506
- // heap strings (single native op); unpacks SSO aux-packed bytes inline.
507
- ctx.core.stdlib['__str_copy'] = `(func $__str_copy (param $src f64) (param $dst i32) (param $len i32)
508
- (local $bits i64) (local $w i32)
509
- (local.set $bits (i64.reinterpret_f64 (local.get $src)))
510
- (if (i32.eq
511
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF)))
512
- (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))
513
574
  (then
514
575
  ;; SSO: up to 4 chars packed in low 32 bits (LE byte order). Unroll: write 1/2/3/4 bytes
515
576
  ;; depending on len. (len > 4 is rare/disallowed in practice — fallback handles up to 4.)
516
- (local.set $w (i32.wrap_i64 (local.get $bits)))
577
+ (local.set $w (i32.wrap_i64 (local.get $src)))
517
578
  (if (i32.ge_u (local.get $len) (i32.const 4))
518
579
  (then (i32.store (local.get $dst) (local.get $w)))
519
580
  (else
@@ -526,11 +587,107 @@ export default (ctx) => {
526
587
  (else
527
588
  ;; Heap STRING: memory.copy directly from string data
528
589
  (memory.copy (local.get $dst)
529
- (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})))
530
591
  (local.get $len)))))`
531
592
 
532
- 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)
533
689
  (local $alen i32) (local $blen i32) (local $total i32) (local $off i32)
690
+ (local $ta i32) (local $aoff i32) (local $newHeap i32)
534
691
  ;; Coerce operands to strings if needed
535
692
  (local.set $a (call $__to_str (local.get $a)))
536
693
  (local.set $b (call $__to_str (local.get $b)))
@@ -538,7 +695,8 @@ export default (ctx) => {
538
695
  (local.set $blen (call $__str_byteLen (local.get $b)))
539
696
  (local.set $total (i32.add (local.get $alen) (local.get $blen)))
540
697
  (if (i32.eqz (local.get $total))
541
- (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}
542
700
  (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $total))))
543
701
  (i32.store (local.get $off) (local.get $total))
544
702
  (local.set $off (i32.add (local.get $off) (i32.const 4)))
@@ -546,13 +704,15 @@ export default (ctx) => {
546
704
  (call $__str_copy (local.get $b) (i32.add (local.get $off) (local.get $alen)) (local.get $blen))
547
705
  (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
548
706
 
549
- 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)
550
708
  (local $alen i32) (local $blen i32) (local $total i32) (local $off i32)
709
+ (local $ta i32) (local $aoff i32) (local $newHeap i32)
551
710
  (local.set $alen (call $__str_byteLen (local.get $a)))
552
711
  (local.set $blen (call $__str_byteLen (local.get $b)))
553
712
  (local.set $total (i32.add (local.get $alen) (local.get $blen)))
554
713
  (if (i32.eqz (local.get $total))
555
- (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}
556
716
  (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $total))))
557
717
  (i32.store (local.get $off) (local.get $total))
558
718
  (local.set $off (i32.add (local.get $off) (i32.const 4)))
@@ -560,39 +720,39 @@ export default (ctx) => {
560
720
  (call $__str_copy (local.get $b) (i32.add (local.get $off) (local.get $alen)) (local.get $blen))
561
721
  (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
562
722
 
563
- 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)
564
724
  (local $idx i32) (local $slen i32)
565
725
  (local.set $idx (call $__str_indexof (local.get $str) (local.get $search) (i32.const 0)))
566
726
  (if (result f64) (i32.lt_s (local.get $idx) (i32.const 0))
567
- (then (local.get $str))
727
+ (then (f64.reinterpret_i64 (local.get $str)))
568
728
  (else
569
729
  (local.set $slen (call $__str_byteLen (local.get $search)))
570
730
  (call $__str_concat
571
- (call $__str_concat
572
- (call $__str_slice (local.get $str) (i32.const 0) (local.get $idx))
573
- (local.get $repl))
574
- (call $__str_slice (local.get $str) (i32.add (local.get $idx) (local.get $slen))
575
- (call $__str_byteLen (local.get $str)))))))`
576
-
577
- ctx.core.stdlib['__str_replaceall'] = `(func $__str_replaceall (param $str f64) (param $search f64) (param $repl f64) (result f64)
578
- (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)
579
739
  (local.set $slen (call $__str_byteLen (local.get $search)))
580
740
  (local.set $result (local.get $str))
581
741
  (local.set $pos (i32.const 0))
582
742
  (block $done (loop $next
583
743
  (local.set $idx (call $__str_indexof (local.get $result) (local.get $search) (local.get $pos)))
584
744
  (br_if $done (i32.lt_s (local.get $idx) (i32.const 0)))
585
- (local.set $result (call $__str_concat
586
- (call $__str_concat
587
- (call $__str_slice (local.get $result) (i32.const 0) (local.get $idx))
588
- (local.get $repl))
589
- (call $__str_slice (local.get $result) (i32.add (local.get $idx) (local.get $slen))
590
- (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)))))))
591
751
  (local.set $pos (i32.add (local.get $idx) (call $__str_byteLen (local.get $repl))))
592
752
  (br $next)))
593
- (local.get $result))`
753
+ (f64.reinterpret_i64 (local.get $result)))`
594
754
 
595
- 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)
596
756
  (local $slen i32) (local $plen i32) (local $count i32)
597
757
  (local $i i32) (local $j i32) (local $match i32)
598
758
  (local $arr i32) (local $piece_start i32) (local $piece_idx i32)
@@ -648,32 +808,32 @@ export default (ctx) => {
648
808
  (call $__str_slice (local.get $str) (local.get $piece_start) (local.get $slen)))
649
809
  (call $__mkptr (i32.const 1) (i32.const 0) (local.get $arr)))`
650
810
 
651
- 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)
652
812
  (local $off i32) (local $len i32) (local $i i32) (local $result f64)
653
813
  (local.set $off (call $__ptr_offset (local.get $arr)))
654
814
  (local.set $len (call $__len (local.get $arr)))
655
815
  (if (i32.eqz (local.get $len))
656
- (then (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 0) (i32.const 0)))))
657
- (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)))))
658
818
  (local.set $i (i32.const 1))
659
819
  (block $done (loop $loop
660
820
  (br_if $done (i32.ge_s (local.get $i) (local.get $len)))
661
- (local.set $result (call $__str_concat (local.get $result) (local.get $sep)))
662
- (local.set $result (call $__str_concat (local.get $result)
663
- (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))))))
664
824
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
665
825
  (br $loop)))
666
826
  (local.get $result))`
667
827
 
668
828
  // Source string copied via __str_copy (handles SSO/heap with memory.copy where possible).
669
829
  // Pad fill loops a single tile of pad bytes — hoist pad dispatch out of the byte loop.
670
- 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)
671
831
  (local $slen i32) (local $plen i32) (local $fill i32) (local $off i32) (local $i i32)
672
832
  (local $str_off i32) (local $pad_off i32)
673
833
  (local $pbits i64) (local $poff i32) (local $psso i32)
674
834
  (local.set $slen (call $__str_byteLen (local.get $str)))
675
835
  (if (i32.ge_s (local.get $slen) (local.get $target))
676
- (then (return (local.get $str))))
836
+ (then (return (f64.reinterpret_i64 (local.get $str)))))
677
837
  (local.set $plen (call $__str_byteLen (local.get $pad)))
678
838
  (local.set $fill (i32.sub (local.get $target) (local.get $slen)))
679
839
  (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $target))))
@@ -682,11 +842,11 @@ export default (ctx) => {
682
842
  (local.set $str_off (select (local.get $fill) (i32.const 0) (local.get $before)))
683
843
  (local.set $pad_off (select (i32.const 0) (local.get $slen) (local.get $before)))
684
844
  (call $__str_copy (local.get $str) (i32.add (local.get $off) (local.get $str_off)) (local.get $slen))
685
- (local.set $pbits (i64.reinterpret_f64 (local.get $pad)))
686
- (local.set $poff (i32.wrap_i64 (i64.and (local.get $pbits) (i64.const 0xFFFFFFFF))))
687
- (local.set $psso (i32.eq
688
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $pbits) (i64.const 47)) (i64.const 0xF)))
689
- (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})))
690
850
  (block $d2 (loop $l2
691
851
  (br_if $d2 (i32.ge_s (local.get $i) (local.get $fill)))
692
852
  (i32.store8 (i32.add (local.get $off) (i32.add (local.get $pad_off) (local.get $i)))
@@ -706,36 +866,51 @@ export default (ctx) => {
706
866
  // === Method emitters ===
707
867
 
708
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
+
709
884
  ctx.core.emit['.string:slice'] = (str, start, end) => {
710
885
  inc('__str_slice')
711
- 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')
712
887
  const t = temp('t')
713
888
  return typed(['block', ['result', 'f64'],
714
889
  ['local.set', `$${t}`, asF64(emit(str))],
715
- ['call', '$__str_slice', ['local.get', `$${t}`], asI32(emit(start)),
716
- ['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')
717
892
  }
718
893
 
719
894
  ctx.core.emit['.string:indexOf'] = (str, search, from) => {
720
895
  inc('__str_indexof')
721
- 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')
722
897
  }
723
898
 
724
899
  ctx.core.emit['.string:includes'] = (str, search) => {
725
900
  inc('__str_indexof')
726
901
  return typed(['f64.convert_i32_s',
727
- ['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')
728
903
  }
729
904
 
730
905
  // Generic (no collision)
731
906
  ctx.core.emit['.substring'] = (str, start, end) => {
732
907
  inc('__str_substring')
733
- 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')
734
909
  const t = temp('t')
735
910
  return typed(['block', ['result', 'f64'],
736
911
  ['local.set', `$${t}`, asF64(emit(str))],
737
- ['call', '$__str_substring', ['local.get', `$${t}`], asI32(emit(start)),
738
- ['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')
739
914
  }
740
915
 
741
916
  // Factory for simple str→call patterns: [emitKey, stdlibName, argCoercions, i32Result?]
@@ -746,44 +921,109 @@ export default (ctx) => {
746
921
  return typed(i32Result ? ['f64.convert_i32_s', call] : call, 'f64')
747
922
  }
748
923
 
749
- // Simple str methods: [emitKey, stdlibName, argCoercions, i32Result?]
750
- ctx.core.emit['.startsWith'] = strMethod('__str_startswith', ['f'], true)
751
- ctx.core.emit['.endsWith'] = strMethod('__str_endswith', ['f'], true)
752
- ctx.core.emit['.trim'] = strMethod('__str_trim', [])
753
- ctx.core.emit['.trimStart'] = strMethod('__str_trimStart', [])
754
- ctx.core.emit['.trimEnd'] = strMethod('__str_trimEnd', [])
755
- ctx.core.emit['.repeat'] = strMethod('__str_repeat', ['i'])
756
- ctx.core.emit['.split'] = strMethod('__str_split', ['f'])
757
- ctx.core.emit['.replace'] = strMethod('__str_replace', ['f', 'f'])
758
- 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'))
759
951
 
760
952
  ctx.core.emit['.toUpperCase'] = (str) => {
761
953
  inc('__str_case')
762
- 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')
763
955
  }
764
956
 
765
957
  ctx.core.emit['.toLowerCase'] = (str) => {
766
958
  inc('__str_case')
767
- 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')
768
972
  }
769
973
 
770
974
  ctx.core.emit['.string:concat'] = (str, ...others) => {
771
975
  inc('__str_concat')
772
976
  let result = asF64(emit(str))
773
- 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')
774
978
  return result
775
979
  }
776
980
 
981
+ ctx.core.emit['strcat'] = (...parts) => {
982
+ inc('__to_str', '__str_byteLen', '__alloc', '__mkptr', '__str_copy')
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')
985
+
986
+ const vals = parts.map(() => temp('s'))
987
+ const lens = parts.map(() => tempI32('sl'))
988
+ const total = tempI32('st')
989
+ const off = tempI32('so')
990
+ const dst = tempI32('sd')
991
+ const ir = []
992
+
993
+ for (let i = 0; i < parts.length; 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]}`]]]])
996
+ }
997
+ ir.push(['local.set', `$${total}`, ['i32.const', 0]])
998
+ for (const len of lens)
999
+ ir.push(['local.set', `$${total}`, ['i32.add', ['local.get', `$${total}`], ['local.get', `$${len}`]]])
1000
+ const alloc = [
1001
+ ['local.set', `$${off}`, ['call', '$__alloc', ['i32.add', ['i32.const', 4], ['local.get', `$${total}`]]]],
1002
+ ['i32.store', ['local.get', `$${off}`], ['local.get', `$${total}`]],
1003
+ ['local.set', `$${off}`, ['i32.add', ['local.get', `$${off}`], ['i32.const', 4]]],
1004
+ ['local.set', `$${dst}`, ['local.get', `$${off}`]],
1005
+ ]
1006
+ for (let i = 0; i < parts.length; i++) {
1007
+ alloc.push(['call', '$__str_copy', ['i64.reinterpret_f64', ['local.get', `$${vals[i]}`]], ['local.get', `$${dst}`], ['local.get', `$${lens[i]}`]])
1008
+ alloc.push(['local.set', `$${dst}`, ['i32.add', ['local.get', `$${dst}`], ['local.get', `$${lens[i]}`]]])
1009
+ }
1010
+ alloc.push(['call', '$__mkptr', ['i32.const', PTR.STRING], ['i32.const', 0], ['local.get', `$${off}`]])
1011
+ ir.push(['if', ['result', 'f64'], ['i32.eqz', ['local.get', `$${total}`]],
1012
+ ['then', mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT, 0)],
1013
+ ['else', ['block', ['result', 'f64'], ...alloc]]])
1014
+ return typed(['block', ['result', 'f64'], ...ir], 'f64')
1015
+ }
1016
+
777
1017
  ctx.core.emit['.padStart'] = (str, len, pad) => {
778
1018
  inc('__str_pad')
779
- const vpad = pad != null ? asF64(emit(pad)) : mkPtrIR(PTR.SSO, 1, 32)
780
- 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')
781
1021
  }
782
1022
 
783
1023
  ctx.core.emit['.padEnd'] = (str, len, pad) => {
784
1024
  inc('__str_pad')
785
- const vpad = pad != null ? asF64(emit(pad)) : mkPtrIR(PTR.SSO, 1, 32)
786
- 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')
787
1027
  }
788
1028
 
789
1029
  // .charAt(i) → 1-char string from char code at index i
@@ -792,8 +1032,8 @@ export default (ctx) => {
792
1032
  const t = tempI32('ch')
793
1033
  // Get char code, create SSO string with 1 byte
794
1034
  return typed(['block', ['result', 'f64'],
795
- ['local.set', `$${t}`, ['call', '$__char_at', asF64(emit(str)), asI32(emit(idx))]],
796
- 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')
797
1037
  }
798
1038
 
799
1039
  // .charCodeAt(i) → integer char code (0..255 for ASCII bytes — unsigned, always
@@ -802,33 +1042,47 @@ export default (ctx) => {
802
1042
  // `c - 48` arithmetic skip the per-iteration f64 widen + i32 trunc round-trip.
803
1043
  ctx.core.emit['.charCodeAt'] = (str, idx) => {
804
1044
  inc('__char_at')
805
- 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')
806
1046
  }
807
1047
 
808
1048
  // String.fromCharCode(code) → 1-char SSO string
809
- ctx.core.emit['String.fromCharCode'] = (code) => mkPtrIR(PTR.SSO, 1, asI32(emit(code)))
1049
+ ctx.core.emit['String'] = (value) => {
1050
+ if (value === undefined) return emit(['str', ''])
1051
+ if (valTypeOf(value) === VAL.STRING) return emit(value)
1052
+ if (valTypeOf(value) === VAL.NUMBER) {
1053
+ inc('__ftoa')
1054
+ return typed(['call', '$__ftoa', asF64(emit(value)), ['i32.const', 0], ['i32.const', 0]], 'f64')
1055
+ }
1056
+ inc('__to_str')
1057
+ return typed(['f64.reinterpret_i64', ['call', '$__to_str', asI64(emit(value))]], 'f64')
1058
+ }
1059
+
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
+ }
810
1064
 
811
1065
  // String.fromCodePoint(cp) → UTF-8 encoded string
812
1066
  ctx.core.stdlib['__fromCodePoint'] = `(func $__fromCodePoint (param $cp i32) (result f64)
813
1067
  (local $off i32) (local $len i32)
814
1068
  ;; ASCII: 1 byte SSO
815
1069
  (if (i32.lt_u (local.get $cp) (i32.const 128))
816
- (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)))))
817
1071
  ;; 2-byte: 0x80-0x7FF → SSO
818
1072
  (if (i32.lt_u (local.get $cp) (i32.const 0x800))
819
- (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})
820
1074
  (i32.or
821
1075
  (i32.or (i32.const 0xC0) (i32.shr_u (local.get $cp) (i32.const 6)))
822
1076
  (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 8)))))))
823
1077
  ;; 3-byte: 0x800-0xFFFF → SSO (3 bytes fits)
824
1078
  (if (i32.lt_u (local.get $cp) (i32.const 0x10000))
825
- (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})
826
1080
  (i32.or (i32.or
827
1081
  (i32.or (i32.const 0xE0) (i32.shr_u (local.get $cp) (i32.const 12)))
828
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)))
829
1083
  (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 16)))))))
830
1084
  ;; 4-byte: 0x10000-0x10FFFF → SSO (4 bytes fits)
831
- (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 4)
1085
+ (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT | 4})
832
1086
  (i32.or (i32.or (i32.or
833
1087
  (i32.or (i32.const 0xF0) (i32.shr_u (local.get $cp) (i32.const 18)))
834
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)))
@@ -836,12 +1090,13 @@ export default (ctx) => {
836
1090
  (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 24))))))`
837
1091
 
838
1092
  ctx.core.emit['String.fromCodePoint'] = (code) => {
1093
+ if (code === undefined) return emit(['str', ''])
839
1094
  inc('__fromCodePoint')
840
1095
  return typed(['call', '$__fromCodePoint', asI32(emit(code))], 'f64')
841
1096
  }
842
1097
 
843
1098
  // .at(i) → charAt with negative index support
844
- ctx.core.emit['.at'] = (str, idx) => {
1099
+ ctx.core.emit['.string:at'] = (str, idx) => {
845
1100
  inc('__char_at', '__str_byteLen')
846
1101
  const t = tempI32('at'), s = temp('as')
847
1102
  return typed(['block', ['result', 'f64'],
@@ -850,14 +1105,14 @@ export default (ctx) => {
850
1105
  // Negative index: t += length
851
1106
  ['if', ['i32.lt_s', ['local.get', `$${t}`], ['i32.const', 0]],
852
1107
  ['then', ['local.set', `$${t}`, ['i32.add', ['local.get', `$${t}`],
853
- ['call', '$__str_byteLen', ['local.get', `$${s}`]]]]]],
854
- 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')
855
1110
  }
856
1111
 
857
1112
  // .search(str) → indexOf (same as indexOf for string args)
858
1113
  ctx.core.emit['.search'] = (str, search) => {
859
1114
  inc('__str_indexof')
860
- 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')
861
1116
  }
862
1117
 
863
1118
  // .match(str) → [match] array if found, or 0 (null) if not
@@ -869,24 +1124,25 @@ export default (ctx) => {
869
1124
  return typed(['block', ['result', 'f64'],
870
1125
  ['local.set', `$${s}`, asF64(emit(str))],
871
1126
  ['local.set', `$${q}`, asF64(emit(search))],
872
- ['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]]],
873
1128
  ['if', ['result', 'f64'], ['i32.lt_s', ['local.get', `$${idx}`], ['i32.const', 0]],
874
1129
  ['then', ['f64.const', 0]], // null
875
1130
  ['else',
876
1131
  // Build 1-element array containing the search string
877
1132
  ['call', '$__wrap1',
878
- ['call', '$__str_slice', ['local.get', `$${s}`],
879
- ['local.get', `$${idx}`],
880
- ['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')
881
1137
  }
882
1138
 
883
- // __wrap1(val: f64) → f64 — create 1-element array [val]
884
- 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)
885
1141
  (local $ptr i32)
886
1142
  (local.set $ptr (call $__alloc (i32.const 16)))
887
1143
  (i32.store (local.get $ptr) (i32.const 1))
888
1144
  (i32.store (i32.add (local.get $ptr) (i32.const 4)) (i32.const 1))
889
- (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))
890
1146
  (call $__mkptr (i32.const 1) (i32.const 0) (i32.add (local.get $ptr) (i32.const 8))))`
891
1147
 
892
1148
  // TextEncoder() / TextDecoder() → dummy values (methods do the work)
@@ -895,7 +1151,7 @@ export default (ctx) => {
895
1151
 
896
1152
  // .encode(str) → Uint8Array of string's UTF-8 bytes
897
1153
  // Copies bytes from string (SSO or heap) into a new Uint8Array
898
- 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)
899
1155
  (local $len i32) (local $dst i32)
900
1156
  (local.set $len (call $__str_byteLen (local.get $str)))
901
1157
  (local.set $dst (call $__alloc (i32.add (i32.const 8) (local.get $len))))
@@ -907,11 +1163,11 @@ export default (ctx) => {
907
1163
 
908
1164
  ctx.core.emit['.encode'] = (obj, str) => {
909
1165
  inc('__str_encode')
910
- return typed(['call', '$__str_encode', asF64(emit(str))], 'f64')
1166
+ return typed(['call', '$__str_encode', asI64(emit(str))], 'f64')
911
1167
  }
912
1168
 
913
1169
  // .decode(uint8arr) → string from byte data
914
- 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)
915
1171
  (local $off i32) (local $len i32) (local $dst i32)
916
1172
  (local.set $off (call $__ptr_offset (local.get $arr)))
917
1173
  (local.set $len (call $__len (local.get $arr)))
@@ -923,6 +1179,6 @@ export default (ctx) => {
923
1179
 
924
1180
  ctx.core.emit['.decode'] = (obj, arr) => {
925
1181
  inc('__bytes_decode')
926
- return typed(['call', '$__bytes_decode', asF64(emit(arr))], 'f64')
1182
+ return typed(['call', '$__bytes_decode', asI64(emit(arr))], 'f64')
927
1183
  }
928
1184
  }