jz 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,985 @@
1
+ /**
2
+ * String module — literals, char access, and string methods.
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).
6
+ *
7
+ * Methods use type-qualified keys (.string:slice) for array-colliding names,
8
+ * generic keys (.toUpperCase) for non-colliding ones.
9
+ *
10
+ * @module string
11
+ */
12
+
13
+ import { typed, asF64, asI32, NULL_NAN, UNDEF_NAN, mkPtrIR, temp, tempI32 } from '../src/ir.js'
14
+ import { emit } from '../src/emit.js'
15
+ import { valTypeOf, VAL } from '../src/analyze.js'
16
+ import { inc, PTR } from '../src/ctx.js'
17
+
18
+
19
+ export default (ctx) => {
20
+ Object.assign(ctx.core.stdlibDeps, {
21
+ __str_concat: ['__to_str', '__str_byteLen', '__alloc', '__mkptr', '__str_copy'],
22
+ __str_concat_raw: ['__str_byteLen', '__alloc', '__mkptr', '__str_copy'],
23
+ __str_copy: [],
24
+ __str_slice: ['__str_byteLen', '__alloc'],
25
+ __str_indexof: ['__str_byteLen'],
26
+ __str_substring: ['__str_slice'],
27
+ __str_startswith: ['__str_byteLen'],
28
+ __str_endswith: ['__str_byteLen'],
29
+ __str_case: ['__str_byteLen', '__alloc'],
30
+ __str_trim: ['__str_slice', '__str_byteLen', '__char_at'],
31
+ __str_trimStart: ['__str_slice', '__str_byteLen', '__char_at'],
32
+ __str_trimEnd: ['__str_slice', '__str_byteLen', '__char_at'],
33
+ __str_repeat: ['__str_byteLen', '__str_copy', '__alloc'],
34
+ __str_replace: ['__str_indexof', '__str_slice', '__str_concat'],
35
+ __str_replaceall: ['__str_indexof', '__str_slice', '__str_concat'],
36
+ __str_split: ['__str_slice', '__str_byteLen', '__char_at', '__alloc'],
37
+ __str_idx: ['__str_byteLen', '__char_at', '__mkptr'],
38
+ __str_eq: ['__char_at'],
39
+ __str_pad: ['__str_byteLen', '__str_copy', '__alloc'],
40
+ __str_join: ['__str_concat', '__to_str', '__str_byteLen', '__len', '__ptr_offset'],
41
+ __str_encode: ['__str_byteLen', '__str_copy'],
42
+ __to_str: ['__ftoa', '__static_str', '__str_join', '__mkptr'],
43
+ __str_byteLen: ['__ptr_type', '__ptr_aux', '__str_len'],
44
+ })
45
+
46
+ inc('__mkptr', '__alloc')
47
+
48
+ // === String literal: "abc" → SSO if ≤4 ASCII, else heap ===
49
+
50
+ ctx.core.emit['str'] = (str) => {
51
+ const MAX_SSO = 4
52
+ if (ctx.features.sso && str.length <= MAX_SSO && /^[\x00-\x7f]*$/.test(str)) {
53
+ let packed = 0
54
+ for (let i = 0; i < str.length; i++) packed |= str.charCodeAt(i) << (i * 8)
55
+ return mkPtrIR(PTR.SSO, str.length, packed)
56
+ }
57
+ const bytes = new TextEncoder().encode(str)
58
+ const len = bytes.length
59
+ if (!ctx.memory.shared) {
60
+ // Own memory: place in static data segment (no runtime allocation)
61
+ if (!ctx.runtime.data) ctx.runtime.data = ''
62
+ const prior = ctx.runtime.dataDedup.get(str)
63
+ if (prior !== undefined) return mkPtrIR(PTR.STRING, 0, prior + 4)
64
+ while (ctx.runtime.data.length % 4 !== 0) ctx.runtime.data += '\0'
65
+ const offset = ctx.runtime.data.length
66
+ ctx.runtime.data += String.fromCharCode(len & 0xFF, (len >> 8) & 0xFF, (len >> 16) & 0xFF, (len >> 24) & 0xFF)
67
+ for (let i = 0; i < len; i++) ctx.runtime.data += String.fromCharCode(bytes[i])
68
+ ctx.runtime.dataDedup.set(str, offset)
69
+ return mkPtrIR(PTR.STRING, 0, offset + 4)
70
+ }
71
+ // Shared memory: pack all string literals into one passive data segment with 4-byte
72
+ // length prefixes. At __start, alloc the whole pool once and memory.init it in a single
73
+ // call. Each use site resolves to `strBase + compile-time-offset` — O(1) IR nodes per
74
+ // use, independent of string length AND reused across uses.
75
+ if (!ctx.runtime.strPool) {
76
+ ctx.runtime.strPool = ''
77
+ ctx.scope.globals.set('__strBase', '(global $__strBase (mut i32) (i32.const 0))')
78
+ }
79
+ let off = ctx.runtime.strPoolDedup.get(str)
80
+ if (off === undefined) {
81
+ // Pack length header then UTF-8 bytes; offset points PAST the length (at the data).
82
+ ctx.runtime.strPool += String.fromCharCode(len & 0xFF, (len >> 8) & 0xFF, (len >> 16) & 0xFF, (len >> 24) & 0xFF)
83
+ off = ctx.runtime.strPool.length
84
+ for (let i = 0; i < len; i++) ctx.runtime.strPool += String.fromCharCode(bytes[i])
85
+ ctx.runtime.strPoolDedup.set(str, off)
86
+ }
87
+ return mkPtrIR(PTR.STRING, 0, ['i32.add', ['global.get', '$__strBase'], ['i32.const', off]])
88
+ }
89
+
90
+ // === WAT: char extraction ===
91
+
92
+ // SSO/STRING ptrs never have forwarding pointers (only ARRAY does), so we extract
93
+ // 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)
95
+ (i32.and
96
+ (i32.shr_u
97
+ (i32.wrap_i64 (i64.and (i64.reinterpret_f64 (local.get $ptr)) (i64.const 0xFFFFFFFF)))
98
+ (i32.mul (local.get $i) (i32.const 8)))
99
+ (i32.const 0xFF)))`
100
+
101
+ ctx.core.stdlib['__str_char'] = `(func $__str_char (param $ptr f64) (param $i i32) (result i32)
102
+ (i32.load8_u (i32.add
103
+ (i32.wrap_i64 (i64.and (i64.reinterpret_f64 (local.get $ptr)) (i64.const 0xFFFFFFFF)))
104
+ (local.get $i))))`
105
+
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))))
112
+ (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}))
116
+ (then
117
+ (i32.and
118
+ (i32.shr_u (local.get $off) (i32.mul (local.get $i) (i32.const 8)))
119
+ (i32.const 0xFF)))
120
+ (else
121
+ (i32.load8_u (i32.add (local.get $off) (local.get $i))))))`
122
+
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)))
126
+ (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)))
130
+ (then (f64.const nan:${UNDEF_NAN}))
131
+ (else
132
+ (call $__mkptr
133
+ (i32.const ${PTR.SSO})
134
+ (i32.const 1)
135
+ (call $__char_at (local.get $ptr) (local.get $i))))))`
136
+
137
+ // Hot: ~53M calls in watr self-host. Bit-eq covers identity. SSO/SSO with !bit-eq
138
+ // 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)
142
+ (local $len i32) (local $lenB i32) (local $i i32)
143
+ (local $ba i64) (local $bb i64) (local $ta i32) (local $tb i32)
144
+ (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))
148
+ (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})))
155
+ (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})))
161
+ (then
162
+ (if (i32.or (i32.lt_u (local.get $offA) (i32.const 4)) (i32.lt_u (local.get $offB) (i32.const 4)))
163
+ (then (return (i32.const 0))))
164
+ (local.set $len (i32.load (i32.sub (local.get $offA) (i32.const 4))))
165
+ (local.set $lenB (i32.load (i32.sub (local.get $offB) (i32.const 4))))
166
+ (if (i32.ne (local.get $len) (local.get $lenB))
167
+ (then (return (i32.const 0))))
168
+ (local.set $lenB (i32.and (local.get $len) (i32.const -4)))
169
+ (block $d4 (loop $l4
170
+ (br_if $d4 (i32.ge_s (local.get $i) (local.get $lenB)))
171
+ (if (i32.ne
172
+ (i32.load (i32.add (local.get $offA) (local.get $i)))
173
+ (i32.load (i32.add (local.get $offB) (local.get $i))))
174
+ (then (return (i32.const 0))))
175
+ (local.set $i (i32.add (local.get $i) (i32.const 4)))
176
+ (br $l4)))
177
+ (block $dh (loop $lh
178
+ (br_if $dh (i32.ge_s (local.get $i) (local.get $len)))
179
+ (if (i32.ne
180
+ (i32.load8_u (i32.add (local.get $offA) (local.get $i)))
181
+ (i32.load8_u (i32.add (local.get $offB) (local.get $i))))
182
+ (then (return (i32.const 0))))
183
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
184
+ (br $lh)))
185
+ (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)))))
189
+ (else
190
+ (if (i32.and (i32.eq (local.get $ta) (i32.const ${PTR.STRING})) (i32.ge_u (local.get $offA) (i32.const 4)))
191
+ (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)))))
194
+ (else
195
+ (if (i32.and (i32.eq (local.get $tb) (i32.const ${PTR.STRING})) (i32.ge_u (local.get $offB) (i32.const 4)))
196
+ (then (local.set $lenB (i32.load (i32.sub (local.get $offB) (i32.const 4))))))))
197
+ (if (i32.ne (local.get $len) (local.get $lenB))
198
+ (then (return (i32.const 0))))
199
+ (block $dm (loop $lm
200
+ (br_if $dm (i32.ge_s (local.get $i) (local.get $len)))
201
+ (if (i32.ne (call $__char_at (local.get $a) (local.get $i))
202
+ (call $__char_at (local.get $b) (local.get $i)))
203
+ (then (return (i32.const 0))))
204
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
205
+ (br $lm)))
206
+ (i32.const 1))`
207
+
208
+ // === WAT: unified byte length (SSO → aux, heap → header) ===
209
+
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))))))`
222
+
223
+ // === WAT: string methods ===
224
+
225
+ // SSO source uses an unrolled byte-extract loop (len ≤ 4); heap source uses memory.copy
226
+ // (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)
228
+ (local $len i32) (local $nlen i32) (local $off i32) (local $i i32)
229
+ (local $bits i64) (local $srcOff i32) (local $isSso i32)
230
+ (local.set $len (call $__str_byteLen (local.get $ptr)))
231
+ (if (i32.lt_s (local.get $start) (i32.const 0))
232
+ (then (local.set $start (i32.add (local.get $len) (local.get $start)))))
233
+ (if (i32.lt_s (local.get $end) (i32.const 0))
234
+ (then (local.set $end (i32.add (local.get $len) (local.get $end)))))
235
+ (if (i32.lt_s (local.get $start) (i32.const 0))
236
+ (then (local.set $start (i32.const 0))))
237
+ (if (i32.gt_s (local.get $start) (local.get $len))
238
+ (then (local.set $start (local.get $len))))
239
+ (if (i32.lt_s (local.get $end) (i32.const 0))
240
+ (then (local.set $end (i32.const 0))))
241
+ (if (i32.gt_s (local.get $end) (local.get $len))
242
+ (then (local.set $end (local.get $len))))
243
+ (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)))))
245
+ (local.set $nlen (i32.sub (local.get $end) (local.get $start)))
246
+ (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $nlen))))
247
+ (i32.store (local.get $off) (local.get $nlen))
248
+ (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})))
254
+ (if (local.get $isSso)
255
+ (then
256
+ (block $done (loop $loop
257
+ (br_if $done (i32.ge_s (local.get $i) (local.get $nlen)))
258
+ (i32.store8 (i32.add (local.get $off) (local.get $i))
259
+ (i32.and (i32.shr_u (local.get $srcOff)
260
+ (i32.shl (i32.add (local.get $start) (local.get $i)) (i32.const 3)))
261
+ (i32.const 0xFF)))
262
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
263
+ (br $loop))))
264
+ (else
265
+ (memory.copy (local.get $off) (i32.add (local.get $srcOff) (local.get $start)) (local.get $nlen))))
266
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
267
+
268
+ ctx.core.stdlib['__str_substring'] = `(func $__str_substring (param $ptr f64) (param $start i32) (param $end i32) (result f64)
269
+ (local $len i32) (local $tmp i32)
270
+ (local.set $len (call $__str_byteLen (local.get $ptr)))
271
+ (if (i32.lt_s (local.get $start) (i32.const 0))
272
+ (then (local.set $start (i32.const 0))))
273
+ (if (i32.lt_s (local.get $end) (i32.const 0))
274
+ (then (local.set $end (i32.const 0))))
275
+ (if (i32.gt_s (local.get $start) (local.get $len))
276
+ (then (local.set $start (local.get $len))))
277
+ (if (i32.gt_s (local.get $end) (local.get $len))
278
+ (then (local.set $end (local.get $len))))
279
+ (if (i32.gt_s (local.get $start) (local.get $end))
280
+ (then
281
+ (local.set $tmp (local.get $start))
282
+ (local.set $start (local.get $end))
283
+ (local.set $end (local.get $tmp))))
284
+ (call $__str_slice (local.get $ptr) (local.get $start) (local.get $end)))`
285
+
286
+ // Hoist SSO/heap dispatch for hay and ndl out of the inner byte loop. Inner
287
+ // 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)
289
+ (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)
291
+ (local $hsso i32) (local $nsso i32) (local $hb i32) (local $nb i32) (local $k i32)
292
+ (local.set $hlen (call $__str_byteLen (local.get $hay)))
293
+ (local.set $nlen (call $__str_byteLen (local.get $ndl)))
294
+ (if (i32.eqz (local.get $nlen)) (then (return (local.get $from))))
295
+ (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})))
306
+ (local.set $i (if (result i32) (i32.gt_s (local.get $from) (i32.const 0)) (then (local.get $from)) (else (i32.const 0))))
307
+ (block $done (loop $outer
308
+ (br_if $done (i32.gt_s (local.get $i) (i32.sub (local.get $hlen) (local.get $nlen))))
309
+ (local.set $match (i32.const 1))
310
+ (local.set $j (i32.const 0))
311
+ (block $nomatch (loop $inner
312
+ (br_if $nomatch (i32.ge_s (local.get $j) (local.get $nlen)))
313
+ (local.set $k (i32.add (local.get $i) (local.get $j)))
314
+ (local.set $hb (if (result i32) (local.get $hsso)
315
+ (then (i32.and (i32.shr_u (local.get $hoff) (i32.shl (local.get $k) (i32.const 3))) (i32.const 0xFF)))
316
+ (else (i32.load8_u (i32.add (local.get $hoff) (local.get $k))))))
317
+ (local.set $nb (if (result i32) (local.get $nsso)
318
+ (then (i32.and (i32.shr_u (local.get $noff) (i32.shl (local.get $j) (i32.const 3))) (i32.const 0xFF)))
319
+ (else (i32.load8_u (i32.add (local.get $noff) (local.get $j))))))
320
+ (if (i32.ne (local.get $hb) (local.get $nb))
321
+ (then (local.set $match (i32.const 0)) (br $nomatch)))
322
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
323
+ (br $inner)))
324
+ (if (local.get $match) (then (return (local.get $i))))
325
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
326
+ (br $outer)))
327
+ (i32.const -1))`
328
+
329
+ // 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)
331
+ (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)
333
+ (local.set $plen (call $__str_byteLen (local.get $pfx)))
334
+ (if (i32.gt_s (local.get $plen) (call $__str_byteLen (local.get $str)))
335
+ (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})))
346
+ (block $done (loop $loop
347
+ (br_if $done (i32.ge_s (local.get $i) (local.get $plen)))
348
+ (if (i32.ne
349
+ (if (result i32) (local.get $ssso)
350
+ (then (i32.and (i32.shr_u (local.get $soff) (i32.shl (local.get $i) (i32.const 3))) (i32.const 0xFF)))
351
+ (else (i32.load8_u (i32.add (local.get $soff) (local.get $i)))))
352
+ (if (result i32) (local.get $psso)
353
+ (then (i32.and (i32.shr_u (local.get $poff) (i32.shl (local.get $i) (i32.const 3))) (i32.const 0xFF)))
354
+ (else (i32.load8_u (i32.add (local.get $poff) (local.get $i))))))
355
+ (then (return (i32.const 0))))
356
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
357
+ (br $loop)))
358
+ (i32.const 1))`
359
+
360
+ ctx.core.stdlib['__str_endswith'] = `(func $__str_endswith (param $str f64) (param $sfx f64) (result i32)
361
+ (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)
363
+ (local.set $slen (call $__str_byteLen (local.get $str)))
364
+ (local.set $flen (call $__str_byteLen (local.get $sfx)))
365
+ (if (i32.gt_s (local.get $flen) (local.get $slen))
366
+ (then (return (i32.const 0))))
367
+ (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})))
378
+ (block $done (loop $loop
379
+ (br_if $done (i32.ge_s (local.get $i) (local.get $flen)))
380
+ (local.set $k (i32.add (local.get $off) (local.get $i)))
381
+ (if (i32.ne
382
+ (if (result i32) (local.get $ssso)
383
+ (then (i32.and (i32.shr_u (local.get $soff) (i32.shl (local.get $k) (i32.const 3))) (i32.const 0xFF)))
384
+ (else (i32.load8_u (i32.add (local.get $soff) (local.get $k)))))
385
+ (if (result i32) (local.get $fsso)
386
+ (then (i32.and (i32.shr_u (local.get $foff) (i32.shl (local.get $i) (i32.const 3))) (i32.const 0xFF)))
387
+ (else (i32.load8_u (i32.add (local.get $foff) (local.get $i))))))
388
+ (then (return (i32.const 0))))
389
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
390
+ (br $loop)))
391
+ (i32.const 1))`
392
+
393
+ // 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)
395
+ (local $len i32) (local $off i32) (local $i i32) (local $c i32)
396
+ (local $bits i64) (local $srcOff i32)
397
+ (local.set $len (call $__str_byteLen (local.get $ptr)))
398
+ (if (i32.eqz (local.get $len))
399
+ (then (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 0) (i32.const 0)))))
400
+ (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $len))))
401
+ (i32.store (local.get $off) (local.get $len))
402
+ (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}))
408
+ (then
409
+ (block $dsso (loop $lsso
410
+ (br_if $dsso (i32.ge_s (local.get $i) (local.get $len)))
411
+ (local.set $c (i32.and
412
+ (i32.shr_u (local.get $srcOff) (i32.shl (local.get $i) (i32.const 3)))
413
+ (i32.const 0xFF)))
414
+ (if (i32.and (i32.ge_u (local.get $c) (local.get $lo)) (i32.le_u (local.get $c) (local.get $hi)))
415
+ (then (local.set $c (i32.add (local.get $c) (local.get $delta)))))
416
+ (i32.store8 (i32.add (local.get $off) (local.get $i)) (local.get $c))
417
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
418
+ (br $lsso))))
419
+ (else
420
+ (block $dh (loop $lh
421
+ (br_if $dh (i32.ge_s (local.get $i) (local.get $len)))
422
+ (local.set $c (i32.load8_u (i32.add (local.get $srcOff) (local.get $i))))
423
+ (if (i32.and (i32.ge_u (local.get $c) (local.get $lo)) (i32.le_u (local.get $c) (local.get $hi)))
424
+ (then (local.set $c (i32.add (local.get $c) (local.get $delta)))))
425
+ (i32.store8 (i32.add (local.get $off) (local.get $i)) (local.get $c))
426
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
427
+ (br $lh)))))
428
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
429
+
430
+ ctx.core.stdlib['__str_trim'] = `(func $__str_trim (param $ptr f64) (result f64)
431
+ (local $len i32) (local $start i32) (local $end i32)
432
+ (local.set $len (call $__str_byteLen (local.get $ptr)))
433
+ (local.set $start (i32.const 0))
434
+ (local.set $end (local.get $len))
435
+ (block $d1 (loop $l1
436
+ (br_if $d1 (i32.ge_s (local.get $start) (local.get $end)))
437
+ (br_if $d1 (i32.gt_u (call $__char_at (local.get $ptr) (local.get $start)) (i32.const 32)))
438
+ (local.set $start (i32.add (local.get $start) (i32.const 1)))
439
+ (br $l1)))
440
+ (block $d2 (loop $l2
441
+ (br_if $d2 (i32.le_s (local.get $end) (local.get $start)))
442
+ (br_if $d2 (i32.gt_u (call $__char_at (local.get $ptr) (i32.sub (local.get $end) (i32.const 1))) (i32.const 32)))
443
+ (local.set $end (i32.sub (local.get $end) (i32.const 1)))
444
+ (br $l2)))
445
+ (call $__str_slice (local.get $ptr) (local.get $start) (local.get $end)))`
446
+
447
+ ctx.core.stdlib['__str_trimStart'] = `(func $__str_trimStart (param $ptr f64) (result f64)
448
+ (local $len i32) (local $start i32)
449
+ (local.set $len (call $__str_byteLen (local.get $ptr)))
450
+ (local.set $start (i32.const 0))
451
+ (block $done (loop $loop
452
+ (br_if $done (i32.ge_s (local.get $start) (local.get $len)))
453
+ (br_if $done (i32.gt_u (call $__char_at (local.get $ptr) (local.get $start)) (i32.const 32)))
454
+ (local.set $start (i32.add (local.get $start) (i32.const 1)))
455
+ (br $loop)))
456
+ (call $__str_slice (local.get $ptr) (local.get $start) (local.get $len)))`
457
+
458
+ ctx.core.stdlib['__str_trimEnd'] = `(func $__str_trimEnd (param $ptr f64) (result f64)
459
+ (local $len i32) (local $end i32)
460
+ (local.set $len (call $__str_byteLen (local.get $ptr)))
461
+ (local.set $end (local.get $len))
462
+ (block $done (loop $loop
463
+ (br_if $done (i32.le_s (local.get $end) (i32.const 0)))
464
+ (br_if $done (i32.gt_u (call $__char_at (local.get $ptr) (i32.sub (local.get $end) (i32.const 1))) (i32.const 32)))
465
+ (local.set $end (i32.sub (local.get $end) (i32.const 1)))
466
+ (br $loop)))
467
+ (call $__str_slice (local.get $ptr) (i32.const 0) (local.get $end)))`
468
+
469
+ // Materialize source bytes once via __str_copy (handles SSO/heap), then memory.copy
470
+ // 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)
472
+ (local $len i32) (local $total i32) (local $off i32) (local $i i32)
473
+ (local.set $len (call $__str_byteLen (local.get $ptr)))
474
+ (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)))))
476
+ (local.set $total (i32.mul (local.get $len) (local.get $n)))
477
+ (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $total))))
478
+ (i32.store (local.get $off) (local.get $total))
479
+ (local.set $off (i32.add (local.get $off) (i32.const 4)))
480
+ (call $__str_copy (local.get $ptr) (local.get $off) (local.get $len))
481
+ (local.set $i (i32.const 1))
482
+ (block $done (loop $loop
483
+ (br_if $done (i32.ge_s (local.get $i) (local.get $n)))
484
+ (memory.copy
485
+ (i32.add (local.get $off) (i32.mul (local.get $i) (local.get $len)))
486
+ (local.get $off)
487
+ (local.get $len))
488
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
489
+ (br $loop)))
490
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
491
+
492
+ // Coerce value to string: numbers → __ftoa, nullish → static strings,
493
+ // 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)
497
+ ;; 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)))))
505
+ (local.set $type (call $__ptr_type (local.get $val)))
506
+ ;; Plain NaN (type=0) → "NaN" string
507
+ (if (i32.eqz (local.get $type))
508
+ (then (return (call $__static_str (i32.const 0)))))
509
+ ;; Array (type=1) → join(",") like JS Array.toString()
510
+ (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))))))
513
+ (local.get $val))`
514
+
515
+ // 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}))
523
+ (then
524
+ ;; SSO: up to 4 chars packed in low 32 bits (LE byte order). Unroll: write 1/2/3/4 bytes
525
+ ;; 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)))
527
+ (if (i32.ge_u (local.get $len) (i32.const 4))
528
+ (then (i32.store (local.get $dst) (local.get $w)))
529
+ (else
530
+ (if (i32.eq (local.get $len) (i32.const 0)) (then (return)))
531
+ (i32.store8 (local.get $dst) (local.get $w))
532
+ (if (i32.eq (local.get $len) (i32.const 1)) (then (return)))
533
+ (i32.store8 offset=1 (local.get $dst) (i32.shr_u (local.get $w) (i32.const 8)))
534
+ (if (i32.eq (local.get $len) (i32.const 2)) (then (return)))
535
+ (i32.store8 offset=2 (local.get $dst) (i32.shr_u (local.get $w) (i32.const 16))))))
536
+ (else
537
+ ;; Heap STRING: memory.copy directly from string data
538
+ (memory.copy (local.get $dst)
539
+ (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF)))
540
+ (local.get $len)))))`
541
+
542
+ ctx.core.stdlib['__str_concat'] = `(func $__str_concat (param $a f64) (param $b f64) (result f64)
543
+ (local $alen i32) (local $blen i32) (local $total i32) (local $off i32)
544
+ ;; Coerce operands to strings if needed
545
+ (local.set $a (call $__to_str (local.get $a)))
546
+ (local.set $b (call $__to_str (local.get $b)))
547
+ (local.set $alen (call $__str_byteLen (local.get $a)))
548
+ (local.set $blen (call $__str_byteLen (local.get $b)))
549
+ (local.set $total (i32.add (local.get $alen) (local.get $blen)))
550
+ (if (i32.eqz (local.get $total))
551
+ (then (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 0) (i32.const 0)))))
552
+ (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $total))))
553
+ (i32.store (local.get $off) (local.get $total))
554
+ (local.set $off (i32.add (local.get $off) (i32.const 4)))
555
+ (call $__str_copy (local.get $a) (local.get $off) (local.get $alen))
556
+ (call $__str_copy (local.get $b) (i32.add (local.get $off) (local.get $alen)) (local.get $blen))
557
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
558
+
559
+ ctx.core.stdlib['__str_concat_raw'] = `(func $__str_concat_raw (param $a f64) (param $b f64) (result f64)
560
+ (local $alen i32) (local $blen i32) (local $total i32) (local $off i32)
561
+ (local.set $alen (call $__str_byteLen (local.get $a)))
562
+ (local.set $blen (call $__str_byteLen (local.get $b)))
563
+ (local.set $total (i32.add (local.get $alen) (local.get $blen)))
564
+ (if (i32.eqz (local.get $total))
565
+ (then (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 0) (i32.const 0)))))
566
+ (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $total))))
567
+ (i32.store (local.get $off) (local.get $total))
568
+ (local.set $off (i32.add (local.get $off) (i32.const 4)))
569
+ (call $__str_copy (local.get $a) (local.get $off) (local.get $alen))
570
+ (call $__str_copy (local.get $b) (i32.add (local.get $off) (local.get $alen)) (local.get $blen))
571
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
572
+
573
+ ctx.core.stdlib['__str_replace'] = `(func $__str_replace (param $str f64) (param $search f64) (param $repl f64) (result f64)
574
+ (local $idx i32) (local $slen i32)
575
+ (local.set $idx (call $__str_indexof (local.get $str) (local.get $search) (i32.const 0)))
576
+ (if (result f64) (i32.lt_s (local.get $idx) (i32.const 0))
577
+ (then (local.get $str))
578
+ (else
579
+ (local.set $slen (call $__str_byteLen (local.get $search)))
580
+ (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)
589
+ (local.set $slen (call $__str_byteLen (local.get $search)))
590
+ (local.set $result (local.get $str))
591
+ (local.set $pos (i32.const 0))
592
+ (block $done (loop $next
593
+ (local.set $idx (call $__str_indexof (local.get $result) (local.get $search) (local.get $pos)))
594
+ (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)))))
601
+ (local.set $pos (i32.add (local.get $idx) (call $__str_byteLen (local.get $repl))))
602
+ (br $next)))
603
+ (local.get $result))`
604
+
605
+ ctx.core.stdlib['__str_split'] = `(func $__str_split (param $str f64) (param $sep f64) (result f64)
606
+ (local $slen i32) (local $plen i32) (local $count i32)
607
+ (local $i i32) (local $j i32) (local $match i32)
608
+ (local $arr i32) (local $piece_start i32) (local $piece_idx i32)
609
+ (local.set $slen (call $__str_byteLen (local.get $str)))
610
+ (local.set $plen (call $__str_byteLen (local.get $sep)))
611
+ (local.set $count (i32.const 1))
612
+ (local.set $i (i32.const 0))
613
+ (block $d1 (loop $l1
614
+ (br_if $d1 (i32.gt_s (local.get $i) (i32.sub (local.get $slen) (local.get $plen))))
615
+ (local.set $match (i32.const 1))
616
+ (local.set $j (i32.const 0))
617
+ (block $n1 (loop $c1
618
+ (br_if $n1 (i32.ge_s (local.get $j) (local.get $plen)))
619
+ (if (i32.ne (call $__char_at (local.get $str) (i32.add (local.get $i) (local.get $j)))
620
+ (call $__char_at (local.get $sep) (local.get $j)))
621
+ (then (local.set $match (i32.const 0)) (br $n1)))
622
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
623
+ (br $c1)))
624
+ (if (local.get $match) (then
625
+ (local.set $count (i32.add (local.get $count) (i32.const 1)))
626
+ (local.set $i (i32.add (local.get $i) (local.get $plen)))
627
+ (br $l1)))
628
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
629
+ (br $l1)))
630
+ (local.set $arr (call $__alloc (i32.add (i32.const 8) (i32.shl (local.get $count) (i32.const 3)))))
631
+ (i32.store (local.get $arr) (local.get $count))
632
+ (i32.store (i32.add (local.get $arr) (i32.const 4)) (local.get $count))
633
+ (local.set $arr (i32.add (local.get $arr) (i32.const 8)))
634
+ (local.set $piece_start (i32.const 0))
635
+ (local.set $piece_idx (i32.const 0))
636
+ (local.set $i (i32.const 0))
637
+ (block $d2 (loop $l2
638
+ (br_if $d2 (i32.gt_s (local.get $i) (i32.sub (local.get $slen) (local.get $plen))))
639
+ (local.set $match (i32.const 1))
640
+ (local.set $j (i32.const 0))
641
+ (block $n2 (loop $c2
642
+ (br_if $n2 (i32.ge_s (local.get $j) (local.get $plen)))
643
+ (if (i32.ne (call $__char_at (local.get $str) (i32.add (local.get $i) (local.get $j)))
644
+ (call $__char_at (local.get $sep) (local.get $j)))
645
+ (then (local.set $match (i32.const 0)) (br $n2)))
646
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
647
+ (br $c2)))
648
+ (if (local.get $match) (then
649
+ (f64.store (i32.add (local.get $arr) (i32.shl (local.get $piece_idx) (i32.const 3)))
650
+ (call $__str_slice (local.get $str) (local.get $piece_start) (local.get $i)))
651
+ (local.set $piece_idx (i32.add (local.get $piece_idx) (i32.const 1)))
652
+ (local.set $i (i32.add (local.get $i) (local.get $plen)))
653
+ (local.set $piece_start (local.get $i))
654
+ (br $l2)))
655
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
656
+ (br $l2)))
657
+ (f64.store (i32.add (local.get $arr) (i32.shl (local.get $piece_idx) (i32.const 3)))
658
+ (call $__str_slice (local.get $str) (local.get $piece_start) (local.get $slen)))
659
+ (call $__mkptr (i32.const 1) (i32.const 0) (local.get $arr)))`
660
+
661
+ ctx.core.stdlib['__str_join'] = `(func $__str_join (param $arr f64) (param $sep f64) (result f64)
662
+ (local $off i32) (local $len i32) (local $i i32) (local $result f64)
663
+ (local.set $off (call $__ptr_offset (local.get $arr)))
664
+ (local.set $len (call $__len (local.get $arr)))
665
+ (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)))
668
+ (local.set $i (i32.const 1))
669
+ (block $done (loop $loop
670
+ (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))))))
674
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
675
+ (br $loop)))
676
+ (local.get $result))`
677
+
678
+ // Source string copied via __str_copy (handles SSO/heap with memory.copy where possible).
679
+ // 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)
681
+ (local $slen i32) (local $plen i32) (local $fill i32) (local $off i32) (local $i i32)
682
+ (local $str_off i32) (local $pad_off i32)
683
+ (local $pbits i64) (local $poff i32) (local $psso i32)
684
+ (local.set $slen (call $__str_byteLen (local.get $str)))
685
+ (if (i32.ge_s (local.get $slen) (local.get $target))
686
+ (then (return (local.get $str))))
687
+ (local.set $plen (call $__str_byteLen (local.get $pad)))
688
+ (local.set $fill (i32.sub (local.get $target) (local.get $slen)))
689
+ (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $target))))
690
+ (i32.store (local.get $off) (local.get $target))
691
+ (local.set $off (i32.add (local.get $off) (i32.const 4)))
692
+ (local.set $str_off (select (local.get $fill) (i32.const 0) (local.get $before)))
693
+ (local.set $pad_off (select (i32.const 0) (local.get $slen) (local.get $before)))
694
+ (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})))
700
+ (block $d2 (loop $l2
701
+ (br_if $d2 (i32.ge_s (local.get $i) (local.get $fill)))
702
+ (i32.store8 (i32.add (local.get $off) (i32.add (local.get $pad_off) (local.get $i)))
703
+ (if (result i32) (local.get $psso)
704
+ (then (i32.and
705
+ (i32.shr_u (local.get $poff) (i32.shl (i32.rem_u (local.get $i) (local.get $plen)) (i32.const 3)))
706
+ (i32.const 0xFF)))
707
+ (else (i32.load8_u (i32.add (local.get $poff) (i32.rem_u (local.get $i) (local.get $plen)))))))
708
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
709
+ (br $l2)))
710
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
711
+
712
+ // Base helpers (__sso_char/__str_char/__char_at/__str_byteLen) are referenced
713
+ // from other helpers' WAT bodies and from emit sites; their `stdlibDeps`
714
+ // entries pull them transitively when actually used. No unconditional inc.
715
+
716
+ // === Method emitters ===
717
+
718
+ // Type-qualified (collide with array: slice, indexOf, includes)
719
+ ctx.core.emit['.string:slice'] = (str, start, end) => {
720
+ inc('__str_slice')
721
+ if (end != null) return typed(['call', '$__str_slice', asF64(emit(str)), asI32(emit(start)), asI32(emit(end))], 'f64')
722
+ const t = temp('t')
723
+ return typed(['block', ['result', 'f64'],
724
+ ['local.set', `$${t}`, asF64(emit(str))],
725
+ ['call', '$__str_slice', ['local.get', `$${t}`], asI32(emit(start)),
726
+ ['call', '$__str_byteLen', ['local.get', `$${t}`]]]], 'f64')
727
+ }
728
+
729
+ ctx.core.emit['.string:indexOf'] = (str, search, from) => {
730
+ 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')
732
+ }
733
+
734
+ ctx.core.emit['.string:includes'] = (str, search) => {
735
+ inc('__str_indexof')
736
+ 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')
738
+ }
739
+
740
+ // Generic (no collision)
741
+ ctx.core.emit['.substring'] = (str, start, end) => {
742
+ inc('__str_substring')
743
+ if (end != null) return typed(['call', '$__str_substring', asF64(emit(str)), asI32(emit(start)), asI32(emit(end))], 'f64')
744
+ const t = temp('t')
745
+ return typed(['block', ['result', 'f64'],
746
+ ['local.set', `$${t}`, asF64(emit(str))],
747
+ ['call', '$__str_substring', ['local.get', `$${t}`], asI32(emit(start)),
748
+ ['call', '$__str_byteLen', ['local.get', `$${t}`]]]], 'f64')
749
+ }
750
+
751
+ // Factory for simple str→call patterns: [emitKey, stdlibName, argCoercions, i32Result?]
752
+ const coerce = { f: asF64, i: asI32 }
753
+ const strMethod = (name, args, i32Result) => (str, ...params) => {
754
+ inc(name)
755
+ const call = ['call', `$${name}`, asF64(emit(str)), ...params.map((p, i) => coerce[args[i]](emit(p)))]
756
+ return typed(i32Result ? ['f64.convert_i32_s', call] : call, 'f64')
757
+ }
758
+
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'])
769
+
770
+ ctx.core.emit['.toUpperCase'] = (str) => {
771
+ inc('__str_case')
772
+ return typed(['call', '$__str_case', asF64(emit(str)), ['i32.const', 97], ['i32.const', 122], ['i32.const', -32]], 'f64')
773
+ }
774
+
775
+ ctx.core.emit['.toLowerCase'] = (str) => {
776
+ inc('__str_case')
777
+ return typed(['call', '$__str_case', asF64(emit(str)), ['i32.const', 65], ['i32.const', 90], ['i32.const', 32]], 'f64')
778
+ }
779
+
780
+ ctx.core.emit['.string:concat'] = (str, ...others) => {
781
+ inc('__str_concat')
782
+ let result = asF64(emit(str))
783
+ for (const other of others) result = typed(['call', '$__str_concat', result, asF64(emit(other))], 'f64')
784
+ return result
785
+ }
786
+
787
+ ctx.core.emit['strcat'] = (...parts) => {
788
+ 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')
791
+
792
+ const vals = parts.map(() => temp('s'))
793
+ const lens = parts.map(() => tempI32('sl'))
794
+ const total = tempI32('st')
795
+ const off = tempI32('so')
796
+ const dst = tempI32('sd')
797
+ const ir = []
798
+
799
+ 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]}`]]])
802
+ }
803
+ ir.push(['local.set', `$${total}`, ['i32.const', 0]])
804
+ for (const len of lens)
805
+ ir.push(['local.set', `$${total}`, ['i32.add', ['local.get', `$${total}`], ['local.get', `$${len}`]]])
806
+ const alloc = [
807
+ ['local.set', `$${off}`, ['call', '$__alloc', ['i32.add', ['i32.const', 4], ['local.get', `$${total}`]]]],
808
+ ['i32.store', ['local.get', `$${off}`], ['local.get', `$${total}`]],
809
+ ['local.set', `$${off}`, ['i32.add', ['local.get', `$${off}`], ['i32.const', 4]]],
810
+ ['local.set', `$${dst}`, ['local.get', `$${off}`]],
811
+ ]
812
+ 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]}`]])
814
+ alloc.push(['local.set', `$${dst}`, ['i32.add', ['local.get', `$${dst}`], ['local.get', `$${lens[i]}`]]])
815
+ }
816
+ alloc.push(['call', '$__mkptr', ['i32.const', PTR.STRING], ['i32.const', 0], ['local.get', `$${off}`]])
817
+ ir.push(['if', ['result', 'f64'], ['i32.eqz', ['local.get', `$${total}`]],
818
+ ['then', mkPtrIR(PTR.SSO, 0, 0)],
819
+ ['else', ['block', ['result', 'f64'], ...alloc]]])
820
+ return typed(['block', ['result', 'f64'], ...ir], 'f64')
821
+ }
822
+
823
+ ctx.core.emit['.padStart'] = (str, len, pad) => {
824
+ 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')
827
+ }
828
+
829
+ ctx.core.emit['.padEnd'] = (str, len, pad) => {
830
+ 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')
833
+ }
834
+
835
+ // .charAt(i) → 1-char string from char code at index i
836
+ ctx.core.emit['.charAt'] = (str, idx) => {
837
+ inc('__char_at')
838
+ const t = tempI32('ch')
839
+ // Get char code, create SSO string with 1 byte
840
+ 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')
843
+ }
844
+
845
+ // .charCodeAt(i) → integer char code (0..255 for ASCII bytes — unsigned, always
846
+ // representable as i32). Returning i32 directly lets `let c = s.charCodeAt(i)`
847
+ // stay on the i32 ABI: chained comparisons (`c >= 48 && c <= 57`), bit-ops, and
848
+ // `c - 48` arithmetic skip the per-iteration f64 widen + i32 trunc round-trip.
849
+ ctx.core.emit['.charCodeAt'] = (str, idx) => {
850
+ inc('__char_at')
851
+ return typed(['call', '$__char_at', asF64(emit(str)), asI32(emit(idx))], 'i32')
852
+ }
853
+
854
+ // String.fromCharCode(code) → 1-char SSO string
855
+ ctx.core.emit['String'] = (value) => {
856
+ if (value === undefined) return emit(['str', ''])
857
+ if (valTypeOf(value) === VAL.STRING) return emit(value)
858
+ if (valTypeOf(value) === VAL.NUMBER) {
859
+ inc('__ftoa')
860
+ return typed(['call', '$__ftoa', asF64(emit(value)), ['i32.const', 0], ['i32.const', 0]], 'f64')
861
+ }
862
+ inc('__to_str')
863
+ return typed(['call', '$__to_str', asF64(emit(value))], 'f64')
864
+ }
865
+
866
+ ctx.core.emit['String.fromCharCode'] = (code) => mkPtrIR(PTR.SSO, 1, asI32(emit(code)))
867
+
868
+ // String.fromCodePoint(cp) → UTF-8 encoded string
869
+ ctx.core.stdlib['__fromCodePoint'] = `(func $__fromCodePoint (param $cp i32) (result f64)
870
+ (local $off i32) (local $len i32)
871
+ ;; ASCII: 1 byte SSO
872
+ (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)))))
874
+ ;; 2-byte: 0x80-0x7FF → SSO
875
+ (if (i32.lt_u (local.get $cp) (i32.const 0x800))
876
+ (then (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 2)
877
+ (i32.or
878
+ (i32.or (i32.const 0xC0) (i32.shr_u (local.get $cp) (i32.const 6)))
879
+ (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 8)))))))
880
+ ;; 3-byte: 0x800-0xFFFF → SSO (3 bytes fits)
881
+ (if (i32.lt_u (local.get $cp) (i32.const 0x10000))
882
+ (then (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 3)
883
+ (i32.or (i32.or
884
+ (i32.or (i32.const 0xE0) (i32.shr_u (local.get $cp) (i32.const 12)))
885
+ (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
+ (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 16)))))))
887
+ ;; 4-byte: 0x10000-0x10FFFF → SSO (4 bytes fits)
888
+ (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 4)
889
+ (i32.or (i32.or (i32.or
890
+ (i32.or (i32.const 0xF0) (i32.shr_u (local.get $cp) (i32.const 18)))
891
+ (i32.shl (i32.or (i32.const 0x80) (i32.and (i32.shr_u (local.get $cp) (i32.const 12)) (i32.const 0x3F))) (i32.const 8)))
892
+ (i32.shl (i32.or (i32.const 0x80) (i32.and (i32.shr_u (local.get $cp) (i32.const 6)) (i32.const 0x3F))) (i32.const 16)))
893
+ (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 24))))))`
894
+
895
+ ctx.core.emit['String.fromCodePoint'] = (code) => {
896
+ inc('__fromCodePoint')
897
+ return typed(['call', '$__fromCodePoint', asI32(emit(code))], 'f64')
898
+ }
899
+
900
+ // .at(i) → charAt with negative index support
901
+ ctx.core.emit['.at'] = (str, idx) => {
902
+ inc('__char_at', '__str_byteLen')
903
+ const t = tempI32('at'), s = temp('as')
904
+ return typed(['block', ['result', 'f64'],
905
+ ['local.set', `$${s}`, asF64(emit(str))],
906
+ ['local.set', `$${t}`, asI32(emit(idx))],
907
+ // Negative index: t += length
908
+ ['if', ['i32.lt_s', ['local.get', `$${t}`], ['i32.const', 0]],
909
+ ['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')
912
+ }
913
+
914
+ // .search(str) → indexOf (same as indexOf for string args)
915
+ ctx.core.emit['.search'] = (str, search) => {
916
+ inc('__str_indexof')
917
+ return typed(['f64.convert_i32_s', ['call', '$__str_indexof', asF64(emit(str)), asF64(emit(search)), ['i32.const', 0]]], 'f64')
918
+ }
919
+
920
+ // .match(str) → [match] array if found, or 0 (null) if not
921
+ // For string args, returns single-element array with the matched substring
922
+ ctx.core.emit['.match'] = (str, search) => {
923
+ inc('__str_indexof', '__str_slice', '__wrap1')
924
+ const s = temp('ms'), q = temp('mq'), idx = tempI32('mi')
925
+ // indexOf, then if >= 0, create 1-element array with the match slice
926
+ return typed(['block', ['result', 'f64'],
927
+ ['local.set', `$${s}`, asF64(emit(str))],
928
+ ['local.set', `$${q}`, asF64(emit(search))],
929
+ ['local.set', `$${idx}`, ['call', '$__str_indexof', ['local.get', `$${s}`], ['local.get', `$${q}`], ['i32.const', 0]]],
930
+ ['if', ['result', 'f64'], ['i32.lt_s', ['local.get', `$${idx}`], ['i32.const', 0]],
931
+ ['then', ['f64.const', 0]], // null
932
+ ['else',
933
+ // Build 1-element array containing the search string
934
+ ['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')
938
+ }
939
+
940
+ // __wrap1(val: f64) → f64 — create 1-element array [val]
941
+ ctx.core.stdlib['__wrap1'] = `(func $__wrap1 (param $val f64) (result f64)
942
+ (local $ptr i32)
943
+ (local.set $ptr (call $__alloc (i32.const 16)))
944
+ (i32.store (local.get $ptr) (i32.const 1))
945
+ (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))
947
+ (call $__mkptr (i32.const 1) (i32.const 0) (i32.add (local.get $ptr) (i32.const 8))))`
948
+
949
+ // TextEncoder() / TextDecoder() → dummy values (methods do the work)
950
+ ctx.core.emit['TextEncoder'] = () => typed(['f64.const', 1], 'f64')
951
+ ctx.core.emit['TextDecoder'] = () => typed(['f64.const', 2], 'f64')
952
+
953
+ // .encode(str) → Uint8Array of string's UTF-8 bytes
954
+ // 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)
956
+ (local $len i32) (local $dst i32)
957
+ (local.set $len (call $__str_byteLen (local.get $str)))
958
+ (local.set $dst (call $__alloc (i32.add (i32.const 8) (local.get $len))))
959
+ (i32.store (local.get $dst) (local.get $len))
960
+ (i32.store (i32.add (local.get $dst) (i32.const 4)) (local.get $len))
961
+ (local.set $dst (i32.add (local.get $dst) (i32.const 8)))
962
+ (call $__str_copy (local.get $str) (local.get $dst) (local.get $len))
963
+ (call $__mkptr (i32.const 3) (i32.const 1) (local.get $dst)))`
964
+
965
+ ctx.core.emit['.encode'] = (obj, str) => {
966
+ inc('__str_encode')
967
+ return typed(['call', '$__str_encode', asF64(emit(str))], 'f64')
968
+ }
969
+
970
+ // .decode(uint8arr) → string from byte data
971
+ ctx.core.stdlib['__bytes_decode'] = `(func $__bytes_decode (param $arr f64) (result f64)
972
+ (local $off i32) (local $len i32) (local $dst i32)
973
+ (local.set $off (call $__ptr_offset (local.get $arr)))
974
+ (local.set $len (call $__len (local.get $arr)))
975
+ (local.set $dst (call $__alloc (i32.add (i32.const 4) (local.get $len))))
976
+ (i32.store (local.get $dst) (local.get $len))
977
+ (local.set $dst (i32.add (local.get $dst) (i32.const 4)))
978
+ (memory.copy (local.get $dst) (local.get $off) (local.get $len))
979
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $dst)))`
980
+
981
+ ctx.core.emit['.decode'] = (obj, arr) => {
982
+ inc('__bytes_decode')
983
+ return typed(['call', '$__bytes_decode', asF64(emit(arr))], 'f64')
984
+ }
985
+ }