jz 0.5.1 → 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 +266 -153
- 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 +414 -186
- 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 +586 -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} +589 -202
- 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 -3040
- package/src/jzify.js +0 -1580
- 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 —
|
|
@@ -99,7 +197,6 @@ const stripBoolNot = c => {
|
|
|
99
197
|
return c
|
|
100
198
|
}
|
|
101
199
|
const stringValue = node => Array.isArray(node) && node[0] == null && typeof node[1] === 'string' ? node[1] : null
|
|
102
|
-
const flatArgs = args => args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',' ? args[0].slice(1) : args
|
|
103
200
|
const MUTATING_ARRAY_METHODS = new Set(['copyWithin', 'fill', 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'])
|
|
104
201
|
|
|
105
202
|
function staticStringArrayValues(expr) {
|
|
@@ -234,7 +331,12 @@ function staticStringExpr(node) {
|
|
|
234
331
|
if (op === '+') {
|
|
235
332
|
const a = staticStringExpr(args[0])
|
|
236
333
|
const b = staticStringExpr(args[1])
|
|
237
|
-
|
|
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
|
|
238
340
|
}
|
|
239
341
|
if (op === '`') {
|
|
240
342
|
let out = ''
|
|
@@ -270,7 +372,12 @@ function importMetaUrl() {
|
|
|
270
372
|
|
|
271
373
|
function resolveImportMeta(spec) {
|
|
272
374
|
const base = importMetaUrl()
|
|
273
|
-
|
|
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) }
|
|
274
381
|
catch { err(`Cannot resolve import.meta specifier '${spec}' from '${base}'`) }
|
|
275
382
|
}
|
|
276
383
|
|
|
@@ -279,6 +386,7 @@ function recordModuleInitFacts(root) {
|
|
|
279
386
|
dynVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
|
|
280
387
|
hasFuncValue: false, timerNames: new Set(),
|
|
281
388
|
maxDef: 0, maxCall: 0, hasRest: false, hasSpread: false,
|
|
389
|
+
writtenProps: new Set(),
|
|
282
390
|
}
|
|
283
391
|
const visitFuncValue = (node) => {
|
|
284
392
|
if (facts.hasFuncValue || !Array.isArray(node)) return
|
|
@@ -323,13 +431,34 @@ function recordModuleInitFacts(root) {
|
|
|
323
431
|
* @param {ASTNode} node - Raw AST from parser
|
|
324
432
|
* @returns {ASTNode} Normalized AST
|
|
325
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
|
+
|
|
326
454
|
export default function prepare(node) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
|
332
460
|
includeModule('core')
|
|
461
|
+
validateCoalesceMixing(node) // ES2020: reject unparenthesized `??` mixed with `||`/`&&`
|
|
333
462
|
normalizeIdents(node)
|
|
334
463
|
fuseSparseMapReads(node) // AST-level fusion; needs pre-resolution shape — defined at end of file
|
|
335
464
|
seedStaticGlobalAssignments(node)
|
|
@@ -426,9 +555,9 @@ export default function prepare(node) {
|
|
|
426
555
|
return ast
|
|
427
556
|
}
|
|
428
557
|
|
|
429
|
-
// Named constants → numeric literals
|
|
430
|
-
|
|
431
|
-
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 }
|
|
432
561
|
// NaN/Infinity stay as special f64 values in emit()
|
|
433
562
|
const F64_CONSTANTS = { 'NaN': NaN, 'Infinity': Infinity }
|
|
434
563
|
|
|
@@ -472,7 +601,43 @@ function deleteStaticGlobal(name) {
|
|
|
472
601
|
ctx.scope.shapeStrArrays?.delete(name)
|
|
473
602
|
}
|
|
474
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
|
+
|
|
475
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))
|
|
476
641
|
|
|
477
642
|
const renameFunc = (func, nextName) => {
|
|
478
643
|
ctx.func.names.delete(func.name)
|
|
@@ -480,8 +645,8 @@ const renameFunc = (func, nextName) => {
|
|
|
480
645
|
ctx.func.names.add(nextName)
|
|
481
646
|
}
|
|
482
647
|
|
|
483
|
-
|
|
484
|
-
|
|
648
|
+
// `typeof`-string → code table lives in ast.js (TYPEOF) — shared with
|
|
649
|
+
// emitTypeofCmp and flow-types so the codes have one home.
|
|
485
650
|
// Spec §13.5.3: `typeof undeclared_x` returns 'undefined' without throwing.
|
|
486
651
|
// True iff `name` is a bare identifier with no resolution path. Mirrors the
|
|
487
652
|
// resolution chain inside `prep()` so we don't speculate emit-time failures.
|
|
@@ -489,7 +654,7 @@ function isUnresolvableBareIdent(name) {
|
|
|
489
654
|
if (typeof name !== 'string') return false
|
|
490
655
|
if (name in CONSTANTS || name in F64_CONSTANTS) return false
|
|
491
656
|
if (name === 'Boolean' || name === 'Number') return false
|
|
492
|
-
if (
|
|
657
|
+
if (REJECT_IDENTS[name]) return false
|
|
493
658
|
if (scopes.length && isDeclared(name)) return false
|
|
494
659
|
if (ctx.scope.chain[name]) return false
|
|
495
660
|
if (GLOBALS[name]) return false
|
|
@@ -508,9 +673,9 @@ function staticTypeofString(x) {
|
|
|
508
673
|
// Spec §13.5.3: unresolvable bare ref → 'undefined'.
|
|
509
674
|
if (isUnresolvableBareIdent(x)) return 'undefined'
|
|
510
675
|
// Bare callable global: parseInt, parseFloat, isNaN, isFinite, Error, BigInt, etc.
|
|
511
|
-
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'
|
|
512
677
|
const px = prep(x)
|
|
513
|
-
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'
|
|
514
679
|
return null
|
|
515
680
|
}
|
|
516
681
|
// Builtin-namespace constructors expose `prototype`/`length`/`name` as own
|
|
@@ -534,14 +699,14 @@ function resolveTypeof(node) {
|
|
|
534
699
|
if (Array.isArray(a) && a[0] === 'typeof' && Array.isArray(b) && b[0] == null && typeof b[1] === 'string') {
|
|
535
700
|
const known = staticTypeofString(a[1])
|
|
536
701
|
if (known != null) return [, eqLike ? known === b[1] : known !== b[1]]
|
|
537
|
-
const code =
|
|
702
|
+
const code = TYPEOF[b[1]]
|
|
538
703
|
if (code != null) return [op, ['typeof', a[1]], [, code]]
|
|
539
704
|
}
|
|
540
705
|
// 'string' == typeof x
|
|
541
706
|
if (Array.isArray(b) && b[0] === 'typeof' && Array.isArray(a) && a[0] == null && typeof a[1] === 'string') {
|
|
542
707
|
const known = staticTypeofString(b[1])
|
|
543
708
|
if (known != null) return [, eqLike ? known === a[1] : known !== a[1]]
|
|
544
|
-
const code =
|
|
709
|
+
const code = TYPEOF[a[1]]
|
|
545
710
|
if (code != null) return [op, ['typeof', b[1]], [, code]]
|
|
546
711
|
}
|
|
547
712
|
return node
|
|
@@ -602,11 +767,19 @@ function prep(node) {
|
|
|
602
767
|
if (typeof node === 'string') {
|
|
603
768
|
if (node in CONSTANTS) return [, CONSTANTS[node]]
|
|
604
769
|
if (node in F64_CONSTANTS) return [, F64_CONSTANTS[node]]
|
|
605
|
-
if (
|
|
770
|
+
if (REJECT_IDENTS[node]) err(REJECT_IDENTS[node])
|
|
606
771
|
// Boolean/Number as value → identity arrow (for .filter(Boolean), .map(Number) etc.)
|
|
607
772
|
if (node === 'Boolean' || node === 'Number') { includeForCallableValue(); return ['=>', 'x', 'x'] }
|
|
608
773
|
// Block locals shadow module imports/globals, even when the local keeps the same name.
|
|
609
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]]
|
|
610
783
|
const resolved = ctx.scope.chain[node]
|
|
611
784
|
if (resolved?.includes('.')) return resolved
|
|
612
785
|
// Cross-module import: mangled name (e.g. __util_js$clone)
|
|
@@ -619,6 +792,10 @@ function prep(node) {
|
|
|
619
792
|
|
|
620
793
|
const [op, ...args] = node
|
|
621
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.)`)
|
|
622
799
|
if (op == null) {
|
|
623
800
|
if (typeof args[0] === 'string') {
|
|
624
801
|
includeForStringValue()
|
|
@@ -630,19 +807,9 @@ function prep(node) {
|
|
|
630
807
|
return handler ? handler(...args) : [op, ...args.map(prep)]
|
|
631
808
|
}
|
|
632
809
|
|
|
633
|
-
//
|
|
634
|
-
// only reach this point when jzify is off — jzify lowers `class` to a factory
|
|
635
|
-
// arrow and rewrites `arguments` to a rest param before prepare runs. The
|
|
636
|
-
// remainder (`with`, `this`, `super`, `yield`, `eval`) have no safe lowering
|
|
637
|
-
// and stay errors in both modes.
|
|
638
|
-
const PROHIBITED = { 'with': '`with` not supported', 'class': '`class` not supported', 'yield': '`yield` not supported',
|
|
639
|
-
'this': '`this` not supported: use explicit parameter',
|
|
640
|
-
'super': '`super` not supported: no class inheritance',
|
|
641
|
-
'arguments': '`arguments` not supported: use rest params',
|
|
642
|
-
'eval': '`eval` not supported'
|
|
643
|
-
}
|
|
810
|
+
// Identifier prohibitions: op-policy.js REJECT_IDENTS (prep string nodes).
|
|
644
811
|
|
|
645
|
-
// Predefined globals seeded into scope.chain at ctx.reset().
|
|
812
|
+
// Predefined globals seeded into scope.chain at ctx.reset().
|
|
646
813
|
// used in ctx.core.emit[]. Dotted lookups (Math.sin) go through the '.' handler which
|
|
647
814
|
// resolves via scope.chain → module 'math' → registers 'math.sin' emitter.
|
|
648
815
|
// Not actually "implicit imports" — these are ambient globals that exist in every jz/JS
|
|
@@ -683,6 +850,13 @@ export const GLOBALS = {
|
|
|
683
850
|
const patternItems = (node) => node?.[0] === ',' ? node.slice(1) : [node]
|
|
684
851
|
const isDestructPattern = (node) => Array.isArray(node) && (node[0] === '[]' || node[0] === '{}')
|
|
685
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
|
+
|
|
686
860
|
const simpleArrayPatternItems = (pattern) => {
|
|
687
861
|
if (!Array.isArray(pattern) || pattern[0] !== '[]' || pattern.length !== 2) return null
|
|
688
862
|
const items = patternItems(pattern[1])
|
|
@@ -714,7 +888,7 @@ function scalarArrayDestruct(pattern, rhs) {
|
|
|
714
888
|
function declareGlobal(name, user = true) {
|
|
715
889
|
if (depth !== 0 || typeof name !== 'string') return name
|
|
716
890
|
if (ctx.scope.globals.has(name)) err(`'${name}' conflicts with a compiler internal — choose a different name`)
|
|
717
|
-
|
|
891
|
+
declGlobal(name, 'f64')
|
|
718
892
|
if (user) ctx.scope.userGlobals.add(name)
|
|
719
893
|
return name
|
|
720
894
|
}
|
|
@@ -749,7 +923,7 @@ function pushPatternAssign(target, valueExpr, out, decls = null) {
|
|
|
749
923
|
out.push(['=', target, valueExpr])
|
|
750
924
|
}
|
|
751
925
|
|
|
752
|
-
function expandDestruct(pattern, source, out, decls = null) {
|
|
926
|
+
function expandDestruct(pattern, source, out, decls = null, srcLen = null) {
|
|
753
927
|
if (!isDestructPattern(pattern)) return
|
|
754
928
|
|
|
755
929
|
if (pattern[0] === '[]') {
|
|
@@ -764,6 +938,15 @@ function expandDestruct(pattern, source, out, decls = null) {
|
|
|
764
938
|
continue
|
|
765
939
|
}
|
|
766
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
|
+
|
|
767
950
|
pushPatternAssign(item, ['[]', source, [, j]], out, decls)
|
|
768
951
|
}
|
|
769
952
|
return
|
|
@@ -844,7 +1027,7 @@ function prepDecl(op, ...inits) {
|
|
|
844
1027
|
ctx.scope.chain[i] = declName
|
|
845
1028
|
}
|
|
846
1029
|
if (ctx.scope.globals.has(declName)) err(`'${declName}' conflicts with a compiler internal — choose a different name`)
|
|
847
|
-
|
|
1030
|
+
declGlobal(declName, 'f64')
|
|
848
1031
|
ctx.scope.userGlobals.add(declName)
|
|
849
1032
|
} else if (typeof declName === 'string') {
|
|
850
1033
|
// Bare hoisted decl inside a function (var X jzified to `let X` at top
|
|
@@ -859,6 +1042,23 @@ function prepDecl(op, ...inits) {
|
|
|
859
1042
|
continue
|
|
860
1043
|
}
|
|
861
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
|
+
}
|
|
862
1062
|
const staticStr = op === 'const' ? staticStringExpr(init) : null
|
|
863
1063
|
const staticArr = op === 'const' ? staticStringArrayValues(init) : null
|
|
864
1064
|
const normed = prep(init)
|
|
@@ -894,7 +1094,7 @@ function prepDecl(op, ...inits) {
|
|
|
894
1094
|
const p = normed.slice(1).filter(p => Array.isArray(p) && p[0] === ':').map(p => p[1])
|
|
895
1095
|
if (p.length) ctx.schema.vars.set(tmp, ctx.schema.register(p))
|
|
896
1096
|
}
|
|
897
|
-
expandDestruct(name, tmp, rest)
|
|
1097
|
+
expandDestruct(name, tmp, rest, null, inlineArrayLen(normed))
|
|
898
1098
|
continue
|
|
899
1099
|
}
|
|
900
1100
|
|
|
@@ -912,6 +1112,10 @@ function prepDecl(op, ...inits) {
|
|
|
912
1112
|
scopes[scopes.length - 1].set(name, name)
|
|
913
1113
|
}
|
|
914
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)
|
|
915
1119
|
if (op === 'const') bindStaticConst(declName, staticStr, staticArr)
|
|
916
1120
|
// Track const for reassignment checks — only module-scope consts (depth 0)
|
|
917
1121
|
if (typeof declName === 'string' && depth === 0) {
|
|
@@ -942,20 +1146,29 @@ function prepDecl(op, ...inits) {
|
|
|
942
1146
|
// Track object schemas (after prefix so schema is keyed to final name)
|
|
943
1147
|
if (typeof declName === 'string' && Array.isArray(normed) && normed[0] === '{}' && normed.length > 1) {
|
|
944
1148
|
const props = []
|
|
1149
|
+
const addProp = n => { if (!props.includes(n)) props.push(n) }
|
|
1150
|
+
let allKnown = true
|
|
945
1151
|
for (const p of normed.slice(1)) {
|
|
946
|
-
|
|
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])
|
|
947
1157
|
else if (Array.isArray(p) && p[0] === '...') {
|
|
948
|
-
// Merge spread source schema into this object's schema
|
|
949
1158
|
const srcSchema = typeof p[1] === 'string' && ctx.schema.resolve(p[1])
|
|
950
|
-
if (srcSchema) for (const n of srcSchema)
|
|
1159
|
+
if (srcSchema) for (const n of srcSchema) addProp(n)
|
|
1160
|
+
else allKnown = false
|
|
951
1161
|
}
|
|
952
1162
|
}
|
|
953
|
-
|
|
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))
|
|
954
1167
|
}
|
|
955
1168
|
// Module-scope variable → WASM global (mark as user-declared)
|
|
956
1169
|
if (depth === 0 && typeof declName === 'string') {
|
|
957
1170
|
if (ctx.scope.globals.has(declName)) err(`'${declName}' conflicts with a compiler internal — choose a different name`)
|
|
958
|
-
|
|
1171
|
+
declGlobal(declName, 'f64')
|
|
959
1172
|
ctx.scope.userGlobals.add(declName)
|
|
960
1173
|
}
|
|
961
1174
|
rest.push(['=', declName, normed])
|
|
@@ -972,7 +1185,7 @@ function prepDecl(op, ...inits) {
|
|
|
972
1185
|
// `import.meta.resolve("spec")` → the resolved URL as a static string.
|
|
973
1186
|
function foldImportMetaResolve(callee, args) {
|
|
974
1187
|
if (!isImportMetaProp(callee, 'resolve')) return undefined
|
|
975
|
-
const callArgs =
|
|
1188
|
+
const callArgs = handlerArgs(args)
|
|
976
1189
|
if (callArgs.length !== 1) err('`import.meta.resolve` requires one string literal argument')
|
|
977
1190
|
const spec = stringValue(callArgs[0])
|
|
978
1191
|
if (spec == null) err('`import.meta.resolve` supports only string literal arguments')
|
|
@@ -986,8 +1199,11 @@ function foldImportMetaResolve(callee, args) {
|
|
|
986
1199
|
// Returns the replacement IR, or `undefined` for an ordinary call.
|
|
987
1200
|
function dispatchConstructorCall(callee, args) {
|
|
988
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
|
|
989
1205
|
if (callee === 'Array') {
|
|
990
|
-
const callArgs =
|
|
1206
|
+
const callArgs = handlerArgs(args)
|
|
991
1207
|
if (callArgs.length === 1) return handlers['new'](['()', callee, callArgs[0]])
|
|
992
1208
|
}
|
|
993
1209
|
if (CTORS.includes(callee)) return handlers['new'](['()', callee, ...args])
|
|
@@ -1005,7 +1221,7 @@ function foldNamespaceIntrospection(callee, args) {
|
|
|
1005
1221
|
if (!Array.isArray(callee) || callee[0] !== '.') return undefined
|
|
1006
1222
|
const [, obj, prop] = callee
|
|
1007
1223
|
if (obj === 'Array' && prop === 'isArray') {
|
|
1008
|
-
const cargs =
|
|
1224
|
+
const cargs = handlerArgs(args)
|
|
1009
1225
|
const a0 = cargs.length === 1 ? cargs[0] : null
|
|
1010
1226
|
if (typeof a0 === 'string' && GLOBALS[a0] && !(scopes.length && isDeclared(a0)) && !hasFunc(a0))
|
|
1011
1227
|
return [, 0]
|
|
@@ -1013,7 +1229,7 @@ function foldNamespaceIntrospection(callee, args) {
|
|
|
1013
1229
|
if (prop === 'hasOwnProperty' && typeof obj === 'string' && !(scopes.length && isDeclared(obj))) {
|
|
1014
1230
|
const mod = ctx.scope.chain[obj]
|
|
1015
1231
|
if (mod && !mod.includes('.') && hasModule(mod)) {
|
|
1016
|
-
const cargs =
|
|
1232
|
+
const cargs = handlerArgs(args)
|
|
1017
1233
|
const member = cargs.length === 1 ? stringValue(cargs[0]) : null
|
|
1018
1234
|
// Include the module so its emit keys (the namespace's member set) are
|
|
1019
1235
|
// registered; unreferenced emitters/data dead-strip in compile.
|
|
@@ -1027,6 +1243,11 @@ function foldNamespaceIntrospection(callee, args) {
|
|
|
1027
1243
|
// way: a bare identifier through the scope chain, an `obj.prop` member call
|
|
1028
1244
|
// through host imports / named-call / generic-method / namespace tables, and
|
|
1029
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
|
+
|
|
1030
1251
|
function resolveCallee(callee, args) {
|
|
1031
1252
|
if (typeof callee === 'string') {
|
|
1032
1253
|
const local = scopes.length && isDeclared(callee)
|
|
@@ -1038,12 +1259,23 @@ function resolveCallee(callee, args) {
|
|
|
1038
1259
|
if (hasModule(resolved) && !ctx.module.imports.some(i => i[3]?.[1] === `$${resolved}`)) includeModule(resolved)
|
|
1039
1260
|
return callee
|
|
1040
1261
|
}
|
|
1041
|
-
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}`))
|
|
1042
1263
|
includeForCallableValue()
|
|
1043
1264
|
return callee
|
|
1044
1265
|
}
|
|
1045
1266
|
if (Array.isArray(callee) && callee[0] === '.') {
|
|
1046
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
|
+
}
|
|
1047
1279
|
const key = typeof obj === 'string' && typeof prop === 'string' ? `${obj}.${prop}` : null
|
|
1048
1280
|
if (key && ctx.module.hostImports?.[obj]?.[prop]) {
|
|
1049
1281
|
const spec = ctx.module.hostImports[obj][prop]
|
|
@@ -1078,18 +1310,13 @@ function renestSoleCommaArg(args) {
|
|
|
1078
1310
|
}
|
|
1079
1311
|
|
|
1080
1312
|
const handlers = {
|
|
1313
|
+
...rejectHandlers(err),
|
|
1081
1314
|
// Spread operator: [...expr] in arrays, f(...args) in calls, {...obj} in objects
|
|
1082
1315
|
'...'(expr) {
|
|
1083
1316
|
includeForArrayLiteral()
|
|
1084
1317
|
return ['...', prep(expr)]
|
|
1085
1318
|
},
|
|
1086
1319
|
|
|
1087
|
-
// Prohibited ops — duplicated from jzify deliberately: .jz source bypasses jzify,
|
|
1088
|
-
// so prepare is the actual defense. Messages here fire for both .js and .jz.
|
|
1089
|
-
'async': () => err('async/await not supported: WASM is synchronous'),
|
|
1090
|
-
'await': () => err('async/await not supported: WASM is synchronous'),
|
|
1091
|
-
'class': () => err('class not supported: use object literals'),
|
|
1092
|
-
'yield': () => err('generators not supported: use loops'),
|
|
1093
1320
|
'debugger': () => null,
|
|
1094
1321
|
// Static-key delete (.x, ["x"], [literal]) would change the fixed schema → reject.
|
|
1095
1322
|
// Computed-key delete (obj[expr]) — including jessie's `delete ctx[k]` — lowers
|
|
@@ -1104,12 +1331,7 @@ const handlers = {
|
|
|
1104
1331
|
err('delete not supported: object shape is fixed')
|
|
1105
1332
|
},
|
|
1106
1333
|
'in'(key, obj) { return ['in', prep(key), prep(obj)] },
|
|
1107
|
-
'instanceof': () => err('instanceof not supported: use typeof'),
|
|
1108
|
-
'with': () => err('`with` not supported: deprecated'),
|
|
1109
|
-
':': () => err('labeled statements not supported'),
|
|
1110
1334
|
'label'(name, body) { return ['label', name, prep(body)] },
|
|
1111
|
-
'var': () => err('`var` not supported: use let/const'),
|
|
1112
|
-
'function': () => err('`function` not supported: use arrow functions'),
|
|
1113
1335
|
|
|
1114
1336
|
// Destructuring assignment: [a, ...b] = expr or {x, y} = expr
|
|
1115
1337
|
'='(lhs, rhs) {
|
|
@@ -1147,7 +1369,7 @@ const handlers = {
|
|
|
1147
1369
|
// emit a dynamic property read + indirect call instead of a direct call.
|
|
1148
1370
|
if (ctx.func.names.has(name)) {
|
|
1149
1371
|
ctx.func.multiProp.add(`${fnBase}.${lhs[2]}`)
|
|
1150
|
-
do name = `${fnBase}$${lhs[2]}$${ctx.func.uniq++}
|
|
1372
|
+
do { name = `${fnBase}$${lhs[2]}$${ctx.func.uniq++}` } while (ctx.func.names.has(name))
|
|
1151
1373
|
}
|
|
1152
1374
|
// Build the target `.` node directly from the resolved base — re-`prep`ing
|
|
1153
1375
|
// the lhs would resolve a multiProp `fn.prop` to an rvalue (closure
|
|
@@ -1165,22 +1387,25 @@ const handlers = {
|
|
|
1165
1387
|
recordGlobalRep(plhs, prhs)
|
|
1166
1388
|
if (Array.isArray(prhs) && prhs[0] === '{}') {
|
|
1167
1389
|
const props = staticObjectProps(prhs.slice(1))
|
|
1168
|
-
if (props)
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1390
|
+
if (props) bindAssignSchema(plhs, ctx.schema.register(props.names))
|
|
1391
|
+
} else bindAssignSchema(plhs, null)
|
|
1392
|
+
} else bindAssignSchema(plhs, objLiteralSid(prhs))
|
|
1171
1393
|
// Static string/array facts hold only while every assignment is constant.
|
|
1172
1394
|
if (!assignedStaticGlobals.has(plhs) && (staticStr != null || staticArr)) bindStaticGlobal(plhs, staticStr, staticArr)
|
|
1173
1395
|
else deleteStaticGlobal(plhs)
|
|
1174
1396
|
assignedStaticGlobals.add(plhs)
|
|
1175
1397
|
}
|
|
1176
|
-
//
|
|
1177
|
-
//
|
|
1178
|
-
//
|
|
1179
|
-
//
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
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))
|
|
1184
1409
|
}
|
|
1185
1410
|
return ['=', plhs, prhs]
|
|
1186
1411
|
},
|
|
@@ -1191,18 +1416,12 @@ const handlers = {
|
|
|
1191
1416
|
const catchClause = clauses.find(c => Array.isArray(c) && c[0] === 'catch')
|
|
1192
1417
|
const finallyClause = clauses.find(c => Array.isArray(c) && c[0] === 'finally')
|
|
1193
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).
|
|
1194
1421
|
const caught = catchClause
|
|
1195
|
-
? (
|
|
1196
|
-
const [, errName, handler] = catchClause
|
|
1197
|
-
return ['catch', tryBody, errName, prep(handler)]
|
|
1198
|
-
})()
|
|
1422
|
+
? ['catch', tryBody, catchClause[1], prep(catchClause[2])]
|
|
1199
1423
|
: tryBody
|
|
1200
|
-
|
|
1201
|
-
if (catchClause) {
|
|
1202
|
-
const [, errName, handler] = catchClause
|
|
1203
|
-
return ['catch', tryBody, errName, prep(handler)]
|
|
1204
|
-
}
|
|
1205
|
-
return tryBody
|
|
1424
|
+
return finallyClause ? ['finally', caught, prep(finallyClause[1])] : caught
|
|
1206
1425
|
},
|
|
1207
1426
|
'throw'(expr) { return ['throw', prep(expr)] },
|
|
1208
1427
|
|
|
@@ -1238,6 +1457,24 @@ const handlers = {
|
|
|
1238
1457
|
return handlers['from'](fromNode[1], fromNode[2])
|
|
1239
1458
|
},
|
|
1240
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
|
+
|
|
1241
1478
|
'from'(specifiers, source) {
|
|
1242
1479
|
const mod = source?.[1]
|
|
1243
1480
|
if (!mod || typeof mod !== 'string') return err('Invalid import source')
|
|
@@ -1296,8 +1533,8 @@ const handlers = {
|
|
|
1296
1533
|
}
|
|
1297
1534
|
|
|
1298
1535
|
// Tier 2: Source module (bundling)
|
|
1299
|
-
if (
|
|
1300
|
-
const resolved = prepareModule(mod, ctx.module.importSources[mod])
|
|
1536
|
+
if (isBundledModule(mod)) {
|
|
1537
|
+
const resolved = prepareModule(mod, ctx.module.importSources?.[mod])
|
|
1301
1538
|
// Default import: import name from 'mod' → bind to default export
|
|
1302
1539
|
if (typeof specifiers === 'string') {
|
|
1303
1540
|
const mangled = resolved.exports.get('default')
|
|
@@ -1374,6 +1611,17 @@ const handlers = {
|
|
|
1374
1611
|
pushScope(); const b = prep(body); popScope()
|
|
1375
1612
|
return ['while', c, b]
|
|
1376
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
|
+
},
|
|
1377
1625
|
|
|
1378
1626
|
'export': decl => {
|
|
1379
1627
|
if (Array.isArray(decl) && (decl[0] === 'let' || decl[0] === 'const'))
|
|
@@ -1385,8 +1633,8 @@ const handlers = {
|
|
|
1385
1633
|
const mod = decl[2]?.[1]
|
|
1386
1634
|
if (!mod || typeof mod !== 'string') return null
|
|
1387
1635
|
// Source module re-export
|
|
1388
|
-
if (
|
|
1389
|
-
const resolved = prepareModule(mod, ctx.module.importSources[mod])
|
|
1636
|
+
if (isBundledModule(mod)) {
|
|
1637
|
+
const resolved = prepareModule(mod, ctx.module.importSources?.[mod])
|
|
1390
1638
|
if (decl[1] === '*') {
|
|
1391
1639
|
// export * from './mod' → register all exports
|
|
1392
1640
|
for (const [name, mangled] of resolved.exports) {
|
|
@@ -1439,7 +1687,7 @@ const handlers = {
|
|
|
1439
1687
|
if (defFunc('default', prep(val))) return null
|
|
1440
1688
|
}
|
|
1441
1689
|
// export default expr → create global 'default'
|
|
1442
|
-
|
|
1690
|
+
declGlobal('default', 'f64')
|
|
1443
1691
|
ctx.scope.userGlobals.add('default')
|
|
1444
1692
|
return ['=', 'default', prep(val)]
|
|
1445
1693
|
}
|
|
@@ -1456,12 +1704,18 @@ const handlers = {
|
|
|
1456
1704
|
depth++
|
|
1457
1705
|
pushScope(fnScope)
|
|
1458
1706
|
funcLocalNames.push(new Set(collectParamNames(raw)))
|
|
1707
|
+
funcValueNames.push(new Set())
|
|
1459
1708
|
|
|
1460
1709
|
const nextParams = []
|
|
1461
1710
|
const bodyPrefix = []
|
|
1462
1711
|
for (const r of raw) {
|
|
1463
1712
|
const c = classifyParam(r)
|
|
1464
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()
|
|
1465
1719
|
nextParams.push(r)
|
|
1466
1720
|
if (typeof c.name === 'string') fnScope.set(c.name, c.name)
|
|
1467
1721
|
} else if (c.kind === 'plain') {
|
|
@@ -1497,6 +1751,7 @@ const handlers = {
|
|
|
1497
1751
|
const result = ['=>', Array.isArray(params) && params[0] === '()' ? ['()', inner] : inner, preparedBody]
|
|
1498
1752
|
popScope()
|
|
1499
1753
|
funcLocalNames.pop()
|
|
1754
|
+
funcValueNames.pop()
|
|
1500
1755
|
depth--
|
|
1501
1756
|
return result
|
|
1502
1757
|
},
|
|
@@ -1516,9 +1771,13 @@ const handlers = {
|
|
|
1516
1771
|
})]
|
|
1517
1772
|
},
|
|
1518
1773
|
|
|
1519
|
-
// Optional chaining / typeof — need ptr module
|
|
1520
|
-
|
|
1521
|
-
'
|
|
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)] },
|
|
1522
1781
|
'?.()'(callee, callArgs) {
|
|
1523
1782
|
// Parser wraps multi-args in a comma list, like '()'. Unwrap so emit gets flat positional args.
|
|
1524
1783
|
const items = callArgs == null ? []
|
|
@@ -1557,7 +1816,12 @@ const handlers = {
|
|
|
1557
1816
|
return ['+', pa, pb]
|
|
1558
1817
|
},
|
|
1559
1818
|
'-'(a, b) {
|
|
1560
|
-
|
|
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] }
|
|
1561
1825
|
return ['-', prep(a), prep(b)]
|
|
1562
1826
|
},
|
|
1563
1827
|
|
|
@@ -1565,17 +1829,19 @@ const handlers = {
|
|
|
1565
1829
|
'?'(cond, then, els) { return ['?:', prep(stripBoolNot(cond)), prep(then), prep(els)] },
|
|
1566
1830
|
|
|
1567
1831
|
// ++/-- prefix vs postfix: parser sends trailing null for postfix
|
|
1568
|
-
// Postfix i++ = (++i) - 1: increment happens, arithmetic recovers old value
|
|
1569
|
-
// 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.
|
|
1570
1836
|
'++'(a, _post) {
|
|
1571
1837
|
const n = prep(a)
|
|
1572
|
-
|
|
1573
|
-
return _post !== undefined ? ['-',
|
|
1838
|
+
const inc = Array.isArray(n) && (n[0] === '.' || n[0] === '[]') ? ['=', n, ['+', n, [, 1]]] : ['++', n]
|
|
1839
|
+
return _post !== undefined ? ['-', inc, [, 1]] : inc
|
|
1574
1840
|
},
|
|
1575
1841
|
'--'(a, _post) {
|
|
1576
1842
|
const n = prep(a)
|
|
1577
|
-
|
|
1578
|
-
return _post !== undefined ? ['+',
|
|
1843
|
+
const dec = Array.isArray(n) && (n[0] === '.' || n[0] === '[]') ? ['=', n, ['-', n, [, 1]]] : ['--', n]
|
|
1844
|
+
return _post !== undefined ? ['+', dec, [, 1]] : dec
|
|
1579
1845
|
},
|
|
1580
1846
|
|
|
1581
1847
|
// Regex literal: ['//','pattern','flags?'] → include regex module, pass through
|
|
@@ -1583,13 +1849,22 @@ const handlers = {
|
|
|
1583
1849
|
return ['//', pattern, flags]
|
|
1584
1850
|
},
|
|
1585
1851
|
|
|
1586
|
-
'**'(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
|
+
},
|
|
1587
1862
|
|
|
1588
1863
|
// Function call or grouping parens
|
|
1589
1864
|
'()'(callee, ...args) {
|
|
1590
1865
|
// Grouping: (expr) → ['()', expr] with no args. Call: f() → ['()', 'f', null] with null arg.
|
|
1591
1866
|
if (args.length === 0) return prep(callee)
|
|
1592
|
-
if (typeof callee === 'string' &&
|
|
1867
|
+
if (typeof callee === 'string' && REJECT_IDENTS[callee]) err(REJECT_IDENTS[callee])
|
|
1593
1868
|
|
|
1594
1869
|
// Compile-time folds: the callee names something resolvable now. Each fold
|
|
1595
1870
|
// is gated by callee shape, so at most one of the three fires.
|
|
@@ -1607,7 +1882,14 @@ const handlers = {
|
|
|
1607
1882
|
includeForCallableValue(); break
|
|
1608
1883
|
}
|
|
1609
1884
|
}
|
|
1610
|
-
|
|
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]
|
|
1611
1893
|
|
|
1612
1894
|
if (callee === 'Object.assign' && ctx.schema.register) inferAssignSchema(result)
|
|
1613
1895
|
|
|
@@ -1649,9 +1931,11 @@ const handlers = {
|
|
|
1649
1931
|
},
|
|
1650
1932
|
|
|
1651
1933
|
// Object literal - flatten comma, expand shorthand
|
|
1652
|
-
'{}'(
|
|
1653
|
-
|
|
1654
|
-
|
|
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])) {
|
|
1655
1939
|
// Block body: push block scope for let/const shadowing
|
|
1656
1940
|
pushScope()
|
|
1657
1941
|
const result = ['{}', prep(inner)]
|
|
@@ -1660,10 +1944,16 @@ const handlers = {
|
|
|
1660
1944
|
}
|
|
1661
1945
|
|
|
1662
1946
|
includeForObjectLiteral()
|
|
1663
|
-
if (inner == null) return ['{}']
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
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
|
|
1667
1957
|
|
|
1668
1958
|
// Computed keys: `{[k]: v}` where `k` isn't compile-time foldable. jz's
|
|
1669
1959
|
// object layout is slot-based (fixed schema at the literal site), so a
|
|
@@ -1699,6 +1989,12 @@ const handlers = {
|
|
|
1699
1989
|
if (key == null) err('computed property name not supported for fixed-shape object: use a compile-time string/number key')
|
|
1700
1990
|
return [':', key, prep(p[2])]
|
|
1701
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')
|
|
1702
1998
|
return prep(p)
|
|
1703
1999
|
}
|
|
1704
2000
|
let prepped = items.map(prop)
|
|
@@ -1725,27 +2021,56 @@ const handlers = {
|
|
|
1725
2021
|
// For loop
|
|
1726
2022
|
'for'(head, body) {
|
|
1727
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
|
+
}
|
|
1728
2037
|
let r
|
|
1729
2038
|
if (Array.isArray(head) && head[0] === ';') {
|
|
1730
2039
|
let [, init, cond, step] = head
|
|
1731
2040
|
cond = stripBoolNot(cond)
|
|
1732
|
-
//
|
|
1733
|
-
// `i < arr.length` → `
|
|
1734
|
-
// The `| 0` forces i32 even for unknown-typed receivers (where __length
|
|
1735
|
-
//
|
|
1736
|
-
//
|
|
1737
|
-
//
|
|
1738
|
-
//
|
|
1739
|
-
//
|
|
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.
|
|
1740
2051
|
if (cond && Array.isArray(cond) && (cond[0] === '<' || cond[0] === '<=' || cond[0] === '>' || cond[0] === '>=')) {
|
|
1741
2052
|
const lenExpr = cond[0] === '<' || cond[0] === '<=' ? cond[2] : cond[1]
|
|
1742
2053
|
if (Array.isArray(lenExpr) && lenExpr[0] === '.' &&
|
|
1743
2054
|
(lenExpr[2] === 'length' || lenExpr[2] === 'size' || lenExpr[2] === 'byteLength')) {
|
|
1744
|
-
const
|
|
1745
|
-
const
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
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
|
+
}
|
|
1749
2074
|
}
|
|
1750
2075
|
}
|
|
1751
2076
|
r = ['for', init ? prep(init) : null, cond ? prep(cond) : null, step ? prep(step) : null, prep(body)]
|
|
@@ -1757,61 +2082,64 @@ const handlers = {
|
|
|
1757
2082
|
const varName = Array.isArray(decl) && (decl[0] === 'let' || decl[0] === 'const') ? decl[1] : decl
|
|
1758
2083
|
const idx = `${T}i${ctx.func.uniq++}`
|
|
1759
2084
|
const lenVar = `${T}len${ctx.func.uniq++}`
|
|
1760
|
-
const
|
|
1761
|
-
|
|
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.
|
|
1762
2090
|
// Wrap .length in `| 0` so the hoisted bound is i32 even for unknown
|
|
1763
2091
|
// receivers (same rationale as the for-cond hoist above).
|
|
1764
2092
|
const lenE = ['|', ['.', arrVar, 'length'], [, 0]]
|
|
1765
|
-
const decls =
|
|
1766
|
-
? ['let', ['=', idx, [, 0]], ['=', lenVar, lenE]]
|
|
1767
|
-
: ['let', ['=', arrVar, src], ['=', idx, [, 0]], ['=', lenVar, lenE]]
|
|
2093
|
+
const decls = ['let', ['=', arrVar, ['()', '__iter_arr', src]], ['=', idx, [, 0]], ['=', lenVar, lenE]]
|
|
1768
2094
|
const cond = ['<', idx, lenVar]
|
|
1769
2095
|
const step = ['++', idx]
|
|
1770
2096
|
const inner = [';', ['let', ['=', varName, ['[]', arrVar, idx]]], body]
|
|
1771
2097
|
r = prep(['for', [';', decls, cond, step], inner])
|
|
1772
2098
|
} else if (Array.isArray(head) && head[0] === 'in') {
|
|
1773
|
-
// 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.
|
|
1774
2107
|
const [, decl, src] = head
|
|
1775
|
-
const
|
|
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
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
} else {
|
|
1811
|
-
// Dynamic object → HASH runtime iteration
|
|
1812
|
-
includeForRuntimeKeyIteration()
|
|
1813
|
-
r = ['for-in', varName, prep(src), prep(body)]
|
|
1814
|
-
}
|
|
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)
|
|
1815
2143
|
} else {
|
|
1816
2144
|
// Some parser/jzify shapes for `for (;;)` and `for (; cond; )` arrive
|
|
1817
2145
|
// as a null or bare-condition head instead of the canonical
|
|
@@ -1826,14 +2154,22 @@ const handlers = {
|
|
|
1826
2154
|
// Property access - resolve namespaces or object/array properties
|
|
1827
2155
|
'.'(obj, prop) {
|
|
1828
2156
|
prop = typeof prop === 'string' ? prop : staticPropertyKey(prop)
|
|
1829
|
-
|
|
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')
|
|
1830
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] }
|
|
1831
2167
|
const mod = ctx.scope.chain[obj]
|
|
1832
2168
|
// Only treat as module namespace if it's a known built-in module (not a mangled import name)
|
|
1833
2169
|
if (typeof obj === 'string' && mod && !mod.includes('.') && hasModule(mod)) {
|
|
1834
2170
|
includeModule(mod)
|
|
1835
2171
|
const key = mod + '.' + prop
|
|
1836
|
-
if (ctx.core.emit[key]
|
|
2172
|
+
if (emitArity(ctx.core.emit[key]) > 0) includeForCallableValue()
|
|
1837
2173
|
return key
|
|
1838
2174
|
}
|
|
1839
2175
|
// Source module namespace: import * as X → X.prop resolved to mangled name
|
|
@@ -1849,7 +2185,22 @@ const handlers = {
|
|
|
1849
2185
|
'new'(ctor, ...args) {
|
|
1850
2186
|
let name = ctor, ctorArgs = args
|
|
1851
2187
|
if (Array.isArray(ctor) && ctor[0] === '()') { name = ctor[1]; ctorArgs = ctor.slice(2) }
|
|
1852
|
-
|
|
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 = []
|
|
1853
2204
|
// Flatten comma-grouped args: [',', a, b, c] → [a, b, c]
|
|
1854
2205
|
if (ctorArgs.length === 1 && Array.isArray(ctorArgs[0]) && ctorArgs[0][0] === ',')
|
|
1855
2206
|
ctorArgs = ctorArgs[0].slice(1)
|
|
@@ -2009,6 +2360,20 @@ function collectReturns(node, out) {
|
|
|
2009
2360
|
|
|
2010
2361
|
const isLit = n => Array.isArray(n) && n[0] == null
|
|
2011
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
|
+
|
|
2012
2377
|
/** Compile-time bundling: parse + prepare an imported module, collect exports. */
|
|
2013
2378
|
function prepareModule(specifier, source) {
|
|
2014
2379
|
includeModule('core')
|
|
@@ -2020,8 +2385,24 @@ function prepareModule(specifier, source) {
|
|
|
2020
2385
|
|
|
2021
2386
|
ctx.module.moduleStack.push(specifier)
|
|
2022
2387
|
|
|
2023
|
-
// Name mangling prefix
|
|
2024
|
-
|
|
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
|
+
}
|
|
2025
2406
|
|
|
2026
2407
|
// Save caller state
|
|
2027
2408
|
const savedScope = ctx.scope.chain, savedExports = ctx.func.exports
|
|
@@ -2031,8 +2412,18 @@ function prepareModule(specifier, source) {
|
|
|
2031
2412
|
ctx.func.exports = {}
|
|
2032
2413
|
ctx.module.currentPrefix = prefix
|
|
2033
2414
|
|
|
2034
|
-
|
|
2035
|
-
|
|
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
|
+
}
|
|
2036
2427
|
if (ctx.transform.jzify) ast = ctx.transform.jzify(ast)
|
|
2037
2428
|
const savedDepth = depth; depth = 0
|
|
2038
2429
|
const moduleInit = prep(ast)
|
|
@@ -2046,9 +2437,9 @@ function prepareModule(specifier, source) {
|
|
|
2046
2437
|
const func = ctx.func.list.find(f => f.name === localName)
|
|
2047
2438
|
if (func) { renameFunc(func, mangled); func._modulePrefix = prefix }
|
|
2048
2439
|
if (ctx.scope.globals.has(localName)) {
|
|
2049
|
-
|
|
2440
|
+
// Records carry no name — a rename is a pure Map re-key.
|
|
2441
|
+
ctx.scope.globals.set(mangled, ctx.scope.globals.get(localName))
|
|
2050
2442
|
ctx.scope.globals.delete(localName)
|
|
2051
|
-
ctx.scope.globals.set(mangled, wat)
|
|
2052
2443
|
if (ctx.scope.userGlobals.has(localName)) { ctx.scope.userGlobals.delete(localName); ctx.scope.userGlobals.add(mangled) }
|
|
2053
2444
|
if (ctx.scope.globalTypes.has(localName)) { ctx.scope.globalTypes.set(mangled, ctx.scope.globalTypes.get(localName)); ctx.scope.globalTypes.delete(localName) }
|
|
2054
2445
|
}
|
|
@@ -2097,9 +2488,8 @@ function prepareModule(specifier, source) {
|
|
|
2097
2488
|
const func = ctx.func.list.find(f => f.name === alias)
|
|
2098
2489
|
if (func) renameFunc(func, mangled)
|
|
2099
2490
|
if (ctx.scope.globals.has(alias)) {
|
|
2100
|
-
|
|
2491
|
+
ctx.scope.globals.set(mangled, ctx.scope.globals.get(alias))
|
|
2101
2492
|
ctx.scope.globals.delete(alias)
|
|
2102
|
-
ctx.scope.globals.set(mangled, wat)
|
|
2103
2493
|
if (ctx.scope.userGlobals.has(alias)) { ctx.scope.userGlobals.delete(alias); ctx.scope.userGlobals.add(mangled) }
|
|
2104
2494
|
}
|
|
2105
2495
|
}
|
|
@@ -2165,15 +2555,18 @@ function prepareModule(specifier, source) {
|
|
|
2165
2555
|
recordModuleInitFacts(moduleInit)
|
|
2166
2556
|
}
|
|
2167
2557
|
|
|
2168
|
-
// Restore caller state
|
|
2169
|
-
ctx.scope.chain = savedScope
|
|
2170
|
-
ctx.func.exports = savedExports
|
|
2171
|
-
ctx.module.currentPrefix = savedModulePrefix
|
|
2172
|
-
ctx.module.moduleStack.pop()
|
|
2173
|
-
|
|
2174
2558
|
const result = { exports: moduleExports }
|
|
2175
2559
|
ctx.module.resolvedModules.set(specifier, result)
|
|
2176
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
|
+
}
|
|
2177
2570
|
}
|
|
2178
2571
|
|
|
2179
2572
|
// =============================================================================
|
|
@@ -2241,7 +2634,7 @@ function tryFusePair(decl, forNode, seq, declIdx) {
|
|
|
2241
2634
|
if (!hasAnyIndexedRead(forBody, NAME) && !hasAnyIndexedRead(cond, NAME)) return null
|
|
2242
2635
|
// `NAME` must not be read after the for-loop in the same block.
|
|
2243
2636
|
for (let k = declIdx + 2; k < seq.length; k++) {
|
|
2244
|
-
if (refsName(seq[k], NAME)) return null
|
|
2637
|
+
if (refsName(seq[k], NAME, { skipArrow: false })) return null
|
|
2245
2638
|
}
|
|
2246
2639
|
// RECV must not be reassigned inside the for-loop (would invalidate substitution).
|
|
2247
2640
|
if (assignsName(forNode, RECV) || assignsName(forNode, NAME)) return null
|
|
@@ -2283,12 +2676,6 @@ function hasAnyIndexedRead(n, NAME) {
|
|
|
2283
2676
|
for (let i = 1; i < n.length; i++) if (hasAnyIndexedRead(n[i], NAME)) return true
|
|
2284
2677
|
return false
|
|
2285
2678
|
}
|
|
2286
|
-
function refsName(n, NAME) {
|
|
2287
|
-
if (typeof n === 'string') return n === NAME
|
|
2288
|
-
if (!Array.isArray(n)) return false
|
|
2289
|
-
for (let i = 1; i < n.length; i++) if (refsName(n[i], NAME)) return true
|
|
2290
|
-
return false
|
|
2291
|
-
}
|
|
2292
2679
|
function assignsName(n, NAME) {
|
|
2293
2680
|
if (!Array.isArray(n)) return false
|
|
2294
2681
|
const op = n[0]
|