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
|
@@ -44,62 +44,138 @@ import { invalidateProgramFactsCache } from '../program-facts.js'
|
|
|
44
44
|
// mixed ctors) clears the candidacy, keeping the read site polymorphic.
|
|
45
45
|
export const inferModuleLetTypes = (ast) => {
|
|
46
46
|
if (!ctx.scope.userGlobals) return
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
// `
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
47
|
+
// Build an assignment/alias graph over EVERY `=`/`let`/`const` binding in the
|
|
48
|
+
// program (globals and locals alike), then resolve each global's typed-array
|
|
49
|
+
// ctor by least-fixed-point. A single forward pass can't see the double-buffer
|
|
50
|
+
// swap idiom — `let tmp = a; a = b; b = tmp` assigns `a` from `b` and `b` from
|
|
51
|
+
// a local `tmp` that aliases `a`, so neither ref resolves until its sibling is
|
|
52
|
+
// already known. The fixpoint closes that cycle: `a`/`b` each anchor on their
|
|
53
|
+
// `new Float64Array(...)` decl, the alias edges carry the ctor around the loop,
|
|
54
|
+
// and they promote to VAL.TYPED. Without it the swap poisoned both globals and
|
|
55
|
+
// every `a[i]` read forked __str_idx/__typed_idx, every `+` forked __str_concat.
|
|
56
|
+
//
|
|
57
|
+
// Lattice (per name): null (no evidence) < ctor < MIXED. `bad` evidence (a non-
|
|
58
|
+
// typed, non-alias RHS — number, string, call, arithmetic, compound-assign) jumps
|
|
59
|
+
// straight to MIXED; conflicting ctors join to MIXED. We promote a global only
|
|
60
|
+
// when its fixed point is a single concrete ctor — sound: every assignment then
|
|
61
|
+
// provably yields that typed-array kind or nullish.
|
|
62
|
+
const MIXED = MIXED_CTORS
|
|
63
|
+
const defs = new Map() // name → { ctors:Set<string>, refs:Set<string>, bad:bool }
|
|
64
|
+
const getDef = (name) => {
|
|
65
|
+
let d = defs.get(name)
|
|
66
|
+
if (!d) defs.set(name, d = { ctors: new Set(), refs: new Set(), bad: false })
|
|
67
|
+
return d
|
|
68
|
+
}
|
|
60
69
|
|
|
61
70
|
const isNullishLit = (e) => e == null || e === 'undefined' || e === 'null'
|
|
62
71
|
|| (Array.isArray(e) && e[0] == null && (e[1] === undefined || e[1] === null))
|
|
63
72
|
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
// User-function names — a call to one is an alias edge to its return value
|
|
74
|
+
// (virtual node `@ret:<fn>`, populated from each `return`). Lets a global
|
|
75
|
+
// assigned `a = makeBuffer(n)` inherit makeBuffer's typed-array ctor without
|
|
76
|
+
// relying on the call being inlined (locals get it via inlining; globals,
|
|
77
|
+
// typed before inlining runs, did not). `@`/`:` can't occur in a JS identifier,
|
|
78
|
+
// so the virtual key never collides with a real binding.
|
|
79
|
+
const fnames = new Set()
|
|
80
|
+
for (const f of ctx.func.list) if (f.body && !f.raw && typeof f.name === 'string') fnames.add(f.name)
|
|
81
|
+
// Typed-array methods that preserve the receiver's element ctor: `.subarray`
|
|
82
|
+
// and `.slice` (same-kind view/copy), `.map` (same-kind, per propagateTyped).
|
|
83
|
+
const CTOR_PRESERVING = new Set(['subarray', 'slice', 'map'])
|
|
84
|
+
|
|
85
|
+
// Record one assignment `name = rhs` as evidence. Nullish contributes nothing
|
|
86
|
+
// (consistent with any typed-array value); a bare identifier, a ctor-preserving
|
|
87
|
+
// method on a name, or a call to a user function are alias edges; anything else
|
|
88
|
+
// that isn't a typed ctor poisons the name.
|
|
89
|
+
// Scope-qualified binding key. A module global is ONE node program-wide (bare
|
|
90
|
+
// name); a function-local is unique to its scope `sid`. Keying locals by bare
|
|
91
|
+
// name made a numeric counter `let s = 0` in one function poison a typed
|
|
92
|
+
// swap-temp `let s = a` in another — cascading MIXED into the double-buffer
|
|
93
|
+
// globals so every `a[i]` fell back to runtime __str_idx/__typed_idx dispatch
|
|
94
|
+
// (lbm: 3.9× slower than JS). `@ret:` virtual nodes stay bare (module-wide
|
|
95
|
+
// return-value anchors).
|
|
96
|
+
const key = (name, sid) => name[0] === '@' || ctx.scope.userGlobals.has(name) ? name : sid + '\x00' + name
|
|
97
|
+
|
|
98
|
+
const observe = (name, rhs, sid) => {
|
|
99
|
+
const d = getDef(key(name, sid))
|
|
66
100
|
if (isNullishLit(rhs)) return
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
101
|
+
const ctor = typedElemCtor(rhs) ?? ternaryCtorOfRhs(rhs)
|
|
102
|
+
if (ctor === MIXED) { d.bad = true; return }
|
|
103
|
+
if (ctor) { d.ctors.add(ctor); return }
|
|
104
|
+
if (typeof rhs === 'string') { d.refs.add(key(rhs, sid)); return }
|
|
105
|
+
if (Array.isArray(rhs) && rhs[0] === '()') {
|
|
106
|
+
const callee = rhs[1]
|
|
107
|
+
// `recv.subarray(...)` / `recv.slice(...)` / `recv.map(...)` → inherit recv's ctor.
|
|
108
|
+
if (Array.isArray(callee) && callee[0] === '.' && typeof callee[1] === 'string'
|
|
109
|
+
&& CTOR_PRESERVING.has(callee[2])) { d.refs.add(key(callee[1], sid)); return }
|
|
110
|
+
// `fn(...)` to a user function → inherit its return ctor.
|
|
111
|
+
if (typeof callee === 'string' && fnames.has(callee)) { d.refs.add('@ret:' + callee); return }
|
|
74
112
|
}
|
|
75
|
-
|
|
76
|
-
const prev = ctorOf.get(name)
|
|
77
|
-
if (prev && prev !== ctor) { invalid.add(name); return }
|
|
78
|
-
ctorOf.set(name, ctor)
|
|
113
|
+
d.bad = true
|
|
79
114
|
}
|
|
80
115
|
|
|
81
|
-
|
|
116
|
+
// Scope-aware walk. Every `=>` opens a fresh scope so same-named locals across
|
|
117
|
+
// functions (and sibling closures) stay distinct. A function bound to a name
|
|
118
|
+
// descends in a name-stable scope (`fn\0name`) so the ast descent and the
|
|
119
|
+
// func.list sweep below visit it identically (idempotent), and so `return`
|
|
120
|
+
// exprs anchor on `@ret:name`.
|
|
121
|
+
let sidc = 0
|
|
122
|
+
const walk = (node, sid, retFn) => {
|
|
82
123
|
if (!Array.isArray(node)) return
|
|
83
124
|
const op = node[0]
|
|
84
|
-
if (op === '
|
|
125
|
+
if (op === '=>') { walk(node[2], 's' + (++sidc), null); return }
|
|
126
|
+
if (op === 'return' && retFn != null) observe('@ret:' + retFn, node[1], sid)
|
|
127
|
+
const assign = (name, rhs) => {
|
|
128
|
+
if (Array.isArray(rhs) && rhs[0] === '=>') { observe(name, rhs, sid); enterFn(rhs[2], name); return }
|
|
129
|
+
observe(name, rhs, sid); walk(rhs, sid, retFn)
|
|
130
|
+
}
|
|
131
|
+
if (op === '=' && typeof node[1] === 'string') return assign(node[1], node[2])
|
|
85
132
|
if ((op === 'let' || op === 'const') && node.length > 1) {
|
|
86
133
|
for (let i = 1; i < node.length; i++) {
|
|
87
134
|
const d = node[i]
|
|
88
|
-
if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string'
|
|
89
|
-
|
|
135
|
+
if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string') assign(d[1], d[2])
|
|
136
|
+
else walk(d, sid, retFn)
|
|
90
137
|
}
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
// Compound-assigns (`+=`, `++`, …) can't preserve a typed-array kind — poison.
|
|
141
|
+
if (ASSIGN_OPS.has(op) && typeof node[1] === 'string') { getDef(key(node[1], sid)).bad = true; walk(node[2], sid, retFn); return }
|
|
142
|
+
for (let i = 1; i < node.length; i++) walk(node[i], sid, retFn)
|
|
143
|
+
}
|
|
144
|
+
// Descend into a function body anchored on `@ret:fn`. An arrow expr-body IS the
|
|
145
|
+
// implicit return (`(n) => new Float64Array(n)`); a `{}` block uses explicit
|
|
146
|
+
// `return` nodes (captured in walk). Without the implicit-return capture, a
|
|
147
|
+
// global assigned `a = mk(n)` from an expr-body fn never inherited mk's ctor.
|
|
148
|
+
const enterFn = (body, fn) => {
|
|
149
|
+
if (Array.isArray(body) && body[0] !== '{}') observe('@ret:' + fn, body, 'fn\x00' + fn)
|
|
150
|
+
walk(body, 'fn\x00' + fn, fn)
|
|
151
|
+
}
|
|
152
|
+
walk(ast, 'mod', null)
|
|
153
|
+
// Defensive sweep: cover any func.list body not reachable by descent from `ast`
|
|
154
|
+
// (hoisted / submodule). Name-stable scope keeps it idempotent with the descent.
|
|
155
|
+
for (const f of ctx.func.list) if (f.body && !f.raw) enterFn(f.body, f.name)
|
|
156
|
+
|
|
157
|
+
// Least-fixed-point over the alias graph. join: null is bottom, MIXED is top.
|
|
158
|
+
const join = (a, b) => a === MIXED || b === MIXED ? MIXED : a == null ? b : b == null ? a : a === b ? a : MIXED
|
|
159
|
+
const state = new Map() // name → null | ctor | MIXED
|
|
160
|
+
// A ref to a name with no tracked defs resolves via an already-known typed
|
|
161
|
+
// global (const typed array / earlier-recorded rep); otherwise it's opaque → MIXED.
|
|
162
|
+
const refState = (r) => defs.has(r) ? (state.get(r) ?? null)
|
|
163
|
+
: ctx.scope.globalValTypes?.get(r) === VAL.TYPED ? (ctx.scope.globalTypedElem?.get(r) ?? MIXED)
|
|
164
|
+
: MIXED
|
|
165
|
+
let changed = true
|
|
166
|
+
while (changed) {
|
|
167
|
+
changed = false
|
|
168
|
+
for (const [name, d] of defs) {
|
|
169
|
+
let cur = d.bad ? MIXED : null
|
|
170
|
+
if (cur !== MIXED) for (const c of d.ctors) cur = join(cur, c)
|
|
171
|
+
if (cur !== MIXED) for (const r of d.refs) cur = join(cur, refState(r))
|
|
172
|
+
if (cur !== (state.get(name) ?? null)) { state.set(name, cur); changed = true }
|
|
91
173
|
}
|
|
92
|
-
// Compound-assigns (`+=`, etc.) to a typed-array binding can't preserve
|
|
93
|
-
// the typed-array kind — invalidate.
|
|
94
|
-
if (ASSIGN_OPS.has(op) && op !== '=' && typeof node[1] === 'string' && ctorOf.has(node[1]))
|
|
95
|
-
invalid.add(node[1])
|
|
96
|
-
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
97
174
|
}
|
|
98
|
-
walk(ast)
|
|
99
|
-
for (const f of ctx.func.list) if (f.body && !f.raw) walk(f.body)
|
|
100
175
|
|
|
101
|
-
for (const
|
|
102
|
-
|
|
176
|
+
for (const name of ctx.scope.userGlobals) {
|
|
177
|
+
const ctor = state.get(name)
|
|
178
|
+
if (!ctor || ctor === MIXED) continue
|
|
103
179
|
if (ctx.scope.globalValTypes?.get(name) === VAL.TYPED) continue
|
|
104
180
|
;(ctx.scope.globalValTypes ||= new Map()).set(name, VAL.TYPED)
|
|
105
181
|
;(ctx.scope.globalTypedElem ||= new Map()).set(name, ctor)
|
|
@@ -23,6 +23,24 @@ export function observeNodeFacts(node, f) {
|
|
|
23
23
|
if (PROP_WRITE_OPS.has(op) && Array.isArray(args[0]) &&
|
|
24
24
|
(args[0][0] === '.' || args[0][0] === '?.') && typeof args[0][2] === 'string')
|
|
25
25
|
f.writtenProps.add(args[0][2])
|
|
26
|
+
// Computed-key WRITES (`o[k]=v`, `o[k]+=v`, `o[k]++`) are the ONLY operations
|
|
27
|
+
// that add ENUMERABLE keys beyond the static schema — computed reads and dot-adds
|
|
28
|
+
// (`o.b=2`) do not enumerate in jz. Tracked separately from `dynVars` (which also
|
|
29
|
+
// counts reads) so for-in / Object.keys key pooling can trust the static schema
|
|
30
|
+
// for a receiver that is only computed-READ. (`isLiteralStr` excludes literal
|
|
31
|
+
// string keys, which place in fixed schema slots.)
|
|
32
|
+
if (PROP_WRITE_OPS.has(op) && Array.isArray(args[0]) && args[0][0] === '[]') {
|
|
33
|
+
const [, wobj, widx] = args[0]
|
|
34
|
+
// Flag the ROOT array var. `o[k]=v` → o; a NESTED write `o[i][j]=v` mutates an
|
|
35
|
+
// element of o, so walk the receiver chain to its root identifier and flag that
|
|
36
|
+
// too — else o's recorded (nested) element types would be wrongly trusted at a
|
|
37
|
+
// later `o[i][j]` read. Strictly more conservative for every dynWriteVars consumer.
|
|
38
|
+
if (!isLiteralStr(widx)) {
|
|
39
|
+
let root = wobj
|
|
40
|
+
while (Array.isArray(root) && root[0] === '[]') root = root[1]
|
|
41
|
+
if (typeof root === 'string') f.dynWriteVars?.add(root)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
26
44
|
if (op === '[]') {
|
|
27
45
|
const [obj, idx] = args
|
|
28
46
|
if (!isLiteralStr(idx)) { f.anyDyn = true; if (typeof obj === 'string') f.dynVars.add(obj) }
|
|
@@ -68,7 +86,7 @@ export function invalidateProgramFactsCache(...roots) {
|
|
|
68
86
|
|
|
69
87
|
function emptyWalkFacts() {
|
|
70
88
|
return {
|
|
71
|
-
dynVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
|
|
89
|
+
dynVars: new Set(), dynWriteVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
|
|
72
90
|
maxDef: 0, maxCall: 0, hasRest: false, hasSpread: false,
|
|
73
91
|
propMap: new Map(), valueUsed: new Set(), callSites: [],
|
|
74
92
|
writtenProps: new Set(),
|
|
@@ -78,6 +96,7 @@ function emptyWalkFacts() {
|
|
|
78
96
|
function mergeWalkFacts(into, from) {
|
|
79
97
|
if (from.anyDyn) into.anyDyn = true
|
|
80
98
|
for (const v of from.dynVars) into.dynVars.add(v)
|
|
99
|
+
for (const v of from.dynWriteVars) into.dynWriteVars.add(v)
|
|
81
100
|
if (from.hasSchemaLiterals) into.hasSchemaLiterals = true
|
|
82
101
|
if (from.maxDef > into.maxDef) into.maxDef = from.maxDef
|
|
83
102
|
if (from.maxCall > into.maxCall) into.maxCall = from.maxCall
|
|
@@ -234,7 +253,7 @@ export function collectProgramFacts(ast) {
|
|
|
234
253
|
// ctx — a mutated prop name anywhere disqualifies sharing a static instance.
|
|
235
254
|
ctx.module.writtenProps = f.writtenProps
|
|
236
255
|
return {
|
|
237
|
-
dynVars: f.dynVars, anyDyn: f.anyDyn, propMap, valueUsed, callSites,
|
|
256
|
+
dynVars: f.dynVars, dynWriteVars: f.dynWriteVars, anyDyn: f.anyDyn, propMap, valueUsed, callSites,
|
|
238
257
|
maxDef: f.maxDef, maxCall: f.maxCall, hasRest: f.hasRest, hasSpread: f.hasSpread,
|
|
239
258
|
paramReps, hasSchemaLiterals: f.hasSchemaLiterals, writtenProps: f.writtenProps,
|
|
240
259
|
}
|
package/src/ctx.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { makeAbi } from './abi/index.js'
|
|
11
|
-
export { HEAP, LAYOUT, PTR, ATOM, nanPrefixHex, atomNanHex, ssoBitI64Hex, sliceBitI64Hex, ptrNanHex, ptrBoxPrefixBigInt, encodePtrHi, decodePtrType, decodePtrAux, ATOM_HI, oobNanLiteral, oobNanIR, followForwardingWat } from '../layout.js'
|
|
11
|
+
export { HEAP, LAYOUT, PTR, ATOM, FORWARDING_MASK, nanPrefixHex, atomNanHex, ssoBitI64Hex, sliceBitI64Hex, ptrNanHex, ptrBoxPrefixBigInt, encodePtrHi, decodePtrType, decodePtrAux, ATOM_HI, oobNanLiteral, oobNanIR, followForwardingWat } from '../layout.js'
|
|
12
12
|
|
|
13
13
|
// === Carrier layout ===
|
|
14
14
|
// Canonical bit layout lives in layout.js (compiler-free). Re-exported above for
|
|
@@ -38,11 +38,11 @@ export { HEAP, LAYOUT, PTR, ATOM, nanPrefixHex, atomNanHex, ssoBitI64Hex, sliceB
|
|
|
38
38
|
// |-----------|----------|---------------------------------|---------------------------|
|
|
39
39
|
// | core | compile | reset, modules, inc(), emit* | emit, compile, modules |
|
|
40
40
|
// | module | compile | prepare, index.js | prepare, compile, emit |
|
|
41
|
-
// | scope | compile | analyze, compile, plan, modules | compile, emit
|
|
42
|
-
// | func | function | compile, narrow
|
|
41
|
+
// | scope | compile | analyze, compile, plan, modules, assemble | compile, emit |
|
|
42
|
+
// | func | function | compile, narrow, assemble | emit, modules |
|
|
43
43
|
// | types | function | analyze, plan | emit, modules |
|
|
44
44
|
// | schema | compile | prepare, analyze, compile | prepare, analyze, emit |
|
|
45
|
-
// | closure | init | modules (fn plugin)
|
|
45
|
+
// | closure | init | modules (fn plugin), plan, emit | emit, compile |
|
|
46
46
|
// | runtime | compile | emit, modules | emit, compile |
|
|
47
47
|
// | memory | compile | index.js | compile |
|
|
48
48
|
// | error | compile | prepare, compile, emit | err() |
|
|
@@ -63,6 +63,11 @@ export { HEAP, LAYOUT, PTR, ATOM, nanPrefixHex, atomNanHex, ssoBitI64Hex, sliceB
|
|
|
63
63
|
// narrow-phase writers: narrowSignatures (under plan) temporarily swaps
|
|
64
64
|
// ctx.func.{localReps, locals, current} per-function with save/restore
|
|
65
65
|
// so per-call-site signature inference sees the right scope.
|
|
66
|
+
// assemble-phase writers: buildStartFn (wat/assemble.js) re-owns the ctx.func frame
|
|
67
|
+
// (locals/stack/refinements/…) to emit the module-init `start` fn, save/restoring
|
|
68
|
+
// around it; the data pass also const-folds ctx.scope.globals (mut→false) and
|
|
69
|
+
// declares the __heap* globals. emit seeds ctx.closure.{paramTypes,paramTypedCtors}
|
|
70
|
+
// at direct-call sites (read by emitClosureBody); plan sets ctx.closure.{floor,width}.
|
|
66
71
|
export const ctx = {
|
|
67
72
|
core: {}, // emitter table + stdlib registry (seeded by reset + modules)
|
|
68
73
|
module: {}, // module graph: imports, resolved sources, module-init blocks
|
|
@@ -92,9 +97,14 @@ export const ctx = {
|
|
|
92
97
|
// the defaults; see reset() for the field list and who flips each.
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
/** Create a child scope via shallow flat copy
|
|
100
|
+
/** Create a child scope via shallow flat copy with NO prototype chain. Critical:
|
|
101
|
+
* `{ ...parent }` would inherit Object.prototype in V8 (jz.js), so a name-keyed lookup
|
|
102
|
+
* like `chain['valueOf']`/`emit['toString']` returns the inherited method instead of
|
|
103
|
+
* undefined — corrupting resolution of any identifier named like an Object method. The
|
|
104
|
+
* kernel's jz objects are already prototype-less, so this was a jz.js-ONLY footgun. A
|
|
105
|
+
* prototype-less dict (Object.create(null) + assign) is correct in both engines.
|
|
96
106
|
* Mutations to the child do not affect the parent; lookups work via direct property access. */
|
|
97
|
-
export const derive = (parent) => (
|
|
107
|
+
export const derive = (parent) => Object.assign(Object.create(null), parent)
|
|
98
108
|
|
|
99
109
|
/** Include stdlib names for emission. */
|
|
100
110
|
export const inc = (...names) => names.forEach(n => ctx.core.includes.add(n))
|
|
@@ -133,29 +143,63 @@ export const emitter = (deps, fn) => {
|
|
|
133
143
|
* carry it as `.argc`; bare ones expose it as the function's own `.length`. */
|
|
134
144
|
export const emitArity = (h) => h?.argc ?? h?.length
|
|
135
145
|
|
|
136
|
-
/**
|
|
137
|
-
* property is *read* (`re.source`, `m.size`), so the `.`-read
|
|
138
|
-
* Untagged handlers are methods: a bare read of
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
|
|
146
|
+
/** Register `fn` as a property-GETTER emitter for `key` — it yields a value when
|
|
147
|
+
* the property is *read* (`re.source`, `m.size`, `a.byteOffset`), so the `.`-read
|
|
148
|
+
* path fires it. (Untagged `ctx.core.emit` handlers are methods: a bare read of
|
|
149
|
+
* `m.values` must NOT invoke them — that would materialize a view — they fire only
|
|
150
|
+
* from the method-call path.) Getter-ness lives in `ctx.core.getters` (a plain Set),
|
|
151
|
+
* NOT as a flag on the emitter closure: the self-host kernel can't reliably read a
|
|
152
|
+
* dynamic property off a closure returned via a dynamic-key lookup, so a closure tag
|
|
153
|
+
* silently read `undefined` and every getter fell through to `__dyn_get`. A Set
|
|
154
|
+
* key-lookup is kernel-safe. Dispatch (module/core.js) checks `ctx.core.getters.has(key)`. */
|
|
155
|
+
export const registerGetter = (key, fn) => {
|
|
156
|
+
ctx.core.emit[key] = fn
|
|
157
|
+
ctx.core.getters.add(key)
|
|
158
|
+
}
|
|
142
159
|
|
|
143
160
|
/** Expand ctx.core.includes transitively via ctx.core.stdlibDeps. Call before WASM assembly.
|
|
144
161
|
* Each module co-locates its own deps with its stdlib registrations at init time. */
|
|
145
162
|
export function resolveIncludes() {
|
|
146
163
|
const graph = ctx.core.stdlibDeps
|
|
164
|
+
const stdlib = ctx.core.stdlib
|
|
165
|
+
// Auto-derived deps: a stdlib template that calls `$__foo` (a registered stdlib
|
|
166
|
+
// func) depends on it, whether or not the hand-maintained `deps()` list says so.
|
|
167
|
+
// Scanning the *realized* template keeps the graph honest, so a missing manual
|
|
168
|
+
// entry can't silently drop a transitively-needed helper (the bug class the old
|
|
169
|
+
// blanket `inc('__mkptr','__alloc')` masked). Factory templates are realized
|
|
170
|
+
// (called) so feature-gated branches — `${hasExt ? '(call $__ext_prop …)' : ''}`
|
|
171
|
+
// — resolve before scanning; reading raw source would over-pull the dead branch.
|
|
172
|
+
// jz's templates are pure string builders, so realizing here (and again at
|
|
173
|
+
// emission) is side-effect-free. A `$__foo` naming a global (not a stdlib func)
|
|
174
|
+
// is skipped. Realization can fail if called before its inputs are ready — then
|
|
175
|
+
// we return nothing *without caching*, so a later pass retries. Memoized per compile.
|
|
176
|
+
const autoCache = ctx.core._autoDeps ??= new Map()
|
|
177
|
+
const autoDepsOf = (name) => {
|
|
178
|
+
let found = autoCache.get(name)
|
|
179
|
+
if (found !== undefined) return found
|
|
180
|
+
const v = stdlib[name]
|
|
181
|
+
let text
|
|
182
|
+
if (typeof v === 'string') text = v
|
|
183
|
+
else if (typeof v === 'function') { try { text = v() } catch { return [] } }
|
|
184
|
+
if (typeof text !== 'string') return (autoCache.set(name, []), [])
|
|
185
|
+
found = []
|
|
186
|
+
const seen = new Set()
|
|
187
|
+
for (const m of text.matchAll(/\$(__[A-Za-z0-9_]+)/g)) {
|
|
188
|
+
const d = m[1]
|
|
189
|
+
if (d !== name && stdlib[d] && !seen.has(d)) { seen.add(d); found.push(d) }
|
|
190
|
+
}
|
|
191
|
+
autoCache.set(name, found)
|
|
192
|
+
return found
|
|
193
|
+
}
|
|
147
194
|
let changed = true
|
|
148
195
|
while (changed) {
|
|
149
196
|
changed = false
|
|
150
197
|
for (const name of [...ctx.core.includes]) {
|
|
151
198
|
const entry = graph[name]
|
|
152
199
|
const deps = typeof entry === 'function' ? entry() : entry
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
changed = true
|
|
157
|
-
}
|
|
158
|
-
}
|
|
200
|
+
const add = (dep) => { if (!ctx.core.includes.has(dep)) { ctx.core.includes.add(dep); changed = true } }
|
|
201
|
+
if (deps) for (const dep of deps) add(dep)
|
|
202
|
+
for (const dep of autoDepsOf(name)) add(dep)
|
|
159
203
|
}
|
|
160
204
|
}
|
|
161
205
|
}
|
|
@@ -180,6 +224,13 @@ export function reset(proto, globals, bridge) {
|
|
|
180
224
|
// `(import "env" "name" (global $name i64))` at assembly. Same
|
|
181
225
|
// usage-gated pattern as jsstring — emit records, assembly owns
|
|
182
226
|
// the ctx.module.imports write.
|
|
227
|
+
getters: new Set(), // keys of emit entries that are property getters — the
|
|
228
|
+
// kernel-safe authority for getter dispatch (a closure-attached
|
|
229
|
+
// flag was unreadable in the self-host kernel after a dynamic-key
|
|
230
|
+
// lookup, so every getter silently fell through to __dyn_get).
|
|
231
|
+
// MUST remain last: adding fields before stdlib/stdlibDeps/… shifts
|
|
232
|
+
// their slot indices and breaks the self-host compiled kernel's reads.
|
|
233
|
+
// Populated by registerGetter(); checked by module/core.js dispatch.
|
|
183
234
|
}
|
|
184
235
|
|
|
185
236
|
|
|
@@ -207,6 +258,10 @@ export function reset(proto, globals, bridge) {
|
|
|
207
258
|
globalTypedElem: null,
|
|
208
259
|
globalReps: null, // Map<name, ValueRep> — module-level pointer reps (TYPED const globals stored as raw i32 offset, etc.)
|
|
209
260
|
consts: null,
|
|
261
|
+
constInts: null, // Map<name, int> — module const folded to an integer literal (prepare/plan seed; static/ir read)
|
|
262
|
+
constStrs: null, // Map<name, string> — module const folded to a string literal
|
|
263
|
+
shapeStrs: null, // Map<expr, string> / shapeStrArrays: Map<name, string[]> — schema-shape string folds
|
|
264
|
+
shapeStrArrays: null,
|
|
210
265
|
}
|
|
211
266
|
|
|
212
267
|
ctx.func = {
|
|
@@ -214,7 +269,7 @@ export function reset(proto, globals, bridge) {
|
|
|
214
269
|
names: new Set(), // Set<string> — known func names (list + imported funcs); populated at compile() start
|
|
215
270
|
map: new Map(), // Map<string, func> — name → func entry; populated at compile() start
|
|
216
271
|
multiProp: new Set(), // Set<"obj.prop"> — function-properties assigned >1× (wrapper composition); suppresses the static fn.prop() direct call
|
|
217
|
-
exports:
|
|
272
|
+
exports: Object.create(null), // name-keyed: prototype-less (see derive) — `export let valueOf` must not hit Object.prototype
|
|
218
273
|
current: null,
|
|
219
274
|
locals: new Map(),
|
|
220
275
|
localReps: null,
|
|
@@ -241,6 +296,7 @@ export function reset(proto, globals, bridge) {
|
|
|
241
296
|
ctx.types = {
|
|
242
297
|
typedElem: null,
|
|
243
298
|
dynKeyVars: null,
|
|
299
|
+
dynWriteVars: null,
|
|
244
300
|
anyDynKey: false,
|
|
245
301
|
}
|
|
246
302
|
|
|
@@ -296,6 +352,8 @@ export function reset(proto, globals, bridge) {
|
|
|
296
352
|
minArgc: null, // Map<closureBodyName, number> — fewest args any direct call passed.
|
|
297
353
|
// A slot at index ≥ minArgc is omitted by some call (→ may be undefined),
|
|
298
354
|
// so it must NOT be typed NUMBER, else `x === undefined` mis-folds to false.
|
|
355
|
+
floor: null, // min closure-table arity (modules: fn/timer/typedarray/array; read in plan). null ⇒ 0.
|
|
356
|
+
width: null, // closure call/make signature width (plan/scope sets; emit/assemble read). null ⇒ MAX_CLOSURE_ARITY.
|
|
299
357
|
}
|
|
300
358
|
|
|
301
359
|
ctx.runtime = {
|
|
@@ -316,6 +374,7 @@ export function reset(proto, globals, bridge) {
|
|
|
316
374
|
ctx.memory = {
|
|
317
375
|
shared: false,
|
|
318
376
|
pages: 0,
|
|
377
|
+
max: 0, // 0 = unbounded; >0 emits a maximum on the memory type (cap growth)
|
|
319
378
|
}
|
|
320
379
|
|
|
321
380
|
ctx.error = {
|
|
@@ -341,6 +400,15 @@ export function reset(proto, globals, bridge) {
|
|
|
341
400
|
inspect: false, // when true, compile() additionally populates ctx.inspect with the inferred
|
|
342
401
|
// per-function signatures, locals, and JSON shapes — readable by editor
|
|
343
402
|
// hosts for inlay hints / hover types without re-running the analyzer.
|
|
403
|
+
helperCounters: false, // internal profiling mode: export mutable i64 counters for selected
|
|
404
|
+
// runtime helpers and instrument their entry blocks. Build-time opt-in
|
|
405
|
+
// only; normal output is byte-identical and pays no counter cost.
|
|
406
|
+
helperCallsites: false, // profiling-only: export mutable i64 counters for selected runtime
|
|
407
|
+
// helper callsites after optimization, so hot helpers can be traced
|
|
408
|
+
// back to the compiled function that calls them.
|
|
409
|
+
loopXformId: 0, // monotonic id for the per-function loop transforms' generated locals
|
|
410
|
+
// (loop-model freshLoopId). Per-compile (reset here), not a module-global —
|
|
411
|
+
// so compile(P) is deterministic regardless of prior compiles in the process.
|
|
344
412
|
}
|
|
345
413
|
|
|
346
414
|
// Inspection sink. Populated by compile() only when transform.inspect is true.
|
|
@@ -440,6 +508,15 @@ export function warn(code, message, meta = {}, loc = null) {
|
|
|
440
508
|
ctx.warnings.sink.entries.push(entry)
|
|
441
509
|
}
|
|
442
510
|
|
|
511
|
+
/** Advise that an emit site fell back to generic runtime dispatch (the slow,
|
|
512
|
+
* un-inferred path). Called from the actual emission point so it fires only when
|
|
513
|
+
* inference/optimization truly couldn't fold it — never a false positive on a
|
|
514
|
+
* case that vectorized/unrolled/slot-folded. `ctx.error.loc` is the current AST
|
|
515
|
+
* node's byte offset (kept up to date by the emit walk), giving line/column. */
|
|
516
|
+
export function warnDeopt(code, message) {
|
|
517
|
+
warn(code, message, { fn: ctx.func.current?.name }, ctx.error.loc)
|
|
518
|
+
}
|
|
519
|
+
|
|
443
520
|
/** Throw with source location context. */
|
|
444
521
|
export function err(msg, cause) {
|
|
445
522
|
let detail = msg
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { ctx, declGlobal, inc } from './ctx.js'
|
|
2
|
+
import { findBodyStart } from './ir.js'
|
|
3
|
+
|
|
4
|
+
export const HELPER_COUNTERS = [
|
|
5
|
+
['__eq', 'eq'],
|
|
6
|
+
['__same_value_zero', 'same_value_zero'],
|
|
7
|
+
['__is_truthy', 'is_truthy'],
|
|
8
|
+
['__ptr_type', 'ptr_type'],
|
|
9
|
+
['__ptr_offset', 'ptr_offset'],
|
|
10
|
+
['__len', 'len'],
|
|
11
|
+
['__length', 'length'],
|
|
12
|
+
['__typed_idx', 'typed_idx'],
|
|
13
|
+
['__str_len', 'str_len'],
|
|
14
|
+
['__str_eq', 'str_eq'],
|
|
15
|
+
['__str_eq_cold', 'str_eq_cold'],
|
|
16
|
+
['__str_hash', 'str_hash'],
|
|
17
|
+
['__map_hash', 'map_hash'],
|
|
18
|
+
['__hash_get_local', 'hash_get_local'],
|
|
19
|
+
['__hash_set_local', 'hash_set_local'],
|
|
20
|
+
['__ihash_get_local', 'ihash_get_local'],
|
|
21
|
+
['__ihash_set_local', 'ihash_set_local'],
|
|
22
|
+
['__dyn_get', 'dyn_get'],
|
|
23
|
+
['__dyn_get_t', 'dyn_get_t'],
|
|
24
|
+
['__dyn_get_t_h', 'dyn_get_t_h'],
|
|
25
|
+
['__dyn_set', 'dyn_set'],
|
|
26
|
+
['__arr_grow', 'arr_grow'],
|
|
27
|
+
['__arr_grow_known', 'arr_grow_known'],
|
|
28
|
+
['__arr_push1', 'arr_push1'],
|
|
29
|
+
['__arr_shift', 'arr_shift'],
|
|
30
|
+
['__alloc', 'alloc'],
|
|
31
|
+
['__alloc_hdr', 'alloc_hdr'],
|
|
32
|
+
['__alloc_hdr_n', 'alloc_hdr_n'],
|
|
33
|
+
['__memgrow', 'memgrow'],
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const COUNTER_BY_HELPER = new Map(HELPER_COUNTERS.map(([helper, label]) => [helper, `__hc_${label}`]))
|
|
37
|
+
const LABEL_BY_WAT_HELPER = new Map(HELPER_COUNTERS.map(([helper, label]) => [`$${helper}`, label]))
|
|
38
|
+
|
|
39
|
+
export const HELPER_SITE_PREFIX = '__hcs_'
|
|
40
|
+
|
|
41
|
+
export const helperCounterName = helper => COUNTER_BY_HELPER.get(helper)
|
|
42
|
+
|
|
43
|
+
export function installHelperCounters() {
|
|
44
|
+
if (!ctx.transform.helperCounters) return
|
|
45
|
+
for (const counter of COUNTER_BY_HELPER.values()) {
|
|
46
|
+
if (!ctx.scope.globals.has(counter)) declGlobal(counter, 'i64', 0, { export: counter })
|
|
47
|
+
}
|
|
48
|
+
ctx.core.stdlib.__helper_counts_reset = `(func $__helper_counts_reset (export "__helper_counts_reset")
|
|
49
|
+
${[...COUNTER_BY_HELPER.values()].map(counter => ` (global.set $${counter} (i64.const 0))`).join('\n')})`
|
|
50
|
+
inc('__helper_counts_reset')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Bump the helper's counter once on entry. NOTE the semantics: this counts FUNCTION
|
|
54
|
+
// ENTRIES at runtime — a call site that jz inlined or specialized away (fusedRewrite,
|
|
55
|
+
// specializeMkptr/specializePtrBase, …) never enters the function and is NOT counted. So
|
|
56
|
+
// the numbers are a relative ranking / lower bound for picking hot helpers, not exact
|
|
57
|
+
// operation counts. Good enough to choose targets; don't read them as call totals.
|
|
58
|
+
export function instrumentHelperCounter(helper, fn) {
|
|
59
|
+
const counter = ctx.transform.helperCounters && helperCounterName(helper)
|
|
60
|
+
if (!counter || !Array.isArray(fn) || fn[0] !== 'func') return fn
|
|
61
|
+
fn.splice(findBodyStart(fn), 0,
|
|
62
|
+
['global.set', `$${counter}`, ['i64.add', ['global.get', `$${counter}`], ['i64.const', 1]]])
|
|
63
|
+
return fn
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const safeExportPart = name => String(name || 'anon')
|
|
67
|
+
.replace(/^\$/, '')
|
|
68
|
+
.replace(/[^A-Za-z0-9_.-]+/g, '_')
|
|
69
|
+
.slice(0, 80) || 'anon'
|
|
70
|
+
|
|
71
|
+
const helperSiteFilter = () => {
|
|
72
|
+
const opt = ctx.transform.helperCallsites
|
|
73
|
+
if (opt === true) return null
|
|
74
|
+
const raw = Array.isArray(opt) ? opt : String(opt || '').split(',')
|
|
75
|
+
const labels = raw.map(s => String(s).trim()).filter(Boolean).map(s => s.replace(/^\$?__/, ''))
|
|
76
|
+
return labels.length ? new Set(labels) : null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const funcResults = fn => {
|
|
80
|
+
const out = []
|
|
81
|
+
if (!Array.isArray(fn) || fn[0] !== 'func') return out
|
|
82
|
+
for (let i = 2; i < fn.length; i++) {
|
|
83
|
+
const n = fn[i]
|
|
84
|
+
if (Array.isArray(n) && n[0] === 'result') out.push(...n.slice(1))
|
|
85
|
+
}
|
|
86
|
+
return out
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const bumpCounter = counter =>
|
|
90
|
+
['global.set', `$${counter}`, ['i64.add', ['global.get', `$${counter}`], ['i64.const', 1]]]
|
|
91
|
+
|
|
92
|
+
// Profiling-only helper-callsite counters. Unlike instrumentHelperCounter(), this
|
|
93
|
+
// answers "which compiled function executed the helper call?" by wrapping each
|
|
94
|
+
// final `(call $__helper ...)` with a tiny counter block:
|
|
95
|
+
// (block (result T) (global.set $__hcs_N ...) (call $__helper ...))
|
|
96
|
+
//
|
|
97
|
+
// This intentionally runs after whole-module optimization. The profile should
|
|
98
|
+
// observe final codegen, while production output remains byte-identical because
|
|
99
|
+
// ctx.transform.helperCallsites is build-time opt-in.
|
|
100
|
+
export function instrumentHelperCallsites(funcs) {
|
|
101
|
+
if (!ctx.transform.helperCallsites) return 0
|
|
102
|
+
const only = helperSiteFilter()
|
|
103
|
+
|
|
104
|
+
const resultsByName = new Map()
|
|
105
|
+
for (const fn of funcs) {
|
|
106
|
+
if (Array.isArray(fn) && fn[0] === 'func' && typeof fn[1] === 'string')
|
|
107
|
+
resultsByName.set(fn[1], funcResults(fn))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let id = 0
|
|
111
|
+
const wrap = (node, owner) => {
|
|
112
|
+
if (!Array.isArray(node)) return node
|
|
113
|
+
for (let i = 1; i < node.length; i++) node[i] = wrap(node[i], owner)
|
|
114
|
+
|
|
115
|
+
if (node[0] !== 'call' || typeof node[1] !== 'string') return node
|
|
116
|
+
const label = LABEL_BY_WAT_HELPER.get(node[1])
|
|
117
|
+
if (!label) return node
|
|
118
|
+
if (only && !only.has(label) && !only.has(node[1].replace(/^\$?__/, ''))) return node
|
|
119
|
+
const results = resultsByName.get(node[1])
|
|
120
|
+
if (!results) return node
|
|
121
|
+
|
|
122
|
+
const counter = `${HELPER_SITE_PREFIX}${id++}`
|
|
123
|
+
const ownerPart = safeExportPart(owner)
|
|
124
|
+
declGlobal(counter, 'i64', 0, { export: `${counter}:${label}:${ownerPart}` })
|
|
125
|
+
const block = ['block']
|
|
126
|
+
for (const type of results) block.push(['result', type])
|
|
127
|
+
block.push(bumpCounter(counter), node)
|
|
128
|
+
return block
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const fn of funcs) {
|
|
132
|
+
if (!Array.isArray(fn) || fn[0] !== 'func' || typeof fn[1] !== 'string') continue
|
|
133
|
+
const bodyStart = findBodyStart(fn)
|
|
134
|
+
for (let i = bodyStart; i < fn.length; i++) fn[i] = wrap(fn[i], fn[1])
|
|
135
|
+
}
|
|
136
|
+
return id
|
|
137
|
+
}
|