jz 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/module/string.js CHANGED
@@ -12,14 +12,30 @@
12
12
  * @module string
13
13
  */
14
14
 
15
- import { typed, asF64, asI32, asI64, NULL_NAN, UNDEF_NAN, mkPtrIR, temp, tempI32 } from '../src/ir.js'
16
- import { emit } from '../src/emit.js'
15
+ import { typed, asF64, asI32, asI64, NULL_NAN, UNDEF_NAN, mkPtrIR, temp, tempI32, toNumF64, toStrI64 } from '../src/ir.js'
16
+ import { emit, emitBoolStr } from '../src/emit.js'
17
17
  import { valTypeOf, VAL } from '../src/analyze.js'
18
- import { inc, PTR, LAYOUT } from '../src/ctx.js'
18
+ import { inc, emitter, PTR, LAYOUT } from '../src/ctx.js'
19
19
 
20
20
  // SSO discriminator bit pre-shifted to its slot in the full i64 ptr (bit 46).
21
21
  // Used as `i64.and ptr SSO_BIT_I64` for branch-without-extracting-aux.
22
22
  const SSO_BIT_I64 = '0x' + (BigInt(LAYOUT.SSO_BIT) << BigInt(LAYOUT.AUX_SHIFT)).toString(16).toUpperCase().padStart(16, '0')
23
+ // Slice/view discriminator bit, pre-shifted to bit 45 — same branch-without-aux trick.
24
+ const SLICE_BIT_I64 = '0x' + (BigInt(LAYOUT.SLICE_BIT) << BigInt(LAYOUT.AUX_SHIFT)).toString(16).toUpperCase().padStart(16, '0')
25
+
26
+ // WAT (no-locals expression): byte length of a heap STRING given its raw $-local
27
+ // names for the offset and the i64 ptr. A view (SLICE_BIT) carries its length in
28
+ // aux[12:0]; an own heap string reads the i32 header at off-4. Callers must have
29
+ // already excluded SSO. Used inline by the hot char/eq helpers so a slice never
30
+ // reads a bogus length out of a parent buffer's bytes.
31
+ const heapLenExpr = (ptrLocal, offLocal) => `(if (result i32)
32
+ (i64.ne (i64.and (local.get ${ptrLocal}) (i64.const ${SLICE_BIT_I64})) (i64.const 0))
33
+ (then (i32.and
34
+ (i32.wrap_i64 (i64.shr_u (local.get ${ptrLocal}) (i64.const ${LAYOUT.AUX_SHIFT})))
35
+ (i32.const ${LAYOUT.SLICE_LEN_MASK})))
36
+ (else (if (result i32) (i32.ge_u (local.get ${offLocal}) (i32.const 4))
37
+ (then (i32.load (i32.sub (local.get ${offLocal}) (i32.const 4))))
38
+ (else (i32.const 0)))))`
23
39
 
24
40
 
25
41
  export default (ctx) => {
@@ -29,7 +45,8 @@ export default (ctx) => {
29
45
  __str_append_byte: ['__str_byteLen', '__alloc', '__mkptr', '__str_copy'],
30
46
  __str_copy: [],
31
47
  __str_slice: ['__str_byteLen', '__alloc'],
32
- __str_indexof: ['__str_byteLen'],
48
+ __str_slice_view: ['__str_byteLen', '__mkptr', '__str_slice'],
49
+ __str_indexof: ['__str_byteLen', '__to_str'],
33
50
  __str_substring: ['__str_slice'],
34
51
  __str_startswith: ['__str_byteLen'],
35
52
  __str_endswith: ['__str_byteLen'],
@@ -42,11 +59,17 @@ export default (ctx) => {
42
59
  __str_replaceall: ['__str_indexof', '__str_slice', '__str_concat'],
43
60
  __str_split: ['__str_slice', '__str_byteLen', '__char_at', '__alloc'],
44
61
  __str_idx: [],
45
- __str_eq: ['__char_at'],
62
+ __str_eq: ['__char_at', '__str_byteLen'],
46
63
  __str_cmp: ['__char_at', '__str_byteLen'],
64
+ __str_range_eq: ['__char_at', '__str_byteLen'],
65
+ __str_substring_eq: ['__str_byteLen', '__str_range_eq'],
66
+ __str_slice_eq: ['__str_byteLen', '__str_range_eq'],
47
67
  __str_pad: ['__str_byteLen', '__str_copy', '__alloc'],
48
68
  __str_join: ['__str_concat', '__to_str', '__str_byteLen', '__len', '__ptr_offset'],
49
69
  __str_encode: ['__str_byteLen', '__str_copy'],
70
+ __encodeURIComponent: ['__to_str', '__str_byteLen', '__char_at', '__alloc', '__mkptr'],
71
+ __decodeURIComponent: ['__to_str', '__str_byteLen', '__char_at', '__alloc', '__mkptr', '__uri_hex'],
72
+ __uri_hex: [],
50
73
  __to_str: ['__ftoa', '__static_str', '__str_join', '__mkptr'],
51
74
  __str_byteLen: ['__ptr_type', '__ptr_aux', '__str_len'],
52
75
  })
@@ -111,19 +134,63 @@ export default (ctx) => {
111
134
  (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK})))
112
135
  (local.get $i))))`
113
136
 
114
- // Hot (~37M calls in watr self-host). Caller guarantees $ptr is a STRING;
115
- // SSO bit picks inline-byte-extract vs heap memory load.
137
+ // Hot (~37M calls in watr self-host, ~40k/scan in tokenizer bench). Caller
138
+ // guarantees $ptr is a STRING; SSO bit picks inline-byte-extract vs heap memory
139
+ // load. Returns 0 for OOB — internal tokenizer callers (number/json/regex
140
+ // parsers) rely on this sentinel to terminate `while (c > 32)`-shape loops
141
+ // past end-of-string. The SSO bounds check is essential: i32.shr_u wraps shift
142
+ // count mod 32, so without it `'a'.charCodeAt(4)` would return 'a' again
143
+ // (shift 32→0).
144
+ //
145
+ // Body written as a single nested-if expression with NO locals so watr's
146
+ // inliner picks it up (gate: no-locals + ≤4 params + body.length===1). After
147
+ // inlining into a hot loop, V8's LICM hoists the SSO-bit test, offset
148
+ // extraction and heap-length load out — the per-iteration cost collapses to a
149
+ // load+bounds-check, beating call-site overhead. Repeated `i32.wrap_i64 +
150
+ // i64.and OFFSET_MASK` subexpressions rely on CSE in the consumer; both
151
+ // V8/TurboFan and watr's own propagate pass handle this.
116
152
  ctx.core.stdlib['__char_at'] = `(func $__char_at (param $ptr i64) (param $i i32) (result i32)
117
- (local $off i32)
118
- (local.set $off (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
119
153
  (if (result i32)
120
154
  (i64.ne (i64.and (local.get $ptr) (i64.const ${SSO_BIT_I64})) (i64.const 0))
121
155
  (then
122
- (i32.and
123
- (i32.shr_u (local.get $off) (i32.mul (local.get $i) (i32.const 8)))
124
- (i32.const 0xFF)))
156
+ (if (result i32)
157
+ (i32.ge_u (local.get $i)
158
+ (i32.and
159
+ (i32.wrap_i64 (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.AUX_SHIFT})))
160
+ (i32.const ${LAYOUT.SSO_BIT - 1})))
161
+ (then (i32.const 0))
162
+ (else (i32.and
163
+ (i32.shr_u
164
+ (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK})))
165
+ (i32.mul (local.get $i) (i32.const 8)))
166
+ (i32.const 0xFF)))))
125
167
  (else
126
- (i32.load8_u (i32.add (local.get $off) (local.get $i))))))`
168
+ (if (result i32)
169
+ (i32.ge_u (local.get $i)
170
+ ;; non-SSO length: view → aux[12:0]; own heap string → header at off-4
171
+ ;; (off<4 sentinel guards the literal-data-segment edge). Both arms
172
+ ;; are loop-invariant — V8 LICM hoists the whole select.
173
+ (if (result i32)
174
+ (i64.ne (i64.and (local.get $ptr) (i64.const ${SLICE_BIT_I64})) (i64.const 0))
175
+ (then (i32.and
176
+ (i32.wrap_i64 (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.AUX_SHIFT})))
177
+ (i32.const ${LAYOUT.SLICE_LEN_MASK})))
178
+ (else
179
+ (if (result i32)
180
+ (i32.lt_u
181
+ (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK})))
182
+ (i32.const 4))
183
+ (then (i32.const 0))
184
+ (else (i32.load
185
+ (i32.sub
186
+ (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK})))
187
+ (i32.const 4))))))))
188
+ (then (i32.const 0))
189
+ (else
190
+ (i32.load8_u
191
+ (i32.add
192
+ (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK})))
193
+ (local.get $i)))))))))`
127
194
 
128
195
  ctx.core.stdlib['__str_idx'] = `(func $__str_idx (param $ptr i64) (param $i i32) (result f64)
129
196
  (local $t i32) (local $off i32) (local $len i32) (local $isSso i32)
@@ -138,9 +205,15 @@ export default (ctx) => {
138
205
  (i32.wrap_i64 (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.AUX_SHIFT})))
139
206
  (i32.const ${LAYOUT.SSO_BIT - 1})))
140
207
  (else
141
- (if (result i32) (i32.and (i32.eq (local.get $t) (i32.const ${PTR.STRING})) (i32.ge_u (local.get $off) (i32.const 4)))
142
- (then (i32.load (i32.sub (local.get $off) (i32.const 4))))
143
- (else (i32.const 0))))))
208
+ (if (result i32)
209
+ (i64.ne (i64.and (local.get $ptr) (i64.const ${SLICE_BIT_I64})) (i64.const 0))
210
+ (then (i32.and
211
+ (i32.wrap_i64 (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.AUX_SHIFT})))
212
+ (i32.const ${LAYOUT.SLICE_LEN_MASK})))
213
+ (else
214
+ (if (result i32) (i32.and (i32.eq (local.get $t) (i32.const ${PTR.STRING})) (i32.ge_u (local.get $off) (i32.const 4)))
215
+ (then (i32.load (i32.sub (local.get $off) (i32.const 4))))
216
+ (else (i32.const 0))))))))
144
217
  (if (result f64)
145
218
  (i32.or (i32.lt_s (local.get $i) (i32.const 0)) (i32.ge_u (local.get $i) (local.get $len)))
146
219
  (then (f64.const nan:${UNDEF_NAN}))
@@ -188,8 +261,8 @@ export default (ctx) => {
188
261
  (then
189
262
  (if (i32.or (i32.lt_u (local.get $offA) (i32.const 4)) (i32.lt_u (local.get $offB) (i32.const 4)))
190
263
  (then (return (i32.const 0))))
191
- (local.set $len (i32.load (i32.sub (local.get $offA) (i32.const 4))))
192
- (local.set $lenB (i32.load (i32.sub (local.get $offB) (i32.const 4))))
264
+ (local.set $len ${heapLenExpr('$a', '$offA')})
265
+ (local.set $lenB ${heapLenExpr('$b', '$offB')})
193
266
  (if (i32.ne (local.get $len) (local.get $lenB))
194
267
  (then (return (i32.const 0))))
195
268
  (local.set $lenB (i32.and (local.get $len) (i32.const -4)))
@@ -211,20 +284,9 @@ export default (ctx) => {
211
284
  (br $lh)))
212
285
  (return (i32.const 1))))
213
286
  ;; Mixed (SSO×heap) or anything else: compute len per side then per-byte via __char_at.
214
- (if (local.get $ssoA)
215
- (then (local.set $len (i32.and
216
- (i32.wrap_i64 (i64.shr_u (local.get $a) (i64.const ${LAYOUT.AUX_SHIFT})))
217
- (i32.const ${LAYOUT.SSO_BIT - 1}))))
218
- (else
219
- (if (i32.and (i32.eq (local.get $ta) (i32.const ${PTR.STRING})) (i32.ge_u (local.get $offA) (i32.const 4)))
220
- (then (local.set $len (i32.load (i32.sub (local.get $offA) (i32.const 4))))))))
221
- (if (local.get $ssoB)
222
- (then (local.set $lenB (i32.and
223
- (i32.wrap_i64 (i64.shr_u (local.get $b) (i64.const ${LAYOUT.AUX_SHIFT})))
224
- (i32.const ${LAYOUT.SSO_BIT - 1}))))
225
- (else
226
- (if (i32.and (i32.eq (local.get $tb) (i32.const ${PTR.STRING})) (i32.ge_u (local.get $offB) (i32.const 4)))
227
- (then (local.set $lenB (i32.load (i32.sub (local.get $offB) (i32.const 4))))))))
287
+ ;; __str_byteLen handles SSO, slice (SLICE_BIT) and own-heap encodings uniformly.
288
+ (local.set $len (call $__str_byteLen (local.get $a)))
289
+ (local.set $lenB (call $__str_byteLen (local.get $b)))
228
290
  (if (i32.ne (local.get $len) (local.get $lenB))
229
291
  (then (return (i32.const 0))))
230
292
  (block $dm (loop $lm
@@ -279,10 +341,14 @@ export default (ctx) => {
279
341
  (if (result i32) (i32.and (local.get $aux) (i32.const ${LAYOUT.SSO_BIT}))
280
342
  (then (i32.and (local.get $aux) (i32.const ${LAYOUT.SSO_BIT - 1})))
281
343
  (else
282
- (local.set $off (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
283
- (if (result i32) (i32.ge_u (local.get $off) (i32.const 4))
284
- (then (i32.load (i32.sub (local.get $off) (i32.const 4))))
285
- (else (i32.const 0))))))
344
+ (if (result i32) (i32.and (local.get $aux) (i32.const ${LAYOUT.SLICE_BIT}))
345
+ ;; view: length lives in aux[12:0], not a header.
346
+ (then (i32.and (local.get $aux) (i32.const ${LAYOUT.SLICE_LEN_MASK})))
347
+ (else
348
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
349
+ (if (result i32) (i32.ge_u (local.get $off) (i32.const 4))
350
+ (then (i32.load (i32.sub (local.get $off) (i32.const 4))))
351
+ (else (i32.const 0))))))))
286
352
  (else (i32.const 0))))`
287
353
 
288
354
  // === WAT: string methods ===
@@ -329,6 +395,48 @@ export default (ctx) => {
329
395
  (memory.copy (local.get $off) (i32.add (local.get $srcOff) (local.get $start)) (local.get $nlen))))
330
396
  (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
331
397
 
398
+ // No-copy slice: returns a VIEW into the receiver's buffer instead of copying
399
+ // bytes. Only emitted when escape analysis proves the result never outlives the
400
+ // parent (a non-escaping local). A view is a heap STRING with SLICE_BIT set and
401
+ // its length in aux[12:0]; the offset points straight into the parent's bytes,
402
+ // so it stays valid as long as the parent does. Falls back to a real copy
403
+ // (__str_slice) when the parent is SSO (no buffer to point into) or the result
404
+ // is longer than SLICE_LEN_MASK (aux can't hold the length). Clamping mirrors
405
+ // __str_slice; the fallback re-clamps idempotently.
406
+ ctx.core.stdlib['__str_slice_view'] = `(func $__str_slice_view (param $ptr i64) (param $start i32) (param $end i32) (result f64)
407
+ (local $len i32) (local $nlen i32) (local $srcOff i32) (local $tag i32)
408
+ (local.set $len (call $__str_byteLen (local.get $ptr)))
409
+ (if (i32.lt_s (local.get $start) (i32.const 0))
410
+ (then (local.set $start (i32.add (local.get $len) (local.get $start)))))
411
+ (if (i32.lt_s (local.get $end) (i32.const 0))
412
+ (then (local.set $end (i32.add (local.get $len) (local.get $end)))))
413
+ (if (i32.lt_s (local.get $start) (i32.const 0))
414
+ (then (local.set $start (i32.const 0))))
415
+ (if (i32.gt_s (local.get $start) (local.get $len))
416
+ (then (local.set $start (local.get $len))))
417
+ (if (i32.lt_s (local.get $end) (i32.const 0))
418
+ (then (local.set $end (i32.const 0))))
419
+ (if (i32.gt_s (local.get $end) (local.get $len))
420
+ (then (local.set $end (local.get $len))))
421
+ (if (i32.ge_s (local.get $start) (local.get $end))
422
+ (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
423
+ (local.set $nlen (i32.sub (local.get $end) (local.get $start)))
424
+ (local.set $tag (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
425
+ ;; View-eligible: STRING parent, not SSO, length fits aux[12:0].
426
+ (if (i32.and
427
+ (i32.and
428
+ (i32.eq (local.get $tag) (i32.const ${PTR.STRING}))
429
+ (i64.eqz (i64.and (local.get $ptr) (i64.const ${SSO_BIT_I64}))))
430
+ (i32.le_u (local.get $nlen) (i32.const ${LAYOUT.SLICE_LEN_MASK})))
431
+ (then
432
+ (local.set $srcOff (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
433
+ (return (call $__mkptr
434
+ (i32.const ${PTR.STRING})
435
+ (i32.or (i32.const ${LAYOUT.SLICE_BIT}) (local.get $nlen))
436
+ (i32.add (local.get $srcOff) (local.get $start))))))
437
+ ;; Fallback: copy (SSO parent, or slice too long for the aux length field).
438
+ (call $__str_slice (local.get $ptr) (local.get $start) (local.get $end)))`
439
+
332
440
  ctx.core.stdlib['__str_substring'] = `(func $__str_substring (param $ptr i64) (param $start i32) (param $end i32) (result f64)
333
441
  (local $len i32) (local $tmp i32)
334
442
  (local.set $len (call $__str_byteLen (local.get $ptr)))
@@ -347,12 +455,91 @@ export default (ctx) => {
347
455
  (local.set $end (local.get $tmp))))
348
456
  (call $__str_slice (local.get $ptr) (local.get $start) (local.get $end)))`
349
457
 
458
+ // === WAT: fused substring-equality ===
459
+ //
460
+ // `<str>.{substr,substring,slice}(...) === <other>` consumed only by the
461
+ // equality materialises a transient substring (an __alloc + byte copy) just
462
+ // to feed __eq. emit.js's emitSubstringEqCmp peepholes that pair to these
463
+ // helpers, which clamp the range exactly like __str_substring / __str_slice
464
+ // then byte-compare it against `other` in place — zero allocation. Motivating
465
+ // hot path: the parser keyword scan, `cur.substr(i,l) === keyword`.
466
+ //
467
+ // __str_range_eq assumes the receiver is a STRING (every substring method
468
+ // returns one) and type-checks only `other`, mirroring __eq's STRING-vs-?
469
+ // arm: a genuine number never equals a string, and a NaN-boxed non-STRING
470
+ // never does either (jz `==` is strict).
471
+ ctx.core.stdlib['__str_range_eq'] = `(func $__str_range_eq (param $ptr i64) (param $start i32) (param $end i32) (param $other i64) (result i32)
472
+ (local $n i32) (local $i i32) (local $fb f64)
473
+ ;; A genuine number reinterprets to a non-NaN f64 (equals itself) — never a string.
474
+ (local.set $fb (f64.reinterpret_i64 (local.get $other)))
475
+ (if (f64.eq (local.get $fb) (local.get $fb))
476
+ (then (return (i32.const 0))))
477
+ ;; NaN-boxed but not STRING-tagged ⇒ not a string ⇒ not equal.
478
+ (if (i32.ne
479
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $other) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
480
+ (i32.const ${PTR.STRING}))
481
+ (then (return (i32.const 0))))
482
+ (local.set $n (i32.sub (local.get $end) (local.get $start)))
483
+ (if (i32.lt_s (local.get $n) (i32.const 0))
484
+ (then (local.set $n (i32.const 0))))
485
+ (if (i32.ne (local.get $n) (call $__str_byteLen (local.get $other)))
486
+ (then (return (i32.const 0))))
487
+ (block $done (loop $next
488
+ (br_if $done (i32.ge_s (local.get $i) (local.get $n)))
489
+ (if (i32.ne
490
+ (call $__char_at (local.get $ptr) (i32.add (local.get $start) (local.get $i)))
491
+ (call $__char_at (local.get $other) (local.get $i)))
492
+ (then (return (i32.const 0))))
493
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
494
+ (br $next)))
495
+ (i32.const 1))`
496
+
497
+ // Clamp mirrors __str_substring (negatives floor to 0, swap when start>end).
498
+ ctx.core.stdlib['__str_substring_eq'] = `(func $__str_substring_eq (param $ptr i64) (param $start i32) (param $end i32) (param $other i64) (result i32)
499
+ (local $len i32) (local $tmp i32)
500
+ (local.set $len (call $__str_byteLen (local.get $ptr)))
501
+ (if (i32.lt_s (local.get $start) (i32.const 0))
502
+ (then (local.set $start (i32.const 0))))
503
+ (if (i32.lt_s (local.get $end) (i32.const 0))
504
+ (then (local.set $end (i32.const 0))))
505
+ (if (i32.gt_s (local.get $start) (local.get $len))
506
+ (then (local.set $start (local.get $len))))
507
+ (if (i32.gt_s (local.get $end) (local.get $len))
508
+ (then (local.set $end (local.get $len))))
509
+ (if (i32.gt_s (local.get $start) (local.get $end))
510
+ (then
511
+ (local.set $tmp (local.get $start))
512
+ (local.set $start (local.get $end))
513
+ (local.set $end (local.get $tmp))))
514
+ (call $__str_range_eq (local.get $ptr) (local.get $start) (local.get $end) (local.get $other)))`
515
+
516
+ // Clamp mirrors __str_slice (negatives count from the end; __str_range_eq
517
+ // floors a negative span to an empty match).
518
+ ctx.core.stdlib['__str_slice_eq'] = `(func $__str_slice_eq (param $ptr i64) (param $start i32) (param $end i32) (param $other i64) (result i32)
519
+ (local $len i32)
520
+ (local.set $len (call $__str_byteLen (local.get $ptr)))
521
+ (if (i32.lt_s (local.get $start) (i32.const 0))
522
+ (then (local.set $start (i32.add (local.get $len) (local.get $start)))))
523
+ (if (i32.lt_s (local.get $end) (i32.const 0))
524
+ (then (local.set $end (i32.add (local.get $len) (local.get $end)))))
525
+ (if (i32.lt_s (local.get $start) (i32.const 0))
526
+ (then (local.set $start (i32.const 0))))
527
+ (if (i32.gt_s (local.get $start) (local.get $len))
528
+ (then (local.set $start (local.get $len))))
529
+ (if (i32.lt_s (local.get $end) (i32.const 0))
530
+ (then (local.set $end (i32.const 0))))
531
+ (if (i32.gt_s (local.get $end) (local.get $len))
532
+ (then (local.set $end (local.get $len))))
533
+ (call $__str_range_eq (local.get $ptr) (local.get $start) (local.get $end) (local.get $other)))`
534
+
350
535
  // Hoist SSO/heap dispatch for hay and ndl out of the inner byte loop. Inner
351
536
  // loop becomes (load8_u OR sso byte-extract) per side — no per-byte calls.
352
537
  ctx.core.stdlib['__str_indexof'] = `(func $__str_indexof (param $hay i64) (param $ndl i64) (param $from i32) (result i32)
353
538
  (local $hlen i32) (local $nlen i32) (local $i i32) (local $j i32) (local $match i32)
354
539
  (local $hoff i32) (local $noff i32)
355
540
  (local $hsso i32) (local $nsso i32) (local $hb i32) (local $nb i32) (local $k i32)
541
+ ;; ToString the search value (21.1.3.9 step 4) — coerces undefined/null/number/etc.
542
+ (local.set $ndl (call $__to_str (local.get $ndl)))
356
543
  (local.set $hlen (call $__str_byteLen (local.get $hay)))
357
544
  (local.set $nlen (call $__str_byteLen (local.get $ndl)))
358
545
  (if (i32.eqz (local.get $nlen)) (then (return (local.get $from))))
@@ -598,15 +785,41 @@ export default (ctx) => {
598
785
  // length too, so this departs from strict JS string immutability for the rare
599
786
  // `let b = a; a += x` aliasing case. The fast path can't trigger when other
600
787
  // allocations have happened since `a` was created (it's no longer at heap top).
601
- // Only emitted for own-memory mode; shared memory falls back to slow path.
602
- const concatFast = !ctx.memory.shared ? `
788
+ // Only emitted for own-memory mode with a $__heap global; shared memory and
789
+ // alloc:false (which routes the heap pointer through memory[1020]) fall back.
790
+ // SSO-result fast path: both operands SSO and total ≤ 4 → repack inline,
791
+ // no alloc, no copy. Combined offset field = a's bytes | (b's bytes shifted
792
+ // by alen*8). New aux = SSO_BIT | total. Mode-independent (pointer arith
793
+ // only, no heap); fires whenever the upstream `'+'` couldn't fold at
794
+ // compile time (one or both operands runtime values). Critical for the
795
+ // identifier-style `'$' + x` / `prefix + s` patterns hot in parsers.
796
+ const ssoResultFast = `
797
+ (if (i32.and
798
+ (i32.and
799
+ (i64.ne (i64.and (local.get $a) (i64.const ${SSO_BIT_I64})) (i64.const 0))
800
+ (i64.ne (i64.and (local.get $b) (i64.const ${SSO_BIT_I64})) (i64.const 0)))
801
+ (i32.le_u (local.get $total) (i32.const 4)))
802
+ (then
803
+ (return (call $__mkptr
804
+ (i32.const ${PTR.STRING})
805
+ (i32.or (i32.const ${LAYOUT.SSO_BIT}) (local.get $total))
806
+ (i32.or
807
+ (i32.wrap_i64 (i64.and (local.get $a) (i64.const 0xFFFFFFFF)))
808
+ (i32.shl
809
+ (i32.wrap_i64 (i64.and (local.get $b) (i64.const 0xFFFFFFFF)))
810
+ (i32.shl (local.get $alen) (i32.const 3))))))))`
811
+
812
+ const concatFast = !ctx.memory.shared && ctx.transform.alloc !== false ? `
603
813
  (local.set $ta (i32.wrap_i64 (i64.and (i64.shr_u (local.get $a) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
604
814
  (local.set $aoff (i32.wrap_i64 (i64.and (local.get $a) (i64.const ${LAYOUT.OFFSET_MASK}))))
605
- ;; Bump-extend requires heap STRING (not SSO — its offset holds packed bytes).
815
+ ;; Bump-extend requires an OWN heap STRING not SSO (offset holds packed bytes)
816
+ ;; and not a slice/view (bumping would corrupt the parent buffer it points into).
606
817
  (if (i32.and
607
818
  (i32.and
608
819
  (i32.eq (local.get $ta) (i32.const ${PTR.STRING}))
609
- (i64.eqz (i64.and (local.get $a) (i64.const ${SSO_BIT_I64}))))
820
+ (i32.and
821
+ (i64.eqz (i64.and (local.get $a) (i64.const ${SSO_BIT_I64})))
822
+ (i64.eqz (i64.and (local.get $a) (i64.const ${SLICE_BIT_I64})))))
610
823
  (i32.eq
611
824
  (i32.and (i32.add (i32.add (local.get $aoff) (local.get $alen)) (i32.const 7)) (i32.const -8))
612
825
  (global.get $__heap)))
@@ -633,12 +846,15 @@ export default (ctx) => {
633
846
  (local $newHeap i32) (local $off i32) (local $total i32)
634
847
  (local.set $ta (i32.wrap_i64 (i64.and (i64.shr_u (local.get $a) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
635
848
  (local.set $aoff (i32.wrap_i64 (i64.and (local.get $a) (i64.const ${LAYOUT.OFFSET_MASK}))))
636
- ;; Heap STRING at heap top: bump-extend by 1 byte (own-memory mode only).
637
- ;; Gate on STRING tag AND !SSO_BIT — for SSO, $aoff holds packed bytes (not a heap addr).
638
- ${!ctx.memory.shared ? `
849
+ ;; Heap STRING at heap top: bump-extend by 1 byte (own-memory mode w/ $__heap global only).
850
+ ;; Gate on STRING tag AND !SSO_BIT ($aoff would hold packed bytes) AND !SLICE_BIT
851
+ ;; (a view's $aoff points into a parent buffer — bumping it would corrupt the parent).
852
+ ${!ctx.memory.shared && ctx.transform.alloc !== false ? `
639
853
  (if (i32.and
640
854
  (i32.eq (local.get $ta) (i32.const ${PTR.STRING}))
641
- (i64.eqz (i64.and (local.get $a) (i64.const ${SSO_BIT_I64}))))
855
+ (i32.and
856
+ (i64.eqz (i64.and (local.get $a) (i64.const ${SSO_BIT_I64})))
857
+ (i64.eqz (i64.and (local.get $a) (i64.const ${SLICE_BIT_I64})))))
642
858
  (then
643
859
  (local.set $alen (i32.load (i32.sub (local.get $aoff) (i32.const 4))))
644
860
  (if (i32.eq
@@ -696,6 +912,7 @@ export default (ctx) => {
696
912
  (local.set $total (i32.add (local.get $alen) (local.get $blen)))
697
913
  (if (i32.eqz (local.get $total))
698
914
  (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
915
+ ${ssoResultFast}
699
916
  ${concatFast}
700
917
  (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $total))))
701
918
  (i32.store (local.get $off) (local.get $total))
@@ -712,6 +929,7 @@ export default (ctx) => {
712
929
  (local.set $total (i32.add (local.get $alen) (local.get $blen)))
713
930
  (if (i32.eqz (local.get $total))
714
931
  (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
932
+ ${ssoResultFast}
715
933
  ${concatFast}
716
934
  (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $total))))
717
935
  (i32.store (local.get $off) (local.get $total))
@@ -752,12 +970,27 @@ export default (ctx) => {
752
970
  (br $next)))
753
971
  (f64.reinterpret_i64 (local.get $result)))`
754
972
 
973
+ // Empty separator: per JS spec, split into individual byte-chars
974
+ // ("abc".split("") -> ["a","b","c"], "".split("") -> []). Without this
975
+ // guard the main loop advances by plen=0 and spins forever.
755
976
  ctx.core.stdlib['__str_split'] = `(func $__str_split (param $str i64) (param $sep i64) (result f64)
756
977
  (local $slen i32) (local $plen i32) (local $count i32)
757
978
  (local $i i32) (local $j i32) (local $match i32)
758
979
  (local $arr i32) (local $piece_start i32) (local $piece_idx i32)
759
980
  (local.set $slen (call $__str_byteLen (local.get $str)))
760
981
  (local.set $plen (call $__str_byteLen (local.get $sep)))
982
+ (if (i32.eqz (local.get $plen)) (then
983
+ (local.set $arr (call $__alloc (i32.add (i32.const 8) (i32.shl (local.get $slen) (i32.const 3)))))
984
+ (i32.store (local.get $arr) (local.get $slen))
985
+ (i32.store (i32.add (local.get $arr) (i32.const 4)) (local.get $slen))
986
+ (local.set $arr (i32.add (local.get $arr) (i32.const 8)))
987
+ (block $de (loop $le
988
+ (br_if $de (i32.ge_s (local.get $i) (local.get $slen)))
989
+ (f64.store (i32.add (local.get $arr) (i32.shl (local.get $i) (i32.const 3)))
990
+ (call $__str_slice (local.get $str) (local.get $i) (i32.add (local.get $i) (i32.const 1))))
991
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
992
+ (br $le)))
993
+ (return (call $__mkptr (i32.const 1) (i32.const 0) (local.get $arr)))))
761
994
  (local.set $count (i32.const 1))
762
995
  (local.set $i (i32.const 0))
763
996
  (block $d1 (loop $l1
@@ -881,26 +1114,64 @@ export default (ctx) => {
881
1114
  // override to return the primitive — strings already covered by .string:valueOf.
882
1115
  ctx.core.emit['.valueOf'] = (val) => asF64(emit(val))
883
1116
 
884
- ctx.core.emit['.string:slice'] = (str, start, end) => {
885
- inc('__str_slice')
1117
+ // `.slice` lowering, parametrised on the backing helper: __str_slice copies
1118
+ // bytes; __str_slice_view returns a no-copy SLICE_BIT view — used only when
1119
+ // escape analysis proved the result never outlives its parent buffer (see
1120
+ // scanSliceViews / emitDecl). The `#view` key cannot be produced by method
1121
+ // dispatch (`#` is not a legal identifier char), so the view variant is
1122
+ // reachable only through the explicit emitDecl route.
1123
+ const sliceEmitter = (fn) => (str, start, end) => {
1124
+ inc(fn)
886
1125
  const startIR = start == null ? ['i32.const', 0] : asI32(emit(start))
887
- if (end != null) return typed(['call', '$__str_slice', asI64(emit(str)), startIR, asI32(emit(end))], 'f64')
1126
+ if (end != null) return typed(['call', `$${fn}`, asI64(emit(str)), startIR, asI32(emit(end))], 'f64')
888
1127
  const t = temp('t')
889
1128
  return typed(['block', ['result', 'f64'],
890
1129
  ['local.set', `$${t}`, asF64(emit(str))],
891
- ['call', '$__str_slice', ['i64.reinterpret_f64', ['local.get', `$${t}`]], startIR,
1130
+ ['call', `$${fn}`, ['i64.reinterpret_f64', ['local.get', `$${t}`]], startIR,
892
1131
  ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]], 'f64')
893
1132
  }
1133
+ ctx.core.emit['.string:slice'] = sliceEmitter('__str_slice')
1134
+ ctx.core.emit['.string:slice#view'] = sliceEmitter('__str_slice_view')
1135
+
1136
+ // ToIntegerOrInfinity for a string-method position argument: ToNumber (so
1137
+ // string / boolean / null / undefined positions coerce per spec) then trunc.
1138
+ // trunc_sat maps NaN→0 and ±∞→±maxint — both clamp correctly downstream.
1139
+ // `toNumF64` routes an object position through `valueOf`/`toString`
1140
+ // (ToPrimitive), so a throwing method propagates as an abrupt completion.
1141
+ const posIndex = (node) => {
1142
+ if (node == null) return ['i32.const', 0]
1143
+ return asI32(toNumF64(node, emit(node)))
1144
+ }
1145
+
1146
+ // ToString(searchString) per spec step 3. __str_indexof's internal __to_str
1147
+ // covers string/number/null/undefined needles, but two cases need help here:
1148
+ // a BOOL rides the 0/1 carrier (→ "0"/"1" not "true"/"false"), and an OBJECT
1149
+ // needs compile-time ToPrimitive(string) (__to_str can't invoke user toString).
1150
+ const searchArg = (search) =>
1151
+ valTypeOf(search) === VAL.BOOL ? asI64(emitBoolStr(search)) :
1152
+ valTypeOf(search) === VAL.OBJECT ? toStrI64(search, emit(search)) : asI64(emit(search))
894
1153
 
895
1154
  ctx.core.emit['.string:indexOf'] = (str, search, from) => {
896
1155
  inc('__str_indexof')
897
- return typed(['f64.convert_i32_s', ['call', '$__str_indexof', asI64(emit(str)), asI64(emit(search)), from ? asI32(emit(from)) : ['i32.const', 0]]], 'f64')
1156
+ const hay = asI64(emit(str)), ndl = searchArg(search)
1157
+ return typed(['f64.convert_i32_s', ['call', '$__str_indexof', hay, ndl, posIndex(from)]], 'f64')
898
1158
  }
899
1159
 
900
- ctx.core.emit['.string:includes'] = (str, search) => {
1160
+ // String.prototype.{includes,startsWith,endsWith} run IsRegExp(searchString)
1161
+ // and throw a TypeError when it is a RegExp. Detect a regex-typed search arg
1162
+ // at compile time and lower to a $__jz_err throw.
1163
+ const regexpSearchGuard = (search) => {
1164
+ if (valTypeOf(search) !== VAL.REGEX) return null
1165
+ ctx.runtime.throws = true
1166
+ return typed(['block', ['result', 'f64'], ['throw', '$__jz_err', ['f64.const', 0]]], 'f64')
1167
+ }
1168
+
1169
+ ctx.core.emit['.string:includes'] = (str, search, from) => {
1170
+ const guard = regexpSearchGuard(search); if (guard) return guard
901
1171
  inc('__str_indexof')
1172
+ const hay = asI64(emit(str)), ndl = searchArg(search)
902
1173
  return typed(['f64.convert_i32_s',
903
- ['i32.ge_s', ['call', '$__str_indexof', asI64(emit(str)), asI64(emit(search)), ['i32.const', 0]], ['i32.const', 0]]], 'f64')
1174
+ ['i32.ge_s', ['call', '$__str_indexof', hay, ndl, posIndex(from)], ['i32.const', 0]]], 'f64')
904
1175
  }
905
1176
 
906
1177
  // Generic (no collision)
@@ -914,20 +1185,47 @@ export default (ctx) => {
914
1185
  ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]], 'f64')
915
1186
  }
916
1187
 
917
- // Factory for simple str→call patterns: [emitKey, stdlibName, argCoercions, i32Result?]
918
- const coerce = { f: asF64, i: asI32 }
919
- const strMethod = (name, args, i32Result) => (str, ...params) => {
920
- inc(name)
921
- const call = ['call', `$${name}`, asF64(emit(str)), ...params.map((p, i) => coerce[args[i]](emit(p)))]
922
- return typed(i32Result ? ['f64.convert_i32_s', call] : call, 'f64')
1188
+ // .substr(start, length) Annex B / legacy. Equivalent to substring(start, start+length).
1189
+ // __str_substring clamps end to byteLen and start/end to [0, byteLen], so negative
1190
+ // values are floored to 0 (matches v8 for length<0 empty; for start<0 spec wants
1191
+ // max(0, len+start), which we don't implement — rare in practice).
1192
+ ctx.core.emit['.substr'] = (str, start, length) => {
1193
+ inc('__str_substring')
1194
+ if (length != null) {
1195
+ const s = tempI32('substrS')
1196
+ return typed(['block', ['result', 'f64'],
1197
+ ['local.set', `$${s}`, asI32(emit(start))],
1198
+ ['call', '$__str_substring', asI64(emit(str)),
1199
+ ['local.get', `$${s}`],
1200
+ ['i32.add', ['local.get', `$${s}`], asI32(emit(length))]]
1201
+ ], 'f64')
1202
+ }
1203
+ const t = temp('t')
1204
+ return typed(['block', ['result', 'f64'],
1205
+ ['local.set', `$${t}`, asF64(emit(str))],
1206
+ ['call', '$__str_substring', ['i64.reinterpret_f64', ['local.get', `$${t}`]], asI32(emit(start)),
1207
+ ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]], 'f64')
923
1208
  }
924
1209
 
1210
+ // Declarative emitter for regular `(recv, ...args) → call($stdlib, coerced...)` methods.
1211
+ // `coerce` is a per-argument code string ('I' i64 · 'F' f64 · 'i' i32); index 0 is the receiver.
1212
+ // `ret` is the helper's result type — 'f64' is returned directly, 'i32' is widened to the f64 ABI.
1213
+ const CO = { I: asI64, F: asF64, i: asI32 }
1214
+ const method = (stdlib, coerce, ret = 'f64') => emitter([stdlib], (...nodes) => {
1215
+ const c = ['call', `$${stdlib}`, ...nodes.map((n, i) => CO[coerce[i]](emit(n)))]
1216
+ return typed(ret === 'i32' ? ['f64.convert_i32_s', c] : c, 'f64')
1217
+ })
1218
+
925
1219
  // Search args go through ToString per spec — coerce non-string-typed args
926
1220
  // via __to_str so the underlying byte-compare receives an actual string.
927
1221
  const stringSearchMethod = (name) => (str, sfx) => {
1222
+ const guard = regexpSearchGuard(sfx); if (guard) return guard
928
1223
  inc(name)
929
- let sfxArg = asI64(emit(sfx))
930
- if (valTypeOf(sfx) !== VAL.STRING) {
1224
+ const esfx = emit(sfx)
1225
+ let sfxArg = asI64(esfx)
1226
+ if (valTypeOf(sfx) === VAL.OBJECT) {
1227
+ sfxArg = toStrI64(sfx, esfx)
1228
+ } else if (valTypeOf(sfx) !== VAL.STRING) {
931
1229
  inc('__to_str')
932
1230
  sfxArg = ['call', '$__to_str', sfxArg]
933
1231
  }
@@ -935,20 +1233,13 @@ export default (ctx) => {
935
1233
  }
936
1234
  ctx.core.emit['.startsWith'] = stringSearchMethod('__str_startswith')
937
1235
  ctx.core.emit['.endsWith'] = stringSearchMethod('__str_endswith')
938
- ctx.core.emit['.trim'] = (str) => (inc('__str_trim'),
939
- typed(['call', '$__str_trim', asI64(emit(str))], 'f64'))
940
- ctx.core.emit['.trimStart'] = (str) => (inc('__str_trimStart'),
941
- typed(['call', '$__str_trimStart', asI64(emit(str))], 'f64'))
942
- ctx.core.emit['.trimEnd'] = (str) => (inc('__str_trimEnd'),
943
- typed(['call', '$__str_trimEnd', asI64(emit(str))], 'f64'))
944
- ctx.core.emit['.repeat'] = (str, n) => (inc('__str_repeat'),
945
- typed(['call', '$__str_repeat', asI64(emit(str)), asI32(emit(n))], 'f64'))
946
- ctx.core.emit['.split'] = (str, sep) => (inc('__str_split'),
947
- typed(['call', '$__str_split', asI64(emit(str)), asI64(emit(sep))], 'f64'))
948
- ctx.core.emit['.replace'] = (str, search, repl) => (inc('__str_replace'),
949
- typed(['call', '$__str_replace', asI64(emit(str)), asI64(emit(search)), asI64(emit(repl))], 'f64'))
950
- ctx.core.emit['.replaceAll'] = (str, search, repl) => (inc('__str_replaceall'),
951
- typed(['call', '$__str_replaceall', asI64(emit(str)), asI64(emit(search)), asI64(emit(repl))], 'f64'))
1236
+ ctx.core.emit['.trim'] = method('__str_trim', 'I')
1237
+ ctx.core.emit['.trimStart'] = method('__str_trimStart', 'I')
1238
+ ctx.core.emit['.trimEnd'] = method('__str_trimEnd', 'I')
1239
+ ctx.core.emit['.repeat'] = method('__str_repeat', 'Ii')
1240
+ ctx.core.emit['.split'] = method('__str_split', 'II')
1241
+ ctx.core.emit['.replace'] = method('__str_replace', 'III')
1242
+ ctx.core.emit['.replaceAll'] = method('__str_replaceall', 'III')
952
1243
 
953
1244
  ctx.core.emit['.toUpperCase'] = (str) => {
954
1245
  inc('__str_case')
@@ -972,10 +1263,7 @@ export default (ctx) => {
972
1263
  // the spec exactly; for non-ASCII it follows UTF-8 byte order, which is
973
1264
  // codepoint order for well-formed strings — close enough for sort-stability
974
1265
  // use cases, wrong for human-language collation.
975
- ctx.core.emit['.localeCompare'] = (str, other) => {
976
- inc('__str_cmp')
977
- return typed(['f64.convert_i32_s', ['call', '$__str_cmp', asI64(emit(str)), asI64(emit(other))]], 'f64')
978
- }
1266
+ ctx.core.emit['.localeCompare'] = method('__str_cmp', 'II', 'i32')
979
1267
 
980
1268
  ctx.core.emit['.string:concat'] = (str, ...others) => {
981
1269
  inc('__str_concat')
@@ -984,10 +1272,15 @@ export default (ctx) => {
984
1272
  return result
985
1273
  }
986
1274
 
1275
+ // A VAL.BOOL part rides the 0/1 carrier, so __to_str would render "1"/"0".
1276
+ // emitBoolStr selects the interned "true"/"false" literal (constant-folded
1277
+ // when the operand is known); every other part goes through __to_str.
1278
+ const partStrI64 = (p) => valTypeOf(p) === VAL.BOOL ? asI64(emitBoolStr(p)) : toStrI64(p, emit(p))
1279
+
987
1280
  ctx.core.emit['strcat'] = (...parts) => {
988
1281
  inc('__to_str', '__str_byteLen', '__alloc', '__mkptr', '__str_copy')
989
1282
  if (!parts.length) return mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT, 0)
990
- if (parts.length === 1) return typed(['f64.reinterpret_i64', ['call', '$__to_str', asI64(emit(parts[0]))]], 'f64')
1283
+ if (parts.length === 1) return typed(['f64.reinterpret_i64', partStrI64(parts[0])], 'f64')
991
1284
 
992
1285
  const vals = parts.map(() => temp('s'))
993
1286
  const lens = parts.map(() => tempI32('sl'))
@@ -997,7 +1290,7 @@ export default (ctx) => {
997
1290
  const ir = []
998
1291
 
999
1292
  for (let i = 0; i < parts.length; i++) {
1000
- ir.push(['local.set', `$${vals[i]}`, ['f64.reinterpret_i64', ['call', '$__to_str', asI64(emit(parts[i]))]]])
1293
+ ir.push(['local.set', `$${vals[i]}`, ['f64.reinterpret_i64', partStrI64(parts[i])]])
1001
1294
  ir.push(['local.set', `$${lens[i]}`, ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${vals[i]}`]]]])
1002
1295
  }
1003
1296
  ir.push(['local.set', `$${total}`, ['i32.const', 0]])
@@ -1042,35 +1335,47 @@ export default (ctx) => {
1042
1335
  mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1, ['local.get', `$${t}`])], 'f64')
1043
1336
  }
1044
1337
 
1045
- // .charCodeAt(i) → integer char code (0..255 for ASCII bytes unsigned, always
1046
- // representable as i32). Returning i32 directly lets `let c = s.charCodeAt(i)`
1047
- // stay on the i32 ABI: chained comparisons (`c >= 48 && c <= 57`), bit-ops, and
1048
- // `c - 48` arithmetic skip the per-iteration f64 widen + i32 trunc round-trip.
1049
- ctx.core.emit['.charCodeAt'] = (str, idx) => {
1050
- inc('__char_at')
1051
- return typed(['call', '$__char_at', asI64(emit(str)), asI32(emit(idx))], 'i32')
1052
- }
1338
+ // .charCodeAt(i) → JS-spec char code: the UTF-16 code unit at `i`, or NaN
1339
+ // when `i` is out of range (`i < 0 || i >= length`). Result is f64 because
1340
+ // NaN is not representable as i32 an i32 `0` sentinel for OOB silently
1341
+ // miscompiles any reader that distinguishes 0 from NaN, e.g. the parser hot
1342
+ // loop `while ((cc = s.charCodeAt(i++)) <= 32) {}` would never terminate
1343
+ // (`0 <= 32` is true, `NaN <= 32` is false). The narrower may re-narrow the
1344
+ // result to i32 where it can prove the index in-bounds.
1345
+ ctx.core.emit['.charCodeAt'] = (str, idx) =>
1346
+ typed(ctx.abi.string.ops.charCodeAt(asF64(emit(str)), asI32(emit(idx)), ctx, true), 'f64')
1053
1347
 
1054
1348
  // String.fromCharCode(code) → 1-char SSO string
1055
1349
  ctx.core.emit['String'] = (value) => {
1056
1350
  if (value === undefined) return emit(['str', ''])
1057
1351
  if (valTypeOf(value) === VAL.STRING) return emit(value)
1352
+ if (valTypeOf(value) === VAL.BOOL) return emitBoolStr(value)
1058
1353
  if (valTypeOf(value) === VAL.NUMBER) {
1059
1354
  inc('__ftoa')
1060
1355
  return typed(['call', '$__ftoa', asF64(emit(value)), ['i32.const', 0], ['i32.const', 0]], 'f64')
1061
1356
  }
1062
- inc('__to_str')
1063
- return typed(['f64.reinterpret_i64', ['call', '$__to_str', asI64(emit(value))]], 'f64')
1357
+ return typed(['f64.reinterpret_i64', toStrI64(value, emit(value))], 'f64')
1064
1358
  }
1065
1359
 
1066
1360
  ctx.core.emit['String.fromCharCode'] = (code) => {
1067
1361
  if (code === undefined) return emit(['str', ''])
1068
- return mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1, asI32(emit(code)))
1362
+ // ToUint16(ToNumber(code)): `toNumF64` performs ToPrimitive on an object
1363
+ // argument, so a throwing valueOf/toString propagates per spec.
1364
+ return mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1, asI32(toNumF64(code, emit(code))))
1069
1365
  }
1070
1366
 
1071
- // String.fromCodePoint(cp) → UTF-8 encoded string
1072
- ctx.core.stdlib['__fromCodePoint'] = `(func $__fromCodePoint (param $cp i32) (result f64)
1073
- (local $off i32) (local $len i32)
1367
+ // String.fromCodePoint(cp) → UTF-8 encoded string for one code point.
1368
+ // Param is f64 (already ToNumber-coerced); throws RangeError ($__jz_err) when
1369
+ // the value is not an integer in [0, 0x10FFFF] (22.1.2.2 step 5.d).
1370
+ ctx.core.stdlib['__fromCodePoint'] = `(func $__fromCodePoint (param $cpf f64) (result f64)
1371
+ (local $cp i32) (local $off i32) (local $len i32)
1372
+ (if (i32.or
1373
+ (i32.or
1374
+ (f64.ne (f64.trunc (local.get $cpf)) (local.get $cpf))
1375
+ (f64.lt (local.get $cpf) (f64.const 0)))
1376
+ (f64.gt (local.get $cpf) (f64.const 0x10FFFF)))
1377
+ (then (throw $__jz_err (f64.const 0))))
1378
+ (local.set $cp (i32.trunc_sat_f64_s (local.get $cpf)))
1074
1379
  ;; ASCII: 1 byte SSO
1075
1380
  (if (i32.lt_u (local.get $cp) (i32.const 128))
1076
1381
  (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT | 1}) (local.get $cp)))))
@@ -1095,10 +1400,164 @@ export default (ctx) => {
1095
1400
  (i32.shl (i32.or (i32.const 0x80) (i32.and (i32.shr_u (local.get $cp) (i32.const 6)) (i32.const 0x3F))) (i32.const 16)))
1096
1401
  (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 24))))))`
1097
1402
 
1098
- ctx.core.emit['String.fromCodePoint'] = (code) => {
1099
- if (code === undefined) return emit(['str', ''])
1403
+ // String.fromCodePoint(...codePoints) variadic; each arg is ToNumber-coerced
1404
+ // then validated/encoded by __fromCodePoint, results concatenated left to right.
1405
+ ctx.core.emit['String.fromCodePoint'] = (...codes) => {
1406
+ if (codes.length === 0) return emit(['str', ''])
1407
+ ctx.runtime.throws = true
1100
1408
  inc('__fromCodePoint')
1101
- return typed(['call', '$__fromCodePoint', asI32(emit(code))], 'f64')
1409
+ const one = (node) => typed(['call', '$__fromCodePoint',
1410
+ toNumF64(node, emit(node))], 'f64')
1411
+ let r = one(codes[0])
1412
+ for (let i = 1; i < codes.length; i++) {
1413
+ inc('__str_concat_raw')
1414
+ r = typed(['call', '$__str_concat_raw', asI64(r), asI64(one(codes[i]))], 'f64')
1415
+ }
1416
+ return r
1417
+ }
1418
+
1419
+ ctx.core.stdlib['__encodeURIComponent'] = `(func $__encodeURIComponent (param $val i64) (result f64)
1420
+ (local $str i64) (local $slen i32) (local $base i32) (local $out i32)
1421
+ (local $i i32) (local $j i32) (local $c i32) (local $hi i32) (local $lo i32)
1422
+ (local.set $str (call $__to_str (local.get $val)))
1423
+ (local.set $slen (call $__str_byteLen (local.get $str)))
1424
+ (if (i32.eqz (local.get $slen))
1425
+ (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
1426
+ (local.set $base (call $__alloc (i32.add (i32.const 4) (i32.mul (local.get $slen) (i32.const 3)))))
1427
+ (local.set $out (i32.add (local.get $base) (i32.const 4)))
1428
+ (block $done (loop $loop
1429
+ (br_if $done (i32.ge_u (local.get $i) (local.get $slen)))
1430
+ (local.set $c (call $__char_at (local.get $str) (local.get $i)))
1431
+ (if (i32.or
1432
+ (i32.or
1433
+ (i32.or
1434
+ (i32.and (i32.ge_u (local.get $c) (i32.const 65)) (i32.le_u (local.get $c) (i32.const 90)))
1435
+ (i32.and (i32.ge_u (local.get $c) (i32.const 97)) (i32.le_u (local.get $c) (i32.const 122))))
1436
+ (i32.and (i32.ge_u (local.get $c) (i32.const 48)) (i32.le_u (local.get $c) (i32.const 57))))
1437
+ (i32.or
1438
+ (i32.or
1439
+ (i32.or (i32.eq (local.get $c) (i32.const 45)) (i32.eq (local.get $c) (i32.const 95)))
1440
+ (i32.or (i32.eq (local.get $c) (i32.const 46)) (i32.eq (local.get $c) (i32.const 33))))
1441
+ (i32.or
1442
+ (i32.or (i32.eq (local.get $c) (i32.const 126)) (i32.eq (local.get $c) (i32.const 42)))
1443
+ (i32.or
1444
+ (i32.eq (local.get $c) (i32.const 39))
1445
+ (i32.or (i32.eq (local.get $c) (i32.const 40)) (i32.eq (local.get $c) (i32.const 41)))))))
1446
+ (then
1447
+ (i32.store8 (i32.add (local.get $out) (local.get $j)) (local.get $c))
1448
+ (local.set $j (i32.add (local.get $j) (i32.const 1))))
1449
+ (else
1450
+ (local.set $hi (i32.shr_u (local.get $c) (i32.const 4)))
1451
+ (local.set $lo (i32.and (local.get $c) (i32.const 15)))
1452
+ (i32.store8 (i32.add (local.get $out) (local.get $j)) (i32.const 37))
1453
+ (i32.store8 (i32.add (local.get $out) (i32.add (local.get $j) (i32.const 1)))
1454
+ (i32.add (local.get $hi) (select (i32.const 55) (i32.const 48) (i32.gt_u (local.get $hi) (i32.const 9)))))
1455
+ (i32.store8 (i32.add (local.get $out) (i32.add (local.get $j) (i32.const 2)))
1456
+ (i32.add (local.get $lo) (select (i32.const 55) (i32.const 48) (i32.gt_u (local.get $lo) (i32.const 9)))))
1457
+ (local.set $j (i32.add (local.get $j) (i32.const 3)))))
1458
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
1459
+ (br $loop)))
1460
+ (i32.store (local.get $base) (local.get $j))
1461
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $out)))`
1462
+
1463
+ ctx.core.emit['encodeURIComponent'] = (value) => {
1464
+ inc('__encodeURIComponent')
1465
+ const input = value === undefined ? ['i64.const', UNDEF_NAN] : asI64(emit(value))
1466
+ return typed(['call', '$__encodeURIComponent', input], 'f64')
1467
+ }
1468
+
1469
+ ctx.core.stdlib['__uri_hex'] = `(func $__uri_hex (param $c i32) (result i32)
1470
+ (if (result i32) (i32.and (i32.ge_u (local.get $c) (i32.const 48)) (i32.le_u (local.get $c) (i32.const 57)))
1471
+ (then (i32.sub (local.get $c) (i32.const 48)))
1472
+ (else (if (result i32) (i32.and (i32.ge_u (local.get $c) (i32.const 65)) (i32.le_u (local.get $c) (i32.const 70)))
1473
+ (then (i32.sub (local.get $c) (i32.const 55)))
1474
+ (else (if (result i32) (i32.and (i32.ge_u (local.get $c) (i32.const 97)) (i32.le_u (local.get $c) (i32.const 102)))
1475
+ (then (i32.sub (local.get $c) (i32.const 87)))
1476
+ (else (i32.const -1))))))))`
1477
+
1478
+ ctx.core.stdlib['__decodeURIComponent'] = `(func $__decodeURIComponent (param $v i64) (result f64)
1479
+ (local $s i64) (local $len i32) (local $i i32)
1480
+ (local $base i32) (local $dst i32) (local $outLen i32)
1481
+ (local $c i32) (local $hi i32) (local $lo i32)
1482
+ (local $b i32) (local $n i32) (local $j i32) (local $cp i32) (local $min i32) (local $stored i32)
1483
+ (local.set $s (call $__to_str (local.get $v)))
1484
+ (local.set $len (call $__str_byteLen (local.get $s)))
1485
+ (local.set $base (call $__alloc (i32.add (i32.const 4) (local.get $len))))
1486
+ (local.set $dst (i32.add (local.get $base) (i32.const 4)))
1487
+ (block $done (loop $loop
1488
+ (br_if $done (i32.ge_s (local.get $i) (local.get $len)))
1489
+ (local.set $stored (i32.const 0))
1490
+ (local.set $c (call $__char_at (local.get $s) (local.get $i)))
1491
+ (if (i32.eq (local.get $c) (i32.const 37))
1492
+ (then
1493
+ (if (i32.ge_s (i32.add (local.get $i) (i32.const 2)) (local.get $len))
1494
+ (then (throw $__jz_err (f64.const 0))))
1495
+ (local.set $hi (call $__uri_hex (call $__char_at (local.get $s) (i32.add (local.get $i) (i32.const 1)))))
1496
+ (local.set $lo (call $__uri_hex (call $__char_at (local.get $s) (i32.add (local.get $i) (i32.const 2)))))
1497
+ (if (i32.or (i32.lt_s (local.get $hi) (i32.const 0)) (i32.lt_s (local.get $lo) (i32.const 0)))
1498
+ (then (throw $__jz_err (f64.const 0))))
1499
+ (local.set $c (i32.or (i32.shl (local.get $hi) (i32.const 4)) (local.get $lo)))
1500
+ (local.set $i (i32.add (local.get $i) (i32.const 3)))
1501
+ (if (i32.ge_u (local.get $c) (i32.const 128))
1502
+ (then
1503
+ (if (i32.and (i32.ge_u (local.get $c) (i32.const 0xC2)) (i32.le_u (local.get $c) (i32.const 0xDF)))
1504
+ (then
1505
+ (local.set $n (i32.const 2))
1506
+ (local.set $cp (i32.and (local.get $c) (i32.const 0x1F)))
1507
+ (local.set $min (i32.const 0x80)))
1508
+ (else (if (i32.and (i32.ge_u (local.get $c) (i32.const 0xE0)) (i32.le_u (local.get $c) (i32.const 0xEF)))
1509
+ (then
1510
+ (local.set $n (i32.const 3))
1511
+ (local.set $cp (i32.and (local.get $c) (i32.const 0x0F)))
1512
+ (local.set $min (i32.const 0x800)))
1513
+ (else (if (i32.and (i32.ge_u (local.get $c) (i32.const 0xF0)) (i32.le_u (local.get $c) (i32.const 0xF4)))
1514
+ (then
1515
+ (local.set $n (i32.const 4))
1516
+ (local.set $cp (i32.and (local.get $c) (i32.const 0x07)))
1517
+ (local.set $min (i32.const 0x10000)))
1518
+ (else (throw $__jz_err (f64.const 0))))))))
1519
+ (i32.store8 (i32.add (local.get $dst) (local.get $outLen)) (local.get $c))
1520
+ (local.set $outLen (i32.add (local.get $outLen) (i32.const 1)))
1521
+ (local.set $j (i32.const 1))
1522
+ (block $seqDone (loop $seq
1523
+ (br_if $seqDone (i32.ge_s (local.get $j) (local.get $n)))
1524
+ (if (i32.ge_s (i32.add (local.get $i) (i32.const 2)) (local.get $len))
1525
+ (then (throw $__jz_err (f64.const 0))))
1526
+ (if (i32.ne (call $__char_at (local.get $s) (local.get $i)) (i32.const 37))
1527
+ (then (throw $__jz_err (f64.const 0))))
1528
+ (local.set $hi (call $__uri_hex (call $__char_at (local.get $s) (i32.add (local.get $i) (i32.const 1)))))
1529
+ (local.set $lo (call $__uri_hex (call $__char_at (local.get $s) (i32.add (local.get $i) (i32.const 2)))))
1530
+ (if (i32.or (i32.lt_s (local.get $hi) (i32.const 0)) (i32.lt_s (local.get $lo) (i32.const 0)))
1531
+ (then (throw $__jz_err (f64.const 0))))
1532
+ (local.set $b (i32.or (i32.shl (local.get $hi) (i32.const 4)) (local.get $lo)))
1533
+ (if (i32.or (i32.lt_u (local.get $b) (i32.const 0x80)) (i32.gt_u (local.get $b) (i32.const 0xBF)))
1534
+ (then (throw $__jz_err (f64.const 0))))
1535
+ (local.set $cp (i32.or (i32.shl (local.get $cp) (i32.const 6)) (i32.and (local.get $b) (i32.const 0x3F))))
1536
+ (i32.store8 (i32.add (local.get $dst) (local.get $outLen)) (local.get $b))
1537
+ (local.set $outLen (i32.add (local.get $outLen) (i32.const 1)))
1538
+ (local.set $i (i32.add (local.get $i) (i32.const 3)))
1539
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
1540
+ (br $seq)))
1541
+ (if (i32.or
1542
+ (i32.or (i32.lt_u (local.get $cp) (local.get $min)) (i32.gt_u (local.get $cp) (i32.const 0x10FFFF)))
1543
+ (i32.and (i32.ge_u (local.get $cp) (i32.const 0xD800)) (i32.le_u (local.get $cp) (i32.const 0xDFFF))))
1544
+ (then (throw $__jz_err (f64.const 0))))
1545
+ (local.set $stored (i32.const 1)))))
1546
+ (else
1547
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))))
1548
+ (if (i32.eqz (local.get $stored))
1549
+ (then
1550
+ (i32.store8 (i32.add (local.get $dst) (local.get $outLen)) (local.get $c))
1551
+ (local.set $outLen (i32.add (local.get $outLen) (i32.const 1)))))
1552
+ (br $loop)))
1553
+ (i32.store (local.get $base) (local.get $outLen))
1554
+ (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $dst)))`
1555
+
1556
+ ctx.core.emit['decodeURIComponent'] = (value) => {
1557
+ ctx.runtime.throws = true
1558
+ inc('__decodeURIComponent')
1559
+ return typed(['call', '$__decodeURIComponent',
1560
+ value === undefined ? ['i64.const', UNDEF_NAN] : asI64(emit(value))], 'f64')
1102
1561
  }
1103
1562
 
1104
1563
  // .at(i) → charAt with negative index support