jz 0.1.0 → 0.2.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 +221 -62
- package/cli.js +34 -8
- package/index.js +133 -14
- package/module/array.js +399 -131
- package/module/collection.js +457 -212
- package/module/console.js +170 -87
- package/module/core.js +247 -143
- package/module/date.js +691 -0
- package/module/function.js +84 -23
- package/module/index.js +2 -1
- package/module/json.js +485 -138
- package/module/math.js +81 -40
- package/module/number.js +189 -83
- package/module/object.js +228 -46
- package/module/regex.js +34 -30
- package/module/schema.js +17 -18
- package/module/string.js +483 -227
- package/module/symbol.js +6 -4
- package/module/timer.js +90 -32
- package/module/typedarray.js +176 -44
- package/package.json +5 -10
- package/src/analyze.js +639 -124
- package/src/autoload.js +183 -0
- package/src/compile.js +448 -1083
- package/src/ctx.js +42 -12
- package/src/emit.js +646 -147
- package/src/host.js +273 -68
- package/src/ir.js +81 -31
- package/src/jzify.js +220 -36
- package/src/narrow.js +956 -0
- package/src/optimize.js +628 -42
- package/src/plan.js +1233 -0
- package/src/prepare.js +438 -256
- package/src/vectorize.js +1016 -0
- package/wasi.js +23 -4
package/src/host.js
CHANGED
|
@@ -20,10 +20,23 @@ import { wasi } from '../wasi.js'
|
|
|
20
20
|
|
|
21
21
|
// NaN-boxing encode/decode — shared 8-byte scratch buffer
|
|
22
22
|
const _buf = new ArrayBuffer(8), _u32 = new Uint32Array(_buf), _f64 = new Float64Array(_buf)
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
//
|
|
23
|
+
// Cross-typed-array view for i64↔f64 reinterpretation. Used at every wasm↔JS
|
|
24
|
+
// boundary that carries a NaN-boxed pointer as i64 bits — V8 may canonicalize
|
|
25
|
+
// f64 NaN payloads at the boundary, so the carrier is BigInt and reinterpret
|
|
26
|
+
// runs once on each side. Separate buffer so it never aliases _u32/_f64.
|
|
27
|
+
const _bi64 = (() => {
|
|
28
|
+
const ab = new ArrayBuffer(8), bi = new BigInt64Array(ab), fv = new Float64Array(ab)
|
|
29
|
+
return {
|
|
30
|
+
i64ToF64: (big) => { bi[0] = big; return fv[0] },
|
|
31
|
+
f64ToI64: (f) => { fv[0] = f; return bi[0] },
|
|
32
|
+
}
|
|
33
|
+
})()
|
|
34
|
+
export const i64ToF64 = _bi64.i64ToF64
|
|
35
|
+
export const f64ToI64 = _bi64.f64ToI64
|
|
36
|
+
// Reserved atoms (type=0, offset=0): aux=1 → null, aux=2 → undefined.
|
|
37
|
+
// Distinct from 0, JS NaN (payload=0), and all pointers.
|
|
26
38
|
_u32[1] = 0x7FF80001; _u32[0] = 0; export const NULL_NAN = _f64[0]
|
|
39
|
+
_u32[1] = 0x7FF80002; _u32[0] = 0; export const UNDEF_NAN = _f64[0]
|
|
27
40
|
|
|
28
41
|
// Coerce JS null/undefined → NaN-boxed sentinels for WASM boundary
|
|
29
42
|
export const coerce = v => v === null ? NULL_NAN : v === undefined ? UNDEF_NAN : v
|
|
@@ -32,8 +45,9 @@ export const coerce = v => v === null ? NULL_NAN : v === undefined ? UNDEF_NAN :
|
|
|
32
45
|
const decode = v => {
|
|
33
46
|
if (v === v) return v // fast path: non-NaN
|
|
34
47
|
_f64[0] = v
|
|
35
|
-
if (_u32[
|
|
36
|
-
if (_u32[1] ===
|
|
48
|
+
if (_u32[0] !== 0) return v
|
|
49
|
+
if (_u32[1] === 0x7FF80001) return null
|
|
50
|
+
if (_u32[1] === 0x7FF80002) return undefined
|
|
37
51
|
return v
|
|
38
52
|
}
|
|
39
53
|
|
|
@@ -99,16 +113,22 @@ export const memory = (src) => {
|
|
|
99
113
|
|
|
100
114
|
// JS-side bump allocator (heap ptr at byte 1020, same convention as WASM)
|
|
101
115
|
const jsAlloc = (bytes) => {
|
|
102
|
-
|
|
116
|
+
let d = dv(), p = d.getInt32(1020, true)
|
|
103
117
|
const aligned = (p + 7) & ~7 // 8-byte align
|
|
104
118
|
const next = aligned + bytes
|
|
105
|
-
if (next > mem.buffer.byteLength)
|
|
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
|
+
}
|
|
106
123
|
d.setInt32(1020, next, true)
|
|
107
124
|
return aligned
|
|
108
125
|
}
|
|
109
126
|
|
|
110
127
|
// Use WASM allocator if available, else JS-side bump
|
|
111
128
|
let alloc = wasmExports?._alloc || jsAlloc
|
|
129
|
+
// JS-side reset: rewind the bump pointer at byte 1020. Only used when no WASM
|
|
130
|
+
// _clear is present (otherwise the WASM global / shared slot is authoritative).
|
|
131
|
+
const jsReset = () => dv().setInt32(1020, 1024, true)
|
|
112
132
|
|
|
113
133
|
// Initialize heap pointer if not yet set
|
|
114
134
|
const initDv = dv()
|
|
@@ -150,7 +170,8 @@ export const memory = (src) => {
|
|
|
150
170
|
if (_enhanced.has(mem)) {
|
|
151
171
|
mem.schemas = schemas
|
|
152
172
|
if (wasmExports?._alloc) { alloc = wasmExports._alloc; mem.alloc = alloc }
|
|
153
|
-
if (wasmExports?.
|
|
173
|
+
if (wasmExports?._clear) mem.reset = wasmExports._clear
|
|
174
|
+
else if (!mem.reset) mem.reset = jsReset
|
|
154
175
|
if (extMap) mem._extMap = extMap
|
|
155
176
|
return mem
|
|
156
177
|
}
|
|
@@ -160,8 +181,14 @@ export const memory = (src) => {
|
|
|
160
181
|
mem._extMap = extMap
|
|
161
182
|
|
|
162
183
|
mem.Array = (data) => {
|
|
163
|
-
const n = data.length, off = hdr(n, n, n * 8)
|
|
164
|
-
|
|
184
|
+
const n = data.length, off = hdr(n, n, n * 8)
|
|
185
|
+
// Stage as i64 bits, not as JS Numbers: V8 may transition a JS Array holding
|
|
186
|
+
// NaN-payload doubles to HOLEY_DOUBLE_ELEMENTS, which canonicalizes the NaN
|
|
187
|
+
// payload to 0x7FF8000000000000 — destroying the type/offset bits.
|
|
188
|
+
const wrapped = new BigInt64Array(n)
|
|
189
|
+
for (let i = 0; i < n; i++) wrapped[i] = f64ToI64(mem.wrapVal(data[i]))
|
|
190
|
+
const dst = new BigInt64Array(mem.buffer, off, n)
|
|
191
|
+
for (let i = 0; i < n; i++) dst[i] = wrapped[i]
|
|
165
192
|
return ptr(1, 0, off)
|
|
166
193
|
}
|
|
167
194
|
|
|
@@ -169,7 +196,7 @@ export const memory = (src) => {
|
|
|
169
196
|
if (str.length <= 4 && /^[\x00-\x7f]*$/.test(str)) {
|
|
170
197
|
let packed = 0
|
|
171
198
|
for (let i = 0; i < str.length; i++) packed |= str.charCodeAt(i) << (i * 8)
|
|
172
|
-
return ptr(
|
|
199
|
+
return ptr(4, 0x4000 | str.length, packed) // STRING + SSO_BIT
|
|
173
200
|
}
|
|
174
201
|
const enc = new TextEncoder().encode(str)
|
|
175
202
|
const n = enc.length, raw = alloc(4 + n), m = dv()
|
|
@@ -192,6 +219,10 @@ export const memory = (src) => {
|
|
|
192
219
|
if (v === null || v === undefined) return coerce(v)
|
|
193
220
|
if (typeof v === 'number' || typeof v === 'boolean') return Number(v)
|
|
194
221
|
if (typeof v === 'string') return this.String(v)
|
|
222
|
+
// BigInt as a data value crosses the boundary as a decimal-string. wasm-side
|
|
223
|
+
// numeric parsers accept string form. Direct i64 function args go through this
|
|
224
|
+
// path too; the call wrappers below detect BigInt before adaptArgs reinterprets.
|
|
225
|
+
if (typeof v === 'bigint') return this.String(v.toString())
|
|
195
226
|
if (Array.isArray(v)) return this.Array(v)
|
|
196
227
|
if (v instanceof ArrayBuffer) return this.Buffer(v)
|
|
197
228
|
if (v instanceof DataView) return this.Buffer(v.buffer)
|
|
@@ -223,14 +254,19 @@ export const memory = (src) => {
|
|
|
223
254
|
else if (this._extMap) return this.External(obj)
|
|
224
255
|
else throw Error(`No schema for {${key}}`)
|
|
225
256
|
}
|
|
226
|
-
const schema = schemas[sid], n = schema.length, raw = alloc(n * 8)
|
|
257
|
+
const schema = schemas[sid], n = schema.length, raw = alloc(n * 8)
|
|
258
|
+
// Stage as i64 bits so V8 can't canonicalize NaN-payload pointers across
|
|
259
|
+
// recursive allocations. See mem.Array for the same pattern.
|
|
260
|
+
const wrapped = new BigInt64Array(n)
|
|
227
261
|
for (let i = 0; i < n; i++) {
|
|
228
262
|
let v = obj[schema[i]]
|
|
229
263
|
if (v === null || v === undefined) v = coerce(v)
|
|
230
264
|
else if (typeof v === 'string') v = this.String(v)
|
|
231
265
|
else if (Array.isArray(v)) v = this.Array(v)
|
|
232
|
-
|
|
266
|
+
wrapped[i] = f64ToI64(v)
|
|
233
267
|
}
|
|
268
|
+
const dst = new BigInt64Array(mem.buffer, raw, n)
|
|
269
|
+
for (let i = 0; i < n; i++) dst[i] = wrapped[i]
|
|
234
270
|
return ptr(6, sid, raw)
|
|
235
271
|
}
|
|
236
272
|
|
|
@@ -238,8 +274,10 @@ export const memory = (src) => {
|
|
|
238
274
|
if (Array.isArray(p)) return p.map(v => this.read(v)) // multi-value tuple
|
|
239
275
|
if (p === p) return p // regular number passthrough (NaN fails ===)
|
|
240
276
|
const t = type(p), a = aux(p), off = offset(p)
|
|
241
|
-
if (t === 0 &&
|
|
242
|
-
|
|
277
|
+
if (t === 0 && off === 0) {
|
|
278
|
+
if (a === 1) return null
|
|
279
|
+
if (a === 2) return undefined
|
|
280
|
+
}
|
|
243
281
|
if (t === 11 && this._extMap) return this._extMap[off]
|
|
244
282
|
if (t === 1) { // ARRAY
|
|
245
283
|
let m = dv(), aOff = off
|
|
@@ -267,15 +305,16 @@ export const memory = (src) => {
|
|
|
267
305
|
new Uint8Array(out).set(new Uint8Array(mem.buffer, off, byteLen))
|
|
268
306
|
return out
|
|
269
307
|
}
|
|
270
|
-
if (t === 4) { // STRING (heap)
|
|
308
|
+
if (t === 4) { // STRING (aux bit 0x4000 = SSO inline, else heap)
|
|
309
|
+
const a2 = aux(p)
|
|
310
|
+
if (a2 & 0x4000) {
|
|
311
|
+
const len = a2 & 0x7; let s = ''
|
|
312
|
+
for (let i = 0; i < len; i++) s += String.fromCharCode((off >>> (i * 8)) & 0xFF)
|
|
313
|
+
return s
|
|
314
|
+
}
|
|
271
315
|
const len = dv().getInt32(off - 4, true)
|
|
272
316
|
return new TextDecoder().decode(new Uint8Array(mem.buffer, off, len))
|
|
273
317
|
}
|
|
274
|
-
if (t === 5) { // STRING_SSO
|
|
275
|
-
const len = aux(p); let s = ''
|
|
276
|
-
for (let i = 0; i < len; i++) s += String.fromCharCode((off >>> (i * 8)) & 0xFF)
|
|
277
|
-
return s
|
|
278
|
-
}
|
|
279
318
|
if (t === 6) { // OBJECT
|
|
280
319
|
const m = dv(), sid = aux(p), keys = this.schemas[sid]
|
|
281
320
|
if (!keys) return p
|
|
@@ -352,7 +391,7 @@ export const memory = (src) => {
|
|
|
352
391
|
}
|
|
353
392
|
|
|
354
393
|
mem.alloc = alloc
|
|
355
|
-
mem.reset = wasmExports?.
|
|
394
|
+
mem.reset = wasmExports?._clear || jsReset
|
|
356
395
|
|
|
357
396
|
// TypedArray constructors: memory.Float64Array(data), etc.
|
|
358
397
|
for (const [name, [elemId, stride, , setter]] of Object.entries(ELEMS)) {
|
|
@@ -382,6 +421,17 @@ export const wrap = (memSrc, inst) => {
|
|
|
382
421
|
restFuncs.set(typeof entry === 'string' ? entry : entry.name, typeof entry === 'string' ? 0 : entry.fixed)
|
|
383
422
|
} catch (e) { /* ignore */ }
|
|
384
423
|
}
|
|
424
|
+
// i64-ABI exports: boundary-wrapped funcs whose NaN-boxed pointer params/
|
|
425
|
+
// result ride i64 to dodge V8's NaN canonicalization. Map: name → { p, r }
|
|
426
|
+
// with p = bit mask of i64 params, r = 1 iff result is i64. JS side
|
|
427
|
+
// reinterprets f64↔BigInt only at those positions (see
|
|
428
|
+
// synthesizeBoundaryWrappers).
|
|
429
|
+
const i64Exp = new Map(), EMPTY_SET = new Set()
|
|
430
|
+
const i64Secs = WebAssembly.Module.customSections(mod, 'jz:i64exp')
|
|
431
|
+
if (i64Secs.length) {
|
|
432
|
+
try { for (const e of JSON.parse(new TextDecoder().decode(i64Secs[0]))) i64Exp.set(e.name, { p: new Set(e.p), r: e.r }) }
|
|
433
|
+
catch { /* ignore */ }
|
|
434
|
+
}
|
|
385
435
|
|
|
386
436
|
const mem = memory(memSrc)
|
|
387
437
|
const lastErrBits = realInst.exports.__jz_last_err_bits
|
|
@@ -398,32 +448,87 @@ export const wrap = (memSrc, inst) => {
|
|
|
398
448
|
throw wrapped
|
|
399
449
|
}
|
|
400
450
|
const exports = {}
|
|
451
|
+
// Per-position carrier swap: f64 stays Number, i64 positions reinterpret to
|
|
452
|
+
// BigInt before the call and back to Number after. p is a Set of i64 param
|
|
453
|
+
// indices; r = result is i64. Numeric (f64) positions pass through unchanged.
|
|
454
|
+
const adaptArgs = (a, p) => p.size === 0 ? a : a.map((x, i) => p.has(i) ? f64ToI64(x) : x)
|
|
455
|
+
const adaptRet = (ret, r) => r ? i64ToF64(ret) : ret
|
|
456
|
+
|
|
457
|
+
// Arity-specialized wrapper: rest-spread + .map() + .apply() costs ~85ns/call
|
|
458
|
+
// on hot loops (mandelbrot benchmark: 51ms wrapped vs 35ms direct over 200K
|
|
459
|
+
// calls). Generating positional `function(a0, a1, ...)` via Function lets V8
|
|
460
|
+
// fully inline the WASM call. Falls back to the spread-form wrapper if the
|
|
461
|
+
// Function constructor is unavailable (CSP) or arity is unusually large.
|
|
462
|
+
const makeFastWrapper = (fn, len, p, r, decode_, wrap_) => {
|
|
463
|
+
const params = [], wrapped = []
|
|
464
|
+
for (let i = 0; i < len; i++) {
|
|
465
|
+
const a = `a${i}`
|
|
466
|
+
params.push(a)
|
|
467
|
+
const w = wrap_ ? `wrap_(${a})` : `coerce(${a})`
|
|
468
|
+
wrapped.push(p.has(i) ? `f64ToI64(${w})` : w)
|
|
469
|
+
}
|
|
470
|
+
const callExpr = `fn(${wrapped.join(',')})`
|
|
471
|
+
const retExpr = r ? `i64ToF64(${callExpr})` : callExpr
|
|
472
|
+
const body = `return function(${params.join(',')}) {\n` +
|
|
473
|
+
` try { return decode_(${retExpr}) } catch (e) { decodeThrown(e) }\n` +
|
|
474
|
+
`}`
|
|
475
|
+
return new Function('fn', 'wrap_', 'coerce', 'decode_', 'f64ToI64', 'i64ToF64', 'decodeThrown', body)(
|
|
476
|
+
fn, wrap_, coerce, decode_, f64ToI64, i64ToF64, decodeThrown)
|
|
477
|
+
}
|
|
478
|
+
|
|
401
479
|
// Pure scalar module (no memory): pass f64 values directly, no marshaling
|
|
402
480
|
if (!mem) {
|
|
403
|
-
for (const [name, fn] of Object.entries(realInst.exports))
|
|
404
|
-
exports[name] =
|
|
405
|
-
|
|
406
|
-
|
|
481
|
+
for (const [name, fn] of Object.entries(realInst.exports)) {
|
|
482
|
+
if (typeof fn !== 'function') { exports[name] = fn; continue }
|
|
483
|
+
const sig = i64Exp.get(name)
|
|
484
|
+
const p = sig?.p || EMPTY_SET, r = sig?.r || 0
|
|
485
|
+
const len = fn.length
|
|
486
|
+
try {
|
|
487
|
+
exports[name] = makeFastWrapper(fn, len, p, r, decode, null)
|
|
488
|
+
continue
|
|
489
|
+
} catch { /* CSP fallback */ }
|
|
490
|
+
exports[name] = (...args) => {
|
|
491
|
+
while (args.length < len) args.push(undefined)
|
|
492
|
+
try {
|
|
493
|
+
const wasmArgs = adaptArgs(args.map(coerce), p)
|
|
494
|
+
return decode(adaptRet(fn(...wasmArgs), r))
|
|
495
|
+
} catch (e) { decodeThrown(e) }
|
|
496
|
+
}
|
|
497
|
+
}
|
|
407
498
|
return exports
|
|
408
499
|
}
|
|
500
|
+
const memWrapVal = mem.wrapVal.bind(mem)
|
|
501
|
+
const memRead = mem.read.bind(mem)
|
|
409
502
|
for (const [name, fn] of Object.entries(realInst.exports)) {
|
|
410
503
|
if (restFuncs.has(name) && typeof fn === 'function') {
|
|
411
504
|
const fixed = restFuncs.get(name)
|
|
505
|
+
const sig = i64Exp.get(name)
|
|
506
|
+
const p = sig?.p || EMPTY_SET, r = sig?.r || 0
|
|
412
507
|
exports[name] = (...args) => {
|
|
413
508
|
const a = args.slice(0, fixed).map(x => mem.wrapVal(x))
|
|
414
509
|
while (a.length < fixed) a.push(UNDEF_NAN)
|
|
415
510
|
a.push(mem.Array(args.slice(fixed)))
|
|
416
511
|
try {
|
|
417
|
-
|
|
512
|
+
const ret = fn.apply(null, adaptArgs(a, p))
|
|
513
|
+
return mem.read(adaptRet(ret, r))
|
|
418
514
|
} catch (error) {
|
|
419
515
|
decodeThrown(error)
|
|
420
516
|
}
|
|
421
517
|
}
|
|
422
518
|
} else if (typeof fn === 'function') {
|
|
519
|
+
const sig = i64Exp.get(name)
|
|
520
|
+
const p = sig?.p || EMPTY_SET, r = sig?.r || 0
|
|
521
|
+
const len = fn.length
|
|
522
|
+
try {
|
|
523
|
+
exports[name] = makeFastWrapper(fn, len, p, r, memRead, memWrapVal)
|
|
524
|
+
continue
|
|
525
|
+
} catch { /* CSP fallback */ }
|
|
423
526
|
exports[name] = (...args) => {
|
|
424
|
-
while (args.length <
|
|
527
|
+
while (args.length < len) args.push(undefined)
|
|
425
528
|
try {
|
|
426
|
-
|
|
529
|
+
const boxed = args.map(x => mem.wrapVal(x))
|
|
530
|
+
const ret = fn.apply(null, adaptArgs(boxed, p))
|
|
531
|
+
return mem.read(adaptRet(ret, r))
|
|
427
532
|
} catch (error) {
|
|
428
533
|
decodeThrown(error)
|
|
429
534
|
}
|
|
@@ -435,41 +540,109 @@ export const wrap = (memSrc, inst) => {
|
|
|
435
540
|
return exports
|
|
436
541
|
}
|
|
437
542
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
* `compile` is the jz.compile function (injected to avoid importing the compiler core).
|
|
441
|
-
*/
|
|
442
|
-
export const instantiate = (compile, code, opts = {}) => {
|
|
443
|
-
const extMap = [null]
|
|
444
|
-
let mem = null
|
|
543
|
+
const prepareInterop = (opts) => {
|
|
544
|
+
const state = { extMap: [null], mem: null }
|
|
445
545
|
opts._interp = opts._interp || {}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
546
|
+
// __ext_* receive NaN-boxed pointers across the env boundary as i64 (BigInt
|
|
547
|
+
// in JS) — see module/collection.js header for rationale. f64 returns are
|
|
548
|
+
// wrapped back to BigInt so the wasm side reinterprets a non-canonicalized
|
|
549
|
+
// bit pattern.
|
|
550
|
+
opts._interp.__ext_prop = (objBig, propBig) => {
|
|
551
|
+
const objPtr = i64ToF64(objBig), propPtr = i64ToF64(propBig)
|
|
552
|
+
const obj = state.extMap[offset(objPtr)]
|
|
553
|
+
const prop = state.mem.read(propPtr)
|
|
554
|
+
return f64ToI64(state.mem.wrapVal(typeof obj[prop] === 'function' ? obj[prop].bind(obj) : obj[prop]))
|
|
450
555
|
}
|
|
451
|
-
opts._interp.__ext_has = (
|
|
452
|
-
return (mem.read(
|
|
556
|
+
opts._interp.__ext_has = (objBig, propBig) => {
|
|
557
|
+
return (state.mem.read(i64ToF64(propBig)) in state.extMap[offset(i64ToF64(objBig))]) ? 1 : 0
|
|
453
558
|
}
|
|
454
|
-
opts._interp.__ext_set = (
|
|
455
|
-
extMap[offset(
|
|
559
|
+
opts._interp.__ext_set = (objBig, propBig, valBig) => {
|
|
560
|
+
state.extMap[offset(i64ToF64(objBig))][state.mem.read(i64ToF64(propBig))] = state.mem.read(i64ToF64(valBig))
|
|
456
561
|
return 1
|
|
457
562
|
}
|
|
458
|
-
opts._interp.__ext_call = (
|
|
459
|
-
const obj = extMap[offset(
|
|
460
|
-
const prop = mem.read(
|
|
461
|
-
const args = mem.read(
|
|
462
|
-
return mem.wrapVal(obj[prop].apply(obj, args))
|
|
563
|
+
opts._interp.__ext_call = (objBig, propBig, argsBig) => {
|
|
564
|
+
const obj = state.extMap[offset(i64ToF64(objBig))]
|
|
565
|
+
const prop = state.mem.read(i64ToF64(propBig))
|
|
566
|
+
const args = state.mem.read(i64ToF64(argsBig))
|
|
567
|
+
return f64ToI64(state.mem.wrapVal(obj[prop].apply(obj, args)))
|
|
463
568
|
}
|
|
569
|
+
return state
|
|
570
|
+
}
|
|
464
571
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
572
|
+
// Default JS-host wiring for env.print + env.now — auto-installed when the wasm
|
|
573
|
+
// imports them (host: 'js' mode lowering in module/console.js). Caller-provided
|
|
574
|
+
// opts.imports.env entries take precedence.
|
|
575
|
+
const installDefaultEnvImports = (mod, imports, state) => {
|
|
576
|
+
const envFns = new Set(WebAssembly.Module.imports(mod)
|
|
577
|
+
.filter(i => i.module === 'env' && i.kind === 'function').map(i => i.name))
|
|
578
|
+
if (!envFns.size) return
|
|
579
|
+
if (!imports.env) imports.env = {}
|
|
580
|
+
if (envFns.has('print') && !imports.env.print) {
|
|
581
|
+
const buf = ['', '', ''] // fd 0/1/2 line buffers
|
|
582
|
+
const pending = []
|
|
583
|
+
const flush = (fd) => {
|
|
584
|
+
const out = fd === 2 ? console.error : console.log
|
|
585
|
+
out(buf[fd])
|
|
586
|
+
buf[fd] = ''
|
|
587
|
+
}
|
|
588
|
+
// env.print's val param is i64 to dodge V8's f64 NaN canonicalization
|
|
589
|
+
// across the wasm→JS boundary (see module/console.js header). Reinterpret
|
|
590
|
+
// the BigInt's bits as f64 here so mem.read sees the original NaN-box.
|
|
591
|
+
const write = (valBig, fd, sep) => {
|
|
592
|
+
const v = state.mem.read(i64ToF64(valBig))
|
|
593
|
+
buf[fd] += String(v)
|
|
594
|
+
if (sep === 32) buf[fd] += ' '
|
|
595
|
+
else if (sep === 10) flush(fd)
|
|
596
|
+
}
|
|
597
|
+
imports.env.print = (val, fd, sep) => {
|
|
598
|
+
if (!state.mem) pending.push([val, fd, sep])
|
|
599
|
+
else write(val, fd, sep)
|
|
600
|
+
}
|
|
601
|
+
state.flushPrint = () => {
|
|
602
|
+
for (const args of pending) write(...args)
|
|
603
|
+
pending.length = 0
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (envFns.has('now') && !imports.env.now) {
|
|
607
|
+
imports.env.now = (clock) =>
|
|
608
|
+
clock === 1 ? (typeof performance !== 'undefined' ? performance.now() : Date.now()) : Date.now()
|
|
609
|
+
}
|
|
610
|
+
// host: 'js' timer wiring. Wasm calls env.setTimeout/clearTimeout; we drive
|
|
611
|
+
// callbacks back via the exported __invoke_closure trampoline (state.invoke).
|
|
612
|
+
// Each id maps to a cancel thunk so set/clear share state without tagging.
|
|
613
|
+
// env.setTimeout receives cbPtr as i64 bits (BigInt) — see module/timer.js;
|
|
614
|
+
// __invoke_closure also takes i64 now, so the BigInt feeds it directly.
|
|
615
|
+
if (envFns.has('setTimeout') || envFns.has('clearTimeout')) {
|
|
616
|
+
const cancel = new Map()
|
|
617
|
+
let nextId = 1
|
|
618
|
+
if (envFns.has('setTimeout') && !imports.env.setTimeout) imports.env.setTimeout = (cbBig, delayMs, repeat) => {
|
|
619
|
+
const id = nextId++
|
|
620
|
+
const fire = () => state.invoke?.(cbBig)
|
|
621
|
+
if (repeat) {
|
|
622
|
+
const h = setInterval(fire, delayMs)
|
|
623
|
+
cancel.set(id, () => clearInterval(h))
|
|
624
|
+
} else {
|
|
625
|
+
const h = setTimeout(() => { cancel.delete(id); fire() }, delayMs)
|
|
626
|
+
cancel.set(id, () => clearTimeout(h))
|
|
627
|
+
}
|
|
628
|
+
return id
|
|
629
|
+
}
|
|
630
|
+
if (envFns.has('clearTimeout') && !imports.env.clearTimeout) imports.env.clearTimeout = (id) => {
|
|
631
|
+
const c = cancel.get(id)
|
|
632
|
+
if (c) { c(); cancel.delete(id) }
|
|
633
|
+
return 0
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const buildImports = (mod, opts, state) => {
|
|
468
639
|
const needsWasi = WebAssembly.Module.imports(mod).some(i => i.module === 'wasi_snapshot_preview1')
|
|
469
640
|
const imports = needsWasi ? wasi(opts) : {}
|
|
470
641
|
if (opts._interp) imports.env = { ...imports.env, ...opts._interp }
|
|
471
642
|
|
|
472
|
-
// Host imports: decode NaN-boxed args for JS and wrap JS returns back into jz
|
|
643
|
+
// Host imports: decode NaN-boxed args for JS and wrap JS returns back into jz
|
|
644
|
+
// values. Args/return ride i64 across the boundary (Step 2c) so V8 cannot
|
|
645
|
+
// canonicalize the NaN payload — convert BigInt↔f64 via reinterpret bits.
|
|
473
646
|
if (opts.imports) for (const [modName, fns] of Object.entries(opts.imports)) {
|
|
474
647
|
if (!imports[modName]) imports[modName] = {}
|
|
475
648
|
for (const name of Object.getOwnPropertyNames(fns)) {
|
|
@@ -477,33 +650,49 @@ export const instantiate = (compile, code, opts = {}) => {
|
|
|
477
650
|
const fn = typeof spec === 'function' ? spec : (spec && typeof spec === 'object' ? spec.fn : null)
|
|
478
651
|
if (typeof fn === 'function')
|
|
479
652
|
imports[modName][name] = (...args) => {
|
|
480
|
-
|
|
481
|
-
|
|
653
|
+
// i64 carrier: reinterpret BigInt bits → f64 NaN-box. Pure-scalar modules
|
|
654
|
+
// have no memory so skip mem.read; the f64 IS the JS number for numerics.
|
|
655
|
+
const decoded = args.map(a => {
|
|
656
|
+
const f = typeof a === 'bigint' ? i64ToF64(a) : a
|
|
657
|
+
return state.mem ? state.mem.read(f) : decode(f)
|
|
658
|
+
})
|
|
659
|
+
const ret = fn.call(fns, ...decoded)
|
|
660
|
+
return f64ToI64(state.mem ? state.mem.wrapVal(ret) : coerce(ret))
|
|
482
661
|
}
|
|
483
662
|
}
|
|
484
663
|
}
|
|
485
|
-
|
|
486
|
-
|
|
664
|
+
|
|
665
|
+
installDefaultEnvImports(mod, imports, state)
|
|
666
|
+
// Shared memory: normalize (auto-wrap raw Memory), pass as import.
|
|
667
|
+
// Numeric opts.memory is a compile-time page count shorthand, not an import.
|
|
668
|
+
if (opts.memory instanceof WebAssembly.Memory) {
|
|
487
669
|
// Auto-wrap raw WebAssembly.Memory → enhanced jz.memory
|
|
488
|
-
if (
|
|
670
|
+
if (!_enhanced.has(opts.memory)) opts.memory = memory(opts.memory)
|
|
489
671
|
if (!imports.env) imports.env = {}
|
|
490
|
-
imports.env.memory = opts.memory
|
|
672
|
+
imports.env.memory = opts.memory
|
|
491
673
|
}
|
|
492
|
-
// Auto-imported host globals: provide as WebAssembly.Global wrapping NaN-boxed
|
|
674
|
+
// Auto-imported host globals: provide as WebAssembly.Global wrapping NaN-boxed
|
|
675
|
+
// external refs. Carrier is i64 so the NaN payload survives V8's boundary
|
|
676
|
+
// canonicalization — wasm side reinterprets to f64 (see asF64 in src/ir.js).
|
|
493
677
|
for (const imp of WebAssembly.Module.imports(mod)) {
|
|
494
678
|
if (imp.kind === 'global' && imp.module === 'env') {
|
|
495
679
|
const host = globalThis[imp.name]
|
|
496
680
|
if (host !== undefined) {
|
|
497
681
|
if (!imports.env) imports.env = {}
|
|
498
|
-
let id = extMap.indexOf(host); if (id === -1) { id = extMap.length; extMap.push(host) }
|
|
499
|
-
imports.env[imp.name] = new WebAssembly.Global({ value: '
|
|
682
|
+
let id = state.extMap.indexOf(host); if (id === -1) { id = state.extMap.length; state.extMap.push(host) }
|
|
683
|
+
imports.env[imp.name] = new WebAssembly.Global({ value: 'i64', mutable: false }, f64ToI64(ptr(11, 0, id)))
|
|
500
684
|
}
|
|
501
685
|
}
|
|
502
686
|
}
|
|
503
|
-
|
|
504
|
-
|
|
687
|
+
return { imports, needsWasi }
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const finishInstantiation = (mod, inst, imports, needsWasi, opts, state) => {
|
|
505
691
|
if (needsWasi) imports._setMemory(inst.exports.memory)
|
|
506
692
|
|
|
693
|
+
// Trampoline used by env.setTimeout/clearTimeout to fire scheduled closures.
|
|
694
|
+
state.invoke = inst.exports.__invoke_closure || null
|
|
695
|
+
|
|
507
696
|
// Drive WASM timer queue via JS scheduling (non-blocking)
|
|
508
697
|
if (inst.exports.__timer_tick) {
|
|
509
698
|
const tick = inst.exports.__timer_tick
|
|
@@ -515,10 +704,26 @@ export const instantiate = (compile, code, opts = {}) => {
|
|
|
515
704
|
}, 1)
|
|
516
705
|
}
|
|
517
706
|
|
|
518
|
-
// For shared memory, resolve memory from import; for own memory, from export
|
|
519
|
-
const rawMemory = opts.memory
|
|
520
|
-
const memSrc = { module: mod, instance: inst, exports: { ...inst.exports, memory: rawMemory }, extMap }
|
|
707
|
+
// For shared memory, resolve memory from import; for own memory, from export.
|
|
708
|
+
const rawMemory = opts.memory instanceof WebAssembly.Memory ? opts.memory : inst.exports.memory
|
|
709
|
+
const memSrc = { module: mod, instance: inst, exports: { ...inst.exports, memory: rawMemory }, extMap: state.extMap }
|
|
521
710
|
const enhanced = memory(memSrc)
|
|
522
|
-
mem = enhanced
|
|
711
|
+
state.mem = enhanced
|
|
712
|
+
state.flushPrint?.()
|
|
523
713
|
return { exports: wrap(memSrc), memory: enhanced, instance: inst, module: mod }
|
|
524
714
|
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Compile, instantiate, and wrap exports (with WASI + rest-param support).
|
|
718
|
+
* `compile` is the jz.compile function (injected to avoid importing the compiler core).
|
|
719
|
+
*/
|
|
720
|
+
export const instantiate = (compile, code, opts = {}) => {
|
|
721
|
+
const state = prepareInterop(opts)
|
|
722
|
+
const wasm = compile(code, opts)
|
|
723
|
+
opts.extMap = state.extMap
|
|
724
|
+
const mod = new WebAssembly.Module(wasm)
|
|
725
|
+
const { imports, needsWasi } = buildImports(mod, opts, state)
|
|
726
|
+
const hasImports = Object.keys(imports).some(k => k !== '_setMemory')
|
|
727
|
+
const inst = new WebAssembly.Instance(mod, hasImports ? imports : undefined)
|
|
728
|
+
return finishInstantiation(mod, inst, imports, needsWasi, opts, state)
|
|
729
|
+
}
|