jz 0.4.0 → 0.5.1
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 +275 -255
- package/cli.js +8 -51
- package/index.js +166 -31
- package/{src/host.js → interop.js} +291 -88
- package/module/array.js +181 -71
- package/module/collection.js +222 -8
- package/module/console.js +1 -1
- package/module/core.js +277 -118
- package/module/date.js +9 -5
- package/module/function.js +11 -1
- package/module/json.js +361 -55
- package/module/math.js +601 -130
- package/module/number.js +390 -227
- package/module/object.js +252 -34
- package/module/regex.js +123 -16
- package/module/schema.js +15 -1
- package/module/string.js +545 -96
- package/module/timer.js +1 -1
- package/module/typedarray.js +674 -57
- package/package.json +10 -7
- package/src/abi/array.js +89 -0
- package/src/abi/index.js +35 -0
- package/src/abi/number.js +137 -0
- package/src/abi/object.js +57 -0
- package/src/abi/string.js +529 -0
- package/src/analyze.js +1870 -485
- package/src/assemble.js +27 -12
- package/src/autoload.js +13 -1
- package/src/compile.js +446 -179
- package/src/ctx.js +85 -18
- package/src/emit.js +705 -196
- package/src/infer.js +548 -0
- package/src/ir.js +291 -23
- package/src/jzify.js +851 -132
- package/src/narrow.js +433 -251
- package/src/optimize.js +126 -122
- package/src/plan.js +703 -34
- package/src/prepare.js +833 -153
- package/src/resolve.js +85 -0
- package/src/vectorize.js +99 -18
- package/src/ast.js +0 -160
- package/src/auto-config.js +0 -120
- package/src/fuse.js +0 -159
package/src/prepare.js
CHANGED
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
|
|
25
25
|
import { parse } from 'subscript/feature/jessie'
|
|
26
26
|
import { ctx, err, derive } from './ctx.js'
|
|
27
|
-
import { T, STMT_OPS, VAL,
|
|
27
|
+
import { T, STMT_OPS, VAL, extractParams, collectParamNames, classifyParam, observeNodeFacts, staticObjectProps, staticPropertyKey } from './analyze.js'
|
|
28
|
+
import { recordGlobalRep } from './infer.js'
|
|
28
29
|
import { isFuncRef } from './ir.js'
|
|
29
|
-
import { fuseSparseMapReads } from './fuse.js'
|
|
30
30
|
import {
|
|
31
31
|
CTORS, TIMER_NAMES,
|
|
32
32
|
hasModule, includeModule,
|
|
@@ -38,6 +38,8 @@ import {
|
|
|
38
38
|
|
|
39
39
|
let depth = 0 // arrow nesting depth (0=top-level, >0=inside function)
|
|
40
40
|
let scopes = [] // block scope stack: [{names: Set, renames: Map}]
|
|
41
|
+
let staticConstScopes = [] // lexical const facts: [{strings: Map, arrays: Map}]
|
|
42
|
+
let assignedStaticGlobals = null
|
|
41
43
|
// Per-arrow set of names already declared anywhere in the function body. Used
|
|
42
44
|
// to force a rename when the same identifier is declared in two sibling blocks
|
|
43
45
|
// (else-if arms, separate { ... } chunks): without renaming, both decls lower
|
|
@@ -89,10 +91,106 @@ const addHostImport = (mod, name, alias, spec) => {
|
|
|
89
91
|
|
|
90
92
|
const isImportMeta = node => Array.isArray(node) && node[0] === '.' && node[1] === 'import' && node[2] === 'meta'
|
|
91
93
|
const isImportMetaProp = (node, prop) => Array.isArray(node) && node[0] === '.' && isImportMeta(node[1]) && node[2] === prop
|
|
94
|
+
// In a pure boolean position (consumer reads only truthiness) `!!e` is exactly `e`.
|
|
95
|
+
// Drop redundant double-negation; recurse so `!!!!e → e`. NOT valid for `&&`/`||`
|
|
96
|
+
// operands — those are value-preserving (`!!a && b` returns `false`, not `a`).
|
|
97
|
+
const stripBoolNot = c => {
|
|
98
|
+
while (Array.isArray(c) && c[0] === '!' && Array.isArray(c[1]) && c[1][0] === '!') c = c[1][1]
|
|
99
|
+
return c
|
|
100
|
+
}
|
|
92
101
|
const stringValue = node => Array.isArray(node) && node[0] == null && typeof node[1] === 'string' ? node[1] : null
|
|
93
102
|
const flatArgs = args => args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',' ? args[0].slice(1) : args
|
|
94
103
|
const MUTATING_ARRAY_METHODS = new Set(['copyWithin', 'fill', 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'])
|
|
95
104
|
|
|
105
|
+
function staticStringArrayValues(expr) {
|
|
106
|
+
if (!Array.isArray(expr) || expr[0] !== '[]' || expr.length !== 2) return null
|
|
107
|
+
const raw = Array.isArray(expr[1]) && expr[1][0] === ',' ? expr[1].slice(1) : [expr[1]]
|
|
108
|
+
const out = []
|
|
109
|
+
for (const item of raw) {
|
|
110
|
+
const s = staticStringExpr(item)
|
|
111
|
+
if (s == null) return null
|
|
112
|
+
out.push(s)
|
|
113
|
+
}
|
|
114
|
+
return out
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function immediateStaticStringExpr(node) {
|
|
118
|
+
const lit = stringValue(node)
|
|
119
|
+
if (lit != null) return lit
|
|
120
|
+
if (Array.isArray(node) && node[0] === 'str' && typeof node[1] === 'string') return node[1]
|
|
121
|
+
if (!Array.isArray(node)) return null
|
|
122
|
+
const [op, ...args] = node
|
|
123
|
+
if (op === '+') {
|
|
124
|
+
const a = immediateStaticStringExpr(args[0])
|
|
125
|
+
const b = immediateStaticStringExpr(args[1])
|
|
126
|
+
return a != null && b != null ? a + b : null
|
|
127
|
+
}
|
|
128
|
+
if (op === '`') {
|
|
129
|
+
let out = ''
|
|
130
|
+
for (const part of args) {
|
|
131
|
+
const s = immediateStaticStringExpr(part)
|
|
132
|
+
if (s == null) return null
|
|
133
|
+
out += s
|
|
134
|
+
}
|
|
135
|
+
return out
|
|
136
|
+
}
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function immediateStaticStringArrayValues(expr) {
|
|
141
|
+
if (!Array.isArray(expr) || expr[0] !== '[]' || expr.length !== 2) return null
|
|
142
|
+
const raw = Array.isArray(expr[1]) && expr[1][0] === ',' ? expr[1].slice(1) : [expr[1]]
|
|
143
|
+
const out = []
|
|
144
|
+
for (const item of raw) {
|
|
145
|
+
const s = immediateStaticStringExpr(item)
|
|
146
|
+
if (s == null) return null
|
|
147
|
+
out.push(s)
|
|
148
|
+
}
|
|
149
|
+
return out
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function eachTopLevelStatement(node, fn) {
|
|
153
|
+
if (Array.isArray(node) && node[0] === ';') {
|
|
154
|
+
for (let i = 1; i < node.length; i++) fn(node[i])
|
|
155
|
+
} else {
|
|
156
|
+
fn(node)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function collectAssignmentWrites(node, writes) {
|
|
161
|
+
if (!Array.isArray(node)) return
|
|
162
|
+
const [op, lhs] = node
|
|
163
|
+
if (op === '=' && typeof lhs === 'string') writes.set(lhs, (writes.get(lhs) || 0) + 1)
|
|
164
|
+
if ((op === '++' || op === '--') && typeof lhs === 'string') writes.set(lhs, (writes.get(lhs) || 0) + 1)
|
|
165
|
+
for (let i = 1; i < node.length; i++) collectAssignmentWrites(node[i], writes)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function collectTopLevelStaticAssignments(node, facts) {
|
|
169
|
+
if (!Array.isArray(node)) return
|
|
170
|
+
if (node[0] === ',') {
|
|
171
|
+
for (let i = 1; i < node.length; i++) collectTopLevelStaticAssignments(node[i], facts)
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
if (node[0] !== '=' || typeof node[1] !== 'string') return
|
|
175
|
+
const str = immediateStaticStringExpr(node[2])
|
|
176
|
+
const arr = immediateStaticStringArrayValues(node[2])
|
|
177
|
+
if (str != null || arr) facts.set(node[1], { str, arr })
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function seedStaticGlobalAssignments(node) {
|
|
181
|
+
// jzify hoists function declarations ahead of `var` initializer assignments.
|
|
182
|
+
// Seed one-write static globals before preparing those function bodies so
|
|
183
|
+
// compile-time-only consumers (for example `new RegExp(`${PART}`)`) can still
|
|
184
|
+
// resolve the same constants they would see after module initialization.
|
|
185
|
+
const writes = new Map()
|
|
186
|
+
const facts = new Map()
|
|
187
|
+
collectAssignmentWrites(node, writes)
|
|
188
|
+
eachTopLevelStatement(node, stmt => collectTopLevelStaticAssignments(stmt, facts))
|
|
189
|
+
for (const [name, fact] of facts) {
|
|
190
|
+
if (writes.get(name) === 1) bindStaticGlobal(name, fact.str, fact.arr)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
96
194
|
function stringArrayValues(expr) {
|
|
97
195
|
if (!Array.isArray(expr) || expr[0] !== '[' || expr.length === 1) return null
|
|
98
196
|
const out = []
|
|
@@ -108,6 +206,63 @@ function staticString(value) {
|
|
|
108
206
|
return ['str', value]
|
|
109
207
|
}
|
|
110
208
|
|
|
209
|
+
function lookupStaticString(name) {
|
|
210
|
+
const resolved = scopes.length && isDeclared(name) ? resolveScope(name) : (ctx.scope.chain[name] || name)
|
|
211
|
+
for (let i = staticConstScopes.length - 1; i >= 0; i--) {
|
|
212
|
+
const v = staticConstScopes[i].strings.get(resolved)
|
|
213
|
+
if (v != null) return v
|
|
214
|
+
}
|
|
215
|
+
return ctx.scope.shapeStrs?.get(resolved) ?? ctx.scope.constStrs?.get(resolved) ?? null
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function lookupStaticStringArray(name) {
|
|
219
|
+
const resolved = scopes.length && isDeclared(name) ? resolveScope(name) : (ctx.scope.chain[name] || name)
|
|
220
|
+
for (let i = staticConstScopes.length - 1; i >= 0; i--) {
|
|
221
|
+
const v = staticConstScopes[i].arrays.get(resolved)
|
|
222
|
+
if (v) return v
|
|
223
|
+
}
|
|
224
|
+
return ctx.scope.shapeStrArrays?.get(resolved) ?? null
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function staticStringExpr(node) {
|
|
228
|
+
const lit = stringValue(node)
|
|
229
|
+
if (lit != null) return lit
|
|
230
|
+
if (Array.isArray(node) && node[0] === 'str' && typeof node[1] === 'string') return node[1]
|
|
231
|
+
if (typeof node === 'string') return lookupStaticString(node)
|
|
232
|
+
if (!Array.isArray(node)) return null
|
|
233
|
+
const [op, ...args] = node
|
|
234
|
+
if (op === '+') {
|
|
235
|
+
const a = staticStringExpr(args[0])
|
|
236
|
+
const b = staticStringExpr(args[1])
|
|
237
|
+
return a != null && b != null ? a + b : null
|
|
238
|
+
}
|
|
239
|
+
if (op === '`') {
|
|
240
|
+
let out = ''
|
|
241
|
+
for (const part of args) {
|
|
242
|
+
const s = staticStringExpr(part)
|
|
243
|
+
if (s == null) return null
|
|
244
|
+
out += s
|
|
245
|
+
}
|
|
246
|
+
return out
|
|
247
|
+
}
|
|
248
|
+
if (op === '``' && Array.isArray(args[0]) && args[0][0] === '.' && args[0][1] === 'String' && args[0][2] === 'raw') {
|
|
249
|
+
let out = ''
|
|
250
|
+
for (const part of args.slice(1)) {
|
|
251
|
+
const s = staticStringExpr(part)
|
|
252
|
+
if (s == null) return null
|
|
253
|
+
out += s
|
|
254
|
+
}
|
|
255
|
+
return out
|
|
256
|
+
}
|
|
257
|
+
if (op === '()' && Array.isArray(args[0]) && args[0][0] === '.' && args[0][2] === 'join' && typeof args[0][1] === 'string') {
|
|
258
|
+
const arr = lookupStaticStringArray(args[0][1])
|
|
259
|
+
if (!arr) return null
|
|
260
|
+
const sep = args.length > 1 && args[1] != null ? staticStringExpr(args[1]) : ','
|
|
261
|
+
return sep != null ? arr.join(sep) : null
|
|
262
|
+
}
|
|
263
|
+
return null
|
|
264
|
+
}
|
|
265
|
+
|
|
111
266
|
function importMetaUrl() {
|
|
112
267
|
if (!ctx.transform.importMetaUrl) err('`import.meta.url` requires compile option `importMetaUrl`')
|
|
113
268
|
return ctx.transform.importMetaUrl
|
|
@@ -119,17 +274,6 @@ function resolveImportMeta(spec) {
|
|
|
119
274
|
catch { err(`Cannot resolve import.meta specifier '${spec}' from '${base}'`) }
|
|
120
275
|
}
|
|
121
276
|
|
|
122
|
-
function recordGlobalValueFact(name, expr) {
|
|
123
|
-
if (typeof name !== 'string') return
|
|
124
|
-
const vt = valTypeOf(expr)
|
|
125
|
-
if (vt) {
|
|
126
|
-
;(ctx.scope.globalValTypes ||= new Map()).set(name, vt)
|
|
127
|
-
if (vt === VAL.REGEX && ctx.runtime.regex) ctx.runtime.regex.vars.set(name, expr)
|
|
128
|
-
}
|
|
129
|
-
const ctor = typedElemCtor(expr)
|
|
130
|
-
if (ctor) (ctx.scope.globalTypedElem ||= new Map()).set(name, ctor)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
277
|
function recordModuleInitFacts(root) {
|
|
134
278
|
const facts = ctx.module.initFacts ||= {
|
|
135
279
|
dynVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
|
|
@@ -182,10 +326,13 @@ function recordModuleInitFacts(root) {
|
|
|
182
326
|
export default function prepare(node) {
|
|
183
327
|
depth = 0
|
|
184
328
|
scopes = []
|
|
329
|
+
staticConstScopes = []
|
|
330
|
+
assignedStaticGlobals = new Set()
|
|
185
331
|
funcLocalNames = [new Set()]
|
|
186
332
|
includeModule('core')
|
|
187
333
|
normalizeIdents(node)
|
|
188
|
-
fuseSparseMapReads(node) // AST-level fusion; needs pre-resolution shape —
|
|
334
|
+
fuseSparseMapReads(node) // AST-level fusion; needs pre-resolution shape — defined at end of file
|
|
335
|
+
seedStaticGlobalAssignments(node)
|
|
189
336
|
const ast = prep(node)
|
|
190
337
|
// Top-level functions referenced as first-class values (e.g. `let o = { fn: g }`,
|
|
191
338
|
// `arr.push(g)`, `return g`) need trampoline emission, which depends on the fn
|
|
@@ -250,7 +397,7 @@ export default function prepare(node) {
|
|
|
250
397
|
|
|
251
398
|
// Invalidate shapeStrs for any module-level binding that's later assigned to.
|
|
252
399
|
// shapeStrs is "effectively-const string literals at module scope" — used by
|
|
253
|
-
//
|
|
400
|
+
// shape.js's jsonConstString to enable shape inference on `let SRC = '{...}'`
|
|
254
401
|
// patterns (bench convention) without enabling the const-only static fold.
|
|
255
402
|
// The scan must skip `=` nodes that are children of `let`/`const`/`export` —
|
|
256
403
|
// those are decl-initializers, not reassignments.
|
|
@@ -281,7 +428,7 @@ export default function prepare(node) {
|
|
|
281
428
|
|
|
282
429
|
// Named constants → numeric literals
|
|
283
430
|
export const JZ_NULL = Symbol('null')
|
|
284
|
-
const CONSTANTS = { 'true':
|
|
431
|
+
const CONSTANTS = { 'true': true, 'false': false, 'null': JZ_NULL, 'undefined': JZ_NULL }
|
|
285
432
|
// NaN/Infinity stay as special f64 values in emit()
|
|
286
433
|
const F64_CONSTANTS = { 'NaN': NaN, 'Infinity': Infinity }
|
|
287
434
|
|
|
@@ -297,6 +444,34 @@ function isDeclared(name) {
|
|
|
297
444
|
return scopes.some(s => s.has(name))
|
|
298
445
|
}
|
|
299
446
|
|
|
447
|
+
function pushScope(scope = new Map()) {
|
|
448
|
+
scopes.push(scope)
|
|
449
|
+
staticConstScopes.push({ strings: new Map(), arrays: new Map() })
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function popScope() {
|
|
453
|
+
scopes.pop()
|
|
454
|
+
staticConstScopes.pop()
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function bindStaticConst(name, str, arr) {
|
|
458
|
+
const frame = staticConstScopes.at(-1)
|
|
459
|
+
if (!frame || typeof name !== 'string') return
|
|
460
|
+
if (str != null) frame.strings.set(name, str)
|
|
461
|
+
if (arr) frame.arrays.set(name, arr)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function bindStaticGlobal(name, str, arr) {
|
|
465
|
+
if (typeof name !== 'string') return
|
|
466
|
+
if (str != null) (ctx.scope.shapeStrs ||= new Map()).set(name, str)
|
|
467
|
+
if (arr) (ctx.scope.shapeStrArrays ||= new Map()).set(name, arr)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function deleteStaticGlobal(name) {
|
|
471
|
+
ctx.scope.shapeStrs?.delete(name)
|
|
472
|
+
ctx.scope.shapeStrArrays?.delete(name)
|
|
473
|
+
}
|
|
474
|
+
|
|
300
475
|
const hasFunc = name => ctx.func.names.has(name)
|
|
301
476
|
|
|
302
477
|
const renameFunc = (func, nextName) => {
|
|
@@ -307,33 +482,81 @@ const renameFunc = (func, nextName) => {
|
|
|
307
482
|
|
|
308
483
|
/** Map JS typeof strings to jz type checks. Codes < 0 trigger specialized emitTypeofCmp paths. */
|
|
309
484
|
const TYPEOF_MAP = { 'number': -1, 'string': -2, 'undefined': -3, 'boolean': -4, 'object': -5, 'function': -6, 'bigint': -7 }
|
|
485
|
+
// Spec §13.5.3: `typeof undeclared_x` returns 'undefined' without throwing.
|
|
486
|
+
// True iff `name` is a bare identifier with no resolution path. Mirrors the
|
|
487
|
+
// resolution chain inside `prep()` so we don't speculate emit-time failures.
|
|
488
|
+
function isUnresolvableBareIdent(name) {
|
|
489
|
+
if (typeof name !== 'string') return false
|
|
490
|
+
if (name in CONSTANTS || name in F64_CONSTANTS) return false
|
|
491
|
+
if (name === 'Boolean' || name === 'Number') return false
|
|
492
|
+
if (PROHIBITED[name]) return false
|
|
493
|
+
if (scopes.length && isDeclared(name)) return false
|
|
494
|
+
if (ctx.scope.chain[name]) return false
|
|
495
|
+
if (GLOBALS[name]) return false
|
|
496
|
+
if (ctx.func.names.has(name)) return false
|
|
497
|
+
if (ctx.func?.locals?.has?.(name)) return false
|
|
498
|
+
// Top-level decls live in ctx.scope.globals / userGlobals (set by prepDecl at
|
|
499
|
+
// depth 0). Current arrow's local names are tracked in funcLocalNames.
|
|
500
|
+
if (ctx.scope.globals?.has?.(name)) return false
|
|
501
|
+
if (ctx.scope.userGlobals?.has?.(name)) return false
|
|
502
|
+
const fnNames = funcLocalNames[funcLocalNames.length - 1]
|
|
503
|
+
if (fnNames?.has(name)) return false
|
|
504
|
+
return true
|
|
505
|
+
}
|
|
310
506
|
// Constant fold typeof for known builtin namespaces (e.g. Math.exp). prep(x) resolves Math.exp → 'math.exp'.
|
|
311
507
|
function staticTypeofString(x) {
|
|
508
|
+
// Spec §13.5.3: unresolvable bare ref → 'undefined'.
|
|
509
|
+
if (isUnresolvableBareIdent(x)) return 'undefined'
|
|
312
510
|
// Bare callable global: parseInt, parseFloat, isNaN, isFinite, Error, BigInt, etc.
|
|
313
511
|
if (typeof x === 'string' && !ctx.func?.locals?.has(x) && GLOBALS[x] && ctx.core.emit?.[x]?.length > 0) return 'function'
|
|
314
512
|
const px = prep(x)
|
|
315
513
|
if (typeof px === 'string' && px.includes('.') && ctx.core.emit?.[px]?.length > 0) return 'function'
|
|
316
514
|
return null
|
|
317
515
|
}
|
|
516
|
+
// Builtin-namespace constructors expose `prototype`/`length`/`name` as own
|
|
517
|
+
// properties; plain namespaces (Math, JSON, Reflect, Atomics) do not.
|
|
518
|
+
const NS_CTORS = new Set(['Number', 'String', 'Boolean', 'BigInt', 'Object',
|
|
519
|
+
'Array', 'Symbol', 'Error', 'Date', 'RegExp', 'Function', 'Map', 'Set',
|
|
520
|
+
'Promise', 'ArrayBuffer', 'DataView', 'WeakMap', 'WeakSet'])
|
|
521
|
+
// `NS.hasOwnProperty("member")` is a compile-time question: jz models a
|
|
522
|
+
// builtin namespace as a set of emit keys, so a member is owned iff jz emits
|
|
523
|
+
// it — plus the universal constructor trio for constructor namespaces.
|
|
524
|
+
function namespaceHasOwn(mod, name, member) {
|
|
525
|
+
if (ctx.core.emit[`${mod}.${member}`] != null) return true
|
|
526
|
+
return NS_CTORS.has(name) && (member === 'prototype' || member === 'length' || member === 'name')
|
|
527
|
+
}
|
|
318
528
|
function resolveTypeof(node) {
|
|
319
529
|
const [op, a, b] = node
|
|
530
|
+
// `typeof` always yields a string, so `==`/`===` (and `!=`/`!==`) are
|
|
531
|
+
// equivalent here — both collapse to the same type check.
|
|
532
|
+
const eqLike = op === '==' || op === '==='
|
|
320
533
|
// typeof x == 'string' → type check
|
|
321
534
|
if (Array.isArray(a) && a[0] === 'typeof' && Array.isArray(b) && b[0] == null && typeof b[1] === 'string') {
|
|
322
535
|
const known = staticTypeofString(a[1])
|
|
323
|
-
if (known != null) return [,
|
|
536
|
+
if (known != null) return [, eqLike ? known === b[1] : known !== b[1]]
|
|
324
537
|
const code = TYPEOF_MAP[b[1]]
|
|
325
538
|
if (code != null) return [op, ['typeof', a[1]], [, code]]
|
|
326
539
|
}
|
|
327
540
|
// 'string' == typeof x
|
|
328
541
|
if (Array.isArray(b) && b[0] === 'typeof' && Array.isArray(a) && a[0] == null && typeof a[1] === 'string') {
|
|
329
542
|
const known = staticTypeofString(b[1])
|
|
330
|
-
if (known != null) return [,
|
|
543
|
+
if (known != null) return [, eqLike ? known === a[1] : known !== a[1]]
|
|
331
544
|
const code = TYPEOF_MAP[a[1]]
|
|
332
545
|
if (code != null) return [op, ['typeof', b[1]], [, code]]
|
|
333
546
|
}
|
|
334
547
|
return node
|
|
335
548
|
}
|
|
336
549
|
|
|
550
|
+
// Prepare a strict `===`/`!==`. resolveTypeof may fold `typeof x === 'type'` to a
|
|
551
|
+
// literal or rewrite it to a numeric-code compare; either way we prep the result's
|
|
552
|
+
// operands directly. The strict op stays intact (no collapse to loose `==`) so
|
|
553
|
+
// emit can apply the no-coercion type-mismatch fold.
|
|
554
|
+
function prepStrictEq(op, a, b) {
|
|
555
|
+
const r = resolveTypeof([op, a, b])
|
|
556
|
+
if (r[0] !== op) return prep(r) // folded to a literal — re-prep is safe
|
|
557
|
+
return [op, prep(r[1]), prep(r[2])] // keep strict op; prep operands only
|
|
558
|
+
}
|
|
559
|
+
|
|
337
560
|
const cloneNode = (node) => {
|
|
338
561
|
if (!Array.isArray(node)) return node
|
|
339
562
|
const copy = node.map(cloneNode)
|
|
@@ -341,12 +564,40 @@ const cloneNode = (node) => {
|
|
|
341
564
|
return copy
|
|
342
565
|
}
|
|
343
566
|
|
|
567
|
+
/** True if `node` contains a `break`/`continue` that belongs to it — i.e. not
|
|
568
|
+
* one nested inside its own function. (Nested loops are intentionally counted:
|
|
569
|
+
* an over-detection only opts into the safe frame-carrying lowering below.) */
|
|
570
|
+
const hasLoopJump = (node) => {
|
|
571
|
+
if (!Array.isArray(node)) return false
|
|
572
|
+
const op = node[0]
|
|
573
|
+
if (op === 'break' || op === 'continue') return true
|
|
574
|
+
if (op === '=>' || op === 'function') return false
|
|
575
|
+
return node.some(hasLoopJump)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** Retarget a for-in iteration's *own* unlabeled `break`/`continue` to explicit
|
|
579
|
+
* block labels — `break` to the construct-wide label, `continue` to this
|
|
580
|
+
* iteration's label. Nested loops/functions own their jumps and are skipped;
|
|
581
|
+
* labeled jumps already name their target and are left untouched. */
|
|
582
|
+
const retargetLoopJumps = (node, brkLabel, contLabel) => {
|
|
583
|
+
if (!Array.isArray(node)) return node
|
|
584
|
+
const op = node[0]
|
|
585
|
+
if (op === 'break' && node.length === 1) return ['break', brkLabel]
|
|
586
|
+
if (op === 'continue' && node.length === 1) return ['break', contLabel]
|
|
587
|
+
if (op === 'for' || op === 'for-in' || op === 'while' || op === 'do'
|
|
588
|
+
|| op === '=>' || op === 'function') return node
|
|
589
|
+
return node.map(c => retargetLoopJumps(c, brkLabel, contLabel))
|
|
590
|
+
}
|
|
591
|
+
|
|
344
592
|
function prep(node) {
|
|
345
593
|
if (Array.isArray(node)) includeForOp(node[0])
|
|
346
594
|
if (Array.isArray(node) && node.loc != null) ctx.error.loc = node.loc
|
|
347
595
|
if (node == null) return [, 0] // null/undefined → 0 literal
|
|
348
|
-
|
|
349
|
-
|
|
596
|
+
// Keep boolean identity (was folded to 1/0). The working representation is
|
|
597
|
+
// still i32/f64 0/1 — emit lowers the raw boolean — but valTypeOf now reads
|
|
598
|
+
// VAL.BOOL off the literal, so typeof/String/JSON/host boundary stay faithful.
|
|
599
|
+
if (node === true) return [, true]
|
|
600
|
+
if (node === false) return [, false]
|
|
350
601
|
if (!Array.isArray(node)) {
|
|
351
602
|
if (typeof node === 'string') {
|
|
352
603
|
if (node in CONSTANTS) return [, CONSTANTS[node]]
|
|
@@ -379,7 +630,11 @@ function prep(node) {
|
|
|
379
630
|
return handler ? handler(...args) : [op, ...args.map(prep)]
|
|
380
631
|
}
|
|
381
632
|
|
|
382
|
-
//
|
|
633
|
+
// Strict-jz prohibitions. `class` and `arguments` are *also* listed here but
|
|
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.
|
|
383
638
|
const PROHIBITED = { 'with': '`with` not supported', 'class': '`class` not supported', 'yield': '`yield` not supported',
|
|
384
639
|
'this': '`this` not supported: use explicit parameter',
|
|
385
640
|
'super': '`super` not supported: no class inheritance',
|
|
@@ -405,7 +660,21 @@ export const GLOBALS = {
|
|
|
405
660
|
isFinite: 'number',
|
|
406
661
|
parseInt: 'number',
|
|
407
662
|
parseFloat: 'number',
|
|
663
|
+
encodeURIComponent: 'encodeURIComponent',
|
|
664
|
+
decodeURIComponent: 'decodeURIComponent',
|
|
408
665
|
Error: 'Error',
|
|
666
|
+
// Error subclasses: distinct names in JS, but jz doesn't carry typed error
|
|
667
|
+
// info — `throw` accepts any value and stringification goes through the
|
|
668
|
+
// host. Treat them all as Error-shaped passthrough constructors so user
|
|
669
|
+
// code that throws specific subclasses (`throw new SyntaxError(msg)`) compiles
|
|
670
|
+
// identically. If we ever model `instanceof SyntaxError`, this is where to
|
|
671
|
+
// distinguish them; until then the surfaced message is what matters.
|
|
672
|
+
TypeError: 'Error',
|
|
673
|
+
SyntaxError: 'Error',
|
|
674
|
+
RangeError: 'Error',
|
|
675
|
+
ReferenceError: 'Error',
|
|
676
|
+
URIError: 'Error',
|
|
677
|
+
EvalError: 'Error',
|
|
409
678
|
BigInt: 'BigInt',
|
|
410
679
|
TextEncoder: 'TextEncoder',
|
|
411
680
|
TextDecoder: 'TextDecoder',
|
|
@@ -442,6 +711,27 @@ function scalarArrayDestruct(pattern, rhs) {
|
|
|
442
711
|
return prep([';', ['let', ...decls], ...assigns])
|
|
443
712
|
}
|
|
444
713
|
|
|
714
|
+
function declareGlobal(name, user = true) {
|
|
715
|
+
if (depth !== 0 || typeof name !== 'string') return name
|
|
716
|
+
if (ctx.scope.globals.has(name)) err(`'${name}' conflicts with a compiler internal — choose a different name`)
|
|
717
|
+
ctx.scope.globals.set(name, `(global $${name} (mut f64) (f64.const 0))`)
|
|
718
|
+
if (user) ctx.scope.userGlobals.add(name)
|
|
719
|
+
return name
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function bindingNames(pattern, out = new Set()) {
|
|
723
|
+
if (typeof pattern === 'string') out.add(pattern)
|
|
724
|
+
else if (Array.isArray(pattern)) {
|
|
725
|
+
if (pattern[0] === '...' && typeof pattern[1] === 'string') out.add(pattern[1])
|
|
726
|
+
else if (pattern[0] === '=') bindingNames(pattern[1], out)
|
|
727
|
+
else if (pattern[0] === ':') bindingNames(pattern[2], out)
|
|
728
|
+
else if (pattern[0] === '[]' || pattern[0] === '{}' || pattern[0] === ',') {
|
|
729
|
+
for (const item of pattern.slice(1)) bindingNames(item, out)
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return out
|
|
733
|
+
}
|
|
734
|
+
|
|
445
735
|
function pushPatternAssign(target, valueExpr, out, decls = null) {
|
|
446
736
|
if (Array.isArray(target) && target[0] === '=') {
|
|
447
737
|
pushPatternAssign(target[1], ['??', valueExpr, prep(target[2])], out, decls)
|
|
@@ -556,19 +846,51 @@ function prepDecl(op, ...inits) {
|
|
|
556
846
|
if (ctx.scope.globals.has(declName)) err(`'${declName}' conflicts with a compiler internal — choose a different name`)
|
|
557
847
|
ctx.scope.globals.set(declName, `(global $${declName} (mut f64) (f64.const 0))`)
|
|
558
848
|
ctx.scope.userGlobals.add(declName)
|
|
849
|
+
} else if (typeof declName === 'string') {
|
|
850
|
+
// Bare hoisted decl inside a function (var X jzified to `let X` at top
|
|
851
|
+
// of arrow + a later `X = …` assignment). Without registering here, the
|
|
852
|
+
// name is invisible to scope predicates like `isUnresolvableBareIdent`
|
|
853
|
+
// until the assignment runs — which is after any reference to it.
|
|
854
|
+
const fnNames = funcLocalNames[funcLocalNames.length - 1]
|
|
855
|
+
if (fnNames) fnNames.add(declName)
|
|
856
|
+
if (scopes.length > 0) scopes[scopes.length - 1].set(declName, declName)
|
|
559
857
|
}
|
|
560
858
|
rest.push(declName)
|
|
561
859
|
continue
|
|
562
860
|
}
|
|
563
|
-
const [, name, init] = i
|
|
861
|
+
const [, name, init] = i
|
|
862
|
+
const staticStr = op === 'const' ? staticStringExpr(init) : null
|
|
863
|
+
const staticArr = op === 'const' ? staticStringArrayValues(init) : null
|
|
864
|
+
const normed = prep(init)
|
|
564
865
|
|
|
565
866
|
if (isDestructPattern(name)) {
|
|
867
|
+
// Register each binding both as a module global (depth 0) and in the
|
|
868
|
+
// current arrow's local scope (depth ≠ 0). Without the local registration
|
|
869
|
+
// the name is invisible to `isUnresolvableBareIdent`, so a later
|
|
870
|
+
// `typeof x` would mis-fold to 'undefined' (spec §13.5.3) before emit ever
|
|
871
|
+
// sees the binding — see the bare-hoisted-decl branch above for the same fix.
|
|
872
|
+
const fnNames = funcLocalNames[funcLocalNames.length - 1]
|
|
873
|
+
for (const n of bindingNames(name)) {
|
|
874
|
+
declareGlobal(n)
|
|
875
|
+
if (depth !== 0 && typeof n === 'string') {
|
|
876
|
+
if (fnNames) fnNames.add(n)
|
|
877
|
+
if (scopes.length > 0) scopes[scopes.length - 1].set(n, n)
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
// A bare-identifier source needs no temp: reads are idempotent and
|
|
881
|
+
// side-effect-free, so we destructure straight off it. This keeps each
|
|
882
|
+
// element's static type tag (e.g. `let [, x] = strs` resolves `x` to the
|
|
883
|
+
// same STRING that `strs[1]` would) — a copy temp drops the array's
|
|
884
|
+
// element-type shape and `typeof x` would degrade to 'undefined'.
|
|
885
|
+
if (typeof normed === 'string') {
|
|
886
|
+
expandDestruct(name, normed, rest)
|
|
887
|
+
continue
|
|
888
|
+
}
|
|
566
889
|
const tmp = `${T}d${ctx.func.uniq++}`
|
|
890
|
+
declareGlobal(tmp, false)
|
|
567
891
|
rest.push(['=', tmp, normed])
|
|
568
892
|
// Propagate schema to temp so rest destructuring can resolve it
|
|
569
|
-
if (
|
|
570
|
-
ctx.schema.vars.set(tmp, ctx.schema.vars.get(normed))
|
|
571
|
-
else if (Array.isArray(normed) && normed[0] === '{}') {
|
|
893
|
+
if (Array.isArray(normed) && normed[0] === '{}') {
|
|
572
894
|
const p = normed.slice(1).filter(p => Array.isArray(p) && p[0] === ':').map(p => p[1])
|
|
573
895
|
if (p.length) ctx.schema.vars.set(tmp, ctx.schema.register(p))
|
|
574
896
|
}
|
|
@@ -590,18 +912,19 @@ function prepDecl(op, ...inits) {
|
|
|
590
912
|
scopes[scopes.length - 1].set(name, name)
|
|
591
913
|
}
|
|
592
914
|
if (typeof declName === 'string' && fnNames) fnNames.add(declName)
|
|
915
|
+
if (op === 'const') bindStaticConst(declName, staticStr, staticArr)
|
|
593
916
|
// Track const for reassignment checks — only module-scope consts (depth 0)
|
|
594
917
|
if (typeof declName === 'string' && depth === 0) {
|
|
595
918
|
if (ctx.module.currentPrefix) {
|
|
596
919
|
declName = `${ctx.module.currentPrefix}$${declName}`
|
|
597
920
|
ctx.scope.chain[name] = declName
|
|
598
921
|
}
|
|
922
|
+
if (op === 'const') bindStaticGlobal(declName, staticStr, staticArr)
|
|
599
923
|
if (op === 'const') {
|
|
600
924
|
if (!ctx.scope.consts) ctx.scope.consts = new Set()
|
|
601
925
|
ctx.scope.consts.add(declName)
|
|
602
|
-
if (
|
|
603
|
-
|
|
604
|
-
const strs = stringArrayValues(normed)
|
|
926
|
+
if (staticStr != null) (ctx.scope.constStrs ||= new Map()).set(declName, staticStr)
|
|
927
|
+
const strs = staticArr || stringArrayValues(normed)
|
|
605
928
|
if (strs) (ctx.scope.shapeStrArrays ||= new Map()).set(declName, strs)
|
|
606
929
|
} else if (op === 'let' && ctx.scope.consts?.has(declName)) {
|
|
607
930
|
ctx.scope.consts.delete(declName)
|
|
@@ -614,7 +937,7 @@ function prepDecl(op, ...inits) {
|
|
|
614
937
|
// entry whose name is later assigned to.
|
|
615
938
|
if (Array.isArray(normed) && normed[0] === 'str' && typeof normed[1] === 'string')
|
|
616
939
|
(ctx.scope.shapeStrs ||= new Map()).set(declName, normed[1])
|
|
617
|
-
|
|
940
|
+
recordGlobalRep(declName, normed)
|
|
618
941
|
}
|
|
619
942
|
// Track object schemas (after prefix so schema is keyed to final name)
|
|
620
943
|
if (typeof declName === 'string' && Array.isArray(normed) && normed[0] === '{}' && normed.length > 1) {
|
|
@@ -641,6 +964,119 @@ function prepDecl(op, ...inits) {
|
|
|
641
964
|
return rest.length ? [op, ...rest] : null
|
|
642
965
|
}
|
|
643
966
|
|
|
967
|
+
// --- `'()'` call-handler helpers --------------------------------------------
|
|
968
|
+
// The call handler is a thin dispatcher: it tries the compile-time folds
|
|
969
|
+
// below (each gated by callee shape, so at most one fires), then resolves the
|
|
970
|
+
// callee, then assembles the call. Each helper moves one concern out of line.
|
|
971
|
+
|
|
972
|
+
// `import.meta.resolve("spec")` → the resolved URL as a static string.
|
|
973
|
+
function foldImportMetaResolve(callee, args) {
|
|
974
|
+
if (!isImportMetaProp(callee, 'resolve')) return undefined
|
|
975
|
+
const callArgs = flatArgs(args).filter(a => a != null)
|
|
976
|
+
if (callArgs.length !== 1) err('`import.meta.resolve` requires one string literal argument')
|
|
977
|
+
const spec = stringValue(callArgs[0])
|
|
978
|
+
if (spec == null) err('`import.meta.resolve` supports only string literal arguments')
|
|
979
|
+
return staticString(resolveImportMeta(spec))
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// String-callee constructor / named-builtin folds: `Array(n)` and the `CTORS`
|
|
983
|
+
// set redirect to the `new` handler; `BigInt64Array`/`BigUint64Array` build a
|
|
984
|
+
// direct module call. `includeForNamedCall` is probed for every string callee
|
|
985
|
+
// — that probe is also how a module-backed builtin gets its modules included.
|
|
986
|
+
// Returns the replacement IR, or `undefined` for an ordinary call.
|
|
987
|
+
function dispatchConstructorCall(callee, args) {
|
|
988
|
+
if (typeof callee !== 'string') return undefined
|
|
989
|
+
if (callee === 'Array') {
|
|
990
|
+
const callArgs = flatArgs(args).filter(a => a != null)
|
|
991
|
+
if (callArgs.length === 1) return handlers['new'](['()', callee, callArgs[0]])
|
|
992
|
+
}
|
|
993
|
+
if (CTORS.includes(callee)) return handlers['new'](['()', callee, ...args])
|
|
994
|
+
if (includeForNamedCall(callee) && (callee === 'BigInt64Array' || callee === 'BigUint64Array'))
|
|
995
|
+
return ['()', callee, ...args.filter(a => a != null).map(prep)]
|
|
996
|
+
return undefined
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Compile-time namespace introspection on a `obj.prop(...)` callee:
|
|
1000
|
+
// `Array.isArray(NS)` on a bare builtin global folds to `false` (a namespace
|
|
1001
|
+
// value is never an array); `NS.hasOwnProperty("member")` on a builtin
|
|
1002
|
+
// namespace folds to a literal — no runtime namespace object. Returns the
|
|
1003
|
+
// folded literal IR, or `undefined` when nothing folds.
|
|
1004
|
+
function foldNamespaceIntrospection(callee, args) {
|
|
1005
|
+
if (!Array.isArray(callee) || callee[0] !== '.') return undefined
|
|
1006
|
+
const [, obj, prop] = callee
|
|
1007
|
+
if (obj === 'Array' && prop === 'isArray') {
|
|
1008
|
+
const cargs = flatArgs(args).filter(a => a != null)
|
|
1009
|
+
const a0 = cargs.length === 1 ? cargs[0] : null
|
|
1010
|
+
if (typeof a0 === 'string' && GLOBALS[a0] && !(scopes.length && isDeclared(a0)) && !hasFunc(a0))
|
|
1011
|
+
return [, 0]
|
|
1012
|
+
}
|
|
1013
|
+
if (prop === 'hasOwnProperty' && typeof obj === 'string' && !(scopes.length && isDeclared(obj))) {
|
|
1014
|
+
const mod = ctx.scope.chain[obj]
|
|
1015
|
+
if (mod && !mod.includes('.') && hasModule(mod)) {
|
|
1016
|
+
const cargs = flatArgs(args).filter(a => a != null)
|
|
1017
|
+
const member = cargs.length === 1 ? stringValue(cargs[0]) : null
|
|
1018
|
+
// Include the module so its emit keys (the namespace's member set) are
|
|
1019
|
+
// registered; unreferenced emitters/data dead-strip in compile.
|
|
1020
|
+
if (member != null) { includeModule(mod); return [, namespaceHasOwn(mod, obj, member) ? 1 : 0] }
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return undefined
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// Resolve a callee to its lowered form, triggering module autoloads along the
|
|
1027
|
+
// way: a bare identifier through the scope chain, an `obj.prop` member call
|
|
1028
|
+
// through host imports / named-call / generic-method / namespace tables, and
|
|
1029
|
+
// any other expression through `prep` (a callable runtime value).
|
|
1030
|
+
function resolveCallee(callee, args) {
|
|
1031
|
+
if (typeof callee === 'string') {
|
|
1032
|
+
const local = scopes.length && isDeclared(callee)
|
|
1033
|
+
const resolved = local ? null : ctx.scope.chain[callee]
|
|
1034
|
+
if (local) return resolveScope(callee)
|
|
1035
|
+
if (resolved?.includes('.')) return resolved
|
|
1036
|
+
if (resolved && hasFunc(resolved)) return resolved
|
|
1037
|
+
if (resolved && !resolved.includes('.')) {
|
|
1038
|
+
if (hasModule(resolved) && !ctx.module.imports.some(i => i[3]?.[1] === `$${resolved}`)) includeModule(resolved)
|
|
1039
|
+
return callee
|
|
1040
|
+
}
|
|
1041
|
+
if (depth > 0 && !resolved && !ctx.func.exports[callee] && !ctx.module.imports.some(i => i[3]?.[1] === `$${callee}`))
|
|
1042
|
+
includeForCallableValue()
|
|
1043
|
+
return callee
|
|
1044
|
+
}
|
|
1045
|
+
if (Array.isArray(callee) && callee[0] === '.') {
|
|
1046
|
+
const [, obj, prop] = callee
|
|
1047
|
+
const key = typeof obj === 'string' && typeof prop === 'string' ? `${obj}.${prop}` : null
|
|
1048
|
+
if (key && ctx.module.hostImports?.[obj]?.[prop]) {
|
|
1049
|
+
const spec = ctx.module.hostImports[obj][prop]
|
|
1050
|
+
const alias = `${obj}$${prop}`
|
|
1051
|
+
addHostImport(obj, prop, alias, spec)
|
|
1052
|
+
return alias
|
|
1053
|
+
}
|
|
1054
|
+
if (key && includeForNamedCall(key)) return key
|
|
1055
|
+
if (includeForGenericMethod(prop)) return prep(callee)
|
|
1056
|
+
const mod = ctx.scope.chain[obj]
|
|
1057
|
+
if (typeof obj === 'string' && mod && !mod.includes('.') && hasModule(mod))
|
|
1058
|
+
return (includeModule(mod), mod + '.' + prop)
|
|
1059
|
+
return prep(callee)
|
|
1060
|
+
}
|
|
1061
|
+
includeForCallableValue()
|
|
1062
|
+
return prep(callee)
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// A lone parenthesized comma-expression argument — `f((a, b, c))` — is ONE
|
|
1066
|
+
// argument whose value is the last comma operand. The parser keeps it wrapped
|
|
1067
|
+
// (`['()', [',', …]]`); prep would strip the grouping, leaving a bare comma
|
|
1068
|
+
// that emit can no longer tell apart from an arg list and splats into N args.
|
|
1069
|
+
// With ≥2 args an outer arg-list comma already nests it — only the sole-arg
|
|
1070
|
+
// case loses the distinction. Re-nest it under a 1-element arg-list comma.
|
|
1071
|
+
function renestSoleCommaArg(args) {
|
|
1072
|
+
if (args.length === 1 && Array.isArray(args[0]) && args[0][0] === '()' && args[0].length === 2) {
|
|
1073
|
+
const ungroup = n => Array.isArray(n) && n[0] === '()' && n.length === 2 ? ungroup(n[1]) : n
|
|
1074
|
+
const core = ungroup(args[0])
|
|
1075
|
+
if (Array.isArray(core) && core[0] === ',') return [[',', args[0]]]
|
|
1076
|
+
}
|
|
1077
|
+
return args
|
|
1078
|
+
}
|
|
1079
|
+
|
|
644
1080
|
const handlers = {
|
|
645
1081
|
// Spread operator: [...expr] in arrays, f(...args) in calls, {...obj} in objects
|
|
646
1082
|
'...'(expr) {
|
|
@@ -655,11 +1091,23 @@ const handlers = {
|
|
|
655
1091
|
'class': () => err('class not supported: use object literals'),
|
|
656
1092
|
'yield': () => err('generators not supported: use loops'),
|
|
657
1093
|
'debugger': () => null,
|
|
658
|
-
|
|
1094
|
+
// Static-key delete (.x, ["x"], [literal]) would change the fixed schema → reject.
|
|
1095
|
+
// Computed-key delete (obj[expr]) — including jessie's `delete ctx[k]` — lowers
|
|
1096
|
+
// to runtime __dyn_del against the per-object shadow property store.
|
|
1097
|
+
'delete'(target) {
|
|
1098
|
+
const t = prep(target)
|
|
1099
|
+
if (Array.isArray(t) && t[0] === '[]' && t.length === 3) {
|
|
1100
|
+
const key = t[2]
|
|
1101
|
+
const isLiteralKey = Array.isArray(key) && key[0] == null && key.length === 2
|
|
1102
|
+
if (!isLiteralKey) return ['delete', t[1], key]
|
|
1103
|
+
}
|
|
1104
|
+
err('delete not supported: object shape is fixed')
|
|
1105
|
+
},
|
|
659
1106
|
'in'(key, obj) { return ['in', prep(key), prep(obj)] },
|
|
660
1107
|
'instanceof': () => err('instanceof not supported: use typeof'),
|
|
661
1108
|
'with': () => err('`with` not supported: deprecated'),
|
|
662
1109
|
':': () => err('labeled statements not supported'),
|
|
1110
|
+
'label'(name, body) { return ['label', name, prep(body)] },
|
|
663
1111
|
'var': () => err('`var` not supported: use let/const'),
|
|
664
1112
|
'function': () => err('`function` not supported: use arrow functions'),
|
|
665
1113
|
|
|
@@ -681,23 +1129,60 @@ const handlers = {
|
|
|
681
1129
|
expandDestruct(lhs, tmp, stmts, decls)
|
|
682
1130
|
return prep([';', ['let', ...decls], ...stmts])
|
|
683
1131
|
}
|
|
684
|
-
//
|
|
685
|
-
//
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
1132
|
+
// Function property assignment: fn.prop = arrow → extract as top-level function fn$prop.
|
|
1133
|
+
// A property can be reassigned — esbuild/jessie wrapper-composition does
|
|
1134
|
+
// `p.s = ...; var old = p.s; p.s = () => old()...`. Each assignment extracts
|
|
1135
|
+
// its own top-level function; the property holds whichever was assigned last,
|
|
1136
|
+
// and an earlier snapshot keeps pointing at the prior one. Collide → fresh name.
|
|
1137
|
+
// The base resolves through the scope chain first so an *imported* function
|
|
1138
|
+
// (mangled to `_mod$fn`) is recognised the same as a local one — the
|
|
1139
|
+
// subscript parser's plugin model mutates `parse.step` etc. across modules,
|
|
1140
|
+
// and a reassignment in module B must mark module A's call sites mutable.
|
|
1141
|
+
if (depth === 0 && Array.isArray(lhs) && lhs[0] === '.' && typeof lhs[1] === 'string'
|
|
1142
|
+
&& Array.isArray(rhs) && rhs[0] === '=>') {
|
|
1143
|
+
const fnBase = ctx.scope.chain[lhs[1]] || lhs[1]
|
|
1144
|
+
if (hasFunc(fnBase)) {
|
|
1145
|
+
let name = `${fnBase}$${lhs[2]}`
|
|
1146
|
+
// Reassignment → the property is mutable; record it so `fn.prop()` calls
|
|
1147
|
+
// emit a dynamic property read + indirect call instead of a direct call.
|
|
1148
|
+
if (ctx.func.names.has(name)) {
|
|
1149
|
+
ctx.func.multiProp.add(`${fnBase}.${lhs[2]}`)
|
|
1150
|
+
do name = `${fnBase}$${lhs[2]}$${ctx.func.uniq++}`; while (ctx.func.names.has(name))
|
|
1151
|
+
}
|
|
1152
|
+
// Build the target `.` node directly from the resolved base — re-`prep`ing
|
|
1153
|
+
// the lhs would resolve a multiProp `fn.prop` to an rvalue (closure
|
|
1154
|
+
// materialization block), which is not a valid assignment target.
|
|
1155
|
+
if (defFunc(name, prep(rhs))) return ['=', ['.', fnBase, lhs[2]], name]
|
|
692
1156
|
}
|
|
693
1157
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
1158
|
+
const staticStr = staticStringExpr(rhs)
|
|
1159
|
+
const staticArr = staticStringArrayValues(rhs)
|
|
1160
|
+
const plhs = prep(lhs)
|
|
1161
|
+
const prhs = prep(rhs)
|
|
1162
|
+
if (depth === 0 && typeof plhs === 'string' && ctx.scope.globals.has(plhs)) {
|
|
1163
|
+
// First assignment fixes the global's representation + object schema.
|
|
1164
|
+
if (!ctx.scope.globalReps?.has(plhs)) {
|
|
1165
|
+
recordGlobalRep(plhs, prhs)
|
|
1166
|
+
if (Array.isArray(prhs) && prhs[0] === '{}') {
|
|
1167
|
+
const props = staticObjectProps(prhs.slice(1))
|
|
1168
|
+
if (props) ctx.schema.vars.set(plhs, ctx.schema.register(props.names))
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
// Static string/array facts hold only while every assignment is constant.
|
|
1172
|
+
if (!assignedStaticGlobals.has(plhs) && (staticStr != null || staticArr)) bindStaticGlobal(plhs, staticStr, staticArr)
|
|
1173
|
+
else deleteStaticGlobal(plhs)
|
|
1174
|
+
assignedStaticGlobals.add(plhs)
|
|
1175
|
+
}
|
|
1176
|
+
// Local object-literal assignment to a not-yet-shaped variable — e.g. a `var`
|
|
1177
|
+
// that jzify hoisted into `let x; x = {…}`. Recording the schema here lets the
|
|
1178
|
+
// binding behave like `let x = {…}`: fixed-slot field access and for-in unroll.
|
|
1179
|
+
// First assignment fixes the shape (mirrors the global rule above).
|
|
1180
|
+
else if (typeof plhs === 'string' && Array.isArray(prhs) && prhs[0] === '{}'
|
|
1181
|
+
&& !ctx.schema.vars.has(plhs)) {
|
|
1182
|
+
const props = staticObjectProps(prhs.slice(1))
|
|
1183
|
+
if (props) ctx.schema.vars.set(plhs, ctx.schema.register(props.names))
|
|
699
1184
|
}
|
|
700
|
-
return ['=',
|
|
1185
|
+
return ['=', plhs, prhs]
|
|
701
1186
|
},
|
|
702
1187
|
|
|
703
1188
|
// try/catch/throw
|
|
@@ -730,16 +1215,14 @@ const handlers = {
|
|
|
730
1215
|
},
|
|
731
1216
|
|
|
732
1217
|
// Tagged template: tag`a${x}b` → tag(['a','b'], x)
|
|
733
|
-
// Parser drops empty string segments; reinsert them to satisfy the strings.length === exprs.length + 1 invariant.
|
|
734
1218
|
'``'(tag, ...parts) {
|
|
1219
|
+
const raw = staticStringExpr(['``', tag, ...parts])
|
|
1220
|
+
if (raw != null) return staticString(raw)
|
|
735
1221
|
const strs = [], exprs = []
|
|
736
|
-
let prev = false
|
|
737
1222
|
for (const p of parts) {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
else { if (!prev) strs.push([null, '']); exprs.push(p); prev = false }
|
|
1223
|
+
if (Array.isArray(p) && p[0] == null && typeof p[1] === 'string') strs.push(p)
|
|
1224
|
+
else exprs.push(p)
|
|
741
1225
|
}
|
|
742
|
-
if (!prev) strs.push([null, ''])
|
|
743
1226
|
const arr = strs.length === 1 ? ['[]', strs[0]] : ['[]', [',', ...strs]]
|
|
744
1227
|
const callArgs = exprs.length === 0 ? arr : [',', arr, ...exprs]
|
|
745
1228
|
return prep(['()', tag, callArgs])
|
|
@@ -747,6 +1230,9 @@ const handlers = {
|
|
|
747
1230
|
|
|
748
1231
|
// Import
|
|
749
1232
|
'import'(fromNode) {
|
|
1233
|
+
// Bare side-effect: `import './sub.js'` → AST is ['import', [null, 'path']]
|
|
1234
|
+
if (Array.isArray(fromNode) && fromNode[0] == null && typeof fromNode[1] === 'string')
|
|
1235
|
+
return handlers['from'](null, fromNode)
|
|
750
1236
|
if (!Array.isArray(fromNode) || fromNode[0] !== 'from')
|
|
751
1237
|
return err('Dynamic import() not supported')
|
|
752
1238
|
return handlers['from'](fromNode[1], fromNode[2])
|
|
@@ -863,9 +1349,13 @@ const handlers = {
|
|
|
863
1349
|
err(`Unknown module '${mod}'. Provide it via { modules: { '${mod}': source } } or { imports: { '${mod}': {...} } }`)
|
|
864
1350
|
},
|
|
865
1351
|
|
|
866
|
-
//
|
|
867
|
-
|
|
868
|
-
|
|
1352
|
+
// `===`/`!==` keep strict semantics (no coercion); emit folds a statically-known
|
|
1353
|
+
// type mismatch to false and otherwise shares the loose `==`/`!=` same-type path.
|
|
1354
|
+
// resolveTypeof still collapses `typeof x === 'type'` to a compile-time check.
|
|
1355
|
+
// Prep operands directly (not via `prep` on the node) so the strict op survives
|
|
1356
|
+
// to emit instead of re-dispatching this handler forever.
|
|
1357
|
+
'==='(a, b) { return prepStrictEq('===', a, b) },
|
|
1358
|
+
'!=='(a, b) { return prepStrictEq('!==', a, b) },
|
|
869
1359
|
|
|
870
1360
|
// Statements
|
|
871
1361
|
';': (...stmts) => [';', ...stmts.map(prep).filter(x => x != null)],
|
|
@@ -874,14 +1364,14 @@ const handlers = {
|
|
|
874
1364
|
|
|
875
1365
|
// Block-scoped control flow: push scope for bodies so inner let/const shadows correctly
|
|
876
1366
|
'if': (cond, then, els) => {
|
|
877
|
-
const c = prep(cond)
|
|
878
|
-
|
|
879
|
-
if (els != null) {
|
|
1367
|
+
const c = prep(stripBoolNot(cond))
|
|
1368
|
+
pushScope(); const t = prep(then); popScope()
|
|
1369
|
+
if (els != null) { pushScope(); const e = prep(els); popScope(); return ['if', c, t, e] }
|
|
880
1370
|
return ['if', c, t]
|
|
881
1371
|
},
|
|
882
1372
|
'while': (cond, body) => {
|
|
883
|
-
const c = prep(cond)
|
|
884
|
-
|
|
1373
|
+
const c = prep(stripBoolNot(cond))
|
|
1374
|
+
pushScope(); const b = prep(body); popScope()
|
|
885
1375
|
return ['while', c, b]
|
|
886
1376
|
},
|
|
887
1377
|
|
|
@@ -964,7 +1454,7 @@ const handlers = {
|
|
|
964
1454
|
for (const n of collectParamNames(raw)) fnScope.set(n, n)
|
|
965
1455
|
|
|
966
1456
|
depth++
|
|
967
|
-
|
|
1457
|
+
pushScope(fnScope)
|
|
968
1458
|
funcLocalNames.push(new Set(collectParamNames(raw)))
|
|
969
1459
|
|
|
970
1460
|
const nextParams = []
|
|
@@ -986,6 +1476,14 @@ const handlers = {
|
|
|
986
1476
|
}
|
|
987
1477
|
}
|
|
988
1478
|
let preparedBody = prep(body)
|
|
1479
|
+
// An expression-bodied arrow returning an empty object literal — `() => ({})`
|
|
1480
|
+
// — preps to a bare `['{}']`, structurally identical to an empty block body.
|
|
1481
|
+
// The grouping `()` that marked it an expression is unwrapped by then, so
|
|
1482
|
+
// wrap it in an explicit `return` — otherwise downstream block/expression
|
|
1483
|
+
// classification (compile.js `isBlockBody`) misreads it as an empty block.
|
|
1484
|
+
if (!(Array.isArray(body) && body[0] === '{}')
|
|
1485
|
+
&& Array.isArray(preparedBody) && preparedBody[0] === '{}' && preparedBody.length === 1)
|
|
1486
|
+
preparedBody = ['{}', [';', ['return', ['{}']]]]
|
|
989
1487
|
if (bodyPrefix.length) {
|
|
990
1488
|
const prefix = bodyPrefix.filter(x => x != null)
|
|
991
1489
|
if (Array.isArray(preparedBody) && preparedBody[0] === '{}' && Array.isArray(preparedBody[1]) && preparedBody[1][0] === ';')
|
|
@@ -997,7 +1495,7 @@ const handlers = {
|
|
|
997
1495
|
}
|
|
998
1496
|
const inner = nextParams.length === 0 ? null : nextParams.length === 1 ? nextParams[0] : [',', ...nextParams]
|
|
999
1497
|
const result = ['=>', Array.isArray(params) && params[0] === '()' ? ['()', inner] : inner, preparedBody]
|
|
1000
|
-
|
|
1498
|
+
popScope()
|
|
1001
1499
|
funcLocalNames.pop()
|
|
1002
1500
|
depth--
|
|
1003
1501
|
return result
|
|
@@ -1029,6 +1527,8 @@ const handlers = {
|
|
|
1029
1527
|
return ['?.()', prep(callee), ...items.map(prep)]
|
|
1030
1528
|
},
|
|
1031
1529
|
// Boolean literals NaN-box as f64 — typeof at runtime returns 'number'. Fold here so the JS-spec value survives.
|
|
1530
|
+
// Unresolvable bare refs fold to 'undefined' via staticTypeofString (spec §13.5.3) —
|
|
1531
|
+
// the only place a stray identifier doesn't ReferenceError.
|
|
1032
1532
|
'typeof'(a) {
|
|
1033
1533
|
if (Array.isArray(a) && a[0] == null && typeof a[1] === 'boolean') { includeForStringOnly(); return ['str', 'boolean'] }
|
|
1034
1534
|
const known = staticTypeofString(a)
|
|
@@ -1044,7 +1544,17 @@ const handlers = {
|
|
|
1044
1544
|
includeForNumericCoercion()
|
|
1045
1545
|
return ['u+', na]
|
|
1046
1546
|
}
|
|
1047
|
-
|
|
1547
|
+
const pa = prep(a), pb = prep(b)
|
|
1548
|
+
// Compile-time fold of literal string concat. The combined bytes flow
|
|
1549
|
+
// through the `str` emitter as a single literal — SSO if ≤4 ASCII (zero
|
|
1550
|
+
// heap), otherwise one dataDedup entry (still cheaper than runtime
|
|
1551
|
+
// __str_concat_raw + heap alloc). Bottom-up, so `'a' + 'b' + 'c'` folds
|
|
1552
|
+
// left-associatively into one literal.
|
|
1553
|
+
if (Array.isArray(pa) && pa[0] === 'str' && typeof pa[1] === 'string' &&
|
|
1554
|
+
Array.isArray(pb) && pb[0] === 'str' && typeof pb[1] === 'string') {
|
|
1555
|
+
return ['str', pa[1] + pb[1]]
|
|
1556
|
+
}
|
|
1557
|
+
return ['+', pa, pb]
|
|
1048
1558
|
},
|
|
1049
1559
|
'-'(a, b) {
|
|
1050
1560
|
if (b === undefined) { const na = prep(a); return isLit(na) && typeof na[1] === 'number' ? [, -na[1]] : ['u-', na] }
|
|
@@ -1052,7 +1562,7 @@ const handlers = {
|
|
|
1052
1562
|
},
|
|
1053
1563
|
|
|
1054
1564
|
// Ternary: parser emits '?' not '?:'
|
|
1055
|
-
'?'(cond, then, els) { return ['?:', prep(cond), prep(then), prep(els)] },
|
|
1565
|
+
'?'(cond, then, els) { return ['?:', prep(stripBoolNot(cond)), prep(then), prep(els)] },
|
|
1056
1566
|
|
|
1057
1567
|
// ++/-- prefix vs postfix: parser sends trailing null for postfix
|
|
1058
1568
|
// Postfix i++ = (++i) - 1: increment happens, arithmetic recovers old value
|
|
@@ -1079,73 +1589,18 @@ const handlers = {
|
|
|
1079
1589
|
'()'(callee, ...args) {
|
|
1080
1590
|
// Grouping: (expr) → ['()', expr] with no args. Call: f() → ['()', 'f', null] with null arg.
|
|
1081
1591
|
if (args.length === 0) return prep(callee)
|
|
1592
|
+
if (typeof callee === 'string' && PROHIBITED[callee]) err(PROHIBITED[callee])
|
|
1082
1593
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
const hasRealArgs = args.some(a => a != null)
|
|
1594
|
+
// Compile-time folds: the callee names something resolvable now. Each fold
|
|
1595
|
+
// is gated by callee shape, so at most one of the three fires.
|
|
1596
|
+
const folded = foldImportMetaResolve(callee, args)
|
|
1597
|
+
?? dispatchConstructorCall(callee, args)
|
|
1598
|
+
?? foldNamespaceIntrospection(callee, args)
|
|
1599
|
+
if (folded !== undefined) return folded
|
|
1092
1600
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
if (CTORS.includes(callee)) return handlers['new'](['()', callee, ...args])
|
|
1601
|
+
callee = resolveCallee(callee, args)
|
|
1602
|
+
args = renestSoleCommaArg(args)
|
|
1096
1603
|
|
|
1097
|
-
if (includeForNamedCall(callee)) {
|
|
1098
|
-
if (callee === 'BigInt64Array' || callee === 'BigUint64Array') {
|
|
1099
|
-
return ['()', callee, ...args.filter(a => a != null).map(prep)]
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
const local = scopes.length && isDeclared(callee)
|
|
1104
|
-
const resolved = local ? null : ctx.scope.chain[callee]
|
|
1105
|
-
if (local) callee = resolveScope(callee)
|
|
1106
|
-
else if (resolved?.includes('.')) callee = resolved
|
|
1107
|
-
else if (resolved && hasFunc(resolved)) callee = resolved
|
|
1108
|
-
else if (resolved && !resolved.includes('.')) {
|
|
1109
|
-
if (hasModule(resolved) && !ctx.module.imports.some(i => i[3]?.[1] === `$${resolved}`)) includeModule(resolved)
|
|
1110
|
-
}
|
|
1111
|
-
else if (depth > 0 && !resolved && !ctx.func.exports[callee] && !ctx.module.imports.some(i => i[3]?.[1] === `$${callee}`)) {
|
|
1112
|
-
includeForCallableValue()
|
|
1113
|
-
}
|
|
1114
|
-
} else if (Array.isArray(callee) && callee[0] === '.') {
|
|
1115
|
-
const [, obj, prop] = callee
|
|
1116
|
-
const key = typeof obj === 'string' && typeof prop === 'string' ? `${obj}.${prop}` : null
|
|
1117
|
-
if (key && ctx.module.hostImports?.[obj]?.[prop]) {
|
|
1118
|
-
const spec = ctx.module.hostImports[obj][prop]
|
|
1119
|
-
const alias = `${obj}$${prop}`
|
|
1120
|
-
addHostImport(obj, prop, alias, spec)
|
|
1121
|
-
callee = alias
|
|
1122
|
-
} else if (key && includeForNamedCall(key)) {
|
|
1123
|
-
callee = key
|
|
1124
|
-
} else if (includeForGenericMethod(prop)) {
|
|
1125
|
-
callee = prep(callee)
|
|
1126
|
-
} else {
|
|
1127
|
-
const mod = ctx.scope.chain[obj]
|
|
1128
|
-
if (typeof obj === 'string' && mod && !mod.includes('.') && hasModule(mod)) {
|
|
1129
|
-
callee = (includeModule(mod), mod + '.' + prop)
|
|
1130
|
-
} else {
|
|
1131
|
-
callee = prep(callee)
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
} else {
|
|
1135
|
-
includeForCallableValue()
|
|
1136
|
-
callee = prep(callee)
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
// Drop trailing-comma sentinel inside a comma group: `f(a, b,)` parses as
|
|
1140
|
-
// ['()', 'f', [',', a, b, null]] — without trimming, the trailing null
|
|
1141
|
-
// becomes a [, 0] literal and inflates arguments.length.
|
|
1142
|
-
if (args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',') {
|
|
1143
|
-
let end = args[0].length
|
|
1144
|
-
while (end > 1 && args[0][end - 1] == null) end--
|
|
1145
|
-
if (end < args[0].length) {
|
|
1146
|
-
args[0] = end === 2 ? args[0][1] : args[0].slice(0, end)
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
1604
|
const preppedArgs = args.filter(a => a != null).map(prep)
|
|
1150
1605
|
for (const a of preppedArgs) {
|
|
1151
1606
|
if (typeof a === 'string' && hasFunc(a)) {
|
|
@@ -1165,7 +1620,9 @@ const handlers = {
|
|
|
1165
1620
|
const inner = args[0]
|
|
1166
1621
|
includeForArrayLiteral()
|
|
1167
1622
|
if (inner == null) return ['[']
|
|
1168
|
-
|
|
1623
|
+
// jessie consumes the trailing comma itself; every remaining `null` in the
|
|
1624
|
+
// element list is a genuine elision (`[,]` → length 1, `[1,,]` → length 2).
|
|
1625
|
+
if (Array.isArray(inner) && inner[0] === ',') { const items = inner.slice(1); return ['[', ...items.map(item => item == null ? [, undefined] : prep(item))] }
|
|
1169
1626
|
return ['[', prep(inner)]
|
|
1170
1627
|
}
|
|
1171
1628
|
if (typeof args[0] === 'string' && ctx.module.namespaces?.[args[0]]) {
|
|
@@ -1185,9 +1642,9 @@ const handlers = {
|
|
|
1185
1642
|
|
|
1186
1643
|
// Bare block statement: push scope for let/const shadowing
|
|
1187
1644
|
'{'(inner) {
|
|
1188
|
-
|
|
1645
|
+
pushScope()
|
|
1189
1646
|
const result = ['{', prep(inner)]
|
|
1190
|
-
|
|
1647
|
+
popScope()
|
|
1191
1648
|
return result
|
|
1192
1649
|
},
|
|
1193
1650
|
|
|
@@ -1196,14 +1653,44 @@ const handlers = {
|
|
|
1196
1653
|
// Detect block body vs object literal
|
|
1197
1654
|
if (Array.isArray(inner) && STMT_OPS.has(inner[0])) {
|
|
1198
1655
|
// Block body: push block scope for let/const shadowing
|
|
1199
|
-
|
|
1656
|
+
pushScope()
|
|
1200
1657
|
const result = ['{}', prep(inner)]
|
|
1201
|
-
|
|
1658
|
+
popScope()
|
|
1202
1659
|
return result
|
|
1203
1660
|
}
|
|
1204
1661
|
|
|
1205
1662
|
includeForObjectLiteral()
|
|
1206
1663
|
if (inner == null) return ['{}']
|
|
1664
|
+
const items = Array.isArray(inner) && inner[0] === ','
|
|
1665
|
+
? inner.slice(1)
|
|
1666
|
+
: [inner]
|
|
1667
|
+
|
|
1668
|
+
// Computed keys: `{[k]: v}` where `k` isn't compile-time foldable. jz's
|
|
1669
|
+
// object layout is slot-based (fixed schema at the literal site), so a
|
|
1670
|
+
// truly-dynamic key can't slot in. Lower to the existing dict path:
|
|
1671
|
+
// {a:1, [k]:v, b:2} → ((__t) => (__t[k]=v, __t))({a:1, b:2})
|
|
1672
|
+
// Static-but-non-string keys still fold via `staticPropertyKey` below.
|
|
1673
|
+
const isComputed = p => Array.isArray(p) && p[0] === ':'
|
|
1674
|
+
&& typeof p[1] !== 'string' && staticPropertyKey(p[1]) == null
|
|
1675
|
+
if (items.some(isComputed)) {
|
|
1676
|
+
const staticItems = items.filter(p => !isComputed(p))
|
|
1677
|
+
const computedItems = items.filter(isComputed)
|
|
1678
|
+
const tmp = `${T}o${ctx.func.uniq++}`
|
|
1679
|
+
// Body: comma sequence of dict-sets, terminated with the tmp itself.
|
|
1680
|
+
// Computed key shape from parser is `[':', ['[]', keyExpr], valExpr]` —
|
|
1681
|
+
// unwrap the `['[]', keyExpr]` to grab keyExpr directly.
|
|
1682
|
+
const assigns = computedItems.map(p => {
|
|
1683
|
+
const keyExpr = Array.isArray(p[1]) && p[1][0] === '[]' ? p[1][1] : p[1]
|
|
1684
|
+
return ['=', ['[]', tmp, keyExpr], p[2]]
|
|
1685
|
+
})
|
|
1686
|
+
const body = [',', ...assigns, tmp]
|
|
1687
|
+
const arrow = ['=>', ['()', tmp], body]
|
|
1688
|
+
const arg = staticItems.length === 1 ? ['{}', staticItems[0]]
|
|
1689
|
+
: staticItems.length ? ['{}', [',', ...staticItems]]
|
|
1690
|
+
: ['{}']
|
|
1691
|
+
return prep(['()', arrow, arg])
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1207
1694
|
// Process properties: shorthand 'x' → [':', 'x', 'x'], or [':', key, val] → prep val only
|
|
1208
1695
|
const prop = p => {
|
|
1209
1696
|
if (typeof p === 'string') return [':', p, prep(p)]
|
|
@@ -1214,13 +1701,6 @@ const handlers = {
|
|
|
1214
1701
|
}
|
|
1215
1702
|
return prep(p)
|
|
1216
1703
|
}
|
|
1217
|
-
// Drop trailing-comma artifacts: subscript represents `{a:1, b,}` as
|
|
1218
|
-
// `[",", [":","a",1], "b", null]` — the trailing `null` would prep to a
|
|
1219
|
-
// literal-0 entry, leaving the literal carrying a phantom slot and
|
|
1220
|
-
// shifting any subsequent slot-position resolution.
|
|
1221
|
-
const items = Array.isArray(inner) && inner[0] === ','
|
|
1222
|
-
? inner.slice(1).filter(p => p != null)
|
|
1223
|
-
: [inner]
|
|
1224
1704
|
let prepped = items.map(prop)
|
|
1225
1705
|
// ES spec: duplicate keys allowed; key takes first-seen position, last-seen value.
|
|
1226
1706
|
const lastValue = new Map()
|
|
@@ -1244,10 +1724,11 @@ const handlers = {
|
|
|
1244
1724
|
|
|
1245
1725
|
// For loop
|
|
1246
1726
|
'for'(head, body) {
|
|
1247
|
-
|
|
1727
|
+
pushScope()
|
|
1248
1728
|
let r
|
|
1249
1729
|
if (Array.isArray(head) && head[0] === ';') {
|
|
1250
1730
|
let [, init, cond, step] = head
|
|
1731
|
+
cond = stripBoolNot(cond)
|
|
1251
1732
|
// Hoist .length / .size / .byteLength from for-condition:
|
|
1252
1733
|
// `i < arr.length` → `let __len = arr.length | 0; ... i < __len`
|
|
1253
1734
|
// The `| 0` forces i32 even for unknown-typed receivers (where __length
|
|
@@ -1297,30 +1778,54 @@ const handlers = {
|
|
|
1297
1778
|
if (sid != null) {
|
|
1298
1779
|
// Known schema → compile-time unrolling with string keys
|
|
1299
1780
|
const keys = ctx.schema.list[sid]
|
|
1300
|
-
if (!keys || !keys.length) {
|
|
1781
|
+
if (!keys || !keys.length) { popScope(); return null }
|
|
1301
1782
|
includeForKnownKeyIteration()
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
stmts
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1783
|
+
if (!hasLoopJump(body)) {
|
|
1784
|
+
// No break/continue → flat unroll, no loop frame needed.
|
|
1785
|
+
const stmts = []
|
|
1786
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1787
|
+
stmts.push(i === 0
|
|
1788
|
+
? ['let', ['=', varName, [, keys[i]]]]
|
|
1789
|
+
: ['=', varName, [, keys[i]]])
|
|
1790
|
+
stmts.push(cloneNode(body))
|
|
1791
|
+
}
|
|
1792
|
+
r = prep([';', ...stmts])
|
|
1793
|
+
} else {
|
|
1794
|
+
// break/continue present → an unrolled loop still needs its frames.
|
|
1795
|
+
// Wrap each iteration in a labeled block (continue target) and the
|
|
1796
|
+
// whole run in an outer labeled block (break target): `break` exits
|
|
1797
|
+
// the construct, `continue` falls through to the next iteration.
|
|
1798
|
+
const brkL = `${T}fibrk${ctx.func.uniq++}`
|
|
1799
|
+
const decl = prep(['let', ['=', varName, [, keys[0]]]])
|
|
1800
|
+
const parts = [decl]
|
|
1801
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1802
|
+
const contL = `${T}ficont${ctx.func.uniq++}`
|
|
1803
|
+
const iter = prep(i === 0
|
|
1804
|
+
? cloneNode(body)
|
|
1805
|
+
: [';', ['=', varName, [, keys[i]]], cloneNode(body)])
|
|
1806
|
+
parts.push(['label', contL, retargetLoopJumps(iter, brkL, contL)])
|
|
1807
|
+
}
|
|
1808
|
+
r = ['label', brkL, [';', ...parts]]
|
|
1308
1809
|
}
|
|
1309
|
-
r = prep([';', ...stmts])
|
|
1310
1810
|
} else {
|
|
1311
1811
|
// Dynamic object → HASH runtime iteration
|
|
1312
1812
|
includeForRuntimeKeyIteration()
|
|
1313
1813
|
r = ['for-in', varName, prep(src), prep(body)]
|
|
1314
1814
|
}
|
|
1315
1815
|
} else {
|
|
1316
|
-
|
|
1816
|
+
// Some parser/jzify shapes for `for (;;)` and `for (; cond; )` arrive
|
|
1817
|
+
// as a null or bare-condition head instead of the canonical
|
|
1818
|
+
// `[';', init, cond, step]` tuple. Normalize them before emit so they
|
|
1819
|
+
// remain ordinary for-loops, not malformed two-slot nodes.
|
|
1820
|
+
r = ['for', null, head == null ? null : prep(head), null, prep(body)]
|
|
1317
1821
|
}
|
|
1318
|
-
|
|
1822
|
+
popScope()
|
|
1319
1823
|
return r
|
|
1320
1824
|
},
|
|
1321
1825
|
|
|
1322
1826
|
// Property access - resolve namespaces or object/array properties
|
|
1323
1827
|
'.'(obj, prop) {
|
|
1828
|
+
prop = typeof prop === 'string' ? prop : staticPropertyKey(prop)
|
|
1324
1829
|
if (prop === 'caller' || prop === 'callee') err('`.caller` and `.callee` are prohibited: deprecated stack introspection')
|
|
1325
1830
|
if (prop === 'url' && isImportMeta(obj)) return staticString(importMetaUrl())
|
|
1326
1831
|
const mod = ctx.scope.chain[obj]
|
|
@@ -1358,6 +1863,20 @@ const handlers = {
|
|
|
1358
1863
|
}
|
|
1359
1864
|
}
|
|
1360
1865
|
|
|
1866
|
+
// `new RegExp("pattern", "flags?")` with string-literal pattern → compile
|
|
1867
|
+
// like a regex literal `/pattern/flags`. Dynamic pattern is not supported
|
|
1868
|
+
// (would require a runtime regex interpreter). Reported as build blocker #6.
|
|
1869
|
+
if (name === 'RegExp') {
|
|
1870
|
+
const literalArgs = ctorArgs.filter(a => a != null)
|
|
1871
|
+
const pattern = staticStringExpr(literalArgs[0])
|
|
1872
|
+
if (pattern == null)
|
|
1873
|
+
err('new RegExp() requires a string-literal pattern; dynamic regex construction is not supported')
|
|
1874
|
+
const flags = literalArgs.length > 1 ? staticStringExpr(literalArgs[1]) : ''
|
|
1875
|
+
if (flags == null)
|
|
1876
|
+
err('new RegExp() flags must be a string literal')
|
|
1877
|
+
return prep(['//', pattern, flags || undefined])
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1361
1880
|
// Wrap multi-arg ctor arg lists back into a single comma-group — the '()' op
|
|
1362
1881
|
// expects callArgs as a single element (possibly comma-grouped).
|
|
1363
1882
|
const wrapArgs = (args) => args.length === 0 ? [null]
|
|
@@ -1525,7 +2044,7 @@ function prepareModule(specifier, source) {
|
|
|
1525
2044
|
const mangled = `${prefix}$${localName}`
|
|
1526
2045
|
moduleExports.set(exportName, mangled)
|
|
1527
2046
|
const func = ctx.func.list.find(f => f.name === localName)
|
|
1528
|
-
if (func) renameFunc(func, mangled)
|
|
2047
|
+
if (func) { renameFunc(func, mangled); func._modulePrefix = prefix }
|
|
1529
2048
|
if (ctx.scope.globals.has(localName)) {
|
|
1530
2049
|
const wat = ctx.scope.globals.get(localName).replace(`$${localName}`, `$${mangled}`)
|
|
1531
2050
|
ctx.scope.globals.delete(localName)
|
|
@@ -1587,15 +2106,17 @@ function prepareModule(specifier, source) {
|
|
|
1587
2106
|
}
|
|
1588
2107
|
|
|
1589
2108
|
// Rename ALL non-exported functions created during this module's prep
|
|
1590
|
-
// (fn property assignments like f32.parse, internal helpers like cleanInt)
|
|
2109
|
+
// (fn property assignments like f32.parse, internal helpers like cleanInt).
|
|
2110
|
+
// Funcs added by nested prepareModule calls are tagged with `_modulePrefix`
|
|
2111
|
+
// by their own pass; skip those so prefixes don't stack (`a$b$name`).
|
|
1591
2112
|
for (let i = savedFuncCount; i < ctx.func.list.length; i++) {
|
|
1592
2113
|
const func = ctx.func.list[i]
|
|
1593
2114
|
if (func.raw || func.name.startsWith(prefix + '$')) continue
|
|
1594
|
-
|
|
1595
|
-
if (func.name.includes('__') && func.name.includes('$')) continue
|
|
2115
|
+
if (func._modulePrefix && func._modulePrefix !== prefix) continue
|
|
1596
2116
|
const mangled = `${prefix}$${func.name}`
|
|
1597
2117
|
moduleExports.set(func.name, mangled)
|
|
1598
2118
|
renameFunc(func, mangled)
|
|
2119
|
+
func._modulePrefix = prefix
|
|
1599
2120
|
}
|
|
1600
2121
|
|
|
1601
2122
|
// Add mangled non-exported globals to moduleExports for walk renaming
|
|
@@ -1627,6 +2148,8 @@ function prepareModule(specifier, source) {
|
|
|
1627
2148
|
for (let i = savedFuncCount; i < ctx.func.list.length; i++) {
|
|
1628
2149
|
const func = ctx.func.list[i]
|
|
1629
2150
|
if (!func.body) continue
|
|
2151
|
+
// Sub-module funcs already had their own walk; parent's rename map doesn't apply.
|
|
2152
|
+
if (func._modulePrefix && func._modulePrefix !== prefix) continue
|
|
1630
2153
|
const funcParams = new Set(func.sig?.params?.map(p => p.name) || [])
|
|
1631
2154
|
walk(func.body, funcParams)
|
|
1632
2155
|
if (func.defaults) for (const [k, v] of Object.entries(func.defaults)) func.defaults[k] = walk(v, funcParams)
|
|
@@ -1652,3 +2175,160 @@ function prepareModule(specifier, source) {
|
|
|
1652
2175
|
ctx.module.resolvedModules.set(specifier, result)
|
|
1653
2176
|
return result
|
|
1654
2177
|
}
|
|
2178
|
+
|
|
2179
|
+
// =============================================================================
|
|
2180
|
+
// AST-level fusion passes (pre-resolution)
|
|
2181
|
+
// =============================================================================
|
|
2182
|
+
// Unlike src/optimize.js (a pure WAT IR→IR rewrite, post-emission), these
|
|
2183
|
+
// rewrites need the *raw, pre-resolution* AST shape — bindings still named,
|
|
2184
|
+
// arrow bodies still inline — so they run inside prepare(), before scope
|
|
2185
|
+
// resolution and emit. They mutate the AST in place; shape guards are strict
|
|
2186
|
+
// enough that misfires are impossible.
|
|
2187
|
+
|
|
2188
|
+
/** Sparse-read .map fusion: rewrite `const b = a.map(arrow); for(...; j<b.length; ...) USE(b[j])`
|
|
2189
|
+
* into a fused for-loop that inlines `arrow(a[j])` at the read site, eliminating the materialized
|
|
2190
|
+
* intermediate array. Only fires on shapes where every use of `b` is a numeric `b[idx]` read or a
|
|
2191
|
+
* `b.length` read, the arrow is pure with a single named param, and `b` is not referenced after the
|
|
2192
|
+
* consumer for-loop. Preserves observable behavior because the arrow's pure-expression body has no
|
|
2193
|
+
* order-dependent effects. */
|
|
2194
|
+
function fuseSparseMapReads(root) {
|
|
2195
|
+
walkSparse(root)
|
|
2196
|
+
}
|
|
2197
|
+
function walkSparse(node) {
|
|
2198
|
+
if (!Array.isArray(node)) return
|
|
2199
|
+
for (let i = 1; i < node.length; i++) walkSparse(node[i])
|
|
2200
|
+
if (node[0] === ';') tryFuseInBlock(node)
|
|
2201
|
+
}
|
|
2202
|
+
function tryFuseInBlock(seq) {
|
|
2203
|
+
for (let i = 1; i < seq.length - 1; i++) {
|
|
2204
|
+
const fused = tryFusePair(seq[i], seq[i + 1], seq, i)
|
|
2205
|
+
if (fused) {
|
|
2206
|
+
seq.splice(i, 2, ...fused)
|
|
2207
|
+
i-- // re-examine same position (chained fusions)
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
function tryFusePair(decl, forNode, seq, declIdx) {
|
|
2212
|
+
if (!Array.isArray(decl) || (decl[0] !== 'const' && decl[0] !== 'let')) return null
|
|
2213
|
+
if (decl.length !== 2) return null // single binding only
|
|
2214
|
+
const bind = decl[1]
|
|
2215
|
+
if (!Array.isArray(bind) || bind[0] !== '=' || typeof bind[1] !== 'string') return null
|
|
2216
|
+
const NAME = bind[1], rhs = bind[2]
|
|
2217
|
+
if (!Array.isArray(rhs) || rhs[0] !== '()') return null
|
|
2218
|
+
const callee = rhs[1]
|
|
2219
|
+
if (!Array.isArray(callee) || callee[0] !== '.' || callee[2] !== 'map') return null
|
|
2220
|
+
const RECV = callee[1]
|
|
2221
|
+
if (typeof RECV !== 'string' || RECV === NAME) return null
|
|
2222
|
+
const arrow = rhs[2]
|
|
2223
|
+
if (!Array.isArray(arrow) || arrow[0] !== '=>') return null
|
|
2224
|
+
// Single-name param only: `x => …` or `(x) => …`
|
|
2225
|
+
const ap = arrow[1]
|
|
2226
|
+
const PARAM = typeof ap === 'string' ? ap :
|
|
2227
|
+
(Array.isArray(ap) && ap[0] === '()' && typeof ap[1] === 'string' ? ap[1] : null)
|
|
2228
|
+
if (!PARAM || PARAM === NAME || PARAM === RECV) return null
|
|
2229
|
+
// Body: single-expression arrow only (block bodies skipped — could extend later).
|
|
2230
|
+
const aBody = arrow[2]
|
|
2231
|
+
if (Array.isArray(aBody) && aBody[0] === '{}') return null
|
|
2232
|
+
if (!isPureSparseArrowBody(aBody, PARAM)) return null
|
|
2233
|
+
// For-loop: ['for', [';', initStmt, cond, inc], body]
|
|
2234
|
+
if (!Array.isArray(forNode) || forNode[0] !== 'for' || forNode.length !== 3) return null
|
|
2235
|
+
const head = forNode[1]
|
|
2236
|
+
if (!Array.isArray(head) || head[0] !== ';' || head.length !== 4) return null
|
|
2237
|
+
const cond = head[2], forBody = forNode[2]
|
|
2238
|
+
// Verify `NAME` is used only as `NAME[idx]` or `NAME.length` inside cond+forBody.
|
|
2239
|
+
if (!hasOnlySparseUses(cond, NAME)) return null
|
|
2240
|
+
if (!hasOnlySparseUses(forBody, NAME)) return null
|
|
2241
|
+
if (!hasAnyIndexedRead(forBody, NAME) && !hasAnyIndexedRead(cond, NAME)) return null
|
|
2242
|
+
// `NAME` must not be read after the for-loop in the same block.
|
|
2243
|
+
for (let k = declIdx + 2; k < seq.length; k++) {
|
|
2244
|
+
if (refsName(seq[k], NAME)) return null
|
|
2245
|
+
}
|
|
2246
|
+
// RECV must not be reassigned inside the for-loop (would invalidate substitution).
|
|
2247
|
+
if (assignsName(forNode, RECV) || assignsName(forNode, NAME)) return null
|
|
2248
|
+
// PARAM must not collide with any binding inside forBody (otherwise substitution shadows wrongly).
|
|
2249
|
+
if (bindsName(forNode, PARAM)) return null
|
|
2250
|
+
// Apply substitution: NAME.length → RECV.length; NAME[idx] → arrowBody[PARAM ← RECV[idx]].
|
|
2251
|
+
const newCond = substSparse(cond, NAME, RECV, PARAM, aBody)
|
|
2252
|
+
const newBody = substSparse(forBody, NAME, RECV, PARAM, aBody)
|
|
2253
|
+
const newHead = [';', head[1], newCond, head[3]]
|
|
2254
|
+
return [['for', newHead, newBody]]
|
|
2255
|
+
}
|
|
2256
|
+
function isPureSparseArrowBody(n, PARAM) {
|
|
2257
|
+
if (typeof n === 'string') return true
|
|
2258
|
+
if (!Array.isArray(n)) return true
|
|
2259
|
+
const op = n[0]
|
|
2260
|
+
// Calls / new / assignments / increments are unsafe for repeated-substitution semantics.
|
|
2261
|
+
if (op === '()' || op === '?.()' || op === 'new' || op === '++' || op === '--') return false
|
|
2262
|
+
if (op === '=>') return false // nested closure is opaque
|
|
2263
|
+
if (typeof op === 'string' && op !== '=>' && op !== '===' && op !== '!==' && op !== '==' && op !== '!=' && op !== '<=' && op !== '>=' && op.endsWith('=') && op !== '=') return false
|
|
2264
|
+
if (op === '=') return false
|
|
2265
|
+
for (let i = 1; i < n.length; i++) if (!isPureSparseArrowBody(n[i], PARAM)) return false
|
|
2266
|
+
return true
|
|
2267
|
+
}
|
|
2268
|
+
function hasOnlySparseUses(n, NAME) {
|
|
2269
|
+
if (typeof n === 'string') return n !== NAME
|
|
2270
|
+
if (!Array.isArray(n)) return true
|
|
2271
|
+
const op = n[0]
|
|
2272
|
+
if (op === '[]' && n.length === 3 && n[1] === NAME) return hasOnlySparseUses(n[2], NAME) // NAME[idx] — idx must not reference NAME
|
|
2273
|
+
if (op === '.' && n[1] === NAME) {
|
|
2274
|
+
if (n[2] === 'length') return true
|
|
2275
|
+
return false // any other property access on NAME is opaque
|
|
2276
|
+
}
|
|
2277
|
+
for (let i = 1; i < n.length; i++) if (!hasOnlySparseUses(n[i], NAME)) return false
|
|
2278
|
+
return true
|
|
2279
|
+
}
|
|
2280
|
+
function hasAnyIndexedRead(n, NAME) {
|
|
2281
|
+
if (!Array.isArray(n)) return false
|
|
2282
|
+
if (n[0] === '[]' && n.length === 3 && n[1] === NAME) return true
|
|
2283
|
+
for (let i = 1; i < n.length; i++) if (hasAnyIndexedRead(n[i], NAME)) return true
|
|
2284
|
+
return false
|
|
2285
|
+
}
|
|
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
|
+
function assignsName(n, NAME) {
|
|
2293
|
+
if (!Array.isArray(n)) return false
|
|
2294
|
+
const op = n[0]
|
|
2295
|
+
if ((op === '=' || op === '++' || op === '--' ||
|
|
2296
|
+
(typeof op === 'string' && op.endsWith('=') && op !== '==' && op !== '===' && op !== '!=' && op !== '!==' && op !== '<=' && op !== '>='))
|
|
2297
|
+
&& n[1] === NAME) return true
|
|
2298
|
+
for (let i = 1; i < n.length; i++) if (assignsName(n[i], NAME)) return true
|
|
2299
|
+
return false
|
|
2300
|
+
}
|
|
2301
|
+
function bindsName(n, NAME) {
|
|
2302
|
+
if (!Array.isArray(n)) return false
|
|
2303
|
+
const op = n[0]
|
|
2304
|
+
if ((op === 'let' || op === 'const')) {
|
|
2305
|
+
for (let i = 1; i < n.length; i++) {
|
|
2306
|
+
const bind = n[i]
|
|
2307
|
+
if (Array.isArray(bind) && bind[0] === '=' && bind[1] === NAME) return true
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
if (op === '=>') {
|
|
2311
|
+
const p = n[1]
|
|
2312
|
+
if (p === NAME) return true
|
|
2313
|
+
if (Array.isArray(p)) {
|
|
2314
|
+
if (p[0] === '()' && p[1] === NAME) return true
|
|
2315
|
+
// skip deeper destructuring forms — conservative
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
for (let i = 1; i < n.length; i++) if (bindsName(n[i], NAME)) return true
|
|
2319
|
+
return false
|
|
2320
|
+
}
|
|
2321
|
+
function substSparse(n, NAME, RECV, PARAM, arrowBody) {
|
|
2322
|
+
if (typeof n !== 'object' || n === null || !Array.isArray(n)) return n
|
|
2323
|
+
if (n[0] === '.' && n[1] === NAME && n[2] === 'length') return ['.', RECV, 'length']
|
|
2324
|
+
if (n[0] === '[]' && n.length === 3 && n[1] === NAME) {
|
|
2325
|
+
const idx = substSparse(n[2], NAME, RECV, PARAM, arrowBody)
|
|
2326
|
+
return cloneAndBind(arrowBody, PARAM, ['[]', RECV, idx])
|
|
2327
|
+
}
|
|
2328
|
+
return n.map((c, i) => i === 0 ? c : substSparse(c, NAME, RECV, PARAM, arrowBody))
|
|
2329
|
+
}
|
|
2330
|
+
function cloneAndBind(node, PARAM, replacement) {
|
|
2331
|
+
if (node === PARAM) return replacement
|
|
2332
|
+
if (!Array.isArray(node)) return node
|
|
2333
|
+
return node.map((c, i) => i === 0 ? c : cloneAndBind(c, PARAM, replacement))
|
|
2334
|
+
}
|