jz 0.4.0 → 0.5.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 +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 +251 -118
- package/module/date.js +9 -5
- package/module/function.js +11 -1
- package/module/json.js +361 -55
- package/module/math.js +551 -128
- 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 +540 -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 +662 -196
- package/src/infer.js +548 -0
- package/src/ir.js +291 -23
- package/src/jzify.js +786 -94
- package/src/narrow.js +433 -251
- package/src/optimize.js +126 -122
- package/src/plan.js +703 -34
- package/src/prepare.js +822 -150
- 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
|
@@ -1,22 +1,118 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* jz/interop — host-side boundary codec.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Importable as `jz/interop` without pulling the compiler, parser, or watr —
|
|
5
|
+
* use this to run prebuilt jz wasm from a host that doesn't need to compile.
|
|
6
|
+
* Sole external dependency: `./wasi.js`.
|
|
7
|
+
*
|
|
8
|
+
* Marshals NaN-boxed `f64` values across the boundary: bump-allocated heap
|
|
9
|
+
* blobs (strings, arrays, typed arrays, objects), schema transport for
|
|
10
|
+
* fixed-shape objects, host-object externrefs.
|
|
8
11
|
*
|
|
9
12
|
* Exports:
|
|
10
13
|
* UNDEF_NAN, NULL_NAN, coerce — null/undefined sentinels
|
|
14
|
+
* i64ToF64, f64ToI64 — bit-cast across the i64 boundary
|
|
11
15
|
* ptr / offset / type / aux — NaN-boxed pointer codec
|
|
12
|
-
* memory(src) — enhance a WebAssembly.Memory
|
|
13
|
-
* wrap(memSrc, inst?) — adapt
|
|
14
|
-
* instantiate(
|
|
16
|
+
* memory(src) — enhance a WebAssembly.Memory with read/write/String/Array/…
|
|
17
|
+
* wrap(memSrc, inst?) — adapt raw wasm exports to JS calling convention
|
|
18
|
+
* instantiate(wasm, opts?) — instantiate prebuilt wasm bytes + wrap
|
|
19
|
+
*
|
|
20
|
+
* One boundary codec per binary: a jz wasm picks its host shape at compile
|
|
21
|
+
* time (`opts.host`). There is no runtime "driver sniff" — the host loading
|
|
22
|
+
* the binary knows which variant it asked for.
|
|
15
23
|
*
|
|
16
|
-
* @module
|
|
24
|
+
* @module jz/interop
|
|
17
25
|
*/
|
|
18
26
|
|
|
19
|
-
import { wasi } from '
|
|
27
|
+
import { wasi } from './wasi.js'
|
|
28
|
+
|
|
29
|
+
// ── WASI linking ────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const linkWasi = (mod, opts) => {
|
|
32
|
+
const needsWasi = WebAssembly.Module.imports(mod).some(i => i.module === 'wasi_snapshot_preview1')
|
|
33
|
+
return { needsWasi, wasiImports: needsWasi ? wasi(opts) : null }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const envFuncNames = (mod) =>
|
|
37
|
+
new Set(WebAssembly.Module.imports(mod)
|
|
38
|
+
.filter(i => i.module === 'env' && i.kind === 'function').map(i => i.name))
|
|
39
|
+
|
|
40
|
+
// ── Allocator wiring ────────────────────────────────────────────────────────
|
|
41
|
+
// Heap pointer: the exported `$__heap` global when the module has one (non-shared
|
|
42
|
+
// memory), else memory[1020] (shared memory — globals are per-instance, so
|
|
43
|
+
// threads must share a pointer cell in linear memory). 8-byte aligned bump on
|
|
44
|
+
// the JS side; wasm `_alloc` takes over if exported.
|
|
45
|
+
|
|
46
|
+
const HEAP_PTR_ADDR = 1020
|
|
47
|
+
const HEAP_START = 1024
|
|
48
|
+
|
|
49
|
+
const makeJsAllocator = (mem, heapGlobal) => {
|
|
50
|
+
const dv = () => new DataView(mem.buffer)
|
|
51
|
+
const getPtr = heapGlobal ? () => heapGlobal.value : () => dv().getInt32(HEAP_PTR_ADDR, true)
|
|
52
|
+
const setPtr = heapGlobal ? v => { heapGlobal.value = v } : v => dv().setInt32(HEAP_PTR_ADDR, v, true)
|
|
53
|
+
// Rewind target: the global's post-static-init value, else the fixed start.
|
|
54
|
+
const base = heapGlobal ? heapGlobal.value : HEAP_START
|
|
55
|
+
const alloc = (bytes) => {
|
|
56
|
+
const aligned = (getPtr() + 7) & ~7
|
|
57
|
+
const next = aligned + bytes
|
|
58
|
+
if (next > mem.buffer.byteLength)
|
|
59
|
+
mem.grow(Math.ceil((next - mem.buffer.byteLength) / 65536))
|
|
60
|
+
setPtr(next)
|
|
61
|
+
return aligned
|
|
62
|
+
}
|
|
63
|
+
const reset = () => setPtr(base)
|
|
64
|
+
// The global is initialized by wasm at module load; only the memory cell needs
|
|
65
|
+
// a JS-side nudge in case it underflows the heap start.
|
|
66
|
+
const initHeapPtr = () => {
|
|
67
|
+
if (heapGlobal) return
|
|
68
|
+
const d = dv()
|
|
69
|
+
if (d.getInt32(HEAP_PTR_ADDR, true) < HEAP_START) d.setInt32(HEAP_PTR_ADDR, HEAP_START, true)
|
|
70
|
+
}
|
|
71
|
+
return { alloc, reset, initHeapPtr }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Custom-section reading ──────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
const customSection = (mod, name) => {
|
|
77
|
+
const secs = WebAssembly.Module.customSections(mod, name)
|
|
78
|
+
return secs.length ? new Uint8Array(secs[0]) : null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const sectionReader = (bytes) => {
|
|
82
|
+
const td = new TextDecoder()
|
|
83
|
+
let i = 0
|
|
84
|
+
return {
|
|
85
|
+
pos: () => i,
|
|
86
|
+
seek: (p) => { i = p },
|
|
87
|
+
eof: () => i >= bytes.length,
|
|
88
|
+
u8: () => bytes[i++],
|
|
89
|
+
varint: () => {
|
|
90
|
+
let r = 0, s = 0
|
|
91
|
+
// eslint-disable-next-line no-constant-condition
|
|
92
|
+
while (true) {
|
|
93
|
+
const x = bytes[i++]
|
|
94
|
+
r |= (x & 0x7F) << s
|
|
95
|
+
if (!(x & 0x80)) return r
|
|
96
|
+
s += 7
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
str: (n) => { const s = td.decode(bytes.subarray(i, i + n)); i += n; return s },
|
|
100
|
+
bytes: (n) => { const r = bytes.subarray(i, i + n); i += n; return r },
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const attachTimers = (inst) => {
|
|
105
|
+
if (!inst.exports.__timer_tick) return
|
|
106
|
+
const tick = inst.exports.__timer_tick
|
|
107
|
+
let hadTimers = false
|
|
108
|
+
const id = setInterval(() => {
|
|
109
|
+
const remaining = tick()
|
|
110
|
+
if (remaining > 0) hadTimers = true
|
|
111
|
+
if (hadTimers && remaining <= 0) clearInterval(id)
|
|
112
|
+
}, 1)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── NaN-box codec ───────────────────────────────────────────────────────────
|
|
20
116
|
|
|
21
117
|
// NaN-boxing encode/decode — shared 8-byte scratch buffer
|
|
22
118
|
const _buf = new ArrayBuffer(8), _u32 = new Uint32Array(_buf), _f64 = new Float64Array(_buf)
|
|
@@ -37,6 +133,11 @@ export const f64ToI64 = _bi64.f64ToI64
|
|
|
37
133
|
// Distinct from 0, JS NaN (payload=0), and all pointers.
|
|
38
134
|
_u32[1] = 0x7FF80001; _u32[0] = 0; export const NULL_NAN = _f64[0]
|
|
39
135
|
_u32[1] = 0x7FF80002; _u32[0] = 0; export const UNDEF_NAN = _f64[0]
|
|
136
|
+
// Boxed-boolean atoms (type=0, offset=0): aux=4 → false, aux=5 → true. The
|
|
137
|
+
// low aux bit is the truth value; only exported scalars cross as these — the
|
|
138
|
+
// inner 0/1 number carrier never escapes the wasm boundary.
|
|
139
|
+
_u32[1] = 0x7FF80004; _u32[0] = 0; export const FALSE_NAN = _f64[0]
|
|
140
|
+
_u32[1] = 0x7FF80005; _u32[0] = 0; export const TRUE_NAN = _f64[0]
|
|
40
141
|
|
|
41
142
|
// Coerce JS null/undefined → NaN-boxed sentinels for WASM boundary
|
|
42
143
|
export const coerce = v => v === null ? NULL_NAN : v === undefined ? UNDEF_NAN : v
|
|
@@ -48,6 +149,8 @@ const decode = v => {
|
|
|
48
149
|
if (_u32[0] !== 0) return v
|
|
49
150
|
if (_u32[1] === 0x7FF80001) return null
|
|
50
151
|
if (_u32[1] === 0x7FF80002) return undefined
|
|
152
|
+
if (_u32[1] === 0x7FF80004) return false
|
|
153
|
+
if (_u32[1] === 0x7FF80005) return true
|
|
51
154
|
return v
|
|
52
155
|
}
|
|
53
156
|
|
|
@@ -111,58 +214,44 @@ export const memory = (src) => {
|
|
|
111
214
|
|
|
112
215
|
const dv = () => new DataView(mem.buffer)
|
|
113
216
|
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const aligned = (p + 7) & ~7 // 8-byte align
|
|
118
|
-
const next = aligned + bytes
|
|
119
|
-
if (next > mem.buffer.byteLength) {
|
|
120
|
-
mem.grow(Math.ceil((next - mem.buffer.byteLength) / 65536))
|
|
121
|
-
d = dv() // buffer was detached by grow
|
|
122
|
-
}
|
|
123
|
-
d.setInt32(1020, next, true)
|
|
124
|
-
return aligned
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Use WASM allocator if available, else JS-side bump
|
|
217
|
+
// Allocator scaffold: bumps the exported `$__heap` global (or memory[1020] for
|
|
218
|
+
// shared memory). Wasm `_alloc` takes over when exported; `_clear`/jsReset rewinds.
|
|
219
|
+
const { alloc: jsAlloc, reset: jsReset, initHeapPtr } = makeJsAllocator(mem, wasmExports?.__heap)
|
|
128
220
|
let alloc = wasmExports?._alloc || jsAlloc
|
|
129
|
-
|
|
130
|
-
// _clear is present (otherwise the WASM global / shared slot is authoritative).
|
|
131
|
-
const jsReset = () => dv().setInt32(1020, 1024, true)
|
|
132
|
-
|
|
133
|
-
// Initialize heap pointer if not yet set
|
|
134
|
-
const initDv = dv()
|
|
135
|
-
if (initDv.getInt32(1020, true) < 1024) initDv.setInt32(1020, 1024, true)
|
|
221
|
+
initHeapPtr()
|
|
136
222
|
|
|
137
|
-
// Write header
|
|
223
|
+
// Write 16-byte header matching WASM `__alloc_hdr`:
|
|
224
|
+
// [propsPtr@+0(i64=0), len@+8, cap@+12], return data offset (raw+16).
|
|
225
|
+
// Read paths (ARRAY at off-8/-4, BUFFER at off-8) and the propsPtr slot at
|
|
226
|
+
// off-16 then work uniformly on JS- and WASM-allocated values.
|
|
138
227
|
const hdr = (len, cap, bytes) => {
|
|
139
|
-
const raw = alloc(
|
|
228
|
+
const raw = alloc(16 + bytes)
|
|
140
229
|
const m = dv()
|
|
141
|
-
m.
|
|
142
|
-
m.setInt32(raw +
|
|
143
|
-
|
|
230
|
+
m.setBigInt64(raw, 0n, true)
|
|
231
|
+
m.setInt32(raw + 8, len, true)
|
|
232
|
+
m.setInt32(raw + 12, cap, true)
|
|
233
|
+
return raw + 16
|
|
144
234
|
}
|
|
145
235
|
|
|
146
|
-
// Read schemas from module custom section, merge into memory.schemas
|
|
236
|
+
// Read schemas from module custom section, merge into memory.schemas. Schema
|
|
237
|
+
// entries are { type, payload } where type=0 means null (computed/missing
|
|
238
|
+
// key), type=1 means nested [null, name] (synthetic shape), else a UTF-8
|
|
239
|
+
// length-prefixed property name. Section format is varint-prefixed list.
|
|
147
240
|
let schemas = mem.schemas || []
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
for (const s of newSchemas) {
|
|
163
|
-
const key = s.join(',')
|
|
164
|
-
if (!schemas.some(existing => existing.join(',') === key)) schemas.push(s)
|
|
165
|
-
}
|
|
241
|
+
const schemaBytes = mod && customSection(mod, 'jz:schema')
|
|
242
|
+
if (schemaBytes) {
|
|
243
|
+
const r = sectionReader(schemaBytes)
|
|
244
|
+
const dec = () => {
|
|
245
|
+
const t = r.u8()
|
|
246
|
+
if (t === 0) return null
|
|
247
|
+
if (t === 1) return [null, dec()]
|
|
248
|
+
return r.str(r.varint())
|
|
249
|
+
}
|
|
250
|
+
const nS = r.varint(), newSchemas = []
|
|
251
|
+
for (let j = 0; j < nS; j++) { const k = r.varint(), props = []; for (let p = 0; p < k; p++) props.push(dec()); newSchemas.push(props) }
|
|
252
|
+
for (const s of newSchemas) {
|
|
253
|
+
const key = s.join(',')
|
|
254
|
+
if (!schemas.some(existing => existing.join(',') === key)) schemas.push(s)
|
|
166
255
|
}
|
|
167
256
|
}
|
|
168
257
|
|
|
@@ -277,6 +366,8 @@ export const memory = (src) => {
|
|
|
277
366
|
if (t === 0 && off === 0) {
|
|
278
367
|
if (a === 1) return null
|
|
279
368
|
if (a === 2) return undefined
|
|
369
|
+
if (a === 4) return false
|
|
370
|
+
if (a === 5) return true
|
|
280
371
|
}
|
|
281
372
|
if (t === 11 && this._extMap) return this._extMap[off]
|
|
282
373
|
if (t === 1) { // ARRAY
|
|
@@ -414,10 +505,11 @@ export const wrap = (memSrc, inst) => {
|
|
|
414
505
|
const restFuncs = new Map()
|
|
415
506
|
const mod = inst ? memSrc : memSrc.module || memSrc
|
|
416
507
|
const realInst = inst || memSrc.instance || memSrc
|
|
417
|
-
const
|
|
418
|
-
|
|
508
|
+
const td = new TextDecoder()
|
|
509
|
+
const restBytes = customSection(mod, 'jz:rest')
|
|
510
|
+
if (restBytes) {
|
|
419
511
|
try {
|
|
420
|
-
for (const entry of JSON.parse(
|
|
512
|
+
for (const entry of JSON.parse(td.decode(restBytes)))
|
|
421
513
|
restFuncs.set(typeof entry === 'string' ? entry : entry.name, typeof entry === 'string' ? 0 : entry.fixed)
|
|
422
514
|
} catch (e) { /* ignore */ }
|
|
423
515
|
}
|
|
@@ -427,11 +519,29 @@ export const wrap = (memSrc, inst) => {
|
|
|
427
519
|
// reinterprets f64↔BigInt only at those positions (see
|
|
428
520
|
// synthesizeBoundaryWrappers).
|
|
429
521
|
const i64Exp = new Map(), EMPTY_SET = new Set()
|
|
430
|
-
const
|
|
431
|
-
if (
|
|
432
|
-
try { for (const e of JSON.parse(
|
|
522
|
+
const i64Bytes = customSection(mod, 'jz:i64exp')
|
|
523
|
+
if (i64Bytes) {
|
|
524
|
+
try { for (const e of JSON.parse(td.decode(i64Bytes))) i64Exp.set(e.name, { p: new Set(e.p), r: e.r }) }
|
|
433
525
|
catch { /* ignore */ }
|
|
434
526
|
}
|
|
527
|
+
// externref-param exports: positions where the wasm side takes an externref
|
|
528
|
+
// (e.g. jsstring boundary opt-in). JS values at these positions pass through
|
|
529
|
+
// unchanged — no `mem.wrapVal` (would NaN-box into f64, defeating the point).
|
|
530
|
+
// `def` (optional) maps idx → default-string for jsstring params whose
|
|
531
|
+
// default substitution happens JS-side (the wasm side never sees null).
|
|
532
|
+
const extExp = new Map()
|
|
533
|
+
const extBytes = customSection(mod, 'jz:extparam')
|
|
534
|
+
if (extBytes) {
|
|
535
|
+
try {
|
|
536
|
+
for (const e of JSON.parse(td.decode(extBytes))) {
|
|
537
|
+
const idx = new Set(e.p)
|
|
538
|
+
// Hang the defaults off the Set as a property so call-sites that only
|
|
539
|
+
// check membership stay unchanged; the slow path reads `extInfo.def`.
|
|
540
|
+
if (e.d) idx.def = new Map(Object.entries(e.d).map(([k, v]) => [Number(k), v]))
|
|
541
|
+
extExp.set(e.name, idx)
|
|
542
|
+
}
|
|
543
|
+
} catch { /* ignore */ }
|
|
544
|
+
}
|
|
435
545
|
|
|
436
546
|
const mem = memory(memSrc)
|
|
437
547
|
const lastErrBits = realInst.exports.__jz_last_err_bits
|
|
@@ -453,17 +563,36 @@ export const wrap = (memSrc, inst) => {
|
|
|
453
563
|
// indices; r = result is i64. Numeric (f64) positions pass through unchanged.
|
|
454
564
|
const adaptArgs = (a, p) => p.size === 0 ? a : a.map((x, i) => p.has(i) ? f64ToI64(x) : x)
|
|
455
565
|
const adaptRet = (ret, r) => r ? i64ToF64(ret) : ret
|
|
566
|
+
// Externref positions skip mem.wrapVal — the raw JS value (string, object,
|
|
567
|
+
// …) flows through as externref. mem.wrapVal would NaN-box it.
|
|
568
|
+
const wrapArg = (ext, i, x) => ext?.has(i) ? x : mem.wrapVal(x)
|
|
456
569
|
|
|
457
570
|
// Arity-specialized wrapper: rest-spread + .map() + .apply() costs ~85ns/call
|
|
458
571
|
// on hot loops (mandelbrot benchmark: 51ms wrapped vs 35ms direct over 200K
|
|
459
572
|
// calls). Generating positional `function(a0, a1, ...)` via Function lets V8
|
|
460
573
|
// fully inline the WASM call. Falls back to the spread-form wrapper if the
|
|
461
574
|
// Function constructor is unavailable (CSP) or arity is unusually large.
|
|
462
|
-
|
|
575
|
+
// `ext` is a Set of externref param positions — those slots skip wrap/coerce
|
|
576
|
+
// and pass the JS value directly (jsstring carrier: JS string → externref).
|
|
577
|
+
// `ext.def` (optional Map idx→string) carries jsstring-literal defaults that
|
|
578
|
+
// are substituted JS-side when the caller passes `undefined`.
|
|
579
|
+
const makeFastWrapper = (fn, len, p, r, decode_, wrap_, ext) => {
|
|
463
580
|
const params = [], wrapped = []
|
|
581
|
+
const defs = ext?.def
|
|
582
|
+
const defArgs = [], defNames = []
|
|
464
583
|
for (let i = 0; i < len; i++) {
|
|
465
584
|
const a = `a${i}`
|
|
466
585
|
params.push(a)
|
|
586
|
+
if (ext?.has(i)) {
|
|
587
|
+
if (defs?.has(i)) {
|
|
588
|
+
const dn = `def${i}`
|
|
589
|
+
defNames.push(dn); defArgs.push(defs.get(i))
|
|
590
|
+
wrapped.push(`(${a} === undefined ? ${dn} : ${a})`)
|
|
591
|
+
} else {
|
|
592
|
+
wrapped.push(a)
|
|
593
|
+
}
|
|
594
|
+
continue
|
|
595
|
+
}
|
|
467
596
|
const w = wrap_ ? `wrap_(${a})` : `coerce(${a})`
|
|
468
597
|
wrapped.push(p.has(i) ? `f64ToI64(${w})` : w)
|
|
469
598
|
}
|
|
@@ -472,8 +601,8 @@ export const wrap = (memSrc, inst) => {
|
|
|
472
601
|
const body = `return function(${params.join(',')}) {\n` +
|
|
473
602
|
` try { return decode_(${retExpr}) } catch (e) { decodeThrown(e) }\n` +
|
|
474
603
|
`}`
|
|
475
|
-
return new Function('fn', 'wrap_', 'coerce', 'decode_', 'f64ToI64', 'i64ToF64', 'decodeThrown', body)(
|
|
476
|
-
fn, wrap_, coerce, decode_, f64ToI64, i64ToF64, decodeThrown)
|
|
604
|
+
return new Function('fn', 'wrap_', 'coerce', 'decode_', 'f64ToI64', 'i64ToF64', 'decodeThrown', ...defNames, body)(
|
|
605
|
+
fn, wrap_, coerce, decode_, f64ToI64, i64ToF64, decodeThrown, ...defArgs)
|
|
477
606
|
}
|
|
478
607
|
|
|
479
608
|
// Pure scalar module (no memory): pass f64 values directly, no marshaling
|
|
@@ -482,15 +611,19 @@ export const wrap = (memSrc, inst) => {
|
|
|
482
611
|
if (typeof fn !== 'function') { exports[name] = fn; continue }
|
|
483
612
|
const sig = i64Exp.get(name)
|
|
484
613
|
const p = sig?.p || EMPTY_SET, r = sig?.r || 0
|
|
614
|
+
const ext = extExp.get(name)
|
|
485
615
|
const len = fn.length
|
|
486
616
|
try {
|
|
487
|
-
exports[name] = makeFastWrapper(fn, len, p, r, decode, null)
|
|
617
|
+
exports[name] = makeFastWrapper(fn, len, p, r, decode, null, ext)
|
|
488
618
|
continue
|
|
489
619
|
} catch { /* CSP fallback */ }
|
|
490
620
|
exports[name] = (...args) => {
|
|
491
621
|
while (args.length < len) args.push(undefined)
|
|
492
622
|
try {
|
|
493
|
-
const wasmArgs = adaptArgs(args.map(
|
|
623
|
+
const wasmArgs = adaptArgs(args.map((x, i) => {
|
|
624
|
+
if (!ext?.has(i)) return coerce(x)
|
|
625
|
+
return x === undefined && ext.def?.has(i) ? ext.def.get(i) : x
|
|
626
|
+
}), p)
|
|
494
627
|
return decode(adaptRet(fn(...wasmArgs), r))
|
|
495
628
|
} catch (e) { decodeThrown(e) }
|
|
496
629
|
}
|
|
@@ -504,8 +637,12 @@ export const wrap = (memSrc, inst) => {
|
|
|
504
637
|
const fixed = restFuncs.get(name)
|
|
505
638
|
const sig = i64Exp.get(name)
|
|
506
639
|
const p = sig?.p || EMPTY_SET, r = sig?.r || 0
|
|
640
|
+
const ext = extExp.get(name)
|
|
507
641
|
exports[name] = (...args) => {
|
|
508
|
-
const a = args.slice(0, fixed).map(x =>
|
|
642
|
+
const a = args.slice(0, fixed).map((x, i) => {
|
|
643
|
+
if (!ext?.has(i)) return mem.wrapVal(x)
|
|
644
|
+
return x === undefined && ext.def?.has(i) ? ext.def.get(i) : x
|
|
645
|
+
})
|
|
509
646
|
while (a.length < fixed) a.push(UNDEF_NAN)
|
|
510
647
|
a.push(mem.Array(args.slice(fixed)))
|
|
511
648
|
try {
|
|
@@ -518,15 +655,19 @@ export const wrap = (memSrc, inst) => {
|
|
|
518
655
|
} else if (typeof fn === 'function') {
|
|
519
656
|
const sig = i64Exp.get(name)
|
|
520
657
|
const p = sig?.p || EMPTY_SET, r = sig?.r || 0
|
|
658
|
+
const ext = extExp.get(name)
|
|
521
659
|
const len = fn.length
|
|
522
660
|
try {
|
|
523
|
-
exports[name] = makeFastWrapper(fn, len, p, r, memRead, memWrapVal)
|
|
661
|
+
exports[name] = makeFastWrapper(fn, len, p, r, memRead, memWrapVal, ext)
|
|
524
662
|
continue
|
|
525
663
|
} catch { /* CSP fallback */ }
|
|
526
664
|
exports[name] = (...args) => {
|
|
527
665
|
while (args.length < len) args.push(undefined)
|
|
528
666
|
try {
|
|
529
|
-
const boxed = args.map(x =>
|
|
667
|
+
const boxed = args.map((x, i) => {
|
|
668
|
+
if (!ext?.has(i)) return mem.wrapVal(x)
|
|
669
|
+
return x === undefined && ext.def?.has(i) ? ext.def.get(i) : x
|
|
670
|
+
})
|
|
530
671
|
const ret = fn.apply(null, adaptArgs(boxed, p))
|
|
531
672
|
return mem.read(adaptRet(ret, r))
|
|
532
673
|
} catch (error) {
|
|
@@ -573,8 +714,7 @@ const prepareInterop = (opts) => {
|
|
|
573
714
|
// imports them (host: 'js' mode lowering in module/console.js). Caller-provided
|
|
574
715
|
// opts.imports.env entries take precedence.
|
|
575
716
|
const installDefaultEnvImports = (mod, imports, state) => {
|
|
576
|
-
const envFns =
|
|
577
|
-
.filter(i => i.module === 'env' && i.kind === 'function').map(i => i.name))
|
|
717
|
+
const envFns = envFuncNames(mod)
|
|
578
718
|
if (!envFns.size) return
|
|
579
719
|
if (!imports.env) imports.env = {}
|
|
580
720
|
if (envFns.has('print') && !imports.env.print) {
|
|
@@ -647,11 +787,61 @@ const installDefaultEnvImports = (mod, imports, state) => {
|
|
|
647
787
|
}
|
|
648
788
|
}
|
|
649
789
|
|
|
790
|
+
// JS-side polyfills for `wasm:js-string` builtins. Used when the engine does
|
|
791
|
+
// NOT honor `new WebAssembly.Module(buf, { builtins: ['js-string'] })` — older
|
|
792
|
+
// V8, Hermes, JSC pre-18.4, etc. With native builtins the engine inlines
|
|
793
|
+
// these calls to direct string accesses; with the polyfill each call is a
|
|
794
|
+
// wasm→JS hop (still correct, just no boundary win).
|
|
795
|
+
const JSS_POLYFILL = {
|
|
796
|
+
length: (s) => s.length,
|
|
797
|
+
charCodeAt: (s, i) => s.charCodeAt(i),
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Probe once: does this engine honor the `{ builtins: ['js-string'] }` option
|
|
801
|
+
// on WebAssembly.Module? Compiles a tiny module that imports a wasm:js-string
|
|
802
|
+
// fn; if instantiation succeeds with no imports object, native is available.
|
|
803
|
+
let jssNativeProbed = false
|
|
804
|
+
let jssNativeSupported = false
|
|
805
|
+
const jssProbeNative = () => {
|
|
806
|
+
if (jssNativeProbed) return jssNativeSupported
|
|
807
|
+
jssNativeProbed = true
|
|
808
|
+
try {
|
|
809
|
+
// Minimal module: (module (import "wasm:js-string" "length" (func (param externref) (result i32))))
|
|
810
|
+
const bytes = new Uint8Array([
|
|
811
|
+
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, // header
|
|
812
|
+
0x01, 0x06, 0x01, 0x60, 0x01, 0x6f, 0x01, 0x7f, // type: (externref)→i32
|
|
813
|
+
0x02, 0x18, 0x01, // import section
|
|
814
|
+
0x0f, ...new TextEncoder().encode('wasm:js-string'), // mod name
|
|
815
|
+
0x06, ...new TextEncoder().encode('length'), // name
|
|
816
|
+
0x00, 0x00, // kind=func, type=0
|
|
817
|
+
])
|
|
818
|
+
const mod = new WebAssembly.Module(bytes, { builtins: ['js-string'] })
|
|
819
|
+
new WebAssembly.Instance(mod, {})
|
|
820
|
+
jssNativeSupported = true
|
|
821
|
+
} catch {
|
|
822
|
+
jssNativeSupported = false
|
|
823
|
+
}
|
|
824
|
+
return jssNativeSupported
|
|
825
|
+
}
|
|
826
|
+
|
|
650
827
|
const buildImports = (mod, opts, state) => {
|
|
651
|
-
const needsWasi =
|
|
652
|
-
const imports =
|
|
828
|
+
const { needsWasi, wasiImports } = linkWasi(mod, opts)
|
|
829
|
+
const imports = wasiImports || {}
|
|
653
830
|
if (opts._interp) imports.env = { ...imports.env, ...opts._interp }
|
|
654
831
|
|
|
832
|
+
// `wasm:js-string` polyfills — only attach when native builtins aren't honored
|
|
833
|
+
// by this engine. With `{ builtins: ['js-string'] }` the import slots are
|
|
834
|
+
// already filled by the engine; supplying a JS function would error or just
|
|
835
|
+
// be ignored. Without native support, polyfill the names this module imports.
|
|
836
|
+
if (!jssProbeNative()) {
|
|
837
|
+
for (const imp of WebAssembly.Module.imports(mod)) {
|
|
838
|
+
if (imp.module === 'wasm:js-string' && JSS_POLYFILL[imp.name]) {
|
|
839
|
+
if (!imports['wasm:js-string']) imports['wasm:js-string'] = {}
|
|
840
|
+
imports['wasm:js-string'][imp.name] = JSS_POLYFILL[imp.name]
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
655
845
|
// Host imports: decode NaN-boxed args for JS and wrap JS returns back into jz
|
|
656
846
|
// values. Args/return ride i64 across the boundary (Step 2c) so V8 cannot
|
|
657
847
|
// canonicalize the NaN payload — convert BigInt↔f64 via reinterpret bits.
|
|
@@ -705,16 +895,8 @@ const finishInstantiation = (mod, inst, imports, needsWasi, opts, state) => {
|
|
|
705
895
|
// Trampoline used by env.setTimeout/clearTimeout to fire scheduled closures.
|
|
706
896
|
state.invoke = inst.exports.__invoke_closure || null
|
|
707
897
|
|
|
708
|
-
// Drive WASM timer queue via JS scheduling (non-blocking)
|
|
709
|
-
|
|
710
|
-
const tick = inst.exports.__timer_tick
|
|
711
|
-
let hadTimers = false
|
|
712
|
-
const id = setInterval(() => {
|
|
713
|
-
const remaining = tick()
|
|
714
|
-
if (remaining > 0) hadTimers = true
|
|
715
|
-
if (hadTimers && remaining <= 0) clearInterval(id)
|
|
716
|
-
}, 1)
|
|
717
|
-
}
|
|
898
|
+
// Drive WASM timer queue via JS scheduling (non-blocking, no-op if absent).
|
|
899
|
+
attachTimers(inst)
|
|
718
900
|
|
|
719
901
|
// For shared memory, resolve memory from import; for own memory, from export.
|
|
720
902
|
const rawMemory = opts.memory instanceof WebAssembly.Memory ? opts.memory : inst.exports.memory
|
|
@@ -726,14 +908,35 @@ const finishInstantiation = (mod, inst, imports, needsWasi, opts, state) => {
|
|
|
726
908
|
}
|
|
727
909
|
|
|
728
910
|
/**
|
|
729
|
-
*
|
|
730
|
-
*
|
|
911
|
+
* Instantiate prebuilt jz wasm and wrap exports (WASI imports, rest-params,
|
|
912
|
+
* host-object externrefs, default env.print/now wiring, optional shared memory).
|
|
913
|
+
*
|
|
914
|
+
* Compile-and-instantiate is the caller's job — pass already-compiled bytes:
|
|
915
|
+
* import { instantiate } from 'jz/interop'
|
|
916
|
+
* const { exports, memory } = instantiate(wasmBytes)
|
|
917
|
+
*
|
|
918
|
+
* @param {Uint8Array|ArrayBuffer|WebAssembly.Module} wasm prebuilt wasm
|
|
919
|
+
* @param {object} [opts] host options: imports, memory, _interp, host-shape flags
|
|
920
|
+
* @returns {{ exports, memory, instance, module }}
|
|
731
921
|
*/
|
|
732
|
-
export const instantiate = (
|
|
922
|
+
export const instantiate = (wasm, opts = {}) => {
|
|
733
923
|
const state = prepareInterop(opts)
|
|
734
|
-
const wasm = compile(code, opts)
|
|
735
924
|
opts.extMap = state.extMap
|
|
736
|
-
|
|
925
|
+
// Prefer native `wasm:js-string` builtins when the engine honors the option.
|
|
926
|
+
// The option is silently accepted by V8 17+/Safari 18.4+; older engines that
|
|
927
|
+
// don't recognize it either throw or ignore it — try-fallback handles both.
|
|
928
|
+
let mod
|
|
929
|
+
if (wasm instanceof WebAssembly.Module) {
|
|
930
|
+
mod = wasm
|
|
931
|
+
} else if (jssProbeNative()) {
|
|
932
|
+
try {
|
|
933
|
+
mod = new WebAssembly.Module(wasm, { builtins: ['js-string'] })
|
|
934
|
+
} catch {
|
|
935
|
+
mod = new WebAssembly.Module(wasm)
|
|
936
|
+
}
|
|
937
|
+
} else {
|
|
938
|
+
mod = new WebAssembly.Module(wasm)
|
|
939
|
+
}
|
|
737
940
|
const { imports, needsWasi } = buildImports(mod, opts, state)
|
|
738
941
|
const hasImports = Object.keys(imports).some(k => k !== '_setMemory')
|
|
739
942
|
const inst = new WebAssembly.Instance(mod, hasImports ? imports : undefined)
|