jz 0.5.1 → 0.7.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 +315 -318
- package/bench/README.md +369 -0
- package/bench/bench.svg +102 -0
- package/cli.js +104 -30
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +362 -84
- package/interop.js +159 -186
- 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 +184 -0
- package/module/array.js +377 -176
- package/module/collection.js +639 -145
- package/module/console.js +55 -43
- package/module/core.js +264 -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 +551 -187
- package/module/number.js +327 -60
- package/module/object.js +474 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +117 -0
- package/module/string.js +621 -226
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +459 -73
- package/package.json +58 -14
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +29 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +824 -0
- package/src/compile/analyze.js +1600 -0
- package/src/compile/emit-assign.js +411 -0
- package/src/compile/emit.js +3512 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +778 -128
- package/src/{infer.js → compile/infer.js} +62 -98
- package/src/compile/loop-divmod.js +155 -0
- package/src/{narrow.js → compile/narrow.js} +409 -106
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +370 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +122 -0
- package/src/compile/plan/inline.js +682 -0
- package/src/compile/plan/literals.js +1199 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +649 -0
- package/src/compile/program-facts.js +423 -0
- package/src/ctx.js +220 -64
- package/src/ir.js +589 -172
- package/src/kind-traits.js +132 -0
- package/src/kind.js +524 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1432 -473
- package/src/optimize/vectorize.js +4660 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +649 -205
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +116 -0
- package/src/resolve.js +12 -3
- package/src/static.js +208 -0
- package/src/type.js +651 -0
- package/src/{assemble.js → wat/assemble.js} +275 -55
- package/src/wat/optimize.js +3938 -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,
|
|
32
|
-
hasModule, includeModule,
|
|
46
|
+
CTORS, COLLECTION_CTORS, TIMER_NAMES,
|
|
47
|
+
hasModule, includeModule, includeMods,
|
|
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) {
|
|
@@ -224,6 +321,30 @@ function lookupStaticStringArray(name) {
|
|
|
224
321
|
return ctx.scope.shapeStrArrays?.get(resolved) ?? null
|
|
225
322
|
}
|
|
226
323
|
|
|
324
|
+
/** Evaluate a constant numeric expression (number literals + basic arithmetic) for
|
|
325
|
+
* compile-time string/template folding. Returns null when it isn't a pure-number
|
|
326
|
+
* constant — string `+` and dynamic parts fall through to the caller's runtime path. */
|
|
327
|
+
function constNum(node) {
|
|
328
|
+
if (Array.isArray(node) && node[0] == null && typeof node[1] === 'number') return node[1]
|
|
329
|
+
if (!Array.isArray(node)) return null
|
|
330
|
+
const [op, a, b] = node
|
|
331
|
+
if ((op === 'u-' || op === '-' || op === '+') && b === undefined) {
|
|
332
|
+
const x = constNum(a)
|
|
333
|
+
return x == null ? null : op === 'u-' || op === '-' ? -x : +x
|
|
334
|
+
}
|
|
335
|
+
const x = constNum(a), y = constNum(b)
|
|
336
|
+
if (x == null || y == null) return null
|
|
337
|
+
switch (op) {
|
|
338
|
+
case '+': return x + y
|
|
339
|
+
case '-': return x - y
|
|
340
|
+
case '*': return x * y
|
|
341
|
+
case '/': return y === 0 ? null : x / y
|
|
342
|
+
case '%': return y === 0 ? null : x % y
|
|
343
|
+
case '**': return x ** y
|
|
344
|
+
}
|
|
345
|
+
return null
|
|
346
|
+
}
|
|
347
|
+
|
|
227
348
|
function staticStringExpr(node) {
|
|
228
349
|
const lit = stringValue(node)
|
|
229
350
|
if (lit != null) return lit
|
|
@@ -234,12 +355,21 @@ function staticStringExpr(node) {
|
|
|
234
355
|
if (op === '+') {
|
|
235
356
|
const a = staticStringExpr(args[0])
|
|
236
357
|
const b = staticStringExpr(args[1])
|
|
237
|
-
|
|
358
|
+
// Accumulate from a fresh empty string (`'' + a + b`) rather than concatenating two
|
|
359
|
+
// source-derived substrings directly. Under self-host the latter can yield a string
|
|
360
|
+
// backed by transient parse-time storage that's invalid by the time emit['//'] reads
|
|
361
|
+
// it for regex compilation (OOB); forcing a fresh allocation, as the template-literal
|
|
362
|
+
// path already does, keeps it stable. Identical value in both legs.
|
|
363
|
+
return a != null && b != null ? '' + a + b : null
|
|
238
364
|
}
|
|
239
365
|
if (op === '`') {
|
|
240
366
|
let out = ''
|
|
241
367
|
for (const part of args) {
|
|
242
|
-
|
|
368
|
+
let s = staticStringExpr(part)
|
|
369
|
+
// A numeric interpolation (`${123}`, `${1+2}`) is a constant in string context —
|
|
370
|
+
// ToString it so a fully-static template folds to one literal instead of a runtime
|
|
371
|
+
// concat. (Only the template case stringifies numbers; `+` stays polymorphic.)
|
|
372
|
+
if (s == null) { const n = constNum(part); if (n != null) s = String(n) }
|
|
243
373
|
if (s == null) return null
|
|
244
374
|
out += s
|
|
245
375
|
}
|
|
@@ -270,15 +400,21 @@ function importMetaUrl() {
|
|
|
270
400
|
|
|
271
401
|
function resolveImportMeta(spec) {
|
|
272
402
|
const base = importMetaUrl()
|
|
273
|
-
|
|
403
|
+
// URL resolution is a host capability (WHATWG URL parsing), injected via
|
|
404
|
+
// ctx.transform.resolveUrl rather than referencing the `URL` global — the same
|
|
405
|
+
// inversion as ctx.transform.parse. Keeps the self-host kernel (which bundles
|
|
406
|
+
// its module graph and never resolves import.meta at runtime) free of `URL`.
|
|
407
|
+
if (!ctx.transform.resolveUrl) err('import.meta resolution requires ctx.transform.resolveUrl (injected by the jz pipeline)')
|
|
408
|
+
try { return ctx.transform.resolveUrl(spec, base) }
|
|
274
409
|
catch { err(`Cannot resolve import.meta specifier '${spec}' from '${base}'`) }
|
|
275
410
|
}
|
|
276
411
|
|
|
277
412
|
function recordModuleInitFacts(root) {
|
|
278
413
|
const facts = ctx.module.initFacts ||= {
|
|
279
|
-
dynVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
|
|
414
|
+
dynVars: new Set(), dynWriteVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
|
|
280
415
|
hasFuncValue: false, timerNames: new Set(),
|
|
281
416
|
maxDef: 0, maxCall: 0, hasRest: false, hasSpread: false,
|
|
417
|
+
writtenProps: new Set(),
|
|
282
418
|
}
|
|
283
419
|
const visitFuncValue = (node) => {
|
|
284
420
|
if (facts.hasFuncValue || !Array.isArray(node)) return
|
|
@@ -323,13 +459,39 @@ function recordModuleInitFacts(root) {
|
|
|
323
459
|
* @param {ASTNode} node - Raw AST from parser
|
|
324
460
|
* @returns {ASTNode} Normalized AST
|
|
325
461
|
*/
|
|
462
|
+
// ES2020 §13.13: the nullish-coalescing `??` cannot be combined with `||` or `&&`
|
|
463
|
+
// without parentheses — V8 raises a SyntaxError. subscript/jessie doesn't enforce
|
|
464
|
+
// it, so jz would otherwise silently accept (and pick its own parse for) the mix.
|
|
465
|
+
// Run on the RAW input AST: a parenthesized operand parses as `['()', …]`, so a
|
|
466
|
+
// bare `??`/`||`/`&&` child is exactly the illegal unparenthesized form — and at
|
|
467
|
+
// this stage no compiler-synthesized `??` (e.g. destructuring defaults) exists yet,
|
|
468
|
+
// so `let [a = b || c] = arr` can't false-positive.
|
|
469
|
+
function validateCoalesceMixing(n) {
|
|
470
|
+
if (!Array.isArray(n)) return
|
|
471
|
+
const op = n[0]
|
|
472
|
+
if (op === '||' || op === '&&') {
|
|
473
|
+
for (let i = 1; i < n.length; i++) if (Array.isArray(n[i]) && n[i][0] === '??')
|
|
474
|
+
err(`'??' cannot be mixed with '${op}' without parentheses (ES2020) — wrap one side, e.g. (a ?? b) ${op} c`)
|
|
475
|
+
} else if (op === '??') {
|
|
476
|
+
for (let i = 1; i < n.length; i++) if (Array.isArray(n[i]) && (n[i][0] === '||' || n[i][0] === '&&'))
|
|
477
|
+
err(`'??' cannot be mixed with '||' / '&&' without parentheses (ES2020) — wrap one side, e.g. a ?? (b || c)`)
|
|
478
|
+
}
|
|
479
|
+
for (let i = 1; i < n.length; i++) validateCoalesceMixing(n[i])
|
|
480
|
+
}
|
|
481
|
+
|
|
326
482
|
export default function prepare(node) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
483
|
+
resetPrepState()
|
|
484
|
+
// Inject the module-include primitive so stdlib modules can pull dependency
|
|
485
|
+
// modules (e.g. object → collection) without importing autoload.js — that
|
|
486
|
+
// import would cycle (autoload imports every module via module/index.js).
|
|
487
|
+
ctx.module.include = includeModule
|
|
332
488
|
includeModule('core')
|
|
489
|
+
// Empty or whitespace-only source parses to a bare '' — an empty program, not an
|
|
490
|
+
// identifier reference. Normalize to an empty statement so it compiles to a bare
|
|
491
|
+
// `(module)` instead of a `(local.get $)` against a zero-length name. (A non-empty
|
|
492
|
+
// bare identifier like `foo` parses to `'foo'` and stays a real reference.)
|
|
493
|
+
if (node === '') node = [';']
|
|
494
|
+
validateCoalesceMixing(node) // ES2020: reject unparenthesized `??` mixed with `||`/`&&`
|
|
333
495
|
normalizeIdents(node)
|
|
334
496
|
fuseSparseMapReads(node) // AST-level fusion; needs pre-resolution shape — defined at end of file
|
|
335
497
|
seedStaticGlobalAssignments(node)
|
|
@@ -426,9 +588,9 @@ export default function prepare(node) {
|
|
|
426
588
|
return ast
|
|
427
589
|
}
|
|
428
590
|
|
|
429
|
-
// Named constants → numeric literals
|
|
430
|
-
|
|
431
|
-
const CONSTANTS = { 'true': true, 'false': false, 'null': JZ_NULL, 'undefined':
|
|
591
|
+
// Named constants → numeric literals. The JZ_NULL/JZ_UNDEF atom sentinels live
|
|
592
|
+
// in ast.js — shared with emit without crossing the prepare↔compile boundary.
|
|
593
|
+
const CONSTANTS = { 'true': true, 'false': false, 'null': JZ_NULL, 'undefined': JZ_UNDEF }
|
|
432
594
|
// NaN/Infinity stay as special f64 values in emit()
|
|
433
595
|
const F64_CONSTANTS = { 'NaN': NaN, 'Infinity': Infinity }
|
|
434
596
|
|
|
@@ -472,7 +634,43 @@ function deleteStaticGlobal(name) {
|
|
|
472
634
|
ctx.scope.shapeStrArrays?.delete(name)
|
|
473
635
|
}
|
|
474
636
|
|
|
637
|
+
// Schema id when prhs is a bare object literal with static keys, else null.
|
|
638
|
+
function objLiteralSid(prhs) {
|
|
639
|
+
if (!Array.isArray(prhs) || prhs[0] !== '{}') return null
|
|
640
|
+
const props = staticObjectProps(prhs.slice(1))
|
|
641
|
+
return props ? ctx.schema.register(props.names) : null
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Shape-consensus accounting for every `name = …` assignment. `sid` is the
|
|
645
|
+
// RHS literal's schema id (null for any non-literal source). A name's schema
|
|
646
|
+
// binds only while ALL its assignments agree on that one literal shape: the
|
|
647
|
+
// first literal binds; any disagreeing assignment — non-literal RHS or a
|
|
648
|
+
// different-shape literal — unbinds and poisons. Poisoned names never rebind,
|
|
649
|
+
// so compile-time fixed-slot reads can't be aimed at one shape while the
|
|
650
|
+
// variable holds another (the misread class: `.x` returning a foreign
|
|
651
|
+
// object's slot-0 value). Compile consumes the END state — order-insensitive.
|
|
652
|
+
function bindAssignSchema(name, sid) {
|
|
653
|
+
const had = ctx.schema.vars.get(name)
|
|
654
|
+
if (had != null) {
|
|
655
|
+
if (had !== sid) { ctx.schema.vars.delete(name); ctx.schema.poisoned?.add(name) }
|
|
656
|
+
} else if (sid != null) {
|
|
657
|
+
if (!ctx.schema.poisoned?.has(name)) ctx.schema.vars.set(name, sid)
|
|
658
|
+
} else ctx.schema.poisoned?.add(name)
|
|
659
|
+
}
|
|
660
|
+
|
|
475
661
|
const hasFunc = name => ctx.func.names.has(name)
|
|
662
|
+
// A builtin name (`Map`, `Array`, `Math`, …) is shadowed when the user bound it
|
|
663
|
+
// as a local (let/const/param, via `isDeclared`), a top-level function (via
|
|
664
|
+
// `hasFunc`), or a top-level let/const global (via `userGlobals`). A shadowed
|
|
665
|
+
// name must resolve to the user binding, so the constructor / named-call
|
|
666
|
+
// fast-paths bail and fall through to `resolveCallee`, which already routes a
|
|
667
|
+
// declared name to its local value. Mirrors the guard in
|
|
668
|
+
// `foldNamespaceIntrospection`.
|
|
669
|
+
const shadowsBuiltin = name => typeof name === 'string' &&
|
|
670
|
+
((scopes.length && isDeclared(name)) || hasFunc(name) || ctx.scope.userGlobals?.has?.(name))
|
|
671
|
+
// A local bound to a function literal in any active arrow scope (the nested-
|
|
672
|
+
// closure counterpart to `hasFunc`, which only knows depth-0 lifted functions).
|
|
673
|
+
const isFuncValueLocal = name => typeof name === 'string' && funcValueNames.some(s => s.has(name))
|
|
476
674
|
|
|
477
675
|
const renameFunc = (func, nextName) => {
|
|
478
676
|
ctx.func.names.delete(func.name)
|
|
@@ -480,8 +678,8 @@ const renameFunc = (func, nextName) => {
|
|
|
480
678
|
ctx.func.names.add(nextName)
|
|
481
679
|
}
|
|
482
680
|
|
|
483
|
-
|
|
484
|
-
|
|
681
|
+
// `typeof`-string → code table lives in ast.js (TYPEOF) — shared with
|
|
682
|
+
// emitTypeofCmp and flow-types so the codes have one home.
|
|
485
683
|
// Spec §13.5.3: `typeof undeclared_x` returns 'undefined' without throwing.
|
|
486
684
|
// True iff `name` is a bare identifier with no resolution path. Mirrors the
|
|
487
685
|
// resolution chain inside `prep()` so we don't speculate emit-time failures.
|
|
@@ -489,7 +687,7 @@ function isUnresolvableBareIdent(name) {
|
|
|
489
687
|
if (typeof name !== 'string') return false
|
|
490
688
|
if (name in CONSTANTS || name in F64_CONSTANTS) return false
|
|
491
689
|
if (name === 'Boolean' || name === 'Number') return false
|
|
492
|
-
if (
|
|
690
|
+
if (REJECT_IDENTS[name]) return false
|
|
493
691
|
if (scopes.length && isDeclared(name)) return false
|
|
494
692
|
if (ctx.scope.chain[name]) return false
|
|
495
693
|
if (GLOBALS[name]) return false
|
|
@@ -508,9 +706,9 @@ function staticTypeofString(x) {
|
|
|
508
706
|
// Spec §13.5.3: unresolvable bare ref → 'undefined'.
|
|
509
707
|
if (isUnresolvableBareIdent(x)) return 'undefined'
|
|
510
708
|
// 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]
|
|
709
|
+
if (typeof x === 'string' && !ctx.func?.locals?.has(x) && GLOBALS[x] && emitArity(ctx.core.emit?.[x]) > 0) return 'function'
|
|
512
710
|
const px = prep(x)
|
|
513
|
-
if (typeof px === 'string' && px.includes('.') && ctx.core.emit?.[px]
|
|
711
|
+
if (typeof px === 'string' && px.includes('.') && emitArity(ctx.core.emit?.[px]) > 0) return 'function'
|
|
514
712
|
return null
|
|
515
713
|
}
|
|
516
714
|
// Builtin-namespace constructors expose `prototype`/`length`/`name` as own
|
|
@@ -534,14 +732,14 @@ function resolveTypeof(node) {
|
|
|
534
732
|
if (Array.isArray(a) && a[0] === 'typeof' && Array.isArray(b) && b[0] == null && typeof b[1] === 'string') {
|
|
535
733
|
const known = staticTypeofString(a[1])
|
|
536
734
|
if (known != null) return [, eqLike ? known === b[1] : known !== b[1]]
|
|
537
|
-
const code =
|
|
735
|
+
const code = TYPEOF[b[1]]
|
|
538
736
|
if (code != null) return [op, ['typeof', a[1]], [, code]]
|
|
539
737
|
}
|
|
540
738
|
// 'string' == typeof x
|
|
541
739
|
if (Array.isArray(b) && b[0] === 'typeof' && Array.isArray(a) && a[0] == null && typeof a[1] === 'string') {
|
|
542
740
|
const known = staticTypeofString(b[1])
|
|
543
741
|
if (known != null) return [, eqLike ? known === a[1] : known !== a[1]]
|
|
544
|
-
const code =
|
|
742
|
+
const code = TYPEOF[a[1]]
|
|
545
743
|
if (code != null) return [op, ['typeof', b[1]], [, code]]
|
|
546
744
|
}
|
|
547
745
|
return node
|
|
@@ -602,11 +800,19 @@ function prep(node) {
|
|
|
602
800
|
if (typeof node === 'string') {
|
|
603
801
|
if (node in CONSTANTS) return [, CONSTANTS[node]]
|
|
604
802
|
if (node in F64_CONSTANTS) return [, F64_CONSTANTS[node]]
|
|
605
|
-
if (
|
|
803
|
+
if (REJECT_IDENTS[node]) err(REJECT_IDENTS[node])
|
|
606
804
|
// Boolean/Number as value → identity arrow (for .filter(Boolean), .map(Number) etc.)
|
|
607
805
|
if (node === 'Boolean' || node === 'Number') { includeForCallableValue(); return ['=>', 'x', 'x'] }
|
|
608
806
|
// Block locals shadow module imports/globals, even when the local keeps the same name.
|
|
609
807
|
if (scopes.length && isDeclared(node)) return resolveScope(node)
|
|
808
|
+
// A user top-level binding (`let Math = …`) shadows a same-named builtin
|
|
809
|
+
// namespace seeded into the scope chain (`Math → math`). Resolve to the
|
|
810
|
+
// user global, not the builtin. (Mangled globals drop their original name
|
|
811
|
+
// from userGlobals, so this fires only for un-renamed user bindings.)
|
|
812
|
+
if (ctx.scope.userGlobals?.has?.(node)) return node
|
|
813
|
+
// Host numeric constant (`Math.PI` etc.) → fold to its f64 literal. Placed after the
|
|
814
|
+
// local/user-global checks above so a same-named binding still shadows it.
|
|
815
|
+
if (ctx.scope.hostConsts && node in ctx.scope.hostConsts) return [, ctx.scope.hostConsts[node]]
|
|
610
816
|
const resolved = ctx.scope.chain[node]
|
|
611
817
|
if (resolved?.includes('.')) return resolved
|
|
612
818
|
// Cross-module import: mangled name (e.g. __util_js$clone)
|
|
@@ -619,6 +825,10 @@ function prep(node) {
|
|
|
619
825
|
|
|
620
826
|
const [op, ...args] = node
|
|
621
827
|
if (op === 'void' && ctx.transform.strict) err('strict mode: `void` is prohibited. It diverges from JS by evaluating to 0.')
|
|
828
|
+
// jz's `==`/`!=` already never coerce (identical to `===`/`!==`), so default mode accepts them.
|
|
829
|
+
// strict enforces the canonical subset, where `===`/`!==` are the one spelling — reject the loose form.
|
|
830
|
+
if ((op === '==' || op === '!=') && ctx.transform.strict)
|
|
831
|
+
err(`strict mode: \`${op}\` is prohibited — use \`${op}=\`. (jz's \`${op}\` doesn't coerce, but the canonical subset is \`===\`/\`!==\` only.)`)
|
|
622
832
|
if (op == null) {
|
|
623
833
|
if (typeof args[0] === 'string') {
|
|
624
834
|
includeForStringValue()
|
|
@@ -630,19 +840,9 @@ function prep(node) {
|
|
|
630
840
|
return handler ? handler(...args) : [op, ...args.map(prep)]
|
|
631
841
|
}
|
|
632
842
|
|
|
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
|
-
}
|
|
843
|
+
// Identifier prohibitions: op-policy.js REJECT_IDENTS (prep string nodes).
|
|
644
844
|
|
|
645
|
-
// Predefined globals seeded into scope.chain at ctx.reset().
|
|
845
|
+
// Predefined globals seeded into scope.chain at ctx.reset().
|
|
646
846
|
// used in ctx.core.emit[]. Dotted lookups (Math.sin) go through the '.' handler which
|
|
647
847
|
// resolves via scope.chain → module 'math' → registers 'math.sin' emitter.
|
|
648
848
|
// Not actually "implicit imports" — these are ambient globals that exist in every jz/JS
|
|
@@ -683,6 +883,13 @@ export const GLOBALS = {
|
|
|
683
883
|
const patternItems = (node) => node?.[0] === ',' ? node.slice(1) : [node]
|
|
684
884
|
const isDestructPattern = (node) => Array.isArray(node) && (node[0] === '[]' || node[0] === '{}')
|
|
685
885
|
|
|
886
|
+
// Element count of a prepared inline array literal `['[', e0, e1, …]` with no
|
|
887
|
+
// spread (spread → dynamic length). Returns null when not such a literal, so
|
|
888
|
+
// destructuring a non-literal source keeps its runtime element reads.
|
|
889
|
+
const inlineArrayLen = (e) =>
|
|
890
|
+
Array.isArray(e) && e[0] === '[' && !e.slice(1).some(x => Array.isArray(x) && x[0] === '...')
|
|
891
|
+
? e.length - 1 : null
|
|
892
|
+
|
|
686
893
|
const simpleArrayPatternItems = (pattern) => {
|
|
687
894
|
if (!Array.isArray(pattern) || pattern[0] !== '[]' || pattern.length !== 2) return null
|
|
688
895
|
const items = patternItems(pattern[1])
|
|
@@ -714,7 +921,7 @@ function scalarArrayDestruct(pattern, rhs) {
|
|
|
714
921
|
function declareGlobal(name, user = true) {
|
|
715
922
|
if (depth !== 0 || typeof name !== 'string') return name
|
|
716
923
|
if (ctx.scope.globals.has(name)) err(`'${name}' conflicts with a compiler internal — choose a different name`)
|
|
717
|
-
|
|
924
|
+
declGlobal(name, 'f64')
|
|
718
925
|
if (user) ctx.scope.userGlobals.add(name)
|
|
719
926
|
return name
|
|
720
927
|
}
|
|
@@ -749,7 +956,7 @@ function pushPatternAssign(target, valueExpr, out, decls = null) {
|
|
|
749
956
|
out.push(['=', target, valueExpr])
|
|
750
957
|
}
|
|
751
958
|
|
|
752
|
-
function expandDestruct(pattern, source, out, decls = null) {
|
|
959
|
+
function expandDestruct(pattern, source, out, decls = null, srcLen = null) {
|
|
753
960
|
if (!isDestructPattern(pattern)) return
|
|
754
961
|
|
|
755
962
|
if (pattern[0] === '[]') {
|
|
@@ -764,6 +971,15 @@ function expandDestruct(pattern, source, out, decls = null) {
|
|
|
764
971
|
continue
|
|
765
972
|
}
|
|
766
973
|
|
|
974
|
+
// Source is a known-length inline literal and this index is past its end →
|
|
975
|
+
// the element is statically `undefined` (so any `= default` applies). Folding
|
|
976
|
+
// it here skips a provably out-of-range read — which both avoids the runtime
|
|
977
|
+
// access and dodges an optimizer miscompile of the destructuring-temp shape.
|
|
978
|
+
if (srcLen != null && j >= srcLen) {
|
|
979
|
+
pushPatternAssign(item, [, JZ_UNDEF], out, decls)
|
|
980
|
+
continue
|
|
981
|
+
}
|
|
982
|
+
|
|
767
983
|
pushPatternAssign(item, ['[]', source, [, j]], out, decls)
|
|
768
984
|
}
|
|
769
985
|
return
|
|
@@ -844,7 +1060,7 @@ function prepDecl(op, ...inits) {
|
|
|
844
1060
|
ctx.scope.chain[i] = declName
|
|
845
1061
|
}
|
|
846
1062
|
if (ctx.scope.globals.has(declName)) err(`'${declName}' conflicts with a compiler internal — choose a different name`)
|
|
847
|
-
|
|
1063
|
+
declGlobal(declName, 'f64')
|
|
848
1064
|
ctx.scope.userGlobals.add(declName)
|
|
849
1065
|
} else if (typeof declName === 'string') {
|
|
850
1066
|
// Bare hoisted decl inside a function (var X jzified to `let X` at top
|
|
@@ -859,6 +1075,23 @@ function prepDecl(op, ...inits) {
|
|
|
859
1075
|
continue
|
|
860
1076
|
}
|
|
861
1077
|
const [, name, init] = i
|
|
1078
|
+
// `const alias = fn` whose RHS is a bare identifier naming a known function
|
|
1079
|
+
// is a compile-time function alias — the ES `export { fn as alias }` written
|
|
1080
|
+
// in declaration form (a recurring kernel idiom: paramList = extractParams,
|
|
1081
|
+
// toBoolFromEmitted = truthyIR …). Resolve `alias` straight to the function
|
|
1082
|
+
// so calls compile to a direct call and the export table re-exports the same
|
|
1083
|
+
// mangled func. Otherwise it would box a closure into a module global that a
|
|
1084
|
+
// cross-module callee resolves to the bare, unmangled name → "not in scope".
|
|
1085
|
+
// Module scope + `const` only: depth>0 aliases already work as closure values,
|
|
1086
|
+
// and a reassignable `let` is a genuine value binding, not an alias.
|
|
1087
|
+
if (op === 'const' && depth === 0 && typeof name === 'string' && typeof init === 'string') {
|
|
1088
|
+
const fn = hasFunc(init) ? init : (hasFunc(ctx.scope.chain[init]) ? ctx.scope.chain[init] : null)
|
|
1089
|
+
if (fn) {
|
|
1090
|
+
ctx.scope.chain[name] = fn
|
|
1091
|
+
if (name in ctx.func.exports) ctx.func.exports[name] = fn
|
|
1092
|
+
continue
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
862
1095
|
const staticStr = op === 'const' ? staticStringExpr(init) : null
|
|
863
1096
|
const staticArr = op === 'const' ? staticStringArrayValues(init) : null
|
|
864
1097
|
const normed = prep(init)
|
|
@@ -894,7 +1127,7 @@ function prepDecl(op, ...inits) {
|
|
|
894
1127
|
const p = normed.slice(1).filter(p => Array.isArray(p) && p[0] === ':').map(p => p[1])
|
|
895
1128
|
if (p.length) ctx.schema.vars.set(tmp, ctx.schema.register(p))
|
|
896
1129
|
}
|
|
897
|
-
expandDestruct(name, tmp, rest)
|
|
1130
|
+
expandDestruct(name, tmp, rest, null, inlineArrayLen(normed))
|
|
898
1131
|
continue
|
|
899
1132
|
}
|
|
900
1133
|
|
|
@@ -912,6 +1145,10 @@ function prepDecl(op, ...inits) {
|
|
|
912
1145
|
scopes[scopes.length - 1].set(name, name)
|
|
913
1146
|
}
|
|
914
1147
|
if (typeof declName === 'string' && fnNames) fnNames.add(declName)
|
|
1148
|
+
// A nested arrow stays a closure value (defFunc only lifts depth-0). Record
|
|
1149
|
+
// the binding so `.caller`/`.callee` on it reads as prohibited introspection.
|
|
1150
|
+
if (typeof declName === 'string' && Array.isArray(normed) && normed[0] === '=>')
|
|
1151
|
+
funcValueNames[funcValueNames.length - 1]?.add(declName)
|
|
915
1152
|
if (op === 'const') bindStaticConst(declName, staticStr, staticArr)
|
|
916
1153
|
// Track const for reassignment checks — only module-scope consts (depth 0)
|
|
917
1154
|
if (typeof declName === 'string' && depth === 0) {
|
|
@@ -942,20 +1179,29 @@ function prepDecl(op, ...inits) {
|
|
|
942
1179
|
// Track object schemas (after prefix so schema is keyed to final name)
|
|
943
1180
|
if (typeof declName === 'string' && Array.isArray(normed) && normed[0] === '{}' && normed.length > 1) {
|
|
944
1181
|
const props = []
|
|
1182
|
+
const addProp = n => { if (!props.includes(n)) props.push(n) }
|
|
1183
|
+
let allKnown = true
|
|
945
1184
|
for (const p of normed.slice(1)) {
|
|
946
|
-
|
|
1185
|
+
// Dedupe every key (explicit AND spread-sourced) so a `k: v` that overrides
|
|
1186
|
+
// a spread-provided key doesn't push a duplicate — that would shift the
|
|
1187
|
+
// indices of later keys past emitObjectSpread's deduped slot assignment
|
|
1188
|
+
// (its `addName` dedupes both), making `decl.laterKey` read the wrong slot.
|
|
1189
|
+
if (Array.isArray(p) && p[0] === ':') addProp(p[1])
|
|
947
1190
|
else if (Array.isArray(p) && p[0] === '...') {
|
|
948
|
-
// Merge spread source schema into this object's schema
|
|
949
1191
|
const srcSchema = typeof p[1] === 'string' && ctx.schema.resolve(p[1])
|
|
950
|
-
if (srcSchema) for (const n of srcSchema)
|
|
1192
|
+
if (srcSchema) for (const n of srcSchema) addProp(n)
|
|
1193
|
+
else allKnown = false
|
|
951
1194
|
}
|
|
952
1195
|
}
|
|
953
|
-
|
|
1196
|
+
// An unknown spread source makes the value a runtime HASH (see
|
|
1197
|
+
// emitObjectSpread). Binding a static schema would compile `decl.prop`
|
|
1198
|
+
// to a fixed slot load that misreads the hash, so leave reads dynamic.
|
|
1199
|
+
if (allKnown && props.length && ctx.schema.register) ctx.schema.vars.set(declName, ctx.schema.register(props))
|
|
954
1200
|
}
|
|
955
1201
|
// Module-scope variable → WASM global (mark as user-declared)
|
|
956
1202
|
if (depth === 0 && typeof declName === 'string') {
|
|
957
1203
|
if (ctx.scope.globals.has(declName)) err(`'${declName}' conflicts with a compiler internal — choose a different name`)
|
|
958
|
-
|
|
1204
|
+
declGlobal(declName, 'f64')
|
|
959
1205
|
ctx.scope.userGlobals.add(declName)
|
|
960
1206
|
}
|
|
961
1207
|
rest.push(['=', declName, normed])
|
|
@@ -972,7 +1218,7 @@ function prepDecl(op, ...inits) {
|
|
|
972
1218
|
// `import.meta.resolve("spec")` → the resolved URL as a static string.
|
|
973
1219
|
function foldImportMetaResolve(callee, args) {
|
|
974
1220
|
if (!isImportMetaProp(callee, 'resolve')) return undefined
|
|
975
|
-
const callArgs =
|
|
1221
|
+
const callArgs = handlerArgs(args)
|
|
976
1222
|
if (callArgs.length !== 1) err('`import.meta.resolve` requires one string literal argument')
|
|
977
1223
|
const spec = stringValue(callArgs[0])
|
|
978
1224
|
if (spec == null) err('`import.meta.resolve` supports only string literal arguments')
|
|
@@ -986,8 +1232,11 @@ function foldImportMetaResolve(callee, args) {
|
|
|
986
1232
|
// Returns the replacement IR, or `undefined` for an ordinary call.
|
|
987
1233
|
function dispatchConstructorCall(callee, args) {
|
|
988
1234
|
if (typeof callee !== 'string') return undefined
|
|
1235
|
+
// A user binding named like a constructor (`let Map = …`, `let Array = …`)
|
|
1236
|
+
// shadows the builtin — don't lower `Map(x)` to `new.Map`.
|
|
1237
|
+
if (shadowsBuiltin(callee)) return undefined
|
|
989
1238
|
if (callee === 'Array') {
|
|
990
|
-
const callArgs =
|
|
1239
|
+
const callArgs = handlerArgs(args)
|
|
991
1240
|
if (callArgs.length === 1) return handlers['new'](['()', callee, callArgs[0]])
|
|
992
1241
|
}
|
|
993
1242
|
if (CTORS.includes(callee)) return handlers['new'](['()', callee, ...args])
|
|
@@ -1005,7 +1254,7 @@ function foldNamespaceIntrospection(callee, args) {
|
|
|
1005
1254
|
if (!Array.isArray(callee) || callee[0] !== '.') return undefined
|
|
1006
1255
|
const [, obj, prop] = callee
|
|
1007
1256
|
if (obj === 'Array' && prop === 'isArray') {
|
|
1008
|
-
const cargs =
|
|
1257
|
+
const cargs = handlerArgs(args)
|
|
1009
1258
|
const a0 = cargs.length === 1 ? cargs[0] : null
|
|
1010
1259
|
if (typeof a0 === 'string' && GLOBALS[a0] && !(scopes.length && isDeclared(a0)) && !hasFunc(a0))
|
|
1011
1260
|
return [, 0]
|
|
@@ -1013,7 +1262,7 @@ function foldNamespaceIntrospection(callee, args) {
|
|
|
1013
1262
|
if (prop === 'hasOwnProperty' && typeof obj === 'string' && !(scopes.length && isDeclared(obj))) {
|
|
1014
1263
|
const mod = ctx.scope.chain[obj]
|
|
1015
1264
|
if (mod && !mod.includes('.') && hasModule(mod)) {
|
|
1016
|
-
const cargs =
|
|
1265
|
+
const cargs = handlerArgs(args)
|
|
1017
1266
|
const member = cargs.length === 1 ? stringValue(cargs[0]) : null
|
|
1018
1267
|
// Include the module so its emit keys (the namespace's member set) are
|
|
1019
1268
|
// registered; unreferenced emitters/data dead-strip in compile.
|
|
@@ -1027,6 +1276,11 @@ function foldNamespaceIntrospection(callee, args) {
|
|
|
1027
1276
|
// way: a bare identifier through the scope chain, an `obj.prop` member call
|
|
1028
1277
|
// through host imports / named-call / generic-method / namespace tables, and
|
|
1029
1278
|
// any other expression through `prep` (a callable runtime value).
|
|
1279
|
+
// Compiler-internal synthetic callees: emit-handled intrinsics, never user
|
|
1280
|
+
// function values — so a bare reference must not pull in the callable-value
|
|
1281
|
+
// (function table / closure) machinery.
|
|
1282
|
+
const INTRINSIC_CALLEES = new Set(['__iter_arr', '__keys_ro'])
|
|
1283
|
+
|
|
1030
1284
|
function resolveCallee(callee, args) {
|
|
1031
1285
|
if (typeof callee === 'string') {
|
|
1032
1286
|
const local = scopes.length && isDeclared(callee)
|
|
@@ -1038,12 +1292,23 @@ function resolveCallee(callee, args) {
|
|
|
1038
1292
|
if (hasModule(resolved) && !ctx.module.imports.some(i => i[3]?.[1] === `$${resolved}`)) includeModule(resolved)
|
|
1039
1293
|
return callee
|
|
1040
1294
|
}
|
|
1041
|
-
if (depth > 0 && !resolved && !ctx.func.exports[callee] && !ctx.module.imports.some(i => i[3]?.[1] === `$${callee}`))
|
|
1295
|
+
if (depth > 0 && !resolved && !INTRINSIC_CALLEES.has(callee) && !ctx.func.exports[callee] && !ctx.module.imports.some(i => i[3]?.[1] === `$${callee}`))
|
|
1042
1296
|
includeForCallableValue()
|
|
1043
1297
|
return callee
|
|
1044
1298
|
}
|
|
1045
1299
|
if (Array.isArray(callee) && callee[0] === '.') {
|
|
1046
1300
|
const [, obj, prop] = callee
|
|
1301
|
+
// A user binding named like a builtin namespace (`let Math = {…}`) shadows
|
|
1302
|
+
// it — resolve `Math.max(…)` as a method call on the local value, not the
|
|
1303
|
+
// builtin named-call. (Property reads route through the `.` handler's own
|
|
1304
|
+
// shadow check.)
|
|
1305
|
+
if (shadowsBuiltin(obj)) return prep(callee)
|
|
1306
|
+
// SIMD intrinsic namespaces resolve members directly to their emit key, ahead of
|
|
1307
|
+
// generic-method dispatch — they're pure namespaces (never runtime values), and
|
|
1308
|
+
// names like `f32x4.add` must not be mistaken for the generic `.add` (Set/Map).
|
|
1309
|
+
if (typeof obj === 'string' && typeof prop === 'string' && SIMD_NS.has(obj) && !(scopes.length && isDeclared(obj)) && !ctx.scope.userGlobals?.has?.(obj)) {
|
|
1310
|
+
includeModule(obj); return `${obj}.${prop}`
|
|
1311
|
+
}
|
|
1047
1312
|
const key = typeof obj === 'string' && typeof prop === 'string' ? `${obj}.${prop}` : null
|
|
1048
1313
|
if (key && ctx.module.hostImports?.[obj]?.[prop]) {
|
|
1049
1314
|
const spec = ctx.module.hostImports[obj][prop]
|
|
@@ -1078,18 +1343,13 @@ function renestSoleCommaArg(args) {
|
|
|
1078
1343
|
}
|
|
1079
1344
|
|
|
1080
1345
|
const handlers = {
|
|
1346
|
+
...rejectHandlers(err),
|
|
1081
1347
|
// Spread operator: [...expr] in arrays, f(...args) in calls, {...obj} in objects
|
|
1082
1348
|
'...'(expr) {
|
|
1083
1349
|
includeForArrayLiteral()
|
|
1084
1350
|
return ['...', prep(expr)]
|
|
1085
1351
|
},
|
|
1086
1352
|
|
|
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
1353
|
'debugger': () => null,
|
|
1094
1354
|
// Static-key delete (.x, ["x"], [literal]) would change the fixed schema → reject.
|
|
1095
1355
|
// Computed-key delete (obj[expr]) — including jessie's `delete ctx[k]` — lowers
|
|
@@ -1104,12 +1364,7 @@ const handlers = {
|
|
|
1104
1364
|
err('delete not supported: object shape is fixed')
|
|
1105
1365
|
},
|
|
1106
1366
|
'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
1367
|
'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
1368
|
|
|
1114
1369
|
// Destructuring assignment: [a, ...b] = expr or {x, y} = expr
|
|
1115
1370
|
'='(lhs, rhs) {
|
|
@@ -1147,7 +1402,7 @@ const handlers = {
|
|
|
1147
1402
|
// emit a dynamic property read + indirect call instead of a direct call.
|
|
1148
1403
|
if (ctx.func.names.has(name)) {
|
|
1149
1404
|
ctx.func.multiProp.add(`${fnBase}.${lhs[2]}`)
|
|
1150
|
-
do name = `${fnBase}$${lhs[2]}$${ctx.func.uniq++}
|
|
1405
|
+
do { name = `${fnBase}$${lhs[2]}$${ctx.func.uniq++}` } while (ctx.func.names.has(name))
|
|
1151
1406
|
}
|
|
1152
1407
|
// Build the target `.` node directly from the resolved base — re-`prep`ing
|
|
1153
1408
|
// the lhs would resolve a multiProp `fn.prop` to an rvalue (closure
|
|
@@ -1165,22 +1420,25 @@ const handlers = {
|
|
|
1165
1420
|
recordGlobalRep(plhs, prhs)
|
|
1166
1421
|
if (Array.isArray(prhs) && prhs[0] === '{}') {
|
|
1167
1422
|
const props = staticObjectProps(prhs.slice(1))
|
|
1168
|
-
if (props)
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1423
|
+
if (props) bindAssignSchema(plhs, ctx.schema.register(props.names))
|
|
1424
|
+
} else bindAssignSchema(plhs, null)
|
|
1425
|
+
} else bindAssignSchema(plhs, objLiteralSid(prhs))
|
|
1171
1426
|
// Static string/array facts hold only while every assignment is constant.
|
|
1172
1427
|
if (!assignedStaticGlobals.has(plhs) && (staticStr != null || staticArr)) bindStaticGlobal(plhs, staticStr, staticArr)
|
|
1173
1428
|
else deleteStaticGlobal(plhs)
|
|
1174
1429
|
assignedStaticGlobals.add(plhs)
|
|
1175
1430
|
}
|
|
1176
|
-
//
|
|
1177
|
-
//
|
|
1178
|
-
//
|
|
1179
|
-
//
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1431
|
+
// Object-literal assignment to a variable — e.g. a `var` that jzify hoisted
|
|
1432
|
+
// into `let x; x = {…}`. Recording the schema lets the binding behave like
|
|
1433
|
+
// `let x = {…}`: fixed-slot field access and for-in unroll. SOUNDNESS: the
|
|
1434
|
+
// shape holds only while EVERY assignment to the name agrees — one literal
|
|
1435
|
+
// shape, no other sources. Any disagreeing assignment (non-literal RHS such
|
|
1436
|
+
// as a table/Map lookup, or a different-shape literal) unbinds and poisons
|
|
1437
|
+
// the name; fixed-slot reads against one literal's layout would misread the
|
|
1438
|
+
// other sources' objects (e.g. `.x` returning another shape's slot-0 value).
|
|
1439
|
+
// Compile reads the END state, so the conflict check is order-insensitive.
|
|
1440
|
+
else if (typeof plhs === 'string') {
|
|
1441
|
+
bindAssignSchema(plhs, objLiteralSid(prhs))
|
|
1184
1442
|
}
|
|
1185
1443
|
return ['=', plhs, prhs]
|
|
1186
1444
|
},
|
|
@@ -1191,23 +1449,21 @@ const handlers = {
|
|
|
1191
1449
|
const catchClause = clauses.find(c => Array.isArray(c) && c[0] === 'catch')
|
|
1192
1450
|
const finallyClause = clauses.find(c => Array.isArray(c) && c[0] === 'finally')
|
|
1193
1451
|
const tryBody = prep(body)
|
|
1452
|
+
// prep(handler) ONCE — it has side effects (uniq++, scope pushes, includes), so
|
|
1453
|
+
// the no-finally catch branch must reuse `caught`, not re-prep (FE-3 fix).
|
|
1194
1454
|
const caught = catchClause
|
|
1195
|
-
? (
|
|
1196
|
-
const [, errName, handler] = catchClause
|
|
1197
|
-
return ['catch', tryBody, errName, prep(handler)]
|
|
1198
|
-
})()
|
|
1455
|
+
? ['catch', tryBody, catchClause[1], prep(catchClause[2])]
|
|
1199
1456
|
: tryBody
|
|
1200
|
-
|
|
1201
|
-
if (catchClause) {
|
|
1202
|
-
const [, errName, handler] = catchClause
|
|
1203
|
-
return ['catch', tryBody, errName, prep(handler)]
|
|
1204
|
-
}
|
|
1205
|
-
return tryBody
|
|
1457
|
+
return finallyClause ? ['finally', caught, prep(finallyClause[1])] : caught
|
|
1206
1458
|
},
|
|
1207
1459
|
'throw'(expr) { return ['throw', prep(expr)] },
|
|
1208
1460
|
|
|
1209
1461
|
// Template literal: [``, part, ...] → fused single-allocation string concat.
|
|
1210
1462
|
'`'(...parts) {
|
|
1463
|
+
// Fully-static template (`a${123}b`, `hello ${1+2} world`) folds to a single string
|
|
1464
|
+
// literal — a static data segment / SSO box, no runtime concat and no heap machinery.
|
|
1465
|
+
const folded = staticStringExpr(['`', ...parts])
|
|
1466
|
+
if (folded != null) return staticString(folded)
|
|
1211
1467
|
includeForStringValue()
|
|
1212
1468
|
const nodes = parts.map(p =>
|
|
1213
1469
|
Array.isArray(p) && p[0] == null && typeof p[1] === 'string' ? ['str', p[1]] : prep(p))
|
|
@@ -1238,6 +1494,24 @@ const handlers = {
|
|
|
1238
1494
|
return handlers['from'](fromNode[1], fromNode[2])
|
|
1239
1495
|
},
|
|
1240
1496
|
|
|
1497
|
+
// Mixed default+named import `import d, { n } from 'm'` — jessie emits it as a
|
|
1498
|
+
// statement-level comma `[',', ['import', d], ['from', spec, src]]` (the default
|
|
1499
|
+
// fragment lost its source). Reunite: bind the default, then the named specifiers,
|
|
1500
|
+
// both against the shared source. (prepareModule caches by specifier, so preparing
|
|
1501
|
+
// the source twice is a no-op — same as two separate `import` statements.)
|
|
1502
|
+
// Any other comma is a sequence expression: fall through to generic prep.
|
|
1503
|
+
','(...items) {
|
|
1504
|
+
if (items.length === 2
|
|
1505
|
+
&& Array.isArray(items[0]) && items[0][0] === 'import' && typeof items[0][1] === 'string'
|
|
1506
|
+
&& Array.isArray(items[1]) && items[1][0] === 'from') {
|
|
1507
|
+
const source = items[1][2]
|
|
1508
|
+
handlers['from'](items[0][1], source)
|
|
1509
|
+
handlers['from'](items[1][1], source)
|
|
1510
|
+
return null
|
|
1511
|
+
}
|
|
1512
|
+
return [',', ...items.map(prep)]
|
|
1513
|
+
},
|
|
1514
|
+
|
|
1241
1515
|
'from'(specifiers, source) {
|
|
1242
1516
|
const mod = source?.[1]
|
|
1243
1517
|
if (!mod || typeof mod !== 'string') return err('Invalid import source')
|
|
@@ -1296,8 +1570,8 @@ const handlers = {
|
|
|
1296
1570
|
}
|
|
1297
1571
|
|
|
1298
1572
|
// Tier 2: Source module (bundling)
|
|
1299
|
-
if (
|
|
1300
|
-
const resolved = prepareModule(mod, ctx.module.importSources[mod])
|
|
1573
|
+
if (isBundledModule(mod)) {
|
|
1574
|
+
const resolved = prepareModule(mod, ctx.module.importSources?.[mod])
|
|
1301
1575
|
// Default import: import name from 'mod' → bind to default export
|
|
1302
1576
|
if (typeof specifiers === 'string') {
|
|
1303
1577
|
const mangled = resolved.exports.get('default')
|
|
@@ -1374,19 +1648,39 @@ const handlers = {
|
|
|
1374
1648
|
pushScope(); const b = prep(body); popScope()
|
|
1375
1649
|
return ['while', c, b]
|
|
1376
1650
|
},
|
|
1651
|
+
// do { body } while (cond) → flag-guarded while: `flag=true; while (flag||cond) { flag=false; body }`.
|
|
1652
|
+
// jzify lowers this in default mode (jzify/transform.js), but strict mode skips jzify — without
|
|
1653
|
+
// this prepare-stage twin, strict `do-while` reaches emit as a raw 'do' and dies ("Unknown op: do"),
|
|
1654
|
+
// contradicting the README's strict-subset list. Re-prep the synthetic tree so scope/normalize apply.
|
|
1655
|
+
'do': (body, cond) => {
|
|
1656
|
+
const flag = `${T}do${ctx.func.uniq++}`
|
|
1657
|
+
return prep([';',
|
|
1658
|
+
['let', ['=', flag, [null, true]]],
|
|
1659
|
+
['while', ['||', flag, cond],
|
|
1660
|
+
['{}', [';', ['=', flag, [null, false]], body]]]])
|
|
1661
|
+
},
|
|
1377
1662
|
|
|
1378
1663
|
'export': decl => {
|
|
1379
1664
|
if (Array.isArray(decl) && (decl[0] === 'let' || decl[0] === 'const'))
|
|
1380
1665
|
for (const i of decl.slice(1))
|
|
1381
1666
|
if (Array.isArray(i) && i[0] === '=' && typeof i[1] === 'string')
|
|
1382
1667
|
ctx.func.exports[i[1]] = true
|
|
1668
|
+
// export name → bare-identifier re-export (shorthand for `export { name }`).
|
|
1669
|
+
// Register the binding and emit nothing; without this the name falls through
|
|
1670
|
+
// to `prep(decl)` below and compiles as a dead `global.get; drop` statement
|
|
1671
|
+
// while the export itself is silently lost.
|
|
1672
|
+
if (typeof decl === 'string') {
|
|
1673
|
+
const resolved = ctx.scope.chain[decl]
|
|
1674
|
+
ctx.func.exports[decl] = (resolved && resolved !== decl) ? resolved : decl
|
|
1675
|
+
return null
|
|
1676
|
+
}
|
|
1383
1677
|
// export { name, name as alias } from './mod' or export * from './mod'
|
|
1384
1678
|
if (Array.isArray(decl) && decl[0] === 'from') {
|
|
1385
1679
|
const mod = decl[2]?.[1]
|
|
1386
1680
|
if (!mod || typeof mod !== 'string') return null
|
|
1387
1681
|
// Source module re-export
|
|
1388
|
-
if (
|
|
1389
|
-
const resolved = prepareModule(mod, ctx.module.importSources[mod])
|
|
1682
|
+
if (isBundledModule(mod)) {
|
|
1683
|
+
const resolved = prepareModule(mod, ctx.module.importSources?.[mod])
|
|
1390
1684
|
if (decl[1] === '*') {
|
|
1391
1685
|
// export * from './mod' → register all exports
|
|
1392
1686
|
for (const [name, mangled] of resolved.exports) {
|
|
@@ -1439,7 +1733,7 @@ const handlers = {
|
|
|
1439
1733
|
if (defFunc('default', prep(val))) return null
|
|
1440
1734
|
}
|
|
1441
1735
|
// export default expr → create global 'default'
|
|
1442
|
-
|
|
1736
|
+
declGlobal('default', 'f64')
|
|
1443
1737
|
ctx.scope.userGlobals.add('default')
|
|
1444
1738
|
return ['=', 'default', prep(val)]
|
|
1445
1739
|
}
|
|
@@ -1456,12 +1750,18 @@ const handlers = {
|
|
|
1456
1750
|
depth++
|
|
1457
1751
|
pushScope(fnScope)
|
|
1458
1752
|
funcLocalNames.push(new Set(collectParamNames(raw)))
|
|
1753
|
+
funcValueNames.push(new Set())
|
|
1459
1754
|
|
|
1460
1755
|
const nextParams = []
|
|
1461
1756
|
const bodyPrefix = []
|
|
1462
1757
|
for (const r of raw) {
|
|
1463
1758
|
const c = classifyParam(r)
|
|
1464
1759
|
if (c.kind === 'rest') {
|
|
1760
|
+
// A rest param is an array: the binding holds one, and every call site
|
|
1761
|
+
// builds the rest array via `['[', …]`. Pull in the array emitter even
|
|
1762
|
+
// when the body never names an array literal (e.g. `(...xs) => 0`),
|
|
1763
|
+
// otherwise the call-site rest construction hits "Unknown op: [".
|
|
1764
|
+
includeForArrayLiteral()
|
|
1465
1765
|
nextParams.push(r)
|
|
1466
1766
|
if (typeof c.name === 'string') fnScope.set(c.name, c.name)
|
|
1467
1767
|
} else if (c.kind === 'plain') {
|
|
@@ -1497,6 +1797,7 @@ const handlers = {
|
|
|
1497
1797
|
const result = ['=>', Array.isArray(params) && params[0] === '()' ? ['()', inner] : inner, preparedBody]
|
|
1498
1798
|
popScope()
|
|
1499
1799
|
funcLocalNames.pop()
|
|
1800
|
+
funcValueNames.pop()
|
|
1500
1801
|
depth--
|
|
1501
1802
|
return result
|
|
1502
1803
|
},
|
|
@@ -1516,9 +1817,13 @@ const handlers = {
|
|
|
1516
1817
|
})]
|
|
1517
1818
|
},
|
|
1518
1819
|
|
|
1519
|
-
// Optional chaining / typeof — need ptr module
|
|
1520
|
-
|
|
1521
|
-
'
|
|
1820
|
+
// Optional chaining / typeof — need ptr module. Optional member access pulls
|
|
1821
|
+
// the same modules as plain `.`/`[]` (a method like `includes` needs string +
|
|
1822
|
+
// array for emit's runtime dispatch); the only difference is the nullish guard,
|
|
1823
|
+
// which is emit's concern. Without this, `obj?.m(…)` reaches emit missing the
|
|
1824
|
+
// `.m` emitter and falls to the dynamic path that needs an unincluded module.
|
|
1825
|
+
'?.'(obj, prop) { includeForProperty(prop); return ['?.', prep(obj), prop] },
|
|
1826
|
+
'?.[]'(obj, idx) { includeForArrayAccess(); return ['?.[]', prep(obj), prep(idx)] },
|
|
1522
1827
|
'?.()'(callee, callArgs) {
|
|
1523
1828
|
// Parser wraps multi-args in a comma list, like '()'. Unwrap so emit gets flat positional args.
|
|
1524
1829
|
const items = callArgs == null ? []
|
|
@@ -1557,7 +1862,16 @@ const handlers = {
|
|
|
1557
1862
|
return ['+', pa, pb]
|
|
1558
1863
|
},
|
|
1559
1864
|
'-'(a, b) {
|
|
1560
|
-
|
|
1865
|
+
// Fold `-<numeric literal>` to a literal, but NOT a bigint: jz's own `typeof` reports
|
|
1866
|
+
// a bigint value as 'number' too (its carrier is an f64), so under self-host this test
|
|
1867
|
+
// alone wrongly folds `-5n`, and negating the bigint here yields garbage (-2^63+5).
|
|
1868
|
+
// `typeof !== 'bigint'` excludes it in both engines (real JS: 'bigint'; jz: matches
|
|
1869
|
+
// 'bigint'). Bigint negation then flows to emit's i64.sub(0,·) path correctly.
|
|
1870
|
+
// `-0` is NOT folded: the self-host kernel evaluates the constant `-na[1]` with
|
|
1871
|
+
// i32 negation (i32 has no signed zero), collapsing -0→+0 — observable via sort's
|
|
1872
|
+
// -0<+0 tiebreak, Object.is, and 1/x. Leaving it as a runtime `u-` emits f64.neg,
|
|
1873
|
+
// which preserves the sign in both engines; V8 re-folds it, so no native cost.
|
|
1874
|
+
if (b === undefined) { const na = prep(a); return isLit(na) && typeof na[1] === 'number' && typeof na[1] !== 'bigint' && na[1] !== 0 ? [, -na[1]] : ['u-', na] }
|
|
1561
1875
|
return ['-', prep(a), prep(b)]
|
|
1562
1876
|
},
|
|
1563
1877
|
|
|
@@ -1565,17 +1879,19 @@ const handlers = {
|
|
|
1565
1879
|
'?'(cond, then, els) { return ['?:', prep(stripBoolNot(cond)), prep(then), prep(els)] },
|
|
1566
1880
|
|
|
1567
1881
|
// ++/-- prefix vs postfix: parser sends trailing null for postfix
|
|
1568
|
-
// Postfix i++ = (++i) - 1: increment happens, arithmetic recovers old value
|
|
1569
|
-
// Property
|
|
1882
|
+
// Postfix i++ = (++i) - 1: increment happens, arithmetic recovers old value.
|
|
1883
|
+
// Property obj.prop++ has no dedicated ++ node (the ++ emitter is name-based),
|
|
1884
|
+
// so it lowers to `obj.prop = obj.prop + 1` (returns the NEW value) — and the
|
|
1885
|
+
// same -1/+1 recovery wraps it for postfix to yield the OLD value.
|
|
1570
1886
|
'++'(a, _post) {
|
|
1571
1887
|
const n = prep(a)
|
|
1572
|
-
|
|
1573
|
-
return _post !== undefined ? ['-',
|
|
1888
|
+
const inc = Array.isArray(n) && (n[0] === '.' || n[0] === '[]') ? ['=', n, ['+', n, [, 1]]] : ['++', n]
|
|
1889
|
+
return _post !== undefined ? ['-', inc, [, 1]] : inc
|
|
1574
1890
|
},
|
|
1575
1891
|
'--'(a, _post) {
|
|
1576
1892
|
const n = prep(a)
|
|
1577
|
-
|
|
1578
|
-
return _post !== undefined ? ['+',
|
|
1893
|
+
const dec = Array.isArray(n) && (n[0] === '.' || n[0] === '[]') ? ['=', n, ['-', n, [, 1]]] : ['--', n]
|
|
1894
|
+
return _post !== undefined ? ['+', dec, [, 1]] : dec
|
|
1579
1895
|
},
|
|
1580
1896
|
|
|
1581
1897
|
// Regex literal: ['//','pattern','flags?'] → include regex module, pass through
|
|
@@ -1583,13 +1899,22 @@ const handlers = {
|
|
|
1583
1899
|
return ['//', pattern, flags]
|
|
1584
1900
|
},
|
|
1585
1901
|
|
|
1586
|
-
'**'(a, b) {
|
|
1902
|
+
'**'(a, b) {
|
|
1903
|
+
// ES2016 §13.6: an unparenthesized unary expression cannot be the base of `**`
|
|
1904
|
+
// — `-x**2`, `~x**2`, `!x**2`, `+x**2`, `typeof x**2`, `void x**2`, `delete o[k]**2`
|
|
1905
|
+
// are all SyntaxErrors (the precedence is ambiguous). The parser leaves a grouping
|
|
1906
|
+
// as `['()', …]`, so a parenthesized base `(-x)**2` (and `-(x**2)`, where the unary
|
|
1907
|
+
// sits outside the `**`) arrives with a non-unary root op and is allowed.
|
|
1908
|
+
if (Array.isArray(a) && a.length === 2 && (a[0] === '-' || a[0] === '+' || a[0] === '!' || a[0] === '~' || a[0] === 'typeof' || a[0] === 'void' || a[0] === 'delete'))
|
|
1909
|
+
err(`Unary '${a[0]}' before '**' is a SyntaxError (ES2016 §13.6) — parenthesize: (${a[0]} x) ** 2 or ${a[0]} (x ** 2)`)
|
|
1910
|
+
return ['**', prep(a), prep(b)]
|
|
1911
|
+
},
|
|
1587
1912
|
|
|
1588
1913
|
// Function call or grouping parens
|
|
1589
1914
|
'()'(callee, ...args) {
|
|
1590
1915
|
// Grouping: (expr) → ['()', expr] with no args. Call: f() → ['()', 'f', null] with null arg.
|
|
1591
1916
|
if (args.length === 0) return prep(callee)
|
|
1592
|
-
if (typeof callee === 'string' &&
|
|
1917
|
+
if (typeof callee === 'string' && REJECT_IDENTS[callee]) err(REJECT_IDENTS[callee])
|
|
1593
1918
|
|
|
1594
1919
|
// Compile-time folds: the callee names something resolvable now. Each fold
|
|
1595
1920
|
// is gated by callee shape, so at most one of the three fires.
|
|
@@ -1607,7 +1932,14 @@ const handlers = {
|
|
|
1607
1932
|
includeForCallableValue(); break
|
|
1608
1933
|
}
|
|
1609
1934
|
}
|
|
1610
|
-
|
|
1935
|
+
// A zero-arg call keeps its explicit `null` args slot: `['()', callee, null]`,
|
|
1936
|
+
// not the slot-less `['()', callee]`. The latter is indistinguishable from a
|
|
1937
|
+
// grouping `(expr)`, so a second `prep` pass (the destructuring-assignment
|
|
1938
|
+
// lowering re-`prep`s its result) would re-read `x.pop()` as the grouping
|
|
1939
|
+
// `(x.pop)` and drop the call. Keeping the slot makes `prep` idempotent for
|
|
1940
|
+
// calls and matches `setCallArgs`'s canonical shape; `commaList(node[2])`
|
|
1941
|
+
// reads it back as zero args everywhere downstream.
|
|
1942
|
+
const result = preppedArgs.length ? ['()', callee, ...preppedArgs] : ['()', callee, null]
|
|
1611
1943
|
|
|
1612
1944
|
if (callee === 'Object.assign' && ctx.schema.register) inferAssignSchema(result)
|
|
1613
1945
|
|
|
@@ -1649,9 +1981,11 @@ const handlers = {
|
|
|
1649
1981
|
},
|
|
1650
1982
|
|
|
1651
1983
|
// Object literal - flatten comma, expand shorthand
|
|
1652
|
-
'{}'(
|
|
1653
|
-
|
|
1654
|
-
|
|
1984
|
+
'{}'(...args) {
|
|
1985
|
+
const inner = args[0]
|
|
1986
|
+
// Block body: a single statement-op child (object props always start with
|
|
1987
|
+
// ':' or '...', never a statement op, so this never misfires on a literal).
|
|
1988
|
+
if (args.length === 1 && Array.isArray(inner) && STMT_OPS.has(inner[0])) {
|
|
1655
1989
|
// Block body: push block scope for let/const shadowing
|
|
1656
1990
|
pushScope()
|
|
1657
1991
|
const result = ['{}', prep(inner)]
|
|
@@ -1660,10 +1994,16 @@ const handlers = {
|
|
|
1660
1994
|
}
|
|
1661
1995
|
|
|
1662
1996
|
includeForObjectLiteral()
|
|
1663
|
-
if (inner == null) return ['{}']
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1997
|
+
if (args.length === 0 || inner == null) return ['{}']
|
|
1998
|
+
// The parser emits one comma-grouped child `['{}', [',', p1, p2]]`, but prep's
|
|
1999
|
+
// own output is spread `['{}', p1, p2]` (see `result` below). Accept both so
|
|
2000
|
+
// prep stays idempotent: the destructuring-assignment lowering ('=' handler)
|
|
2001
|
+
// re-preps a wrapper that already holds a normalized literal, and reading only
|
|
2002
|
+
// the first child here would drop every property but the first — mis-sizing the
|
|
2003
|
+
// schema to cap-1 and losing the rest.
|
|
2004
|
+
const items = args.length === 1
|
|
2005
|
+
? (Array.isArray(inner) && inner[0] === ',' ? inner.slice(1) : [inner])
|
|
2006
|
+
: args
|
|
1667
2007
|
|
|
1668
2008
|
// Computed keys: `{[k]: v}` where `k` isn't compile-time foldable. jz's
|
|
1669
2009
|
// object layout is slot-based (fixed schema at the literal site), so a
|
|
@@ -1699,6 +2039,12 @@ const handlers = {
|
|
|
1699
2039
|
if (key == null) err('computed property name not supported for fixed-shape object: use a compile-time string/number key')
|
|
1700
2040
|
return [':', key, prep(p[2])]
|
|
1701
2041
|
}
|
|
2042
|
+
// Accessors (`{ get x() {…} }` / `{ set x(v) {…} }`) parse to ['get'|'set', …].
|
|
2043
|
+
// jz objects are fixed-shape slot records with no accessor protocol, so they'd
|
|
2044
|
+
// otherwise fall through and compile to dead code (0 schema slots → `o.x` reads
|
|
2045
|
+
// undefined). Reject loudly — silent miscompile breaks "valid jz = valid JS".
|
|
2046
|
+
if (Array.isArray(p) && (p[0] === 'get' || p[0] === 'set'))
|
|
2047
|
+
err('object getter/setter not supported — jz objects have no accessors; use a method or a plain property + function')
|
|
1702
2048
|
return prep(p)
|
|
1703
2049
|
}
|
|
1704
2050
|
let prepped = items.map(prop)
|
|
@@ -1725,27 +2071,56 @@ const handlers = {
|
|
|
1725
2071
|
// For loop
|
|
1726
2072
|
'for'(head, body) {
|
|
1727
2073
|
pushScope()
|
|
2074
|
+
// A comma/sequence Expression in a for-IN head RHS — `for (x in a, b)` — is valid (the RHS is
|
|
2075
|
+
// an Expression): evaluate left-to-right for side effects, value as the last element. (for-OF's
|
|
2076
|
+
// RHS is an AssignmentExpression — no comma — so it is left alone.) jzify lands it as a bare `,`
|
|
2077
|
+
// node in the source slot. Don't wrap it in `()`: Object.keys((a, obj)) hides `obj` behind the
|
|
2078
|
+
// sequence and loses its static schema (a non-escaping literal scalarizes → 0 keys). Instead take
|
|
2079
|
+
// the LAST element as the (direct) iteration source and run the earlier elements once first.
|
|
2080
|
+
let forInSeqPre = null
|
|
2081
|
+
if (Array.isArray(head) && head[0] === 'in' && Array.isArray(head[2]) && head[2][0] === ',') {
|
|
2082
|
+
const parts = head[2].slice(1)
|
|
2083
|
+
head = [head[0], head[1], parts[parts.length - 1]]
|
|
2084
|
+
if (parts.length > 2) forInSeqPre = [',', ...parts.slice(0, -1)]
|
|
2085
|
+
else if (parts.length === 2) forInSeqPre = parts[0]
|
|
2086
|
+
}
|
|
1728
2087
|
let r
|
|
1729
2088
|
if (Array.isArray(head) && head[0] === ';') {
|
|
1730
2089
|
let [, init, cond, step] = head
|
|
1731
2090
|
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
|
-
//
|
|
2091
|
+
// Keep a `.length` / `.size` / `.byteLength` for-bound i32 without snapshotting it:
|
|
2092
|
+
// `i < arr.length` → `i < (arr.length | 0)` (re-read every iteration)
|
|
2093
|
+
// The `| 0` forces i32 even for unknown-typed receivers (where __length returns
|
|
2094
|
+
// f64), so the counter `i` stays i32 through the comparison and `i++` — no
|
|
2095
|
+
// per-iteration f64.convert_i32_s + f64.lt + f64.add + i32.trunc_sat round-trip.
|
|
2096
|
+
// It must stay INLINE, not hoisted into a pre-loop local: JS re-reads the bound
|
|
2097
|
+
// each step, and a loop body can grow/shrink the array mid-iteration — including
|
|
2098
|
+
// through an alias the compiler can't see locally (e.g. `arr` shares identity with
|
|
2099
|
+
// a field a called helper pushes to, as compilePendingClosures does over
|
|
2100
|
+
// ctx.closure.bodies). A snapshot diverges from JS and silently truncates such loops.
|
|
1740
2101
|
if (cond && Array.isArray(cond) && (cond[0] === '<' || cond[0] === '<=' || cond[0] === '>' || cond[0] === '>=')) {
|
|
1741
2102
|
const lenExpr = cond[0] === '<' || cond[0] === '<=' ? cond[2] : cond[1]
|
|
1742
2103
|
if (Array.isArray(lenExpr) && lenExpr[0] === '.' &&
|
|
1743
2104
|
(lenExpr[2] === 'length' || lenExpr[2] === 'size' || lenExpr[2] === 'byteLength')) {
|
|
1744
|
-
const
|
|
1745
|
-
const
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
2105
|
+
const recv = lenExpr[1]
|
|
2106
|
+
const bound = ['|', lenExpr, [, 0]]
|
|
2107
|
+
const lengthStable = typeof recv === 'string' &&
|
|
2108
|
+
boundSafeCalls(body) && boundSafeCalls(step) && !writesReceiver(body, recv) && !writesReceiver(step, recv)
|
|
2109
|
+
if (lengthStable) {
|
|
2110
|
+
// Body can't change the bound → snapshot it once into an i32 local. Keeps
|
|
2111
|
+
// the counter `i` i32 through compare + `i++` (no per-iteration f64 round
|
|
2112
|
+
// trip) and gives the vectorizer the hoisted trip count it matches on.
|
|
2113
|
+
const lenVar = `${T}len${ctx.func.uniq++}`
|
|
2114
|
+
const lenDecl = ['let', ['=', lenVar, bound]]
|
|
2115
|
+
init = init ? [';', init, lenDecl] : lenDecl
|
|
2116
|
+
if (cond[0] === '<' || cond[0] === '<=') cond = [cond[0], cond[1], lenVar]
|
|
2117
|
+
else cond = [cond[0], lenVar, cond[2]]
|
|
2118
|
+
} else {
|
|
2119
|
+
// Body may grow/shrink the array (push/pop, or alias mutation through a
|
|
2120
|
+
// call) → re-read every iteration, as JS does. Still `| 0` for an i32 bound.
|
|
2121
|
+
if (cond[0] === '<' || cond[0] === '<=') cond = [cond[0], cond[1], bound]
|
|
2122
|
+
else cond = [cond[0], bound, cond[2]]
|
|
2123
|
+
}
|
|
1749
2124
|
}
|
|
1750
2125
|
}
|
|
1751
2126
|
r = ['for', init ? prep(init) : null, cond ? prep(cond) : null, step ? prep(step) : null, prep(body)]
|
|
@@ -1757,61 +2132,71 @@ const handlers = {
|
|
|
1757
2132
|
const varName = Array.isArray(decl) && (decl[0] === 'let' || decl[0] === 'const') ? decl[1] : decl
|
|
1758
2133
|
const idx = `${T}i${ctx.func.uniq++}`
|
|
1759
2134
|
const lenVar = `${T}len${ctx.func.uniq++}`
|
|
1760
|
-
const
|
|
1761
|
-
|
|
2135
|
+
const arrVar = `${T}arr${ctx.func.uniq++}`
|
|
2136
|
+
// Normalize the source to an index-iterable once: a Set→keys / Map→[k,v]
|
|
2137
|
+
// array, while an Array/String/TypedArray passes through untouched (no
|
|
2138
|
+
// copy). Without this, `coll[i]` on a Set/Map reads raw open-addressing
|
|
2139
|
+
// slot words instead of live entries.
|
|
1762
2140
|
// Wrap .length in `| 0` so the hoisted bound is i32 even for unknown
|
|
1763
2141
|
// receivers (same rationale as the for-cond hoist above).
|
|
1764
2142
|
const lenE = ['|', ['.', arrVar, 'length'], [, 0]]
|
|
1765
|
-
const decls =
|
|
1766
|
-
? ['let', ['=', idx, [, 0]], ['=', lenVar, lenE]]
|
|
1767
|
-
: ['let', ['=', arrVar, src], ['=', idx, [, 0]], ['=', lenVar, lenE]]
|
|
2143
|
+
const decls = ['let', ['=', arrVar, ['()', '__iter_arr', src]], ['=', idx, [, 0]], ['=', lenVar, lenE]]
|
|
1768
2144
|
const cond = ['<', idx, lenVar]
|
|
1769
2145
|
const step = ['++', idx]
|
|
1770
2146
|
const inner = [';', ['let', ['=', varName, ['[]', arrVar, idx]]], body]
|
|
1771
2147
|
r = prep(['for', [';', decls, cond, step], inner])
|
|
1772
2148
|
} else if (Array.isArray(head) && head[0] === 'in') {
|
|
1773
|
-
// for
|
|
2149
|
+
// `for…in` relies on runtime key enumeration — outside the pure canonical subset. strict
|
|
2150
|
+
// rejects it (consistent with `obj[k]` / unknown-receiver methods); use `Object.keys(obj)`.
|
|
2151
|
+
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.')
|
|
2152
|
+
// for (let k in src) → enumerate src's own keys via Object.keys (schema ∪ any keys added
|
|
2153
|
+
// later for objects; "0".."n-1" for arrays/strings; [] for Set/Map) and iterate the resulting
|
|
2154
|
+
// array by index. One uniform path keeps for-in consistent with Object.keys (so dynamically
|
|
2155
|
+
// added keys appear in both), and break/continue work as in any for-loop. Object.keys'
|
|
2156
|
+
// enumeration stdlib is pulled only when for-in is actually used.
|
|
1774
2157
|
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
|
-
|
|
1811
|
-
//
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
2158
|
+
const isDecl = Array.isArray(decl) && (decl[0] === 'let' || decl[0] === 'const')
|
|
2159
|
+
// `for ((x) in …)` — the LHS may be a cover-parenthesized identifier; unwrap to the target.
|
|
2160
|
+
let lhs = decl; while (Array.isArray(lhs) && lhs[0] === '()') lhs = lhs[1]
|
|
2161
|
+
const target = isDecl ? decl[1] : lhs
|
|
2162
|
+
// A member/computed LHS (`for (x.y in …)`, `for (obj[k] in …)`) assigns each key into the
|
|
2163
|
+
// existing place; let/const and a bare name take a fresh per-iteration `let` binding.
|
|
2164
|
+
const isMemberTarget = Array.isArray(target) && (target[0] === '.' || target[0] === '[]')
|
|
2165
|
+
// for-in over null/undefined is a no-op — ES ForIn/OfHeadEvaluation returns a break
|
|
2166
|
+
// completion before enumerating — but Object.keys(null|undefined) throws. So a nullish
|
|
2167
|
+
// source must enumerate the empty set. A static null (`[null,null]`) / undefined (`[]`)
|
|
2168
|
+
// skips Object.keys entirely; a bare identifier is guarded by a runtime `== null` test
|
|
2169
|
+
// (evaluated twice — side-effect-free — keeping Object.keys' *direct* receiver so its
|
|
2170
|
+
// static key schema still resolves); object/array literals and other expressions, which
|
|
2171
|
+
// are never nullish or carry no static schema to lose, stay direct.
|
|
2172
|
+
// A nullish literal node is `[<nullish-op>, value]` with both slots nullish: `null` is
|
|
2173
|
+
// `[null, null]`, `undefined` is `[null]` (empty value slot). A numeric/string literal
|
|
2174
|
+
// `[null, v]` has a non-nullish value slot, so `src[1] == null` discriminates them.
|
|
2175
|
+
const nullish = Array.isArray(src) && src[0] == null && src[1] == null
|
|
2176
|
+
// `__keys_ro` is for-in's read-only key list: identical to Object.keys, but
|
|
2177
|
+
// when the receiver has a complete static schema the keys are a compile-time
|
|
2178
|
+
// constant, so it pools ONE static-data array instead of allocating a fresh
|
|
2179
|
+
// one each evaluation — the per-iteration heap-growth cliff (jz#deopt-forin).
|
|
2180
|
+
// Sound only because for-in reads ks[i]/ks.length and never mutates (unlike
|
|
2181
|
+
// user Object.keys, which permits in-place `.sort()`/`.reverse()`).
|
|
2182
|
+
includeMods('core', 'object', 'string')
|
|
2183
|
+
const keysExpr = nullish ? ['[]', null]
|
|
2184
|
+
: typeof src === 'string'
|
|
2185
|
+
? ['?', ['==', src, [null, null]], ['[]', null], ['()', '__keys_ro', src]]
|
|
2186
|
+
: ['()', '__keys_ro', src]
|
|
2187
|
+
const ks = `${T}fik${ctx.func.uniq++}`, ix = `${T}fii${ctx.func.uniq++}`, lenV = `${T}fil${ctx.func.uniq++}`
|
|
2188
|
+
const decls = ['let',
|
|
2189
|
+
['=', ks, keysExpr],
|
|
2190
|
+
['=', ix, [, 0]],
|
|
2191
|
+
['=', lenV, ['|', ['.', ks, 'length'], [, 0]]]]
|
|
2192
|
+
const bindEach = isMemberTarget
|
|
2193
|
+
? ['=', target, ['[]', ks, ix]] // x.y = key / obj[k] = key
|
|
2194
|
+
: ['let', ['=', target, ['[]', ks, ix]]] // let k = key (fresh per-iteration binding)
|
|
2195
|
+
const forNode = ['for', [';', decls, ['<', ix, lenV], ['++', ix]],
|
|
2196
|
+
[';', bindEach, body]]
|
|
2197
|
+
// Run the dropped sequence prefix (earlier comma elements) once for side effects, before
|
|
2198
|
+
// the loop. Built raw and prepped as a unit so prep inserts the value-drop on the prefix.
|
|
2199
|
+
r = prep(forInSeqPre ? [';', forInSeqPre, forNode] : forNode)
|
|
1815
2200
|
} else {
|
|
1816
2201
|
// Some parser/jzify shapes for `for (;;)` and `for (; cond; )` arrive
|
|
1817
2202
|
// as a null or bare-condition head instead of the canonical
|
|
@@ -1826,14 +2211,22 @@ const handlers = {
|
|
|
1826
2211
|
// Property access - resolve namespaces or object/array properties
|
|
1827
2212
|
'.'(obj, prop) {
|
|
1828
2213
|
prop = typeof prop === 'string' ? prop : staticPropertyKey(prop)
|
|
1829
|
-
|
|
2214
|
+
// `.caller`/`.callee` on a function value (or `arguments`) are deprecated
|
|
2215
|
+
// stack introspection — prohibited as bad practice. On a plain data object
|
|
2216
|
+
// they are ordinary field names (e.g. an ESTree call node's `.callee`), so
|
|
2217
|
+
// the ban keys off a known-function receiver, not the bare property name.
|
|
2218
|
+
if ((obj === 'arguments' || hasFunc(obj) || isFuncValueLocal(obj)) && (prop === 'caller' || prop === 'callee'))
|
|
2219
|
+
err('`.caller`/`.callee` are prohibited: deprecated function stack introspection')
|
|
1830
2220
|
if (prop === 'url' && isImportMeta(obj)) return staticString(importMetaUrl())
|
|
2221
|
+
// A user binding named like a builtin namespace (`let Math = {…}`) shadows it
|
|
2222
|
+
// — read the property off the local value, not the builtin namespace table.
|
|
2223
|
+
if (shadowsBuiltin(obj)) { includeForProperty(prop); return ['.', prep(obj), prop] }
|
|
1831
2224
|
const mod = ctx.scope.chain[obj]
|
|
1832
2225
|
// Only treat as module namespace if it's a known built-in module (not a mangled import name)
|
|
1833
2226
|
if (typeof obj === 'string' && mod && !mod.includes('.') && hasModule(mod)) {
|
|
1834
2227
|
includeModule(mod)
|
|
1835
2228
|
const key = mod + '.' + prop
|
|
1836
|
-
if (ctx.core.emit[key]
|
|
2229
|
+
if (emitArity(ctx.core.emit[key]) > 0) includeForCallableValue()
|
|
1837
2230
|
return key
|
|
1838
2231
|
}
|
|
1839
2232
|
// Source module namespace: import * as X → X.prop resolved to mangled name
|
|
@@ -1849,7 +2242,22 @@ const handlers = {
|
|
|
1849
2242
|
'new'(ctor, ...args) {
|
|
1850
2243
|
let name = ctor, ctorArgs = args
|
|
1851
2244
|
if (Array.isArray(ctor) && ctor[0] === '()') { name = ctor[1]; ctorArgs = ctor.slice(2) }
|
|
1852
|
-
|
|
2245
|
+
// No GC → weakness is unobservable, so we fold WeakSet/WeakMap to Set/Map right here:
|
|
2246
|
+
// construction and every .add/.has/.get/.set/.delete reuse the concrete emit path.
|
|
2247
|
+
// The fold lives in prepare, not jzify — so `strict` (which only skips jzify) wouldn't
|
|
2248
|
+
// drop it on its own; we reject explicitly. It's a deviation anyway (accepts primitive
|
|
2249
|
+
// keys, exposes .size/iteration), not a true subset member — there, use Set/Map directly.
|
|
2250
|
+
if (name === 'WeakSet' || name === 'WeakMap') {
|
|
2251
|
+
const concrete = name === 'WeakSet' ? 'Set' : 'Map'
|
|
2252
|
+
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).`)
|
|
2253
|
+
name = concrete
|
|
2254
|
+
}
|
|
2255
|
+
// A lone `null` ctorArg is the parser's no-args sentinel (`new Map()`), and
|
|
2256
|
+
// `new Map(null)`/`new Map(undefined)` are spec-equivalent to it (null/undefined
|
|
2257
|
+
// → empty collection). Drop it so the emit hits the empty-collection fast path
|
|
2258
|
+
// rather than lowering `prep(null)` → `[, 0]` and routing through `__map_from`.
|
|
2259
|
+
// Typed arrays keep the sentinel: there `[, 0]` is a legitimate zero length.
|
|
2260
|
+
if (ctorArgs.length === 1 && ctorArgs[0] == null && (name === 'Date' || COLLECTION_CTORS.includes(name))) ctorArgs = []
|
|
1853
2261
|
// Flatten comma-grouped args: [',', a, b, c] → [a, b, c]
|
|
1854
2262
|
if (ctorArgs.length === 1 && Array.isArray(ctorArgs[0]) && ctorArgs[0][0] === ',')
|
|
1855
2263
|
ctorArgs = ctorArgs[0].slice(1)
|
|
@@ -2009,6 +2417,20 @@ function collectReturns(node, out) {
|
|
|
2009
2417
|
|
|
2010
2418
|
const isLit = n => Array.isArray(n) && n[0] == null
|
|
2011
2419
|
|
|
2420
|
+
/** Self-host: pre-parsed module AST for a specifier, or undefined. Linear scan over
|
|
2421
|
+
* [specifier, ast] pairs — array indexing + string `===` are the ABI-safe primitives
|
|
2422
|
+
* the kernel can read off a host-marshalled argument (dynamic-key object reads aren't). */
|
|
2423
|
+
function moduleAstFor(specifier) {
|
|
2424
|
+
const asts = ctx.module.importAsts
|
|
2425
|
+
if (!asts) return undefined
|
|
2426
|
+
for (let i = 0; i < asts.length; i++) if (asts[i][0] === specifier) return asts[i][1]
|
|
2427
|
+
return undefined
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
/** True when `mod` is bundled in-process — as source (host parses it) or as a
|
|
2431
|
+
* pre-parsed AST (self-host kernel). Either path routes through prepareModule. */
|
|
2432
|
+
const isBundledModule = mod => !!ctx.module.importSources?.[mod] || moduleAstFor(mod) !== undefined
|
|
2433
|
+
|
|
2012
2434
|
/** Compile-time bundling: parse + prepare an imported module, collect exports. */
|
|
2013
2435
|
function prepareModule(specifier, source) {
|
|
2014
2436
|
includeModule('core')
|
|
@@ -2020,8 +2442,24 @@ function prepareModule(specifier, source) {
|
|
|
2020
2442
|
|
|
2021
2443
|
ctx.module.moduleStack.push(specifier)
|
|
2022
2444
|
|
|
2023
|
-
// Name mangling prefix
|
|
2024
|
-
|
|
2445
|
+
// Name mangling prefix. Long specifiers (the bundler keys modules by
|
|
2446
|
+
// ABSOLUTE path — 40-60 byte '_Users_…' / '_home_runner_…' prefixes on every
|
|
2447
|
+
// symbol) compact to 'm<N>_<basename>': symbol strings shrink ~4×, which is
|
|
2448
|
+
// a direct hot-path win in the SELF-HOST — watr resolves every `call $name`
|
|
2449
|
+
// and `local.get $name` through name-keyed maps, paying hash+compare per
|
|
2450
|
+
// byte, and shared 35-byte path prefixes defeated the hash-probe early-outs.
|
|
2451
|
+
// Deterministic per compile (registration order); short relative specifiers
|
|
2452
|
+
// keep the readable form.
|
|
2453
|
+
const sanitized = specifier.replace(/[^a-zA-Z0-9]/g, '_')
|
|
2454
|
+
let prefix
|
|
2455
|
+
if (sanitized.length <= 24) prefix = sanitized
|
|
2456
|
+
else {
|
|
2457
|
+
if (!ctx.module.prefixIds) ctx.module.prefixIds = new Map()
|
|
2458
|
+
let id = ctx.module.prefixIds.get(specifier)
|
|
2459
|
+
if (id == null) { id = ctx.module.prefixIds.size; ctx.module.prefixIds.set(specifier, id) }
|
|
2460
|
+
const base = sanitized.replace(/_(js|mjs|jz)$/, '').match(/[a-zA-Z0-9]+$/)?.[0] ?? ''
|
|
2461
|
+
prefix = `m${id}_${base.slice(-16)}`
|
|
2462
|
+
}
|
|
2025
2463
|
|
|
2026
2464
|
// Save caller state
|
|
2027
2465
|
const savedScope = ctx.scope.chain, savedExports = ctx.func.exports
|
|
@@ -2031,8 +2469,18 @@ function prepareModule(specifier, source) {
|
|
|
2031
2469
|
ctx.func.exports = {}
|
|
2032
2470
|
ctx.module.currentPrefix = prefix
|
|
2033
2471
|
|
|
2034
|
-
|
|
2035
|
-
|
|
2472
|
+
try {
|
|
2473
|
+
// Parse + prepare imported source (may trigger recursive imports). The parser
|
|
2474
|
+
// is injected via ctx.transform.parse (the host pipeline sets it) rather than
|
|
2475
|
+
// imported, so prepare carries no hard dependency on a concrete parser — the
|
|
2476
|
+
// same inversion as ctx.transform.jzify. The self-host kernel can't parse, so it
|
|
2477
|
+
// pre-parses the whole graph on the host and passes the ASTs via importAsts;
|
|
2478
|
+
// we consult those first and only parse `source` when no AST was supplied.
|
|
2479
|
+
let ast = moduleAstFor(specifier)
|
|
2480
|
+
if (ast === undefined) {
|
|
2481
|
+
if (!ctx.transform.parse) err('compile-time module bundling requires ctx.transform.parse (injected by the jz pipeline)')
|
|
2482
|
+
ast = ctx.transform.parse(source)
|
|
2483
|
+
}
|
|
2036
2484
|
if (ctx.transform.jzify) ast = ctx.transform.jzify(ast)
|
|
2037
2485
|
const savedDepth = depth; depth = 0
|
|
2038
2486
|
const moduleInit = prep(ast)
|
|
@@ -2046,9 +2494,9 @@ function prepareModule(specifier, source) {
|
|
|
2046
2494
|
const func = ctx.func.list.find(f => f.name === localName)
|
|
2047
2495
|
if (func) { renameFunc(func, mangled); func._modulePrefix = prefix }
|
|
2048
2496
|
if (ctx.scope.globals.has(localName)) {
|
|
2049
|
-
|
|
2497
|
+
// Records carry no name — a rename is a pure Map re-key.
|
|
2498
|
+
ctx.scope.globals.set(mangled, ctx.scope.globals.get(localName))
|
|
2050
2499
|
ctx.scope.globals.delete(localName)
|
|
2051
|
-
ctx.scope.globals.set(mangled, wat)
|
|
2052
2500
|
if (ctx.scope.userGlobals.has(localName)) { ctx.scope.userGlobals.delete(localName); ctx.scope.userGlobals.add(mangled) }
|
|
2053
2501
|
if (ctx.scope.globalTypes.has(localName)) { ctx.scope.globalTypes.set(mangled, ctx.scope.globalTypes.get(localName)); ctx.scope.globalTypes.delete(localName) }
|
|
2054
2502
|
}
|
|
@@ -2097,9 +2545,8 @@ function prepareModule(specifier, source) {
|
|
|
2097
2545
|
const func = ctx.func.list.find(f => f.name === alias)
|
|
2098
2546
|
if (func) renameFunc(func, mangled)
|
|
2099
2547
|
if (ctx.scope.globals.has(alias)) {
|
|
2100
|
-
|
|
2548
|
+
ctx.scope.globals.set(mangled, ctx.scope.globals.get(alias))
|
|
2101
2549
|
ctx.scope.globals.delete(alias)
|
|
2102
|
-
ctx.scope.globals.set(mangled, wat)
|
|
2103
2550
|
if (ctx.scope.userGlobals.has(alias)) { ctx.scope.userGlobals.delete(alias); ctx.scope.userGlobals.add(mangled) }
|
|
2104
2551
|
}
|
|
2105
2552
|
}
|
|
@@ -2165,15 +2612,18 @@ function prepareModule(specifier, source) {
|
|
|
2165
2612
|
recordModuleInitFacts(moduleInit)
|
|
2166
2613
|
}
|
|
2167
2614
|
|
|
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
2615
|
const result = { exports: moduleExports }
|
|
2175
2616
|
ctx.module.resolvedModules.set(specifier, result)
|
|
2176
2617
|
return result
|
|
2618
|
+
} finally {
|
|
2619
|
+
// ALWAYS restore caller state (FE-6): if `prep(ast)` or a recursive import threw
|
|
2620
|
+
// mid-prep, skipping this would leave ctx.scope/exports/prefix/moduleStack
|
|
2621
|
+
// corrupted for the rest of the pipeline.
|
|
2622
|
+
ctx.scope.chain = savedScope
|
|
2623
|
+
ctx.func.exports = savedExports
|
|
2624
|
+
ctx.module.currentPrefix = savedModulePrefix
|
|
2625
|
+
ctx.module.moduleStack.pop()
|
|
2626
|
+
}
|
|
2177
2627
|
}
|
|
2178
2628
|
|
|
2179
2629
|
// =============================================================================
|
|
@@ -2241,7 +2691,7 @@ function tryFusePair(decl, forNode, seq, declIdx) {
|
|
|
2241
2691
|
if (!hasAnyIndexedRead(forBody, NAME) && !hasAnyIndexedRead(cond, NAME)) return null
|
|
2242
2692
|
// `NAME` must not be read after the for-loop in the same block.
|
|
2243
2693
|
for (let k = declIdx + 2; k < seq.length; k++) {
|
|
2244
|
-
if (refsName(seq[k], NAME)) return null
|
|
2694
|
+
if (refsName(seq[k], NAME, { skipArrow: false })) return null
|
|
2245
2695
|
}
|
|
2246
2696
|
// RECV must not be reassigned inside the for-loop (would invalidate substitution).
|
|
2247
2697
|
if (assignsName(forNode, RECV) || assignsName(forNode, NAME)) return null
|
|
@@ -2283,12 +2733,6 @@ function hasAnyIndexedRead(n, NAME) {
|
|
|
2283
2733
|
for (let i = 1; i < n.length; i++) if (hasAnyIndexedRead(n[i], NAME)) return true
|
|
2284
2734
|
return false
|
|
2285
2735
|
}
|
|
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
2736
|
function assignsName(n, NAME) {
|
|
2293
2737
|
if (!Array.isArray(n)) return false
|
|
2294
2738
|
const op = n[0]
|