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
|
@@ -19,33 +19,70 @@
|
|
|
19
19
|
* Each handler may touch multiple concerns, but helpers keep each concern self-contained.
|
|
20
20
|
* Unhandled ops fall through to recursive prep() of their children.
|
|
21
21
|
*
|
|
22
|
+
* # Forward seeding (the two compile/ imports — deliberate, not a layering leak)
|
|
23
|
+
* Prepare is the only pass that sees module-scope declarations in source order,
|
|
24
|
+
* so it seeds two compile-stage fact stores AS it walks (re-deriving them later
|
|
25
|
+
* would need a second whole-AST pass over information prepare already holds):
|
|
26
|
+
* - `recordGlobalRep` (compile/infer.js) — module-global value reps
|
|
27
|
+
* - `observeNodeFacts` (compile/program-facts.js) — per-node program facts
|
|
28
|
+
* The contract is write-only: prepare never READS compile-stage state, so the
|
|
29
|
+
* stage remains re-runnable and compile owns every read path.
|
|
30
|
+
*
|
|
22
31
|
* @module prepare
|
|
23
32
|
*/
|
|
24
33
|
|
|
25
|
-
import {
|
|
26
|
-
import { ctx, err, derive } from '
|
|
27
|
-
import { T
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
34
|
+
import { handlerArgs, refsName, ASSIGN_OPS, JZ_NULL, JZ_UNDEF, TYPEOF } from '../ast.js'
|
|
35
|
+
import { ctx, err, derive, emitArity, declGlobal } from '../ctx.js'
|
|
36
|
+
import { T } from '../ast.js'
|
|
37
|
+
import { extractParams, collectParamNames, classifyParam } from '../ast.js'
|
|
38
|
+
import { observeNodeFacts } from '../compile/program-facts.js'
|
|
39
|
+
import { staticObjectProps, staticPropertyKey } from '../static.js'
|
|
40
|
+
import { VAL } from '../reps.js'
|
|
41
|
+
import { STMT_OPS } from '../ast.js'
|
|
42
|
+
import { REJECT_IDENTS, REJECT_OPS, rejectHandlers } from '../op-policy.js'
|
|
43
|
+
import { recordGlobalRep } from '../compile/infer.js'
|
|
44
|
+
import { isFuncRef } from '../ir.js'
|
|
30
45
|
import {
|
|
31
|
-
CTORS, TIMER_NAMES,
|
|
46
|
+
CTORS, COLLECTION_CTORS, TIMER_NAMES,
|
|
32
47
|
hasModule, includeModule,
|
|
33
48
|
includeForArrayAccess, includeForArrayLiteral, includeForArrayPattern, includeForCallableValue,
|
|
34
49
|
includeForGenericMethod, includeForKnownKeyIteration, includeForNamedCall, includeForNumericCoercion,
|
|
35
50
|
includeForObjectLiteral, includeForObjectPattern, includeForOp, includeForProperty, includeForRuntimeCtor,
|
|
36
51
|
includeForRuntimeKeyIteration, includeForStringOnly, includeForStringValue, includeForTimerRuntime,
|
|
37
|
-
} from '
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
52
|
+
} from '../autoload.js'
|
|
53
|
+
|
|
54
|
+
// SIMD intrinsic namespaces — pure namespaces backed by the `simd` module.
|
|
55
|
+
const SIMD_NS = new Set(['f32x4', 'i32x4', 'f64x2', 'v128'])
|
|
56
|
+
|
|
57
|
+
// Module-level prepare state. Six independent stacks/scalars that together form
|
|
58
|
+
// the prepare-pass working set. Lifecycle: reinitialized via `resetPrepState()`
|
|
59
|
+
// at the top of `prepare()` (line ~368) — any throw inside prepare is cleared
|
|
60
|
+
// on the next entry, so leak across compilations is impossible. Kept at module
|
|
61
|
+
// scope (rather than ctx.prepare.*) because 78 read sites would mean a single
|
|
62
|
+
// indirection on every scope query; the consolidated reset documents the set.
|
|
63
|
+
let depth // arrow nesting depth (0=top-level, >0=inside function)
|
|
64
|
+
let scopes // block scope stack: [{names: Set, renames: Map}]
|
|
65
|
+
let staticConstScopes // lexical const facts: [{strings: Map, arrays: Map}]
|
|
66
|
+
let assignedStaticGlobals
|
|
43
67
|
// Per-arrow set of names already declared anywhere in the function body. Used
|
|
44
68
|
// to force a rename when the same identifier is declared in two sibling blocks
|
|
45
69
|
// (else-if arms, separate { ... } chunks): without renaming, both decls lower
|
|
46
70
|
// to the same WASM local, but downstream optimizations (directClosures) gate
|
|
47
71
|
// on per-decl `isReassigned`, not per-WASM-local — they'd read a stale binding.
|
|
48
|
-
let funcLocalNames
|
|
72
|
+
let funcLocalNames
|
|
73
|
+
// Per-arrow set of local names bound to a function literal (`let g = () => …`).
|
|
74
|
+
// Lets the `.`-handler tell a function receiver — where `.caller`/`.callee` are
|
|
75
|
+
// prohibited introspection — from a data object that merely has such a field.
|
|
76
|
+
let funcValueNames
|
|
77
|
+
|
|
78
|
+
const resetPrepState = () => {
|
|
79
|
+
depth = 0
|
|
80
|
+
scopes = []
|
|
81
|
+
staticConstScopes = []
|
|
82
|
+
assignedStaticGlobals = new Set()
|
|
83
|
+
funcLocalNames = [new Set()]
|
|
84
|
+
funcValueNames = [new Set()]
|
|
85
|
+
}
|
|
49
86
|
|
|
50
87
|
// ES spec: identifier with \uHHHH or \u{...} escape is equivalent to the decoded
|
|
51
88
|
// form. subscript preserves raw spelling in the AST; normalize once before prep.
|
|
@@ -54,6 +91,55 @@ const decodeIdent = s => s.includes('\\u')
|
|
|
54
91
|
? s.replace(IDESC, (_, b, p) => String.fromCodePoint(parseInt(b || p, 16)))
|
|
55
92
|
: s
|
|
56
93
|
|
|
94
|
+
// A for-loop bound `arr.length` may be snapshotted into a pre-loop local only when
|
|
95
|
+
// nothing in the loop can change it. Two ways it can change: a write to the receiver
|
|
96
|
+
// (`arr = …`, `arr.length = …`, `arr[k] = …`) or a call — push/pop/splice mutate
|
|
97
|
+
// directly, and any call can reach `arr` through an alias the compiler can't track
|
|
98
|
+
// locally (compilePendingClosures grows ctx.closure.bodies this way). Both predicates
|
|
99
|
+
// recurse the whole node; nested arrow *definitions* are harmless until invoked, and
|
|
100
|
+
// an invocation is itself a call node, so `callFree` already covers escaped mutators.
|
|
101
|
+
const callFree = node => {
|
|
102
|
+
if (!Array.isArray(node)) return true
|
|
103
|
+
if (node[0] === '()' || node[0] === 'new') return false
|
|
104
|
+
for (let i = 1; i < node.length; i++) if (!callFree(node[i])) return false
|
|
105
|
+
return true
|
|
106
|
+
}
|
|
107
|
+
// Calls that provably can't resize ANY receiver: read-only builtin methods
|
|
108
|
+
// (no mutators, no callback-takers — a callback could close over the receiver
|
|
109
|
+
// and push) and pure namespaces. Everything else (user fns, push/splice,
|
|
110
|
+
// map/forEach) may reach the bound receiver through an alias — disqualifies
|
|
111
|
+
// the length snapshot. A user object shadowing one of these names with a
|
|
112
|
+
// mutating closure is a documented divergence (same class as for-of's).
|
|
113
|
+
const _BOUND_PURE_NS = new Set(['Math', 'math', 'Number', 'String', 'JSON', 'console', 'Date', 'performance'])
|
|
114
|
+
const _BOUND_RO_METHODS = new Set([
|
|
115
|
+
'charCodeAt', 'charAt', 'codePointAt', 'at', 'indexOf', 'lastIndexOf', 'includes',
|
|
116
|
+
'startsWith', 'endsWith', 'slice', 'substring', 'trim', 'toUpperCase', 'toLowerCase',
|
|
117
|
+
'join', 'concat', 'toString', 'get', 'has', 'now',
|
|
118
|
+
])
|
|
119
|
+
const boundSafeCalls = node => {
|
|
120
|
+
if (!Array.isArray(node)) return true
|
|
121
|
+
if (node[0] === 'new') return false
|
|
122
|
+
if (node[0] === '()' || node[0] === '?.()') {
|
|
123
|
+
const callee = node[1]
|
|
124
|
+
const safe = Array.isArray(callee) && (callee[0] === '.' || callee[0] === '?.') &&
|
|
125
|
+
(_BOUND_RO_METHODS.has(callee[2]) ||
|
|
126
|
+
(typeof callee[1] === 'string' && _BOUND_PURE_NS.has(callee[1])))
|
|
127
|
+
if (!safe) return false
|
|
128
|
+
}
|
|
129
|
+
for (let i = 1; i < node.length; i++) if (!boundSafeCalls(node[i])) return false
|
|
130
|
+
return true
|
|
131
|
+
}
|
|
132
|
+
const writesReceiver = (node, recv) => {
|
|
133
|
+
if (!Array.isArray(node)) return false
|
|
134
|
+
const op = node[0]
|
|
135
|
+
if ((ASSIGN_OPS.has(op) || op === '++' || op === '--') &&
|
|
136
|
+
(node[1] === recv ||
|
|
137
|
+
(Array.isArray(node[1]) && (node[1][0] === '[]' || node[1][0] === '.') && node[1][1] === recv)))
|
|
138
|
+
return true
|
|
139
|
+
for (let i = 1; i < node.length; i++) if (writesReceiver(node[i], recv)) return true
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
|
|
57
143
|
const normalizeIdents = node => {
|
|
58
144
|
if (!Array.isArray(node)) return
|
|
59
145
|
// Literal-value wrapper [null, X] / [undefined, X]: X is a value, not an identifier
|
|
@@ -67,14 +153,26 @@ const normalizeIdents = node => {
|
|
|
67
153
|
|
|
68
154
|
const hostReturnValType = spec => {
|
|
69
155
|
if (!spec || typeof spec === 'function') return null
|
|
156
|
+
// Return type is the canonical string name ('number'/'string'/'bigint'/'f64').
|
|
157
|
+
// (Earlier this also accepted the constructor identity `ret === String` etc.,
|
|
158
|
+
// but that references host-only globals with no first-class value in jz — it
|
|
159
|
+
// broke self-hosting and was never used. String names are the portable form.)
|
|
70
160
|
const ret = spec.returns ?? spec.return ?? spec.result
|
|
71
|
-
if (ret === 'number' || ret === 'f64'
|
|
72
|
-
if (ret === 'string'
|
|
73
|
-
if (ret === 'bigint'
|
|
161
|
+
if (ret === 'number' || ret === 'f64') return VAL.NUMBER
|
|
162
|
+
if (ret === 'string') return VAL.STRING
|
|
163
|
+
if (ret === 'bigint') return VAL.BIGINT
|
|
74
164
|
return null
|
|
75
165
|
}
|
|
76
166
|
|
|
77
167
|
const addHostImport = (mod, name, alias, spec) => {
|
|
168
|
+
// A numeric host constant (e.g. `Math.PI` via `{ imports: { math: Math } }`) has no callable
|
|
169
|
+
// ABI — record it so references fold to an f64 literal (see prep's identifier resolution) instead
|
|
170
|
+
// of emitting a 0-arg func import that can't be read as a value ("'PI' is not in scope").
|
|
171
|
+
if (typeof spec === 'number') {
|
|
172
|
+
if (!ctx.scope.hostConsts) ctx.scope.hostConsts = {}
|
|
173
|
+
ctx.scope.hostConsts[alias] = spec
|
|
174
|
+
return
|
|
175
|
+
}
|
|
78
176
|
const nParams = typeof spec === 'function' ? spec.length : (spec?.params || 0)
|
|
79
177
|
// User-supplied imports carry NaN-boxed values via i64 (not f64) so V8 cannot
|
|
80
178
|
// canonicalize the NaN payload across the wasm↔JS function boundary —
|
|
@@ -91,8 +189,14 @@ const addHostImport = (mod, name, alias, spec) => {
|
|
|
91
189
|
|
|
92
190
|
const isImportMeta = node => Array.isArray(node) && node[0] === '.' && node[1] === 'import' && node[2] === 'meta'
|
|
93
191
|
const isImportMetaProp = (node, prop) => Array.isArray(node) && node[0] === '.' && isImportMeta(node[1]) && node[2] === prop
|
|
192
|
+
// In a pure boolean position (consumer reads only truthiness) `!!e` is exactly `e`.
|
|
193
|
+
// Drop redundant double-negation; recurse so `!!!!e → e`. NOT valid for `&&`/`||`
|
|
194
|
+
// operands — those are value-preserving (`!!a && b` returns `false`, not `a`).
|
|
195
|
+
const stripBoolNot = c => {
|
|
196
|
+
while (Array.isArray(c) && c[0] === '!' && Array.isArray(c[1]) && c[1][0] === '!') c = c[1][1]
|
|
197
|
+
return c
|
|
198
|
+
}
|
|
94
199
|
const stringValue = node => Array.isArray(node) && node[0] == null && typeof node[1] === 'string' ? node[1] : null
|
|
95
|
-
const flatArgs = args => args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',' ? args[0].slice(1) : args
|
|
96
200
|
const MUTATING_ARRAY_METHODS = new Set(['copyWithin', 'fill', 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'])
|
|
97
201
|
|
|
98
202
|
function staticStringArrayValues(expr) {
|
|
@@ -227,7 +331,12 @@ function staticStringExpr(node) {
|
|
|
227
331
|
if (op === '+') {
|
|
228
332
|
const a = staticStringExpr(args[0])
|
|
229
333
|
const b = staticStringExpr(args[1])
|
|
230
|
-
|
|
334
|
+
// Accumulate from a fresh empty string (`'' + a + b`) rather than concatenating two
|
|
335
|
+
// source-derived substrings directly. Under self-host the latter can yield a string
|
|
336
|
+
// backed by transient parse-time storage that's invalid by the time emit['//'] reads
|
|
337
|
+
// it for regex compilation (OOB); forcing a fresh allocation, as the template-literal
|
|
338
|
+
// path already does, keeps it stable. Identical value in both legs.
|
|
339
|
+
return a != null && b != null ? '' + a + b : null
|
|
231
340
|
}
|
|
232
341
|
if (op === '`') {
|
|
233
342
|
let out = ''
|
|
@@ -263,7 +372,12 @@ function importMetaUrl() {
|
|
|
263
372
|
|
|
264
373
|
function resolveImportMeta(spec) {
|
|
265
374
|
const base = importMetaUrl()
|
|
266
|
-
|
|
375
|
+
// URL resolution is a host capability (WHATWG URL parsing), injected via
|
|
376
|
+
// ctx.transform.resolveUrl rather than referencing the `URL` global — the same
|
|
377
|
+
// inversion as ctx.transform.parse. Keeps the self-host kernel (which bundles
|
|
378
|
+
// its module graph and never resolves import.meta at runtime) free of `URL`.
|
|
379
|
+
if (!ctx.transform.resolveUrl) err('import.meta resolution requires ctx.transform.resolveUrl (injected by the jz pipeline)')
|
|
380
|
+
try { return ctx.transform.resolveUrl(spec, base) }
|
|
267
381
|
catch { err(`Cannot resolve import.meta specifier '${spec}' from '${base}'`) }
|
|
268
382
|
}
|
|
269
383
|
|
|
@@ -272,6 +386,7 @@ function recordModuleInitFacts(root) {
|
|
|
272
386
|
dynVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
|
|
273
387
|
hasFuncValue: false, timerNames: new Set(),
|
|
274
388
|
maxDef: 0, maxCall: 0, hasRest: false, hasSpread: false,
|
|
389
|
+
writtenProps: new Set(),
|
|
275
390
|
}
|
|
276
391
|
const visitFuncValue = (node) => {
|
|
277
392
|
if (facts.hasFuncValue || !Array.isArray(node)) return
|
|
@@ -316,13 +431,34 @@ function recordModuleInitFacts(root) {
|
|
|
316
431
|
* @param {ASTNode} node - Raw AST from parser
|
|
317
432
|
* @returns {ASTNode} Normalized AST
|
|
318
433
|
*/
|
|
434
|
+
// ES2020 §13.13: the nullish-coalescing `??` cannot be combined with `||` or `&&`
|
|
435
|
+
// without parentheses — V8 raises a SyntaxError. subscript/jessie doesn't enforce
|
|
436
|
+
// it, so jz would otherwise silently accept (and pick its own parse for) the mix.
|
|
437
|
+
// Run on the RAW input AST: a parenthesized operand parses as `['()', …]`, so a
|
|
438
|
+
// bare `??`/`||`/`&&` child is exactly the illegal unparenthesized form — and at
|
|
439
|
+
// this stage no compiler-synthesized `??` (e.g. destructuring defaults) exists yet,
|
|
440
|
+
// so `let [a = b || c] = arr` can't false-positive.
|
|
441
|
+
function validateCoalesceMixing(n) {
|
|
442
|
+
if (!Array.isArray(n)) return
|
|
443
|
+
const op = n[0]
|
|
444
|
+
if (op === '||' || op === '&&') {
|
|
445
|
+
for (let i = 1; i < n.length; i++) if (Array.isArray(n[i]) && n[i][0] === '??')
|
|
446
|
+
err(`'??' cannot be mixed with '${op}' without parentheses (ES2020) — wrap one side, e.g. (a ?? b) ${op} c`)
|
|
447
|
+
} else if (op === '??') {
|
|
448
|
+
for (let i = 1; i < n.length; i++) if (Array.isArray(n[i]) && (n[i][0] === '||' || n[i][0] === '&&'))
|
|
449
|
+
err(`'??' cannot be mixed with '||' / '&&' without parentheses (ES2020) — wrap one side, e.g. a ?? (b || c)`)
|
|
450
|
+
}
|
|
451
|
+
for (let i = 1; i < n.length; i++) validateCoalesceMixing(n[i])
|
|
452
|
+
}
|
|
453
|
+
|
|
319
454
|
export default function prepare(node) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
455
|
+
resetPrepState()
|
|
456
|
+
// Inject the module-include primitive so stdlib modules can pull dependency
|
|
457
|
+
// modules (e.g. object → collection) without importing autoload.js — that
|
|
458
|
+
// import would cycle (autoload imports every module via module/index.js).
|
|
459
|
+
ctx.module.include = includeModule
|
|
325
460
|
includeModule('core')
|
|
461
|
+
validateCoalesceMixing(node) // ES2020: reject unparenthesized `??` mixed with `||`/`&&`
|
|
326
462
|
normalizeIdents(node)
|
|
327
463
|
fuseSparseMapReads(node) // AST-level fusion; needs pre-resolution shape — defined at end of file
|
|
328
464
|
seedStaticGlobalAssignments(node)
|
|
@@ -419,9 +555,9 @@ export default function prepare(node) {
|
|
|
419
555
|
return ast
|
|
420
556
|
}
|
|
421
557
|
|
|
422
|
-
// Named constants → numeric literals
|
|
423
|
-
|
|
424
|
-
const CONSTANTS = { 'true': true, 'false': false, 'null': JZ_NULL, 'undefined':
|
|
558
|
+
// Named constants → numeric literals. The JZ_NULL/JZ_UNDEF atom sentinels live
|
|
559
|
+
// in ast.js — shared with emit without crossing the prepare↔compile boundary.
|
|
560
|
+
const CONSTANTS = { 'true': true, 'false': false, 'null': JZ_NULL, 'undefined': JZ_UNDEF }
|
|
425
561
|
// NaN/Infinity stay as special f64 values in emit()
|
|
426
562
|
const F64_CONSTANTS = { 'NaN': NaN, 'Infinity': Infinity }
|
|
427
563
|
|
|
@@ -465,7 +601,43 @@ function deleteStaticGlobal(name) {
|
|
|
465
601
|
ctx.scope.shapeStrArrays?.delete(name)
|
|
466
602
|
}
|
|
467
603
|
|
|
604
|
+
// Schema id when prhs is a bare object literal with static keys, else null.
|
|
605
|
+
function objLiteralSid(prhs) {
|
|
606
|
+
if (!Array.isArray(prhs) || prhs[0] !== '{}') return null
|
|
607
|
+
const props = staticObjectProps(prhs.slice(1))
|
|
608
|
+
return props ? ctx.schema.register(props.names) : null
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Shape-consensus accounting for every `name = …` assignment. `sid` is the
|
|
612
|
+
// RHS literal's schema id (null for any non-literal source). A name's schema
|
|
613
|
+
// binds only while ALL its assignments agree on that one literal shape: the
|
|
614
|
+
// first literal binds; any disagreeing assignment — non-literal RHS or a
|
|
615
|
+
// different-shape literal — unbinds and poisons. Poisoned names never rebind,
|
|
616
|
+
// so compile-time fixed-slot reads can't be aimed at one shape while the
|
|
617
|
+
// variable holds another (the misread class: `.x` returning a foreign
|
|
618
|
+
// object's slot-0 value). Compile consumes the END state — order-insensitive.
|
|
619
|
+
function bindAssignSchema(name, sid) {
|
|
620
|
+
const had = ctx.schema.vars.get(name)
|
|
621
|
+
if (had != null) {
|
|
622
|
+
if (had !== sid) { ctx.schema.vars.delete(name); ctx.schema.poisoned?.add(name) }
|
|
623
|
+
} else if (sid != null) {
|
|
624
|
+
if (!ctx.schema.poisoned?.has(name)) ctx.schema.vars.set(name, sid)
|
|
625
|
+
} else ctx.schema.poisoned?.add(name)
|
|
626
|
+
}
|
|
627
|
+
|
|
468
628
|
const hasFunc = name => ctx.func.names.has(name)
|
|
629
|
+
// A builtin name (`Map`, `Array`, `Math`, …) is shadowed when the user bound it
|
|
630
|
+
// as a local (let/const/param, via `isDeclared`), a top-level function (via
|
|
631
|
+
// `hasFunc`), or a top-level let/const global (via `userGlobals`). A shadowed
|
|
632
|
+
// name must resolve to the user binding, so the constructor / named-call
|
|
633
|
+
// fast-paths bail and fall through to `resolveCallee`, which already routes a
|
|
634
|
+
// declared name to its local value. Mirrors the guard in
|
|
635
|
+
// `foldNamespaceIntrospection`.
|
|
636
|
+
const shadowsBuiltin = name => typeof name === 'string' &&
|
|
637
|
+
((scopes.length && isDeclared(name)) || hasFunc(name) || ctx.scope.userGlobals?.has?.(name))
|
|
638
|
+
// A local bound to a function literal in any active arrow scope (the nested-
|
|
639
|
+
// closure counterpart to `hasFunc`, which only knows depth-0 lifted functions).
|
|
640
|
+
const isFuncValueLocal = name => typeof name === 'string' && funcValueNames.some(s => s.has(name))
|
|
469
641
|
|
|
470
642
|
const renameFunc = (func, nextName) => {
|
|
471
643
|
ctx.func.names.delete(func.name)
|
|
@@ -473,8 +645,8 @@ const renameFunc = (func, nextName) => {
|
|
|
473
645
|
ctx.func.names.add(nextName)
|
|
474
646
|
}
|
|
475
647
|
|
|
476
|
-
|
|
477
|
-
|
|
648
|
+
// `typeof`-string → code table lives in ast.js (TYPEOF) — shared with
|
|
649
|
+
// emitTypeofCmp and flow-types so the codes have one home.
|
|
478
650
|
// Spec §13.5.3: `typeof undeclared_x` returns 'undefined' without throwing.
|
|
479
651
|
// True iff `name` is a bare identifier with no resolution path. Mirrors the
|
|
480
652
|
// resolution chain inside `prep()` so we don't speculate emit-time failures.
|
|
@@ -482,7 +654,7 @@ function isUnresolvableBareIdent(name) {
|
|
|
482
654
|
if (typeof name !== 'string') return false
|
|
483
655
|
if (name in CONSTANTS || name in F64_CONSTANTS) return false
|
|
484
656
|
if (name === 'Boolean' || name === 'Number') return false
|
|
485
|
-
if (
|
|
657
|
+
if (REJECT_IDENTS[name]) return false
|
|
486
658
|
if (scopes.length && isDeclared(name)) return false
|
|
487
659
|
if (ctx.scope.chain[name]) return false
|
|
488
660
|
if (GLOBALS[name]) return false
|
|
@@ -501,9 +673,9 @@ function staticTypeofString(x) {
|
|
|
501
673
|
// Spec §13.5.3: unresolvable bare ref → 'undefined'.
|
|
502
674
|
if (isUnresolvableBareIdent(x)) return 'undefined'
|
|
503
675
|
// Bare callable global: parseInt, parseFloat, isNaN, isFinite, Error, BigInt, etc.
|
|
504
|
-
if (typeof x === 'string' && !ctx.func?.locals?.has(x) && GLOBALS[x] && ctx.core.emit?.[x]
|
|
676
|
+
if (typeof x === 'string' && !ctx.func?.locals?.has(x) && GLOBALS[x] && emitArity(ctx.core.emit?.[x]) > 0) return 'function'
|
|
505
677
|
const px = prep(x)
|
|
506
|
-
if (typeof px === 'string' && px.includes('.') && ctx.core.emit?.[px]
|
|
678
|
+
if (typeof px === 'string' && px.includes('.') && emitArity(ctx.core.emit?.[px]) > 0) return 'function'
|
|
507
679
|
return null
|
|
508
680
|
}
|
|
509
681
|
// Builtin-namespace constructors expose `prototype`/`length`/`name` as own
|
|
@@ -527,14 +699,14 @@ function resolveTypeof(node) {
|
|
|
527
699
|
if (Array.isArray(a) && a[0] === 'typeof' && Array.isArray(b) && b[0] == null && typeof b[1] === 'string') {
|
|
528
700
|
const known = staticTypeofString(a[1])
|
|
529
701
|
if (known != null) return [, eqLike ? known === b[1] : known !== b[1]]
|
|
530
|
-
const code =
|
|
702
|
+
const code = TYPEOF[b[1]]
|
|
531
703
|
if (code != null) return [op, ['typeof', a[1]], [, code]]
|
|
532
704
|
}
|
|
533
705
|
// 'string' == typeof x
|
|
534
706
|
if (Array.isArray(b) && b[0] === 'typeof' && Array.isArray(a) && a[0] == null && typeof a[1] === 'string') {
|
|
535
707
|
const known = staticTypeofString(b[1])
|
|
536
708
|
if (known != null) return [, eqLike ? known === a[1] : known !== a[1]]
|
|
537
|
-
const code =
|
|
709
|
+
const code = TYPEOF[a[1]]
|
|
538
710
|
if (code != null) return [op, ['typeof', b[1]], [, code]]
|
|
539
711
|
}
|
|
540
712
|
return node
|
|
@@ -595,11 +767,19 @@ function prep(node) {
|
|
|
595
767
|
if (typeof node === 'string') {
|
|
596
768
|
if (node in CONSTANTS) return [, CONSTANTS[node]]
|
|
597
769
|
if (node in F64_CONSTANTS) return [, F64_CONSTANTS[node]]
|
|
598
|
-
if (
|
|
770
|
+
if (REJECT_IDENTS[node]) err(REJECT_IDENTS[node])
|
|
599
771
|
// Boolean/Number as value → identity arrow (for .filter(Boolean), .map(Number) etc.)
|
|
600
772
|
if (node === 'Boolean' || node === 'Number') { includeForCallableValue(); return ['=>', 'x', 'x'] }
|
|
601
773
|
// Block locals shadow module imports/globals, even when the local keeps the same name.
|
|
602
774
|
if (scopes.length && isDeclared(node)) return resolveScope(node)
|
|
775
|
+
// A user top-level binding (`let Math = …`) shadows a same-named builtin
|
|
776
|
+
// namespace seeded into the scope chain (`Math → math`). Resolve to the
|
|
777
|
+
// user global, not the builtin. (Mangled globals drop their original name
|
|
778
|
+
// from userGlobals, so this fires only for un-renamed user bindings.)
|
|
779
|
+
if (ctx.scope.userGlobals?.has?.(node)) return node
|
|
780
|
+
// Host numeric constant (`Math.PI` etc.) → fold to its f64 literal. Placed after the
|
|
781
|
+
// local/user-global checks above so a same-named binding still shadows it.
|
|
782
|
+
if (ctx.scope.hostConsts && node in ctx.scope.hostConsts) return [, ctx.scope.hostConsts[node]]
|
|
603
783
|
const resolved = ctx.scope.chain[node]
|
|
604
784
|
if (resolved?.includes('.')) return resolved
|
|
605
785
|
// Cross-module import: mangled name (e.g. __util_js$clone)
|
|
@@ -612,6 +792,10 @@ function prep(node) {
|
|
|
612
792
|
|
|
613
793
|
const [op, ...args] = node
|
|
614
794
|
if (op === 'void' && ctx.transform.strict) err('strict mode: `void` is prohibited. It diverges from JS by evaluating to 0.')
|
|
795
|
+
// jz's `==`/`!=` already never coerce (identical to `===`/`!==`), so default mode accepts them.
|
|
796
|
+
// strict enforces the canonical subset, where `===`/`!==` are the one spelling — reject the loose form.
|
|
797
|
+
if ((op === '==' || op === '!=') && ctx.transform.strict)
|
|
798
|
+
err(`strict mode: \`${op}\` is prohibited — use \`${op}=\`. (jz's \`${op}\` doesn't coerce, but the canonical subset is \`===\`/\`!==\` only.)`)
|
|
615
799
|
if (op == null) {
|
|
616
800
|
if (typeof args[0] === 'string') {
|
|
617
801
|
includeForStringValue()
|
|
@@ -623,19 +807,9 @@ function prep(node) {
|
|
|
623
807
|
return handler ? handler(...args) : [op, ...args.map(prep)]
|
|
624
808
|
}
|
|
625
809
|
|
|
626
|
-
//
|
|
627
|
-
// only reach this point when jzify is off — jzify lowers `class` to a factory
|
|
628
|
-
// arrow and rewrites `arguments` to a rest param before prepare runs. The
|
|
629
|
-
// remainder (`with`, `this`, `super`, `yield`, `eval`) have no safe lowering
|
|
630
|
-
// and stay errors in both modes.
|
|
631
|
-
const PROHIBITED = { 'with': '`with` not supported', 'class': '`class` not supported', 'yield': '`yield` not supported',
|
|
632
|
-
'this': '`this` not supported: use explicit parameter',
|
|
633
|
-
'super': '`super` not supported: no class inheritance',
|
|
634
|
-
'arguments': '`arguments` not supported: use rest params',
|
|
635
|
-
'eval': '`eval` not supported'
|
|
636
|
-
}
|
|
810
|
+
// Identifier prohibitions: op-policy.js REJECT_IDENTS (prep string nodes).
|
|
637
811
|
|
|
638
|
-
// Predefined globals seeded into scope.chain at ctx.reset().
|
|
812
|
+
// Predefined globals seeded into scope.chain at ctx.reset().
|
|
639
813
|
// used in ctx.core.emit[]. Dotted lookups (Math.sin) go through the '.' handler which
|
|
640
814
|
// resolves via scope.chain → module 'math' → registers 'math.sin' emitter.
|
|
641
815
|
// Not actually "implicit imports" — these are ambient globals that exist in every jz/JS
|
|
@@ -676,6 +850,13 @@ export const GLOBALS = {
|
|
|
676
850
|
const patternItems = (node) => node?.[0] === ',' ? node.slice(1) : [node]
|
|
677
851
|
const isDestructPattern = (node) => Array.isArray(node) && (node[0] === '[]' || node[0] === '{}')
|
|
678
852
|
|
|
853
|
+
// Element count of a prepared inline array literal `['[', e0, e1, …]` with no
|
|
854
|
+
// spread (spread → dynamic length). Returns null when not such a literal, so
|
|
855
|
+
// destructuring a non-literal source keeps its runtime element reads.
|
|
856
|
+
const inlineArrayLen = (e) =>
|
|
857
|
+
Array.isArray(e) && e[0] === '[' && !e.slice(1).some(x => Array.isArray(x) && x[0] === '...')
|
|
858
|
+
? e.length - 1 : null
|
|
859
|
+
|
|
679
860
|
const simpleArrayPatternItems = (pattern) => {
|
|
680
861
|
if (!Array.isArray(pattern) || pattern[0] !== '[]' || pattern.length !== 2) return null
|
|
681
862
|
const items = patternItems(pattern[1])
|
|
@@ -707,7 +888,7 @@ function scalarArrayDestruct(pattern, rhs) {
|
|
|
707
888
|
function declareGlobal(name, user = true) {
|
|
708
889
|
if (depth !== 0 || typeof name !== 'string') return name
|
|
709
890
|
if (ctx.scope.globals.has(name)) err(`'${name}' conflicts with a compiler internal — choose a different name`)
|
|
710
|
-
|
|
891
|
+
declGlobal(name, 'f64')
|
|
711
892
|
if (user) ctx.scope.userGlobals.add(name)
|
|
712
893
|
return name
|
|
713
894
|
}
|
|
@@ -742,7 +923,7 @@ function pushPatternAssign(target, valueExpr, out, decls = null) {
|
|
|
742
923
|
out.push(['=', target, valueExpr])
|
|
743
924
|
}
|
|
744
925
|
|
|
745
|
-
function expandDestruct(pattern, source, out, decls = null) {
|
|
926
|
+
function expandDestruct(pattern, source, out, decls = null, srcLen = null) {
|
|
746
927
|
if (!isDestructPattern(pattern)) return
|
|
747
928
|
|
|
748
929
|
if (pattern[0] === '[]') {
|
|
@@ -757,6 +938,15 @@ function expandDestruct(pattern, source, out, decls = null) {
|
|
|
757
938
|
continue
|
|
758
939
|
}
|
|
759
940
|
|
|
941
|
+
// Source is a known-length inline literal and this index is past its end →
|
|
942
|
+
// the element is statically `undefined` (so any `= default` applies). Folding
|
|
943
|
+
// it here skips a provably out-of-range read — which both avoids the runtime
|
|
944
|
+
// access and dodges an optimizer miscompile of the destructuring-temp shape.
|
|
945
|
+
if (srcLen != null && j >= srcLen) {
|
|
946
|
+
pushPatternAssign(item, [, JZ_UNDEF], out, decls)
|
|
947
|
+
continue
|
|
948
|
+
}
|
|
949
|
+
|
|
760
950
|
pushPatternAssign(item, ['[]', source, [, j]], out, decls)
|
|
761
951
|
}
|
|
762
952
|
return
|
|
@@ -837,7 +1027,7 @@ function prepDecl(op, ...inits) {
|
|
|
837
1027
|
ctx.scope.chain[i] = declName
|
|
838
1028
|
}
|
|
839
1029
|
if (ctx.scope.globals.has(declName)) err(`'${declName}' conflicts with a compiler internal — choose a different name`)
|
|
840
|
-
|
|
1030
|
+
declGlobal(declName, 'f64')
|
|
841
1031
|
ctx.scope.userGlobals.add(declName)
|
|
842
1032
|
} else if (typeof declName === 'string') {
|
|
843
1033
|
// Bare hoisted decl inside a function (var X jzified to `let X` at top
|
|
@@ -852,6 +1042,23 @@ function prepDecl(op, ...inits) {
|
|
|
852
1042
|
continue
|
|
853
1043
|
}
|
|
854
1044
|
const [, name, init] = i
|
|
1045
|
+
// `const alias = fn` whose RHS is a bare identifier naming a known function
|
|
1046
|
+
// is a compile-time function alias — the ES `export { fn as alias }` written
|
|
1047
|
+
// in declaration form (a recurring kernel idiom: paramList = extractParams,
|
|
1048
|
+
// toBoolFromEmitted = truthyIR …). Resolve `alias` straight to the function
|
|
1049
|
+
// so calls compile to a direct call and the export table re-exports the same
|
|
1050
|
+
// mangled func. Otherwise it would box a closure into a module global that a
|
|
1051
|
+
// cross-module callee resolves to the bare, unmangled name → "not in scope".
|
|
1052
|
+
// Module scope + `const` only: depth>0 aliases already work as closure values,
|
|
1053
|
+
// and a reassignable `let` is a genuine value binding, not an alias.
|
|
1054
|
+
if (op === 'const' && depth === 0 && typeof name === 'string' && typeof init === 'string') {
|
|
1055
|
+
const fn = hasFunc(init) ? init : (hasFunc(ctx.scope.chain[init]) ? ctx.scope.chain[init] : null)
|
|
1056
|
+
if (fn) {
|
|
1057
|
+
ctx.scope.chain[name] = fn
|
|
1058
|
+
if (name in ctx.func.exports) ctx.func.exports[name] = fn
|
|
1059
|
+
continue
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
855
1062
|
const staticStr = op === 'const' ? staticStringExpr(init) : null
|
|
856
1063
|
const staticArr = op === 'const' ? staticStringArrayValues(init) : null
|
|
857
1064
|
const normed = prep(init)
|
|
@@ -887,7 +1094,7 @@ function prepDecl(op, ...inits) {
|
|
|
887
1094
|
const p = normed.slice(1).filter(p => Array.isArray(p) && p[0] === ':').map(p => p[1])
|
|
888
1095
|
if (p.length) ctx.schema.vars.set(tmp, ctx.schema.register(p))
|
|
889
1096
|
}
|
|
890
|
-
expandDestruct(name, tmp, rest)
|
|
1097
|
+
expandDestruct(name, tmp, rest, null, inlineArrayLen(normed))
|
|
891
1098
|
continue
|
|
892
1099
|
}
|
|
893
1100
|
|
|
@@ -905,6 +1112,10 @@ function prepDecl(op, ...inits) {
|
|
|
905
1112
|
scopes[scopes.length - 1].set(name, name)
|
|
906
1113
|
}
|
|
907
1114
|
if (typeof declName === 'string' && fnNames) fnNames.add(declName)
|
|
1115
|
+
// A nested arrow stays a closure value (defFunc only lifts depth-0). Record
|
|
1116
|
+
// the binding so `.caller`/`.callee` on it reads as prohibited introspection.
|
|
1117
|
+
if (typeof declName === 'string' && Array.isArray(normed) && normed[0] === '=>')
|
|
1118
|
+
funcValueNames[funcValueNames.length - 1]?.add(declName)
|
|
908
1119
|
if (op === 'const') bindStaticConst(declName, staticStr, staticArr)
|
|
909
1120
|
// Track const for reassignment checks — only module-scope consts (depth 0)
|
|
910
1121
|
if (typeof declName === 'string' && depth === 0) {
|
|
@@ -935,20 +1146,29 @@ function prepDecl(op, ...inits) {
|
|
|
935
1146
|
// Track object schemas (after prefix so schema is keyed to final name)
|
|
936
1147
|
if (typeof declName === 'string' && Array.isArray(normed) && normed[0] === '{}' && normed.length > 1) {
|
|
937
1148
|
const props = []
|
|
1149
|
+
const addProp = n => { if (!props.includes(n)) props.push(n) }
|
|
1150
|
+
let allKnown = true
|
|
938
1151
|
for (const p of normed.slice(1)) {
|
|
939
|
-
|
|
1152
|
+
// Dedupe every key (explicit AND spread-sourced) so a `k: v` that overrides
|
|
1153
|
+
// a spread-provided key doesn't push a duplicate — that would shift the
|
|
1154
|
+
// indices of later keys past emitObjectSpread's deduped slot assignment
|
|
1155
|
+
// (its `addName` dedupes both), making `decl.laterKey` read the wrong slot.
|
|
1156
|
+
if (Array.isArray(p) && p[0] === ':') addProp(p[1])
|
|
940
1157
|
else if (Array.isArray(p) && p[0] === '...') {
|
|
941
|
-
// Merge spread source schema into this object's schema
|
|
942
1158
|
const srcSchema = typeof p[1] === 'string' && ctx.schema.resolve(p[1])
|
|
943
|
-
if (srcSchema) for (const n of srcSchema)
|
|
1159
|
+
if (srcSchema) for (const n of srcSchema) addProp(n)
|
|
1160
|
+
else allKnown = false
|
|
944
1161
|
}
|
|
945
1162
|
}
|
|
946
|
-
|
|
1163
|
+
// An unknown spread source makes the value a runtime HASH (see
|
|
1164
|
+
// emitObjectSpread). Binding a static schema would compile `decl.prop`
|
|
1165
|
+
// to a fixed slot load that misreads the hash, so leave reads dynamic.
|
|
1166
|
+
if (allKnown && props.length && ctx.schema.register) ctx.schema.vars.set(declName, ctx.schema.register(props))
|
|
947
1167
|
}
|
|
948
1168
|
// Module-scope variable → WASM global (mark as user-declared)
|
|
949
1169
|
if (depth === 0 && typeof declName === 'string') {
|
|
950
1170
|
if (ctx.scope.globals.has(declName)) err(`'${declName}' conflicts with a compiler internal — choose a different name`)
|
|
951
|
-
|
|
1171
|
+
declGlobal(declName, 'f64')
|
|
952
1172
|
ctx.scope.userGlobals.add(declName)
|
|
953
1173
|
}
|
|
954
1174
|
rest.push(['=', declName, normed])
|
|
@@ -965,7 +1185,7 @@ function prepDecl(op, ...inits) {
|
|
|
965
1185
|
// `import.meta.resolve("spec")` → the resolved URL as a static string.
|
|
966
1186
|
function foldImportMetaResolve(callee, args) {
|
|
967
1187
|
if (!isImportMetaProp(callee, 'resolve')) return undefined
|
|
968
|
-
const callArgs =
|
|
1188
|
+
const callArgs = handlerArgs(args)
|
|
969
1189
|
if (callArgs.length !== 1) err('`import.meta.resolve` requires one string literal argument')
|
|
970
1190
|
const spec = stringValue(callArgs[0])
|
|
971
1191
|
if (spec == null) err('`import.meta.resolve` supports only string literal arguments')
|
|
@@ -979,8 +1199,11 @@ function foldImportMetaResolve(callee, args) {
|
|
|
979
1199
|
// Returns the replacement IR, or `undefined` for an ordinary call.
|
|
980
1200
|
function dispatchConstructorCall(callee, args) {
|
|
981
1201
|
if (typeof callee !== 'string') return undefined
|
|
1202
|
+
// A user binding named like a constructor (`let Map = …`, `let Array = …`)
|
|
1203
|
+
// shadows the builtin — don't lower `Map(x)` to `new.Map`.
|
|
1204
|
+
if (shadowsBuiltin(callee)) return undefined
|
|
982
1205
|
if (callee === 'Array') {
|
|
983
|
-
const callArgs =
|
|
1206
|
+
const callArgs = handlerArgs(args)
|
|
984
1207
|
if (callArgs.length === 1) return handlers['new'](['()', callee, callArgs[0]])
|
|
985
1208
|
}
|
|
986
1209
|
if (CTORS.includes(callee)) return handlers['new'](['()', callee, ...args])
|
|
@@ -998,7 +1221,7 @@ function foldNamespaceIntrospection(callee, args) {
|
|
|
998
1221
|
if (!Array.isArray(callee) || callee[0] !== '.') return undefined
|
|
999
1222
|
const [, obj, prop] = callee
|
|
1000
1223
|
if (obj === 'Array' && prop === 'isArray') {
|
|
1001
|
-
const cargs =
|
|
1224
|
+
const cargs = handlerArgs(args)
|
|
1002
1225
|
const a0 = cargs.length === 1 ? cargs[0] : null
|
|
1003
1226
|
if (typeof a0 === 'string' && GLOBALS[a0] && !(scopes.length && isDeclared(a0)) && !hasFunc(a0))
|
|
1004
1227
|
return [, 0]
|
|
@@ -1006,7 +1229,7 @@ function foldNamespaceIntrospection(callee, args) {
|
|
|
1006
1229
|
if (prop === 'hasOwnProperty' && typeof obj === 'string' && !(scopes.length && isDeclared(obj))) {
|
|
1007
1230
|
const mod = ctx.scope.chain[obj]
|
|
1008
1231
|
if (mod && !mod.includes('.') && hasModule(mod)) {
|
|
1009
|
-
const cargs =
|
|
1232
|
+
const cargs = handlerArgs(args)
|
|
1010
1233
|
const member = cargs.length === 1 ? stringValue(cargs[0]) : null
|
|
1011
1234
|
// Include the module so its emit keys (the namespace's member set) are
|
|
1012
1235
|
// registered; unreferenced emitters/data dead-strip in compile.
|
|
@@ -1020,6 +1243,11 @@ function foldNamespaceIntrospection(callee, args) {
|
|
|
1020
1243
|
// way: a bare identifier through the scope chain, an `obj.prop` member call
|
|
1021
1244
|
// through host imports / named-call / generic-method / namespace tables, and
|
|
1022
1245
|
// any other expression through `prep` (a callable runtime value).
|
|
1246
|
+
// Compiler-internal synthetic callees: emit-handled intrinsics, never user
|
|
1247
|
+
// function values — so a bare reference must not pull in the callable-value
|
|
1248
|
+
// (function table / closure) machinery.
|
|
1249
|
+
const INTRINSIC_CALLEES = new Set(['__iter_arr'])
|
|
1250
|
+
|
|
1023
1251
|
function resolveCallee(callee, args) {
|
|
1024
1252
|
if (typeof callee === 'string') {
|
|
1025
1253
|
const local = scopes.length && isDeclared(callee)
|
|
@@ -1031,12 +1259,23 @@ function resolveCallee(callee, args) {
|
|
|
1031
1259
|
if (hasModule(resolved) && !ctx.module.imports.some(i => i[3]?.[1] === `$${resolved}`)) includeModule(resolved)
|
|
1032
1260
|
return callee
|
|
1033
1261
|
}
|
|
1034
|
-
if (depth > 0 && !resolved && !ctx.func.exports[callee] && !ctx.module.imports.some(i => i[3]?.[1] === `$${callee}`))
|
|
1262
|
+
if (depth > 0 && !resolved && !INTRINSIC_CALLEES.has(callee) && !ctx.func.exports[callee] && !ctx.module.imports.some(i => i[3]?.[1] === `$${callee}`))
|
|
1035
1263
|
includeForCallableValue()
|
|
1036
1264
|
return callee
|
|
1037
1265
|
}
|
|
1038
1266
|
if (Array.isArray(callee) && callee[0] === '.') {
|
|
1039
1267
|
const [, obj, prop] = callee
|
|
1268
|
+
// A user binding named like a builtin namespace (`let Math = {…}`) shadows
|
|
1269
|
+
// it — resolve `Math.max(…)` as a method call on the local value, not the
|
|
1270
|
+
// builtin named-call. (Property reads route through the `.` handler's own
|
|
1271
|
+
// shadow check.)
|
|
1272
|
+
if (shadowsBuiltin(obj)) return prep(callee)
|
|
1273
|
+
// SIMD intrinsic namespaces resolve members directly to their emit key, ahead of
|
|
1274
|
+
// generic-method dispatch — they're pure namespaces (never runtime values), and
|
|
1275
|
+
// names like `f32x4.add` must not be mistaken for the generic `.add` (Set/Map).
|
|
1276
|
+
if (typeof obj === 'string' && typeof prop === 'string' && SIMD_NS.has(obj) && !(scopes.length && isDeclared(obj)) && !ctx.scope.userGlobals?.has?.(obj)) {
|
|
1277
|
+
includeModule(obj); return `${obj}.${prop}`
|
|
1278
|
+
}
|
|
1040
1279
|
const key = typeof obj === 'string' && typeof prop === 'string' ? `${obj}.${prop}` : null
|
|
1041
1280
|
if (key && ctx.module.hostImports?.[obj]?.[prop]) {
|
|
1042
1281
|
const spec = ctx.module.hostImports[obj][prop]
|
|
@@ -1071,18 +1310,13 @@ function renestSoleCommaArg(args) {
|
|
|
1071
1310
|
}
|
|
1072
1311
|
|
|
1073
1312
|
const handlers = {
|
|
1313
|
+
...rejectHandlers(err),
|
|
1074
1314
|
// Spread operator: [...expr] in arrays, f(...args) in calls, {...obj} in objects
|
|
1075
1315
|
'...'(expr) {
|
|
1076
1316
|
includeForArrayLiteral()
|
|
1077
1317
|
return ['...', prep(expr)]
|
|
1078
1318
|
},
|
|
1079
1319
|
|
|
1080
|
-
// Prohibited ops — duplicated from jzify deliberately: .jz source bypasses jzify,
|
|
1081
|
-
// so prepare is the actual defense. Messages here fire for both .js and .jz.
|
|
1082
|
-
'async': () => err('async/await not supported: WASM is synchronous'),
|
|
1083
|
-
'await': () => err('async/await not supported: WASM is synchronous'),
|
|
1084
|
-
'class': () => err('class not supported: use object literals'),
|
|
1085
|
-
'yield': () => err('generators not supported: use loops'),
|
|
1086
1320
|
'debugger': () => null,
|
|
1087
1321
|
// Static-key delete (.x, ["x"], [literal]) would change the fixed schema → reject.
|
|
1088
1322
|
// Computed-key delete (obj[expr]) — including jessie's `delete ctx[k]` — lowers
|
|
@@ -1097,12 +1331,7 @@ const handlers = {
|
|
|
1097
1331
|
err('delete not supported: object shape is fixed')
|
|
1098
1332
|
},
|
|
1099
1333
|
'in'(key, obj) { return ['in', prep(key), prep(obj)] },
|
|
1100
|
-
'instanceof': () => err('instanceof not supported: use typeof'),
|
|
1101
|
-
'with': () => err('`with` not supported: deprecated'),
|
|
1102
|
-
':': () => err('labeled statements not supported'),
|
|
1103
1334
|
'label'(name, body) { return ['label', name, prep(body)] },
|
|
1104
|
-
'var': () => err('`var` not supported: use let/const'),
|
|
1105
|
-
'function': () => err('`function` not supported: use arrow functions'),
|
|
1106
1335
|
|
|
1107
1336
|
// Destructuring assignment: [a, ...b] = expr or {x, y} = expr
|
|
1108
1337
|
'='(lhs, rhs) {
|
|
@@ -1140,7 +1369,7 @@ const handlers = {
|
|
|
1140
1369
|
// emit a dynamic property read + indirect call instead of a direct call.
|
|
1141
1370
|
if (ctx.func.names.has(name)) {
|
|
1142
1371
|
ctx.func.multiProp.add(`${fnBase}.${lhs[2]}`)
|
|
1143
|
-
do name = `${fnBase}$${lhs[2]}$${ctx.func.uniq++}
|
|
1372
|
+
do { name = `${fnBase}$${lhs[2]}$${ctx.func.uniq++}` } while (ctx.func.names.has(name))
|
|
1144
1373
|
}
|
|
1145
1374
|
// Build the target `.` node directly from the resolved base — re-`prep`ing
|
|
1146
1375
|
// the lhs would resolve a multiProp `fn.prop` to an rvalue (closure
|
|
@@ -1158,22 +1387,25 @@ const handlers = {
|
|
|
1158
1387
|
recordGlobalRep(plhs, prhs)
|
|
1159
1388
|
if (Array.isArray(prhs) && prhs[0] === '{}') {
|
|
1160
1389
|
const props = staticObjectProps(prhs.slice(1))
|
|
1161
|
-
if (props)
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1390
|
+
if (props) bindAssignSchema(plhs, ctx.schema.register(props.names))
|
|
1391
|
+
} else bindAssignSchema(plhs, null)
|
|
1392
|
+
} else bindAssignSchema(plhs, objLiteralSid(prhs))
|
|
1164
1393
|
// Static string/array facts hold only while every assignment is constant.
|
|
1165
1394
|
if (!assignedStaticGlobals.has(plhs) && (staticStr != null || staticArr)) bindStaticGlobal(plhs, staticStr, staticArr)
|
|
1166
1395
|
else deleteStaticGlobal(plhs)
|
|
1167
1396
|
assignedStaticGlobals.add(plhs)
|
|
1168
1397
|
}
|
|
1169
|
-
//
|
|
1170
|
-
//
|
|
1171
|
-
//
|
|
1172
|
-
//
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1398
|
+
// Object-literal assignment to a variable — e.g. a `var` that jzify hoisted
|
|
1399
|
+
// into `let x; x = {…}`. Recording the schema lets the binding behave like
|
|
1400
|
+
// `let x = {…}`: fixed-slot field access and for-in unroll. SOUNDNESS: the
|
|
1401
|
+
// shape holds only while EVERY assignment to the name agrees — one literal
|
|
1402
|
+
// shape, no other sources. Any disagreeing assignment (non-literal RHS such
|
|
1403
|
+
// as a table/Map lookup, or a different-shape literal) unbinds and poisons
|
|
1404
|
+
// the name; fixed-slot reads against one literal's layout would misread the
|
|
1405
|
+
// other sources' objects (e.g. `.x` returning another shape's slot-0 value).
|
|
1406
|
+
// Compile reads the END state, so the conflict check is order-insensitive.
|
|
1407
|
+
else if (typeof plhs === 'string') {
|
|
1408
|
+
bindAssignSchema(plhs, objLiteralSid(prhs))
|
|
1177
1409
|
}
|
|
1178
1410
|
return ['=', plhs, prhs]
|
|
1179
1411
|
},
|
|
@@ -1184,18 +1416,12 @@ const handlers = {
|
|
|
1184
1416
|
const catchClause = clauses.find(c => Array.isArray(c) && c[0] === 'catch')
|
|
1185
1417
|
const finallyClause = clauses.find(c => Array.isArray(c) && c[0] === 'finally')
|
|
1186
1418
|
const tryBody = prep(body)
|
|
1419
|
+
// prep(handler) ONCE — it has side effects (uniq++, scope pushes, includes), so
|
|
1420
|
+
// the no-finally catch branch must reuse `caught`, not re-prep (FE-3 fix).
|
|
1187
1421
|
const caught = catchClause
|
|
1188
|
-
? (
|
|
1189
|
-
const [, errName, handler] = catchClause
|
|
1190
|
-
return ['catch', tryBody, errName, prep(handler)]
|
|
1191
|
-
})()
|
|
1422
|
+
? ['catch', tryBody, catchClause[1], prep(catchClause[2])]
|
|
1192
1423
|
: tryBody
|
|
1193
|
-
|
|
1194
|
-
if (catchClause) {
|
|
1195
|
-
const [, errName, handler] = catchClause
|
|
1196
|
-
return ['catch', tryBody, errName, prep(handler)]
|
|
1197
|
-
}
|
|
1198
|
-
return tryBody
|
|
1424
|
+
return finallyClause ? ['finally', caught, prep(finallyClause[1])] : caught
|
|
1199
1425
|
},
|
|
1200
1426
|
'throw'(expr) { return ['throw', prep(expr)] },
|
|
1201
1427
|
|
|
@@ -1231,6 +1457,24 @@ const handlers = {
|
|
|
1231
1457
|
return handlers['from'](fromNode[1], fromNode[2])
|
|
1232
1458
|
},
|
|
1233
1459
|
|
|
1460
|
+
// Mixed default+named import `import d, { n } from 'm'` — jessie emits it as a
|
|
1461
|
+
// statement-level comma `[',', ['import', d], ['from', spec, src]]` (the default
|
|
1462
|
+
// fragment lost its source). Reunite: bind the default, then the named specifiers,
|
|
1463
|
+
// both against the shared source. (prepareModule caches by specifier, so preparing
|
|
1464
|
+
// the source twice is a no-op — same as two separate `import` statements.)
|
|
1465
|
+
// Any other comma is a sequence expression: fall through to generic prep.
|
|
1466
|
+
','(...items) {
|
|
1467
|
+
if (items.length === 2
|
|
1468
|
+
&& Array.isArray(items[0]) && items[0][0] === 'import' && typeof items[0][1] === 'string'
|
|
1469
|
+
&& Array.isArray(items[1]) && items[1][0] === 'from') {
|
|
1470
|
+
const source = items[1][2]
|
|
1471
|
+
handlers['from'](items[0][1], source)
|
|
1472
|
+
handlers['from'](items[1][1], source)
|
|
1473
|
+
return null
|
|
1474
|
+
}
|
|
1475
|
+
return [',', ...items.map(prep)]
|
|
1476
|
+
},
|
|
1477
|
+
|
|
1234
1478
|
'from'(specifiers, source) {
|
|
1235
1479
|
const mod = source?.[1]
|
|
1236
1480
|
if (!mod || typeof mod !== 'string') return err('Invalid import source')
|
|
@@ -1289,8 +1533,8 @@ const handlers = {
|
|
|
1289
1533
|
}
|
|
1290
1534
|
|
|
1291
1535
|
// Tier 2: Source module (bundling)
|
|
1292
|
-
if (
|
|
1293
|
-
const resolved = prepareModule(mod, ctx.module.importSources[mod])
|
|
1536
|
+
if (isBundledModule(mod)) {
|
|
1537
|
+
const resolved = prepareModule(mod, ctx.module.importSources?.[mod])
|
|
1294
1538
|
// Default import: import name from 'mod' → bind to default export
|
|
1295
1539
|
if (typeof specifiers === 'string') {
|
|
1296
1540
|
const mangled = resolved.exports.get('default')
|
|
@@ -1357,16 +1601,27 @@ const handlers = {
|
|
|
1357
1601
|
|
|
1358
1602
|
// Block-scoped control flow: push scope for bodies so inner let/const shadows correctly
|
|
1359
1603
|
'if': (cond, then, els) => {
|
|
1360
|
-
const c = prep(cond)
|
|
1604
|
+
const c = prep(stripBoolNot(cond))
|
|
1361
1605
|
pushScope(); const t = prep(then); popScope()
|
|
1362
1606
|
if (els != null) { pushScope(); const e = prep(els); popScope(); return ['if', c, t, e] }
|
|
1363
1607
|
return ['if', c, t]
|
|
1364
1608
|
},
|
|
1365
1609
|
'while': (cond, body) => {
|
|
1366
|
-
const c = prep(cond)
|
|
1610
|
+
const c = prep(stripBoolNot(cond))
|
|
1367
1611
|
pushScope(); const b = prep(body); popScope()
|
|
1368
1612
|
return ['while', c, b]
|
|
1369
1613
|
},
|
|
1614
|
+
// do { body } while (cond) → flag-guarded while: `flag=true; while (flag||cond) { flag=false; body }`.
|
|
1615
|
+
// jzify lowers this in default mode (jzify/transform.js), but strict mode skips jzify — without
|
|
1616
|
+
// this prepare-stage twin, strict `do-while` reaches emit as a raw 'do' and dies ("Unknown op: do"),
|
|
1617
|
+
// contradicting the README's strict-subset list. Re-prep the synthetic tree so scope/normalize apply.
|
|
1618
|
+
'do': (body, cond) => {
|
|
1619
|
+
const flag = `${T}do${ctx.func.uniq++}`
|
|
1620
|
+
return prep([';',
|
|
1621
|
+
['let', ['=', flag, [null, true]]],
|
|
1622
|
+
['while', ['||', flag, cond],
|
|
1623
|
+
['{}', [';', ['=', flag, [null, false]], body]]]])
|
|
1624
|
+
},
|
|
1370
1625
|
|
|
1371
1626
|
'export': decl => {
|
|
1372
1627
|
if (Array.isArray(decl) && (decl[0] === 'let' || decl[0] === 'const'))
|
|
@@ -1378,8 +1633,8 @@ const handlers = {
|
|
|
1378
1633
|
const mod = decl[2]?.[1]
|
|
1379
1634
|
if (!mod || typeof mod !== 'string') return null
|
|
1380
1635
|
// Source module re-export
|
|
1381
|
-
if (
|
|
1382
|
-
const resolved = prepareModule(mod, ctx.module.importSources[mod])
|
|
1636
|
+
if (isBundledModule(mod)) {
|
|
1637
|
+
const resolved = prepareModule(mod, ctx.module.importSources?.[mod])
|
|
1383
1638
|
if (decl[1] === '*') {
|
|
1384
1639
|
// export * from './mod' → register all exports
|
|
1385
1640
|
for (const [name, mangled] of resolved.exports) {
|
|
@@ -1432,7 +1687,7 @@ const handlers = {
|
|
|
1432
1687
|
if (defFunc('default', prep(val))) return null
|
|
1433
1688
|
}
|
|
1434
1689
|
// export default expr → create global 'default'
|
|
1435
|
-
|
|
1690
|
+
declGlobal('default', 'f64')
|
|
1436
1691
|
ctx.scope.userGlobals.add('default')
|
|
1437
1692
|
return ['=', 'default', prep(val)]
|
|
1438
1693
|
}
|
|
@@ -1449,12 +1704,18 @@ const handlers = {
|
|
|
1449
1704
|
depth++
|
|
1450
1705
|
pushScope(fnScope)
|
|
1451
1706
|
funcLocalNames.push(new Set(collectParamNames(raw)))
|
|
1707
|
+
funcValueNames.push(new Set())
|
|
1452
1708
|
|
|
1453
1709
|
const nextParams = []
|
|
1454
1710
|
const bodyPrefix = []
|
|
1455
1711
|
for (const r of raw) {
|
|
1456
1712
|
const c = classifyParam(r)
|
|
1457
1713
|
if (c.kind === 'rest') {
|
|
1714
|
+
// A rest param is an array: the binding holds one, and every call site
|
|
1715
|
+
// builds the rest array via `['[', …]`. Pull in the array emitter even
|
|
1716
|
+
// when the body never names an array literal (e.g. `(...xs) => 0`),
|
|
1717
|
+
// otherwise the call-site rest construction hits "Unknown op: [".
|
|
1718
|
+
includeForArrayLiteral()
|
|
1458
1719
|
nextParams.push(r)
|
|
1459
1720
|
if (typeof c.name === 'string') fnScope.set(c.name, c.name)
|
|
1460
1721
|
} else if (c.kind === 'plain') {
|
|
@@ -1490,6 +1751,7 @@ const handlers = {
|
|
|
1490
1751
|
const result = ['=>', Array.isArray(params) && params[0] === '()' ? ['()', inner] : inner, preparedBody]
|
|
1491
1752
|
popScope()
|
|
1492
1753
|
funcLocalNames.pop()
|
|
1754
|
+
funcValueNames.pop()
|
|
1493
1755
|
depth--
|
|
1494
1756
|
return result
|
|
1495
1757
|
},
|
|
@@ -1509,9 +1771,13 @@ const handlers = {
|
|
|
1509
1771
|
})]
|
|
1510
1772
|
},
|
|
1511
1773
|
|
|
1512
|
-
// Optional chaining / typeof — need ptr module
|
|
1513
|
-
|
|
1514
|
-
'
|
|
1774
|
+
// Optional chaining / typeof — need ptr module. Optional member access pulls
|
|
1775
|
+
// the same modules as plain `.`/`[]` (a method like `includes` needs string +
|
|
1776
|
+
// array for emit's runtime dispatch); the only difference is the nullish guard,
|
|
1777
|
+
// which is emit's concern. Without this, `obj?.m(…)` reaches emit missing the
|
|
1778
|
+
// `.m` emitter and falls to the dynamic path that needs an unincluded module.
|
|
1779
|
+
'?.'(obj, prop) { includeForProperty(prop); return ['?.', prep(obj), prop] },
|
|
1780
|
+
'?.[]'(obj, idx) { includeForArrayAccess(); return ['?.[]', prep(obj), prep(idx)] },
|
|
1515
1781
|
'?.()'(callee, callArgs) {
|
|
1516
1782
|
// Parser wraps multi-args in a comma list, like '()'. Unwrap so emit gets flat positional args.
|
|
1517
1783
|
const items = callArgs == null ? []
|
|
@@ -1550,25 +1816,32 @@ const handlers = {
|
|
|
1550
1816
|
return ['+', pa, pb]
|
|
1551
1817
|
},
|
|
1552
1818
|
'-'(a, b) {
|
|
1553
|
-
|
|
1819
|
+
// Fold `-<numeric literal>` to a literal, but NOT a bigint: jz's own `typeof` reports
|
|
1820
|
+
// a bigint value as 'number' too (its carrier is an f64), so under self-host this test
|
|
1821
|
+
// alone wrongly folds `-5n`, and negating the bigint here yields garbage (-2^63+5).
|
|
1822
|
+
// `typeof !== 'bigint'` excludes it in both engines (real JS: 'bigint'; jz: matches
|
|
1823
|
+
// 'bigint'). Bigint negation then flows to emit's i64.sub(0,·) path correctly.
|
|
1824
|
+
if (b === undefined) { const na = prep(a); return isLit(na) && typeof na[1] === 'number' && typeof na[1] !== 'bigint' ? [, -na[1]] : ['u-', na] }
|
|
1554
1825
|
return ['-', prep(a), prep(b)]
|
|
1555
1826
|
},
|
|
1556
1827
|
|
|
1557
1828
|
// Ternary: parser emits '?' not '?:'
|
|
1558
|
-
'?'(cond, then, els) { return ['?:', prep(cond), prep(then), prep(els)] },
|
|
1829
|
+
'?'(cond, then, els) { return ['?:', prep(stripBoolNot(cond)), prep(then), prep(els)] },
|
|
1559
1830
|
|
|
1560
1831
|
// ++/-- prefix vs postfix: parser sends trailing null for postfix
|
|
1561
|
-
// Postfix i++ = (++i) - 1: increment happens, arithmetic recovers old value
|
|
1562
|
-
// Property
|
|
1832
|
+
// Postfix i++ = (++i) - 1: increment happens, arithmetic recovers old value.
|
|
1833
|
+
// Property obj.prop++ has no dedicated ++ node (the ++ emitter is name-based),
|
|
1834
|
+
// so it lowers to `obj.prop = obj.prop + 1` (returns the NEW value) — and the
|
|
1835
|
+
// same -1/+1 recovery wraps it for postfix to yield the OLD value.
|
|
1563
1836
|
'++'(a, _post) {
|
|
1564
1837
|
const n = prep(a)
|
|
1565
|
-
|
|
1566
|
-
return _post !== undefined ? ['-',
|
|
1838
|
+
const inc = Array.isArray(n) && (n[0] === '.' || n[0] === '[]') ? ['=', n, ['+', n, [, 1]]] : ['++', n]
|
|
1839
|
+
return _post !== undefined ? ['-', inc, [, 1]] : inc
|
|
1567
1840
|
},
|
|
1568
1841
|
'--'(a, _post) {
|
|
1569
1842
|
const n = prep(a)
|
|
1570
|
-
|
|
1571
|
-
return _post !== undefined ? ['+',
|
|
1843
|
+
const dec = Array.isArray(n) && (n[0] === '.' || n[0] === '[]') ? ['=', n, ['-', n, [, 1]]] : ['--', n]
|
|
1844
|
+
return _post !== undefined ? ['+', dec, [, 1]] : dec
|
|
1572
1845
|
},
|
|
1573
1846
|
|
|
1574
1847
|
// Regex literal: ['//','pattern','flags?'] → include regex module, pass through
|
|
@@ -1576,13 +1849,22 @@ const handlers = {
|
|
|
1576
1849
|
return ['//', pattern, flags]
|
|
1577
1850
|
},
|
|
1578
1851
|
|
|
1579
|
-
'**'(a, b) {
|
|
1852
|
+
'**'(a, b) {
|
|
1853
|
+
// ES2016 §13.6: an unparenthesized unary expression cannot be the base of `**`
|
|
1854
|
+
// — `-x**2`, `~x**2`, `!x**2`, `+x**2`, `typeof x**2`, `void x**2`, `delete o[k]**2`
|
|
1855
|
+
// are all SyntaxErrors (the precedence is ambiguous). The parser leaves a grouping
|
|
1856
|
+
// as `['()', …]`, so a parenthesized base `(-x)**2` (and `-(x**2)`, where the unary
|
|
1857
|
+
// sits outside the `**`) arrives with a non-unary root op and is allowed.
|
|
1858
|
+
if (Array.isArray(a) && a.length === 2 && (a[0] === '-' || a[0] === '+' || a[0] === '!' || a[0] === '~' || a[0] === 'typeof' || a[0] === 'void' || a[0] === 'delete'))
|
|
1859
|
+
err(`Unary '${a[0]}' before '**' is a SyntaxError (ES2016 §13.6) — parenthesize: (${a[0]} x) ** 2 or ${a[0]} (x ** 2)`)
|
|
1860
|
+
return ['**', prep(a), prep(b)]
|
|
1861
|
+
},
|
|
1580
1862
|
|
|
1581
1863
|
// Function call or grouping parens
|
|
1582
1864
|
'()'(callee, ...args) {
|
|
1583
1865
|
// Grouping: (expr) → ['()', expr] with no args. Call: f() → ['()', 'f', null] with null arg.
|
|
1584
1866
|
if (args.length === 0) return prep(callee)
|
|
1585
|
-
if (typeof callee === 'string' &&
|
|
1867
|
+
if (typeof callee === 'string' && REJECT_IDENTS[callee]) err(REJECT_IDENTS[callee])
|
|
1586
1868
|
|
|
1587
1869
|
// Compile-time folds: the callee names something resolvable now. Each fold
|
|
1588
1870
|
// is gated by callee shape, so at most one of the three fires.
|
|
@@ -1600,7 +1882,14 @@ const handlers = {
|
|
|
1600
1882
|
includeForCallableValue(); break
|
|
1601
1883
|
}
|
|
1602
1884
|
}
|
|
1603
|
-
|
|
1885
|
+
// A zero-arg call keeps its explicit `null` args slot: `['()', callee, null]`,
|
|
1886
|
+
// not the slot-less `['()', callee]`. The latter is indistinguishable from a
|
|
1887
|
+
// grouping `(expr)`, so a second `prep` pass (the destructuring-assignment
|
|
1888
|
+
// lowering re-`prep`s its result) would re-read `x.pop()` as the grouping
|
|
1889
|
+
// `(x.pop)` and drop the call. Keeping the slot makes `prep` idempotent for
|
|
1890
|
+
// calls and matches `setCallArgs`'s canonical shape; `commaList(node[2])`
|
|
1891
|
+
// reads it back as zero args everywhere downstream.
|
|
1892
|
+
const result = preppedArgs.length ? ['()', callee, ...preppedArgs] : ['()', callee, null]
|
|
1604
1893
|
|
|
1605
1894
|
if (callee === 'Object.assign' && ctx.schema.register) inferAssignSchema(result)
|
|
1606
1895
|
|
|
@@ -1642,9 +1931,11 @@ const handlers = {
|
|
|
1642
1931
|
},
|
|
1643
1932
|
|
|
1644
1933
|
// Object literal - flatten comma, expand shorthand
|
|
1645
|
-
'{}'(
|
|
1646
|
-
|
|
1647
|
-
|
|
1934
|
+
'{}'(...args) {
|
|
1935
|
+
const inner = args[0]
|
|
1936
|
+
// Block body: a single statement-op child (object props always start with
|
|
1937
|
+
// ':' or '...', never a statement op, so this never misfires on a literal).
|
|
1938
|
+
if (args.length === 1 && Array.isArray(inner) && STMT_OPS.has(inner[0])) {
|
|
1648
1939
|
// Block body: push block scope for let/const shadowing
|
|
1649
1940
|
pushScope()
|
|
1650
1941
|
const result = ['{}', prep(inner)]
|
|
@@ -1653,10 +1944,16 @@ const handlers = {
|
|
|
1653
1944
|
}
|
|
1654
1945
|
|
|
1655
1946
|
includeForObjectLiteral()
|
|
1656
|
-
if (inner == null) return ['{}']
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1947
|
+
if (args.length === 0 || inner == null) return ['{}']
|
|
1948
|
+
// The parser emits one comma-grouped child `['{}', [',', p1, p2]]`, but prep's
|
|
1949
|
+
// own output is spread `['{}', p1, p2]` (see `result` below). Accept both so
|
|
1950
|
+
// prep stays idempotent: the destructuring-assignment lowering ('=' handler)
|
|
1951
|
+
// re-preps a wrapper that already holds a normalized literal, and reading only
|
|
1952
|
+
// the first child here would drop every property but the first — mis-sizing the
|
|
1953
|
+
// schema to cap-1 and losing the rest.
|
|
1954
|
+
const items = args.length === 1
|
|
1955
|
+
? (Array.isArray(inner) && inner[0] === ',' ? inner.slice(1) : [inner])
|
|
1956
|
+
: args
|
|
1660
1957
|
|
|
1661
1958
|
// Computed keys: `{[k]: v}` where `k` isn't compile-time foldable. jz's
|
|
1662
1959
|
// object layout is slot-based (fixed schema at the literal site), so a
|
|
@@ -1692,6 +1989,12 @@ const handlers = {
|
|
|
1692
1989
|
if (key == null) err('computed property name not supported for fixed-shape object: use a compile-time string/number key')
|
|
1693
1990
|
return [':', key, prep(p[2])]
|
|
1694
1991
|
}
|
|
1992
|
+
// Accessors (`{ get x() {…} }` / `{ set x(v) {…} }`) parse to ['get'|'set', …].
|
|
1993
|
+
// jz objects are fixed-shape slot records with no accessor protocol, so they'd
|
|
1994
|
+
// otherwise fall through and compile to dead code (0 schema slots → `o.x` reads
|
|
1995
|
+
// undefined). Reject loudly — silent miscompile breaks "valid jz = valid JS".
|
|
1996
|
+
if (Array.isArray(p) && (p[0] === 'get' || p[0] === 'set'))
|
|
1997
|
+
err('object getter/setter not supported — jz objects have no accessors; use a method or a plain property + function')
|
|
1695
1998
|
return prep(p)
|
|
1696
1999
|
}
|
|
1697
2000
|
let prepped = items.map(prop)
|
|
@@ -1718,26 +2021,56 @@ const handlers = {
|
|
|
1718
2021
|
// For loop
|
|
1719
2022
|
'for'(head, body) {
|
|
1720
2023
|
pushScope()
|
|
2024
|
+
// A comma/sequence Expression in a for-IN head RHS — `for (x in a, b)` — is valid (the RHS is
|
|
2025
|
+
// an Expression): evaluate left-to-right for side effects, value as the last element. (for-OF's
|
|
2026
|
+
// RHS is an AssignmentExpression — no comma — so it is left alone.) jzify lands it as a bare `,`
|
|
2027
|
+
// node in the source slot. Don't wrap it in `()`: Object.keys((a, obj)) hides `obj` behind the
|
|
2028
|
+
// sequence and loses its static schema (a non-escaping literal scalarizes → 0 keys). Instead take
|
|
2029
|
+
// the LAST element as the (direct) iteration source and run the earlier elements once first.
|
|
2030
|
+
let forInSeqPre = null
|
|
2031
|
+
if (Array.isArray(head) && head[0] === 'in' && Array.isArray(head[2]) && head[2][0] === ',') {
|
|
2032
|
+
const parts = head[2].slice(1)
|
|
2033
|
+
head = [head[0], head[1], parts[parts.length - 1]]
|
|
2034
|
+
if (parts.length > 2) forInSeqPre = [',', ...parts.slice(0, -1)]
|
|
2035
|
+
else if (parts.length === 2) forInSeqPre = parts[0]
|
|
2036
|
+
}
|
|
1721
2037
|
let r
|
|
1722
2038
|
if (Array.isArray(head) && head[0] === ';') {
|
|
1723
2039
|
let [, init, cond, step] = head
|
|
1724
|
-
|
|
1725
|
-
//
|
|
1726
|
-
//
|
|
1727
|
-
//
|
|
1728
|
-
//
|
|
1729
|
-
//
|
|
1730
|
-
//
|
|
1731
|
-
//
|
|
2040
|
+
cond = stripBoolNot(cond)
|
|
2041
|
+
// Keep a `.length` / `.size` / `.byteLength` for-bound i32 without snapshotting it:
|
|
2042
|
+
// `i < arr.length` → `i < (arr.length | 0)` (re-read every iteration)
|
|
2043
|
+
// The `| 0` forces i32 even for unknown-typed receivers (where __length returns
|
|
2044
|
+
// f64), so the counter `i` stays i32 through the comparison and `i++` — no
|
|
2045
|
+
// per-iteration f64.convert_i32_s + f64.lt + f64.add + i32.trunc_sat round-trip.
|
|
2046
|
+
// It must stay INLINE, not hoisted into a pre-loop local: JS re-reads the bound
|
|
2047
|
+
// each step, and a loop body can grow/shrink the array mid-iteration — including
|
|
2048
|
+
// through an alias the compiler can't see locally (e.g. `arr` shares identity with
|
|
2049
|
+
// a field a called helper pushes to, as compilePendingClosures does over
|
|
2050
|
+
// ctx.closure.bodies). A snapshot diverges from JS and silently truncates such loops.
|
|
1732
2051
|
if (cond && Array.isArray(cond) && (cond[0] === '<' || cond[0] === '<=' || cond[0] === '>' || cond[0] === '>=')) {
|
|
1733
2052
|
const lenExpr = cond[0] === '<' || cond[0] === '<=' ? cond[2] : cond[1]
|
|
1734
2053
|
if (Array.isArray(lenExpr) && lenExpr[0] === '.' &&
|
|
1735
2054
|
(lenExpr[2] === 'length' || lenExpr[2] === 'size' || lenExpr[2] === 'byteLength')) {
|
|
1736
|
-
const
|
|
1737
|
-
const
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
2055
|
+
const recv = lenExpr[1]
|
|
2056
|
+
const bound = ['|', lenExpr, [, 0]]
|
|
2057
|
+
const lengthStable = typeof recv === 'string' &&
|
|
2058
|
+
boundSafeCalls(body) && boundSafeCalls(step) && !writesReceiver(body, recv) && !writesReceiver(step, recv)
|
|
2059
|
+
if (lengthStable) {
|
|
2060
|
+
// Body can't change the bound → snapshot it once into an i32 local. Keeps
|
|
2061
|
+
// the counter `i` i32 through compare + `i++` (no per-iteration f64 round
|
|
2062
|
+
// trip) and gives the vectorizer the hoisted trip count it matches on.
|
|
2063
|
+
const lenVar = `${T}len${ctx.func.uniq++}`
|
|
2064
|
+
const lenDecl = ['let', ['=', lenVar, bound]]
|
|
2065
|
+
init = init ? [';', init, lenDecl] : lenDecl
|
|
2066
|
+
if (cond[0] === '<' || cond[0] === '<=') cond = [cond[0], cond[1], lenVar]
|
|
2067
|
+
else cond = [cond[0], lenVar, cond[2]]
|
|
2068
|
+
} else {
|
|
2069
|
+
// Body may grow/shrink the array (push/pop, or alias mutation through a
|
|
2070
|
+
// call) → re-read every iteration, as JS does. Still `| 0` for an i32 bound.
|
|
2071
|
+
if (cond[0] === '<' || cond[0] === '<=') cond = [cond[0], cond[1], bound]
|
|
2072
|
+
else cond = [cond[0], bound, cond[2]]
|
|
2073
|
+
}
|
|
1741
2074
|
}
|
|
1742
2075
|
}
|
|
1743
2076
|
r = ['for', init ? prep(init) : null, cond ? prep(cond) : null, step ? prep(step) : null, prep(body)]
|
|
@@ -1749,61 +2082,64 @@ const handlers = {
|
|
|
1749
2082
|
const varName = Array.isArray(decl) && (decl[0] === 'let' || decl[0] === 'const') ? decl[1] : decl
|
|
1750
2083
|
const idx = `${T}i${ctx.func.uniq++}`
|
|
1751
2084
|
const lenVar = `${T}len${ctx.func.uniq++}`
|
|
1752
|
-
const
|
|
1753
|
-
|
|
2085
|
+
const arrVar = `${T}arr${ctx.func.uniq++}`
|
|
2086
|
+
// Normalize the source to an index-iterable once: a Set→keys / Map→[k,v]
|
|
2087
|
+
// array, while an Array/String/TypedArray passes through untouched (no
|
|
2088
|
+
// copy). Without this, `coll[i]` on a Set/Map reads raw open-addressing
|
|
2089
|
+
// slot words instead of live entries.
|
|
1754
2090
|
// Wrap .length in `| 0` so the hoisted bound is i32 even for unknown
|
|
1755
2091
|
// receivers (same rationale as the for-cond hoist above).
|
|
1756
2092
|
const lenE = ['|', ['.', arrVar, 'length'], [, 0]]
|
|
1757
|
-
const decls =
|
|
1758
|
-
? ['let', ['=', idx, [, 0]], ['=', lenVar, lenE]]
|
|
1759
|
-
: ['let', ['=', arrVar, src], ['=', idx, [, 0]], ['=', lenVar, lenE]]
|
|
2093
|
+
const decls = ['let', ['=', arrVar, ['()', '__iter_arr', src]], ['=', idx, [, 0]], ['=', lenVar, lenE]]
|
|
1760
2094
|
const cond = ['<', idx, lenVar]
|
|
1761
2095
|
const step = ['++', idx]
|
|
1762
2096
|
const inner = [';', ['let', ['=', varName, ['[]', arrVar, idx]]], body]
|
|
1763
2097
|
r = prep(['for', [';', decls, cond, step], inner])
|
|
1764
2098
|
} else if (Array.isArray(head) && head[0] === 'in') {
|
|
1765
|
-
// for
|
|
2099
|
+
// `for…in` relies on runtime key enumeration — outside the pure canonical subset. strict
|
|
2100
|
+
// rejects it (consistent with `obj[k]` / unknown-receiver methods); use `Object.keys(obj)`.
|
|
2101
|
+
if (ctx.transform.strict) err('strict mode: `for…in` is not in the canonical subset — it relies on runtime key enumeration. Iterate `Object.keys(obj)` explicitly instead.')
|
|
2102
|
+
// for (let k in src) → enumerate src's own keys via Object.keys (schema ∪ any keys added
|
|
2103
|
+
// later for objects; "0".."n-1" for arrays/strings; [] for Set/Map) and iterate the resulting
|
|
2104
|
+
// array by index. One uniform path keeps for-in consistent with Object.keys (so dynamically
|
|
2105
|
+
// added keys appear in both), and break/continue work as in any for-loop. Object.keys'
|
|
2106
|
+
// enumeration stdlib is pulled only when for-in is actually used.
|
|
1766
2107
|
const [, decl, src] = head
|
|
1767
|
-
const
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
} else {
|
|
1803
|
-
// Dynamic object → HASH runtime iteration
|
|
1804
|
-
includeForRuntimeKeyIteration()
|
|
1805
|
-
r = ['for-in', varName, prep(src), prep(body)]
|
|
1806
|
-
}
|
|
2108
|
+
const isDecl = Array.isArray(decl) && (decl[0] === 'let' || decl[0] === 'const')
|
|
2109
|
+
// `for ((x) in …)` — the LHS may be a cover-parenthesized identifier; unwrap to the target.
|
|
2110
|
+
let lhs = decl; while (Array.isArray(lhs) && lhs[0] === '()') lhs = lhs[1]
|
|
2111
|
+
const target = isDecl ? decl[1] : lhs
|
|
2112
|
+
// A member/computed LHS (`for (x.y in …)`, `for (obj[k] in …)`) assigns each key into the
|
|
2113
|
+
// existing place; let/const and a bare name take a fresh per-iteration `let` binding.
|
|
2114
|
+
const isMemberTarget = Array.isArray(target) && (target[0] === '.' || target[0] === '[]')
|
|
2115
|
+
// for-in over null/undefined is a no-op — ES ForIn/OfHeadEvaluation returns a break
|
|
2116
|
+
// completion before enumerating — but Object.keys(null|undefined) throws. So a nullish
|
|
2117
|
+
// source must enumerate the empty set. A static null (`[null,null]`) / undefined (`[]`)
|
|
2118
|
+
// skips Object.keys entirely; a bare identifier is guarded by a runtime `== null` test
|
|
2119
|
+
// (evaluated twice — side-effect-free — keeping Object.keys' *direct* receiver so its
|
|
2120
|
+
// static key schema still resolves); object/array literals and other expressions, which
|
|
2121
|
+
// are never nullish or carry no static schema to lose, stay direct.
|
|
2122
|
+
// A nullish literal node is `[<nullish-op>, value]` with both slots nullish: `null` is
|
|
2123
|
+
// `[null, null]`, `undefined` is `[null]` (empty value slot). A numeric/string literal
|
|
2124
|
+
// `[null, v]` has a non-nullish value slot, so `src[1] == null` discriminates them.
|
|
2125
|
+
const nullish = Array.isArray(src) && src[0] == null && src[1] == null
|
|
2126
|
+
const keysExpr = nullish ? ['[]', null]
|
|
2127
|
+
: typeof src === 'string'
|
|
2128
|
+
? ['?', ['==', src, [null, null]], ['[]', null], ['()', ['.', 'Object', 'keys'], src]]
|
|
2129
|
+
: ['()', ['.', 'Object', 'keys'], src]
|
|
2130
|
+
const ks = `${T}fik${ctx.func.uniq++}`, ix = `${T}fii${ctx.func.uniq++}`, lenV = `${T}fil${ctx.func.uniq++}`
|
|
2131
|
+
const decls = ['let',
|
|
2132
|
+
['=', ks, keysExpr],
|
|
2133
|
+
['=', ix, [, 0]],
|
|
2134
|
+
['=', lenV, ['|', ['.', ks, 'length'], [, 0]]]]
|
|
2135
|
+
const bindEach = isMemberTarget
|
|
2136
|
+
? ['=', target, ['[]', ks, ix]] // x.y = key / obj[k] = key
|
|
2137
|
+
: ['let', ['=', target, ['[]', ks, ix]]] // let k = key (fresh per-iteration binding)
|
|
2138
|
+
const forNode = ['for', [';', decls, ['<', ix, lenV], ['++', ix]],
|
|
2139
|
+
[';', bindEach, body]]
|
|
2140
|
+
// Run the dropped sequence prefix (earlier comma elements) once for side effects, before
|
|
2141
|
+
// the loop. Built raw and prepped as a unit so prep inserts the value-drop on the prefix.
|
|
2142
|
+
r = prep(forInSeqPre ? [';', forInSeqPre, forNode] : forNode)
|
|
1807
2143
|
} else {
|
|
1808
2144
|
// Some parser/jzify shapes for `for (;;)` and `for (; cond; )` arrive
|
|
1809
2145
|
// as a null or bare-condition head instead of the canonical
|
|
@@ -1818,14 +2154,22 @@ const handlers = {
|
|
|
1818
2154
|
// Property access - resolve namespaces or object/array properties
|
|
1819
2155
|
'.'(obj, prop) {
|
|
1820
2156
|
prop = typeof prop === 'string' ? prop : staticPropertyKey(prop)
|
|
1821
|
-
|
|
2157
|
+
// `.caller`/`.callee` on a function value (or `arguments`) are deprecated
|
|
2158
|
+
// stack introspection — prohibited as bad practice. On a plain data object
|
|
2159
|
+
// they are ordinary field names (e.g. an ESTree call node's `.callee`), so
|
|
2160
|
+
// the ban keys off a known-function receiver, not the bare property name.
|
|
2161
|
+
if ((obj === 'arguments' || hasFunc(obj) || isFuncValueLocal(obj)) && (prop === 'caller' || prop === 'callee'))
|
|
2162
|
+
err('`.caller`/`.callee` are prohibited: deprecated function stack introspection')
|
|
1822
2163
|
if (prop === 'url' && isImportMeta(obj)) return staticString(importMetaUrl())
|
|
2164
|
+
// A user binding named like a builtin namespace (`let Math = {…}`) shadows it
|
|
2165
|
+
// — read the property off the local value, not the builtin namespace table.
|
|
2166
|
+
if (shadowsBuiltin(obj)) { includeForProperty(prop); return ['.', prep(obj), prop] }
|
|
1823
2167
|
const mod = ctx.scope.chain[obj]
|
|
1824
2168
|
// Only treat as module namespace if it's a known built-in module (not a mangled import name)
|
|
1825
2169
|
if (typeof obj === 'string' && mod && !mod.includes('.') && hasModule(mod)) {
|
|
1826
2170
|
includeModule(mod)
|
|
1827
2171
|
const key = mod + '.' + prop
|
|
1828
|
-
if (ctx.core.emit[key]
|
|
2172
|
+
if (emitArity(ctx.core.emit[key]) > 0) includeForCallableValue()
|
|
1829
2173
|
return key
|
|
1830
2174
|
}
|
|
1831
2175
|
// Source module namespace: import * as X → X.prop resolved to mangled name
|
|
@@ -1841,7 +2185,22 @@ const handlers = {
|
|
|
1841
2185
|
'new'(ctor, ...args) {
|
|
1842
2186
|
let name = ctor, ctorArgs = args
|
|
1843
2187
|
if (Array.isArray(ctor) && ctor[0] === '()') { name = ctor[1]; ctorArgs = ctor.slice(2) }
|
|
1844
|
-
|
|
2188
|
+
// No GC → weakness is unobservable, so we fold WeakSet/WeakMap to Set/Map right here:
|
|
2189
|
+
// construction and every .add/.has/.get/.set/.delete reuse the concrete emit path.
|
|
2190
|
+
// The fold lives in prepare, not jzify — so `strict` (which only skips jzify) wouldn't
|
|
2191
|
+
// drop it on its own; we reject explicitly. It's a deviation anyway (accepts primitive
|
|
2192
|
+
// keys, exposes .size/iteration), not a true subset member — there, use Set/Map directly.
|
|
2193
|
+
if (name === 'WeakSet' || name === 'WeakMap') {
|
|
2194
|
+
const concrete = name === 'WeakSet' ? 'Set' : 'Map'
|
|
2195
|
+
if (ctx.transform.strict) err(`strict mode: ${name} is not in the canonical subset — use ${concrete} (jz has no GC, so weak references are unobservable).`)
|
|
2196
|
+
name = concrete
|
|
2197
|
+
}
|
|
2198
|
+
// A lone `null` ctorArg is the parser's no-args sentinel (`new Map()`), and
|
|
2199
|
+
// `new Map(null)`/`new Map(undefined)` are spec-equivalent to it (null/undefined
|
|
2200
|
+
// → empty collection). Drop it so the emit hits the empty-collection fast path
|
|
2201
|
+
// rather than lowering `prep(null)` → `[, 0]` and routing through `__map_from`.
|
|
2202
|
+
// Typed arrays keep the sentinel: there `[, 0]` is a legitimate zero length.
|
|
2203
|
+
if (ctorArgs.length === 1 && ctorArgs[0] == null && (name === 'Date' || COLLECTION_CTORS.includes(name))) ctorArgs = []
|
|
1845
2204
|
// Flatten comma-grouped args: [',', a, b, c] → [a, b, c]
|
|
1846
2205
|
if (ctorArgs.length === 1 && Array.isArray(ctorArgs[0]) && ctorArgs[0][0] === ',')
|
|
1847
2206
|
ctorArgs = ctorArgs[0].slice(1)
|
|
@@ -2001,6 +2360,20 @@ function collectReturns(node, out) {
|
|
|
2001
2360
|
|
|
2002
2361
|
const isLit = n => Array.isArray(n) && n[0] == null
|
|
2003
2362
|
|
|
2363
|
+
/** Self-host: pre-parsed module AST for a specifier, or undefined. Linear scan over
|
|
2364
|
+
* [specifier, ast] pairs — array indexing + string `===` are the ABI-safe primitives
|
|
2365
|
+
* the kernel can read off a host-marshalled argument (dynamic-key object reads aren't). */
|
|
2366
|
+
function moduleAstFor(specifier) {
|
|
2367
|
+
const asts = ctx.module.importAsts
|
|
2368
|
+
if (!asts) return undefined
|
|
2369
|
+
for (let i = 0; i < asts.length; i++) if (asts[i][0] === specifier) return asts[i][1]
|
|
2370
|
+
return undefined
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
/** True when `mod` is bundled in-process — as source (host parses it) or as a
|
|
2374
|
+
* pre-parsed AST (self-host kernel). Either path routes through prepareModule. */
|
|
2375
|
+
const isBundledModule = mod => !!ctx.module.importSources?.[mod] || moduleAstFor(mod) !== undefined
|
|
2376
|
+
|
|
2004
2377
|
/** Compile-time bundling: parse + prepare an imported module, collect exports. */
|
|
2005
2378
|
function prepareModule(specifier, source) {
|
|
2006
2379
|
includeModule('core')
|
|
@@ -2012,8 +2385,24 @@ function prepareModule(specifier, source) {
|
|
|
2012
2385
|
|
|
2013
2386
|
ctx.module.moduleStack.push(specifier)
|
|
2014
2387
|
|
|
2015
|
-
// Name mangling prefix
|
|
2016
|
-
|
|
2388
|
+
// Name mangling prefix. Long specifiers (the bundler keys modules by
|
|
2389
|
+
// ABSOLUTE path — 40-60 byte '_Users_…' / '_home_runner_…' prefixes on every
|
|
2390
|
+
// symbol) compact to 'm<N>_<basename>': symbol strings shrink ~4×, which is
|
|
2391
|
+
// a direct hot-path win in the SELF-HOST — watr resolves every `call $name`
|
|
2392
|
+
// and `local.get $name` through name-keyed maps, paying hash+compare per
|
|
2393
|
+
// byte, and shared 35-byte path prefixes defeated the hash-probe early-outs.
|
|
2394
|
+
// Deterministic per compile (registration order); short relative specifiers
|
|
2395
|
+
// keep the readable form.
|
|
2396
|
+
const sanitized = specifier.replace(/[^a-zA-Z0-9]/g, '_')
|
|
2397
|
+
let prefix
|
|
2398
|
+
if (sanitized.length <= 24) prefix = sanitized
|
|
2399
|
+
else {
|
|
2400
|
+
if (!ctx.module.prefixIds) ctx.module.prefixIds = new Map()
|
|
2401
|
+
let id = ctx.module.prefixIds.get(specifier)
|
|
2402
|
+
if (id == null) { id = ctx.module.prefixIds.size; ctx.module.prefixIds.set(specifier, id) }
|
|
2403
|
+
const base = sanitized.replace(/_(js|mjs|jz)$/, '').match(/[a-zA-Z0-9]+$/)?.[0] ?? ''
|
|
2404
|
+
prefix = `m${id}_${base.slice(-16)}`
|
|
2405
|
+
}
|
|
2017
2406
|
|
|
2018
2407
|
// Save caller state
|
|
2019
2408
|
const savedScope = ctx.scope.chain, savedExports = ctx.func.exports
|
|
@@ -2023,8 +2412,18 @@ function prepareModule(specifier, source) {
|
|
|
2023
2412
|
ctx.func.exports = {}
|
|
2024
2413
|
ctx.module.currentPrefix = prefix
|
|
2025
2414
|
|
|
2026
|
-
|
|
2027
|
-
|
|
2415
|
+
try {
|
|
2416
|
+
// Parse + prepare imported source (may trigger recursive imports). The parser
|
|
2417
|
+
// is injected via ctx.transform.parse (the host pipeline sets it) rather than
|
|
2418
|
+
// imported, so prepare carries no hard dependency on a concrete parser — the
|
|
2419
|
+
// same inversion as ctx.transform.jzify. The self-host kernel can't parse, so it
|
|
2420
|
+
// pre-parses the whole graph on the host and passes the ASTs via importAsts;
|
|
2421
|
+
// we consult those first and only parse `source` when no AST was supplied.
|
|
2422
|
+
let ast = moduleAstFor(specifier)
|
|
2423
|
+
if (ast === undefined) {
|
|
2424
|
+
if (!ctx.transform.parse) err('compile-time module bundling requires ctx.transform.parse (injected by the jz pipeline)')
|
|
2425
|
+
ast = ctx.transform.parse(source)
|
|
2426
|
+
}
|
|
2028
2427
|
if (ctx.transform.jzify) ast = ctx.transform.jzify(ast)
|
|
2029
2428
|
const savedDepth = depth; depth = 0
|
|
2030
2429
|
const moduleInit = prep(ast)
|
|
@@ -2038,9 +2437,9 @@ function prepareModule(specifier, source) {
|
|
|
2038
2437
|
const func = ctx.func.list.find(f => f.name === localName)
|
|
2039
2438
|
if (func) { renameFunc(func, mangled); func._modulePrefix = prefix }
|
|
2040
2439
|
if (ctx.scope.globals.has(localName)) {
|
|
2041
|
-
|
|
2440
|
+
// Records carry no name — a rename is a pure Map re-key.
|
|
2441
|
+
ctx.scope.globals.set(mangled, ctx.scope.globals.get(localName))
|
|
2042
2442
|
ctx.scope.globals.delete(localName)
|
|
2043
|
-
ctx.scope.globals.set(mangled, wat)
|
|
2044
2443
|
if (ctx.scope.userGlobals.has(localName)) { ctx.scope.userGlobals.delete(localName); ctx.scope.userGlobals.add(mangled) }
|
|
2045
2444
|
if (ctx.scope.globalTypes.has(localName)) { ctx.scope.globalTypes.set(mangled, ctx.scope.globalTypes.get(localName)); ctx.scope.globalTypes.delete(localName) }
|
|
2046
2445
|
}
|
|
@@ -2089,9 +2488,8 @@ function prepareModule(specifier, source) {
|
|
|
2089
2488
|
const func = ctx.func.list.find(f => f.name === alias)
|
|
2090
2489
|
if (func) renameFunc(func, mangled)
|
|
2091
2490
|
if (ctx.scope.globals.has(alias)) {
|
|
2092
|
-
|
|
2491
|
+
ctx.scope.globals.set(mangled, ctx.scope.globals.get(alias))
|
|
2093
2492
|
ctx.scope.globals.delete(alias)
|
|
2094
|
-
ctx.scope.globals.set(mangled, wat)
|
|
2095
2493
|
if (ctx.scope.userGlobals.has(alias)) { ctx.scope.userGlobals.delete(alias); ctx.scope.userGlobals.add(mangled) }
|
|
2096
2494
|
}
|
|
2097
2495
|
}
|
|
@@ -2157,15 +2555,18 @@ function prepareModule(specifier, source) {
|
|
|
2157
2555
|
recordModuleInitFacts(moduleInit)
|
|
2158
2556
|
}
|
|
2159
2557
|
|
|
2160
|
-
// Restore caller state
|
|
2161
|
-
ctx.scope.chain = savedScope
|
|
2162
|
-
ctx.func.exports = savedExports
|
|
2163
|
-
ctx.module.currentPrefix = savedModulePrefix
|
|
2164
|
-
ctx.module.moduleStack.pop()
|
|
2165
|
-
|
|
2166
2558
|
const result = { exports: moduleExports }
|
|
2167
2559
|
ctx.module.resolvedModules.set(specifier, result)
|
|
2168
2560
|
return result
|
|
2561
|
+
} finally {
|
|
2562
|
+
// ALWAYS restore caller state (FE-6): if `prep(ast)` or a recursive import threw
|
|
2563
|
+
// mid-prep, skipping this would leave ctx.scope/exports/prefix/moduleStack
|
|
2564
|
+
// corrupted for the rest of the pipeline.
|
|
2565
|
+
ctx.scope.chain = savedScope
|
|
2566
|
+
ctx.func.exports = savedExports
|
|
2567
|
+
ctx.module.currentPrefix = savedModulePrefix
|
|
2568
|
+
ctx.module.moduleStack.pop()
|
|
2569
|
+
}
|
|
2169
2570
|
}
|
|
2170
2571
|
|
|
2171
2572
|
// =============================================================================
|
|
@@ -2233,7 +2634,7 @@ function tryFusePair(decl, forNode, seq, declIdx) {
|
|
|
2233
2634
|
if (!hasAnyIndexedRead(forBody, NAME) && !hasAnyIndexedRead(cond, NAME)) return null
|
|
2234
2635
|
// `NAME` must not be read after the for-loop in the same block.
|
|
2235
2636
|
for (let k = declIdx + 2; k < seq.length; k++) {
|
|
2236
|
-
if (refsName(seq[k], NAME)) return null
|
|
2637
|
+
if (refsName(seq[k], NAME, { skipArrow: false })) return null
|
|
2237
2638
|
}
|
|
2238
2639
|
// RECV must not be reassigned inside the for-loop (would invalidate substitution).
|
|
2239
2640
|
if (assignsName(forNode, RECV) || assignsName(forNode, NAME)) return null
|
|
@@ -2275,12 +2676,6 @@ function hasAnyIndexedRead(n, NAME) {
|
|
|
2275
2676
|
for (let i = 1; i < n.length; i++) if (hasAnyIndexedRead(n[i], NAME)) return true
|
|
2276
2677
|
return false
|
|
2277
2678
|
}
|
|
2278
|
-
function refsName(n, NAME) {
|
|
2279
|
-
if (typeof n === 'string') return n === NAME
|
|
2280
|
-
if (!Array.isArray(n)) return false
|
|
2281
|
-
for (let i = 1; i < n.length; i++) if (refsName(n[i], NAME)) return true
|
|
2282
|
-
return false
|
|
2283
|
-
}
|
|
2284
2679
|
function assignsName(n, NAME) {
|
|
2285
2680
|
if (!Array.isArray(n)) return false
|
|
2286
2681
|
const op = n[0]
|