jz 0.6.0 → 0.8.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 +99 -72
- package/bench/README.md +253 -100
- package/bench/bench.svg +58 -81
- package/cli.js +85 -12
- package/dist/interop.js +1 -0
- package/dist/jz.js +6196 -0
- package/index.d.ts +126 -0
- package/index.js +222 -35
- package/interop.js +240 -141
- package/layout.js +34 -18
- package/module/array.js +99 -111
- package/module/collection.js +124 -30
- package/module/console.js +1 -1
- package/module/core.js +163 -19
- package/module/json.js +3 -3
- package/module/math.js +162 -3
- package/module/number.js +268 -13
- package/module/object.js +37 -5
- package/module/regex.js +8 -7
- package/module/simd.js +37 -5
- package/module/string.js +203 -168
- package/module/typedarray.js +565 -112
- package/package.json +26 -7
- package/src/abi/string.js +29 -29
- package/src/ast.js +19 -2
- package/src/autoload.js +3 -0
- package/src/compile/analyze-scans.js +174 -11
- package/src/compile/analyze.js +101 -4
- package/src/compile/cse-load.js +200 -0
- package/src/compile/emit-assign.js +82 -20
- package/src/compile/emit.js +592 -51
- package/src/compile/index.js +504 -89
- package/src/compile/infer.js +36 -1
- package/src/compile/loop-divmod.js +109 -0
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +275 -41
- package/src/compile/peel-stencil.js +215 -0
- package/src/compile/plan/advise.js +55 -1
- package/src/compile/plan/common.js +29 -0
- package/src/compile/plan/index.js +8 -1
- package/src/compile/plan/inline.js +180 -22
- package/src/compile/plan/literals.js +313 -24
- package/src/compile/plan/scope.js +115 -39
- package/src/compile/program-facts.js +21 -2
- package/src/ctx.js +96 -19
- package/src/helper-counters.js +137 -0
- package/src/ir.js +157 -20
- package/src/kind-traits.js +34 -3
- package/src/kind.js +79 -4
- package/src/op-policy.js +5 -2
- package/src/ops.js +119 -0
- package/src/optimize/index.js +1274 -151
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +4187 -253
- package/src/prepare/index.js +75 -14
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +6 -2
- package/src/static.js +9 -0
- package/src/type.js +63 -51
- package/src/wat/assemble.js +286 -21
- package/src/widen.js +21 -0
- package/src/wat/optimize.js +0 -3760
package/interop.js
CHANGED
|
@@ -106,51 +106,81 @@ const sectionReader = (bytes) => {
|
|
|
106
106
|
|
|
107
107
|
// ── NaN-box codec ───────────────────────────────────────────────────────────
|
|
108
108
|
|
|
109
|
-
// NaN-
|
|
109
|
+
// NaN-box codec — integer / BigInt based. A box NEVER becomes a JS number: JSC (Safari)
|
|
110
|
+
// canonicalizes a NaN payload the instant it materializes as f64 (boundary return,
|
|
111
|
+
// Float64Array read, getFloat64), so a box is carried in JS-land as a BigInt (the i64
|
|
112
|
+
// bits) and decoded with integer ops. Only genuine (non-NaN) numbers ever touch f64.
|
|
113
|
+
const MASK32 = 0xffffffffn
|
|
114
|
+
// Reinterpret for GENUINE numbers (and freshly-built boxes leaving JS): `_f64` only ever
|
|
115
|
+
// holds a real number here, never a live NaN-box, so there is nothing for JSC to purify.
|
|
110
116
|
const _buf = new ArrayBuffer(8), _u32 = new Uint32Array(_buf), _f64 = new Float64Array(_buf)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
export const
|
|
123
|
-
export const
|
|
117
|
+
export const f64ToI64 = (n) => { _f64[0] = n; return (BigInt(_u32[1]) << 32n) | BigInt(_u32[0] >>> 0) }
|
|
118
|
+
export const i64ToF64 = (b) => { _u32[0] = Number(b & MASK32); _u32[1] = Number((b >> 32n) & MASK32); return _f64[0] }
|
|
119
|
+
|
|
120
|
+
const hi32 = (b) => Number((b >> 32n) & MASK32)
|
|
121
|
+
// A NaN-box is a sign-0 quiet NaN — high u32 carries jz's 0x7FF8 prefix.
|
|
122
|
+
const isBox = (b) => (hi32(b) & 0x7FF80000) === 0x7FF80000
|
|
123
|
+
// i64 bits for a wrapVal result (BigInt box, or number → its f64 bits): memory staging + i64 params.
|
|
124
|
+
const bits = (v) => typeof v === 'bigint' ? v : f64ToI64(v)
|
|
125
|
+
|
|
126
|
+
// Reserved atoms (type=ATOM, offset=0): aux 1/2/4/5 → null/undefined/false/true. BigInt boxes.
|
|
127
|
+
export const NULL_NAN = BigInt(ATOM_HI[ATOM.NULL]) << 32n
|
|
128
|
+
export const UNDEF_NAN = BigInt(ATOM_HI[ATOM.UNDEF]) << 32n
|
|
129
|
+
export const FALSE_NAN = BigInt(ATOM_HI[ATOM.FALSE]) << 32n
|
|
130
|
+
export const TRUE_NAN = BigInt(ATOM_HI[ATOM.TRUE]) << 32n
|
|
131
|
+
|
|
132
|
+
// Coerce JS null/undefined → boxed atom (BigInt); everything else passes through.
|
|
133
|
+
export const coerce = v => v === null ? NULL_NAN : v === undefined ? UNDEF_NAN : v
|
|
124
134
|
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
135
|
+
// SSO-encode a string ≤6 ASCII chars to a NaN-box BigInt (no heap needed).
|
|
136
|
+
// Mirrors mem.String's SSO branch. Used when marshaling a string into an i64-carrier
|
|
137
|
+
// param of a memoryless module (no linear memory, so only self-contained bit encodings
|
|
138
|
+
// like SSO can survive the boundary). Non-SSO strings throw clearly rather than silently
|
|
139
|
+
// becoming NaN.
|
|
140
|
+
const encodeSSO = (s) => {
|
|
141
|
+
let p = 0n
|
|
142
|
+
for (let i = 0; i < s.length; i++) p |= BigInt(s.charCodeAt(i)) << BigInt(i * 7)
|
|
143
|
+
p |= BigInt(s.length) << 42n
|
|
144
|
+
return ptr(4, Number(p >> 32n) | LAYOUT.SSO_BIT, Number(p & 0xFFFFFFFFn))
|
|
145
|
+
}
|
|
131
146
|
|
|
132
|
-
//
|
|
133
|
-
|
|
147
|
+
// Accept either the i64 carrier (BigInt, canonical) or a legacy f64 NaN-box (intact on V8 —
|
|
148
|
+
// e.g. an adaptI64 result, or user code holding a pre-i64 pointer) — normalize before decode.
|
|
149
|
+
const asBits = (p) => typeof p === 'bigint' ? p : f64ToI64(p)
|
|
150
|
+
export const ptr = (type, aux, offset) => (BigInt(encodePtrHi(type, aux)) << 32n) | BigInt(offset >>> 0)
|
|
151
|
+
export const offset = (p) => Number(asBits(p) & MASK32)
|
|
152
|
+
export const type = (p) => decodePtrType(hi32(asBits(p)))
|
|
153
|
+
export const aux = (p) => decodePtrAux(hi32(asBits(p)))
|
|
154
|
+
|
|
155
|
+
// SSO string decode from i64 bits: 7-bit ASCII, char i at payload bit i*7, len at bits 42-44.
|
|
156
|
+
const decodeSSO = (b) => {
|
|
157
|
+
const a = decodePtrAux(hi32(b)), len = (a >>> 10) & 7
|
|
158
|
+
const payload = (BigInt(a) << 32n) | BigInt(Number(b & MASK32))
|
|
159
|
+
let s = ''
|
|
160
|
+
for (let i = 0; i < len; i++) s += String.fromCharCode(Number((payload >> BigInt(i * 7)) & 0x7fn))
|
|
161
|
+
return s
|
|
162
|
+
}
|
|
134
163
|
|
|
135
|
-
//
|
|
164
|
+
// Memory-free decode of an i64-bits boundary value: numbers pass through, a box becomes
|
|
165
|
+
// its atom / SSO string. Exactly the forms a *memoryless* module can carry (no linear
|
|
166
|
+
// memory → no heap string/array/object). Heap-carrying modules route through `mem.read`.
|
|
136
167
|
const decode = v => {
|
|
137
|
-
if (v
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
if (
|
|
141
|
-
if (
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
|
|
168
|
+
if (Array.isArray(v)) return v.map(decode) // multi-value tuple — each lane is an i64-carrier (memoryless)
|
|
169
|
+
if (typeof v === 'number') { if (v === v) return v; v = f64ToI64(v) } // f64 NaN-box (intact on V8) → bits
|
|
170
|
+
else if (typeof v !== 'bigint') return v // already-decoded JS value
|
|
171
|
+
if (!isBox(v)) return i64ToF64(v) // non-NaN bits → number
|
|
172
|
+
if (type(v) === 4 && (aux(v) & LAYOUT.SSO_BIT)) return decodeSSO(v)
|
|
173
|
+
if (offset(v) === 0) {
|
|
174
|
+
if (v === NULL_NAN) return null
|
|
175
|
+
if (v === UNDEF_NAN) return undefined
|
|
176
|
+
if (v === FALSE_NAN) return false
|
|
177
|
+
if (v === TRUE_NAN) return true
|
|
178
|
+
}
|
|
179
|
+
return i64ToF64(v) // canonical NaN-number / unknown
|
|
145
180
|
}
|
|
146
181
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
_u32[0] = offset >>> 0; return _f64[0]
|
|
150
|
-
}
|
|
151
|
-
export const offset = (p) => { _f64[0] = p; return _u32[0] }
|
|
152
|
-
export const type = (p) => { _f64[0] = p; return decodePtrType(_u32[1]) }
|
|
153
|
-
export const aux = (p) => { _f64[0] = p; return decodePtrAux(_u32[1]) }
|
|
182
|
+
// Decode a boundary value arriving as i64 bits (BigInt). Heap modules go through mem.read.
|
|
183
|
+
const readArgBits = (state, big) => state.mem ? state.mem.read(big) : decode(big)
|
|
154
184
|
|
|
155
185
|
// Typed element metadata: [elemId, byteStride, DataView getter, DataView setter]
|
|
156
186
|
const ELEMS = {
|
|
@@ -196,7 +226,11 @@ export const memory = (src) => {
|
|
|
196
226
|
// Instance result: { module, instance, exports, extMap }
|
|
197
227
|
const raw = src?.instance?.exports || src?.exports || src
|
|
198
228
|
mem = src?.exports?.memory || raw.memory
|
|
199
|
-
|
|
229
|
+
// Memoryless module (SSO strings / atoms / numbers only — no linear memory):
|
|
230
|
+
// hand back a minimal reader instead of null so callers can still decode its
|
|
231
|
+
// boundary values from bits. `read`/`wrapVal` cover the value forms that exist
|
|
232
|
+
// without memory; `scalar` flags the fast path that skips heap marshaling.
|
|
233
|
+
if (!mem) return { read: decode, wrapVal: coerce, scalar: true }
|
|
200
234
|
wasmExports = { ...raw, memory: mem }
|
|
201
235
|
extMap = src.extMap || null
|
|
202
236
|
mod = src.module || null
|
|
@@ -249,8 +283,7 @@ export const memory = (src) => {
|
|
|
249
283
|
if (_enhanced.has(mem)) {
|
|
250
284
|
mem.schemas = schemas
|
|
251
285
|
if (wasmExports?._alloc) { alloc = wasmExports._alloc; mem.alloc = alloc }
|
|
252
|
-
|
|
253
|
-
else if (!mem.reset) mem.reset = jsReset
|
|
286
|
+
mem.reset = jsReset // post-init rewind — see the note at the first-enhance path
|
|
254
287
|
if (extMap) mem._extMap = extMap
|
|
255
288
|
return mem
|
|
256
289
|
}
|
|
@@ -265,17 +298,19 @@ export const memory = (src) => {
|
|
|
265
298
|
// NaN-payload doubles to HOLEY_DOUBLE_ELEMENTS, which canonicalizes the NaN
|
|
266
299
|
// payload to 0x7FF8000000000000 — destroying the type/offset bits.
|
|
267
300
|
const wrapped = new BigInt64Array(n)
|
|
268
|
-
for (let i = 0; i < n; i++) wrapped[i] =
|
|
301
|
+
for (let i = 0; i < n; i++) wrapped[i] = bits(mem.wrapVal(data[i]))
|
|
269
302
|
const dst = new BigInt64Array(mem.buffer, off, n)
|
|
270
303
|
for (let i = 0; i < n; i++) dst[i] = wrapped[i]
|
|
271
304
|
return ptr(1, 0, off)
|
|
272
305
|
}
|
|
273
306
|
|
|
274
307
|
mem.String = (str) => {
|
|
275
|
-
if (str.length <=
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
308
|
+
if (str.length <= 6 && /^[\x00-\x7f]*$/.test(str)) {
|
|
309
|
+
// 7-bit ASCII SSO: char i at payload bit i*7, len at bits 42-44 (see module/string.js codec).
|
|
310
|
+
let p = 0n
|
|
311
|
+
for (let i = 0; i < str.length; i++) p |= BigInt(str.charCodeAt(i)) << BigInt(i * 7)
|
|
312
|
+
p |= BigInt(str.length) << 42n
|
|
313
|
+
return ptr(4, Number(p >> 32n) | LAYOUT.SSO_BIT, Number(p & 0xFFFFFFFFn)) // STRING + SSO_BIT
|
|
279
314
|
}
|
|
280
315
|
const enc = TEXT_ENC.encode(str)
|
|
281
316
|
const n = enc.length, raw = alloc(4 + n), m = dv()
|
|
@@ -298,9 +333,10 @@ export const memory = (src) => {
|
|
|
298
333
|
if (v === null || v === undefined) return coerce(v)
|
|
299
334
|
if (typeof v === 'number' || typeof v === 'boolean') return Number(v)
|
|
300
335
|
if (typeof v === 'string') return mem.String(v)
|
|
301
|
-
// BigInt
|
|
302
|
-
//
|
|
303
|
-
|
|
336
|
+
// A BigInt that is a NaN-box (jz's i64 carrier — e.g. a value pre-built via memory.String/
|
|
337
|
+
// ptr) passes straight through. A plain bigint *value* crosses as a decimal-string (wasm
|
|
338
|
+
// numeric parsers accept it).
|
|
339
|
+
if (typeof v === 'bigint') return isBox(v) ? v : mem.String(v.toString())
|
|
304
340
|
if (Array.isArray(v)) return mem.Array(v)
|
|
305
341
|
if (v instanceof ArrayBuffer) return mem.Buffer(v)
|
|
306
342
|
if (v instanceof DataView) return mem.Buffer(v.buffer)
|
|
@@ -341,7 +377,7 @@ export const memory = (src) => {
|
|
|
341
377
|
if (v === null || v === undefined) v = coerce(v)
|
|
342
378
|
else if (typeof v === 'string') v = mem.String(v)
|
|
343
379
|
else if (Array.isArray(v)) v = mem.Array(v)
|
|
344
|
-
wrapped[i] =
|
|
380
|
+
wrapped[i] = bits(v)
|
|
345
381
|
}
|
|
346
382
|
const dst = new BigInt64Array(mem.buffer, raw, n)
|
|
347
383
|
for (let i = 0; i < n; i++) dst[i] = wrapped[i]
|
|
@@ -350,8 +386,15 @@ export const memory = (src) => {
|
|
|
350
386
|
|
|
351
387
|
mem.read = function(p) {
|
|
352
388
|
if (Array.isArray(p)) return p.map(v => mem.read(v)) // multi-value tuple
|
|
353
|
-
if (p ===
|
|
354
|
-
|
|
389
|
+
if (typeof p === 'number') {
|
|
390
|
+
if (p === p) return p // genuine number passthrough (NaN fails ===)
|
|
391
|
+
p = f64ToI64(p) // f64 NaN-box (intact on V8) → bits; decode below
|
|
392
|
+
} else if (typeof p !== 'bigint') {
|
|
393
|
+
return p // already a decoded JS value (string/object/…) — passthrough
|
|
394
|
+
}
|
|
395
|
+
// p is now i64 bits (BigInt). Decode with integer ops — never materialize as f64.
|
|
396
|
+
if (!isBox(p)) return i64ToF64(p) // non-NaN bits → genuine number
|
|
397
|
+
const m = dv(), t = type(p), a = aux(p), off = offset(p)
|
|
355
398
|
if (t === 0 && off === 0) {
|
|
356
399
|
if (a === 1) return null
|
|
357
400
|
if (a === 2) return undefined
|
|
@@ -360,19 +403,18 @@ export const memory = (src) => {
|
|
|
360
403
|
}
|
|
361
404
|
if (t === 11 && mem._extMap) return mem._extMap[off]
|
|
362
405
|
if (t === 1) { // ARRAY
|
|
363
|
-
let
|
|
406
|
+
let aOff = off
|
|
364
407
|
// Follow forwarding pointers (cap === -1 means array was reallocated)
|
|
365
408
|
while (m.getInt32(aOff - 4, true) === -1) aOff = m.getInt32(aOff - 8, true)
|
|
366
409
|
const len = m.getInt32(aOff - 8, true), out = new Array(len)
|
|
367
|
-
for (let i = 0; i < len; i++) out[i] = mem.read(m.
|
|
410
|
+
for (let i = 0; i < len; i++) out[i] = mem.read(m.getBigInt64(aOff + i * 8, true))
|
|
368
411
|
return out
|
|
369
412
|
}
|
|
370
413
|
if (t === 3) { // TYPED
|
|
371
|
-
const
|
|
414
|
+
const elem = a & 7
|
|
372
415
|
const [, stride] = ELEM_BY_ID[elem]
|
|
373
416
|
const Ctor = [Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array][elem]
|
|
374
|
-
|
|
375
|
-
if (a2 & 8) {
|
|
417
|
+
if (a & 8) {
|
|
376
418
|
const byteLen = m.getInt32(off, true), dataOff = m.getInt32(off + 4, true)
|
|
377
419
|
return new Ctor(mem.buffer, dataOff, byteLen / stride)
|
|
378
420
|
}
|
|
@@ -380,61 +422,47 @@ export const memory = (src) => {
|
|
|
380
422
|
return new Ctor(mem.buffer, off, byteLen / stride)
|
|
381
423
|
}
|
|
382
424
|
if (t === 2) { // BUFFER
|
|
383
|
-
const byteLen =
|
|
425
|
+
const byteLen = m.getInt32(off - 8, true)
|
|
384
426
|
const out = new ArrayBuffer(byteLen)
|
|
385
427
|
new Uint8Array(out).set(new Uint8Array(mem.buffer, off, byteLen))
|
|
386
428
|
return out
|
|
387
429
|
}
|
|
388
430
|
if (t === 4) { // STRING (aux SSO_BIT = inline, else heap)
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const len = a2 & 0x7; let s = ''
|
|
392
|
-
for (let i = 0; i < len; i++) s += String.fromCharCode((off >>> (i * 8)) & 0xFF)
|
|
393
|
-
return s
|
|
394
|
-
}
|
|
395
|
-
const len = dv().getInt32(off - 4, true)
|
|
431
|
+
if (a & LAYOUT.SSO_BIT) return decodeSSO(p)
|
|
432
|
+
const len = m.getInt32(off - 4, true)
|
|
396
433
|
return TEXT_DEC.decode(new Uint8Array(mem.buffer, off, len))
|
|
397
434
|
}
|
|
398
435
|
if (t === 6) { // OBJECT
|
|
399
|
-
const
|
|
436
|
+
const keys = mem.schemas[a]
|
|
400
437
|
if (!keys) return p
|
|
401
438
|
const obj = {}
|
|
402
|
-
for (let i = 0; i < keys.length; i++) obj[keys[i]] = mem.read(m.
|
|
439
|
+
for (let i = 0; i < keys.length; i++) obj[keys[i]] = mem.read(m.getBigInt64(off + i * 8, true))
|
|
403
440
|
return obj
|
|
404
441
|
}
|
|
405
442
|
if (t === 7) { // HASH
|
|
406
|
-
const
|
|
407
|
-
const obj = {}
|
|
443
|
+
const size = m.getInt32(off - 8, true), cap = m.getInt32(off - 4, true), obj = {}
|
|
408
444
|
for (let i = 0, found = 0; i < cap && found < size; i++) {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const key = mem.read(m.getFloat64(off + i * 24 + 8, true))
|
|
412
|
-
obj[key] = mem.read(m.getFloat64(off + i * 24 + 16, true))
|
|
445
|
+
if (m.getBigInt64(off + i * 24, true) !== 0n) {
|
|
446
|
+
obj[mem.read(m.getBigInt64(off + i * 24 + 8, true))] = mem.read(m.getBigInt64(off + i * 24 + 16, true))
|
|
413
447
|
found++
|
|
414
448
|
}
|
|
415
449
|
}
|
|
416
450
|
return obj
|
|
417
451
|
}
|
|
418
452
|
if (t === 8) { // SET
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const hash = m.getFloat64(off + i * 16, true)
|
|
423
|
-
if (hash !== 0) set.add(mem.read(m.getFloat64(off + i * 16 + 8, true)))
|
|
424
|
-
}
|
|
453
|
+
const size = m.getInt32(off - 8, true), cap = m.getInt32(off - 4, true), set = new Set()
|
|
454
|
+
for (let i = 0; i < cap && set.size < size; i++)
|
|
455
|
+
if (m.getBigInt64(off + i * 16, true) !== 0n) set.add(mem.read(m.getBigInt64(off + i * 16 + 8, true)))
|
|
425
456
|
return set
|
|
426
457
|
}
|
|
427
458
|
if (t === 9) { // MAP
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if (hash !== 0) map.set(mem.read(m.getFloat64(off + i * 24 + 8, true)), mem.read(m.getFloat64(off + i * 24 + 16, true)))
|
|
433
|
-
}
|
|
459
|
+
const size = m.getInt32(off - 8, true), cap = m.getInt32(off - 4, true), map = new Map()
|
|
460
|
+
for (let i = 0; i < cap && map.size < size; i++)
|
|
461
|
+
if (m.getBigInt64(off + i * 24, true) !== 0n)
|
|
462
|
+
map.set(mem.read(m.getBigInt64(off + i * 24 + 8, true)), mem.read(m.getBigInt64(off + i * 24 + 16, true)))
|
|
434
463
|
return map
|
|
435
464
|
}
|
|
436
|
-
|
|
437
|
-
return p
|
|
465
|
+
return i64ToF64(p) // canonical NaN-number / CLOSURE / unknown — reinterpret to f64
|
|
438
466
|
}
|
|
439
467
|
|
|
440
468
|
mem.write = function(p, data) {
|
|
@@ -443,7 +471,7 @@ export const memory = (src) => {
|
|
|
443
471
|
const cap = m.getInt32(off - 4, true)
|
|
444
472
|
if (data.length > cap) throw Error(`write: ${data.length} exceeds capacity ${cap}`)
|
|
445
473
|
m.setInt32(off - 8, data.length, true)
|
|
446
|
-
for (let i = 0; i < data.length; i++) m.
|
|
474
|
+
for (let i = 0; i < data.length; i++) m.setBigInt64(off + i * 8, bits(coerce(data[i])), true)
|
|
447
475
|
} else if (t === 3) {
|
|
448
476
|
const a2 = aux(p), elem = a2 & 7
|
|
449
477
|
const [, stride, , setter] = ELEM_BY_ID[elem]
|
|
@@ -463,7 +491,7 @@ export const memory = (src) => {
|
|
|
463
491
|
if (!schema) throw Error(`write: unknown schema`)
|
|
464
492
|
for (const k of Object.keys(data)) {
|
|
465
493
|
const i = schema.indexOf(k)
|
|
466
|
-
if (i >= 0) m.
|
|
494
|
+
if (i >= 0) m.setBigInt64(off + i * 8, bits(coerce(data[k])), true)
|
|
467
495
|
}
|
|
468
496
|
} else {
|
|
469
497
|
throw Error(`write: unsupported type ${t}`)
|
|
@@ -471,7 +499,14 @@ export const memory = (src) => {
|
|
|
471
499
|
}
|
|
472
500
|
|
|
473
501
|
mem.alloc = alloc
|
|
474
|
-
|
|
502
|
+
// Rewind to the JS-captured post-init heap mark, NOT the wasm `_clear` (which
|
|
503
|
+
// rewinds to the static-data end and would clobber module-global heap values —
|
|
504
|
+
// a top-level `let o = {…}` — on the first alloc after reset). `jsReset`'s base
|
|
505
|
+
// is `$__heap` read after instantiation (start ran), i.e. exactly the high-water
|
|
506
|
+
// mark above all module-init allocations. Both share `$__heap`, so a wasm `_alloc`
|
|
507
|
+
// and this reset stay consistent. (Shared memory has no `$__heap` global → base
|
|
508
|
+
// is the fixed start, preserving prior behavior.)
|
|
509
|
+
mem.reset = jsReset
|
|
475
510
|
|
|
476
511
|
// TypedArray constructors: memory.Float64Array(data), etc.
|
|
477
512
|
// Bulk-copy path: when input is a TypedArray whose element type matches
|
|
@@ -481,7 +516,9 @@ export const memory = (src) => {
|
|
|
481
516
|
for (const [name, [elemId, stride, , setter]] of Object.entries(ELEMS)) {
|
|
482
517
|
mem[name] = (data) => {
|
|
483
518
|
const n = data.length, bytes = n * stride, off = hdr(bytes, bytes, bytes)
|
|
484
|
-
|
|
519
|
+
// Same-type source → native memcpy via `.set` (incl. stride-1 Uint8Array:
|
|
520
|
+
// a multi-MB file copied byte-by-byte through DataView dominates decode).
|
|
521
|
+
if (TA[elemId] && data instanceof TA[elemId]) {
|
|
485
522
|
new TA[elemId](mem.buffer, off, n).set(data)
|
|
486
523
|
} else {
|
|
487
524
|
const m = dv()
|
|
@@ -491,6 +528,24 @@ export const memory = (src) => {
|
|
|
491
528
|
}
|
|
492
529
|
}
|
|
493
530
|
|
|
531
|
+
// Zero-copy input: reserve a typed-array region in wasm memory and return BOTH
|
|
532
|
+
// a live `view` over it and the NaN-box `box` pointer to pass as an argument.
|
|
533
|
+
// The caller fills `view` directly (one I/O-side copy, no second JS→wasm copy)
|
|
534
|
+
// and hands `box` to the export, which reads the bytes in place. Decoded typed
|
|
535
|
+
// arrays already come back as views (mem.read), so a decode can be copy-free
|
|
536
|
+
// end-to-end. LIFETIME: `view` is detached by any mem.grow() (alloc past the
|
|
537
|
+
// current buffer) and clobbered by mem.reset()/the next decode — re-derive a
|
|
538
|
+
// fresh view with mem.read(box) after growth, or copy out what must persist.
|
|
539
|
+
// Back the module with a shared memory (WebAssembly.Memory{shared:true}) to
|
|
540
|
+
// keep views valid across grow and to hand them to a worker/AudioWorklet.
|
|
541
|
+
mem.allocTyped = (Ctor, n) => {
|
|
542
|
+
const meta = ELEMS[Ctor?.name]
|
|
543
|
+
if (!meta) throw Error(`allocTyped: unsupported type ${Ctor?.name ?? Ctor}`)
|
|
544
|
+
const [elemId, stride] = meta
|
|
545
|
+
const bytes = n * stride, off = hdr(bytes, bytes, bytes)
|
|
546
|
+
return { view: new Ctor(mem.buffer, off, n), box: ptr(3, elemId, off) }
|
|
547
|
+
}
|
|
548
|
+
|
|
494
549
|
_enhanced.add(mem)
|
|
495
550
|
return mem
|
|
496
551
|
}
|
|
@@ -529,14 +584,24 @@ export const wrap = (memSrc, inst) => {
|
|
|
529
584
|
}
|
|
530
585
|
} catch { /* ignore */ }
|
|
531
586
|
}
|
|
587
|
+
// i64-carrier map: per export, which param positions ride i64 (BigInt) and whether the
|
|
588
|
+
// result does. The boxed (NaN-box) carrier crosses as i64 so JSC can't canonicalize the
|
|
589
|
+
// payload; we reinterpret BigInt↔f64 by bits at exactly those positions. A bigint result
|
|
590
|
+
// has no entry — its BigInt already IS the value. (Mirror of the test/data.js adapter.)
|
|
591
|
+
const i64Exp = new Map()
|
|
592
|
+
const i64Bytes = customSection(mod, 'jz:i64exp')
|
|
593
|
+
if (i64Bytes) {
|
|
594
|
+
try { for (const e of JSON.parse(td.decode(i64Bytes))) i64Exp.set(e.name, { p: new Set(e.p || []), r: !!e.r }) }
|
|
595
|
+
catch { /* ignore */ }
|
|
596
|
+
}
|
|
532
597
|
const mem = memory(memSrc)
|
|
533
598
|
const lastErrBits = realInst.exports.__jz_last_err_bits
|
|
534
599
|
const decodeThrown = error => {
|
|
535
600
|
if (!(error instanceof WebAssembly.Exception) || !lastErrBits) throw error
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const value = mem ? mem.read(
|
|
601
|
+
const errBits = lastErrBits.value // i64 bits (BigInt)
|
|
602
|
+
// Memoryless module: the thrown value is a number/atom/SSO string — decode it
|
|
603
|
+
// from bits. (A heap Error/string can only exist when the module has memory.)
|
|
604
|
+
const value = mem ? mem.read(errBits) : decode(errBits)
|
|
540
605
|
if (value instanceof Error) throw value
|
|
541
606
|
const wrapped = new Error(typeof value === 'string' ? value : String(value))
|
|
542
607
|
wrapped.cause = error
|
|
@@ -548,21 +613,44 @@ export const wrap = (memSrc, inst) => {
|
|
|
548
613
|
// value straight through — `mem.wrapVal` would NaN-box it — substituting a
|
|
549
614
|
// jsstring literal default for a missing arg. Every other slot marshals via
|
|
550
615
|
// `box`: `coerce` for pure-scalar modules, `mem.wrapVal` for heap modules.
|
|
551
|
-
// Quiet-NaN ABI: every export value is f64, so there is no per-position i64
|
|
552
|
-
// carrier — args flow straight to the wasm call, results straight to decode/read.
|
|
553
616
|
const wrapArgAt = (ext, i, x, box) =>
|
|
554
617
|
ext?.has(i) ? (x === undefined && ext.def?.has(i) ? ext.def.get(i) : x) : box(x)
|
|
618
|
+
// Per-position arg marshaller: box the value, then for an i64-carrier param (per
|
|
619
|
+
// jz:i64exp) pass its i64 bits (a boxed value is already a BigInt; a numeric arg to a
|
|
620
|
+
// dynamic i64 param → its f64 bits). The box never materializes as f64, so JSC can't
|
|
621
|
+
// canonicalize it. Numeric/externref positions keep their f64/externref carrier.
|
|
622
|
+
const i64Arg = (ie, ext, box) => (x, i) => {
|
|
623
|
+
const w = wrapArgAt(ext, i, x, box)
|
|
624
|
+
if (ie && ie.p.has(i)) {
|
|
625
|
+
// i64-carrier slot: a string that coerce() left raw (scalar/memoryless module)
|
|
626
|
+
// must be NaN-box encoded. SSO handles ≤6 ASCII chars without heap memory; longer
|
|
627
|
+
// or non-ASCII strings need a heap that this module lacks — throw clearly.
|
|
628
|
+
if (typeof w === 'string') {
|
|
629
|
+
if (w.length > 6 || !/^[\x00-\x7f]*$/.test(w))
|
|
630
|
+
throw new Error('jz: string arg too long or non-ASCII for memoryless module — compile with a string operation to enable heap marshaling')
|
|
631
|
+
return encodeSSO(w)
|
|
632
|
+
}
|
|
633
|
+
return bits(w) // i64 param: pass the box bits
|
|
634
|
+
}
|
|
635
|
+
// f64 position: a box (BigInt) must reinterpret to f64. Happens for an un-wrapped export
|
|
636
|
+
// (e.g. a multi-value result skips wrapping) whose boxed param keeps the legacy f64 carrier
|
|
637
|
+
// — intact on V8; that path is inherently JSC-limited for boxed lanes anyway.
|
|
638
|
+
return typeof w === 'bigint' ? i64ToF64(w) : w
|
|
639
|
+
}
|
|
555
640
|
|
|
556
641
|
// Pure scalar module (no memory): pass f64 values directly, no marshaling
|
|
557
|
-
if (!mem) {
|
|
642
|
+
if (!mem || mem.scalar) {
|
|
558
643
|
for (const [name, fn] of Object.entries(realInst.exports)) {
|
|
559
644
|
if (typeof fn !== 'function') { exports[name] = fn; continue }
|
|
560
645
|
const ext = extExp.get(name)
|
|
646
|
+
const ie = i64Exp.get(name)
|
|
561
647
|
const len = fn.length
|
|
562
648
|
exports[name] = (...args) => {
|
|
563
649
|
while (args.length < len) args.push(undefined)
|
|
564
650
|
try {
|
|
565
|
-
|
|
651
|
+
const ret = fn(...args.map(i64Arg(ie, ext, coerce)))
|
|
652
|
+
// A bigint-value result returns raw; a boxed i64 result or an f64/number result decodes.
|
|
653
|
+
return typeof ret === 'bigint' && !(ie && ie.r) ? ret : decode(ret)
|
|
566
654
|
} catch (e) { decodeThrown(e) }
|
|
567
655
|
}
|
|
568
656
|
}
|
|
@@ -573,23 +661,28 @@ export const wrap = (memSrc, inst) => {
|
|
|
573
661
|
if (restFuncs.has(name) && typeof fn === 'function') {
|
|
574
662
|
const fixed = restFuncs.get(name)
|
|
575
663
|
const ext = extExp.get(name)
|
|
664
|
+
const ie = i64Exp.get(name)
|
|
576
665
|
exports[name] = (...args) => {
|
|
577
|
-
const a = args.slice(0, fixed).map((
|
|
578
|
-
while (a.length < fixed) a.push(UNDEF_NAN)
|
|
579
|
-
|
|
666
|
+
const a = args.slice(0, fixed).map(i64Arg(ie, ext, memWrapVal))
|
|
667
|
+
while (a.length < fixed) { const i = a.length; a.push(ie && ie.p.has(i) ? UNDEF_NAN : i64ToF64(UNDEF_NAN)) }
|
|
668
|
+
const restArr = mem.Array(args.slice(fixed)) // BigInt box (i64 carrier)
|
|
669
|
+
a.push(ie && ie.p.has(fixed) ? restArr : i64ToF64(restArr))
|
|
580
670
|
try {
|
|
581
|
-
|
|
671
|
+
const ret = fn.apply(null, a)
|
|
672
|
+
return typeof ret === 'bigint' && !(ie && ie.r) ? ret : mem.read(ret)
|
|
582
673
|
} catch (error) {
|
|
583
674
|
decodeThrown(error)
|
|
584
675
|
}
|
|
585
676
|
}
|
|
586
677
|
} else if (typeof fn === 'function') {
|
|
587
678
|
const ext = extExp.get(name)
|
|
679
|
+
const ie = i64Exp.get(name)
|
|
588
680
|
const len = fn.length
|
|
589
681
|
exports[name] = (...args) => {
|
|
590
682
|
while (args.length < len) args.push(undefined)
|
|
591
683
|
try {
|
|
592
|
-
|
|
684
|
+
const ret = fn.apply(null, args.map(i64Arg(ie, ext, memWrapVal)))
|
|
685
|
+
return typeof ret === 'bigint' && !(ie && ie.r) ? ret : mem.read(ret)
|
|
593
686
|
} catch (error) {
|
|
594
687
|
decodeThrown(error)
|
|
595
688
|
}
|
|
@@ -609,23 +702,22 @@ const prepareInterop = (opts) => {
|
|
|
609
702
|
// wrapped back to BigInt so the wasm side reinterprets a non-canonicalized
|
|
610
703
|
// bit pattern.
|
|
611
704
|
opts._interp.__ext_prop = (objBig, propBig) => {
|
|
612
|
-
const
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
return f64ToI64(state.mem.wrapVal(typeof obj[prop] === 'function' ? obj[prop].bind(obj) : obj[prop]))
|
|
705
|
+
const obj = state.extMap[offset(objBig)]
|
|
706
|
+
const prop = state.mem.read(propBig)
|
|
707
|
+
return bits(state.mem.wrapVal(typeof obj[prop] === 'function' ? obj[prop].bind(obj) : obj[prop]))
|
|
616
708
|
}
|
|
617
709
|
opts._interp.__ext_has = (objBig, propBig) => {
|
|
618
|
-
return (state.mem.read(
|
|
710
|
+
return (state.mem.read(propBig) in state.extMap[offset(objBig)]) ? 1 : 0
|
|
619
711
|
}
|
|
620
712
|
opts._interp.__ext_set = (objBig, propBig, valBig) => {
|
|
621
|
-
state.extMap[offset(
|
|
713
|
+
state.extMap[offset(objBig)][state.mem.read(propBig)] = state.mem.read(valBig)
|
|
622
714
|
return 1
|
|
623
715
|
}
|
|
624
716
|
opts._interp.__ext_call = (objBig, propBig, argsBig) => {
|
|
625
|
-
const obj = state.extMap[offset(
|
|
626
|
-
const prop = state.mem.read(
|
|
627
|
-
const args = state.mem.read(
|
|
628
|
-
return
|
|
717
|
+
const obj = state.extMap[offset(objBig)]
|
|
718
|
+
const prop = state.mem.read(propBig)
|
|
719
|
+
const args = state.mem.read(argsBig)
|
|
720
|
+
return bits(state.mem.wrapVal(obj[prop].apply(obj, args)))
|
|
629
721
|
}
|
|
630
722
|
return state
|
|
631
723
|
}
|
|
@@ -649,7 +741,7 @@ const installDefaultEnvImports = (mod, imports, state) => {
|
|
|
649
741
|
// across the wasm→JS boundary (see module/console.js header). Reinterpret
|
|
650
742
|
// the BigInt's bits as f64 here so mem.read sees the original NaN-box.
|
|
651
743
|
const write = (valBig, fd, sep) => {
|
|
652
|
-
const v = state
|
|
744
|
+
const v = readArgBits(state, valBig)
|
|
653
745
|
buf[fd] += String(v)
|
|
654
746
|
if (sep === 32) buf[fd] += ' '
|
|
655
747
|
else if (sep === 10) flush(fd)
|
|
@@ -679,13 +771,13 @@ const installDefaultEnvImports = (mod, imports, state) => {
|
|
|
679
771
|
}
|
|
680
772
|
if (envFns.has('parseFloat') && !imports.env.parseFloat) {
|
|
681
773
|
imports.env.parseFloat = (valBig) => {
|
|
682
|
-
const s = state
|
|
774
|
+
const s = readArgBits(state, valBig)
|
|
683
775
|
return parseFloat(s)
|
|
684
776
|
}
|
|
685
777
|
}
|
|
686
778
|
if (envFns.has('parseInt') && !imports.env.parseInt) {
|
|
687
779
|
imports.env.parseInt = (valBig, radix) => {
|
|
688
|
-
const s = state
|
|
780
|
+
const s = readArgBits(state, valBig)
|
|
689
781
|
return parseInt(s, radix || undefined)
|
|
690
782
|
}
|
|
691
783
|
}
|
|
@@ -782,14 +874,11 @@ const buildImports = (mod, opts, state) => {
|
|
|
782
874
|
const fn = typeof spec === 'function' ? spec : (spec && typeof spec === 'object' ? spec.fn : null)
|
|
783
875
|
if (typeof fn === 'function')
|
|
784
876
|
imports[modName][name] = (...args) => {
|
|
785
|
-
// i64 carrier:
|
|
786
|
-
//
|
|
787
|
-
const decoded = args.map(a =>
|
|
788
|
-
const f = typeof a === 'bigint' ? i64ToF64(a) : a
|
|
789
|
-
return state.mem ? state.mem.read(f) : decode(f)
|
|
790
|
-
})
|
|
877
|
+
// i64 carrier: args arrive as BigInt bits (box) or number; decode with integer
|
|
878
|
+
// ops — never materialize a box as f64. Return the i64 bits of the wrapped result.
|
|
879
|
+
const decoded = args.map(a => state.mem ? state.mem.read(a) : decode(a))
|
|
791
880
|
const ret = fn.call(fns, ...decoded)
|
|
792
|
-
return
|
|
881
|
+
return bits(state.mem ? state.mem.wrapVal(ret) : coerce(ret))
|
|
793
882
|
}
|
|
794
883
|
}
|
|
795
884
|
}
|
|
@@ -812,7 +901,7 @@ const buildImports = (mod, opts, state) => {
|
|
|
812
901
|
if (host !== undefined) {
|
|
813
902
|
if (!imports.env) imports.env = {}
|
|
814
903
|
let id = state.extMap.indexOf(host); if (id === -1) { id = state.extMap.length; state.extMap.push(host) }
|
|
815
|
-
imports.env[imp.name] = new WebAssembly.Global({ value: 'i64', mutable: false },
|
|
904
|
+
imports.env[imp.name] = new WebAssembly.Global({ value: 'i64', mutable: false }, ptr(11, 0, id))
|
|
816
905
|
}
|
|
817
906
|
}
|
|
818
907
|
}
|
|
@@ -834,7 +923,11 @@ const finishInstantiation = (mod, inst, imports, needsWasi, opts, state) => {
|
|
|
834
923
|
const enhanced = memory(memSrc)
|
|
835
924
|
state.mem = enhanced
|
|
836
925
|
state.flushPrint?.()
|
|
837
|
-
|
|
926
|
+
// A memoryless module keeps a minimal reader internally (state.mem, for decoding
|
|
927
|
+
// its SSO/atom boundary values), but the result's `.memory` stays null — the
|
|
928
|
+
// module genuinely exposes no linear memory. `jz.memory(result)` still hands back
|
|
929
|
+
// a usable reader on demand.
|
|
930
|
+
return { exports: wrap(memSrc), memory: enhanced?.scalar ? null : enhanced, instance: inst, module: mod }
|
|
838
931
|
}
|
|
839
932
|
|
|
840
933
|
/**
|
|
@@ -849,23 +942,29 @@ const finishInstantiation = (mod, inst, imports, needsWasi, opts, state) => {
|
|
|
849
942
|
* @param {object} [opts] host options: imports, memory, _interp, host-shape flags
|
|
850
943
|
* @returns {{ exports, memory, instance, module }}
|
|
851
944
|
*/
|
|
945
|
+
/**
|
|
946
|
+
* Compile wasm bytes to a `WebAssembly.Module`, preferring native
|
|
947
|
+
* `wasm:js-string` builtins when the engine honors the option (V8 17+/Safari
|
|
948
|
+
* 18.4+; older engines throw or ignore it — try-fallback handles both). A
|
|
949
|
+
* `WebAssembly.Module` passed in is returned as-is. Factor it out so callers
|
|
950
|
+
* can compile once and instantiate many times without re-validating the bytes
|
|
951
|
+
* (`instantiate(toModule(wasm))` skips the per-call compile on hot loops).
|
|
952
|
+
*
|
|
953
|
+
* @param {Uint8Array|ArrayBuffer|WebAssembly.Module} wasm
|
|
954
|
+
* @returns {WebAssembly.Module}
|
|
955
|
+
*/
|
|
956
|
+
export const toModule = (wasm) => {
|
|
957
|
+
if (wasm instanceof WebAssembly.Module) return wasm
|
|
958
|
+
if (jssProbeNative()) {
|
|
959
|
+
try { return new WebAssembly.Module(wasm, { builtins: ['js-string'] }) }
|
|
960
|
+
catch { return new WebAssembly.Module(wasm) }
|
|
961
|
+
}
|
|
962
|
+
return new WebAssembly.Module(wasm)
|
|
963
|
+
}
|
|
964
|
+
|
|
852
965
|
export const instantiate = (wasm, opts = {}) => {
|
|
853
966
|
const state = prepareInterop(opts)
|
|
854
|
-
|
|
855
|
-
// The option is silently accepted by V8 17+/Safari 18.4+; older engines that
|
|
856
|
-
// don't recognize it either throw or ignore it — try-fallback handles both.
|
|
857
|
-
let mod
|
|
858
|
-
if (wasm instanceof WebAssembly.Module) {
|
|
859
|
-
mod = wasm
|
|
860
|
-
} else if (jssProbeNative()) {
|
|
861
|
-
try {
|
|
862
|
-
mod = new WebAssembly.Module(wasm, { builtins: ['js-string'] })
|
|
863
|
-
} catch {
|
|
864
|
-
mod = new WebAssembly.Module(wasm)
|
|
865
|
-
}
|
|
866
|
-
} else {
|
|
867
|
-
mod = new WebAssembly.Module(wasm)
|
|
868
|
-
}
|
|
967
|
+
const mod = toModule(wasm)
|
|
869
968
|
const { imports, needsWasi } = buildImports(mod, opts, state)
|
|
870
969
|
const hasImports = Object.keys(imports).some(k => k !== '_setMemory')
|
|
871
970
|
const inst = new WebAssembly.Instance(mod, hasImports ? imports : undefined)
|