jz 0.0.0 → 0.1.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.
@@ -0,0 +1,928 @@
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 { emit, typed, asF64, asI32, UNDEF_NAN, mkPtrIR, temp, tempI32 } from '../src/compile.js'
14
+ import { inc, PTR } from '../src/ctx.js'
15
+
16
+
17
+ export default (ctx) => {
18
+ Object.assign(ctx.core.stdlibDeps, {
19
+ __str_concat: ['__to_str', '__str_byteLen', '__alloc', '__mkptr', '__str_copy'],
20
+ __str_concat_raw: ['__str_byteLen', '__alloc', '__mkptr', '__str_copy'],
21
+ __str_copy: [],
22
+ __str_slice: ['__str_byteLen', '__alloc'],
23
+ __str_indexof: ['__str_byteLen'],
24
+ __str_substring: ['__str_slice'],
25
+ __str_startswith: ['__str_byteLen'],
26
+ __str_endswith: ['__str_byteLen'],
27
+ __str_case: ['__str_byteLen', '__alloc'],
28
+ __str_trim: ['__str_slice', '__str_byteLen', '__char_at'],
29
+ __str_trimStart: ['__str_slice', '__str_byteLen', '__char_at'],
30
+ __str_trimEnd: ['__str_slice', '__str_byteLen', '__char_at'],
31
+ __str_repeat: ['__str_byteLen', '__str_copy', '__alloc'],
32
+ __str_replace: ['__str_indexof', '__str_slice', '__str_concat'],
33
+ __str_replaceall: ['__str_indexof', '__str_slice', '__str_concat'],
34
+ __str_split: ['__str_slice', '__str_byteLen', '__char_at', '__alloc'],
35
+ __str_idx: ['__str_byteLen', '__char_at', '__mkptr'],
36
+ __str_eq: ['__char_at'],
37
+ __str_pad: ['__str_byteLen', '__str_copy', '__alloc'],
38
+ __str_join: ['__str_concat', '__to_str', '__str_byteLen', '__len', '__ptr_offset'],
39
+ __str_encode: ['__str_byteLen', '__str_copy'],
40
+ __to_str: ['__ftoa', '__static_str', '__str_join', '__mkptr'],
41
+ __str_byteLen: ['__ptr_type', '__ptr_aux', '__str_len'],
42
+ })
43
+
44
+ inc('__mkptr', '__alloc')
45
+
46
+ // === String literal: "abc" → SSO if ≤4 ASCII, else heap ===
47
+
48
+ ctx.core.emit['str'] = (str) => {
49
+ const MAX_SSO = 4
50
+ if (ctx.features.sso && str.length <= MAX_SSO && /^[\x00-\x7f]*$/.test(str)) {
51
+ let packed = 0
52
+ for (let i = 0; i < str.length; i++) packed |= str.charCodeAt(i) << (i * 8)
53
+ return mkPtrIR(PTR.SSO, str.length, packed)
54
+ }
55
+ const len = str.length
56
+ if (!ctx.memory.shared) {
57
+ // Own memory: place in static data segment (no runtime allocation)
58
+ if (!ctx.runtime.data) ctx.runtime.data = ''
59
+ const prior = ctx.runtime.dataDedup.get(str)
60
+ if (prior !== undefined) return mkPtrIR(PTR.STRING, 0, prior + 4)
61
+ while (ctx.runtime.data.length % 4 !== 0) ctx.runtime.data += '\0'
62
+ const offset = ctx.runtime.data.length
63
+ 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))
65
+ ctx.runtime.dataDedup.set(str, offset)
66
+ return mkPtrIR(PTR.STRING, 0, offset + 4)
67
+ }
68
+ // Shared memory: pack all string literals into one passive data segment with 4-byte
69
+ // length prefixes. At __start, alloc the whole pool once and memory.init it in a single
70
+ // call. Each use site resolves to `strBase + compile-time-offset` — O(1) IR nodes per
71
+ // use, independent of string length AND reused across uses.
72
+ if (!ctx.runtime.strPool) {
73
+ ctx.runtime.strPool = ''
74
+ ctx.scope.globals.set('__strBase', '(global $__strBase (mut i32) (i32.const 0))')
75
+ }
76
+ let off = ctx.runtime.strPoolDedup.get(str)
77
+ if (off === undefined) {
78
+ // Pack length header then bytes; offset points PAST the length (at the data).
79
+ ctx.runtime.strPool += String.fromCharCode(len & 0xFF, (len >> 8) & 0xFF, (len >> 16) & 0xFF, (len >> 24) & 0xFF)
80
+ off = ctx.runtime.strPool.length
81
+ ctx.runtime.strPool += str
82
+ ctx.runtime.strPoolDedup.set(str, off)
83
+ }
84
+ return mkPtrIR(PTR.STRING, 0, ['i32.add', ['global.get', '$__strBase'], ['i32.const', off]])
85
+ }
86
+
87
+ // === WAT: char extraction ===
88
+
89
+ // SSO/STRING ptrs never have forwarding pointers (only ARRAY does), so we extract
90
+ // 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)
92
+ (i32.and
93
+ (i32.shr_u
94
+ (i32.wrap_i64 (i64.and (i64.reinterpret_f64 (local.get $ptr)) (i64.const 0xFFFFFFFF)))
95
+ (i32.mul (local.get $i) (i32.const 8)))
96
+ (i32.const 0xFF)))`
97
+
98
+ ctx.core.stdlib['__str_char'] = `(func $__str_char (param $ptr f64) (param $i i32) (result i32)
99
+ (i32.load8_u (i32.add
100
+ (i32.wrap_i64 (i64.and (i64.reinterpret_f64 (local.get $ptr)) (i64.const 0xFFFFFFFF)))
101
+ (local.get $i))))`
102
+
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))))
109
+ (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}))
113
+ (then
114
+ (i32.and
115
+ (i32.shr_u (local.get $off) (i32.mul (local.get $i) (i32.const 8)))
116
+ (i32.const 0xFF)))
117
+ (else
118
+ (i32.load8_u (i32.add (local.get $off) (local.get $i))))))`
119
+
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)))
123
+ (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)))
127
+ (then (f64.const nan:${UNDEF_NAN}))
128
+ (else
129
+ (call $__mkptr
130
+ (i32.const ${PTR.SSO})
131
+ (i32.const 1)
132
+ (call $__char_at (local.get $ptr) (local.get $i))))))`
133
+
134
+ // Hot: ~53M calls in watr self-host. Bit-eq covers identity. SSO/SSO with !bit-eq
135
+ // 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)
139
+ (local $len i32) (local $lenB i32) (local $i i32)
140
+ (local $ba i64) (local $bb i64) (local $ta i32) (local $tb i32)
141
+ (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))
145
+ (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})))
152
+ (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})))
158
+ (then
159
+ (if (i32.or (i32.lt_u (local.get $offA) (i32.const 4)) (i32.lt_u (local.get $offB) (i32.const 4)))
160
+ (then (return (i32.const 0))))
161
+ (local.set $len (i32.load (i32.sub (local.get $offA) (i32.const 4))))
162
+ (local.set $lenB (i32.load (i32.sub (local.get $offB) (i32.const 4))))
163
+ (if (i32.ne (local.get $len) (local.get $lenB))
164
+ (then (return (i32.const 0))))
165
+ (local.set $lenB (i32.and (local.get $len) (i32.const -4)))
166
+ (block $d4 (loop $l4
167
+ (br_if $d4 (i32.ge_s (local.get $i) (local.get $lenB)))
168
+ (if (i32.ne
169
+ (i32.load (i32.add (local.get $offA) (local.get $i)))
170
+ (i32.load (i32.add (local.get $offB) (local.get $i))))
171
+ (then (return (i32.const 0))))
172
+ (local.set $i (i32.add (local.get $i) (i32.const 4)))
173
+ (br $l4)))
174
+ (block $dh (loop $lh
175
+ (br_if $dh (i32.ge_s (local.get $i) (local.get $len)))
176
+ (if (i32.ne
177
+ (i32.load8_u (i32.add (local.get $offA) (local.get $i)))
178
+ (i32.load8_u (i32.add (local.get $offB) (local.get $i))))
179
+ (then (return (i32.const 0))))
180
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
181
+ (br $lh)))
182
+ (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)))))
186
+ (else
187
+ (if (i32.and (i32.eq (local.get $ta) (i32.const ${PTR.STRING})) (i32.ge_u (local.get $offA) (i32.const 4)))
188
+ (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)))))
191
+ (else
192
+ (if (i32.and (i32.eq (local.get $tb) (i32.const ${PTR.STRING})) (i32.ge_u (local.get $offB) (i32.const 4)))
193
+ (then (local.set $lenB (i32.load (i32.sub (local.get $offB) (i32.const 4))))))))
194
+ (if (i32.ne (local.get $len) (local.get $lenB))
195
+ (then (return (i32.const 0))))
196
+ (block $dm (loop $lm
197
+ (br_if $dm (i32.ge_s (local.get $i) (local.get $len)))
198
+ (if (i32.ne (call $__char_at (local.get $a) (local.get $i))
199
+ (call $__char_at (local.get $b) (local.get $i)))
200
+ (then (return (i32.const 0))))
201
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
202
+ (br $lm)))
203
+ (i32.const 1))`
204
+
205
+ // === WAT: unified byte length (SSO → aux, heap → header) ===
206
+
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))))))`
219
+
220
+ // === WAT: string methods ===
221
+
222
+ // SSO source uses an unrolled byte-extract loop (len ≤ 4); heap source uses memory.copy
223
+ // (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)
225
+ (local $len i32) (local $nlen i32) (local $off i32) (local $i i32)
226
+ (local $bits i64) (local $srcOff i32) (local $isSso i32)
227
+ (local.set $len (call $__str_byteLen (local.get $ptr)))
228
+ (if (i32.lt_s (local.get $start) (i32.const 0))
229
+ (then (local.set $start (i32.add (local.get $len) (local.get $start)))))
230
+ (if (i32.lt_s (local.get $end) (i32.const 0))
231
+ (then (local.set $end (i32.add (local.get $len) (local.get $end)))))
232
+ (if (i32.lt_s (local.get $start) (i32.const 0))
233
+ (then (local.set $start (i32.const 0))))
234
+ (if (i32.gt_s (local.get $start) (local.get $len))
235
+ (then (local.set $start (local.get $len))))
236
+ (if (i32.lt_s (local.get $end) (i32.const 0))
237
+ (then (local.set $end (i32.const 0))))
238
+ (if (i32.gt_s (local.get $end) (local.get $len))
239
+ (then (local.set $end (local.get $len))))
240
+ (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)))))
242
+ (local.set $nlen (i32.sub (local.get $end) (local.get $start)))
243
+ (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $nlen))))
244
+ (i32.store (local.get $off) (local.get $nlen))
245
+ (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})))
251
+ (if (local.get $isSso)
252
+ (then
253
+ (block $done (loop $loop
254
+ (br_if $done (i32.ge_s (local.get $i) (local.get $nlen)))
255
+ (i32.store8 (i32.add (local.get $off) (local.get $i))
256
+ (i32.and (i32.shr_u (local.get $srcOff)
257
+ (i32.shl (i32.add (local.get $start) (local.get $i)) (i32.const 3)))
258
+ (i32.const 0xFF)))
259
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
260
+ (br $loop))))
261
+ (else
262
+ (memory.copy (local.get $off) (i32.add (local.get $srcOff) (local.get $start)) (local.get $nlen))))
263
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
264
+
265
+ ctx.core.stdlib['__str_substring'] = `(func $__str_substring (param $ptr f64) (param $start i32) (param $end i32) (result f64)
266
+ (local $len i32) (local $tmp i32)
267
+ (local.set $len (call $__str_byteLen (local.get $ptr)))
268
+ (if (i32.lt_s (local.get $start) (i32.const 0))
269
+ (then (local.set $start (i32.const 0))))
270
+ (if (i32.lt_s (local.get $end) (i32.const 0))
271
+ (then (local.set $end (i32.const 0))))
272
+ (if (i32.gt_s (local.get $start) (local.get $len))
273
+ (then (local.set $start (local.get $len))))
274
+ (if (i32.gt_s (local.get $end) (local.get $len))
275
+ (then (local.set $end (local.get $len))))
276
+ (if (i32.gt_s (local.get $start) (local.get $end))
277
+ (then
278
+ (local.set $tmp (local.get $start))
279
+ (local.set $start (local.get $end))
280
+ (local.set $end (local.get $tmp))))
281
+ (call $__str_slice (local.get $ptr) (local.get $start) (local.get $end)))`
282
+
283
+ // Hoist SSO/heap dispatch for hay and ndl out of the inner byte loop. Inner
284
+ // 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)
286
+ (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)
288
+ (local $hsso i32) (local $nsso i32) (local $hb i32) (local $nb i32) (local $k i32)
289
+ (local.set $hlen (call $__str_byteLen (local.get $hay)))
290
+ (local.set $nlen (call $__str_byteLen (local.get $ndl)))
291
+ (if (i32.eqz (local.get $nlen)) (then (return (local.get $from))))
292
+ (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})))
303
+ (local.set $i (if (result i32) (i32.gt_s (local.get $from) (i32.const 0)) (then (local.get $from)) (else (i32.const 0))))
304
+ (block $done (loop $outer
305
+ (br_if $done (i32.gt_s (local.get $i) (i32.sub (local.get $hlen) (local.get $nlen))))
306
+ (local.set $match (i32.const 1))
307
+ (local.set $j (i32.const 0))
308
+ (block $nomatch (loop $inner
309
+ (br_if $nomatch (i32.ge_s (local.get $j) (local.get $nlen)))
310
+ (local.set $k (i32.add (local.get $i) (local.get $j)))
311
+ (local.set $hb (if (result i32) (local.get $hsso)
312
+ (then (i32.and (i32.shr_u (local.get $hoff) (i32.shl (local.get $k) (i32.const 3))) (i32.const 0xFF)))
313
+ (else (i32.load8_u (i32.add (local.get $hoff) (local.get $k))))))
314
+ (local.set $nb (if (result i32) (local.get $nsso)
315
+ (then (i32.and (i32.shr_u (local.get $noff) (i32.shl (local.get $j) (i32.const 3))) (i32.const 0xFF)))
316
+ (else (i32.load8_u (i32.add (local.get $noff) (local.get $j))))))
317
+ (if (i32.ne (local.get $hb) (local.get $nb))
318
+ (then (local.set $match (i32.const 0)) (br $nomatch)))
319
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
320
+ (br $inner)))
321
+ (if (local.get $match) (then (return (local.get $i))))
322
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
323
+ (br $outer)))
324
+ (i32.const -1))`
325
+
326
+ // 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)
328
+ (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)
330
+ (local.set $plen (call $__str_byteLen (local.get $pfx)))
331
+ (if (i32.gt_s (local.get $plen) (call $__str_byteLen (local.get $str)))
332
+ (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})))
343
+ (block $done (loop $loop
344
+ (br_if $done (i32.ge_s (local.get $i) (local.get $plen)))
345
+ (if (i32.ne
346
+ (if (result i32) (local.get $ssso)
347
+ (then (i32.and (i32.shr_u (local.get $soff) (i32.shl (local.get $i) (i32.const 3))) (i32.const 0xFF)))
348
+ (else (i32.load8_u (i32.add (local.get $soff) (local.get $i)))))
349
+ (if (result i32) (local.get $psso)
350
+ (then (i32.and (i32.shr_u (local.get $poff) (i32.shl (local.get $i) (i32.const 3))) (i32.const 0xFF)))
351
+ (else (i32.load8_u (i32.add (local.get $poff) (local.get $i))))))
352
+ (then (return (i32.const 0))))
353
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
354
+ (br $loop)))
355
+ (i32.const 1))`
356
+
357
+ ctx.core.stdlib['__str_endswith'] = `(func $__str_endswith (param $str f64) (param $sfx f64) (result i32)
358
+ (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)
360
+ (local.set $slen (call $__str_byteLen (local.get $str)))
361
+ (local.set $flen (call $__str_byteLen (local.get $sfx)))
362
+ (if (i32.gt_s (local.get $flen) (local.get $slen))
363
+ (then (return (i32.const 0))))
364
+ (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})))
375
+ (block $done (loop $loop
376
+ (br_if $done (i32.ge_s (local.get $i) (local.get $flen)))
377
+ (local.set $k (i32.add (local.get $off) (local.get $i)))
378
+ (if (i32.ne
379
+ (if (result i32) (local.get $ssso)
380
+ (then (i32.and (i32.shr_u (local.get $soff) (i32.shl (local.get $k) (i32.const 3))) (i32.const 0xFF)))
381
+ (else (i32.load8_u (i32.add (local.get $soff) (local.get $k)))))
382
+ (if (result i32) (local.get $fsso)
383
+ (then (i32.and (i32.shr_u (local.get $foff) (i32.shl (local.get $i) (i32.const 3))) (i32.const 0xFF)))
384
+ (else (i32.load8_u (i32.add (local.get $foff) (local.get $i))))))
385
+ (then (return (i32.const 0))))
386
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
387
+ (br $loop)))
388
+ (i32.const 1))`
389
+
390
+ // 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)
392
+ (local $len i32) (local $off i32) (local $i i32) (local $c i32)
393
+ (local $bits i64) (local $srcOff i32)
394
+ (local.set $len (call $__str_byteLen (local.get $ptr)))
395
+ (if (i32.eqz (local.get $len))
396
+ (then (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 0) (i32.const 0)))))
397
+ (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $len))))
398
+ (i32.store (local.get $off) (local.get $len))
399
+ (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}))
405
+ (then
406
+ (block $dsso (loop $lsso
407
+ (br_if $dsso (i32.ge_s (local.get $i) (local.get $len)))
408
+ (local.set $c (i32.and
409
+ (i32.shr_u (local.get $srcOff) (i32.shl (local.get $i) (i32.const 3)))
410
+ (i32.const 0xFF)))
411
+ (if (i32.and (i32.ge_u (local.get $c) (local.get $lo)) (i32.le_u (local.get $c) (local.get $hi)))
412
+ (then (local.set $c (i32.add (local.get $c) (local.get $delta)))))
413
+ (i32.store8 (i32.add (local.get $off) (local.get $i)) (local.get $c))
414
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
415
+ (br $lsso))))
416
+ (else
417
+ (block $dh (loop $lh
418
+ (br_if $dh (i32.ge_s (local.get $i) (local.get $len)))
419
+ (local.set $c (i32.load8_u (i32.add (local.get $srcOff) (local.get $i))))
420
+ (if (i32.and (i32.ge_u (local.get $c) (local.get $lo)) (i32.le_u (local.get $c) (local.get $hi)))
421
+ (then (local.set $c (i32.add (local.get $c) (local.get $delta)))))
422
+ (i32.store8 (i32.add (local.get $off) (local.get $i)) (local.get $c))
423
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
424
+ (br $lh)))))
425
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
426
+
427
+ ctx.core.stdlib['__str_trim'] = `(func $__str_trim (param $ptr f64) (result f64)
428
+ (local $len i32) (local $start i32) (local $end i32)
429
+ (local.set $len (call $__str_byteLen (local.get $ptr)))
430
+ (local.set $start (i32.const 0))
431
+ (local.set $end (local.get $len))
432
+ (block $d1 (loop $l1
433
+ (br_if $d1 (i32.ge_s (local.get $start) (local.get $end)))
434
+ (br_if $d1 (i32.gt_u (call $__char_at (local.get $ptr) (local.get $start)) (i32.const 32)))
435
+ (local.set $start (i32.add (local.get $start) (i32.const 1)))
436
+ (br $l1)))
437
+ (block $d2 (loop $l2
438
+ (br_if $d2 (i32.le_s (local.get $end) (local.get $start)))
439
+ (br_if $d2 (i32.gt_u (call $__char_at (local.get $ptr) (i32.sub (local.get $end) (i32.const 1))) (i32.const 32)))
440
+ (local.set $end (i32.sub (local.get $end) (i32.const 1)))
441
+ (br $l2)))
442
+ (call $__str_slice (local.get $ptr) (local.get $start) (local.get $end)))`
443
+
444
+ ctx.core.stdlib['__str_trimStart'] = `(func $__str_trimStart (param $ptr f64) (result f64)
445
+ (local $len i32) (local $start i32)
446
+ (local.set $len (call $__str_byteLen (local.get $ptr)))
447
+ (local.set $start (i32.const 0))
448
+ (block $done (loop $loop
449
+ (br_if $done (i32.ge_s (local.get $start) (local.get $len)))
450
+ (br_if $done (i32.gt_u (call $__char_at (local.get $ptr) (local.get $start)) (i32.const 32)))
451
+ (local.set $start (i32.add (local.get $start) (i32.const 1)))
452
+ (br $loop)))
453
+ (call $__str_slice (local.get $ptr) (local.get $start) (local.get $len)))`
454
+
455
+ ctx.core.stdlib['__str_trimEnd'] = `(func $__str_trimEnd (param $ptr f64) (result f64)
456
+ (local $len i32) (local $end i32)
457
+ (local.set $len (call $__str_byteLen (local.get $ptr)))
458
+ (local.set $end (local.get $len))
459
+ (block $done (loop $loop
460
+ (br_if $done (i32.le_s (local.get $end) (i32.const 0)))
461
+ (br_if $done (i32.gt_u (call $__char_at (local.get $ptr) (i32.sub (local.get $end) (i32.const 1))) (i32.const 32)))
462
+ (local.set $end (i32.sub (local.get $end) (i32.const 1)))
463
+ (br $loop)))
464
+ (call $__str_slice (local.get $ptr) (i32.const 0) (local.get $end)))`
465
+
466
+ // Materialize source bytes once via __str_copy (handles SSO/heap), then memory.copy
467
+ // 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)
469
+ (local $len i32) (local $total i32) (local $off i32) (local $i i32)
470
+ (local.set $len (call $__str_byteLen (local.get $ptr)))
471
+ (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)))))
473
+ (local.set $total (i32.mul (local.get $len) (local.get $n)))
474
+ (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $total))))
475
+ (i32.store (local.get $off) (local.get $total))
476
+ (local.set $off (i32.add (local.get $off) (i32.const 4)))
477
+ (call $__str_copy (local.get $ptr) (local.get $off) (local.get $len))
478
+ (local.set $i (i32.const 1))
479
+ (block $done (loop $loop
480
+ (br_if $done (i32.ge_s (local.get $i) (local.get $n)))
481
+ (memory.copy
482
+ (i32.add (local.get $off) (i32.mul (local.get $i) (local.get $len)))
483
+ (local.get $off)
484
+ (local.get $len))
485
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
486
+ (br $loop)))
487
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
488
+
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)
492
+ ;; 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)))))
495
+ (local.set $type (call $__ptr_type (local.get $val)))
496
+ ;; Plain NaN (type=0) → "NaN" string
497
+ (if (i32.eqz (local.get $type))
498
+ (then (return (call $__static_str (i32.const 0)))))
499
+ ;; Array (type=1) → join(",") like JS Array.toString()
500
+ (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))))))
503
+ (local.get $val))`
504
+
505
+ // 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}))
513
+ (then
514
+ ;; SSO: up to 4 chars packed in low 32 bits (LE byte order). Unroll: write 1/2/3/4 bytes
515
+ ;; 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)))
517
+ (if (i32.ge_u (local.get $len) (i32.const 4))
518
+ (then (i32.store (local.get $dst) (local.get $w)))
519
+ (else
520
+ (if (i32.eq (local.get $len) (i32.const 0)) (then (return)))
521
+ (i32.store8 (local.get $dst) (local.get $w))
522
+ (if (i32.eq (local.get $len) (i32.const 1)) (then (return)))
523
+ (i32.store8 offset=1 (local.get $dst) (i32.shr_u (local.get $w) (i32.const 8)))
524
+ (if (i32.eq (local.get $len) (i32.const 2)) (then (return)))
525
+ (i32.store8 offset=2 (local.get $dst) (i32.shr_u (local.get $w) (i32.const 16))))))
526
+ (else
527
+ ;; Heap STRING: memory.copy directly from string data
528
+ (memory.copy (local.get $dst)
529
+ (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF)))
530
+ (local.get $len)))))`
531
+
532
+ ctx.core.stdlib['__str_concat'] = `(func $__str_concat (param $a f64) (param $b f64) (result f64)
533
+ (local $alen i32) (local $blen i32) (local $total i32) (local $off i32)
534
+ ;; Coerce operands to strings if needed
535
+ (local.set $a (call $__to_str (local.get $a)))
536
+ (local.set $b (call $__to_str (local.get $b)))
537
+ (local.set $alen (call $__str_byteLen (local.get $a)))
538
+ (local.set $blen (call $__str_byteLen (local.get $b)))
539
+ (local.set $total (i32.add (local.get $alen) (local.get $blen)))
540
+ (if (i32.eqz (local.get $total))
541
+ (then (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 0) (i32.const 0)))))
542
+ (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $total))))
543
+ (i32.store (local.get $off) (local.get $total))
544
+ (local.set $off (i32.add (local.get $off) (i32.const 4)))
545
+ (call $__str_copy (local.get $a) (local.get $off) (local.get $alen))
546
+ (call $__str_copy (local.get $b) (i32.add (local.get $off) (local.get $alen)) (local.get $blen))
547
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
548
+
549
+ ctx.core.stdlib['__str_concat_raw'] = `(func $__str_concat_raw (param $a f64) (param $b f64) (result f64)
550
+ (local $alen i32) (local $blen i32) (local $total i32) (local $off i32)
551
+ (local.set $alen (call $__str_byteLen (local.get $a)))
552
+ (local.set $blen (call $__str_byteLen (local.get $b)))
553
+ (local.set $total (i32.add (local.get $alen) (local.get $blen)))
554
+ (if (i32.eqz (local.get $total))
555
+ (then (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 0) (i32.const 0)))))
556
+ (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $total))))
557
+ (i32.store (local.get $off) (local.get $total))
558
+ (local.set $off (i32.add (local.get $off) (i32.const 4)))
559
+ (call $__str_copy (local.get $a) (local.get $off) (local.get $alen))
560
+ (call $__str_copy (local.get $b) (i32.add (local.get $off) (local.get $alen)) (local.get $blen))
561
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
562
+
563
+ ctx.core.stdlib['__str_replace'] = `(func $__str_replace (param $str f64) (param $search f64) (param $repl f64) (result f64)
564
+ (local $idx i32) (local $slen i32)
565
+ (local.set $idx (call $__str_indexof (local.get $str) (local.get $search) (i32.const 0)))
566
+ (if (result f64) (i32.lt_s (local.get $idx) (i32.const 0))
567
+ (then (local.get $str))
568
+ (else
569
+ (local.set $slen (call $__str_byteLen (local.get $search)))
570
+ (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)
579
+ (local.set $slen (call $__str_byteLen (local.get $search)))
580
+ (local.set $result (local.get $str))
581
+ (local.set $pos (i32.const 0))
582
+ (block $done (loop $next
583
+ (local.set $idx (call $__str_indexof (local.get $result) (local.get $search) (local.get $pos)))
584
+ (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)))))
591
+ (local.set $pos (i32.add (local.get $idx) (call $__str_byteLen (local.get $repl))))
592
+ (br $next)))
593
+ (local.get $result))`
594
+
595
+ ctx.core.stdlib['__str_split'] = `(func $__str_split (param $str f64) (param $sep f64) (result f64)
596
+ (local $slen i32) (local $plen i32) (local $count i32)
597
+ (local $i i32) (local $j i32) (local $match i32)
598
+ (local $arr i32) (local $piece_start i32) (local $piece_idx i32)
599
+ (local.set $slen (call $__str_byteLen (local.get $str)))
600
+ (local.set $plen (call $__str_byteLen (local.get $sep)))
601
+ (local.set $count (i32.const 1))
602
+ (local.set $i (i32.const 0))
603
+ (block $d1 (loop $l1
604
+ (br_if $d1 (i32.gt_s (local.get $i) (i32.sub (local.get $slen) (local.get $plen))))
605
+ (local.set $match (i32.const 1))
606
+ (local.set $j (i32.const 0))
607
+ (block $n1 (loop $c1
608
+ (br_if $n1 (i32.ge_s (local.get $j) (local.get $plen)))
609
+ (if (i32.ne (call $__char_at (local.get $str) (i32.add (local.get $i) (local.get $j)))
610
+ (call $__char_at (local.get $sep) (local.get $j)))
611
+ (then (local.set $match (i32.const 0)) (br $n1)))
612
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
613
+ (br $c1)))
614
+ (if (local.get $match) (then
615
+ (local.set $count (i32.add (local.get $count) (i32.const 1)))
616
+ (local.set $i (i32.add (local.get $i) (local.get $plen)))
617
+ (br $l1)))
618
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
619
+ (br $l1)))
620
+ (local.set $arr (call $__alloc (i32.add (i32.const 8) (i32.shl (local.get $count) (i32.const 3)))))
621
+ (i32.store (local.get $arr) (local.get $count))
622
+ (i32.store (i32.add (local.get $arr) (i32.const 4)) (local.get $count))
623
+ (local.set $arr (i32.add (local.get $arr) (i32.const 8)))
624
+ (local.set $piece_start (i32.const 0))
625
+ (local.set $piece_idx (i32.const 0))
626
+ (local.set $i (i32.const 0))
627
+ (block $d2 (loop $l2
628
+ (br_if $d2 (i32.gt_s (local.get $i) (i32.sub (local.get $slen) (local.get $plen))))
629
+ (local.set $match (i32.const 1))
630
+ (local.set $j (i32.const 0))
631
+ (block $n2 (loop $c2
632
+ (br_if $n2 (i32.ge_s (local.get $j) (local.get $plen)))
633
+ (if (i32.ne (call $__char_at (local.get $str) (i32.add (local.get $i) (local.get $j)))
634
+ (call $__char_at (local.get $sep) (local.get $j)))
635
+ (then (local.set $match (i32.const 0)) (br $n2)))
636
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
637
+ (br $c2)))
638
+ (if (local.get $match) (then
639
+ (f64.store (i32.add (local.get $arr) (i32.shl (local.get $piece_idx) (i32.const 3)))
640
+ (call $__str_slice (local.get $str) (local.get $piece_start) (local.get $i)))
641
+ (local.set $piece_idx (i32.add (local.get $piece_idx) (i32.const 1)))
642
+ (local.set $i (i32.add (local.get $i) (local.get $plen)))
643
+ (local.set $piece_start (local.get $i))
644
+ (br $l2)))
645
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
646
+ (br $l2)))
647
+ (f64.store (i32.add (local.get $arr) (i32.shl (local.get $piece_idx) (i32.const 3)))
648
+ (call $__str_slice (local.get $str) (local.get $piece_start) (local.get $slen)))
649
+ (call $__mkptr (i32.const 1) (i32.const 0) (local.get $arr)))`
650
+
651
+ ctx.core.stdlib['__str_join'] = `(func $__str_join (param $arr f64) (param $sep f64) (result f64)
652
+ (local $off i32) (local $len i32) (local $i i32) (local $result f64)
653
+ (local.set $off (call $__ptr_offset (local.get $arr)))
654
+ (local.set $len (call $__len (local.get $arr)))
655
+ (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)))
658
+ (local.set $i (i32.const 1))
659
+ (block $done (loop $loop
660
+ (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))))))
664
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
665
+ (br $loop)))
666
+ (local.get $result))`
667
+
668
+ // Source string copied via __str_copy (handles SSO/heap with memory.copy where possible).
669
+ // 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)
671
+ (local $slen i32) (local $plen i32) (local $fill i32) (local $off i32) (local $i i32)
672
+ (local $str_off i32) (local $pad_off i32)
673
+ (local $pbits i64) (local $poff i32) (local $psso i32)
674
+ (local.set $slen (call $__str_byteLen (local.get $str)))
675
+ (if (i32.ge_s (local.get $slen) (local.get $target))
676
+ (then (return (local.get $str))))
677
+ (local.set $plen (call $__str_byteLen (local.get $pad)))
678
+ (local.set $fill (i32.sub (local.get $target) (local.get $slen)))
679
+ (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $target))))
680
+ (i32.store (local.get $off) (local.get $target))
681
+ (local.set $off (i32.add (local.get $off) (i32.const 4)))
682
+ (local.set $str_off (select (local.get $fill) (i32.const 0) (local.get $before)))
683
+ (local.set $pad_off (select (i32.const 0) (local.get $slen) (local.get $before)))
684
+ (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})))
690
+ (block $d2 (loop $l2
691
+ (br_if $d2 (i32.ge_s (local.get $i) (local.get $fill)))
692
+ (i32.store8 (i32.add (local.get $off) (i32.add (local.get $pad_off) (local.get $i)))
693
+ (if (result i32) (local.get $psso)
694
+ (then (i32.and
695
+ (i32.shr_u (local.get $poff) (i32.shl (i32.rem_u (local.get $i) (local.get $plen)) (i32.const 3)))
696
+ (i32.const 0xFF)))
697
+ (else (i32.load8_u (i32.add (local.get $poff) (i32.rem_u (local.get $i) (local.get $plen)))))))
698
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
699
+ (br $l2)))
700
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
701
+
702
+ // Base helpers (__sso_char/__str_char/__char_at/__str_byteLen) are referenced
703
+ // from other helpers' WAT bodies and from emit sites; their `stdlibDeps`
704
+ // entries pull them transitively when actually used. No unconditional inc.
705
+
706
+ // === Method emitters ===
707
+
708
+ // Type-qualified (collide with array: slice, indexOf, includes)
709
+ ctx.core.emit['.string:slice'] = (str, start, end) => {
710
+ inc('__str_slice')
711
+ if (end != null) return typed(['call', '$__str_slice', asF64(emit(str)), asI32(emit(start)), asI32(emit(end))], 'f64')
712
+ const t = temp('t')
713
+ return typed(['block', ['result', 'f64'],
714
+ ['local.set', `$${t}`, asF64(emit(str))],
715
+ ['call', '$__str_slice', ['local.get', `$${t}`], asI32(emit(start)),
716
+ ['call', '$__str_byteLen', ['local.get', `$${t}`]]]], 'f64')
717
+ }
718
+
719
+ ctx.core.emit['.string:indexOf'] = (str, search, from) => {
720
+ 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')
722
+ }
723
+
724
+ ctx.core.emit['.string:includes'] = (str, search) => {
725
+ inc('__str_indexof')
726
+ 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')
728
+ }
729
+
730
+ // Generic (no collision)
731
+ ctx.core.emit['.substring'] = (str, start, end) => {
732
+ inc('__str_substring')
733
+ if (end != null) return typed(['call', '$__str_substring', asF64(emit(str)), asI32(emit(start)), asI32(emit(end))], 'f64')
734
+ const t = temp('t')
735
+ return typed(['block', ['result', 'f64'],
736
+ ['local.set', `$${t}`, asF64(emit(str))],
737
+ ['call', '$__str_substring', ['local.get', `$${t}`], asI32(emit(start)),
738
+ ['call', '$__str_byteLen', ['local.get', `$${t}`]]]], 'f64')
739
+ }
740
+
741
+ // Factory for simple str→call patterns: [emitKey, stdlibName, argCoercions, i32Result?]
742
+ const coerce = { f: asF64, i: asI32 }
743
+ const strMethod = (name, args, i32Result) => (str, ...params) => {
744
+ inc(name)
745
+ const call = ['call', `$${name}`, asF64(emit(str)), ...params.map((p, i) => coerce[args[i]](emit(p)))]
746
+ return typed(i32Result ? ['f64.convert_i32_s', call] : call, 'f64')
747
+ }
748
+
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'])
759
+
760
+ ctx.core.emit['.toUpperCase'] = (str) => {
761
+ inc('__str_case')
762
+ return typed(['call', '$__str_case', asF64(emit(str)), ['i32.const', 97], ['i32.const', 122], ['i32.const', -32]], 'f64')
763
+ }
764
+
765
+ ctx.core.emit['.toLowerCase'] = (str) => {
766
+ inc('__str_case')
767
+ return typed(['call', '$__str_case', asF64(emit(str)), ['i32.const', 65], ['i32.const', 90], ['i32.const', 32]], 'f64')
768
+ }
769
+
770
+ ctx.core.emit['.string:concat'] = (str, ...others) => {
771
+ inc('__str_concat')
772
+ let result = asF64(emit(str))
773
+ for (const other of others) result = typed(['call', '$__str_concat', result, asF64(emit(other))], 'f64')
774
+ return result
775
+ }
776
+
777
+ ctx.core.emit['.padStart'] = (str, len, pad) => {
778
+ 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')
781
+ }
782
+
783
+ ctx.core.emit['.padEnd'] = (str, len, pad) => {
784
+ 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')
787
+ }
788
+
789
+ // .charAt(i) → 1-char string from char code at index i
790
+ ctx.core.emit['.charAt'] = (str, idx) => {
791
+ inc('__char_at')
792
+ const t = tempI32('ch')
793
+ // Get char code, create SSO string with 1 byte
794
+ 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')
797
+ }
798
+
799
+ // .charCodeAt(i) → integer char code (0..255 for ASCII bytes — unsigned, always
800
+ // representable as i32). Returning i32 directly lets `let c = s.charCodeAt(i)`
801
+ // stay on the i32 ABI: chained comparisons (`c >= 48 && c <= 57`), bit-ops, and
802
+ // `c - 48` arithmetic skip the per-iteration f64 widen + i32 trunc round-trip.
803
+ ctx.core.emit['.charCodeAt'] = (str, idx) => {
804
+ inc('__char_at')
805
+ return typed(['call', '$__char_at', asF64(emit(str)), asI32(emit(idx))], 'i32')
806
+ }
807
+
808
+ // String.fromCharCode(code) → 1-char SSO string
809
+ ctx.core.emit['String.fromCharCode'] = (code) => mkPtrIR(PTR.SSO, 1, asI32(emit(code)))
810
+
811
+ // String.fromCodePoint(cp) → UTF-8 encoded string
812
+ ctx.core.stdlib['__fromCodePoint'] = `(func $__fromCodePoint (param $cp i32) (result f64)
813
+ (local $off i32) (local $len i32)
814
+ ;; ASCII: 1 byte SSO
815
+ (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)))))
817
+ ;; 2-byte: 0x80-0x7FF → SSO
818
+ (if (i32.lt_u (local.get $cp) (i32.const 0x800))
819
+ (then (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 2)
820
+ (i32.or
821
+ (i32.or (i32.const 0xC0) (i32.shr_u (local.get $cp) (i32.const 6)))
822
+ (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 8)))))))
823
+ ;; 3-byte: 0x800-0xFFFF → SSO (3 bytes fits)
824
+ (if (i32.lt_u (local.get $cp) (i32.const 0x10000))
825
+ (then (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 3)
826
+ (i32.or (i32.or
827
+ (i32.or (i32.const 0xE0) (i32.shr_u (local.get $cp) (i32.const 12)))
828
+ (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
+ (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 16)))))))
830
+ ;; 4-byte: 0x10000-0x10FFFF → SSO (4 bytes fits)
831
+ (return (call $__mkptr (i32.const ${PTR.SSO}) (i32.const 4)
832
+ (i32.or (i32.or (i32.or
833
+ (i32.or (i32.const 0xF0) (i32.shr_u (local.get $cp) (i32.const 18)))
834
+ (i32.shl (i32.or (i32.const 0x80) (i32.and (i32.shr_u (local.get $cp) (i32.const 12)) (i32.const 0x3F))) (i32.const 8)))
835
+ (i32.shl (i32.or (i32.const 0x80) (i32.and (i32.shr_u (local.get $cp) (i32.const 6)) (i32.const 0x3F))) (i32.const 16)))
836
+ (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 24))))))`
837
+
838
+ ctx.core.emit['String.fromCodePoint'] = (code) => {
839
+ inc('__fromCodePoint')
840
+ return typed(['call', '$__fromCodePoint', asI32(emit(code))], 'f64')
841
+ }
842
+
843
+ // .at(i) → charAt with negative index support
844
+ ctx.core.emit['.at'] = (str, idx) => {
845
+ inc('__char_at', '__str_byteLen')
846
+ const t = tempI32('at'), s = temp('as')
847
+ return typed(['block', ['result', 'f64'],
848
+ ['local.set', `$${s}`, asF64(emit(str))],
849
+ ['local.set', `$${t}`, asI32(emit(idx))],
850
+ // Negative index: t += length
851
+ ['if', ['i32.lt_s', ['local.get', `$${t}`], ['i32.const', 0]],
852
+ ['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')
855
+ }
856
+
857
+ // .search(str) → indexOf (same as indexOf for string args)
858
+ ctx.core.emit['.search'] = (str, search) => {
859
+ inc('__str_indexof')
860
+ return typed(['f64.convert_i32_s', ['call', '$__str_indexof', asF64(emit(str)), asF64(emit(search)), ['i32.const', 0]]], 'f64')
861
+ }
862
+
863
+ // .match(str) → [match] array if found, or 0 (null) if not
864
+ // For string args, returns single-element array with the matched substring
865
+ ctx.core.emit['.match'] = (str, search) => {
866
+ inc('__str_indexof', '__str_slice', '__wrap1')
867
+ const s = temp('ms'), q = temp('mq'), idx = tempI32('mi')
868
+ // indexOf, then if >= 0, create 1-element array with the match slice
869
+ return typed(['block', ['result', 'f64'],
870
+ ['local.set', `$${s}`, asF64(emit(str))],
871
+ ['local.set', `$${q}`, asF64(emit(search))],
872
+ ['local.set', `$${idx}`, ['call', '$__str_indexof', ['local.get', `$${s}`], ['local.get', `$${q}`], ['i32.const', 0]]],
873
+ ['if', ['result', 'f64'], ['i32.lt_s', ['local.get', `$${idx}`], ['i32.const', 0]],
874
+ ['then', ['f64.const', 0]], // null
875
+ ['else',
876
+ // Build 1-element array containing the search string
877
+ ['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')
881
+ }
882
+
883
+ // __wrap1(val: f64) → f64 — create 1-element array [val]
884
+ ctx.core.stdlib['__wrap1'] = `(func $__wrap1 (param $val f64) (result f64)
885
+ (local $ptr i32)
886
+ (local.set $ptr (call $__alloc (i32.const 16)))
887
+ (i32.store (local.get $ptr) (i32.const 1))
888
+ (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))
890
+ (call $__mkptr (i32.const 1) (i32.const 0) (i32.add (local.get $ptr) (i32.const 8))))`
891
+
892
+ // TextEncoder() / TextDecoder() → dummy values (methods do the work)
893
+ ctx.core.emit['TextEncoder'] = () => typed(['f64.const', 1], 'f64')
894
+ ctx.core.emit['TextDecoder'] = () => typed(['f64.const', 2], 'f64')
895
+
896
+ // .encode(str) → Uint8Array of string's UTF-8 bytes
897
+ // 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)
899
+ (local $len i32) (local $dst i32)
900
+ (local.set $len (call $__str_byteLen (local.get $str)))
901
+ (local.set $dst (call $__alloc (i32.add (i32.const 8) (local.get $len))))
902
+ (i32.store (local.get $dst) (local.get $len))
903
+ (i32.store (i32.add (local.get $dst) (i32.const 4)) (local.get $len))
904
+ (local.set $dst (i32.add (local.get $dst) (i32.const 8)))
905
+ (call $__str_copy (local.get $str) (local.get $dst) (local.get $len))
906
+ (call $__mkptr (i32.const 3) (i32.const 1) (local.get $dst)))`
907
+
908
+ ctx.core.emit['.encode'] = (obj, str) => {
909
+ inc('__str_encode')
910
+ return typed(['call', '$__str_encode', asF64(emit(str))], 'f64')
911
+ }
912
+
913
+ // .decode(uint8arr) → string from byte data
914
+ ctx.core.stdlib['__bytes_decode'] = `(func $__bytes_decode (param $arr f64) (result f64)
915
+ (local $off i32) (local $len i32) (local $dst i32)
916
+ (local.set $off (call $__ptr_offset (local.get $arr)))
917
+ (local.set $len (call $__len (local.get $arr)))
918
+ (local.set $dst (call $__alloc (i32.add (i32.const 4) (local.get $len))))
919
+ (i32.store (local.get $dst) (local.get $len))
920
+ (local.set $dst (i32.add (local.get $dst) (i32.const 4)))
921
+ (memory.copy (local.get $dst) (local.get $off) (local.get $len))
922
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $dst)))`
923
+
924
+ ctx.core.emit['.decode'] = (obj, arr) => {
925
+ inc('__bytes_decode')
926
+ return typed(['call', '$__bytes_decode', asF64(emit(arr))], 'f64')
927
+ }
928
+ }