jz 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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))
@@ -896,26 +1114,64 @@ export default (ctx) => {
896
1114
  // override to return the primitive — strings already covered by .string:valueOf.
897
1115
  ctx.core.emit['.valueOf'] = (val) => asF64(emit(val))
898
1116
 
899
- ctx.core.emit['.string:slice'] = (str, start, end) => {
900
- 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)
901
1125
  const startIR = start == null ? ['i32.const', 0] : asI32(emit(start))
902
- 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')
903
1127
  const t = temp('t')
904
1128
  return typed(['block', ['result', 'f64'],
905
1129
  ['local.set', `$${t}`, asF64(emit(str))],
906
- ['call', '$__str_slice', ['i64.reinterpret_f64', ['local.get', `$${t}`]], startIR,
1130
+ ['call', `$${fn}`, ['i64.reinterpret_f64', ['local.get', `$${t}`]], startIR,
907
1131
  ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]], 'f64')
908
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))
909
1153
 
910
1154
  ctx.core.emit['.string:indexOf'] = (str, search, from) => {
911
1155
  inc('__str_indexof')
912
- 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')
913
1158
  }
914
1159
 
915
- 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
916
1171
  inc('__str_indexof')
1172
+ const hay = asI64(emit(str)), ndl = searchArg(search)
917
1173
  return typed(['f64.convert_i32_s',
918
- ['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')
919
1175
  }
920
1176
 
921
1177
  // Generic (no collision)
@@ -929,20 +1185,47 @@ export default (ctx) => {
929
1185
  ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]], 'f64')
930
1186
  }
931
1187
 
932
- // Factory for simple str→call patterns: [emitKey, stdlibName, argCoercions, i32Result?]
933
- const coerce = { f: asF64, i: asI32 }
934
- const strMethod = (name, args, i32Result) => (str, ...params) => {
935
- inc(name)
936
- const call = ['call', `$${name}`, asF64(emit(str)), ...params.map((p, i) => coerce[args[i]](emit(p)))]
937
- 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')
938
1208
  }
939
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
+
940
1219
  // Search args go through ToString per spec — coerce non-string-typed args
941
1220
  // via __to_str so the underlying byte-compare receives an actual string.
942
1221
  const stringSearchMethod = (name) => (str, sfx) => {
1222
+ const guard = regexpSearchGuard(sfx); if (guard) return guard
943
1223
  inc(name)
944
- let sfxArg = asI64(emit(sfx))
945
- 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) {
946
1229
  inc('__to_str')
947
1230
  sfxArg = ['call', '$__to_str', sfxArg]
948
1231
  }
@@ -950,20 +1233,13 @@ export default (ctx) => {
950
1233
  }
951
1234
  ctx.core.emit['.startsWith'] = stringSearchMethod('__str_startswith')
952
1235
  ctx.core.emit['.endsWith'] = stringSearchMethod('__str_endswith')
953
- ctx.core.emit['.trim'] = (str) => (inc('__str_trim'),
954
- typed(['call', '$__str_trim', asI64(emit(str))], 'f64'))
955
- ctx.core.emit['.trimStart'] = (str) => (inc('__str_trimStart'),
956
- typed(['call', '$__str_trimStart', asI64(emit(str))], 'f64'))
957
- ctx.core.emit['.trimEnd'] = (str) => (inc('__str_trimEnd'),
958
- typed(['call', '$__str_trimEnd', asI64(emit(str))], 'f64'))
959
- ctx.core.emit['.repeat'] = (str, n) => (inc('__str_repeat'),
960
- typed(['call', '$__str_repeat', asI64(emit(str)), asI32(emit(n))], 'f64'))
961
- ctx.core.emit['.split'] = (str, sep) => (inc('__str_split'),
962
- typed(['call', '$__str_split', asI64(emit(str)), asI64(emit(sep))], 'f64'))
963
- ctx.core.emit['.replace'] = (str, search, repl) => (inc('__str_replace'),
964
- typed(['call', '$__str_replace', asI64(emit(str)), asI64(emit(search)), asI64(emit(repl))], 'f64'))
965
- ctx.core.emit['.replaceAll'] = (str, search, repl) => (inc('__str_replaceall'),
966
- 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')
967
1243
 
968
1244
  ctx.core.emit['.toUpperCase'] = (str) => {
969
1245
  inc('__str_case')
@@ -987,10 +1263,7 @@ export default (ctx) => {
987
1263
  // the spec exactly; for non-ASCII it follows UTF-8 byte order, which is
988
1264
  // codepoint order for well-formed strings — close enough for sort-stability
989
1265
  // use cases, wrong for human-language collation.
990
- ctx.core.emit['.localeCompare'] = (str, other) => {
991
- inc('__str_cmp')
992
- return typed(['f64.convert_i32_s', ['call', '$__str_cmp', asI64(emit(str)), asI64(emit(other))]], 'f64')
993
- }
1266
+ ctx.core.emit['.localeCompare'] = method('__str_cmp', 'II', 'i32')
994
1267
 
995
1268
  ctx.core.emit['.string:concat'] = (str, ...others) => {
996
1269
  inc('__str_concat')
@@ -999,10 +1272,15 @@ export default (ctx) => {
999
1272
  return result
1000
1273
  }
1001
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
+
1002
1280
  ctx.core.emit['strcat'] = (...parts) => {
1003
1281
  inc('__to_str', '__str_byteLen', '__alloc', '__mkptr', '__str_copy')
1004
1282
  if (!parts.length) return mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT, 0)
1005
- 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')
1006
1284
 
1007
1285
  const vals = parts.map(() => temp('s'))
1008
1286
  const lens = parts.map(() => tempI32('sl'))
@@ -1012,7 +1290,7 @@ export default (ctx) => {
1012
1290
  const ir = []
1013
1291
 
1014
1292
  for (let i = 0; i < parts.length; i++) {
1015
- 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])]])
1016
1294
  ir.push(['local.set', `$${lens[i]}`, ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${vals[i]}`]]]])
1017
1295
  }
1018
1296
  ir.push(['local.set', `$${total}`, ['i32.const', 0]])
@@ -1057,35 +1335,47 @@ export default (ctx) => {
1057
1335
  mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT | 1, ['local.get', `$${t}`])], 'f64')
1058
1336
  }
1059
1337
 
1060
- // .charCodeAt(i) → integer char code (0..255 for ASCII bytes unsigned, always
1061
- // representable as i32). Returning i32 directly lets `let c = s.charCodeAt(i)`
1062
- // stay on the i32 ABI: chained comparisons (`c >= 48 && c <= 57`), bit-ops, and
1063
- // `c - 48` arithmetic skip the per-iteration f64 widen + i32 trunc round-trip.
1064
- ctx.core.emit['.charCodeAt'] = (str, idx) => {
1065
- inc('__char_at')
1066
- return typed(['call', '$__char_at', asI64(emit(str)), asI32(emit(idx))], 'i32')
1067
- }
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')
1068
1347
 
1069
1348
  // String.fromCharCode(code) → 1-char SSO string
1070
1349
  ctx.core.emit['String'] = (value) => {
1071
1350
  if (value === undefined) return emit(['str', ''])
1072
1351
  if (valTypeOf(value) === VAL.STRING) return emit(value)
1352
+ if (valTypeOf(value) === VAL.BOOL) return emitBoolStr(value)
1073
1353
  if (valTypeOf(value) === VAL.NUMBER) {
1074
1354
  inc('__ftoa')
1075
1355
  return typed(['call', '$__ftoa', asF64(emit(value)), ['i32.const', 0], ['i32.const', 0]], 'f64')
1076
1356
  }
1077
- inc('__to_str')
1078
- return typed(['f64.reinterpret_i64', ['call', '$__to_str', asI64(emit(value))]], 'f64')
1357
+ return typed(['f64.reinterpret_i64', toStrI64(value, emit(value))], 'f64')
1079
1358
  }
1080
1359
 
1081
1360
  ctx.core.emit['String.fromCharCode'] = (code) => {
1082
1361
  if (code === undefined) return emit(['str', ''])
1083
- 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))))
1084
1365
  }
1085
1366
 
1086
- // String.fromCodePoint(cp) → UTF-8 encoded string
1087
- ctx.core.stdlib['__fromCodePoint'] = `(func $__fromCodePoint (param $cp i32) (result f64)
1088
- (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)))
1089
1379
  ;; ASCII: 1 byte SSO
1090
1380
  (if (i32.lt_u (local.get $cp) (i32.const 128))
1091
1381
  (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT | 1}) (local.get $cp)))))
@@ -1110,10 +1400,164 @@ export default (ctx) => {
1110
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)))
1111
1401
  (i32.shl (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))) (i32.const 24))))))`
1112
1402
 
1113
- ctx.core.emit['String.fromCodePoint'] = (code) => {
1114
- 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
1115
1408
  inc('__fromCodePoint')
1116
- 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')
1117
1561
  }
1118
1562
 
1119
1563
  // .at(i) → charAt with negative index support
@@ -1184,6 +1628,11 @@ export default (ctx) => {
1184
1628
 
1185
1629
  ctx.core.emit['.encode'] = (obj, str) => {
1186
1630
  inc('__str_encode')
1631
+ // .encode() yields a runtime PTR.TYPED/u8 array (see __mkptr above). Downstream
1632
+ // indexing/spread dispatch through __typed_idx, whose element-unaware fallback
1633
+ // (f64.load, stride 8) is only valid when no typed array can flow in. Enabling
1634
+ // the feature pulls the element-aware variant — same invariant `.length` follows.
1635
+ ctx.features.typedarray = true
1187
1636
  return typed(['call', '$__str_encode', asI64(emit(str))], 'f64')
1188
1637
  }
1189
1638