jz 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +275 -255
- package/cli.js +8 -51
- package/index.js +166 -31
- package/{src/host.js → interop.js} +291 -88
- package/module/array.js +181 -71
- package/module/collection.js +222 -8
- package/module/console.js +1 -1
- package/module/core.js +277 -118
- package/module/date.js +9 -5
- package/module/function.js +11 -1
- package/module/json.js +361 -55
- package/module/math.js +601 -130
- package/module/number.js +390 -227
- package/module/object.js +252 -34
- package/module/regex.js +123 -16
- package/module/schema.js +15 -1
- package/module/string.js +545 -96
- package/module/timer.js +1 -1
- package/module/typedarray.js +674 -57
- package/package.json +10 -7
- package/src/abi/array.js +89 -0
- package/src/abi/index.js +35 -0
- package/src/abi/number.js +137 -0
- package/src/abi/object.js +57 -0
- package/src/abi/string.js +529 -0
- package/src/analyze.js +1870 -485
- package/src/assemble.js +27 -12
- package/src/autoload.js +13 -1
- package/src/compile.js +446 -179
- package/src/ctx.js +85 -18
- package/src/emit.js +705 -196
- package/src/infer.js +548 -0
- package/src/ir.js +291 -23
- package/src/jzify.js +851 -132
- package/src/narrow.js +433 -251
- package/src/optimize.js +126 -122
- package/src/plan.js +703 -34
- package/src/prepare.js +833 -153
- package/src/resolve.js +85 -0
- package/src/vectorize.js +99 -18
- package/src/ast.js +0 -160
- package/src/auto-config.js +0 -120
- package/src/fuse.js +0 -159
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/abi/string — string carriers.
|
|
3
|
+
*
|
|
4
|
+
* One file holds every strategy the compiler may pick for a string-typed
|
|
5
|
+
* binding. Carriers are named exports; the narrower tags each site with the
|
|
6
|
+
* chosen carrier, and codegen reads `ctx.abi.string[<carrier>]` (today only
|
|
7
|
+
* the default carrier `sso` is reached — per-site picking arrives with the
|
|
8
|
+
* JS String Builtins specialization workstream).
|
|
9
|
+
*
|
|
10
|
+
* Carriers:
|
|
11
|
+
* - `sso` default. NaN-boxed STRING pointer (PTR.STRING=4) with
|
|
12
|
+
* Small-String-Optimization for ≤4 ASCII chars packed inline
|
|
13
|
+
* in the aux+offset fields.
|
|
14
|
+
* - `jsstring` architectural scaffold. Native JS strings via JS String
|
|
15
|
+
* Builtins (`wasm:js-string` imports); externref slot. Empty
|
|
16
|
+
* ops table — the 9-item compiler-wide checklist below blocks
|
|
17
|
+
* real codegen.
|
|
18
|
+
*
|
|
19
|
+
* No `name`/`type` discriminant field — carriers are referenced by object
|
|
20
|
+
* identity from the default-bundle in `src/abi/index.js`.
|
|
21
|
+
*
|
|
22
|
+
* @module src/abi/string
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
26
|
+
// Op contract (shared by every carrier)
|
|
27
|
+
//
|
|
28
|
+
// ops.<op>(...slotCarriers, ctx) → wasm IR
|
|
29
|
+
//
|
|
30
|
+
// - Every string-valued argument is a **slot-carrier IR**: IR whose runtime
|
|
31
|
+
// WASM value matches `slotTypes[0]`. Under `sso` that's `f64` (the NaN-
|
|
32
|
+
// boxed pointer); under `jsstring` that's `externref`. The caller is
|
|
33
|
+
// responsible for producing slot-carrier IR — typically
|
|
34
|
+
// `asF64(emit(strNode))` today; once non-f64 string slots ship, call
|
|
35
|
+
// sites switch to a carrier-driven `coerceSlot` helper.
|
|
36
|
+
// - The op is self-contained: it inlines whatever WASM
|
|
37
|
+
// reinterprets/wraps it needs to reach its stdlib helper signature.
|
|
38
|
+
// `sso` emits `['i64.reinterpret_f64', sF64]` inline rather than
|
|
39
|
+
// importing `asI64` from `src/ir.js` — this module is loaded transitively
|
|
40
|
+
// from `src/ctx.js`, so importing back into `src/ir.js` would read
|
|
41
|
+
// `LAYOUT.NAN_PREFIX_BITS` before `src/ctx.js`'s `LAYOUT` const is bound.
|
|
42
|
+
// - `ctx` is the ambient compilation context, passed last. Each op
|
|
43
|
+
// registers its stdlib dependency via `ctx.core.includes.add(name)`.
|
|
44
|
+
//
|
|
45
|
+
// Layout the `sso` ops are calibrated against (defined in `src/ctx.js`):
|
|
46
|
+
// - LAYOUT.NAN_PREFIX_BITS, LAYOUT.TAG_SHIFT, LAYOUT.AUX_SHIFT,
|
|
47
|
+
// LAYOUT.OFFSET_MASK, LAYOUT.SSO_BIT
|
|
48
|
+
// - PTR.STRING = 4
|
|
49
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
// ── sso ───────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
// Local inline coercer — IR-only, no src/* import (cycle safety). Caller is
|
|
54
|
+
// expected to pass IR whose WASM-level value is already f64 (e.g. via
|
|
55
|
+
// `asF64(emit(strNode))`); this wraps the unbox to i64 the stdlib helpers
|
|
56
|
+
// take. Kept as a one-liner so each op reads as a single `call`.
|
|
57
|
+
const ssoI64 = (sF64) => ['i64.reinterpret_f64', sF64]
|
|
58
|
+
|
|
59
|
+
// LAYOUT lives in `src/ctx.js`, which loads this module mid-bootstrap (line 10
|
|
60
|
+
// of ctx.js, before LAYOUT itself is bound). ESM live bindings mean the import
|
|
61
|
+
// resolves but the value is `undefined` until ctx.js finishes. All charCodeAt /
|
|
62
|
+
// inline paths read these constants at *call* time (compile-time, after ctx is
|
|
63
|
+
// fully initialized) — never at module top level — so this is safe.
|
|
64
|
+
import { LAYOUT } from '../ctx.js'
|
|
65
|
+
|
|
66
|
+
// Local copy of analyze.js#isReassigned — pulling the real one creates a load-
|
|
67
|
+
// order cycle (abi/string ← ctx ← analyze ← ir ← ctx). Kept minimal: detects
|
|
68
|
+
// any `=`/compound-assign/`++`/`--` of `name` anywhere in `body`. `let`/`const`
|
|
69
|
+
// declarations are NOT reassignments (the param can't be redeclared anyway,
|
|
70
|
+
// but a shadowing inner `let s = ...` shouldn't poison the outer param's
|
|
71
|
+
// fast path). False positives are conservative — we just disable the fast
|
|
72
|
+
// path and fall through to shape 2.
|
|
73
|
+
const _CC_ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
|
|
74
|
+
const _paramReassigned = (body, name) => {
|
|
75
|
+
if (!Array.isArray(body)) return false
|
|
76
|
+
const op = body[0]
|
|
77
|
+
if (_CC_ASSIGN_OPS.has(op) && body[1] === name) return true
|
|
78
|
+
if ((op === '++' || op === '--') && body[1] === name) return true
|
|
79
|
+
if (op === 'let' || op === 'const') {
|
|
80
|
+
for (let i = 1; i < body.length; i++) {
|
|
81
|
+
const d = body[i]
|
|
82
|
+
if (Array.isArray(d) && d[0] === '=' && d[2] != null && _paramReassigned(d[2], name)) return true
|
|
83
|
+
}
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
for (let i = 1; i < body.length; i++) if (_paramReassigned(body[i], name)) return true
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Pre-shifted SSO discriminator (bit 46 of the i64 ptr). Computed lazily on
|
|
91
|
+
* first read since LAYOUT is undefined when this module's top level runs.
|
|
92
|
+
* Memoized in the closure so all later call sites pay zero overhead. */
|
|
93
|
+
let _ssoBitI64 = null
|
|
94
|
+
const ssoBitI64 = () => _ssoBitI64 ??= '0x' + (BigInt(LAYOUT.SSO_BIT) << BigInt(LAYOUT.AUX_SHIFT)).toString(16).toUpperCase().padStart(16, '0')
|
|
95
|
+
|
|
96
|
+
/** Allocate a fresh i64 local in the current function. Replicated here (not
|
|
97
|
+
* imported from `src/ir.js`) to keep this module loadable during ctx.js
|
|
98
|
+
* bootstrap — see header note about the cycle. */
|
|
99
|
+
const allocLocalI64 = (ctx, tag) => {
|
|
100
|
+
let name
|
|
101
|
+
do { name = `_${tag}${ctx.func.uniq++}` } while (ctx.func.locals.has(name))
|
|
102
|
+
ctx.func.locals.set(name, 'i64')
|
|
103
|
+
return name
|
|
104
|
+
}
|
|
105
|
+
const allocLocalI32 = (ctx, tag) => {
|
|
106
|
+
let name
|
|
107
|
+
do { name = `_${tag}${ctx.func.uniq++}` } while (ctx.func.locals.has(name))
|
|
108
|
+
ctx.func.locals.set(name, 'i32')
|
|
109
|
+
return name
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const _isLeaf = (n) => Array.isArray(n) &&
|
|
113
|
+
(n[0] === 'local.get' || n[0] === 'global.get' ||
|
|
114
|
+
n[0] === 'i32.const' || n[0] === 'i64.const' ||
|
|
115
|
+
n[0] === 'f64.const' || n[0] === 'f32.const')
|
|
116
|
+
|
|
117
|
+
/** Per-use cheap form of `charCodeAt` against a pre-decomposed param receiver
|
|
118
|
+
* (shape 1): a bounds check plus a 2-arm SSO/heap byte select reading the four
|
|
119
|
+
* i32 decode locals in `dec`. The index is referenced three times, so a
|
|
120
|
+
* side-effecting `iI32` is spilled to a scratch local first — leaves are safe
|
|
121
|
+
* to duplicate. `oobNan` picks the OOB contract / result type exactly as in
|
|
122
|
+
* the generic path. */
|
|
123
|
+
function emitDecompCharRead(dec, iI32, ctx, oobNan) {
|
|
124
|
+
const rt = oobNan ? 'f64' : 'i32'
|
|
125
|
+
let idx = iI32, spill = null
|
|
126
|
+
if (!_isLeaf(iI32)) { spill = allocLocalI32(ctx, 'ci'); idx = ['local.get', `$${spill}`] }
|
|
127
|
+
const ssoByteExpr = ['i32.and',
|
|
128
|
+
['i32.shr_u', ['local.get', `$${dec.base}`], ['i32.shl', idx, ['i32.const', 3]]],
|
|
129
|
+
['i32.const', 0xFF]]
|
|
130
|
+
const heapByteExpr = ['i32.load8_u', ['i32.add', ['local.get', `$${dec.loadbase}`], idx]]
|
|
131
|
+
const ccByte = ['if', ['result', 'i32'],
|
|
132
|
+
['local.get', `$${dec.sso}`],
|
|
133
|
+
['then', ssoByteExpr],
|
|
134
|
+
['else', heapByteExpr]]
|
|
135
|
+
const use = ['if', ['result', rt],
|
|
136
|
+
['i32.ge_u', idx, ['local.get', `$${dec.len}`]],
|
|
137
|
+
['then', oobNan ? ['f64.const', 'nan:0x7FF8000000000000'] : ['i32.const', 0]],
|
|
138
|
+
['else', oobNan ? ['f64.convert_i32_u', ccByte] : ccByte]]
|
|
139
|
+
return spill
|
|
140
|
+
? ['block', ['result', rt], ['local.set', `$${spill}`, iI32], use]
|
|
141
|
+
: use
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Emit the function-entry decomposition prologue for the param-fast-path
|
|
145
|
+
* shape of `charCodeAt`. Decodes the f64 NaN-box once per call into three i32
|
|
146
|
+
* scratch locals (`base`, `len`, `sso`) recorded in `dec`. Per-iter
|
|
147
|
+
* `charCodeAt` then collapses to a length compare + 2-arm select; the load
|
|
148
|
+
* from `(base + i)` stays memory-safe when SSO because the inner select
|
|
149
|
+
* reroutes the address to 0 (always-valid memory[0]).
|
|
150
|
+
*
|
|
151
|
+
* Returns an array of IR statements suitable for splicing between the boxed-
|
|
152
|
+
* param inits and the user body in `emitFunc`. The off<4 guard preserves the
|
|
153
|
+
* pre-decomp semantics: any pointer with offset bits below 4 (null/undefined
|
|
154
|
+
* reaching here via type error) gets `len=0` so every bounds check trips and
|
|
155
|
+
* every `charCodeAt` returns 0 — same shape as the legacy `__char_at`. */
|
|
156
|
+
export function emitCharDecompPrologue(dec) {
|
|
157
|
+
const param = dec.param
|
|
158
|
+
const ptr = ['i64.reinterpret_f64', ['local.get', `$${param}`]]
|
|
159
|
+
const ssoTest = ['i64.ne',
|
|
160
|
+
['i64.and', ptr, ['i64.const', ssoBitI64()]],
|
|
161
|
+
['i64.const', 0]]
|
|
162
|
+
const offMask = `0x${LAYOUT.OFFSET_MASK.toString(16).toUpperCase()}`
|
|
163
|
+
const off = ['i32.wrap_i64', ['i64.and', ptr, ['i64.const', offMask]]]
|
|
164
|
+
const ssoLen = ['i32.and',
|
|
165
|
+
['i32.wrap_i64', ['i64.shr_u', ptr, ['i64.const', LAYOUT.AUX_SHIFT]]],
|
|
166
|
+
['i32.const', LAYOUT.SSO_BIT - 1]]
|
|
167
|
+
// Heap length is `i32.load(off - 4)`; guard against off<4 (corrupt/non-string
|
|
168
|
+
// payload) so the load doesn't trap on a wrapped-negative address.
|
|
169
|
+
const heapLen = ['if', ['result', 'i32'],
|
|
170
|
+
['i32.lt_u', off, ['i32.const', 4]],
|
|
171
|
+
['then', ['i32.const', 0]],
|
|
172
|
+
['else', ['i32.load', ['i32.sub', off, ['i32.const', 4]]]]]
|
|
173
|
+
return [
|
|
174
|
+
['if',
|
|
175
|
+
ssoTest,
|
|
176
|
+
['then',
|
|
177
|
+
['local.set', `$${dec.sso}`, ['i32.const', 1]],
|
|
178
|
+
['local.set', `$${dec.base}`, off],
|
|
179
|
+
['local.set', `$${dec.len}`, ssoLen],
|
|
180
|
+
// SSO: route every per-iter load to address 0 (always valid, byte
|
|
181
|
+
// discarded by the outer select).
|
|
182
|
+
['local.set', `$${dec.loadbase}`, ['i32.const', 0]]],
|
|
183
|
+
['else',
|
|
184
|
+
['local.set', `$${dec.sso}`, ['i32.const', 0]],
|
|
185
|
+
['local.set', `$${dec.base}`, off],
|
|
186
|
+
['local.set', `$${dec.len}`, heapLen],
|
|
187
|
+
// Heap: per-iter loads use the real string-data base.
|
|
188
|
+
['local.set', `$${dec.loadbase}`, off]]],
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export const sso = {
|
|
193
|
+
// Wasm slot type a string value occupies under this carrier: the f64
|
|
194
|
+
// NaN-boxed slot (PTR.STRING tag in the high bits, SSO inline data or
|
|
195
|
+
// 32-bit heap offset in the low). Read by `src/compile.js` signature
|
|
196
|
+
// synthesis to type string params/returns at the JS↔wasm boundary.
|
|
197
|
+
slotTypes: ['f64'],
|
|
198
|
+
|
|
199
|
+
ops: {
|
|
200
|
+
/** Byte length. Receiver: f64 slot carrier. Returns i32 — caller widens
|
|
201
|
+
* to f64 if it needs JS-spec `.length` semantics. */
|
|
202
|
+
byteLen: (sF64, ctx) => {
|
|
203
|
+
ctx.core.includes.add('__str_byteLen')
|
|
204
|
+
return ['call', '$__str_byteLen', ssoI64(sF64)]
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
/** Char code at index i. Receiver: f64 slot carrier; index: i32. The
|
|
208
|
+
* `oobNan` flag picks the out-of-bounds contract (see the param comment
|
|
209
|
+
* below): `false` ⇒ i32 result, `0` for OOB (raw-byte primitive);
|
|
210
|
+
* `true` ⇒ f64 result, NaN for OOB (JS-spec `String.prototype.charCodeAt`).
|
|
211
|
+
*
|
|
212
|
+
* Two emission shapes, picked at the call site:
|
|
213
|
+
*
|
|
214
|
+
* 1. **Param-decomposition fast path** — when the receiver is a `local.get`
|
|
215
|
+
* of a function parameter that isn't boxed (no closure mutation) and
|
|
216
|
+
* isn't a generator/async frame slot, we hoist the SSO-bit test, offset
|
|
217
|
+
* extraction, and heap-length load to a function-entry prologue. The
|
|
218
|
+
* prologue writes three i32 locals — `$<p>$ccbase`, `$<p>$cclen`,
|
|
219
|
+
* `$<p>$ccsso` — once per call. Every `charCodeAt` in the body collapses
|
|
220
|
+
* to a length compare + a 2-arm select between the two byte
|
|
221
|
+
* formulations: `(off >> (i*8)) & 0xFF` for the SSO 4-byte packed form
|
|
222
|
+
* and `i32.load8_u (base + i)` for the heap form. Memory safety of the
|
|
223
|
+
* load when the string is actually SSO is preserved by feeding the load
|
|
224
|
+
* address through `select(0, base+i, sso)` — for SSO strings the load
|
|
225
|
+
* reads byte 0 of linear memory (always valid in wasm), and the outer
|
|
226
|
+
* select discards the garbage. Tokenizer-shape loops go from ~13
|
|
227
|
+
* instructions per char (5 of them loop-invariant but un-LICM'd by V8
|
|
228
|
+
* because of the surrounding `if/else`) to 4: `local.get`, `i32.ge_u`,
|
|
229
|
+
* `i32.add`, `i32.load8_u`. Closes the AS gap on the tokenizer pin.
|
|
230
|
+
*
|
|
231
|
+
* 2. **Generic inline fallback** — when the receiver is some other
|
|
232
|
+
* expression (member access, call result, ternary on f64 strings, etc.)
|
|
233
|
+
* we emit the SSO-vs-heap dispatch in line. Watr's L2 default keeps the
|
|
234
|
+
* WAT-level inliner off (regex-split miscompile, watr 4.6.4), so we
|
|
235
|
+
* can't rely on watr to inline `__char_at` for us. Side-effect-free
|
|
236
|
+
* leaves (`local.get` of non-param locals, `*.const`, `global.get`)
|
|
237
|
+
* duplicate the f64→i64 reinterpret at every use and trust V8 CSE;
|
|
238
|
+
* anything heavier spills once to an i64 temp so each branch reuses the
|
|
239
|
+
* same value.
|
|
240
|
+
*
|
|
241
|
+
* Function-entry prologue emission for shape 1 happens in
|
|
242
|
+
* `src/compile.js#emitFunc` — it drains `ctx.func.charDecomp` after the
|
|
243
|
+
* body emit completes and splices an init block between the boxed-param
|
|
244
|
+
* inits and the user statements. */
|
|
245
|
+
charCodeAt: (sF64, iI32, ctx, oobNan = false) => {
|
|
246
|
+
// `oobNan` selects the out-of-bounds semantics and result type:
|
|
247
|
+
// - false (default): OOB → `i32.const 0`, result i32 — the raw-byte
|
|
248
|
+
// primitive used by the `buf += s[i]` append-byte fast path.
|
|
249
|
+
// - true: OOB → numeric NaN, result f64 — the JS-spec `charCodeAt`
|
|
250
|
+
// contract (`pos < 0 || pos >= length` ⇒ NaN). The parser hot loop
|
|
251
|
+
// `while ((cc = s.charCodeAt(i++)) <= 32)` relies on `NaN <= 32`
|
|
252
|
+
// being false to terminate; an i32 `0` would loop forever.
|
|
253
|
+
// A fresh OOB node per use — IR nodes must not be structurally shared
|
|
254
|
+
// (later passes mutate in place).
|
|
255
|
+
const mkOob = () => oobNan ? ['f64.const', 'nan:0x7FF8000000000000'] : ['i32.const', 0]
|
|
256
|
+
const rt = oobNan ? 'f64' : 'i32'
|
|
257
|
+
const widen = b => oobNan ? ['f64.convert_i32_u', b] : b
|
|
258
|
+
// Shape 1: receiver is a `local.get` of a non-boxed function parameter.
|
|
259
|
+
// The decomposition is correct only when the parameter's value can't
|
|
260
|
+
// change after entry — boxed params are stored in heap cells and read
|
|
261
|
+
// through `f64.load`, so we conservatively skip them. Non-param locals
|
|
262
|
+
// are excluded too: even if narrowing concludes they're never reassigned,
|
|
263
|
+
// their initialisation typically happens inside the function body and the
|
|
264
|
+
// prologue would run before the init.
|
|
265
|
+
if (Array.isArray(sF64) && sF64[0] === 'local.get') {
|
|
266
|
+
const raw = typeof sF64[1] === 'string' ? sF64[1] : ''
|
|
267
|
+
const name = raw.startsWith('$') ? raw.slice(1) : raw
|
|
268
|
+
const param = ctx.func.current?.params?.find(p => p.name === name)
|
|
269
|
+
const isBoxed = ctx.func.boxed?.has(name)
|
|
270
|
+
// The decomposition only stays in sync if the param's f64 slot is
|
|
271
|
+
// never overwritten — `s = s + 'X'` would invalidate the cached
|
|
272
|
+
// base/len/sso/loadbase locals. Also require the param's wasm slot
|
|
273
|
+
// to actually be `f64` (i64.reinterpret_f64 below would fail
|
|
274
|
+
// validation otherwise — narrowed-to-int params have type 'i32').
|
|
275
|
+
if (param && param.type === 'f64' && param.ptrKind == null
|
|
276
|
+
&& !isBoxed && ctx.func.body && !_paramReassigned(ctx.func.body, name)) {
|
|
277
|
+
if (!ctx.func.charDecomp) ctx.func.charDecomp = new Map()
|
|
278
|
+
let dec = ctx.func.charDecomp.get(name)
|
|
279
|
+
if (!dec) {
|
|
280
|
+
const base = `${name}$ccbase`
|
|
281
|
+
const len = `${name}$cclen`
|
|
282
|
+
const sso = `${name}$ccsso`
|
|
283
|
+
// `loadbase` is the address used for the per-iter `load8_u`: equal
|
|
284
|
+
// to `base` when heap (real string data) and 0 when SSO (memory[0]
|
|
285
|
+
// is always valid; the loaded byte is garbage but the outer select
|
|
286
|
+
// discards it). Pre-computing it in the prologue removes a
|
|
287
|
+
// per-iter `select` and lets V8 fold the add into the load.
|
|
288
|
+
const loadbase = `${name}$ccldb`
|
|
289
|
+
ctx.func.locals.set(base, 'i32')
|
|
290
|
+
ctx.func.locals.set(len, 'i32')
|
|
291
|
+
ctx.func.locals.set(sso, 'i32')
|
|
292
|
+
ctx.func.locals.set(loadbase, 'i32')
|
|
293
|
+
dec = { base, len, sso, loadbase, param: name }
|
|
294
|
+
ctx.func.charDecomp.set(name, dec)
|
|
295
|
+
}
|
|
296
|
+
return emitDecompCharRead(dec, iI32, ctx, oobNan)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Shape 2: generic inline form for non-param receivers.
|
|
301
|
+
//
|
|
302
|
+
// Both the receiver `sF64` and the index `iI32` are duplicated across the
|
|
303
|
+
// SSO/heap branches (and within each branch for bounds + load). Either
|
|
304
|
+
// side may be a side-effecting expression — e.g. parse.js's `str[i++]`
|
|
305
|
+
// lowers to a charCodeAt whose index is a `(block (local.set $tmp
|
|
306
|
+
// (f64.add (f64.load $i) 1)) (f64.store $i $tmp) (local.get $tmp))` —
|
|
307
|
+
// so we MUST spill anything that isn't side-effect-free to a local
|
|
308
|
+
// before referencing it more than once. Leaves (`local.get`, `*.const`,
|
|
309
|
+
// `global.get`) are safe to duplicate; everything else is spilled.
|
|
310
|
+
const isLeaf = (n) => Array.isArray(n) &&
|
|
311
|
+
(n[0] === 'local.get' || n[0] === 'global.get' ||
|
|
312
|
+
n[0] === 'i32.const' || n[0] === 'i64.const' || n[0] === 'f64.const' || n[0] === 'f32.const')
|
|
313
|
+
const sLeaf = isLeaf(sF64)
|
|
314
|
+
const iLeaf = isLeaf(iI32)
|
|
315
|
+
const ptrI64Expr = ssoI64(sF64)
|
|
316
|
+
let ptrName = null
|
|
317
|
+
let getPtr
|
|
318
|
+
if (sLeaf) {
|
|
319
|
+
getPtr = () => ssoI64(sF64)
|
|
320
|
+
} else {
|
|
321
|
+
ptrName = allocLocalI64(ctx, 'cc')
|
|
322
|
+
getPtr = () => ['local.get', `$${ptrName}`]
|
|
323
|
+
}
|
|
324
|
+
let idxName = null
|
|
325
|
+
let getIdx
|
|
326
|
+
if (iLeaf) {
|
|
327
|
+
getIdx = () => iI32
|
|
328
|
+
} else {
|
|
329
|
+
idxName = allocLocalI32(ctx, 'ci')
|
|
330
|
+
getIdx = () => ['local.get', `$${idxName}`]
|
|
331
|
+
}
|
|
332
|
+
const offMask = `0x${LAYOUT.OFFSET_MASK.toString(16).toUpperCase()}`
|
|
333
|
+
const offExpr = () => ['i32.wrap_i64', ['i64.and', getPtr(), ['i64.const', offMask]]]
|
|
334
|
+
const ssoLen = ['i32.and',
|
|
335
|
+
['i32.wrap_i64', ['i64.shr_u', getPtr(), ['i64.const', LAYOUT.AUX_SHIFT]]],
|
|
336
|
+
['i32.const', LAYOUT.SSO_BIT - 1]]
|
|
337
|
+
const ssoByte = ['i32.and',
|
|
338
|
+
['i32.shr_u', offExpr(), ['i32.mul', getIdx(), ['i32.const', 8]]],
|
|
339
|
+
['i32.const', 0xFF]]
|
|
340
|
+
const ssoBranch = ['if', ['result', rt],
|
|
341
|
+
['i32.ge_u', getIdx(), ssoLen],
|
|
342
|
+
['then', mkOob()],
|
|
343
|
+
['else', widen(ssoByte)]]
|
|
344
|
+
const heapLen = ['i32.load', ['i32.sub', offExpr(), ['i32.const', 4]]]
|
|
345
|
+
const heapByte = ['i32.load8_u', ['i32.add', offExpr(), getIdx()]]
|
|
346
|
+
const heapBranch = ['if', ['result', rt],
|
|
347
|
+
['i32.lt_u', offExpr(), ['i32.const', 4]],
|
|
348
|
+
['then', mkOob()],
|
|
349
|
+
['else', ['if', ['result', rt],
|
|
350
|
+
['i32.ge_u', getIdx(), heapLen],
|
|
351
|
+
['then', mkOob()],
|
|
352
|
+
['else', widen(heapByte)]]]]
|
|
353
|
+
const dispatch = ['if', ['result', rt],
|
|
354
|
+
['i64.ne', ['i64.and', getPtr(), ['i64.const', ssoBitI64()]], ['i64.const', 0]],
|
|
355
|
+
['then', ssoBranch],
|
|
356
|
+
['else', heapBranch]]
|
|
357
|
+
if (sLeaf && iLeaf) return dispatch
|
|
358
|
+
const preface = ['block', ['result', rt]]
|
|
359
|
+
if (!sLeaf) preface.push(['local.set', `$${ptrName}`, ptrI64Expr])
|
|
360
|
+
if (!iLeaf) preface.push(['local.set', `$${idxName}`, iI32])
|
|
361
|
+
preface.push(dispatch)
|
|
362
|
+
return preface
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
/** Content equality. Both args: f64 slot carriers. Returns i32 boolean. */
|
|
366
|
+
eq: (aF64, bF64, ctx) => {
|
|
367
|
+
ctx.core.includes.add('__str_eq')
|
|
368
|
+
return ['call', '$__str_eq', ssoI64(aF64), ssoI64(bF64)]
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
/** Three-way byte compare. Both args: f64 slot carriers. Returns i32 ∈ {-1, 0, 1}. */
|
|
372
|
+
cmp: (aF64, bF64, ctx) => {
|
|
373
|
+
ctx.core.includes.add('__str_cmp')
|
|
374
|
+
return ['call', '$__str_cmp', ssoI64(aF64), ssoI64(bF64)]
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
/** Concat with ToString coercion on both sides. Both args: f64 slot
|
|
378
|
+
* carriers. Returns f64 (the new STRING ptr's slot carrier). */
|
|
379
|
+
concat: (aF64, bF64, ctx) => {
|
|
380
|
+
ctx.core.includes.add('__str_concat')
|
|
381
|
+
return ['call', '$__str_concat', ssoI64(aF64), ssoI64(bF64)]
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
/** Concat assuming both sides are already strings (skip ToString). */
|
|
385
|
+
concatRaw: (aF64, bF64, ctx) => {
|
|
386
|
+
ctx.core.includes.add('__str_concat_raw')
|
|
387
|
+
return ['call', '$__str_concat_raw', ssoI64(aF64), ssoI64(bF64)]
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ── jsstring ──────────────────────────────────────────────────────────────
|
|
393
|
+
//
|
|
394
|
+
// Architectural scaffold for native JS strings via JS String Builtins.
|
|
395
|
+
// Under this carrier, string values flow across the wasm boundary as
|
|
396
|
+
// `externref` instead of nanbox-tagged heap offsets. String operations
|
|
397
|
+
// (`length`, `charCodeAt`, `concat`, `fromCharCode`, …) are emitted as
|
|
398
|
+
// calls to imports from the `wasm:js-string` namespace — engine-provided
|
|
399
|
+
// builtins that read/write the engine's native String representation.
|
|
400
|
+
//
|
|
401
|
+
// Spec: https://webassembly.github.io/js-string-builtins/js-api/
|
|
402
|
+
// Engine support: V8 17+, Safari 18.4+, Firefox behind a flag.
|
|
403
|
+
//
|
|
404
|
+
// ### Status — scaffold, not a working codegen path
|
|
405
|
+
//
|
|
406
|
+
// Today this carrier exists to:
|
|
407
|
+
// 1. Slot into the default-bundle in `src/abi/index.js` so the dispatch
|
|
408
|
+
// infrastructure is exercised end-to-end with two carriers.
|
|
409
|
+
// 2. Document the contract future string-codegen rerouting will plug into.
|
|
410
|
+
// 3. Outline the compiler-wide changes a real implementation requires —
|
|
411
|
+
// they're larger than "fill in the ops table" and need their own plan.
|
|
412
|
+
//
|
|
413
|
+
// Until those changes land, `jsstring` is exported alongside `sso` but the
|
|
414
|
+
// default bundle in `src/abi/index.js` still picks `sso`. The narrower will
|
|
415
|
+
// flip individual sites to `jsstring` once the codegen paths below are real.
|
|
416
|
+
//
|
|
417
|
+
// ### Wire shape
|
|
418
|
+
//
|
|
419
|
+
// (import "wasm:js-string" "length" (func $__jss_length (param externref) (result i32)))
|
|
420
|
+
// (import "wasm:js-string" "charCodeAt" (func $__jss_charCodeAt (param externref i32) (result i32)))
|
|
421
|
+
// (import "wasm:js-string" "concat" (func $__jss_concat (param externref externref) (result (ref extern))))
|
|
422
|
+
// (import "wasm:js-string" "compare" (func $__jss_compare (param externref externref) (result i32)))
|
|
423
|
+
// (import "wasm:js-string" "test" (func $__jss_test (param externref) (result i32)))
|
|
424
|
+
// (import "wasm:js-string" "fromCharCode" (func $__jss_fromCharCode (param i32) (result (ref extern))))
|
|
425
|
+
// (import "wasm:js-string" "substring" (func $__jss_substring (param externref i32 i32) (result (ref extern))))
|
|
426
|
+
//
|
|
427
|
+
// ### Compiler-wide checklist (dependency order)
|
|
428
|
+
//
|
|
429
|
+
// 1. **Import declaration channel.** Mirror `ctx.core.includes` for
|
|
430
|
+
// `wasm:js-string` imports — a `ctx.core.imports` set that compile.js
|
|
431
|
+
// drains into `(import ...)` nodes. The string-builtins API is feature-
|
|
432
|
+
// detected (`WebAssembly.validate` with the import set), so the host
|
|
433
|
+
// must either gate compile output on builtins support or polyfill the
|
|
434
|
+
// imports from JS for older engines.
|
|
435
|
+
//
|
|
436
|
+
// 2. **STRING-typed locals as externref.** `ctx.func.locals` today stores
|
|
437
|
+
// `'f64' | 'i32'` per local; STRING locals need `'externref'`. Touch
|
|
438
|
+
// every site that declares string locals (closures, params, refinements,
|
|
439
|
+
// destructuring) so the WAT `(local $name externref)` lands.
|
|
440
|
+
//
|
|
441
|
+
// 3. **emit() returns externref for STRING-typed nodes.** Today every
|
|
442
|
+
// `emit(strNode)` returns f64-typed IR carrying a NaN-boxed pointer.
|
|
443
|
+
// Under jsstring it must return externref-typed IR. The `asF64` call
|
|
444
|
+
// sites currently feeding the carrier would route through a carrier-
|
|
445
|
+
// driven `coerceSlot(emit(...))` helper instead, so the slot type swap
|
|
446
|
+
// is transparent to callers.
|
|
447
|
+
//
|
|
448
|
+
// 4. **Boundary wrappers.** `src/compile.js:synthesizeBoundaryWrappers`
|
|
449
|
+
// types every string param/result through f64 (or i64 for ptr carriers).
|
|
450
|
+
// Read `ctx.abi.string.slotTypes[0]` — if `externref`, declare the
|
|
451
|
+
// param/result `externref` and skip the nanbox box/unbox steps.
|
|
452
|
+
//
|
|
453
|
+
// 5. **Literals.** `['str', "foo"]` today writes into the heap and returns
|
|
454
|
+
// a NaN-boxed pointer. Under jsstring it would need a module-level
|
|
455
|
+
// `externref` global initialized from a JS-side literal table — likely
|
|
456
|
+
// via a startup import that hands back the canonical `externref` for
|
|
457
|
+
// each known string. Or build at runtime with `fromCharCodeArray`.
|
|
458
|
+
//
|
|
459
|
+
// 6. **Mutating fast paths.** Heap-string optimizations like
|
|
460
|
+
// `__str_append_byte` (mutate in place when lhs is heap-top) don't
|
|
461
|
+
// translate — engine strings are immutable. These paths must gate off
|
|
462
|
+
// under jsstring (`if (slotTypes[0] === 'f64') …`) or be removed from
|
|
463
|
+
// the carrier's surface entirely.
|
|
464
|
+
//
|
|
465
|
+
// 7. **Cross-carrier interop.** Mixing nanbox numeric values and externref
|
|
466
|
+
// strings in the same function means locals span two slot types.
|
|
467
|
+
// `i32`/`f64`/`externref` already coexist (closures use `i32` for boxed
|
|
468
|
+
// cells), so the multi-slot story is incremental, not novel.
|
|
469
|
+
//
|
|
470
|
+
// 8. **`?.length`, optional access.** The `?.` emit threads `local.get`
|
|
471
|
+
// through `notNullish`, which today inspects f64 NaN-shape bits.
|
|
472
|
+
// `externref` nullishness is a single `ref.is_null` (no NaN inspection),
|
|
473
|
+
// so the optional-chain emit needs a carrier-aware nullish predicate.
|
|
474
|
+
//
|
|
475
|
+
// 9. **Host wiring.** `interop.js` passes externref strings directly
|
|
476
|
+
// (no encode/decode) and supplies the `wasm:js-string` imports object
|
|
477
|
+
// when the binary references any. JS strings already act as externrefs
|
|
478
|
+
// at the host boundary — the bridge mostly hands them through.
|
|
479
|
+
|
|
480
|
+
export const jsstring = {
|
|
481
|
+
// Wasm slot type a string value occupies under this carrier. Read by
|
|
482
|
+
// `src/compile.js:synthesizeBoundaryWrappers` for param/result typing
|
|
483
|
+
// (item 4) and by the slot coercer at every STRING `emit()` call site
|
|
484
|
+
// (item 3).
|
|
485
|
+
slotTypes: ['externref'],
|
|
486
|
+
|
|
487
|
+
// Names of `wasm:js-string` imports this carrier relies on. Used
|
|
488
|
+
// (eventually) by the compiler to declare the import nodes once any op
|
|
489
|
+
// references them.
|
|
490
|
+
imports: ['length', 'charCodeAt', 'concat', 'fromCharCode', 'substring',
|
|
491
|
+
'codePointAt', 'compare', 'test', 'intoCharCodeArray', 'fromCharCodeArray'],
|
|
492
|
+
|
|
493
|
+
// Op hooks — string operations routed through this carrier. Inputs are
|
|
494
|
+
// already externref-typed (the param/local that carries the value); outputs
|
|
495
|
+
// are i32 (`length`, `charCodeAt`) or (for future ops) externref again.
|
|
496
|
+
// Each op registers its builtin import via `ctx.core.jsstring.add(name)`;
|
|
497
|
+
// `compile.js` drains the set into `(import "wasm:js-string" …)` nodes.
|
|
498
|
+
ops: {
|
|
499
|
+
/** Byte length. Receiver: externref. Returns i32 — caller widens to f64
|
|
500
|
+
* if it needs JS-spec `.length` (a number for the JS-visible export). */
|
|
501
|
+
byteLen: (sExt, ctx) => {
|
|
502
|
+
ctx.core.jsstring.add('length')
|
|
503
|
+
return ['call', '$__jss_length', sExt]
|
|
504
|
+
},
|
|
505
|
+
|
|
506
|
+
/** Char code at index i. Receiver: externref; index: i32. Returns i32.
|
|
507
|
+
* `wasm:js-string.charCodeAt` traps on OOB — callers must only use this
|
|
508
|
+
* shape under an in-bounds proof (analyze.js `inBoundsCharCodeAt`). The
|
|
509
|
+
* generic OOB-NaN contract is still handled via the SSO fallback (which
|
|
510
|
+
* doesn't apply when the receiver is externref — see core.js dispatch). */
|
|
511
|
+
charCodeAt: (sExt, iI32, ctx) => {
|
|
512
|
+
ctx.core.jsstring.add('charCodeAt')
|
|
513
|
+
return ['call', '$__jss_charCodeAt', sExt, iI32]
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Signatures for the `wasm:js-string` imports this carrier emits. Indexed by
|
|
519
|
+
// builtin name (the bare key drained from `ctx.core.jsstring`); `compile.js`
|
|
520
|
+
// builds the `(import …)` node from this table.
|
|
521
|
+
export const JSS_IMPORT_SIGS = {
|
|
522
|
+
length: { params: ['externref'], result: 'i32' },
|
|
523
|
+
charCodeAt: { params: ['externref', 'i32'], result: 'i32' },
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Default carrier — picked when narrower has no stronger evidence. Reached
|
|
527
|
+
// via `ctx.abi.string` (which the default-bundle in `src/abi/index.js` binds
|
|
528
|
+
// to this export).
|
|
529
|
+
export default sso
|