jz 0.8.0 → 0.9.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 +56 -9
- package/bench/README.md +121 -50
- package/bench/bench.svg +27 -27
- package/cli.js +3 -1
- package/dist/interop.js +1 -1
- package/dist/jz.js +7541 -6178
- package/index.js +165 -74
- package/interop.js +189 -17
- package/jzify/arguments.js +32 -1
- package/jzify/async.js +409 -0
- package/jzify/classes.js +136 -18
- package/jzify/generators.js +639 -0
- package/jzify/hoist-vars.js +73 -1
- package/jzify/index.js +123 -4
- package/jzify/names.js +1 -0
- package/jzify/transform.js +239 -1
- package/layout.js +48 -3
- package/module/array.js +318 -43
- package/module/atomics.js +144 -0
- package/module/collection.js +1429 -155
- package/module/core.js +431 -40
- package/module/date.js +142 -120
- package/module/fs.js +144 -0
- package/module/function.js +6 -3
- package/module/index.js +4 -1
- package/module/json.js +270 -49
- package/module/math.js +1032 -145
- package/module/number.js +532 -163
- package/module/object.js +353 -95
- package/module/regex.js +157 -7
- package/module/schema.js +100 -2
- package/module/string.js +428 -93
- package/module/typedarray.js +264 -72
- package/module/web.js +36 -0
- package/package.json +5 -5
- package/src/abi/string.js +82 -11
- package/src/ast.js +11 -5
- package/src/autoload.js +28 -1
- package/src/bridge.js +7 -2
- package/src/compile/analyze-scans.js +22 -7
- package/src/compile/analyze.js +136 -30
- package/src/compile/dyn-closure-tables.js +277 -0
- package/src/compile/emit-assign.js +281 -15
- package/src/compile/emit.js +1055 -70
- package/src/compile/index.js +277 -29
- package/src/compile/infer.js +42 -4
- package/src/compile/inplace-store.js +329 -0
- package/src/compile/loop-recurrence.js +167 -0
- package/src/compile/narrow.js +555 -18
- package/src/compile/peel-stencil.js +5 -0
- package/src/compile/plan/index.js +45 -3
- package/src/compile/plan/inline.js +8 -1
- package/src/compile/plan/literals.js +39 -0
- package/src/compile/plan/scope.js +430 -23
- package/src/compile/program-facts.js +732 -38
- package/src/ctx.js +64 -9
- package/src/helper-counters.js +7 -1
- package/src/ir.js +113 -5
- package/src/kind-traits.js +66 -3
- package/src/kind.js +84 -12
- package/src/op-policy.js +7 -4
- package/src/optimize/index.js +1179 -704
- package/src/optimize/recurse.js +2 -2
- package/src/optimize/vectorize.js +982 -67
- package/src/prepare/index.js +809 -67
- package/src/prepare/math-kernel.js +331 -0
- package/src/prepare/pre-eval.js +714 -0
- package/src/snapshot.js +194 -0
- package/src/type.js +1170 -56
- package/src/wat/assemble.js +403 -65
- package/src/wat/codegen.js +74 -14
- package/transform.js +113 -4
- package/wasi.js +3 -0
package/src/snapshot.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-eval tier 3 — module-init snapshotting (V8-snapshot style).
|
|
3
|
+
*
|
|
4
|
+
* Runs the module's OWN top-level init (`__start`) once at COMPILE TIME and
|
|
5
|
+
* bakes the result into the artifact: the post-init heap image becomes the data
|
|
6
|
+
* segment, every mutable global's post-init value becomes its declared
|
|
7
|
+
* initializer, and `__start` is deleted. Instantiation then costs zero init —
|
|
8
|
+
* the init-built tables (watr's OPCODE/IMM dicts, interned atoms, schema
|
|
9
|
+
* registries, lookup tables) are pure data, and the init-vs-runtime storage
|
|
10
|
+
* bug class (durable-dangler landmines) loses its init half entirely.
|
|
11
|
+
*
|
|
12
|
+
* Soundness is proven DYNAMICALLY, not assumed:
|
|
13
|
+
* - the probe instance's env imports are throwing stubs — if init calls ANY
|
|
14
|
+
* host function, instantiation throws and the snapshot is declined (the
|
|
15
|
+
* module compiles exactly as before);
|
|
16
|
+
* - modules whose `__start` textually reads an IMPORTED GLOBAL are declined
|
|
17
|
+
* statically (a read can't be made to throw);
|
|
18
|
+
* - non-returning starts (the `__timer_loop` tail) and shared memories are
|
|
19
|
+
* declined statically.
|
|
20
|
+
* Determinism: everything `__start` can reach without the host is arithmetic
|
|
21
|
+
* over the module's own statics — Date/random/imports all live behind the env
|
|
22
|
+
* boundary the stubs seal.
|
|
23
|
+
*
|
|
24
|
+
* The capture round-trips exact bits: f64 globals are read back through a
|
|
25
|
+
* Float64Array view (a JS number preserves any payload as long as no
|
|
26
|
+
* arithmetic touches it) and re-emitted as `nan:0x…` literals when NaN-boxed,
|
|
27
|
+
* `-0`-aware decimal otherwise.
|
|
28
|
+
*
|
|
29
|
+
* Host-only by construction (needs WebAssembly instantiation of the probe):
|
|
30
|
+
* the self-host kernel never passes the flag; a typeof guard declines cleanly.
|
|
31
|
+
*/
|
|
32
|
+
import { i64Hex } from '../layout.js'
|
|
33
|
+
|
|
34
|
+
const findNode = (mod, pred) => mod.find(n => Array.isArray(n) && pred(n))
|
|
35
|
+
const findAll = (mod, pred) => mod.filter(n => Array.isArray(n) && pred(n))
|
|
36
|
+
|
|
37
|
+
// Per-byte escape for a watr data-segment string literal (mirror of
|
|
38
|
+
// compile/index.js's escBytes, over raw bytes instead of charCodes).
|
|
39
|
+
const escImage = (bytes) => {
|
|
40
|
+
let esc = ''
|
|
41
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
42
|
+
const c = bytes[i]
|
|
43
|
+
if (c >= 32 && c < 127 && c !== 34 && c !== 92) esc += String.fromCharCode(c)
|
|
44
|
+
else esc += '\\' + c.toString(16).padStart(2, '0')
|
|
45
|
+
}
|
|
46
|
+
return esc
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const f64BitsLit = (bits) => {
|
|
50
|
+
bits = BigInt.asUintN(64, bits)
|
|
51
|
+
if ((bits & 0x7FF0000000000000n) === 0x7FF0000000000000n && (bits & 0xFFFFFFFFFFFFFn) !== 0n)
|
|
52
|
+
return `nan:${i64Hex(bits)}` // NaN (boxed or plain): exact payload
|
|
53
|
+
const v = new Float64Array(new BigUint64Array([bits]).buffer)[0]
|
|
54
|
+
if (Object.is(v, -0)) return '-0'
|
|
55
|
+
if (v === Infinity) return 'inf'
|
|
56
|
+
if (v === -Infinity) return '-inf'
|
|
57
|
+
return String(v) // shortest round-trip = exact
|
|
58
|
+
}
|
|
59
|
+
const f32BitsLit = (bits) => {
|
|
60
|
+
const v = new Float32Array(new Int32Array([bits | 0]).buffer)[0]
|
|
61
|
+
if ((bits & 0x7F800000) === 0x7F800000 && (bits & 0x7FFFFF) !== 0)
|
|
62
|
+
return `nan:0x${(bits >>> 0).toString(16)}`
|
|
63
|
+
if (Object.is(v, -0)) return '-0'
|
|
64
|
+
if (v === Infinity) return 'inf'
|
|
65
|
+
if (v === -Infinity) return '-inf'
|
|
66
|
+
return String(v)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Mutates `module` (the final watr AST) in place. Returns true if snapshotted,
|
|
70
|
+
* false if declined (module untouched). */
|
|
71
|
+
export function snapshotInit(module, watrCompile) {
|
|
72
|
+
if (typeof WebAssembly === 'undefined') return false
|
|
73
|
+
if (!Array.isArray(module) || module[0] !== 'module') return false
|
|
74
|
+
|
|
75
|
+
// Init lives in a `(start)` directive (js host) or, under the WASI reactor
|
|
76
|
+
// convention, in a func exported as `_initialize` (host: 'wasi' never emits a
|
|
77
|
+
// start section — the p1 ABI forbids WASI calls there).
|
|
78
|
+
const startDir = findNode(module, n => n[0] === 'start')
|
|
79
|
+
const startFn = startDir
|
|
80
|
+
? findNode(module, n => n[0] === 'func' && n[1] === startDir[1])
|
|
81
|
+
: findNode(module, n => n[0] === 'func' && n.some(c => Array.isArray(c) && c[0] === 'export' && c[1] === '"_initialize"'))
|
|
82
|
+
if (!startFn) return false // nothing to snapshot
|
|
83
|
+
const startName = startFn[1]
|
|
84
|
+
const startText = JSON.stringify(startFn)
|
|
85
|
+
if (startText.includes('__timer_loop')) return false // non-returning start
|
|
86
|
+
|
|
87
|
+
// Shared/imported memory: the image belongs to the host — decline.
|
|
88
|
+
const memNode = findNode(module, n => n[0] === 'memory')
|
|
89
|
+
if (!memNode) return false
|
|
90
|
+
const imports = findAll(module, n => n[0] === 'import')
|
|
91
|
+
if (imports.some(n => JSON.stringify(n).includes('"memory"'))) return false
|
|
92
|
+
|
|
93
|
+
// Imported GLOBALS read during init can't be stub-detected — decline statically.
|
|
94
|
+
const importedGlobals = imports
|
|
95
|
+
.filter(n => n.some(c => Array.isArray(c) && c[0] === 'global'))
|
|
96
|
+
.map(n => n.find(c => Array.isArray(c) && c[0] === 'global')?.[1])
|
|
97
|
+
.filter(Boolean)
|
|
98
|
+
if (importedGlobals.some(g => startText.includes(`"${g}"`) || startText.includes(`${g}"`))) return false
|
|
99
|
+
|
|
100
|
+
const globals = findAll(module, n => n[0] === 'global' && typeof n[1] === 'string')
|
|
101
|
+
|
|
102
|
+
// ── probe: synthesize a bit-exact GETTER per global, encode, instantiate with
|
|
103
|
+
// sealing stubs. Direct `WebAssembly.Global.value` reads are useless for the
|
|
104
|
+
// f64 globals that matter most: the JS API canonicalizes NaN payloads at the
|
|
105
|
+
// boundary, wiping every NaN-boxed pointer. An exported func returning
|
|
106
|
+
// `i64.reinterpret_f64` crosses as BigInt — payload intact. f32 → i32 for the
|
|
107
|
+
// same reason; i32/i64 read direct.
|
|
108
|
+
const gtype = (g) => {
|
|
109
|
+
const m = g.find(c => Array.isArray(c) && c[0] === 'mut')
|
|
110
|
+
return m ? m[1] : g.find(c => typeof c === 'string' && c !== g[1] && ['i32', 'i64', 'f32', 'f64', 'v128'].includes(c))
|
|
111
|
+
}
|
|
112
|
+
const getters = []
|
|
113
|
+
for (const g of globals) {
|
|
114
|
+
const ty = gtype(g)
|
|
115
|
+
if (!ty || ty === 'v128') continue
|
|
116
|
+
const body = ty === 'f64' ? ['i64.reinterpret_f64', ['global.get', g[1]]]
|
|
117
|
+
: ty === 'f32' ? ['i32.reinterpret_f32', ['global.get', g[1]]]
|
|
118
|
+
: ['global.get', g[1]]
|
|
119
|
+
getters.push(['func', ['export', `"__snapg${g[1]}"`], ['result', ty === 'f64' ? 'i64' : ty === 'f32' ? 'i32' : ty], body])
|
|
120
|
+
}
|
|
121
|
+
module.push(...getters)
|
|
122
|
+
let inst
|
|
123
|
+
try {
|
|
124
|
+
const bytes = watrCompile(module)
|
|
125
|
+
const stubs = {}
|
|
126
|
+
for (const imp of imports) {
|
|
127
|
+
const fn = imp.find(c => Array.isArray(c) && c[0] === 'func')
|
|
128
|
+
const gl = imp.find(c => Array.isArray(c) && c[0] === 'global')
|
|
129
|
+
const modName = JSON.parse(imp[1]), name = JSON.parse(imp[2]) // keyed by import module: env AND wasi_snapshot_preview1
|
|
130
|
+
if (fn) (stubs[modName] ||= {})[name] = () => { throw new Error('__hermetic__') }
|
|
131
|
+
else if (gl) (stubs[modName] ||= {})[name] = new WebAssembly.Global({ value: 'i64' }, 0n)
|
|
132
|
+
}
|
|
133
|
+
inst = new WebAssembly.Instance(new WebAssembly.Module(bytes), stubs)
|
|
134
|
+
// Reactor form: init doesn't run at instantiation — drive it explicitly so the
|
|
135
|
+
// hermeticity stubs get their chance to throw (start-section form ran above).
|
|
136
|
+
if (!startDir) inst.exports._initialize()
|
|
137
|
+
} catch (e) {
|
|
138
|
+
module.length -= getters.length // restore, decline
|
|
139
|
+
return false
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── capture ──
|
|
143
|
+
const ex = inst.exports
|
|
144
|
+
const heapFn = ex['__snapg$__heap']
|
|
145
|
+
if (!heapFn) { module.length -= getters.length; return false }
|
|
146
|
+
const heapTop = Number(heapFn())
|
|
147
|
+
const image = new Uint8Array(ex.memory.buffer.slice(0, heapTop))
|
|
148
|
+
|
|
149
|
+
const captured = new Map()
|
|
150
|
+
for (const g of globals) {
|
|
151
|
+
const snap = ex[`__snapg${g[1]}`]
|
|
152
|
+
if (snap) captured.set(g[1], snap()) // i64→BigInt (bit-exact), i32→number
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── bake ──
|
|
156
|
+
module.length -= getters.length // drop probe getters
|
|
157
|
+
for (const g of globals) {
|
|
158
|
+
if (!captured.has(g[1])) continue
|
|
159
|
+
const mutIdx = g.findIndex(c => Array.isArray(c) && c[0] === 'mut')
|
|
160
|
+
if (mutIdx === -1) continue // immutable: init already exact
|
|
161
|
+
const ty = g[mutIdx][1]
|
|
162
|
+
const v = captured.get(g[1]) // f64 arrives as raw i64 BITS (BigInt)
|
|
163
|
+
const lit = ty === 'f64' ? f64BitsLit(BigInt(v))
|
|
164
|
+
: ty === 'f32' ? f32BitsLit(Number(v))
|
|
165
|
+
: ty === 'i64' ? String(BigInt(v))
|
|
166
|
+
: String(Number(v) | 0)
|
|
167
|
+
g[g.length - 1] = [`${ty}.const`, lit]
|
|
168
|
+
}
|
|
169
|
+
// data segment ← post-init image (contains the original statics as its prefix)
|
|
170
|
+
const dataNode = findNode(module, n => n[0] === 'data' && Array.isArray(n[1]) && n[1][0] === 'i32.const' && Number(n[1][1]) === 0)
|
|
171
|
+
const imageStr = '"' + escImage(image) + '"'
|
|
172
|
+
if (dataNode) dataNode[2] = imageStr
|
|
173
|
+
else module.push(['data', ['i32.const', '0'], imageStr])
|
|
174
|
+
// memory floor must cover the image
|
|
175
|
+
const pages = Math.max(1, Math.ceil(image.length / 65536))
|
|
176
|
+
for (let i = 1; i < memNode.length; i++)
|
|
177
|
+
if (typeof memNode[i] === 'string' && /^\d+$/.test(memNode[i])) { if (Number(memNode[i]) < pages) memNode[i] = String(pages); break }
|
|
178
|
+
else if (typeof memNode[i] === 'number') { if (memNode[i] < pages) memNode[i] = pages; break }
|
|
179
|
+
// __start is spent
|
|
180
|
+
if (startDir) module.splice(module.indexOf(startDir), 1)
|
|
181
|
+
module.splice(module.indexOf(startFn), 1)
|
|
182
|
+
// Reactor form: WASI command wrappers self-init via a void `call $__start` —
|
|
183
|
+
// init is baked into the image now, so those calls must go with the func.
|
|
184
|
+
const stripCalls = (n) => {
|
|
185
|
+
for (let i = n.length - 1; i >= 0; i--) {
|
|
186
|
+
const c = n[i]
|
|
187
|
+
if (!Array.isArray(c)) continue
|
|
188
|
+
if (c[0] === 'call' && c[1] === startName && c.length === 2) n.splice(i, 1)
|
|
189
|
+
else stripCalls(c)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
for (const f of findAll(module, n => n[0] === 'func')) stripCalls(f)
|
|
193
|
+
return true
|
|
194
|
+
}
|