jz 0.5.0 → 0.6.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 +288 -314
- package/bench/README.md +319 -0
- package/bench/bench.svg +112 -0
- package/cli.js +32 -24
- package/index.js +177 -55
- package/interop.js +88 -159
- package/jz.svg +5 -0
- package/jzify/arguments.js +97 -0
- package/jzify/bundler.js +382 -0
- package/jzify/classes.js +328 -0
- package/jzify/hoist-vars.js +177 -0
- package/jzify/index.js +51 -0
- package/jzify/names.js +37 -0
- package/jzify/switch.js +106 -0
- package/jzify/transform.js +349 -0
- package/layout.js +179 -0
- package/module/array.js +322 -153
- package/module/collection.js +603 -145
- package/module/console.js +55 -43
- package/module/core.js +281 -142
- package/module/date.js +15 -3
- package/module/function.js +73 -6
- package/module/index.js +2 -1
- package/module/json.js +226 -61
- package/module/math.js +461 -185
- package/module/number.js +306 -60
- package/module/object.js +448 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +85 -0
- package/module/string.js +591 -220
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +45 -48
- package/package.json +41 -12
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +26 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +661 -0
- package/src/compile/analyze.js +1565 -0
- package/src/compile/emit-assign.js +408 -0
- package/src/compile/emit.js +3201 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +497 -125
- package/src/{infer.js → compile/infer.js} +27 -98
- package/src/{narrow.js → compile/narrow.js} +302 -96
- package/src/compile/plan/advise.js +316 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +118 -0
- package/src/compile/plan/inline.js +679 -0
- package/src/compile/plan/literals.js +984 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +573 -0
- package/src/compile/program-facts.js +404 -0
- package/src/ctx.js +176 -58
- package/src/ir.js +540 -171
- package/src/kind-traits.js +105 -0
- package/src/kind.js +462 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1106 -446
- package/src/optimize/vectorize.js +1874 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +600 -205
- package/src/reps.js +115 -0
- package/src/resolve.js +12 -3
- package/src/static.js +199 -0
- package/src/type.js +647 -0
- package/src/{assemble.js → wat/assemble.js} +86 -48
- package/src/wat/optimize.js +3760 -0
- package/transform.js +21 -0
- package/wasi.js +47 -5
- package/src/analyze.js +0 -3818
- package/src/emit.js +0 -2997
- package/src/jzify.js +0 -1553
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /package/src/{codegen.js → wat/codegen.js} +0 -0
package/jzify/switch.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* switch → if/else + fall-through lowering.
|
|
3
|
+
* @module jzify/switch
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Flatten a switch clause body to a single node, dropping ASI position markers.
|
|
7
|
+
* Unlike a plain statement list this keeps any `break` intact — transformSwitch
|
|
8
|
+
* needs the breaks to gate fall-through; it rewrites them to a sticky flag. */
|
|
9
|
+
export function normalizeCaseBody(body) {
|
|
10
|
+
if (!Array.isArray(body) || body[0] !== ';') return body
|
|
11
|
+
const stmts = body.slice(1).filter(s => s != null && typeof s !== 'number')
|
|
12
|
+
return stmts.length === 0 ? null : stmts.length === 1 ? stmts[0] : [';', ...stmts]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const SWITCH_BREAK_BOUNDARIES = new Set(['for', 'for-in', 'for-of', 'while', 'do', 'switch', '=>', 'function', 'class'])
|
|
16
|
+
|
|
17
|
+
function hasOwnSwitchBreak(node) {
|
|
18
|
+
if (!Array.isArray(node)) return false
|
|
19
|
+
if (node[0] === 'break') return true
|
|
20
|
+
if (SWITCH_BREAK_BOUNDARIES.has(node[0])) return false
|
|
21
|
+
for (let i = 1; i < node.length; i++) if (hasOwnSwitchBreak(node[i])) return true
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function rewriteSwitchBreaks(node, flag) {
|
|
26
|
+
if (!Array.isArray(node)) return node
|
|
27
|
+
const op = node[0]
|
|
28
|
+
if (op === 'break') return ['=', flag, [null, true]]
|
|
29
|
+
if (SWITCH_BREAK_BOUNDARIES.has(op)) return node
|
|
30
|
+
|
|
31
|
+
if (op === ';') {
|
|
32
|
+
const out = []
|
|
33
|
+
const stmts = node.slice(1)
|
|
34
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
35
|
+
const stmt = stmts[i]
|
|
36
|
+
out.push(rewriteSwitchBreaks(stmt, flag))
|
|
37
|
+
if (hasOwnSwitchBreak(stmt) && i < stmts.length - 1) {
|
|
38
|
+
const tail = rewriteSwitchBreaks([';', ...stmts.slice(i + 1)], flag)
|
|
39
|
+
out.push(['if', ['!', flag], tail])
|
|
40
|
+
break
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out.length === 0 ? null : out.length === 1 ? out[0] : [';', ...out]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return node.map((part, i) => i === 0 ? part : rewriteSwitchBreaks(part, flag))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Transform a switch into structured control flow with faithful fall-through.
|
|
50
|
+
*
|
|
51
|
+
* A pure if/else-if chain (the former lowering) can't express fall-through,
|
|
52
|
+
* stacked labels, or a `default` clause that isn't last \u2014 it ran only the first
|
|
53
|
+
* matching body. The correct model is two-phase, evaluated once with no goto:
|
|
54
|
+
*
|
|
55
|
+
* 1. ENTRY \u2014 compare the discriminant against each `case` label in source
|
|
56
|
+
* order; the first `===` match fixes the entry index. No case matches \u2192
|
|
57
|
+
* entry = the `default` clause's source index (or past-end if none).
|
|
58
|
+
* 2. RUN \u2014 walk clauses in source order; `entry <= i` runs clause i, so every
|
|
59
|
+
* clause from the entry onward executes (fall-through). A `break` flips the
|
|
60
|
+
* sticky `brk` flag (via rewriteSwitchBreaks) and gates the rest.
|
|
61
|
+
*
|
|
62
|
+
* The discriminant is bound to a temp only when re-reading it isn't free/safe; a
|
|
63
|
+
* bare identifier is compared directly \u2014 a synthetic temp would shed its STRING
|
|
64
|
+
* val-type and mis-fold string `case`s to `false` under strict-=== folding. */
|
|
65
|
+
export function createSwitchLowering(transform, names) {
|
|
66
|
+
return function transformSwitch(discriminant, cases) {
|
|
67
|
+
const disc = transform(discriminant)
|
|
68
|
+
const simple = typeof disc === 'string' || (Array.isArray(disc) && disc[0] == null)
|
|
69
|
+
const tmp = simple ? disc : names.switchDisc()
|
|
70
|
+
const start = names.switchStart()
|
|
71
|
+
const needsBreakFlag = cases.some(c => hasOwnSwitchBreak(c[0] === 'case' ? c[2] : c[1]))
|
|
72
|
+
const brk = needsBreakFlag ? names.switchBreak() : null
|
|
73
|
+
|
|
74
|
+
const n = cases.length
|
|
75
|
+
let defaultIdx = -1
|
|
76
|
+
const bodies = cases.map((c, i) => {
|
|
77
|
+
if (c[0] === 'default') { defaultIdx = i; return transform(c[1]) }
|
|
78
|
+
return transform(c[2])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const stmts = []
|
|
82
|
+
if (!simple) stmts.push(['let', ['=', tmp, disc]])
|
|
83
|
+
|
|
84
|
+
// Phase 1 \u2014 entry index. Init to default's position (or n = "no clause runs"),
|
|
85
|
+
// then let the first matching label override it via an if/else-if chain.
|
|
86
|
+
stmts.push(['let', ['=', start, [null, defaultIdx >= 0 ? defaultIdx : n]]])
|
|
87
|
+
let chain = null
|
|
88
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
89
|
+
if (cases[i][0] !== 'case') continue
|
|
90
|
+
const hit = ['=', start, [null, i]]
|
|
91
|
+
const cond = ['===', tmp, transform(cases[i][1])]
|
|
92
|
+
chain = chain != null ? ['if', cond, hit, chain] : ['if', cond, hit]
|
|
93
|
+
}
|
|
94
|
+
if (chain) stmts.push(chain)
|
|
95
|
+
if (brk) stmts.push(['let', ['=', brk, [null, false]]])
|
|
96
|
+
|
|
97
|
+
// Phase 2 \u2014 run clauses from the entry index, falling through until a break.
|
|
98
|
+
for (let i = 0; i < n; i++) {
|
|
99
|
+
if (bodies[i] == null) continue
|
|
100
|
+
const body = brk ? rewriteSwitchBreaks(bodies[i], brk) : bodies[i]
|
|
101
|
+
const reached = ['<=', start, [null, i]]
|
|
102
|
+
stmts.push(['if', brk ? ['&&', ['!', brk], reached] : reached, body])
|
|
103
|
+
}
|
|
104
|
+
return [';', ...stmts]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST transform handlers — function/class/control-flow lowering after hoisting.
|
|
3
|
+
* @module jzify/transform
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { warn } from '../src/ctx.js'
|
|
7
|
+
import { JZ_BLOCK_OPS, LABEL_BODY_OPS, STMT_ONLY_OPS, paramList } from '../src/ast.js'
|
|
8
|
+
import { isDestructurePat } from './hoist-vars.js'
|
|
9
|
+
|
|
10
|
+
const ERROR_INSTANCEOF = new Set(['Error', 'TypeError', 'SyntaxError', 'RangeError', 'ReferenceError', 'URIError', 'EvalError'])
|
|
11
|
+
|
|
12
|
+
const TYPED_ARRAYS = new Set(['Float64Array','Float32Array','Int32Array','Uint32Array',
|
|
13
|
+
'Int16Array','Uint16Array','Int8Array','Uint8Array',
|
|
14
|
+
'ArrayBuffer','BigInt64Array','BigUint64Array','DataView'])
|
|
15
|
+
|
|
16
|
+
const isProto = n => Array.isArray(n) && n[0] === '.' && Array.isArray(n[1]) && n[1][0] === '.' && n[1][2] === 'prototype'
|
|
17
|
+
|
|
18
|
+
function staticInstanceofFold(val, ctor) {
|
|
19
|
+
if (typeof ctor !== 'string' || !Array.isArray(val)) return null
|
|
20
|
+
if (val[0] === '()' && val.length === 2) return staticInstanceofFold(val[1], ctor)
|
|
21
|
+
if (val[0] === '[]' && val.length <= 2) return ctor === 'Array' || ctor === 'Object'
|
|
22
|
+
if (val[0] === '{}') return ctor === 'Object'
|
|
23
|
+
if (val[0] === '//') return ctor === 'RegExp' || ctor === 'Object'
|
|
24
|
+
if (val[0] === 'new') {
|
|
25
|
+
const inner = val[1]
|
|
26
|
+
const cname = typeof inner === 'string' ? inner
|
|
27
|
+
: (Array.isArray(inner) && inner[0] === '()' && typeof inner[1] === 'string') ? inner[1]
|
|
28
|
+
: null
|
|
29
|
+
if (cname) return cname === ctor || (cname !== 'Object' && ctor === 'Object')
|
|
30
|
+
}
|
|
31
|
+
if (val[0] == null && val.length === 2) {
|
|
32
|
+
const v = val[1]
|
|
33
|
+
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v == null) return false
|
|
34
|
+
}
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function dedupeRedecls(stmts) {
|
|
39
|
+
const declName = d => typeof d === 'string' ? d
|
|
40
|
+
: Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string' ? d[1] : null
|
|
41
|
+
const seen = new Set(), out = []
|
|
42
|
+
for (const s of stmts) {
|
|
43
|
+
if (!Array.isArray(s) || (s[0] !== 'let' && s[0] !== 'const' && s[0] !== 'var')) { out.push(s); continue }
|
|
44
|
+
const keep = [s[0]], reassign = []
|
|
45
|
+
for (let i = 1; i < s.length; i++) {
|
|
46
|
+
const d = s[i], n = declName(d)
|
|
47
|
+
if (n == null) { keep.push(d); continue }
|
|
48
|
+
if (seen.has(n)) { if (Array.isArray(d) && d[0] === '=') reassign.push(['=', d[1], d[2]]) }
|
|
49
|
+
else { seen.add(n); keep.push(d) }
|
|
50
|
+
}
|
|
51
|
+
if (keep.length > 1) out.push(keep)
|
|
52
|
+
for (const r of reassign) out.push(r)
|
|
53
|
+
}
|
|
54
|
+
return out
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function functionBodyBlock(body) {
|
|
58
|
+
if (Array.isArray(body) && body[0] === '{}') return body
|
|
59
|
+
if (Array.isArray(body) && body[0] === ';') return ['{}', body]
|
|
60
|
+
return ['{}', [';', body]]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const arrowParams = params => Array.isArray(params) && params[0] === '()' ? params : ['()', params]
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {object} opts
|
|
67
|
+
* @param {ReturnType<import('./names.js').createNames>} opts.names
|
|
68
|
+
* @param {Function} opts.lowerArguments
|
|
69
|
+
* @param {Function} opts.transformPattern
|
|
70
|
+
* @param {Function} opts.normalizeCaseBody
|
|
71
|
+
* @param {Function} opts.transformSwitch
|
|
72
|
+
* @param {() => Function} opts.lowerClass
|
|
73
|
+
* @param {() => Function} opts.lowerObjectLiteralThis
|
|
74
|
+
* @param {() => Function} opts.lowerArrayConstructor
|
|
75
|
+
*/
|
|
76
|
+
export function createTransform(opts) {
|
|
77
|
+
const { names, lowerArguments, transformPattern, normalizeCaseBody, transformSwitch } = opts
|
|
78
|
+
const lowerClass = (...a) => opts.lowerClass()(...a)
|
|
79
|
+
const lowerObjectLiteralThis = (...a) => opts.lowerObjectLiteralThis()(...a)
|
|
80
|
+
const lowerArrayConstructor = (...a) => opts.lowerArrayConstructor()(...a)
|
|
81
|
+
|
|
82
|
+
const methodOverrideHasOwn = (a, b) => {
|
|
83
|
+
const proto = isProto(a) ? a : isProto(b) ? b : null
|
|
84
|
+
if (!proto) return null
|
|
85
|
+
const other = proto === a ? b : a
|
|
86
|
+
if (!Array.isArray(other) || other[0] !== '.' || other[2] !== proto[2]) return null
|
|
87
|
+
return ['()', ['.', transform(other[1]), 'hasOwnProperty'], [null, proto[2]]]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function wrapArrowBody(body) {
|
|
91
|
+
const t = transformScope(body)
|
|
92
|
+
if (!Array.isArray(t)) return ['{}', [';', t]]
|
|
93
|
+
if (t[0] === ';') return ['{}', t]
|
|
94
|
+
if (t[0] !== '{}') return ['{}', [';', t]]
|
|
95
|
+
if (t.length === 2 && !(Array.isArray(t[1]) && t[1][0] === ';')) return ['{}', [';', t[1]]]
|
|
96
|
+
return t
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function hoistFnDecl(name, params, body) {
|
|
100
|
+
const [p2, b2] = lowerArguments(params, functionBodyBlock(body))
|
|
101
|
+
const decl = ['const', ['=', name, ['=>', p2, wrapArrowBody(b2)]]]
|
|
102
|
+
decl._hoisted = true
|
|
103
|
+
return decl
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function transformScope(node) {
|
|
107
|
+
if (!Array.isArray(node)) return transform(node)
|
|
108
|
+
|
|
109
|
+
const [op, ...args] = node
|
|
110
|
+
|
|
111
|
+
if (op === 'function' && args[0]) return hoistFnDecl(...args)
|
|
112
|
+
if (op === 'class' && args[0]) return ['let', ['=', args[0], lowerClass(...args)]]
|
|
113
|
+
|
|
114
|
+
if (op === ';') {
|
|
115
|
+
const hoisted = [], rest = []
|
|
116
|
+
for (let i = 0; i < args.length; i++) {
|
|
117
|
+
const stmt = args[i]
|
|
118
|
+
if (Array.isArray(stmt) && stmt[0] === 'function' && stmt[1]) {
|
|
119
|
+
hoisted.push(hoistFnDecl(stmt[1], stmt[2], stmt[3]))
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
if (Array.isArray(stmt) && stmt[0] === 'class' && stmt[1]) {
|
|
123
|
+
rest.push(['let', ['=', stmt[1], lowerClass(stmt[1], stmt[2], stmt[3])]])
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
const t = transform(stmt)
|
|
127
|
+
if (t == null) continue
|
|
128
|
+
if (Array.isArray(t) && t[0] === 'const' && t._hoisted) {
|
|
129
|
+
hoisted.push(t)
|
|
130
|
+
} else if (Array.isArray(t) && t[0] === ';') {
|
|
131
|
+
for (const s of t.slice(1)) {
|
|
132
|
+
if (s != null) {
|
|
133
|
+
if (Array.isArray(s) && s[0] === 'const' && s._hoisted) hoisted.push(s)
|
|
134
|
+
else rest.push(s)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
rest.push(t)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// ES hoists every import binding above any function body. jzify mirrors
|
|
142
|
+
// that by floating imports ahead of hoisted function decls. A combo import
|
|
143
|
+
// `import d, { n } from 'm'` parses as `[',', ['import',…], ['from',…]]`, so
|
|
144
|
+
// match the comma-wrapped form too — otherwise its bindings land after the
|
|
145
|
+
// hoisted functions that reference them ("X is not in scope").
|
|
146
|
+
const isImportStmt = s => Array.isArray(s) &&
|
|
147
|
+
(s[0] === 'import' || (s[0] === ',' && Array.isArray(s[1]) && s[1][0] === 'import'))
|
|
148
|
+
const imports = rest.filter(isImportStmt)
|
|
149
|
+
const nonImports = rest.filter(s => !isImportStmt(s))
|
|
150
|
+
const all = dedupeRedecls([...imports, ...hoisted, ...nonImports])
|
|
151
|
+
return all.length === 0 ? null : all.length === 1 ? all[0] : [';', ...all]
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return transform(node)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const handlers = {
|
|
158
|
+
'()'(callee, ...rest) {
|
|
159
|
+
if (callee === 'Array') {
|
|
160
|
+
const lit = lowerArrayConstructor(rest[0])
|
|
161
|
+
if (lit) return lit
|
|
162
|
+
}
|
|
163
|
+
if (Array.isArray(callee) && callee[0] === '()' && Array.isArray(callee[1]) && callee[1][0] === 'function' && callee[1][1]) {
|
|
164
|
+
const [, name, params, body] = callee[1]
|
|
165
|
+
const [p2, b2] = lowerArguments(params, functionBodyBlock(body))
|
|
166
|
+
// `(function name(){…})(args)` — the named binding must be lowered as a
|
|
167
|
+
// self-contained EXPRESSION (it can sit in concise-arrow-body / argument
|
|
168
|
+
// position), so wrap the `let name = arrow; name(args)` in a block IIFE
|
|
169
|
+
// rather than emitting a bare `;`-sequence. A statement-sequence in
|
|
170
|
+
// expression position never reaches the `'=>'` handler's block-wrap (that
|
|
171
|
+
// runs before this transform), and emit miscompiles a `let`-closure decl in
|
|
172
|
+
// a concise `;`-body. Mirrors the bare-`function name` lowering below.
|
|
173
|
+
return ['()', ['=>', null, ['{}', [';',
|
|
174
|
+
['let', ['=', name, ['=>', arrowParams(p2), wrapArrowBody(b2)]]],
|
|
175
|
+
['return', ['()', name, ...rest.map(transform)]],
|
|
176
|
+
]]], null]
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
'function'(name, params, body) {
|
|
181
|
+
const [p2, b2] = lowerArguments(params, functionBodyBlock(body))
|
|
182
|
+
const arrow = ['=>', p2, wrapArrowBody(b2)]
|
|
183
|
+
if (name) {
|
|
184
|
+
return ['()', ['()', ['=>', null, ['{}', [';',
|
|
185
|
+
['let', name],
|
|
186
|
+
['=', name, arrow],
|
|
187
|
+
['return', name]
|
|
188
|
+
]]]], null]
|
|
189
|
+
}
|
|
190
|
+
return arrow
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
'=>'(params, body) {
|
|
194
|
+
let b = body
|
|
195
|
+
if (Array.isArray(b) && b[0] === '{}' && b.length === 2) {
|
|
196
|
+
const inner = b[1]
|
|
197
|
+
if (inner != null && !(Array.isArray(inner) && inner[0] === ';')) {
|
|
198
|
+
b = ['{}', [';', inner]]
|
|
199
|
+
}
|
|
200
|
+
} else if (Array.isArray(b) && STMT_ONLY_OPS.has(b[0])) {
|
|
201
|
+
// Subscript's expression grammar lets statement-only ops (`if`, `for`,
|
|
202
|
+
// `return`, `;`, …) appear in concise-body position — method shorthand
|
|
203
|
+
// `m(){ stmt }` parses to `['=>', p, stmt]` with no `{}` wrap (the body
|
|
204
|
+
// braces are structural, not a group operator). A concise body must
|
|
205
|
+
// yield a value, so these void ops would otherwise be coerced into the
|
|
206
|
+
// f64 return slot ("not enough arguments on the stack for f64.convert_i32_s").
|
|
207
|
+
// Re-wrap as a block — statement bodies belong in block form.
|
|
208
|
+
b = b[0] === ';' ? ['{}', b] : ['{}', [';', b]]
|
|
209
|
+
}
|
|
210
|
+
const [p2, b2] = lowerArguments(params, b)
|
|
211
|
+
return ['=>', p2, transform(b2)]
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
'class'(name, heritage, body) { return lowerClass(name, heritage, body) },
|
|
215
|
+
|
|
216
|
+
'var'(...args) {
|
|
217
|
+
return ['let', ...args.map(transform)]
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
':'(label, body) {
|
|
221
|
+
if (typeof label === 'string' && Array.isArray(body) && LABEL_BODY_OPS.has(body[0]))
|
|
222
|
+
return ['label', label, transform(body)]
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
'='(lhs, rhs) {
|
|
226
|
+
if (isDestructurePat(lhs)) return ['=', transformPattern(lhs), transform(rhs)]
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
'switch'(disc, ...cases) {
|
|
230
|
+
const clean = cases.map(c => {
|
|
231
|
+
if (c[0] === 'case') return ['case', c[1], normalizeCaseBody(c[2])]
|
|
232
|
+
if (c[0] === 'default') return ['default', normalizeCaseBody(c[1])]
|
|
233
|
+
return c
|
|
234
|
+
})
|
|
235
|
+
return transformSwitch(disc, clean)
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
'=='(a, b) { const own = methodOverrideHasOwn(a, b); if (own) return ['!', own]; return isProto(a) || isProto(b) ? 1 : ['==', transform(a), transform(b)] },
|
|
239
|
+
'!='(a, b) { const own = methodOverrideHasOwn(a, b); if (own) return own; return isProto(a) || isProto(b) ? 0 : ['!=', transform(a), transform(b)] },
|
|
240
|
+
'==='(a, b) { const own = methodOverrideHasOwn(a, b); if (own) return ['!', own]; if (isProto(a) || isProto(b)) return 1 },
|
|
241
|
+
'!=='(a, b) { const own = methodOverrideHasOwn(a, b); if (own) return own; if (isProto(a) || isProto(b)) return 0 },
|
|
242
|
+
|
|
243
|
+
'new'(ctor, ...cargs) {
|
|
244
|
+
if (Array.isArray(ctor) && ctor[0] === '()' && ctor[1] === 'Array') {
|
|
245
|
+
const lit = lowerArrayConstructor(ctor[2])
|
|
246
|
+
if (lit) return lit
|
|
247
|
+
}
|
|
248
|
+
const name = typeof ctor === 'string' ? ctor : (Array.isArray(ctor) && ctor[0] === '()' ? ctor[1] : null)
|
|
249
|
+
// Preserve `new` for native constructors the compiler resolves under its own
|
|
250
|
+
// `new` handler (prepare/index.js): typed arrays/Array/RegExp need the `new`
|
|
251
|
+
// form, and `new URL(rel, import.meta.url)` lowers to a static href string there.
|
|
252
|
+
// User classes (lowered to factory arrows by jzify) become plain calls below.
|
|
253
|
+
if (typeof name === 'string' && (TYPED_ARRAYS.has(name) || name === 'Array' || name === 'RegExp' || name === 'URL')) return ['new', transform(ctor), ...cargs.map(transform)]
|
|
254
|
+
if (Array.isArray(ctor) && ctor[0] === '()') return transform(ctor)
|
|
255
|
+
return ['()', transform(ctor), ...(cargs.length ? cargs.map(transform) : [null])]
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
'instanceof'(val, ctor) {
|
|
259
|
+
const t = transform(val)
|
|
260
|
+
const name = typeof ctor === 'string' ? ctor : (Array.isArray(ctor) && ctor[0] === '()' ? ctor[1] : null)
|
|
261
|
+
const fold = staticInstanceofFold(val, name)
|
|
262
|
+
if (fold != null) return [null, fold]
|
|
263
|
+
if (typeof name === 'string' && ERROR_INSTANCEOF.has(name)) {
|
|
264
|
+
warn('untagged-instanceof',
|
|
265
|
+
`\`instanceof ${name}\` does not discriminate thrown values in jz — errors are untagged; inspect the message or value instead`,
|
|
266
|
+
{}, Array.isArray(val) ? val.loc : null)
|
|
267
|
+
}
|
|
268
|
+
if (name === 'Array') return ['()', ['.', 'Array', 'isArray'], t]
|
|
269
|
+
if (name === 'Map') return ['()', '__is_map', t]
|
|
270
|
+
if (name === 'Set') return ['()', '__is_set', t]
|
|
271
|
+
if (typeof name === 'string' && TYPED_ARRAYS.has(name) && name !== 'ArrayBuffer' && name !== 'DataView')
|
|
272
|
+
return ['()', '__is_typed', t]
|
|
273
|
+
return ['===', ['typeof', t], [null, 'object']]
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
'do'(body, cond) {
|
|
277
|
+
const flag = names.doFlag()
|
|
278
|
+
return [';',
|
|
279
|
+
['let', ['=', flag, [null, true]]],
|
|
280
|
+
['while', ['||', flag, transform(cond)], ['{}', [';', ['=', flag, [null, false]], transform(body)]]]]
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
// The classic for-head `[';', init, cond, step]` is a fixed 3-slot structure,
|
|
284
|
+
// NOT a statement sequence. Transform each slot individually and keep null slots
|
|
285
|
+
// in place. Without this handler, `for` falls to the generic recurse, the head
|
|
286
|
+
// hits the `;` handler (transformScope), and an empty `init` (null) is dropped as
|
|
287
|
+
// an empty statement — shifting cond→init/step→cond and miscompiling the loop
|
|
288
|
+
// (`for (; i < n; i++)` ran zero/garbage iterations). for-of/for-in heads aren't
|
|
289
|
+
// `;`-lists, so they pass through transform unchanged.
|
|
290
|
+
'for'(head, body) {
|
|
291
|
+
if (Array.isArray(head) && head[0] === ';')
|
|
292
|
+
return ['for', [';', ...head.slice(1).map(s => s == null ? s : transform(s))], transform(body)]
|
|
293
|
+
return ['for', transform(head), transform(body)]
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
// A bare statement sequence is a block scope too. `parse` only wraps
|
|
297
|
+
// function/arrow bodies in `{}`; loop/conditional bodies arrive as a raw
|
|
298
|
+
// `;`. Route them through transformScope so `function` declarations nested
|
|
299
|
+
// in a loop/if body get hoisted (→ block-top `const f = arrow`) instead of
|
|
300
|
+
// falling to the discard-IIFE path, which would scope the name inside the
|
|
301
|
+
// IIFE and leave later `f()` references dangling.
|
|
302
|
+
';'(...args) { return transformScope([';', ...args]) },
|
|
303
|
+
|
|
304
|
+
'{}'(...args) {
|
|
305
|
+
const loweredObject = lowerObjectLiteralThis(args)
|
|
306
|
+
if (loweredObject) return loweredObject
|
|
307
|
+
|
|
308
|
+
return ['{}', ...args.map((a, i) => {
|
|
309
|
+
const t = transformScope(a) ?? a
|
|
310
|
+
if (i !== 0 || a == null) return t
|
|
311
|
+
const blockIn = Array.isArray(a) && JZ_BLOCK_OPS.has(a[0])
|
|
312
|
+
if (!blockIn || t == null) return t
|
|
313
|
+
return Array.isArray(t) && t[0] === ';' ? t : [';', t]
|
|
314
|
+
})]
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
'export'(inner) {
|
|
318
|
+
if (Array.isArray(inner) && inner[0] === 'function' && inner[1]) {
|
|
319
|
+
return ['export', hoistFnDecl(inner[1], inner[2], inner[3])]
|
|
320
|
+
}
|
|
321
|
+
if (Array.isArray(inner) && inner[0] === 'class' && inner[1]) {
|
|
322
|
+
return ['export', ['let', ['=', inner[1], lowerClass(inner[1], inner[2], inner[3])]]]
|
|
323
|
+
}
|
|
324
|
+
if (Array.isArray(inner) && inner[0] === 'default' && Array.isArray(inner[1]) && inner[1][0] === 'function' && inner[1][1]) {
|
|
325
|
+
// Route a named default-export function through the named-export path: a bare
|
|
326
|
+
// `const NAME` lifted in a bundled module loses its recursive self-reference
|
|
327
|
+
// (the default-alias resolver renames the func but not in-body call sites),
|
|
328
|
+
// so the function is dropped. Exporting NAME as a named binding makes prepare
|
|
329
|
+
// mangle it and resolve self-calls correctly; alias `default` to it.
|
|
330
|
+
const decl = hoistFnDecl(inner[1][1], inner[1][2], inner[1][3])
|
|
331
|
+
return [';', ['export', decl], ['export', ['{}', ['as', inner[1][1], 'default']]]]
|
|
332
|
+
}
|
|
333
|
+
if (Array.isArray(inner) && inner[0] === 'default' && Array.isArray(inner[1]) && inner[1][0] === 'class' && inner[1][1]) {
|
|
334
|
+
return [';', ['let', ['=', inner[1][1], lowerClass(inner[1][1], inner[1][2], inner[1][3])]], ['export', ['default', inner[1][1]]]]
|
|
335
|
+
}
|
|
336
|
+
return ['export', transform(inner)]
|
|
337
|
+
},
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function transform(node) {
|
|
341
|
+
if (node == null || typeof node !== 'object' || !Array.isArray(node)) return node
|
|
342
|
+
const [op, ...args] = node
|
|
343
|
+
if (op == null) return node
|
|
344
|
+
const h = handlers[op]
|
|
345
|
+
return (h && h(...args)) ?? [op, ...args.map(transform)]
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return { transform, transformScope }
|
|
349
|
+
}
|
package/layout.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared runtime layout: heap, NaN-box carrier, pointer tags.
|
|
3
|
+
*
|
|
4
|
+
* Compiler-free — safe for `jz/interop` and tests without pulling the compiler.
|
|
5
|
+
*
|
|
6
|
+
* @module layout
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Bump-allocator cells in linear memory / globals. */
|
|
10
|
+
export const HEAP = { PTR_ADDR: 1020, START: 1024 }
|
|
11
|
+
|
|
12
|
+
/** NaN-box bit layout (i64 carrier). */
|
|
13
|
+
export const LAYOUT = {
|
|
14
|
+
TAG_SHIFT: 47,
|
|
15
|
+
TAG_MASK: 0xF,
|
|
16
|
+
AUX_SHIFT: 32,
|
|
17
|
+
AUX_MASK: 0x7FFF,
|
|
18
|
+
OFFSET_MASK: 0xFFFFFFFF,
|
|
19
|
+
NAN_PREFIX: 0x7FF8,
|
|
20
|
+
NAN_PREFIX_BITS: 0x7FF8000000000000n,
|
|
21
|
+
SSO_BIT: 0x4000,
|
|
22
|
+
SLICE_BIT: 0x2000,
|
|
23
|
+
SLICE_LEN_MASK: 0x1FFF,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 4-bit tagged-pointer type codes. */
|
|
27
|
+
export const PTR = {
|
|
28
|
+
ATOM: 0,
|
|
29
|
+
ARRAY: 1,
|
|
30
|
+
BUFFER: 2,
|
|
31
|
+
TYPED: 3,
|
|
32
|
+
STRING: 4,
|
|
33
|
+
OBJECT: 6,
|
|
34
|
+
HASH: 7,
|
|
35
|
+
SET: 8,
|
|
36
|
+
MAP: 9,
|
|
37
|
+
CLOSURE: 10,
|
|
38
|
+
EXTERNAL: 11,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Reserved atom aux ids (PTR.ATOM). */
|
|
42
|
+
export const ATOM = { NULL: 1, UNDEF: 2, FALSE: 4, TRUE: 5 }
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// PTR.TYPED element-type aux codec — which typed-array flavor lives in the aux
|
|
46
|
+
// field of a PTR.TYPED box. Pure (no compiler state) → lives with the NaN-box
|
|
47
|
+
// layout it encodes, shared by the compiler (type/analyze/narrow/infer) and the
|
|
48
|
+
// `module/typedarray` stdlib.
|
|
49
|
+
// =============================================================================
|
|
50
|
+
|
|
51
|
+
/** Base element-type codes for PTR.TYPED aux (0–7). BigInt ctors share 7 + TYPED_ELEM_BIGINT_FLAG. */
|
|
52
|
+
export const TYPED_ELEM_CODE = {
|
|
53
|
+
Int8Array: 0, Uint8Array: 1, Int16Array: 2, Uint16Array: 3,
|
|
54
|
+
Int32Array: 4, Uint32Array: 5, Float32Array: 6, Float64Array: 7,
|
|
55
|
+
BigInt64Array: 7, BigUint64Array: 7,
|
|
56
|
+
}
|
|
57
|
+
export const TYPED_ELEM_VIEW_FLAG = 8
|
|
58
|
+
export const TYPED_ELEM_BIGINT_FLAG = 16
|
|
59
|
+
|
|
60
|
+
export const TYPED_ELEM_NAMES = ['Int8Array', 'Uint8Array', 'Int16Array', 'Uint16Array',
|
|
61
|
+
'Int32Array', 'Uint32Array', 'Float32Array', 'Float64Array']
|
|
62
|
+
|
|
63
|
+
/** Encode element-type name (+ optional view/bigint flags) to PTR.TYPED aux bits. */
|
|
64
|
+
export function encodeTypedElemAux(name, isView = false) {
|
|
65
|
+
const et = TYPED_ELEM_CODE[name]
|
|
66
|
+
if (et == null) return null
|
|
67
|
+
return et | (isView ? TYPED_ELEM_VIEW_FLAG : 0) |
|
|
68
|
+
(name === 'BigInt64Array' || name === 'BigUint64Array' ? TYPED_ELEM_BIGINT_FLAG : 0)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Encode a `typedElemCtor` string ('new.Int32Array' | 'new.Int32Array.view') to the 4-bit
|
|
72
|
+
* aux value used in PTR.TYPED NaN-boxing. Returns null for unknown ctors (ArrayBuffer/DataView). */
|
|
73
|
+
export function typedElemAux(ctor) {
|
|
74
|
+
if (!ctor || !ctor.startsWith('new.')) return null
|
|
75
|
+
const isView = ctor.endsWith('.view')
|
|
76
|
+
const name = isView ? ctor.slice(4, -5) : ctor.slice(4)
|
|
77
|
+
return encodeTypedElemAux(name, isView)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Reverse of typedElemAux: pick a canonical ctor string for a 4-bit elem aux. Used
|
|
81
|
+
* to round-trip TYPED-narrowed call results through ctx.types.typedElem so the
|
|
82
|
+
* unboxed local's rep picks up the same aux. aux=7 is shared with BigInt typed
|
|
83
|
+
* arrays — Float64Array is canonical (read-side compares aux only). */
|
|
84
|
+
export function ctorFromElemAux(aux) {
|
|
85
|
+
if (aux == null) return null
|
|
86
|
+
const isView = (aux & 8) !== 0
|
|
87
|
+
const name = (aux & 16) !== 0 ? 'BigInt64Array' : TYPED_ELEM_NAMES[aux & 7]
|
|
88
|
+
if (!name) return null
|
|
89
|
+
return isView ? `new.${name}.view` : `new.${name}`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Host-side high u32 word for NaN-boxed f64 pointer encoding (interop). */
|
|
93
|
+
export const encodePtrHi = (type, aux) =>
|
|
94
|
+
(0x7FF80000 | ((type & 0xF) << 15) | (aux & 0x7FFF)) >>> 0
|
|
95
|
+
|
|
96
|
+
export const decodePtrType = hi => (hi >>> 15) & 0xF
|
|
97
|
+
export const decodePtrAux = hi => hi & 0x7FFF
|
|
98
|
+
|
|
99
|
+
/** i64 NaN-prefix OR-mask for WAT `(i64.const …)` templates. */
|
|
100
|
+
export const nanPrefixHex = () =>
|
|
101
|
+
'0x' + LAYOUT.NAN_PREFIX_BITS.toString(16).toUpperCase().padStart(16, '0')
|
|
102
|
+
|
|
103
|
+
/** Atom sentinel as i64 hex (compiler WAT templates). */
|
|
104
|
+
export const atomNanHex = atomId =>
|
|
105
|
+
'0x' + (LAYOUT.NAN_PREFIX_BITS | (BigInt(atomId) << BigInt(LAYOUT.AUX_SHIFT))).toString(16).toUpperCase().padStart(16, '0')
|
|
106
|
+
|
|
107
|
+
/** STRING aux bit 0 on a PLAIN-HEAP string (SSO and SLICE clear): this is a
|
|
108
|
+
* CANONICAL interned string — the static-pool copy (or an intern-table hit
|
|
109
|
+
* resolving to it). Two canonicals are bit-equal iff content-equal, so
|
|
110
|
+
* __str_eq answers unequal canonicals without touching bytes, and interned
|
|
111
|
+
* statics carry a cached FNV hash at offset-8 ([hash u32][len u32][bytes])
|
|
112
|
+
* that __str_hash loads instead of re-hashing. Inert elsewhere: slice-length
|
|
113
|
+
* bits are only read under SLICE_BIT, SSO length under SSO_BIT, and plain-
|
|
114
|
+
* heap consumers read the len header at -4 regardless of aux. */
|
|
115
|
+
export const STR_INTERN_BIT = 0x1
|
|
116
|
+
|
|
117
|
+
/** Pre-shifted STRING SSO aux bit as i64 hex. */
|
|
118
|
+
export const ssoBitI64Hex = () =>
|
|
119
|
+
'0x' + (BigInt(LAYOUT.SSO_BIT) << BigInt(LAYOUT.AUX_SHIFT)).toString(16).toUpperCase().padStart(16, '0')
|
|
120
|
+
|
|
121
|
+
/** Pre-shifted STRING slice/view aux bit as i64 hex. */
|
|
122
|
+
export const sliceBitI64Hex = () =>
|
|
123
|
+
'0x' + (BigInt(LAYOUT.SLICE_BIT) << BigInt(LAYOUT.AUX_SHIFT)).toString(16).toUpperCase().padStart(16, '0')
|
|
124
|
+
|
|
125
|
+
/** Full i64 NaN-box hex for `(i64.const …)` — ptr type + aux, offset OR'd separately. */
|
|
126
|
+
export const ptrNanHex = (ptrType, aux = 0) =>
|
|
127
|
+
'0x' + ptrBoxPrefixBigInt(ptrType, aux).toString(16).toUpperCase().padStart(16, '0')
|
|
128
|
+
|
|
129
|
+
/** Compile-time i64 prefix for mkPtrIR (before offset OR). */
|
|
130
|
+
export const ptrBoxPrefixBigInt = (ptrType, aux = 0) =>
|
|
131
|
+
(0x7FF8n << 48n)
|
|
132
|
+
| ((BigInt(ptrType) & 0xFn) << 47n)
|
|
133
|
+
| ((BigInt(aux) & 0x7FFFn) << 32n)
|
|
134
|
+
|
|
135
|
+
/** Host-side atom sentinel high-u32 values (interop f64 decode). */
|
|
136
|
+
export const ATOM_HI = {
|
|
137
|
+
[ATOM.NULL]: encodePtrHi(PTR.ATOM, ATOM.NULL),
|
|
138
|
+
[ATOM.UNDEF]: encodePtrHi(PTR.ATOM, ATOM.UNDEF),
|
|
139
|
+
[ATOM.FALSE]: encodePtrHi(PTR.ATOM, ATOM.FALSE),
|
|
140
|
+
[ATOM.TRUE]: encodePtrHi(PTR.ATOM, ATOM.TRUE),
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** OOB / canonical quiet-NaN f64 literal for WAT and IR (`nan:0x7FF8…`). */
|
|
144
|
+
export const oobNanLiteral = () => `nan:${nanPrefixHex()}`
|
|
145
|
+
export const oobNanIR = () => ['f64.const', oobNanLiteral()]
|
|
146
|
+
|
|
147
|
+
/** Heap forwarding-pointer follow (WAT fragment).
|
|
148
|
+
* ARRAY/HASH/SET/MAP relocate on growth, leaving the old cell as a forwarding
|
|
149
|
+
* header: cap=-1 sentinel at off-4, relocated offset at off-8. `off` is the
|
|
150
|
+
* i32 local (token incl. `$`) holding the current offset — mutated in place.
|
|
151
|
+
* `lowGuard` adds the `off < 8` bailout; omit it when the caller already
|
|
152
|
+
* proved off≥8.
|
|
153
|
+
*
|
|
154
|
+
* Shape: a loop-free in-bounds + sentinel CHECK with a cold call into
|
|
155
|
+
* $__ptr_offset_fwd (the actual chase loop, module/core.js) only when the
|
|
156
|
+
* first hop is a real forward. Keeping every inline copy loop-free is what
|
|
157
|
+
* lets the engine inline the hot heap helpers (__ptr_offset, __len,
|
|
158
|
+
* __arr_idx_known, __typed_idx…) — a body containing a loop is excluded from
|
|
159
|
+
* V8's wasm inliner, and these helpers sit on ~25% of self-host compile time.
|
|
160
|
+
* Callers must list '__ptr_offset_fwd' in their deps()/wat() dependency set. */
|
|
161
|
+
export const followForwardingWat = (off = '$off', { lowGuard = true } = {}) =>
|
|
162
|
+
`(if (i32.and
|
|
163
|
+
${lowGuard ? `(i32.ge_u (local.get ${off}) (i32.const 8))` : '(i32.const 1)'}
|
|
164
|
+
(i32.le_u (local.get ${off}) (i32.shl (memory.size) (i32.const 16))))
|
|
165
|
+
(then (if (i32.eq (i32.load (i32.sub (local.get ${off}) (i32.const 4))) (i32.const -1))
|
|
166
|
+
(then (local.set ${off} (call $__ptr_offset_fwd (local.get ${off})))))))`
|
|
167
|
+
|
|
168
|
+
/** The cold forwarding-chase loop behind followForwardingWat — the only body
|
|
169
|
+
* allowed to loop. Re-checks the sentinel each hop (first re-check is
|
|
170
|
+
* redundant with the caller's guard; the cold path doesn't care). */
|
|
171
|
+
export const ptrOffsetFwdWat = () =>
|
|
172
|
+
`(func $__ptr_offset_fwd (param $off i32) (result i32)
|
|
173
|
+
(block $done (loop $follow
|
|
174
|
+
(br_if $done (i32.lt_u (local.get $off) (i32.const 8)))
|
|
175
|
+
(br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
|
|
176
|
+
(br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
|
|
177
|
+
(local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
178
|
+
(br $follow)))
|
|
179
|
+
(local.get $off))`
|