jz 0.8.1 → 0.9.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/README.md +56 -9
- package/bench/README.md +121 -50
- package/bench/bench.svg +23 -23
- package/cli.js +3 -1
- package/dist/interop.js +1 -1
- package/dist/jz.js +7541 -6271
- 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 +299 -44
- 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 +892 -32
- 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 +301 -84
- package/module/typedarray.js +264 -72
- package/module/web.js +36 -0
- package/package.json +5 -5
- package/src/abi/string.js +71 -5
- 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 +979 -54
- package/src/compile/index.js +271 -29
- package/src/compile/infer.js +34 -3
- package/src/compile/inplace-store.js +329 -0
- package/src/compile/narrow.js +543 -15
- 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 +1060 -750
- package/src/optimize/recurse.js +2 -2
- package/src/optimize/vectorize.js +972 -67
- package/src/prepare/index.js +792 -63
- 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++) {
|
|
@@ -165,11 +180,13 @@ export default (ctx) => {
|
|
|
165
180
|
__str_trim: ['__str_slice', '__str_byteLen', '__char_at'],
|
|
166
181
|
__str_trimStart: ['__str_slice', '__str_byteLen', '__char_at'],
|
|
167
182
|
__str_trimEnd: ['__str_slice', '__str_byteLen', '__char_at'],
|
|
168
|
-
__str_repeat: ['__str_byteLen', '__str_copy', '__alloc'],
|
|
183
|
+
__str_repeat: ['__str_byteLen', '__str_copy', '__alloc', '__sso_norm'],
|
|
169
184
|
__str_replace: ['__str_indexof', '__str_slice', '__str_concat'],
|
|
170
185
|
__str_replaceall: ['__str_indexof', '__str_slice', '__str_concat'],
|
|
171
186
|
__str_split: ['__str_slice', '__str_byteLen', '__char_at', '__alloc'],
|
|
172
187
|
__str_idx: ['__char1byte'],
|
|
188
|
+
__sso_norm: [],
|
|
189
|
+
__bytes_decode: ['__ptr_offset', '__len', '__alloc', '__mkptr', '__sso_norm'],
|
|
173
190
|
__str_eq: ['__str_eq_cold'],
|
|
174
191
|
__str_eq_cold: ['__char_at', '__str_byteLen'],
|
|
175
192
|
__str_cmp: ['__char_at', '__str_byteLen'],
|
|
@@ -179,8 +196,8 @@ export default (ctx) => {
|
|
|
179
196
|
__str_pad: ['__str_byteLen', '__str_copy', '__alloc'],
|
|
180
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)
|
|
181
198
|
__str_encode: ['__str_byteLen', '__str_copy'],
|
|
182
|
-
__encodeURIComponent: ['__to_str', '__str_byteLen', '__char_at', '__alloc', '__mkptr'],
|
|
183
|
-
__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'],
|
|
184
201
|
__uri_hex: [],
|
|
185
202
|
__to_str: ['__ftoa', '__static_str', '__str_join', '__mkptr'],
|
|
186
203
|
__str_byteLen: ['__ptr_type', '__ptr_aux', '__str_len'],
|
|
@@ -200,13 +217,17 @@ export default (ctx) => {
|
|
|
200
217
|
// so a heap-free program stays heap-free — no allocator, no memory, no exports.
|
|
201
218
|
inc('__mkptr', '__alloc')
|
|
202
219
|
|
|
203
|
-
// === String literal: "abc" → SSO if ≤
|
|
220
|
+
// === String literal: "abc" → SSO if ≤6 ASCII, else static data ===
|
|
204
221
|
|
|
205
222
|
bind('str', (str) => {
|
|
206
223
|
if (ctx.features.sso) {
|
|
207
224
|
const sso = ssoEncode(str)
|
|
208
225
|
if (sso) return mkPtrIR(PTR.STRING, sso.aux, sso.offset)
|
|
209
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).
|
|
210
231
|
const bytes = new TextEncoder().encode(str)
|
|
211
232
|
const len = bytes.length
|
|
212
233
|
if (!ctx.memory.shared) {
|
|
@@ -226,7 +247,9 @@ export default (ctx) => {
|
|
|
226
247
|
const offset = ctx.runtime.data.length
|
|
227
248
|
if (interned) {
|
|
228
249
|
// byte-FNV + clamp — must equal __str_hash's output exactly (it hashes
|
|
229
|
-
// 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).
|
|
230
253
|
let h = 0x811c9dc5 | 0
|
|
231
254
|
for (let i = 0; i < len; i++) h = Math.imul(h ^ bytes[i], 0x01000193) | 0
|
|
232
255
|
if (h <= 1) h = (h + 2) | 0
|
|
@@ -363,17 +386,25 @@ export default (ctx) => {
|
|
|
363
386
|
// the engine's wasm inliner — the call overhead disappears at every site
|
|
364
387
|
// while the byte-walk lives in __str_eq_cold. Mirrors the __ptr_offset_fwd
|
|
365
388
|
// split; a body containing a loop is excluded from V8's inliner.
|
|
366
|
-
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)
|
|
367
390
|
(local $axA i32) (local $axB i32) (local $offA i32) (local $offB i32)
|
|
368
391
|
(if (i64.eq (local.get $a) (local.get $b))
|
|
369
392
|
(then (return (i32.const 1))))
|
|
370
393
|
(local.set $axA (i32.wrap_i64 (i64.shr_u (local.get $a) (i64.const ${LAYOUT.AUX_SHIFT}))))
|
|
371
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))))` : `
|
|
372
403
|
;; both SSO ⇒ bit-ne already decided content-ne
|
|
373
404
|
(if (i32.and
|
|
374
405
|
(i32.and (local.get $axA) (i32.const ${LAYOUT.SSO_BIT}))
|
|
375
406
|
(i32.and (local.get $axB) (i32.const ${LAYOUT.SSO_BIT})))
|
|
376
|
-
(then (return (i32.const 0))))
|
|
407
|
+
(then (return (i32.const 0))))`}
|
|
377
408
|
;; both CANONICAL interned heap strings ⇒ bit-ne ⇒ content-ne
|
|
378
409
|
(if (i32.and
|
|
379
410
|
(i32.eq (i32.and (local.get $axA) (i32.const ${LAYOUT.SSO_BIT | LAYOUT.SLICE_BIT | 0x1})) (i32.const 0x1))
|
|
@@ -531,9 +562,10 @@ export default (ctx) => {
|
|
|
531
562
|
(local.set $nlen (i32.sub (local.get $end) (local.get $start)))
|
|
532
563
|
${sliceSsoPackWat()}
|
|
533
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))')}
|
|
534
|
-
(local.set $off (call $__alloc (i32.add (i32.const
|
|
535
|
-
(i32.store (local.get $off) (
|
|
536
|
-
(
|
|
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)))
|
|
537
569
|
(local.set $srcOff (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
538
570
|
(local.set $isSso (i32.wrap_i64 (i64.shr_u
|
|
539
571
|
(i64.and (local.get $ptr) (i64.const ${SSO_BIT_I64}))
|
|
@@ -550,7 +582,7 @@ export default (ctx) => {
|
|
|
550
582
|
(br $loop))))
|
|
551
583
|
(else
|
|
552
584
|
(memory.copy (local.get $off) (i32.add (local.get $srcOff) (local.get $start)) (local.get $nlen))))
|
|
553
|
-
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const
|
|
585
|
+
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${STR_HCACHE_BIT}) (local.get $off)))`)
|
|
554
586
|
|
|
555
587
|
// No-copy slice: returns a VIEW into the receiver's buffer instead of copying
|
|
556
588
|
// bytes. Only emitted when escape analysis proves the result never outlives the
|
|
@@ -570,13 +602,14 @@ export default (ctx) => {
|
|
|
570
602
|
(then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
|
|
571
603
|
(local.set $nlen (i32.sub (local.get $end) (local.get $start)))
|
|
572
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}))))
|
|
573
|
-
;; View-eligible: STRING parent, not SSO, length fits aux[12:0].
|
|
574
|
-
;; 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).
|
|
575
608
|
(if (i32.and
|
|
576
609
|
(i32.and
|
|
577
610
|
(i32.and
|
|
578
611
|
(i32.eq (local.get $tag) (i32.const ${PTR.STRING}))
|
|
579
|
-
(i32.gt_u (local.get $nlen) (i32.const
|
|
612
|
+
(i32.gt_u (local.get $nlen) (i32.const ${MAX_SSO})))
|
|
580
613
|
(i64.eqz (i64.and (local.get $ptr) (i64.const ${SSO_BIT_I64}))))
|
|
581
614
|
(i32.le_u (local.get $nlen) (i32.const ${LAYOUT.SLICE_LEN_MASK})))
|
|
582
615
|
(then
|
|
@@ -921,33 +954,41 @@ export default (ctx) => {
|
|
|
921
954
|
// Source SSO/heap dispatch hoisted out of the byte loop (was a per-byte __char_at).
|
|
922
955
|
wat('__str_case', `(func $__str_case (param $ptr i64) (param $lo i32) (param $hi i32) (param $delta i32) (result f64)
|
|
923
956
|
(local $len i32) (local $off i32) (local $i i32) (local $c i32)
|
|
924
|
-
(local $srcOff i32)
|
|
957
|
+
(local $srcOff i32) (local $sp64 i64)
|
|
925
958
|
(local.set $len (call $__str_byteLen (local.get $ptr)))
|
|
926
959
|
(if (i32.eqz (local.get $len))
|
|
927
960
|
(then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
(local.set $off (i32.add (local.get $off) (i32.const 4)))
|
|
931
|
-
(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).
|
|
932
963
|
(if (i64.ne (i64.and (local.get $ptr) (i64.const ${SSO_BIT_I64})) (i64.const 0))
|
|
933
964
|
(then
|
|
965
|
+
(local.set $sp64 (i64.const 0))
|
|
934
966
|
(block $dsso (loop $lsso
|
|
935
967
|
(br_if $dsso (i32.ge_s (local.get $i) (local.get $len)))
|
|
936
968
|
(local.set $c ${ssoCharWat('(local.get $ptr)', '(local.get $i)')})
|
|
937
969
|
(if (i32.and (i32.ge_u (local.get $c) (local.get $lo)) (i32.le_u (local.get $c) (local.get $hi)))
|
|
938
970
|
(then (local.set $c (i32.add (local.get $c) (local.get $delta)))))
|
|
939
|
-
(
|
|
940
|
-
|
|
941
|
-
(br $lsso))))
|
|
942
|
-
(else
|
|
943
|
-
(block $dh (loop $lh
|
|
944
|
-
(br_if $dh (i32.ge_s (local.get $i) (local.get $len)))
|
|
945
|
-
(local.set $c (i32.load8_u (i32.add (local.get $srcOff) (local.get $i))))
|
|
946
|
-
(if (i32.and (i32.ge_u (local.get $c) (local.get $lo)) (i32.le_u (local.get $c) (local.get $hi)))
|
|
947
|
-
(then (local.set $c (i32.add (local.get $c) (local.get $delta)))))
|
|
948
|
-
(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)))))
|
|
949
973
|
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
950
|
-
(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)))
|
|
951
992
|
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
|
|
952
993
|
|
|
953
994
|
wat('__str_trim', `(func $__str_trim (param $ptr i64) (result f64)
|
|
@@ -1010,6 +1051,9 @@ export default (ctx) => {
|
|
|
1010
1051
|
(local.get $len))
|
|
1011
1052
|
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
1012
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))))))
|
|
1013
1057
|
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
|
|
1014
1058
|
|
|
1015
1059
|
// Coerce value to string: numbers → __ftoa, nullish → static strings,
|
|
@@ -1075,21 +1119,54 @@ export default (ctx) => {
|
|
|
1075
1119
|
// only, no heap); fires whenever the upstream `'+'` couldn't fold at
|
|
1076
1120
|
// compile time (one or both operands runtime values). Critical for the
|
|
1077
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.
|
|
1078
1123
|
const ssoResultFast = `
|
|
1079
1124
|
(if (i32.and
|
|
1080
1125
|
(i32.and
|
|
1081
1126
|
(i64.ne (i64.and (local.get $a) (i64.const ${SSO_BIT_I64})) (i64.const 0))
|
|
1082
1127
|
(i64.ne (i64.and (local.get $b) (i64.const ${SSO_BIT_I64})) (i64.const 0)))
|
|
1083
|
-
(i32.le_u (local.get $total) (i32.const
|
|
1128
|
+
(i32.le_u (local.get $total) (i32.const ${MAX_SSO})))
|
|
1084
1129
|
(then
|
|
1085
|
-
(return (
|
|
1086
|
-
(
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
(
|
|
1091
|
-
|
|
1092
|
-
(
|
|
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)))))))`
|
|
1093
1170
|
|
|
1094
1171
|
const concatFast = !ctx.memory.shared && ctx.transform.alloc !== false ? `
|
|
1095
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}))))
|
|
@@ -1113,6 +1190,9 @@ export default (ctx) => {
|
|
|
1113
1190
|
(i32.add (local.get $aoff) (local.get $alen))
|
|
1114
1191
|
(local.get $blen))
|
|
1115
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))))
|
|
1116
1196
|
(global.set $__heap (local.get $newHeap))
|
|
1117
1197
|
(return (f64.reinterpret_i64 (local.get $a)))))` : ''
|
|
1118
1198
|
|
|
@@ -1120,13 +1200,16 @@ export default (ctx) => {
|
|
|
1120
1200
|
// bump-extend concats (reached when `a` is NOT heap-top) and the `_fresh` variants
|
|
1121
1201
|
// (which never bump-extend at all — emit routes a NON-self-accumulating `t = s + x`
|
|
1122
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.
|
|
1123
1205
|
const allocCopyTail = `
|
|
1124
|
-
(local.set $off (call $__alloc (i32.add (i32.const
|
|
1125
|
-
(i32.store (local.get $off) (
|
|
1126
|
-
(
|
|
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)))
|
|
1127
1210
|
(call $__str_copy (local.get $a) (local.get $off) (local.get $alen))
|
|
1128
1211
|
(call $__str_copy (local.get $b) (i32.add (local.get $off) (local.get $alen)) (local.get $blen))
|
|
1129
|
-
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const
|
|
1212
|
+
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${STR_HCACHE_BIT}) (local.get $off)))`
|
|
1130
1213
|
|
|
1131
1214
|
// Fused single-byte append: `buf += str[i]` lowers to this when both sides are
|
|
1132
1215
|
// VAL.STRING and the rhs is a string-index. Skips __str_idx's 1-char SSO
|
|
@@ -1157,35 +1240,44 @@ export default (ctx) => {
|
|
|
1157
1240
|
(call $__memgrow (local.get $newHeap))
|
|
1158
1241
|
(i32.store8 (i32.add (local.get $aoff) (local.get $alen)) (local.get $byte))
|
|
1159
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))))
|
|
1160
1246
|
(global.set $__heap (local.get $newHeap))
|
|
1161
1247
|
(return (f64.reinterpret_i64 (local.get $a)))))))` : ''}
|
|
1162
|
-
;; 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.
|
|
1163
1251
|
(if (i32.and
|
|
1164
1252
|
(i32.eq (local.get $ta) (i32.const ${PTR.STRING}))
|
|
1165
|
-
(i32.
|
|
1166
|
-
(i32.
|
|
1167
|
-
|
|
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)))
|
|
1168
1258
|
(then
|
|
1169
1259
|
(local.set $alen ${ssoLenWat('(local.get $a)')})
|
|
1170
1260
|
(if (i32.and
|
|
1171
|
-
(i32.lt_u (local.get $alen) (i32.const
|
|
1261
|
+
(i32.lt_u (local.get $alen) (i32.const ${MAX_SSO}))
|
|
1172
1262
|
(i32.lt_u (local.get $byte) (i32.const 0x80)))
|
|
1173
1263
|
(then
|
|
1174
|
-
(return (
|
|
1175
|
-
(
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
(
|
|
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))))))))))))
|
|
1180
1271
|
;; Slow path: allocate new heap STRING with original bytes + 1 new byte
|
|
1181
1272
|
(local.set $alen (call $__str_byteLen (local.get $a)))
|
|
1182
1273
|
(local.set $total (i32.add (local.get $alen) (i32.const 1)))
|
|
1183
|
-
(local.set $off (call $__alloc (i32.add (i32.const
|
|
1184
|
-
(i32.store (local.get $off) (
|
|
1185
|
-
(
|
|
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)))
|
|
1186
1278
|
(call $__str_copy (local.get $a) (local.get $off) (local.get $alen))
|
|
1187
1279
|
(i32.store8 (i32.add (local.get $off) (local.get $alen)) (local.get $byte))
|
|
1188
|
-
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const
|
|
1280
|
+
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${STR_HCACHE_BIT}) (local.get $off)))`)
|
|
1189
1281
|
|
|
1190
1282
|
// __str_concat / __str_concat_raw bump-EXTEND `a` in place when it is the heap-top own string
|
|
1191
1283
|
// (the O(N) accumulator path). Emit calls these ONLY when the source is a self-accumulation
|
|
@@ -1194,6 +1286,7 @@ export default (ctx) => {
|
|
|
1194
1286
|
wat('__str_concat', `(func $__str_concat (param $a i64) (param $b i64) (result f64)
|
|
1195
1287
|
(local $alen i32) (local $blen i32) (local $total i32) (local $off i32)
|
|
1196
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)
|
|
1197
1290
|
;; Coerce operands to strings if needed
|
|
1198
1291
|
(local.set $a (call $__to_str (local.get $a)))
|
|
1199
1292
|
(local.set $b (call $__to_str (local.get $b)))
|
|
@@ -1203,17 +1296,20 @@ export default (ctx) => {
|
|
|
1203
1296
|
(if (i32.eqz (local.get $total))
|
|
1204
1297
|
(then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
|
|
1205
1298
|
${ssoResultFast}
|
|
1299
|
+
${concatSsoPack}
|
|
1206
1300
|
${concatFast}${allocCopyTail}`)
|
|
1207
1301
|
|
|
1208
1302
|
wat('__str_concat_raw', `(func $__str_concat_raw (param $a i64) (param $b i64) (result f64)
|
|
1209
1303
|
(local $alen i32) (local $blen i32) (local $total i32) (local $off i32)
|
|
1210
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)
|
|
1211
1306
|
(local.set $alen (call $__str_byteLen (local.get $a)))
|
|
1212
1307
|
(local.set $blen (call $__str_byteLen (local.get $b)))
|
|
1213
1308
|
(local.set $total (i32.add (local.get $alen) (local.get $blen)))
|
|
1214
1309
|
(if (i32.eqz (local.get $total))
|
|
1215
1310
|
(then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
|
|
1216
1311
|
${ssoResultFast}
|
|
1312
|
+
${concatSsoPack}
|
|
1217
1313
|
${concatFast}${allocCopyTail}`)
|
|
1218
1314
|
|
|
1219
1315
|
// Non-mutating twins: same SSO-pair fast path, but NEVER bump-extend — always alloc+copy a fresh
|
|
@@ -1221,6 +1317,7 @@ export default (ctx) => {
|
|
|
1221
1317
|
// result is not assigned straight back to `s`), so string immutability holds for live operands.
|
|
1222
1318
|
wat('__str_concat_fresh', `(func $__str_concat_fresh (param $a i64) (param $b i64) (result f64)
|
|
1223
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)
|
|
1224
1321
|
(local.set $a (call $__to_str (local.get $a)))
|
|
1225
1322
|
(local.set $b (call $__to_str (local.get $b)))
|
|
1226
1323
|
(local.set $alen (call $__str_byteLen (local.get $a)))
|
|
@@ -1228,16 +1325,19 @@ export default (ctx) => {
|
|
|
1228
1325
|
(local.set $total (i32.add (local.get $alen) (local.get $blen)))
|
|
1229
1326
|
(if (i32.eqz (local.get $total))
|
|
1230
1327
|
(then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
|
|
1231
|
-
${ssoResultFast}
|
|
1328
|
+
${ssoResultFast}
|
|
1329
|
+
${concatSsoPack}${allocCopyTail}`)
|
|
1232
1330
|
|
|
1233
1331
|
wat('__str_concat_raw_fresh', `(func $__str_concat_raw_fresh (param $a i64) (param $b i64) (result f64)
|
|
1234
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)
|
|
1235
1334
|
(local.set $alen (call $__str_byteLen (local.get $a)))
|
|
1236
1335
|
(local.set $blen (call $__str_byteLen (local.get $b)))
|
|
1237
1336
|
(local.set $total (i32.add (local.get $alen) (local.get $blen)))
|
|
1238
1337
|
(if (i32.eqz (local.get $total))
|
|
1239
1338
|
(then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
|
|
1240
|
-
${ssoResultFast}
|
|
1339
|
+
${ssoResultFast}
|
|
1340
|
+
${concatSsoPack}${allocCopyTail}`)
|
|
1241
1341
|
|
|
1242
1342
|
wat('__str_replace', `(func $__str_replace (param $str i64) (param $search i64) (param $repl i64) (result f64)
|
|
1243
1343
|
(local $idx i32) (local $slen i32)
|
|
@@ -1432,7 +1532,7 @@ export default (ctx) => {
|
|
|
1432
1532
|
wat('__str_pad', `(func $__str_pad (param $str i64) (param $target i32) (param $pad i64) (param $before i32) (result f64)
|
|
1433
1533
|
(local $slen i32) (local $plen i32) (local $fill i32) (local $off i32) (local $i i32)
|
|
1434
1534
|
(local $str_off i32) (local $pad_off i32)
|
|
1435
|
-
(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)
|
|
1436
1536
|
(local.set $slen (call $__str_byteLen (local.get $str)))
|
|
1437
1537
|
(if (i32.ge_s (local.get $slen) (local.get $target))
|
|
1438
1538
|
(then (return (f64.reinterpret_i64 (local.get $str)))))
|
|
@@ -1460,23 +1560,28 @@ export default (ctx) => {
|
|
|
1460
1560
|
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
1461
1561
|
(br $l2)))
|
|
1462
1562
|
${!ctx.memory.shared ? `
|
|
1463
|
-
;; SSO + reclaim for
|
|
1563
|
+
;; SSO + reclaim for ≤${MAX_SSO} ASCII results: pack into the pointer and free this pad
|
|
1464
1564
|
;; allocation (at heap top) so padStart in a builder loop is allocation-neutral
|
|
1465
1565
|
;; — the accumulator stays at heap top and keeps bump-extending (O(n)).
|
|
1466
|
-
(if (i32.le_u (local.get $target) (i32.const
|
|
1566
|
+
(if (i32.le_u (local.get $target) (i32.const ${MAX_SSO}))
|
|
1467
1567
|
(then
|
|
1468
1568
|
(block $heap
|
|
1469
|
-
(local.set $i (i32.const 0)) (local.set $
|
|
1569
|
+
(local.set $i (i32.const 0)) (local.set $sp64 (i64.const 0))
|
|
1470
1570
|
(loop $pk
|
|
1471
1571
|
(if (i32.lt_u (local.get $i) (local.get $target))
|
|
1472
1572
|
(then
|
|
1473
1573
|
(local.set $sb (i32.load8_u (i32.add (local.get $off) (local.get $i))))
|
|
1474
1574
|
(br_if $heap (i32.ge_u (local.get $sb) (i32.const 0x80)))
|
|
1475
|
-
(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)))))
|
|
1476
1577
|
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
1477
1578
|
(br $pk))))
|
|
1478
1579
|
(global.set $__heap (i32.sub (local.get $off) (i32.const 4)))
|
|
1479
|
-
(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)))))))` : ''}
|
|
1480
1585
|
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`)
|
|
1481
1586
|
|
|
1482
1587
|
// Base helpers (__sso_char/__str_char/__char_at/__str_byteLen) are referenced
|
|
@@ -1492,6 +1597,17 @@ export default (ctx) => {
|
|
|
1492
1597
|
// the receiver type can't be statically inferred (e.g. a callback param).
|
|
1493
1598
|
bind('.string:toString', (str) => asF64(emit(str)))
|
|
1494
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
|
+
})
|
|
1495
1611
|
bind('.toString', (val) => {
|
|
1496
1612
|
inc('__to_str')
|
|
1497
1613
|
return typed(['f64.reinterpret_i64', ['call', '$__to_str', asI64(emit(val))]], 'f64')
|
|
@@ -1739,26 +1855,57 @@ export default (ctx) => {
|
|
|
1739
1855
|
// A VAL.BOOL part rides the 0/1 carrier, so __to_str would render "1"/"0".
|
|
1740
1856
|
// bool selects the interned "true"/"false" literal (constant-folded
|
|
1741
1857
|
// when the operand is known); every other part goes through __to_str.
|
|
1742
|
-
|
|
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))
|
|
1743
1860
|
|
|
1744
1861
|
bind('strcat', (...parts) => {
|
|
1745
|
-
inc('__to_str', '__str_byteLen', '__alloc', '__mkptr', '__str_copy')
|
|
1746
1862
|
if (!parts.length) return mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT, 0)
|
|
1747
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)
|
|
1748
1878
|
|
|
1749
|
-
const vals =
|
|
1750
|
-
const lens = parts.map(() => tempI32('sl'))
|
|
1879
|
+
const vals = [], nums = []
|
|
1880
|
+
const lens = parts.map((_, i) => lits[i] != null ? null : tempI32('sl'))
|
|
1751
1881
|
const total = tempI32('st')
|
|
1752
1882
|
const off = tempI32('so')
|
|
1753
1883
|
const dst = tempI32('sd')
|
|
1754
1884
|
const ir = []
|
|
1885
|
+
let litTotal = 0
|
|
1755
1886
|
|
|
1756
1887
|
for (let i = 0; i < parts.length; i++) {
|
|
1757
|
-
|
|
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)]])
|
|
1758
1905
|
ir.push(['local.set', `$${lens[i]}`, ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${vals[i]}`]]]])
|
|
1759
1906
|
}
|
|
1760
|
-
ir.push(['local.set', `$${total}`, ['i32.const',
|
|
1761
|
-
for (const len of lens)
|
|
1907
|
+
ir.push(['local.set', `$${total}`, ['i32.const', litTotal]])
|
|
1908
|
+
for (const len of lens) if (len != null)
|
|
1762
1909
|
ir.push(['local.set', `$${total}`, ['i32.add', ['local.get', `$${total}`], ['local.get', `$${len}`]]])
|
|
1763
1910
|
const alloc = [
|
|
1764
1911
|
['local.set', `$${off}`, ['call', '$__alloc', ['i32.add', ['i32.const', 4], ['local.get', `$${total}`]]]],
|
|
@@ -1766,14 +1913,43 @@ export default (ctx) => {
|
|
|
1766
1913
|
['local.set', `$${off}`, ['i32.add', ['local.get', `$${off}`], ['i32.const', 4]]],
|
|
1767
1914
|
['local.set', `$${dst}`, ['local.get', `$${off}`]],
|
|
1768
1915
|
]
|
|
1916
|
+
const dstAt = (k) => k ? ['i32.add', ['local.get', `$${dst}`], ['i32.const', k]] : ['local.get', `$${dst}`]
|
|
1769
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
|
+
}
|
|
1770
1939
|
alloc.push(['call', '$__str_copy', ['i64.reinterpret_f64', ['local.get', `$${vals[i]}`]], ['local.get', `$${dst}`], ['local.get', `$${lens[i]}`]])
|
|
1771
1940
|
alloc.push(['local.set', `$${dst}`, ['i32.add', ['local.get', `$${dst}`], ['local.get', `$${lens[i]}`]]])
|
|
1772
1941
|
}
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
['
|
|
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]]])
|
|
1777
1953
|
return typed(['block', ['result', 'f64'], ...ir], 'f64')
|
|
1778
1954
|
})
|
|
1779
1955
|
|
|
@@ -1851,6 +2027,44 @@ export default (ctx) => {
|
|
|
1851
2027
|
(i32.store8 (i32.add (local.get $off) (i32.const 4)) (local.get $b))
|
|
1852
2028
|
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (i32.add (local.get $off) (i32.const 4))))))`)
|
|
1853
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
|
+
|
|
1854
2068
|
// String.fromCharCode(...codes) — variadic; each arg is ToUint16(ToNumber(code))
|
|
1855
2069
|
// → a 1-byte string, concatenated left to right (mirrors String.fromCodePoint).
|
|
1856
2070
|
bind('String.fromCharCode', (...codes) => {
|
|
@@ -1975,7 +2189,7 @@ export default (ctx) => {
|
|
|
1975
2189
|
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
1976
2190
|
(br $loop)))
|
|
1977
2191
|
(i32.store (local.get $base) (local.get $j))
|
|
1978
|
-
(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))))`)
|
|
1979
2193
|
|
|
1980
2194
|
bind('encodeURIComponent', (value) => {
|
|
1981
2195
|
inc('__encodeURIComponent')
|
|
@@ -2068,7 +2282,7 @@ export default (ctx) => {
|
|
|
2068
2282
|
(local.set $outLen (i32.add (local.get $outLen) (i32.const 1)))))
|
|
2069
2283
|
(br $loop)))
|
|
2070
2284
|
(i32.store (local.get $base) (local.get $outLen))
|
|
2071
|
-
(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))))`)
|
|
2072
2286
|
|
|
2073
2287
|
bind('decodeURIComponent', (value) => {
|
|
2074
2288
|
ctx.runtime.throws = true
|
|
@@ -2164,6 +2378,9 @@ export default (ctx) => {
|
|
|
2164
2378
|
(i32.store (local.get $dst) (local.get $len))
|
|
2165
2379
|
(local.set $dst (i32.add (local.get $dst) (i32.const 4)))
|
|
2166
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))))))
|
|
2167
2384
|
(call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $dst)))`)
|
|
2168
2385
|
|
|
2169
2386
|
bind('.decode', (obj, arr) => {
|