jz 0.8.0 → 0.9.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/README.md +56 -9
- package/bench/README.md +121 -50
- package/bench/bench.svg +27 -27
- package/cli.js +3 -1
- package/dist/interop.js +1 -1
- package/dist/jz.js +7541 -6178
- package/index.js +165 -74
- package/interop.js +189 -17
- package/jzify/arguments.js +32 -1
- package/jzify/async.js +409 -0
- package/jzify/classes.js +136 -18
- package/jzify/generators.js +639 -0
- package/jzify/hoist-vars.js +73 -1
- package/jzify/index.js +123 -4
- package/jzify/names.js +1 -0
- package/jzify/transform.js +239 -1
- package/layout.js +48 -3
- package/module/array.js +318 -43
- package/module/atomics.js +144 -0
- package/module/collection.js +1429 -155
- package/module/core.js +431 -40
- package/module/date.js +142 -120
- package/module/fs.js +144 -0
- package/module/function.js +6 -3
- package/module/index.js +4 -1
- package/module/json.js +270 -49
- package/module/math.js +1032 -145
- package/module/number.js +532 -163
- package/module/object.js +353 -95
- package/module/regex.js +157 -7
- package/module/schema.js +100 -2
- package/module/string.js +428 -93
- package/module/typedarray.js +264 -72
- package/module/web.js +36 -0
- package/package.json +5 -5
- package/src/abi/string.js +82 -11
- package/src/ast.js +11 -5
- package/src/autoload.js +28 -1
- package/src/bridge.js +7 -2
- package/src/compile/analyze-scans.js +22 -7
- package/src/compile/analyze.js +136 -30
- package/src/compile/dyn-closure-tables.js +277 -0
- package/src/compile/emit-assign.js +281 -15
- package/src/compile/emit.js +1055 -70
- package/src/compile/index.js +277 -29
- package/src/compile/infer.js +42 -4
- package/src/compile/inplace-store.js +329 -0
- package/src/compile/loop-recurrence.js +167 -0
- package/src/compile/narrow.js +555 -18
- package/src/compile/peel-stencil.js +5 -0
- package/src/compile/plan/index.js +45 -3
- package/src/compile/plan/inline.js +8 -1
- package/src/compile/plan/literals.js +39 -0
- package/src/compile/plan/scope.js +430 -23
- package/src/compile/program-facts.js +732 -38
- package/src/ctx.js +64 -9
- package/src/helper-counters.js +7 -1
- package/src/ir.js +113 -5
- package/src/kind-traits.js +66 -3
- package/src/kind.js +84 -12
- package/src/op-policy.js +7 -4
- package/src/optimize/index.js +1179 -704
- package/src/optimize/recurse.js +2 -2
- package/src/optimize/vectorize.js +982 -67
- package/src/prepare/index.js +809 -67
- package/src/prepare/math-kernel.js +331 -0
- package/src/prepare/pre-eval.js +714 -0
- package/src/snapshot.js +194 -0
- package/src/type.js +1170 -56
- package/src/wat/assemble.js +403 -65
- package/src/wat/codegen.js +74 -14
- package/transform.js +113 -4
- package/wasi.js +3 -0
package/module/string.js
CHANGED
|
@@ -3,8 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Type=4 (STRING) covers both encodings; aux bit LAYOUT.SSO_BIT discriminates:
|
|
5
5
|
* bit clear: heap-allocated, length in header [-4:len], offset → bytes.
|
|
6
|
-
* bit set: inline ≤
|
|
7
|
-
* length
|
|
6
|
+
* bit set: inline ≤6 ASCII chars packed in the payload (no memory),
|
|
7
|
+
* length at payload bits 42-44.
|
|
8
|
+
*
|
|
9
|
+
* INVARIANT (load-bearing): every producer normalizes a ≤6-byte all-ASCII result
|
|
10
|
+
* to SSO — literals, slices, concat, append, case, pad, repeat, number/JSON/URI
|
|
11
|
+
* formatting, byte decode, host marshaling (interop.js). Consequence: two strings
|
|
12
|
+
* where exactly one is SSO can never be content-equal, so `__str_eq` decides any
|
|
13
|
+
* mixed compare by bits alone and `x === 'shortLit'` lowers to a bare i64.eq
|
|
14
|
+
* (emit.js emitLooseEq). Breaking the invariant in ONE producer silently breaks
|
|
15
|
+
* string equality — new string constructors MUST route short ASCII results
|
|
16
|
+
* through SSO (see __sso_norm / the concat short-pack).
|
|
8
17
|
*
|
|
9
18
|
* Methods use type-qualified keys (.string:slice) for array-colliding names,
|
|
10
19
|
* generic keys (.toUpperCase) for non-colliding ones.
|
|
@@ -17,10 +26,13 @@ import { emit, bool, method, deps, wat, bind } from '../src/bridge.js'
|
|
|
17
26
|
import { valTypeOf } from '../src/kind.js'
|
|
18
27
|
import { VAL } from '../src/reps.js'
|
|
19
28
|
import { ctx, inc, PTR, LAYOUT, err, declGlobal } from '../src/ctx.js'
|
|
20
|
-
import { ssoBitI64Hex, sliceBitI64Hex, ptrNanHex, STR_INTERN_BIT } from '../layout.js'
|
|
29
|
+
import { ssoBitI64Hex, sliceBitI64Hex, hcacheBitI64Hex, ptrNanHex, STR_INTERN_BIT, STR_HCACHE_BIT } from '../layout.js'
|
|
21
30
|
|
|
22
31
|
const SSO_BIT_I64 = ssoBitI64Hex()
|
|
23
32
|
const SLICE_BIT_I64 = sliceBitI64Hex()
|
|
33
|
+
const HCACHE_BIT_I64 = hcacheBitI64Hex()
|
|
34
|
+
// (SSO_BIT | SLICE_BIT) << AUX_SHIFT as i64 hex — BigInt-free (self-host runs this)
|
|
35
|
+
const SSO_SLICE_I64 = '0x' + (LAYOUT.SSO_BIT | LAYOUT.SLICE_BIT).toString(16) + '00000000'
|
|
24
36
|
|
|
25
37
|
// === SSO codec — single source of truth for the inline-string bit layout ===
|
|
26
38
|
// ASCII chars packed at 7 bits each into the NaN-box payload: char i at payload bit
|
|
@@ -28,13 +40,16 @@ const SLICE_BIT_I64 = sliceBitI64Hex()
|
|
|
28
40
|
// (SLICE_BIT at 45 stays 0). 6 chars fit (6*7=42, + 3-bit len). ASCII-only — a byte
|
|
29
41
|
// ≥0x80 falls back to a heap string. The 7-bit-uniform layout makes equal short strings
|
|
30
42
|
// content-bit-equal (so `op === 'tag'` is a bare i64.eq) and never touches memory.
|
|
31
|
-
const MAX_SSO = 6
|
|
43
|
+
export const MAX_SSO = 6
|
|
32
44
|
const SSO_LEN_SHIFT = 10 // length occupies aux bits 10-12 (= payload bits 42-44)
|
|
45
|
+
const SSO_CHAR_MASK = '0x3ffffffffff' // payload bits 0-41: the 6 × 7-bit char lanes
|
|
33
46
|
// JS: ASCII string → { aux, offset }, or null when ineligible (too long / non-ASCII).
|
|
34
47
|
// BigInt-free (i32 ops only) so the self-hosted compiler — which runs this to encode
|
|
35
48
|
// every string literal — stays on jz's numeric core. offset = payload bits 0-31, aux =
|
|
36
49
|
// bits 32-46; char i at bit i*7 (chars 0-3 fit the offset, char 4 straddles, char 5 → aux).
|
|
37
|
-
|
|
50
|
+
// Exported: collection.js's strHashLiteral reuses this to build the exact lo/hi pair
|
|
51
|
+
// __str_hash's SSO branch mixes — single source of truth for the packing (see its use).
|
|
52
|
+
export const ssoEncode = (str) => {
|
|
38
53
|
if (str.length > MAX_SSO || !/^[\x00-\x7f]*$/.test(str)) return null
|
|
39
54
|
let offset = 0, auxChars = 0
|
|
40
55
|
for (let i = 0; i < str.length; i++) {
|
|
@@ -144,6 +159,8 @@ export default (ctx) => {
|
|
|
144
159
|
deps({
|
|
145
160
|
__str_concat: ['__to_str', '__str_byteLen', '__alloc', '__memgrow', '__mkptr', '__str_copy'],
|
|
146
161
|
__str_concat_raw: ['__str_byteLen', '__alloc', '__memgrow', '__mkptr', '__str_copy'],
|
|
162
|
+
__str_concat_fresh: ['__to_str', '__str_byteLen', '__alloc', '__mkptr', '__str_copy'],
|
|
163
|
+
__str_concat_raw_fresh: ['__str_byteLen', '__alloc', '__mkptr', '__str_copy'],
|
|
147
164
|
__str_append_byte: ['__str_byteLen', '__alloc', '__memgrow', '__mkptr', '__str_copy'],
|
|
148
165
|
__str_copy: [],
|
|
149
166
|
// __str_slice/_view are FUNCTION templates: resolveIncludes' auto-dep scan realizes the
|
|
@@ -163,11 +180,13 @@ export default (ctx) => {
|
|
|
163
180
|
__str_trim: ['__str_slice', '__str_byteLen', '__char_at'],
|
|
164
181
|
__str_trimStart: ['__str_slice', '__str_byteLen', '__char_at'],
|
|
165
182
|
__str_trimEnd: ['__str_slice', '__str_byteLen', '__char_at'],
|
|
166
|
-
__str_repeat: ['__str_byteLen', '__str_copy', '__alloc'],
|
|
183
|
+
__str_repeat: ['__str_byteLen', '__str_copy', '__alloc', '__sso_norm'],
|
|
167
184
|
__str_replace: ['__str_indexof', '__str_slice', '__str_concat'],
|
|
168
185
|
__str_replaceall: ['__str_indexof', '__str_slice', '__str_concat'],
|
|
169
186
|
__str_split: ['__str_slice', '__str_byteLen', '__char_at', '__alloc'],
|
|
170
187
|
__str_idx: ['__char1byte'],
|
|
188
|
+
__sso_norm: [],
|
|
189
|
+
__bytes_decode: ['__ptr_offset', '__len', '__alloc', '__mkptr', '__sso_norm'],
|
|
171
190
|
__str_eq: ['__str_eq_cold'],
|
|
172
191
|
__str_eq_cold: ['__char_at', '__str_byteLen'],
|
|
173
192
|
__str_cmp: ['__char_at', '__str_byteLen'],
|
|
@@ -177,8 +196,8 @@ export default (ctx) => {
|
|
|
177
196
|
__str_pad: ['__str_byteLen', '__str_copy', '__alloc'],
|
|
178
197
|
__str_join: ['__str_concat', '__to_str', '__str_byteLen', '__len', '__ptr_offset', '__mkptr'], // FN template: __mkptr body-called, must be manual (self-host auto-scan diverges)
|
|
179
198
|
__str_encode: ['__str_byteLen', '__str_copy'],
|
|
180
|
-
__encodeURIComponent: ['__to_str', '__str_byteLen', '__char_at', '__alloc', '__mkptr'],
|
|
181
|
-
__decodeURIComponent: ['__to_str', '__str_byteLen', '__char_at', '__alloc', '__mkptr', '__uri_hex'],
|
|
199
|
+
__encodeURIComponent: ['__to_str', '__str_byteLen', '__char_at', '__alloc', '__mkptr', '__sso_norm'],
|
|
200
|
+
__decodeURIComponent: ['__to_str', '__str_byteLen', '__char_at', '__alloc', '__mkptr', '__uri_hex', '__sso_norm'],
|
|
182
201
|
__uri_hex: [],
|
|
183
202
|
__to_str: ['__ftoa', '__static_str', '__str_join', '__mkptr'],
|
|
184
203
|
__str_byteLen: ['__ptr_type', '__ptr_aux', '__str_len'],
|
|
@@ -198,13 +217,17 @@ export default (ctx) => {
|
|
|
198
217
|
// so a heap-free program stays heap-free — no allocator, no memory, no exports.
|
|
199
218
|
inc('__mkptr', '__alloc')
|
|
200
219
|
|
|
201
|
-
// === String literal: "abc" → SSO if ≤
|
|
220
|
+
// === String literal: "abc" → SSO if ≤6 ASCII, else static data ===
|
|
202
221
|
|
|
203
222
|
bind('str', (str) => {
|
|
204
223
|
if (ctx.features.sso) {
|
|
205
224
|
const sso = ssoEncode(str)
|
|
206
225
|
if (sso) return mkPtrIR(PTR.STRING, sso.aux, sso.offset)
|
|
207
226
|
}
|
|
227
|
+
// Falls through to the static-data path below ONLY when ssoEncode returned null
|
|
228
|
+
// (>6 bytes or non-ASCII) — so an interned static NEVER carries ≤6-ASCII content,
|
|
229
|
+
// and its cached hash (below) stays byte-FNV without needing the SSO mix
|
|
230
|
+
// (__str_hash's SSO branch, module/collection.js, never sees an interned pointer).
|
|
208
231
|
const bytes = new TextEncoder().encode(str)
|
|
209
232
|
const len = bytes.length
|
|
210
233
|
if (!ctx.memory.shared) {
|
|
@@ -224,7 +247,9 @@ export default (ctx) => {
|
|
|
224
247
|
const offset = ctx.runtime.data.length
|
|
225
248
|
if (interned) {
|
|
226
249
|
// byte-FNV + clamp — must equal __str_hash's output exactly (it hashes
|
|
227
|
-
// UTF-8 bytes, then clamps ≤1 → +2 for the empty/tombstone sentinels)
|
|
250
|
+
// UTF-8 bytes, then clamps ≤1 → +2 for the empty/tombstone sentinels).
|
|
251
|
+
// Always the byte-FNV branch, never the SSO mix: this string is here
|
|
252
|
+
// because ssoEncode rejected it (see the guard above).
|
|
228
253
|
let h = 0x811c9dc5 | 0
|
|
229
254
|
for (let i = 0; i < len; i++) h = Math.imul(h ^ bytes[i], 0x01000193) | 0
|
|
230
255
|
if (h <= 1) h = (h + 2) | 0
|
|
@@ -361,17 +386,25 @@ export default (ctx) => {
|
|
|
361
386
|
// the engine's wasm inliner — the call overhead disappears at every site
|
|
362
387
|
// while the byte-walk lives in __str_eq_cold. Mirrors the __ptr_offset_fwd
|
|
363
388
|
// split; a body containing a loop is excluded from V8's inliner.
|
|
364
|
-
wat('__str_eq', `(func $__str_eq (param $a i64) (param $b i64) (result i32)
|
|
389
|
+
wat('__str_eq', () => `(func $__str_eq (param $a i64) (param $b i64) (result i32)
|
|
365
390
|
(local $axA i32) (local $axB i32) (local $offA i32) (local $offB i32)
|
|
366
391
|
(if (i64.eq (local.get $a) (local.get $b))
|
|
367
392
|
(then (return (i32.const 1))))
|
|
368
393
|
(local.set $axA (i32.wrap_i64 (i64.shr_u (local.get $a) (i64.const ${LAYOUT.AUX_SHIFT}))))
|
|
369
394
|
(local.set $axB (i32.wrap_i64 (i64.shr_u (local.get $b) (i64.const ${LAYOUT.AUX_SHIFT}))))
|
|
395
|
+
${ctx.features.sso ? `
|
|
396
|
+
;; ANY SSO operand ⇒ bit-ne decided content-ne: ≤${MAX_SSO}-ASCII content is
|
|
397
|
+
;; always SSO (module invariant), so an SSO can equal neither a bit-different
|
|
398
|
+
;; SSO nor any heap string. Kills the mixed SSO×heap byte-walk class outright.
|
|
399
|
+
(if (i32.or
|
|
400
|
+
(i32.and (local.get $axA) (i32.const ${LAYOUT.SSO_BIT}))
|
|
401
|
+
(i32.and (local.get $axB) (i32.const ${LAYOUT.SSO_BIT})))
|
|
402
|
+
(then (return (i32.const 0))))` : `
|
|
370
403
|
;; both SSO ⇒ bit-ne already decided content-ne
|
|
371
404
|
(if (i32.and
|
|
372
405
|
(i32.and (local.get $axA) (i32.const ${LAYOUT.SSO_BIT}))
|
|
373
406
|
(i32.and (local.get $axB) (i32.const ${LAYOUT.SSO_BIT})))
|
|
374
|
-
(then (return (i32.const 0))))
|
|
407
|
+
(then (return (i32.const 0))))`}
|
|
375
408
|
;; both CANONICAL interned heap strings ⇒ bit-ne ⇒ content-ne
|
|
376
409
|
(if (i32.and
|
|
377
410
|
(i32.eq (i32.and (local.get $axA) (i32.const ${LAYOUT.SSO_BIT | LAYOUT.SLICE_BIT | 0x1})) (i32.const 0x1))
|
|
@@ -529,9 +562,10 @@ export default (ctx) => {
|
|
|
529
562
|
(local.set $nlen (i32.sub (local.get $end) (local.get $start)))
|
|
530
563
|
${sliceSsoPackWat()}
|
|
531
564
|
${internProbeWat('(i32.add (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ' + LAYOUT.OFFSET_MASK + '))) (local.get $start))', '(i32.eqz (local.get $isSso))')}
|
|
532
|
-
(local.set $off (call $__alloc (i32.add (i32.const
|
|
533
|
-
(i32.store (local.get $off) (
|
|
534
|
-
(
|
|
565
|
+
(local.set $off (call $__alloc (i32.add (i32.const 8) (local.get $nlen))))
|
|
566
|
+
(i32.store (local.get $off) (i32.const 0))
|
|
567
|
+
(i32.store offset=4 (local.get $off) (local.get $nlen))
|
|
568
|
+
(local.set $off (i32.add (local.get $off) (i32.const 8)))
|
|
535
569
|
(local.set $srcOff (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
536
570
|
(local.set $isSso (i32.wrap_i64 (i64.shr_u
|
|
537
571
|
(i64.and (local.get $ptr) (i64.const ${SSO_BIT_I64}))
|
|
@@ -548,7 +582,7 @@ export default (ctx) => {
|
|
|
548
582
|
(br $loop))))
|
|
549
583
|
(else
|
|
550
584
|
(memory.copy (local.get $off) (i32.add (local.get $srcOff) (local.get $start)) (local.get $nlen))))
|
|
551
|
-
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const
|
|
585
|
+
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${STR_HCACHE_BIT}) (local.get $off)))`)
|
|
552
586
|
|
|
553
587
|
// No-copy slice: returns a VIEW into the receiver's buffer instead of copying
|
|
554
588
|
// bytes. Only emitted when escape analysis proves the result never outlives the
|
|
@@ -568,13 +602,14 @@ export default (ctx) => {
|
|
|
568
602
|
(then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
|
|
569
603
|
(local.set $nlen (i32.sub (local.get $end) (local.get $start)))
|
|
570
604
|
(local.set $tag (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
|
|
571
|
-
;; View-eligible: STRING parent, not SSO, length fits aux[12:0].
|
|
572
|
-
;; results route to __str_slice's SSO pack instead (bit-equal short tokens
|
|
605
|
+
;; View-eligible: STRING parent, not SSO, length fits aux[12:0]. ≤${MAX_SSO}-byte
|
|
606
|
+
;; results route to __str_slice's SSO pack instead (bit-equal short tokens —
|
|
607
|
+
;; a short VIEW would break the ≤${MAX_SSO}-ASCII⇒SSO invariant).
|
|
573
608
|
(if (i32.and
|
|
574
609
|
(i32.and
|
|
575
610
|
(i32.and
|
|
576
611
|
(i32.eq (local.get $tag) (i32.const ${PTR.STRING}))
|
|
577
|
-
(i32.gt_u (local.get $nlen) (i32.const
|
|
612
|
+
(i32.gt_u (local.get $nlen) (i32.const ${MAX_SSO})))
|
|
578
613
|
(i64.eqz (i64.and (local.get $ptr) (i64.const ${SSO_BIT_I64}))))
|
|
579
614
|
(i32.le_u (local.get $nlen) (i32.const ${LAYOUT.SLICE_LEN_MASK})))
|
|
580
615
|
(then
|
|
@@ -674,15 +709,27 @@ export default (ctx) => {
|
|
|
674
709
|
|
|
675
710
|
// Hoist SSO/heap dispatch for hay and ndl out of the inner byte loop. Inner
|
|
676
711
|
// loop becomes (load8_u OR sso byte-extract) per side — no per-byte calls.
|
|
712
|
+
// Needle byte j, SSO-aware (packed-bits extract vs heap load) — lets the SIMD
|
|
713
|
+
// haystack scan serve SSO needles too (the common short "," / "://" needle),
|
|
714
|
+
// not just heap×heap. `$nsso` is loop-invariant so the branch predicts away.
|
|
715
|
+
const nByte = (j) => `(if (result i32) (local.get $nsso)
|
|
716
|
+
(then ${ssoCharWat('(local.get $ndl)', j)})
|
|
717
|
+
(else (i32.load8_u (i32.add (local.get $noff) ${j}))))`
|
|
677
718
|
wat('__str_indexof', `(func $__str_indexof (param $hay i64) (param $ndl i64) (param $from i32) (result i32)
|
|
678
719
|
(local $hlen i32) (local $nlen i32) (local $i i32) (local $j i32) (local $match i32)
|
|
679
720
|
(local $hoff i32) (local $noff i32)
|
|
680
721
|
(local $hsso i32) (local $nsso i32) (local $hb i32) (local $nb i32) (local $k i32)
|
|
722
|
+
(local $splat v128) (local $mask i32) (local $scanEnd i32)
|
|
681
723
|
;; The search value is ToString'd at the call site (searchArg) per 21.1.3.9 step 4 —
|
|
682
724
|
;; a known-string needle passes raw, so this helper carries no float-pulling __to_str.
|
|
683
725
|
(local.set $hlen (call $__str_byteLen (local.get $hay)))
|
|
684
726
|
(local.set $nlen (call $__str_byteLen (local.get $ndl)))
|
|
685
|
-
|
|
727
|
+
;; Empty needle matches at clamp(from, 0, hlen) — per 21.1.3.9 step 6 (Min(Max(pos,0),len)).
|
|
728
|
+
(if (i32.eqz (local.get $nlen))
|
|
729
|
+
(then (return (select
|
|
730
|
+
(select (local.get $from) (local.get $hlen) (i32.lt_s (local.get $from) (local.get $hlen)))
|
|
731
|
+
(i32.const 0)
|
|
732
|
+
(i32.ge_s (local.get $from) (i32.const 0))))))
|
|
686
733
|
(if (i32.gt_s (local.get $nlen) (local.get $hlen)) (then (return (i32.const -1))))
|
|
687
734
|
(local.set $hoff (i32.wrap_i64 (i64.and (local.get $hay) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
688
735
|
(local.set $noff (i32.wrap_i64 (i64.and (local.get $ndl) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
@@ -693,6 +740,83 @@ export default (ctx) => {
|
|
|
693
740
|
(i32.wrap_i64 (i64.shr_u (local.get $ndl) (i64.const ${LAYOUT.AUX_SHIFT})))
|
|
694
741
|
(i32.const ${LAYOUT.SSO_BIT})))
|
|
695
742
|
(local.set $i (if (result i32) (i32.gt_s (local.get $from) (i32.const 0)) (then (local.get $from)) (else (i32.const 0))))
|
|
743
|
+
;; Single-byte needle (the common s.indexOf('/') / s.includes(' ')): scan for one byte without
|
|
744
|
+
;; the per-position match/j/k bookkeeping. Heap haystack gets a branchless load8_u loop (no
|
|
745
|
+
;; per-byte SSO test); a 16-wide SIMD scan here would only add splat/window setup that short
|
|
746
|
+
;; strings — the bulk of single-char searches — never amortize, so the scalar scan stays.
|
|
747
|
+
(if (i32.eq (local.get $nlen) (i32.const 1))
|
|
748
|
+
(then
|
|
749
|
+
(local.set $nb ${nByte('(i32.const 0)')})
|
|
750
|
+
(if (i32.eqz (local.get $hsso))
|
|
751
|
+
(then
|
|
752
|
+
(block $sd (loop $ss
|
|
753
|
+
(br_if $sd (i32.ge_s (local.get $i) (local.get $hlen)))
|
|
754
|
+
(if (i32.eq (i32.load8_u (i32.add (local.get $hoff) (local.get $i))) (local.get $nb))
|
|
755
|
+
(then (return (local.get $i))))
|
|
756
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
757
|
+
(br $ss)))
|
|
758
|
+
(return (i32.const -1)))
|
|
759
|
+
(else
|
|
760
|
+
(block $sd2 (loop $ss2
|
|
761
|
+
(br_if $sd2 (i32.ge_s (local.get $i) (local.get $hlen)))
|
|
762
|
+
(if (i32.eq ${ssoCharWat('(local.get $hay)', '(local.get $i)')} (local.get $nb))
|
|
763
|
+
(then (return (local.get $i))))
|
|
764
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
765
|
+
(br $ss2)))
|
|
766
|
+
(return (i32.const -1))))))
|
|
767
|
+
;; Multi-byte needle, HEAP haystack (needle SSO or heap): a SIMD first-byte memchr — broadcast
|
|
768
|
+
;; needle[0] across 16 lanes, i8x16.eq a 16-byte haystack window, and read the match-bitmask.
|
|
769
|
+
;; Only positions whose first byte matches reach the branchless verify; the rare match (V8's own
|
|
770
|
+
;; StringIndexOf is SIMD here) is what the scalar path lost on. The SIMD load only touches the
|
|
771
|
+
;; haystack, so an SSO needle (the common short "," / "://" / "TARGET") rides this path too —
|
|
772
|
+
;; its bytes are fetched SSO-aware via nByte(). scanEnd = hlen-nlen is the last viable start; the
|
|
773
|
+
;; window stops at hlen-16 (a full 16-byte load stays inside the string), a scalar tail covers
|
|
774
|
+
;; the rest. Only an SSO haystack (≤ MAX_SSO bytes — too short to scan 16-wide) falls through.
|
|
775
|
+
(if (i32.eqz (local.get $hsso))
|
|
776
|
+
(then
|
|
777
|
+
(local.set $nb ${nByte('(i32.const 0)')})
|
|
778
|
+
(local.set $scanEnd (i32.sub (local.get $hlen) (local.get $nlen)))
|
|
779
|
+
(local.set $splat (i8x16.splat (local.get $nb)))
|
|
780
|
+
(block $vd (loop $vo
|
|
781
|
+
(br_if $vd (i32.gt_s (local.get $i) (i32.sub (local.get $hlen) (i32.const 16))))
|
|
782
|
+
(local.set $mask (i8x16.bitmask
|
|
783
|
+
(i8x16.eq (v128.load (i32.add (local.get $hoff) (local.get $i))) (local.get $splat))))
|
|
784
|
+
(block $bd (loop $bo
|
|
785
|
+
(br_if $bd (i32.eqz (local.get $mask)))
|
|
786
|
+
(local.set $k (i32.add (local.get $i) (i32.ctz (local.get $mask)))) ;; candidate start
|
|
787
|
+
(br_if $bd (i32.gt_s (local.get $k) (local.get $scanEnd))) ;; positions only grow
|
|
788
|
+
(local.set $match (i32.const 1))
|
|
789
|
+
(local.set $j (i32.const 1))
|
|
790
|
+
(block $mn (loop $mi
|
|
791
|
+
(br_if $mn (i32.ge_s (local.get $j) (local.get $nlen)))
|
|
792
|
+
(if (i32.ne (i32.load8_u (i32.add (i32.add (local.get $hoff) (local.get $k)) (local.get $j)))
|
|
793
|
+
${nByte('(local.get $j)')})
|
|
794
|
+
(then (local.set $match (i32.const 0)) (br $mn)))
|
|
795
|
+
(local.set $j (i32.add (local.get $j) (i32.const 1)))
|
|
796
|
+
(br $mi)))
|
|
797
|
+
(if (local.get $match) (then (return (local.get $k))))
|
|
798
|
+
(local.set $mask (i32.and (local.get $mask) (i32.sub (local.get $mask) (i32.const 1)))) ;; clear lowest match
|
|
799
|
+
(br $bo)))
|
|
800
|
+
(local.set $i (i32.add (local.get $i) (i32.const 16)))
|
|
801
|
+
(br $vo)))
|
|
802
|
+
;; scalar tail: positions [i, scanEnd] the SIMD window couldn't cover
|
|
803
|
+
(block $md (loop $mo
|
|
804
|
+
(br_if $md (i32.gt_s (local.get $i) (local.get $scanEnd)))
|
|
805
|
+
(if (i32.eq (i32.load8_u (i32.add (local.get $hoff) (local.get $i))) (local.get $nb))
|
|
806
|
+
(then
|
|
807
|
+
(local.set $match (i32.const 1))
|
|
808
|
+
(local.set $j (i32.const 1))
|
|
809
|
+
(block $mn2 (loop $mi2
|
|
810
|
+
(br_if $mn2 (i32.ge_s (local.get $j) (local.get $nlen)))
|
|
811
|
+
(if (i32.ne (i32.load8_u (i32.add (i32.add (local.get $hoff) (local.get $i)) (local.get $j)))
|
|
812
|
+
${nByte('(local.get $j)')})
|
|
813
|
+
(then (local.set $match (i32.const 0)) (br $mn2)))
|
|
814
|
+
(local.set $j (i32.add (local.get $j) (i32.const 1)))
|
|
815
|
+
(br $mi2)))
|
|
816
|
+
(if (local.get $match) (then (return (local.get $i))))))
|
|
817
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
818
|
+
(br $mo)))
|
|
819
|
+
(return (i32.const -1))))
|
|
696
820
|
(block $done (loop $outer
|
|
697
821
|
(br_if $done (i32.gt_s (local.get $i) (i32.sub (local.get $hlen) (local.get $nlen))))
|
|
698
822
|
(local.set $match (i32.const 1))
|
|
@@ -830,33 +954,41 @@ export default (ctx) => {
|
|
|
830
954
|
// Source SSO/heap dispatch hoisted out of the byte loop (was a per-byte __char_at).
|
|
831
955
|
wat('__str_case', `(func $__str_case (param $ptr i64) (param $lo i32) (param $hi i32) (param $delta i32) (result f64)
|
|
832
956
|
(local $len i32) (local $off i32) (local $i i32) (local $c i32)
|
|
833
|
-
(local $srcOff i32)
|
|
957
|
+
(local $srcOff i32) (local $sp64 i64)
|
|
834
958
|
(local.set $len (call $__str_byteLen (local.get $ptr)))
|
|
835
959
|
(if (i32.eqz (local.get $len))
|
|
836
960
|
(then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
(local.set $off (i32.add (local.get $off) (i32.const 4)))
|
|
840
|
-
(local.set $srcOff (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
961
|
+
;; SSO in ⇒ SSO out: case-map the 7-bit lanes in registers (no allocation);
|
|
962
|
+
;; ASCII stays ASCII, so the result is always SSO-eligible (invariant).
|
|
841
963
|
(if (i64.ne (i64.and (local.get $ptr) (i64.const ${SSO_BIT_I64})) (i64.const 0))
|
|
842
964
|
(then
|
|
965
|
+
(local.set $sp64 (i64.const 0))
|
|
843
966
|
(block $dsso (loop $lsso
|
|
844
967
|
(br_if $dsso (i32.ge_s (local.get $i) (local.get $len)))
|
|
845
968
|
(local.set $c ${ssoCharWat('(local.get $ptr)', '(local.get $i)')})
|
|
846
969
|
(if (i32.and (i32.ge_u (local.get $c) (local.get $lo)) (i32.le_u (local.get $c) (local.get $hi)))
|
|
847
970
|
(then (local.set $c (i32.add (local.get $c) (local.get $delta)))))
|
|
848
|
-
(
|
|
849
|
-
|
|
850
|
-
(br $lsso))))
|
|
851
|
-
(else
|
|
852
|
-
(block $dh (loop $lh
|
|
853
|
-
(br_if $dh (i32.ge_s (local.get $i) (local.get $len)))
|
|
854
|
-
(local.set $c (i32.load8_u (i32.add (local.get $srcOff) (local.get $i))))
|
|
855
|
-
(if (i32.and (i32.ge_u (local.get $c) (local.get $lo)) (i32.le_u (local.get $c) (local.get $hi)))
|
|
856
|
-
(then (local.set $c (i32.add (local.get $c) (local.get $delta)))))
|
|
857
|
-
(i32.store8 (i32.add (local.get $off) (local.get $i)) (local.get $c))
|
|
971
|
+
(local.set $sp64 (i64.or (local.get $sp64)
|
|
972
|
+
(i64.shl (i64.extend_i32_u (local.get $c)) (i64.mul (i64.extend_i32_u (local.get $i)) (i64.const 7)))))
|
|
858
973
|
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
859
|
-
(br $
|
|
974
|
+
(br $lsso)))
|
|
975
|
+
(return (f64.reinterpret_i64 (i64.or
|
|
976
|
+
(i64.or
|
|
977
|
+
(i64.const ${ptrNanHex(PTR.STRING, LAYOUT.SSO_BIT)})
|
|
978
|
+
(i64.shl (i64.extend_i32_u (local.get $len)) (i64.const 42)))
|
|
979
|
+
(local.get $sp64))))))
|
|
980
|
+
(local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $len))))
|
|
981
|
+
(i32.store (local.get $off) (local.get $len))
|
|
982
|
+
(local.set $off (i32.add (local.get $off) (i32.const 4)))
|
|
983
|
+
(local.set $srcOff (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
984
|
+
(block $dh (loop $lh
|
|
985
|
+
(br_if $dh (i32.ge_s (local.get $i) (local.get $len)))
|
|
986
|
+
(local.set $c (i32.load8_u (i32.add (local.get $srcOff) (local.get $i))))
|
|
987
|
+
(if (i32.and (i32.ge_u (local.get $c) (local.get $lo)) (i32.le_u (local.get $c) (local.get $hi)))
|
|
988
|
+
(then (local.set $c (i32.add (local.get $c) (local.get $delta)))))
|
|
989
|
+
(i32.store8 (i32.add (local.get $off) (local.get $i)) (local.get $c))
|
|
990
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
991
|
+
(br $lh)))
|
|
860
992
|
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
|
|
861
993
|
|
|
862
994
|
wat('__str_trim', `(func $__str_trim (param $ptr i64) (result f64)
|
|
@@ -919,6 +1051,9 @@ export default (ctx) => {
|
|
|
919
1051
|
(local.get $len))
|
|
920
1052
|
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
921
1053
|
(br $loop)))
|
|
1054
|
+
;; short ASCII result → SSO (invariant); the heap copy stays as arena garbage
|
|
1055
|
+
(if (i32.le_u (local.get $total) (i32.const ${MAX_SSO}))
|
|
1056
|
+
(then (return (call $__sso_norm (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off))))))
|
|
922
1057
|
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
|
|
923
1058
|
|
|
924
1059
|
// Coerce value to string: numbers → __ftoa, nullish → static strings,
|
|
@@ -984,21 +1119,54 @@ export default (ctx) => {
|
|
|
984
1119
|
// only, no heap); fires whenever the upstream `'+'` couldn't fold at
|
|
985
1120
|
// compile time (one or both operands runtime values). Critical for the
|
|
986
1121
|
// identifier-style `'$' + x` / `prefix + s` patterns hot in parsers.
|
|
1122
|
+
// Both-SSO short result: splice the two 42-bit char payloads — no walk, no alloc.
|
|
987
1123
|
const ssoResultFast = `
|
|
988
1124
|
(if (i32.and
|
|
989
1125
|
(i32.and
|
|
990
1126
|
(i64.ne (i64.and (local.get $a) (i64.const ${SSO_BIT_I64})) (i64.const 0))
|
|
991
1127
|
(i64.ne (i64.and (local.get $b) (i64.const ${SSO_BIT_I64})) (i64.const 0)))
|
|
992
|
-
(i32.le_u (local.get $total) (i32.const
|
|
1128
|
+
(i32.le_u (local.get $total) (i32.const ${MAX_SSO})))
|
|
993
1129
|
(then
|
|
994
|
-
(return (
|
|
995
|
-
(
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
(
|
|
1000
|
-
|
|
1001
|
-
(
|
|
1130
|
+
(return (f64.reinterpret_i64 (i64.or
|
|
1131
|
+
(i64.or
|
|
1132
|
+
(i64.const ${ptrNanHex(PTR.STRING, LAYOUT.SSO_BIT)})
|
|
1133
|
+
(i64.shl (i64.extend_i32_u (local.get $total)) (i64.const 42)))
|
|
1134
|
+
(i64.or
|
|
1135
|
+
(i64.and (local.get $a) (i64.const ${SSO_CHAR_MASK}))
|
|
1136
|
+
(i64.shl
|
|
1137
|
+
(i64.and (local.get $b) (i64.const ${SSO_CHAR_MASK}))
|
|
1138
|
+
(i64.extend_i32_u (i32.mul (local.get $alen) (i32.const 7))))))))))`
|
|
1139
|
+
|
|
1140
|
+
// Mixed/heap-source short result: walk the ≤6 source bytes (SSO lane or heap
|
|
1141
|
+
// load per side), bail to the caller's heap path on a non-ASCII byte. Upholds
|
|
1142
|
+
// the module invariant for concat regardless of operand representation.
|
|
1143
|
+
const concatSsoPack = `
|
|
1144
|
+
(if (i32.le_u (local.get $total) (i32.const ${MAX_SSO}))
|
|
1145
|
+
(then (block $kspill
|
|
1146
|
+
(local.set $ksp (i64.const 0))
|
|
1147
|
+
(local.set $ki (i32.const 0))
|
|
1148
|
+
(local.set $kao (i32.wrap_i64 (i64.and (local.get $a) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
1149
|
+
(local.set $kbo (i32.wrap_i64 (i64.and (local.get $b) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
1150
|
+
(loop $kl
|
|
1151
|
+
(if (i32.lt_u (local.get $ki) (local.get $total))
|
|
1152
|
+
(then
|
|
1153
|
+
(local.set $kc (if (result i32) (i32.lt_u (local.get $ki) (local.get $alen))
|
|
1154
|
+
(then (if (result i32) (i64.ne (i64.and (local.get $a) (i64.const ${SSO_BIT_I64})) (i64.const 0))
|
|
1155
|
+
(then ${ssoCharWat('(local.get $a)', '(local.get $ki)')})
|
|
1156
|
+
(else (i32.load8_u (i32.add (local.get $kao) (local.get $ki))))))
|
|
1157
|
+
(else (if (result i32) (i64.ne (i64.and (local.get $b) (i64.const ${SSO_BIT_I64})) (i64.const 0))
|
|
1158
|
+
(then ${ssoCharWat('(local.get $b)', '(i32.sub (local.get $ki) (local.get $alen))')})
|
|
1159
|
+
(else (i32.load8_u (i32.add (local.get $kbo) (i32.sub (local.get $ki) (local.get $alen)))))))))
|
|
1160
|
+
(br_if $kspill (i32.ge_u (local.get $kc) (i32.const 0x80)))
|
|
1161
|
+
(local.set $ksp (i64.or (local.get $ksp)
|
|
1162
|
+
(i64.shl (i64.extend_i32_u (local.get $kc)) (i64.mul (i64.extend_i32_u (local.get $ki)) (i64.const 7)))))
|
|
1163
|
+
(local.set $ki (i32.add (local.get $ki) (i32.const 1)))
|
|
1164
|
+
(br $kl))))
|
|
1165
|
+
(return (f64.reinterpret_i64 (i64.or
|
|
1166
|
+
(i64.or
|
|
1167
|
+
(i64.const ${ptrNanHex(PTR.STRING, LAYOUT.SSO_BIT)})
|
|
1168
|
+
(i64.shl (i64.extend_i32_u (local.get $total)) (i64.const 42)))
|
|
1169
|
+
(local.get $ksp)))))))`
|
|
1002
1170
|
|
|
1003
1171
|
const concatFast = !ctx.memory.shared && ctx.transform.alloc !== false ? `
|
|
1004
1172
|
(local.set $ta (i32.wrap_i64 (i64.and (i64.shr_u (local.get $a) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
|
|
@@ -1022,9 +1190,27 @@ export default (ctx) => {
|
|
|
1022
1190
|
(i32.add (local.get $aoff) (local.get $alen))
|
|
1023
1191
|
(local.get $blen))
|
|
1024
1192
|
(i32.store (i32.sub (local.get $aoff) (i32.const 4)) (local.get $total))
|
|
1193
|
+
;; bytes changed in place — drop the cached hash (cell exists iff HCACHE bit)
|
|
1194
|
+
(if (i64.ne (i64.and (local.get $a) (i64.const ${HCACHE_BIT_I64})) (i64.const 0))
|
|
1195
|
+
(then (i32.store (i32.sub (local.get $aoff) (i32.const 8)) (i32.const 0))))
|
|
1025
1196
|
(global.set $__heap (local.get $newHeap))
|
|
1026
1197
|
(return (f64.reinterpret_i64 (local.get $a)))))` : ''
|
|
1027
1198
|
|
|
1199
|
+
// Always-fresh tail: allocate a new buffer and copy both operands. Shared by the
|
|
1200
|
+
// bump-extend concats (reached when `a` is NOT heap-top) and the `_fresh` variants
|
|
1201
|
+
// (which never bump-extend at all — emit routes a NON-self-accumulating `t = s + x`
|
|
1202
|
+
// here so it can't mutate the live `s` the way the heap-top extend would).
|
|
1203
|
+
// [hash=0 u32][len u32][bytes] + STR_HCACHE_BIT: __str_hash fills the hash cell on
|
|
1204
|
+
// first hash so repeated keying of a built string skips the byte-FNV walk.
|
|
1205
|
+
const allocCopyTail = `
|
|
1206
|
+
(local.set $off (call $__alloc (i32.add (i32.const 8) (local.get $total))))
|
|
1207
|
+
(i32.store (local.get $off) (i32.const 0))
|
|
1208
|
+
(i32.store offset=4 (local.get $off) (local.get $total))
|
|
1209
|
+
(local.set $off (i32.add (local.get $off) (i32.const 8)))
|
|
1210
|
+
(call $__str_copy (local.get $a) (local.get $off) (local.get $alen))
|
|
1211
|
+
(call $__str_copy (local.get $b) (i32.add (local.get $off) (local.get $alen)) (local.get $blen))
|
|
1212
|
+
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${STR_HCACHE_BIT}) (local.get $off)))`
|
|
1213
|
+
|
|
1028
1214
|
// Fused single-byte append: `buf += str[i]` lowers to this when both sides are
|
|
1029
1215
|
// VAL.STRING and the rhs is a string-index. Skips __str_idx's 1-char SSO
|
|
1030
1216
|
// construction and __str_concat's type-dispatch — byte goes directly from
|
|
@@ -1054,39 +1240,53 @@ export default (ctx) => {
|
|
|
1054
1240
|
(call $__memgrow (local.get $newHeap))
|
|
1055
1241
|
(i32.store8 (i32.add (local.get $aoff) (local.get $alen)) (local.get $byte))
|
|
1056
1242
|
(i32.store (i32.sub (local.get $aoff) (i32.const 4)) (i32.add (local.get $alen) (i32.const 1)))
|
|
1243
|
+
;; bytes changed in place — drop the cached hash (cell exists iff HCACHE bit)
|
|
1244
|
+
(if (i64.ne (i64.and (local.get $a) (i64.const ${HCACHE_BIT_I64})) (i64.const 0))
|
|
1245
|
+
(then (i32.store (i32.sub (local.get $aoff) (i32.const 8)) (i32.const 0))))
|
|
1057
1246
|
(global.set $__heap (local.get $newHeap))
|
|
1058
1247
|
(return (f64.reinterpret_i64 (local.get $a)))))))` : ''}
|
|
1059
|
-
;; SSO (STRING with SSO bit) with len <
|
|
1248
|
+
;; SSO (STRING with SSO bit) with len < ${MAX_SSO} and ASCII byte: pack into SSO without allocation.
|
|
1249
|
+
;; NB: the aux test must be a BOOLEAN (i32.ne) — bitwise-ANDing the raw 0x4000 mask
|
|
1250
|
+
;; result with the boolean (i32.eq …) yields 1 & 0x4000 = 0, silently killing the path.
|
|
1060
1251
|
(if (i32.and
|
|
1061
1252
|
(i32.eq (local.get $ta) (i32.const ${PTR.STRING}))
|
|
1062
|
-
(i32.
|
|
1063
|
-
(i32.
|
|
1064
|
-
|
|
1253
|
+
(i32.ne
|
|
1254
|
+
(i32.and
|
|
1255
|
+
(i32.wrap_i64 (i64.shr_u (local.get $a) (i64.const ${LAYOUT.AUX_SHIFT})))
|
|
1256
|
+
(i32.const ${LAYOUT.SSO_BIT}))
|
|
1257
|
+
(i32.const 0)))
|
|
1065
1258
|
(then
|
|
1066
1259
|
(local.set $alen ${ssoLenWat('(local.get $a)')})
|
|
1067
1260
|
(if (i32.and
|
|
1068
|
-
(i32.lt_u (local.get $alen) (i32.const
|
|
1261
|
+
(i32.lt_u (local.get $alen) (i32.const ${MAX_SSO}))
|
|
1069
1262
|
(i32.lt_u (local.get $byte) (i32.const 0x80)))
|
|
1070
1263
|
(then
|
|
1071
|
-
(return (
|
|
1072
|
-
(
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
(
|
|
1264
|
+
(return (f64.reinterpret_i64 (i64.or
|
|
1265
|
+
(i64.or
|
|
1266
|
+
(i64.const ${ptrNanHex(PTR.STRING, LAYOUT.SSO_BIT)})
|
|
1267
|
+
(i64.shl (i64.extend_i32_u (i32.add (local.get $alen) (i32.const 1))) (i64.const 42)))
|
|
1268
|
+
(i64.or
|
|
1269
|
+
(i64.and (local.get $a) (i64.const ${SSO_CHAR_MASK}))
|
|
1270
|
+
(i64.shl (i64.extend_i32_u (local.get $byte)) (i64.extend_i32_u (i32.mul (local.get $alen) (i32.const 7))))))))))))
|
|
1077
1271
|
;; Slow path: allocate new heap STRING with original bytes + 1 new byte
|
|
1078
1272
|
(local.set $alen (call $__str_byteLen (local.get $a)))
|
|
1079
1273
|
(local.set $total (i32.add (local.get $alen) (i32.const 1)))
|
|
1080
|
-
(local.set $off (call $__alloc (i32.add (i32.const
|
|
1081
|
-
(i32.store (local.get $off) (
|
|
1082
|
-
(
|
|
1274
|
+
(local.set $off (call $__alloc (i32.add (i32.const 8) (local.get $total))))
|
|
1275
|
+
(i32.store (local.get $off) (i32.const 0))
|
|
1276
|
+
(i32.store offset=4 (local.get $off) (local.get $total))
|
|
1277
|
+
(local.set $off (i32.add (local.get $off) (i32.const 8)))
|
|
1083
1278
|
(call $__str_copy (local.get $a) (local.get $off) (local.get $alen))
|
|
1084
1279
|
(i32.store8 (i32.add (local.get $off) (local.get $alen)) (local.get $byte))
|
|
1085
|
-
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const
|
|
1280
|
+
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${STR_HCACHE_BIT}) (local.get $off)))`)
|
|
1086
1281
|
|
|
1282
|
+
// __str_concat / __str_concat_raw bump-EXTEND `a` in place when it is the heap-top own string
|
|
1283
|
+
// (the O(N) accumulator path). Emit calls these ONLY when the source is a self-accumulation
|
|
1284
|
+
// `x = x + …` (so the mutated `a` is dead-after-reassign) or when `a` is a provably-fresh
|
|
1285
|
+
// module-internal temporary; a plain `t = s + x` over a live `s` routes to the _fresh twins.
|
|
1087
1286
|
wat('__str_concat', `(func $__str_concat (param $a i64) (param $b i64) (result f64)
|
|
1088
1287
|
(local $alen i32) (local $blen i32) (local $total i32) (local $off i32)
|
|
1089
1288
|
(local $ta i32) (local $aoff i32) (local $newHeap i32)
|
|
1289
|
+
(local $ki i32) (local $kc i32) (local $ksp i64) (local $kao i32) (local $kbo i32)
|
|
1090
1290
|
;; Coerce operands to strings if needed
|
|
1091
1291
|
(local.set $a (call $__to_str (local.get $a)))
|
|
1092
1292
|
(local.set $b (call $__to_str (local.get $b)))
|
|
@@ -1096,30 +1296,48 @@ export default (ctx) => {
|
|
|
1096
1296
|
(if (i32.eqz (local.get $total))
|
|
1097
1297
|
(then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
|
|
1098
1298
|
${ssoResultFast}
|
|
1099
|
-
${
|
|
1100
|
-
|
|
1101
|
-
(i32.store (local.get $off) (local.get $total))
|
|
1102
|
-
(local.set $off (i32.add (local.get $off) (i32.const 4)))
|
|
1103
|
-
(call $__str_copy (local.get $a) (local.get $off) (local.get $alen))
|
|
1104
|
-
(call $__str_copy (local.get $b) (i32.add (local.get $off) (local.get $alen)) (local.get $blen))
|
|
1105
|
-
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
|
|
1299
|
+
${concatSsoPack}
|
|
1300
|
+
${concatFast}${allocCopyTail}`)
|
|
1106
1301
|
|
|
1107
1302
|
wat('__str_concat_raw', `(func $__str_concat_raw (param $a i64) (param $b i64) (result f64)
|
|
1108
1303
|
(local $alen i32) (local $blen i32) (local $total i32) (local $off i32)
|
|
1109
1304
|
(local $ta i32) (local $aoff i32) (local $newHeap i32)
|
|
1305
|
+
(local $ki i32) (local $kc i32) (local $ksp i64) (local $kao i32) (local $kbo i32)
|
|
1110
1306
|
(local.set $alen (call $__str_byteLen (local.get $a)))
|
|
1111
1307
|
(local.set $blen (call $__str_byteLen (local.get $b)))
|
|
1112
1308
|
(local.set $total (i32.add (local.get $alen) (local.get $blen)))
|
|
1113
1309
|
(if (i32.eqz (local.get $total))
|
|
1114
1310
|
(then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
|
|
1115
1311
|
${ssoResultFast}
|
|
1116
|
-
${
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1312
|
+
${concatSsoPack}
|
|
1313
|
+
${concatFast}${allocCopyTail}`)
|
|
1314
|
+
|
|
1315
|
+
// Non-mutating twins: same SSO-pair fast path, but NEVER bump-extend — always alloc+copy a fresh
|
|
1316
|
+
// buffer, leaving `a` untouched. The default for emit's user-level `+` (any `t = s + x` where the
|
|
1317
|
+
// result is not assigned straight back to `s`), so string immutability holds for live operands.
|
|
1318
|
+
wat('__str_concat_fresh', `(func $__str_concat_fresh (param $a i64) (param $b i64) (result f64)
|
|
1319
|
+
(local $alen i32) (local $blen i32) (local $total i32) (local $off i32)
|
|
1320
|
+
(local $ki i32) (local $kc i32) (local $ksp i64) (local $kao i32) (local $kbo i32)
|
|
1321
|
+
(local.set $a (call $__to_str (local.get $a)))
|
|
1322
|
+
(local.set $b (call $__to_str (local.get $b)))
|
|
1323
|
+
(local.set $alen (call $__str_byteLen (local.get $a)))
|
|
1324
|
+
(local.set $blen (call $__str_byteLen (local.get $b)))
|
|
1325
|
+
(local.set $total (i32.add (local.get $alen) (local.get $blen)))
|
|
1326
|
+
(if (i32.eqz (local.get $total))
|
|
1327
|
+
(then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
|
|
1328
|
+
${ssoResultFast}
|
|
1329
|
+
${concatSsoPack}${allocCopyTail}`)
|
|
1330
|
+
|
|
1331
|
+
wat('__str_concat_raw_fresh', `(func $__str_concat_raw_fresh (param $a i64) (param $b i64) (result f64)
|
|
1332
|
+
(local $alen i32) (local $blen i32) (local $total i32) (local $off i32)
|
|
1333
|
+
(local $ki i32) (local $kc i32) (local $ksp i64) (local $kao i32) (local $kbo i32)
|
|
1334
|
+
(local.set $alen (call $__str_byteLen (local.get $a)))
|
|
1335
|
+
(local.set $blen (call $__str_byteLen (local.get $b)))
|
|
1336
|
+
(local.set $total (i32.add (local.get $alen) (local.get $blen)))
|
|
1337
|
+
(if (i32.eqz (local.get $total))
|
|
1338
|
+
(then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
|
|
1339
|
+
${ssoResultFast}
|
|
1340
|
+
${concatSsoPack}${allocCopyTail}`)
|
|
1123
1341
|
|
|
1124
1342
|
wat('__str_replace', `(func $__str_replace (param $str i64) (param $search i64) (param $repl i64) (result f64)
|
|
1125
1343
|
(local $idx i32) (local $slen i32)
|
|
@@ -1314,7 +1532,7 @@ export default (ctx) => {
|
|
|
1314
1532
|
wat('__str_pad', `(func $__str_pad (param $str i64) (param $target i32) (param $pad i64) (param $before i32) (result f64)
|
|
1315
1533
|
(local $slen i32) (local $plen i32) (local $fill i32) (local $off i32) (local $i i32)
|
|
1316
1534
|
(local $str_off i32) (local $pad_off i32)
|
|
1317
|
-
(local $pbits i64) (local $poff i32) (local $psso i32) (local $
|
|
1535
|
+
(local $pbits i64) (local $poff i32) (local $psso i32) (local $sp64 i64) (local $sb i32)
|
|
1318
1536
|
(local.set $slen (call $__str_byteLen (local.get $str)))
|
|
1319
1537
|
(if (i32.ge_s (local.get $slen) (local.get $target))
|
|
1320
1538
|
(then (return (f64.reinterpret_i64 (local.get $str)))))
|
|
@@ -1342,23 +1560,28 @@ export default (ctx) => {
|
|
|
1342
1560
|
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
1343
1561
|
(br $l2)))
|
|
1344
1562
|
${!ctx.memory.shared ? `
|
|
1345
|
-
;; SSO + reclaim for
|
|
1563
|
+
;; SSO + reclaim for ≤${MAX_SSO} ASCII results: pack into the pointer and free this pad
|
|
1346
1564
|
;; allocation (at heap top) so padStart in a builder loop is allocation-neutral
|
|
1347
1565
|
;; — the accumulator stays at heap top and keeps bump-extending (O(n)).
|
|
1348
|
-
(if (i32.le_u (local.get $target) (i32.const
|
|
1566
|
+
(if (i32.le_u (local.get $target) (i32.const ${MAX_SSO}))
|
|
1349
1567
|
(then
|
|
1350
1568
|
(block $heap
|
|
1351
|
-
(local.set $i (i32.const 0)) (local.set $
|
|
1569
|
+
(local.set $i (i32.const 0)) (local.set $sp64 (i64.const 0))
|
|
1352
1570
|
(loop $pk
|
|
1353
1571
|
(if (i32.lt_u (local.get $i) (local.get $target))
|
|
1354
1572
|
(then
|
|
1355
1573
|
(local.set $sb (i32.load8_u (i32.add (local.get $off) (local.get $i))))
|
|
1356
1574
|
(br_if $heap (i32.ge_u (local.get $sb) (i32.const 0x80)))
|
|
1357
|
-
(local.set $
|
|
1575
|
+
(local.set $sp64 (i64.or (local.get $sp64)
|
|
1576
|
+
(i64.shl (i64.extend_i32_u (local.get $sb)) (i64.mul (i64.extend_i32_u (local.get $i)) (i64.const 7)))))
|
|
1358
1577
|
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
1359
1578
|
(br $pk))))
|
|
1360
1579
|
(global.set $__heap (i32.sub (local.get $off) (i32.const 4)))
|
|
1361
|
-
(return (
|
|
1580
|
+
(return (f64.reinterpret_i64 (i64.or
|
|
1581
|
+
(i64.or
|
|
1582
|
+
(i64.const ${ptrNanHex(PTR.STRING, LAYOUT.SSO_BIT)})
|
|
1583
|
+
(i64.shl (i64.extend_i32_u (local.get $target)) (i64.const 42)))
|
|
1584
|
+
(local.get $sp64)))))))` : ''}
|
|
1362
1585
|
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
|
|
1363
1586
|
|
|
1364
1587
|
// Base helpers (__sso_char/__str_char/__char_at/__str_byteLen) are referenced
|
|
@@ -1374,6 +1597,17 @@ export default (ctx) => {
|
|
|
1374
1597
|
// the receiver type can't be statically inferred (e.g. a callback param).
|
|
1375
1598
|
bind('.string:toString', (str) => asF64(emit(str)))
|
|
1376
1599
|
bind('.string:valueOf', (str) => asF64(emit(str)))
|
|
1600
|
+
// String.prototype.normalize — identity: every normalization form (NFC/NFD/
|
|
1601
|
+
// NFKC/NFKD) is the identity on ASCII, and jz strings are UTF-8 bytes with
|
|
1602
|
+
// ASCII-only case/space semantics (README divergences). No Unicode tables.
|
|
1603
|
+
// The form argument is ignored (a bogus form's RangeError needs the same
|
|
1604
|
+
// tables to justify checking — out of scope with them). Generic twin
|
|
1605
|
+
// ToString-coerces an untyped receiver (spec step 1), like .toString.
|
|
1606
|
+
bind('.string:normalize', (str) => asF64(emit(str)))
|
|
1607
|
+
bind('.normalize', (val) => {
|
|
1608
|
+
inc('__to_str')
|
|
1609
|
+
return typed(['f64.reinterpret_i64', ['call', '$__to_str', asI64(emit(val))]], 'f64')
|
|
1610
|
+
})
|
|
1377
1611
|
bind('.toString', (val) => {
|
|
1378
1612
|
inc('__to_str')
|
|
1379
1613
|
return typed(['f64.reinterpret_i64', ['call', '$__to_str', asI64(emit(val))]], 'f64')
|
|
@@ -1621,26 +1855,57 @@ export default (ctx) => {
|
|
|
1621
1855
|
// A VAL.BOOL part rides the 0/1 carrier, so __to_str would render "1"/"0".
|
|
1622
1856
|
// bool selects the interned "true"/"false" literal (constant-folded
|
|
1623
1857
|
// when the operand is known); every other part goes through __to_str.
|
|
1624
|
-
|
|
1858
|
+
// `v` is the part's pre-emitted value when the caller already emitted it.
|
|
1859
|
+
const partStrI64 = (p, v) => valTypeOf(p) === VAL.BOOL ? asI64(bool(p)) : toStrI64(p, v ?? emit(p))
|
|
1625
1860
|
|
|
1626
1861
|
bind('strcat', (...parts) => {
|
|
1627
|
-
inc('__to_str', '__str_byteLen', '__alloc', '__mkptr', '__str_copy')
|
|
1628
1862
|
if (!parts.length) return mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT, 0)
|
|
1629
1863
|
if (parts.length === 1) return typed(['f64.reinterpret_i64', partStrI64(parts[0])], 'f64')
|
|
1864
|
+
inc('__alloc', '__mkptr', '__sso_norm')
|
|
1865
|
+
|
|
1866
|
+
// LITERAL ASCII parts (the serializer separators: ',', '\n', 'x=' …) carry their
|
|
1867
|
+
// bytes and length at compile time: no value temp, no __str_byteLen call, no
|
|
1868
|
+
// __str_copy call — the length const-folds into the total and the bytes store
|
|
1869
|
+
// directly at dst (grouped 4/2/1-wide). Profiled on strbuild: the copy+len
|
|
1870
|
+
// calls on 1-6 byte parts were 38.7% of the row — pure call overhead.
|
|
1871
|
+
const litBytes = (p) => {
|
|
1872
|
+
if (!Array.isArray(p) || p[0] !== 'str' || typeof p[1] !== 'string' || p[1].length === 0) return null
|
|
1873
|
+
const s = p[1]
|
|
1874
|
+
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) > 0x7f) return null
|
|
1875
|
+
return s
|
|
1876
|
+
}
|
|
1877
|
+
const lits = parts.map(litBytes)
|
|
1630
1878
|
|
|
1631
|
-
const vals =
|
|
1632
|
-
const lens = parts.map(() => tempI32('sl'))
|
|
1879
|
+
const vals = [], nums = []
|
|
1880
|
+
const lens = parts.map((_, i) => lits[i] != null ? null : tempI32('sl'))
|
|
1633
1881
|
const total = tempI32('st')
|
|
1634
1882
|
const off = tempI32('so')
|
|
1635
1883
|
const dst = tempI32('sd')
|
|
1636
1884
|
const ir = []
|
|
1885
|
+
let litTotal = 0
|
|
1637
1886
|
|
|
1638
1887
|
for (let i = 0; i < parts.length; i++) {
|
|
1639
|
-
|
|
1888
|
+
if (lits[i] != null) { litTotal += lits[i].length; continue }
|
|
1889
|
+
const vt = valTypeOf(parts[i])
|
|
1890
|
+
const v = vt === VAL.BOOL ? null : emit(parts[i])
|
|
1891
|
+
// i32-PROVEN part (exactly toStrI64's __i32_to_str class): keep the raw value,
|
|
1892
|
+
// not a temp string — __ilen joins the total and __itoa_s renders the digits
|
|
1893
|
+
// directly at dst. Drops the per-number __i32_to_str (alloc+itoa+mkstr),
|
|
1894
|
+
// __str_byteLen and __str_copy — the whole temp-string round trip.
|
|
1895
|
+
if ((vt === VAL.NUMBER || vt == null) && v.type === 'i32' && v.ptrKind == null) {
|
|
1896
|
+
inc('__ilen', '__itoa_s')
|
|
1897
|
+
nums[i] = tempI32('sn')
|
|
1898
|
+
ir.push(['local.set', `$${nums[i]}`, v])
|
|
1899
|
+
ir.push(['local.set', `$${lens[i]}`, ['call', '$__ilen', ['local.get', `$${nums[i]}`]]])
|
|
1900
|
+
continue
|
|
1901
|
+
}
|
|
1902
|
+
inc('__str_byteLen', '__str_copy')
|
|
1903
|
+
vals[i] = temp('s')
|
|
1904
|
+
ir.push(['local.set', `$${vals[i]}`, ['f64.reinterpret_i64', partStrI64(parts[i], v)]])
|
|
1640
1905
|
ir.push(['local.set', `$${lens[i]}`, ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${vals[i]}`]]]])
|
|
1641
1906
|
}
|
|
1642
|
-
ir.push(['local.set', `$${total}`, ['i32.const',
|
|
1643
|
-
for (const len of lens)
|
|
1907
|
+
ir.push(['local.set', `$${total}`, ['i32.const', litTotal]])
|
|
1908
|
+
for (const len of lens) if (len != null)
|
|
1644
1909
|
ir.push(['local.set', `$${total}`, ['i32.add', ['local.get', `$${total}`], ['local.get', `$${len}`]]])
|
|
1645
1910
|
const alloc = [
|
|
1646
1911
|
['local.set', `$${off}`, ['call', '$__alloc', ['i32.add', ['i32.const', 4], ['local.get', `$${total}`]]]],
|
|
@@ -1648,14 +1913,43 @@ export default (ctx) => {
|
|
|
1648
1913
|
['local.set', `$${off}`, ['i32.add', ['local.get', `$${off}`], ['i32.const', 4]]],
|
|
1649
1914
|
['local.set', `$${dst}`, ['local.get', `$${off}`]],
|
|
1650
1915
|
]
|
|
1916
|
+
const dstAt = (k) => k ? ['i32.add', ['local.get', `$${dst}`], ['i32.const', k]] : ['local.get', `$${dst}`]
|
|
1651
1917
|
for (let i = 0; i < parts.length; i++) {
|
|
1918
|
+
if (lits[i] != null) {
|
|
1919
|
+
const s = lits[i]
|
|
1920
|
+
let k = 0 // grouped little-endian stores: 4-byte words, 2-byte tail, then 1
|
|
1921
|
+
for (; k + 4 <= s.length; k += 4)
|
|
1922
|
+
alloc.push(['i32.store', dstAt(k), ['i32.const',
|
|
1923
|
+
(s.charCodeAt(k) | (s.charCodeAt(k + 1) << 8) | (s.charCodeAt(k + 2) << 16) | (s.charCodeAt(k + 3) << 24)) | 0]])
|
|
1924
|
+
if (k + 2 <= s.length) {
|
|
1925
|
+
alloc.push(['i32.store16', dstAt(k), ['i32.const', s.charCodeAt(k) | (s.charCodeAt(k + 1) << 8)]])
|
|
1926
|
+
k += 2
|
|
1927
|
+
}
|
|
1928
|
+
if (k < s.length)
|
|
1929
|
+
alloc.push(['i32.store8', dstAt(k), ['i32.const', s.charCodeAt(k)]])
|
|
1930
|
+
alloc.push(['local.set', `$${dst}`, ['i32.add', ['local.get', `$${dst}`], ['i32.const', s.length]]])
|
|
1931
|
+
continue
|
|
1932
|
+
}
|
|
1933
|
+
if (nums[i] != null) {
|
|
1934
|
+
// digits render at dst; the returned byte count (== $lens[i]) advances it
|
|
1935
|
+
alloc.push(['local.set', `$${dst}`, ['i32.add',
|
|
1936
|
+
['call', '$__itoa_s', ['local.get', `$${nums[i]}`], ['local.get', `$${dst}`]], ['local.get', `$${dst}`]]])
|
|
1937
|
+
continue
|
|
1938
|
+
}
|
|
1652
1939
|
alloc.push(['call', '$__str_copy', ['i64.reinterpret_f64', ['local.get', `$${vals[i]}`]], ['local.get', `$${dst}`], ['local.get', `$${lens[i]}`]])
|
|
1653
1940
|
alloc.push(['local.set', `$${dst}`, ['i32.add', ['local.get', `$${dst}`], ['local.get', `$${lens[i]}`]]])
|
|
1654
1941
|
}
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
['
|
|
1942
|
+
// ≤6-ASCII template results (`\`$\${n}\`` name-building) must SSO-normalize —
|
|
1943
|
+
// the module invariant; a leaked short heap string breaks bare-i64.eq ===.
|
|
1944
|
+
alloc.push(['call', '$__sso_norm',
|
|
1945
|
+
['call', '$__mkptr', ['i32.const', PTR.STRING], ['i32.const', 0], ['local.get', `$${off}`]]])
|
|
1946
|
+
// A non-empty literal or a number part (≥1 digit) proves the result non-empty —
|
|
1947
|
+
// skip the empty-total branch entirely.
|
|
1948
|
+
ir.push(litTotal > 0 || nums.some(t => t != null)
|
|
1949
|
+
? ['block', ['result', 'f64'], ...alloc]
|
|
1950
|
+
: ['if', ['result', 'f64'], ['i32.eqz', ['local.get', `$${total}`]],
|
|
1951
|
+
['then', mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT, 0)],
|
|
1952
|
+
['else', ['block', ['result', 'f64'], ...alloc]]])
|
|
1659
1953
|
return typed(['block', ['result', 'f64'], ...ir], 'f64')
|
|
1660
1954
|
})
|
|
1661
1955
|
|
|
@@ -1733,6 +2027,44 @@ export default (ctx) => {
|
|
|
1733
2027
|
(i32.store8 (i32.add (local.get $off) (i32.const 4)) (local.get $b))
|
|
1734
2028
|
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (i32.add (local.get $off) (i32.const 4))))))`)
|
|
1735
2029
|
|
|
2030
|
+
// Normalize a fresh plain-heap STRING to SSO when its content is ≤${MAX_SSO} ASCII
|
|
2031
|
+
// bytes — the epilogue every string PRODUCER that hand-writes heap bytes (number/
|
|
2032
|
+
// JSON/URI formatting, byte decode, repeat) routes through to uphold the module
|
|
2033
|
+
// invariant. Non-STRING values, SSO, slices, long or non-ASCII strings pass through
|
|
2034
|
+
// untouched. Pure loads, no calls — cheap enough for every producer return.
|
|
2035
|
+
wat('__sso_norm', `(func $__sso_norm (param $s f64) (result f64)
|
|
2036
|
+
(local $b i64) (local $len i32) (local $off i32) (local $i i32) (local $c i32) (local $sp i64)
|
|
2037
|
+
(local.set $b (i64.reinterpret_f64 (local.get $s)))
|
|
2038
|
+
;; plain heap STRING only: tag=STRING, SSO/SLICE clear, header present
|
|
2039
|
+
(if (i32.eqz (i32.and
|
|
2040
|
+
(i32.eq
|
|
2041
|
+
(i32.wrap_i64 (i64.and (i64.shr_u (local.get $b) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
|
|
2042
|
+
(i32.const ${PTR.STRING}))
|
|
2043
|
+
(i32.and
|
|
2044
|
+
(f64.ne (local.get $s) (local.get $s))
|
|
2045
|
+
(i64.eqz (i64.and (local.get $b) (i64.const ${SSO_SLICE_I64}))))))
|
|
2046
|
+
(then (return (local.get $s))))
|
|
2047
|
+
(local.set $off (i32.wrap_i64 (i64.and (local.get $b) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
2048
|
+
(if (i32.lt_u (local.get $off) (i32.const 4)) (then (return (local.get $s))))
|
|
2049
|
+
(local.set $len (i32.load (i32.sub (local.get $off) (i32.const 4))))
|
|
2050
|
+
(if (i32.gt_u (local.get $len) (i32.const ${MAX_SSO})) (then (return (local.get $s))))
|
|
2051
|
+
(local.set $sp (i64.const 0))
|
|
2052
|
+
(block $spill
|
|
2053
|
+
(block $d (loop $l
|
|
2054
|
+
(br_if $d (i32.ge_u (local.get $i) (local.get $len)))
|
|
2055
|
+
(local.set $c (i32.load8_u (i32.add (local.get $off) (local.get $i))))
|
|
2056
|
+
(br_if $spill (i32.ge_u (local.get $c) (i32.const 0x80)))
|
|
2057
|
+
(local.set $sp (i64.or (local.get $sp)
|
|
2058
|
+
(i64.shl (i64.extend_i32_u (local.get $c)) (i64.mul (i64.extend_i32_u (local.get $i)) (i64.const 7)))))
|
|
2059
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
2060
|
+
(br $l)))
|
|
2061
|
+
(return (f64.reinterpret_i64 (i64.or
|
|
2062
|
+
(i64.or
|
|
2063
|
+
(i64.const ${ptrNanHex(PTR.STRING, LAYOUT.SSO_BIT)})
|
|
2064
|
+
(i64.shl (i64.extend_i32_u (local.get $len)) (i64.const 42)))
|
|
2065
|
+
(local.get $sp)))))
|
|
2066
|
+
(local.get $s))`)
|
|
2067
|
+
|
|
1736
2068
|
// String.fromCharCode(...codes) — variadic; each arg is ToUint16(ToNumber(code))
|
|
1737
2069
|
// → a 1-byte string, concatenated left to right (mirrors String.fromCodePoint).
|
|
1738
2070
|
bind('String.fromCharCode', (...codes) => {
|
|
@@ -1857,7 +2189,7 @@ export default (ctx) => {
|
|
|
1857
2189
|
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
1858
2190
|
(br $loop)))
|
|
1859
2191
|
(i32.store (local.get $base) (local.get $j))
|
|
1860
|
-
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $out)))`)
|
|
2192
|
+
(call $__sso_norm (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $out))))`)
|
|
1861
2193
|
|
|
1862
2194
|
bind('encodeURIComponent', (value) => {
|
|
1863
2195
|
inc('__encodeURIComponent')
|
|
@@ -1950,7 +2282,7 @@ export default (ctx) => {
|
|
|
1950
2282
|
(local.set $outLen (i32.add (local.get $outLen) (i32.const 1)))))
|
|
1951
2283
|
(br $loop)))
|
|
1952
2284
|
(i32.store (local.get $base) (local.get $outLen))
|
|
1953
|
-
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $dst)))`)
|
|
2285
|
+
(call $__sso_norm (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $dst))))`)
|
|
1954
2286
|
|
|
1955
2287
|
bind('decodeURIComponent', (value) => {
|
|
1956
2288
|
ctx.runtime.throws = true
|
|
@@ -2046,6 +2378,9 @@ export default (ctx) => {
|
|
|
2046
2378
|
(i32.store (local.get $dst) (local.get $len))
|
|
2047
2379
|
(local.set $dst (i32.add (local.get $dst) (i32.const 4)))
|
|
2048
2380
|
(memory.copy (local.get $dst) (local.get $off) (local.get $len))
|
|
2381
|
+
;; short ASCII decode → SSO (invariant)
|
|
2382
|
+
(if (i32.le_u (local.get $len) (i32.const ${MAX_SSO}))
|
|
2383
|
+
(then (return (call $__sso_norm (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $dst))))))
|
|
2049
2384
|
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $dst)))`)
|
|
2050
2385
|
|
|
2051
2386
|
bind('.decode', (obj, arr) => {
|