jz 0.3.1 → 0.5.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 +277 -258
- package/cli.js +10 -52
- package/index.js +181 -29
- package/{src/host.js → interop.js} +291 -88
- package/module/array.js +195 -76
- package/module/collection.js +249 -20
- package/module/console.js +1 -1
- package/module/core.js +267 -119
- package/module/date.js +9 -5
- package/module/function.js +11 -1
- package/module/json.js +367 -61
- package/module/math.js +568 -128
- package/module/number.js +390 -227
- package/module/object.js +352 -39
- package/module/regex.js +123 -16
- package/module/schema.js +15 -1
- package/module/string.js +555 -96
- package/module/timer.js +1 -1
- package/module/typedarray.js +674 -57
- package/package.json +12 -6
- 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 +1872 -487
- package/src/assemble.js +58 -20
- package/src/autoload.js +13 -1
- package/src/codegen.js +177 -0
- package/src/compile.js +462 -186
- package/src/ctx.js +85 -18
- package/src/emit.js +668 -197
- package/src/infer.js +548 -0
- package/src/ir.js +302 -27
- package/src/jzify.js +898 -259
- package/src/narrow.js +455 -246
- package/src/optimize.js +263 -99
- package/src/plan.js +713 -35
- package/src/prepare.js +818 -293
- 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/prepare.js
CHANGED
|
@@ -24,7 +24,8 @@
|
|
|
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
30
|
import {
|
|
30
31
|
CTORS, TIMER_NAMES,
|
|
@@ -37,6 +38,8 @@ import {
|
|
|
37
38
|
|
|
38
39
|
let depth = 0 // arrow nesting depth (0=top-level, >0=inside function)
|
|
39
40
|
let scopes = [] // block scope stack: [{names: Set, renames: Map}]
|
|
41
|
+
let staticConstScopes = [] // lexical const facts: [{strings: Map, arrays: Map}]
|
|
42
|
+
let assignedStaticGlobals = null
|
|
40
43
|
// Per-arrow set of names already declared anywhere in the function body. Used
|
|
41
44
|
// to force a rename when the same identifier is declared in two sibling blocks
|
|
42
45
|
// (else-if arms, separate { ... } chunks): without renaming, both decls lower
|
|
@@ -92,6 +95,95 @@ const stringValue = node => Array.isArray(node) && node[0] == null && typeof nod
|
|
|
92
95
|
const flatArgs = args => args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',' ? args[0].slice(1) : args
|
|
93
96
|
const MUTATING_ARRAY_METHODS = new Set(['copyWithin', 'fill', 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'])
|
|
94
97
|
|
|
98
|
+
function staticStringArrayValues(expr) {
|
|
99
|
+
if (!Array.isArray(expr) || expr[0] !== '[]' || expr.length !== 2) return null
|
|
100
|
+
const raw = Array.isArray(expr[1]) && expr[1][0] === ',' ? expr[1].slice(1) : [expr[1]]
|
|
101
|
+
const out = []
|
|
102
|
+
for (const item of raw) {
|
|
103
|
+
const s = staticStringExpr(item)
|
|
104
|
+
if (s == null) return null
|
|
105
|
+
out.push(s)
|
|
106
|
+
}
|
|
107
|
+
return out
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function immediateStaticStringExpr(node) {
|
|
111
|
+
const lit = stringValue(node)
|
|
112
|
+
if (lit != null) return lit
|
|
113
|
+
if (Array.isArray(node) && node[0] === 'str' && typeof node[1] === 'string') return node[1]
|
|
114
|
+
if (!Array.isArray(node)) return null
|
|
115
|
+
const [op, ...args] = node
|
|
116
|
+
if (op === '+') {
|
|
117
|
+
const a = immediateStaticStringExpr(args[0])
|
|
118
|
+
const b = immediateStaticStringExpr(args[1])
|
|
119
|
+
return a != null && b != null ? a + b : null
|
|
120
|
+
}
|
|
121
|
+
if (op === '`') {
|
|
122
|
+
let out = ''
|
|
123
|
+
for (const part of args) {
|
|
124
|
+
const s = immediateStaticStringExpr(part)
|
|
125
|
+
if (s == null) return null
|
|
126
|
+
out += s
|
|
127
|
+
}
|
|
128
|
+
return out
|
|
129
|
+
}
|
|
130
|
+
return null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function immediateStaticStringArrayValues(expr) {
|
|
134
|
+
if (!Array.isArray(expr) || expr[0] !== '[]' || expr.length !== 2) return null
|
|
135
|
+
const raw = Array.isArray(expr[1]) && expr[1][0] === ',' ? expr[1].slice(1) : [expr[1]]
|
|
136
|
+
const out = []
|
|
137
|
+
for (const item of raw) {
|
|
138
|
+
const s = immediateStaticStringExpr(item)
|
|
139
|
+
if (s == null) return null
|
|
140
|
+
out.push(s)
|
|
141
|
+
}
|
|
142
|
+
return out
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function eachTopLevelStatement(node, fn) {
|
|
146
|
+
if (Array.isArray(node) && node[0] === ';') {
|
|
147
|
+
for (let i = 1; i < node.length; i++) fn(node[i])
|
|
148
|
+
} else {
|
|
149
|
+
fn(node)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function collectAssignmentWrites(node, writes) {
|
|
154
|
+
if (!Array.isArray(node)) return
|
|
155
|
+
const [op, lhs] = node
|
|
156
|
+
if (op === '=' && typeof lhs === 'string') writes.set(lhs, (writes.get(lhs) || 0) + 1)
|
|
157
|
+
if ((op === '++' || op === '--') && typeof lhs === 'string') writes.set(lhs, (writes.get(lhs) || 0) + 1)
|
|
158
|
+
for (let i = 1; i < node.length; i++) collectAssignmentWrites(node[i], writes)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function collectTopLevelStaticAssignments(node, facts) {
|
|
162
|
+
if (!Array.isArray(node)) return
|
|
163
|
+
if (node[0] === ',') {
|
|
164
|
+
for (let i = 1; i < node.length; i++) collectTopLevelStaticAssignments(node[i], facts)
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
if (node[0] !== '=' || typeof node[1] !== 'string') return
|
|
168
|
+
const str = immediateStaticStringExpr(node[2])
|
|
169
|
+
const arr = immediateStaticStringArrayValues(node[2])
|
|
170
|
+
if (str != null || arr) facts.set(node[1], { str, arr })
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function seedStaticGlobalAssignments(node) {
|
|
174
|
+
// jzify hoists function declarations ahead of `var` initializer assignments.
|
|
175
|
+
// Seed one-write static globals before preparing those function bodies so
|
|
176
|
+
// compile-time-only consumers (for example `new RegExp(`${PART}`)`) can still
|
|
177
|
+
// resolve the same constants they would see after module initialization.
|
|
178
|
+
const writes = new Map()
|
|
179
|
+
const facts = new Map()
|
|
180
|
+
collectAssignmentWrites(node, writes)
|
|
181
|
+
eachTopLevelStatement(node, stmt => collectTopLevelStaticAssignments(stmt, facts))
|
|
182
|
+
for (const [name, fact] of facts) {
|
|
183
|
+
if (writes.get(name) === 1) bindStaticGlobal(name, fact.str, fact.arr)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
95
187
|
function stringArrayValues(expr) {
|
|
96
188
|
if (!Array.isArray(expr) || expr[0] !== '[' || expr.length === 1) return null
|
|
97
189
|
const out = []
|
|
@@ -107,6 +199,63 @@ function staticString(value) {
|
|
|
107
199
|
return ['str', value]
|
|
108
200
|
}
|
|
109
201
|
|
|
202
|
+
function lookupStaticString(name) {
|
|
203
|
+
const resolved = scopes.length && isDeclared(name) ? resolveScope(name) : (ctx.scope.chain[name] || name)
|
|
204
|
+
for (let i = staticConstScopes.length - 1; i >= 0; i--) {
|
|
205
|
+
const v = staticConstScopes[i].strings.get(resolved)
|
|
206
|
+
if (v != null) return v
|
|
207
|
+
}
|
|
208
|
+
return ctx.scope.shapeStrs?.get(resolved) ?? ctx.scope.constStrs?.get(resolved) ?? null
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function lookupStaticStringArray(name) {
|
|
212
|
+
const resolved = scopes.length && isDeclared(name) ? resolveScope(name) : (ctx.scope.chain[name] || name)
|
|
213
|
+
for (let i = staticConstScopes.length - 1; i >= 0; i--) {
|
|
214
|
+
const v = staticConstScopes[i].arrays.get(resolved)
|
|
215
|
+
if (v) return v
|
|
216
|
+
}
|
|
217
|
+
return ctx.scope.shapeStrArrays?.get(resolved) ?? null
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function staticStringExpr(node) {
|
|
221
|
+
const lit = stringValue(node)
|
|
222
|
+
if (lit != null) return lit
|
|
223
|
+
if (Array.isArray(node) && node[0] === 'str' && typeof node[1] === 'string') return node[1]
|
|
224
|
+
if (typeof node === 'string') return lookupStaticString(node)
|
|
225
|
+
if (!Array.isArray(node)) return null
|
|
226
|
+
const [op, ...args] = node
|
|
227
|
+
if (op === '+') {
|
|
228
|
+
const a = staticStringExpr(args[0])
|
|
229
|
+
const b = staticStringExpr(args[1])
|
|
230
|
+
return a != null && b != null ? a + b : null
|
|
231
|
+
}
|
|
232
|
+
if (op === '`') {
|
|
233
|
+
let out = ''
|
|
234
|
+
for (const part of args) {
|
|
235
|
+
const s = staticStringExpr(part)
|
|
236
|
+
if (s == null) return null
|
|
237
|
+
out += s
|
|
238
|
+
}
|
|
239
|
+
return out
|
|
240
|
+
}
|
|
241
|
+
if (op === '``' && Array.isArray(args[0]) && args[0][0] === '.' && args[0][1] === 'String' && args[0][2] === 'raw') {
|
|
242
|
+
let out = ''
|
|
243
|
+
for (const part of args.slice(1)) {
|
|
244
|
+
const s = staticStringExpr(part)
|
|
245
|
+
if (s == null) return null
|
|
246
|
+
out += s
|
|
247
|
+
}
|
|
248
|
+
return out
|
|
249
|
+
}
|
|
250
|
+
if (op === '()' && Array.isArray(args[0]) && args[0][0] === '.' && args[0][2] === 'join' && typeof args[0][1] === 'string') {
|
|
251
|
+
const arr = lookupStaticStringArray(args[0][1])
|
|
252
|
+
if (!arr) return null
|
|
253
|
+
const sep = args.length > 1 && args[1] != null ? staticStringExpr(args[1]) : ','
|
|
254
|
+
return sep != null ? arr.join(sep) : null
|
|
255
|
+
}
|
|
256
|
+
return null
|
|
257
|
+
}
|
|
258
|
+
|
|
110
259
|
function importMetaUrl() {
|
|
111
260
|
if (!ctx.transform.importMetaUrl) err('`import.meta.url` requires compile option `importMetaUrl`')
|
|
112
261
|
return ctx.transform.importMetaUrl
|
|
@@ -118,17 +267,6 @@ function resolveImportMeta(spec) {
|
|
|
118
267
|
catch { err(`Cannot resolve import.meta specifier '${spec}' from '${base}'`) }
|
|
119
268
|
}
|
|
120
269
|
|
|
121
|
-
function recordGlobalValueFact(name, expr) {
|
|
122
|
-
if (typeof name !== 'string') return
|
|
123
|
-
const vt = valTypeOf(expr)
|
|
124
|
-
if (vt) {
|
|
125
|
-
;(ctx.scope.globalValTypes ||= new Map()).set(name, vt)
|
|
126
|
-
if (vt === VAL.REGEX && ctx.runtime.regex) ctx.runtime.regex.vars.set(name, expr)
|
|
127
|
-
}
|
|
128
|
-
const ctor = typedElemCtor(expr)
|
|
129
|
-
if (ctor) (ctx.scope.globalTypedElem ||= new Map()).set(name, ctor)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
270
|
function recordModuleInitFacts(root) {
|
|
133
271
|
const facts = ctx.module.initFacts ||= {
|
|
134
272
|
dynVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
|
|
@@ -181,10 +319,13 @@ function recordModuleInitFacts(root) {
|
|
|
181
319
|
export default function prepare(node) {
|
|
182
320
|
depth = 0
|
|
183
321
|
scopes = []
|
|
322
|
+
staticConstScopes = []
|
|
323
|
+
assignedStaticGlobals = new Set()
|
|
184
324
|
funcLocalNames = [new Set()]
|
|
185
325
|
includeModule('core')
|
|
186
326
|
normalizeIdents(node)
|
|
187
|
-
fuseSparseMapReads(node)
|
|
327
|
+
fuseSparseMapReads(node) // AST-level fusion; needs pre-resolution shape — defined at end of file
|
|
328
|
+
seedStaticGlobalAssignments(node)
|
|
188
329
|
const ast = prep(node)
|
|
189
330
|
// Top-level functions referenced as first-class values (e.g. `let o = { fn: g }`,
|
|
190
331
|
// `arr.push(g)`, `return g`) need trampoline emission, which depends on the fn
|
|
@@ -249,7 +390,7 @@ export default function prepare(node) {
|
|
|
249
390
|
|
|
250
391
|
// Invalidate shapeStrs for any module-level binding that's later assigned to.
|
|
251
392
|
// shapeStrs is "effectively-const string literals at module scope" — used by
|
|
252
|
-
//
|
|
393
|
+
// shape.js's jsonConstString to enable shape inference on `let SRC = '{...}'`
|
|
253
394
|
// patterns (bench convention) without enabling the const-only static fold.
|
|
254
395
|
// The scan must skip `=` nodes that are children of `let`/`const`/`export` —
|
|
255
396
|
// those are decl-initializers, not reassignments.
|
|
@@ -280,7 +421,7 @@ export default function prepare(node) {
|
|
|
280
421
|
|
|
281
422
|
// Named constants → numeric literals
|
|
282
423
|
export const JZ_NULL = Symbol('null')
|
|
283
|
-
const CONSTANTS = { 'true':
|
|
424
|
+
const CONSTANTS = { 'true': true, 'false': false, 'null': JZ_NULL, 'undefined': JZ_NULL }
|
|
284
425
|
// NaN/Infinity stay as special f64 values in emit()
|
|
285
426
|
const F64_CONSTANTS = { 'NaN': NaN, 'Infinity': Infinity }
|
|
286
427
|
|
|
@@ -296,6 +437,34 @@ function isDeclared(name) {
|
|
|
296
437
|
return scopes.some(s => s.has(name))
|
|
297
438
|
}
|
|
298
439
|
|
|
440
|
+
function pushScope(scope = new Map()) {
|
|
441
|
+
scopes.push(scope)
|
|
442
|
+
staticConstScopes.push({ strings: new Map(), arrays: new Map() })
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function popScope() {
|
|
446
|
+
scopes.pop()
|
|
447
|
+
staticConstScopes.pop()
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function bindStaticConst(name, str, arr) {
|
|
451
|
+
const frame = staticConstScopes.at(-1)
|
|
452
|
+
if (!frame || typeof name !== 'string') return
|
|
453
|
+
if (str != null) frame.strings.set(name, str)
|
|
454
|
+
if (arr) frame.arrays.set(name, arr)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function bindStaticGlobal(name, str, arr) {
|
|
458
|
+
if (typeof name !== 'string') return
|
|
459
|
+
if (str != null) (ctx.scope.shapeStrs ||= new Map()).set(name, str)
|
|
460
|
+
if (arr) (ctx.scope.shapeStrArrays ||= new Map()).set(name, arr)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function deleteStaticGlobal(name) {
|
|
464
|
+
ctx.scope.shapeStrs?.delete(name)
|
|
465
|
+
ctx.scope.shapeStrArrays?.delete(name)
|
|
466
|
+
}
|
|
467
|
+
|
|
299
468
|
const hasFunc = name => ctx.func.names.has(name)
|
|
300
469
|
|
|
301
470
|
const renameFunc = (func, nextName) => {
|
|
@@ -306,33 +475,81 @@ const renameFunc = (func, nextName) => {
|
|
|
306
475
|
|
|
307
476
|
/** Map JS typeof strings to jz type checks. Codes < 0 trigger specialized emitTypeofCmp paths. */
|
|
308
477
|
const TYPEOF_MAP = { 'number': -1, 'string': -2, 'undefined': -3, 'boolean': -4, 'object': -5, 'function': -6, 'bigint': -7 }
|
|
478
|
+
// Spec §13.5.3: `typeof undeclared_x` returns 'undefined' without throwing.
|
|
479
|
+
// True iff `name` is a bare identifier with no resolution path. Mirrors the
|
|
480
|
+
// resolution chain inside `prep()` so we don't speculate emit-time failures.
|
|
481
|
+
function isUnresolvableBareIdent(name) {
|
|
482
|
+
if (typeof name !== 'string') return false
|
|
483
|
+
if (name in CONSTANTS || name in F64_CONSTANTS) return false
|
|
484
|
+
if (name === 'Boolean' || name === 'Number') return false
|
|
485
|
+
if (PROHIBITED[name]) return false
|
|
486
|
+
if (scopes.length && isDeclared(name)) return false
|
|
487
|
+
if (ctx.scope.chain[name]) return false
|
|
488
|
+
if (GLOBALS[name]) return false
|
|
489
|
+
if (ctx.func.names.has(name)) return false
|
|
490
|
+
if (ctx.func?.locals?.has?.(name)) return false
|
|
491
|
+
// Top-level decls live in ctx.scope.globals / userGlobals (set by prepDecl at
|
|
492
|
+
// depth 0). Current arrow's local names are tracked in funcLocalNames.
|
|
493
|
+
if (ctx.scope.globals?.has?.(name)) return false
|
|
494
|
+
if (ctx.scope.userGlobals?.has?.(name)) return false
|
|
495
|
+
const fnNames = funcLocalNames[funcLocalNames.length - 1]
|
|
496
|
+
if (fnNames?.has(name)) return false
|
|
497
|
+
return true
|
|
498
|
+
}
|
|
309
499
|
// Constant fold typeof for known builtin namespaces (e.g. Math.exp). prep(x) resolves Math.exp → 'math.exp'.
|
|
310
500
|
function staticTypeofString(x) {
|
|
501
|
+
// Spec §13.5.3: unresolvable bare ref → 'undefined'.
|
|
502
|
+
if (isUnresolvableBareIdent(x)) return 'undefined'
|
|
311
503
|
// Bare callable global: parseInt, parseFloat, isNaN, isFinite, Error, BigInt, etc.
|
|
312
504
|
if (typeof x === 'string' && !ctx.func?.locals?.has(x) && GLOBALS[x] && ctx.core.emit?.[x]?.length > 0) return 'function'
|
|
313
505
|
const px = prep(x)
|
|
314
506
|
if (typeof px === 'string' && px.includes('.') && ctx.core.emit?.[px]?.length > 0) return 'function'
|
|
315
507
|
return null
|
|
316
508
|
}
|
|
509
|
+
// Builtin-namespace constructors expose `prototype`/`length`/`name` as own
|
|
510
|
+
// properties; plain namespaces (Math, JSON, Reflect, Atomics) do not.
|
|
511
|
+
const NS_CTORS = new Set(['Number', 'String', 'Boolean', 'BigInt', 'Object',
|
|
512
|
+
'Array', 'Symbol', 'Error', 'Date', 'RegExp', 'Function', 'Map', 'Set',
|
|
513
|
+
'Promise', 'ArrayBuffer', 'DataView', 'WeakMap', 'WeakSet'])
|
|
514
|
+
// `NS.hasOwnProperty("member")` is a compile-time question: jz models a
|
|
515
|
+
// builtin namespace as a set of emit keys, so a member is owned iff jz emits
|
|
516
|
+
// it — plus the universal constructor trio for constructor namespaces.
|
|
517
|
+
function namespaceHasOwn(mod, name, member) {
|
|
518
|
+
if (ctx.core.emit[`${mod}.${member}`] != null) return true
|
|
519
|
+
return NS_CTORS.has(name) && (member === 'prototype' || member === 'length' || member === 'name')
|
|
520
|
+
}
|
|
317
521
|
function resolveTypeof(node) {
|
|
318
522
|
const [op, a, b] = node
|
|
523
|
+
// `typeof` always yields a string, so `==`/`===` (and `!=`/`!==`) are
|
|
524
|
+
// equivalent here — both collapse to the same type check.
|
|
525
|
+
const eqLike = op === '==' || op === '==='
|
|
319
526
|
// typeof x == 'string' → type check
|
|
320
527
|
if (Array.isArray(a) && a[0] === 'typeof' && Array.isArray(b) && b[0] == null && typeof b[1] === 'string') {
|
|
321
528
|
const known = staticTypeofString(a[1])
|
|
322
|
-
if (known != null) return [,
|
|
529
|
+
if (known != null) return [, eqLike ? known === b[1] : known !== b[1]]
|
|
323
530
|
const code = TYPEOF_MAP[b[1]]
|
|
324
531
|
if (code != null) return [op, ['typeof', a[1]], [, code]]
|
|
325
532
|
}
|
|
326
533
|
// 'string' == typeof x
|
|
327
534
|
if (Array.isArray(b) && b[0] === 'typeof' && Array.isArray(a) && a[0] == null && typeof a[1] === 'string') {
|
|
328
535
|
const known = staticTypeofString(b[1])
|
|
329
|
-
if (known != null) return [,
|
|
536
|
+
if (known != null) return [, eqLike ? known === a[1] : known !== a[1]]
|
|
330
537
|
const code = TYPEOF_MAP[a[1]]
|
|
331
538
|
if (code != null) return [op, ['typeof', b[1]], [, code]]
|
|
332
539
|
}
|
|
333
540
|
return node
|
|
334
541
|
}
|
|
335
542
|
|
|
543
|
+
// Prepare a strict `===`/`!==`. resolveTypeof may fold `typeof x === 'type'` to a
|
|
544
|
+
// literal or rewrite it to a numeric-code compare; either way we prep the result's
|
|
545
|
+
// operands directly. The strict op stays intact (no collapse to loose `==`) so
|
|
546
|
+
// emit can apply the no-coercion type-mismatch fold.
|
|
547
|
+
function prepStrictEq(op, a, b) {
|
|
548
|
+
const r = resolveTypeof([op, a, b])
|
|
549
|
+
if (r[0] !== op) return prep(r) // folded to a literal — re-prep is safe
|
|
550
|
+
return [op, prep(r[1]), prep(r[2])] // keep strict op; prep operands only
|
|
551
|
+
}
|
|
552
|
+
|
|
336
553
|
const cloneNode = (node) => {
|
|
337
554
|
if (!Array.isArray(node)) return node
|
|
338
555
|
const copy = node.map(cloneNode)
|
|
@@ -340,160 +557,40 @@ const cloneNode = (node) => {
|
|
|
340
557
|
return copy
|
|
341
558
|
}
|
|
342
559
|
|
|
343
|
-
/**
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
function walkSparse(node) {
|
|
353
|
-
if (!Array.isArray(node)) return
|
|
354
|
-
for (let i = 1; i < node.length; i++) walkSparse(node[i])
|
|
355
|
-
if (node[0] === ';') tryFuseInBlock(node)
|
|
356
|
-
}
|
|
357
|
-
function tryFuseInBlock(seq) {
|
|
358
|
-
for (let i = 1; i < seq.length - 1; i++) {
|
|
359
|
-
const fused = tryFusePair(seq[i], seq[i + 1], seq, i)
|
|
360
|
-
if (fused) {
|
|
361
|
-
seq.splice(i, 2, ...fused)
|
|
362
|
-
i-- // re-examine same position (chained fusions)
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
function tryFusePair(decl, forNode, seq, declIdx) {
|
|
367
|
-
if (!Array.isArray(decl) || (decl[0] !== 'const' && decl[0] !== 'let')) return null
|
|
368
|
-
if (decl.length !== 2) return null // single binding only
|
|
369
|
-
const bind = decl[1]
|
|
370
|
-
if (!Array.isArray(bind) || bind[0] !== '=' || typeof bind[1] !== 'string') return null
|
|
371
|
-
const NAME = bind[1], rhs = bind[2]
|
|
372
|
-
if (!Array.isArray(rhs) || rhs[0] !== '()') return null
|
|
373
|
-
const callee = rhs[1]
|
|
374
|
-
if (!Array.isArray(callee) || callee[0] !== '.' || callee[2] !== 'map') return null
|
|
375
|
-
const RECV = callee[1]
|
|
376
|
-
if (typeof RECV !== 'string' || RECV === NAME) return null
|
|
377
|
-
const arrow = rhs[2]
|
|
378
|
-
if (!Array.isArray(arrow) || arrow[0] !== '=>') return null
|
|
379
|
-
// Single-name param only: `x => …` or `(x) => …`
|
|
380
|
-
const ap = arrow[1]
|
|
381
|
-
const PARAM = typeof ap === 'string' ? ap :
|
|
382
|
-
(Array.isArray(ap) && ap[0] === '()' && typeof ap[1] === 'string' ? ap[1] : null)
|
|
383
|
-
if (!PARAM || PARAM === NAME || PARAM === RECV) return null
|
|
384
|
-
// Body: single-expression arrow only (block bodies skipped — could extend later).
|
|
385
|
-
const aBody = arrow[2]
|
|
386
|
-
if (Array.isArray(aBody) && aBody[0] === '{}') return null
|
|
387
|
-
if (!isPureSparseArrowBody(aBody, PARAM)) return null
|
|
388
|
-
// For-loop: ['for', [';', initStmt, cond, inc], body]
|
|
389
|
-
if (!Array.isArray(forNode) || forNode[0] !== 'for' || forNode.length !== 3) return null
|
|
390
|
-
const head = forNode[1]
|
|
391
|
-
if (!Array.isArray(head) || head[0] !== ';' || head.length !== 4) return null
|
|
392
|
-
const cond = head[2], forBody = forNode[2]
|
|
393
|
-
// Verify `NAME` is used only as `NAME[idx]` or `NAME.length` inside cond+forBody.
|
|
394
|
-
if (!hasOnlySparseUses(cond, NAME)) return null
|
|
395
|
-
if (!hasOnlySparseUses(forBody, NAME)) return null
|
|
396
|
-
if (!hasAnyIndexedRead(forBody, NAME) && !hasAnyIndexedRead(cond, NAME)) return null
|
|
397
|
-
// `NAME` must not be read after the for-loop in the same block.
|
|
398
|
-
for (let k = declIdx + 2; k < seq.length; k++) {
|
|
399
|
-
if (refsName(seq[k], NAME)) return null
|
|
400
|
-
}
|
|
401
|
-
// RECV must not be reassigned inside the for-loop (would invalidate substitution).
|
|
402
|
-
if (assignsName(forNode, RECV) || assignsName(forNode, NAME)) return null
|
|
403
|
-
// PARAM must not collide with any binding inside forBody (otherwise substitution shadows wrongly).
|
|
404
|
-
if (bindsName(forNode, PARAM)) return null
|
|
405
|
-
// Apply substitution: NAME.length → RECV.length; NAME[idx] → arrowBody[PARAM ← RECV[idx]].
|
|
406
|
-
const newCond = substSparse(cond, NAME, RECV, PARAM, aBody)
|
|
407
|
-
const newBody = substSparse(forBody, NAME, RECV, PARAM, aBody)
|
|
408
|
-
const newHead = [';', head[1], newCond, head[3]]
|
|
409
|
-
return [['for', newHead, newBody]]
|
|
410
|
-
}
|
|
411
|
-
function isPureSparseArrowBody(n, PARAM) {
|
|
412
|
-
if (typeof n === 'string') return true
|
|
413
|
-
if (!Array.isArray(n)) return true
|
|
414
|
-
const op = n[0]
|
|
415
|
-
// Calls / new / assignments / increments are unsafe for repeated-substitution semantics.
|
|
416
|
-
if (op === '()' || op === '?.()' || op === 'new' || op === '++' || op === '--') return false
|
|
417
|
-
if (op === '=>') return false // nested closure is opaque
|
|
418
|
-
if (typeof op === 'string' && op !== '=>' && op !== '===' && op !== '!==' && op !== '==' && op !== '!=' && op !== '<=' && op !== '>=' && op.endsWith('=') && op !== '=') return false
|
|
419
|
-
if (op === '=') return false
|
|
420
|
-
for (let i = 1; i < n.length; i++) if (!isPureSparseArrowBody(n[i], PARAM)) return false
|
|
421
|
-
return true
|
|
422
|
-
}
|
|
423
|
-
function hasOnlySparseUses(n, NAME) {
|
|
424
|
-
if (typeof n === 'string') return n !== NAME
|
|
425
|
-
if (!Array.isArray(n)) return true
|
|
426
|
-
const op = n[0]
|
|
427
|
-
if (op === '[]' && n.length === 3 && n[1] === NAME) return hasOnlySparseUses(n[2], NAME) // NAME[idx] — idx must not reference NAME
|
|
428
|
-
if (op === '.' && n[1] === NAME) {
|
|
429
|
-
if (n[2] === 'length') return true
|
|
430
|
-
return false // any other property access on NAME is opaque
|
|
431
|
-
}
|
|
432
|
-
for (let i = 1; i < n.length; i++) if (!hasOnlySparseUses(n[i], NAME)) return false
|
|
433
|
-
return true
|
|
434
|
-
}
|
|
435
|
-
function hasAnyIndexedRead(n, NAME) {
|
|
436
|
-
if (!Array.isArray(n)) return false
|
|
437
|
-
if (n[0] === '[]' && n.length === 3 && n[1] === NAME) return true
|
|
438
|
-
for (let i = 1; i < n.length; i++) if (hasAnyIndexedRead(n[i], NAME)) return true
|
|
439
|
-
return false
|
|
560
|
+
/** True if `node` contains a `break`/`continue` that belongs to it — i.e. not
|
|
561
|
+
* one nested inside its own function. (Nested loops are intentionally counted:
|
|
562
|
+
* an over-detection only opts into the safe frame-carrying lowering below.) */
|
|
563
|
+
const hasLoopJump = (node) => {
|
|
564
|
+
if (!Array.isArray(node)) return false
|
|
565
|
+
const op = node[0]
|
|
566
|
+
if (op === 'break' || op === 'continue') return true
|
|
567
|
+
if (op === '=>' || op === 'function') return false
|
|
568
|
+
return node.some(hasLoopJump)
|
|
440
569
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
function assignsName(n, NAME) {
|
|
448
|
-
if (!Array.isArray(n)) return false
|
|
449
|
-
const op = n[0]
|
|
450
|
-
if ((op === '=' || op === '++' || op === '--' ||
|
|
451
|
-
(typeof op === 'string' && op.endsWith('=') && op !== '==' && op !== '===' && op !== '!=' && op !== '!==' && op !== '<=' && op !== '>='))
|
|
452
|
-
&& n[1] === NAME) return true
|
|
453
|
-
for (let i = 1; i < n.length; i++) if (assignsName(n[i], NAME)) return true
|
|
454
|
-
return false
|
|
455
|
-
}
|
|
456
|
-
function bindsName(n, NAME) {
|
|
457
|
-
if (!Array.isArray(n)) return false
|
|
458
|
-
const op = n[0]
|
|
459
|
-
if ((op === 'let' || op === 'const')) {
|
|
460
|
-
for (let i = 1; i < n.length; i++) {
|
|
461
|
-
const bind = n[i]
|
|
462
|
-
if (Array.isArray(bind) && bind[0] === '=' && bind[1] === NAME) return true
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
if (op === '=>') {
|
|
466
|
-
const p = n[1]
|
|
467
|
-
if (p === NAME) return true
|
|
468
|
-
if (Array.isArray(p)) {
|
|
469
|
-
if (p[0] === '()' && p[1] === NAME) return true
|
|
470
|
-
// skip deeper destructuring forms — conservative
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
for (let i = 1; i < n.length; i++) if (bindsName(n[i], NAME)) return true
|
|
474
|
-
return false
|
|
475
|
-
}
|
|
476
|
-
function substSparse(n, NAME, RECV, PARAM, arrowBody) {
|
|
477
|
-
if (typeof n !== 'object' || n === null || !Array.isArray(n)) return n
|
|
478
|
-
if (n[0] === '.' && n[1] === NAME && n[2] === 'length') return ['.', RECV, 'length']
|
|
479
|
-
if (n[0] === '[]' && n.length === 3 && n[1] === NAME) {
|
|
480
|
-
const idx = substSparse(n[2], NAME, RECV, PARAM, arrowBody)
|
|
481
|
-
return cloneAndBind(arrowBody, PARAM, ['[]', RECV, idx])
|
|
482
|
-
}
|
|
483
|
-
return n.map((c, i) => i === 0 ? c : substSparse(c, NAME, RECV, PARAM, arrowBody))
|
|
484
|
-
}
|
|
485
|
-
function cloneAndBind(node, PARAM, replacement) {
|
|
486
|
-
if (node === PARAM) return replacement
|
|
570
|
+
|
|
571
|
+
/** Retarget a for-in iteration's *own* unlabeled `break`/`continue` to explicit
|
|
572
|
+
* block labels — `break` to the construct-wide label, `continue` to this
|
|
573
|
+
* iteration's label. Nested loops/functions own their jumps and are skipped;
|
|
574
|
+
* labeled jumps already name their target and are left untouched. */
|
|
575
|
+
const retargetLoopJumps = (node, brkLabel, contLabel) => {
|
|
487
576
|
if (!Array.isArray(node)) return node
|
|
488
|
-
|
|
577
|
+
const op = node[0]
|
|
578
|
+
if (op === 'break' && node.length === 1) return ['break', brkLabel]
|
|
579
|
+
if (op === 'continue' && node.length === 1) return ['break', contLabel]
|
|
580
|
+
if (op === 'for' || op === 'for-in' || op === 'while' || op === 'do'
|
|
581
|
+
|| op === '=>' || op === 'function') return node
|
|
582
|
+
return node.map(c => retargetLoopJumps(c, brkLabel, contLabel))
|
|
489
583
|
}
|
|
490
584
|
|
|
491
585
|
function prep(node) {
|
|
492
586
|
if (Array.isArray(node)) includeForOp(node[0])
|
|
493
587
|
if (Array.isArray(node) && node.loc != null) ctx.error.loc = node.loc
|
|
494
588
|
if (node == null) return [, 0] // null/undefined → 0 literal
|
|
495
|
-
|
|
496
|
-
|
|
589
|
+
// Keep boolean identity (was folded to 1/0). The working representation is
|
|
590
|
+
// still i32/f64 0/1 — emit lowers the raw boolean — but valTypeOf now reads
|
|
591
|
+
// VAL.BOOL off the literal, so typeof/String/JSON/host boundary stay faithful.
|
|
592
|
+
if (node === true) return [, true]
|
|
593
|
+
if (node === false) return [, false]
|
|
497
594
|
if (!Array.isArray(node)) {
|
|
498
595
|
if (typeof node === 'string') {
|
|
499
596
|
if (node in CONSTANTS) return [, CONSTANTS[node]]
|
|
@@ -526,7 +623,11 @@ function prep(node) {
|
|
|
526
623
|
return handler ? handler(...args) : [op, ...args.map(prep)]
|
|
527
624
|
}
|
|
528
625
|
|
|
529
|
-
//
|
|
626
|
+
// Strict-jz prohibitions. `class` and `arguments` are *also* listed here but
|
|
627
|
+
// only reach this point when jzify is off — jzify lowers `class` to a factory
|
|
628
|
+
// arrow and rewrites `arguments` to a rest param before prepare runs. The
|
|
629
|
+
// remainder (`with`, `this`, `super`, `yield`, `eval`) have no safe lowering
|
|
630
|
+
// and stay errors in both modes.
|
|
530
631
|
const PROHIBITED = { 'with': '`with` not supported', 'class': '`class` not supported', 'yield': '`yield` not supported',
|
|
531
632
|
'this': '`this` not supported: use explicit parameter',
|
|
532
633
|
'super': '`super` not supported: no class inheritance',
|
|
@@ -552,7 +653,21 @@ export const GLOBALS = {
|
|
|
552
653
|
isFinite: 'number',
|
|
553
654
|
parseInt: 'number',
|
|
554
655
|
parseFloat: 'number',
|
|
656
|
+
encodeURIComponent: 'encodeURIComponent',
|
|
657
|
+
decodeURIComponent: 'decodeURIComponent',
|
|
555
658
|
Error: 'Error',
|
|
659
|
+
// Error subclasses: distinct names in JS, but jz doesn't carry typed error
|
|
660
|
+
// info — `throw` accepts any value and stringification goes through the
|
|
661
|
+
// host. Treat them all as Error-shaped passthrough constructors so user
|
|
662
|
+
// code that throws specific subclasses (`throw new SyntaxError(msg)`) compiles
|
|
663
|
+
// identically. If we ever model `instanceof SyntaxError`, this is where to
|
|
664
|
+
// distinguish them; until then the surfaced message is what matters.
|
|
665
|
+
TypeError: 'Error',
|
|
666
|
+
SyntaxError: 'Error',
|
|
667
|
+
RangeError: 'Error',
|
|
668
|
+
ReferenceError: 'Error',
|
|
669
|
+
URIError: 'Error',
|
|
670
|
+
EvalError: 'Error',
|
|
556
671
|
BigInt: 'BigInt',
|
|
557
672
|
TextEncoder: 'TextEncoder',
|
|
558
673
|
TextDecoder: 'TextDecoder',
|
|
@@ -589,6 +704,27 @@ function scalarArrayDestruct(pattern, rhs) {
|
|
|
589
704
|
return prep([';', ['let', ...decls], ...assigns])
|
|
590
705
|
}
|
|
591
706
|
|
|
707
|
+
function declareGlobal(name, user = true) {
|
|
708
|
+
if (depth !== 0 || typeof name !== 'string') return name
|
|
709
|
+
if (ctx.scope.globals.has(name)) err(`'${name}' conflicts with a compiler internal — choose a different name`)
|
|
710
|
+
ctx.scope.globals.set(name, `(global $${name} (mut f64) (f64.const 0))`)
|
|
711
|
+
if (user) ctx.scope.userGlobals.add(name)
|
|
712
|
+
return name
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function bindingNames(pattern, out = new Set()) {
|
|
716
|
+
if (typeof pattern === 'string') out.add(pattern)
|
|
717
|
+
else if (Array.isArray(pattern)) {
|
|
718
|
+
if (pattern[0] === '...' && typeof pattern[1] === 'string') out.add(pattern[1])
|
|
719
|
+
else if (pattern[0] === '=') bindingNames(pattern[1], out)
|
|
720
|
+
else if (pattern[0] === ':') bindingNames(pattern[2], out)
|
|
721
|
+
else if (pattern[0] === '[]' || pattern[0] === '{}' || pattern[0] === ',') {
|
|
722
|
+
for (const item of pattern.slice(1)) bindingNames(item, out)
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return out
|
|
726
|
+
}
|
|
727
|
+
|
|
592
728
|
function pushPatternAssign(target, valueExpr, out, decls = null) {
|
|
593
729
|
if (Array.isArray(target) && target[0] === '=') {
|
|
594
730
|
pushPatternAssign(target[1], ['??', valueExpr, prep(target[2])], out, decls)
|
|
@@ -703,19 +839,51 @@ function prepDecl(op, ...inits) {
|
|
|
703
839
|
if (ctx.scope.globals.has(declName)) err(`'${declName}' conflicts with a compiler internal — choose a different name`)
|
|
704
840
|
ctx.scope.globals.set(declName, `(global $${declName} (mut f64) (f64.const 0))`)
|
|
705
841
|
ctx.scope.userGlobals.add(declName)
|
|
842
|
+
} else if (typeof declName === 'string') {
|
|
843
|
+
// Bare hoisted decl inside a function (var X jzified to `let X` at top
|
|
844
|
+
// of arrow + a later `X = …` assignment). Without registering here, the
|
|
845
|
+
// name is invisible to scope predicates like `isUnresolvableBareIdent`
|
|
846
|
+
// until the assignment runs — which is after any reference to it.
|
|
847
|
+
const fnNames = funcLocalNames[funcLocalNames.length - 1]
|
|
848
|
+
if (fnNames) fnNames.add(declName)
|
|
849
|
+
if (scopes.length > 0) scopes[scopes.length - 1].set(declName, declName)
|
|
706
850
|
}
|
|
707
851
|
rest.push(declName)
|
|
708
852
|
continue
|
|
709
853
|
}
|
|
710
|
-
const [, name, init] = i
|
|
854
|
+
const [, name, init] = i
|
|
855
|
+
const staticStr = op === 'const' ? staticStringExpr(init) : null
|
|
856
|
+
const staticArr = op === 'const' ? staticStringArrayValues(init) : null
|
|
857
|
+
const normed = prep(init)
|
|
711
858
|
|
|
712
859
|
if (isDestructPattern(name)) {
|
|
860
|
+
// Register each binding both as a module global (depth 0) and in the
|
|
861
|
+
// current arrow's local scope (depth ≠ 0). Without the local registration
|
|
862
|
+
// the name is invisible to `isUnresolvableBareIdent`, so a later
|
|
863
|
+
// `typeof x` would mis-fold to 'undefined' (spec §13.5.3) before emit ever
|
|
864
|
+
// sees the binding — see the bare-hoisted-decl branch above for the same fix.
|
|
865
|
+
const fnNames = funcLocalNames[funcLocalNames.length - 1]
|
|
866
|
+
for (const n of bindingNames(name)) {
|
|
867
|
+
declareGlobal(n)
|
|
868
|
+
if (depth !== 0 && typeof n === 'string') {
|
|
869
|
+
if (fnNames) fnNames.add(n)
|
|
870
|
+
if (scopes.length > 0) scopes[scopes.length - 1].set(n, n)
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
// A bare-identifier source needs no temp: reads are idempotent and
|
|
874
|
+
// side-effect-free, so we destructure straight off it. This keeps each
|
|
875
|
+
// element's static type tag (e.g. `let [, x] = strs` resolves `x` to the
|
|
876
|
+
// same STRING that `strs[1]` would) — a copy temp drops the array's
|
|
877
|
+
// element-type shape and `typeof x` would degrade to 'undefined'.
|
|
878
|
+
if (typeof normed === 'string') {
|
|
879
|
+
expandDestruct(name, normed, rest)
|
|
880
|
+
continue
|
|
881
|
+
}
|
|
713
882
|
const tmp = `${T}d${ctx.func.uniq++}`
|
|
883
|
+
declareGlobal(tmp, false)
|
|
714
884
|
rest.push(['=', tmp, normed])
|
|
715
885
|
// Propagate schema to temp so rest destructuring can resolve it
|
|
716
|
-
if (
|
|
717
|
-
ctx.schema.vars.set(tmp, ctx.schema.vars.get(normed))
|
|
718
|
-
else if (Array.isArray(normed) && normed[0] === '{}') {
|
|
886
|
+
if (Array.isArray(normed) && normed[0] === '{}') {
|
|
719
887
|
const p = normed.slice(1).filter(p => Array.isArray(p) && p[0] === ':').map(p => p[1])
|
|
720
888
|
if (p.length) ctx.schema.vars.set(tmp, ctx.schema.register(p))
|
|
721
889
|
}
|
|
@@ -737,18 +905,19 @@ function prepDecl(op, ...inits) {
|
|
|
737
905
|
scopes[scopes.length - 1].set(name, name)
|
|
738
906
|
}
|
|
739
907
|
if (typeof declName === 'string' && fnNames) fnNames.add(declName)
|
|
908
|
+
if (op === 'const') bindStaticConst(declName, staticStr, staticArr)
|
|
740
909
|
// Track const for reassignment checks — only module-scope consts (depth 0)
|
|
741
910
|
if (typeof declName === 'string' && depth === 0) {
|
|
742
911
|
if (ctx.module.currentPrefix) {
|
|
743
912
|
declName = `${ctx.module.currentPrefix}$${declName}`
|
|
744
913
|
ctx.scope.chain[name] = declName
|
|
745
914
|
}
|
|
915
|
+
if (op === 'const') bindStaticGlobal(declName, staticStr, staticArr)
|
|
746
916
|
if (op === 'const') {
|
|
747
917
|
if (!ctx.scope.consts) ctx.scope.consts = new Set()
|
|
748
918
|
ctx.scope.consts.add(declName)
|
|
749
|
-
if (
|
|
750
|
-
|
|
751
|
-
const strs = stringArrayValues(normed)
|
|
919
|
+
if (staticStr != null) (ctx.scope.constStrs ||= new Map()).set(declName, staticStr)
|
|
920
|
+
const strs = staticArr || stringArrayValues(normed)
|
|
752
921
|
if (strs) (ctx.scope.shapeStrArrays ||= new Map()).set(declName, strs)
|
|
753
922
|
} else if (op === 'let' && ctx.scope.consts?.has(declName)) {
|
|
754
923
|
ctx.scope.consts.delete(declName)
|
|
@@ -761,7 +930,7 @@ function prepDecl(op, ...inits) {
|
|
|
761
930
|
// entry whose name is later assigned to.
|
|
762
931
|
if (Array.isArray(normed) && normed[0] === 'str' && typeof normed[1] === 'string')
|
|
763
932
|
(ctx.scope.shapeStrs ||= new Map()).set(declName, normed[1])
|
|
764
|
-
|
|
933
|
+
recordGlobalRep(declName, normed)
|
|
765
934
|
}
|
|
766
935
|
// Track object schemas (after prefix so schema is keyed to final name)
|
|
767
936
|
if (typeof declName === 'string' && Array.isArray(normed) && normed[0] === '{}' && normed.length > 1) {
|
|
@@ -788,6 +957,119 @@ function prepDecl(op, ...inits) {
|
|
|
788
957
|
return rest.length ? [op, ...rest] : null
|
|
789
958
|
}
|
|
790
959
|
|
|
960
|
+
// --- `'()'` call-handler helpers --------------------------------------------
|
|
961
|
+
// The call handler is a thin dispatcher: it tries the compile-time folds
|
|
962
|
+
// below (each gated by callee shape, so at most one fires), then resolves the
|
|
963
|
+
// callee, then assembles the call. Each helper moves one concern out of line.
|
|
964
|
+
|
|
965
|
+
// `import.meta.resolve("spec")` → the resolved URL as a static string.
|
|
966
|
+
function foldImportMetaResolve(callee, args) {
|
|
967
|
+
if (!isImportMetaProp(callee, 'resolve')) return undefined
|
|
968
|
+
const callArgs = flatArgs(args).filter(a => a != null)
|
|
969
|
+
if (callArgs.length !== 1) err('`import.meta.resolve` requires one string literal argument')
|
|
970
|
+
const spec = stringValue(callArgs[0])
|
|
971
|
+
if (spec == null) err('`import.meta.resolve` supports only string literal arguments')
|
|
972
|
+
return staticString(resolveImportMeta(spec))
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// String-callee constructor / named-builtin folds: `Array(n)` and the `CTORS`
|
|
976
|
+
// set redirect to the `new` handler; `BigInt64Array`/`BigUint64Array` build a
|
|
977
|
+
// direct module call. `includeForNamedCall` is probed for every string callee
|
|
978
|
+
// — that probe is also how a module-backed builtin gets its modules included.
|
|
979
|
+
// Returns the replacement IR, or `undefined` for an ordinary call.
|
|
980
|
+
function dispatchConstructorCall(callee, args) {
|
|
981
|
+
if (typeof callee !== 'string') return undefined
|
|
982
|
+
if (callee === 'Array') {
|
|
983
|
+
const callArgs = flatArgs(args).filter(a => a != null)
|
|
984
|
+
if (callArgs.length === 1) return handlers['new'](['()', callee, callArgs[0]])
|
|
985
|
+
}
|
|
986
|
+
if (CTORS.includes(callee)) return handlers['new'](['()', callee, ...args])
|
|
987
|
+
if (includeForNamedCall(callee) && (callee === 'BigInt64Array' || callee === 'BigUint64Array'))
|
|
988
|
+
return ['()', callee, ...args.filter(a => a != null).map(prep)]
|
|
989
|
+
return undefined
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Compile-time namespace introspection on a `obj.prop(...)` callee:
|
|
993
|
+
// `Array.isArray(NS)` on a bare builtin global folds to `false` (a namespace
|
|
994
|
+
// value is never an array); `NS.hasOwnProperty("member")` on a builtin
|
|
995
|
+
// namespace folds to a literal — no runtime namespace object. Returns the
|
|
996
|
+
// folded literal IR, or `undefined` when nothing folds.
|
|
997
|
+
function foldNamespaceIntrospection(callee, args) {
|
|
998
|
+
if (!Array.isArray(callee) || callee[0] !== '.') return undefined
|
|
999
|
+
const [, obj, prop] = callee
|
|
1000
|
+
if (obj === 'Array' && prop === 'isArray') {
|
|
1001
|
+
const cargs = flatArgs(args).filter(a => a != null)
|
|
1002
|
+
const a0 = cargs.length === 1 ? cargs[0] : null
|
|
1003
|
+
if (typeof a0 === 'string' && GLOBALS[a0] && !(scopes.length && isDeclared(a0)) && !hasFunc(a0))
|
|
1004
|
+
return [, 0]
|
|
1005
|
+
}
|
|
1006
|
+
if (prop === 'hasOwnProperty' && typeof obj === 'string' && !(scopes.length && isDeclared(obj))) {
|
|
1007
|
+
const mod = ctx.scope.chain[obj]
|
|
1008
|
+
if (mod && !mod.includes('.') && hasModule(mod)) {
|
|
1009
|
+
const cargs = flatArgs(args).filter(a => a != null)
|
|
1010
|
+
const member = cargs.length === 1 ? stringValue(cargs[0]) : null
|
|
1011
|
+
// Include the module so its emit keys (the namespace's member set) are
|
|
1012
|
+
// registered; unreferenced emitters/data dead-strip in compile.
|
|
1013
|
+
if (member != null) { includeModule(mod); return [, namespaceHasOwn(mod, obj, member) ? 1 : 0] }
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return undefined
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Resolve a callee to its lowered form, triggering module autoloads along the
|
|
1020
|
+
// way: a bare identifier through the scope chain, an `obj.prop` member call
|
|
1021
|
+
// through host imports / named-call / generic-method / namespace tables, and
|
|
1022
|
+
// any other expression through `prep` (a callable runtime value).
|
|
1023
|
+
function resolveCallee(callee, args) {
|
|
1024
|
+
if (typeof callee === 'string') {
|
|
1025
|
+
const local = scopes.length && isDeclared(callee)
|
|
1026
|
+
const resolved = local ? null : ctx.scope.chain[callee]
|
|
1027
|
+
if (local) return resolveScope(callee)
|
|
1028
|
+
if (resolved?.includes('.')) return resolved
|
|
1029
|
+
if (resolved && hasFunc(resolved)) return resolved
|
|
1030
|
+
if (resolved && !resolved.includes('.')) {
|
|
1031
|
+
if (hasModule(resolved) && !ctx.module.imports.some(i => i[3]?.[1] === `$${resolved}`)) includeModule(resolved)
|
|
1032
|
+
return callee
|
|
1033
|
+
}
|
|
1034
|
+
if (depth > 0 && !resolved && !ctx.func.exports[callee] && !ctx.module.imports.some(i => i[3]?.[1] === `$${callee}`))
|
|
1035
|
+
includeForCallableValue()
|
|
1036
|
+
return callee
|
|
1037
|
+
}
|
|
1038
|
+
if (Array.isArray(callee) && callee[0] === '.') {
|
|
1039
|
+
const [, obj, prop] = callee
|
|
1040
|
+
const key = typeof obj === 'string' && typeof prop === 'string' ? `${obj}.${prop}` : null
|
|
1041
|
+
if (key && ctx.module.hostImports?.[obj]?.[prop]) {
|
|
1042
|
+
const spec = ctx.module.hostImports[obj][prop]
|
|
1043
|
+
const alias = `${obj}$${prop}`
|
|
1044
|
+
addHostImport(obj, prop, alias, spec)
|
|
1045
|
+
return alias
|
|
1046
|
+
}
|
|
1047
|
+
if (key && includeForNamedCall(key)) return key
|
|
1048
|
+
if (includeForGenericMethod(prop)) return prep(callee)
|
|
1049
|
+
const mod = ctx.scope.chain[obj]
|
|
1050
|
+
if (typeof obj === 'string' && mod && !mod.includes('.') && hasModule(mod))
|
|
1051
|
+
return (includeModule(mod), mod + '.' + prop)
|
|
1052
|
+
return prep(callee)
|
|
1053
|
+
}
|
|
1054
|
+
includeForCallableValue()
|
|
1055
|
+
return prep(callee)
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// A lone parenthesized comma-expression argument — `f((a, b, c))` — is ONE
|
|
1059
|
+
// argument whose value is the last comma operand. The parser keeps it wrapped
|
|
1060
|
+
// (`['()', [',', …]]`); prep would strip the grouping, leaving a bare comma
|
|
1061
|
+
// that emit can no longer tell apart from an arg list and splats into N args.
|
|
1062
|
+
// With ≥2 args an outer arg-list comma already nests it — only the sole-arg
|
|
1063
|
+
// case loses the distinction. Re-nest it under a 1-element arg-list comma.
|
|
1064
|
+
function renestSoleCommaArg(args) {
|
|
1065
|
+
if (args.length === 1 && Array.isArray(args[0]) && args[0][0] === '()' && args[0].length === 2) {
|
|
1066
|
+
const ungroup = n => Array.isArray(n) && n[0] === '()' && n.length === 2 ? ungroup(n[1]) : n
|
|
1067
|
+
const core = ungroup(args[0])
|
|
1068
|
+
if (Array.isArray(core) && core[0] === ',') return [[',', args[0]]]
|
|
1069
|
+
}
|
|
1070
|
+
return args
|
|
1071
|
+
}
|
|
1072
|
+
|
|
791
1073
|
const handlers = {
|
|
792
1074
|
// Spread operator: [...expr] in arrays, f(...args) in calls, {...obj} in objects
|
|
793
1075
|
'...'(expr) {
|
|
@@ -802,11 +1084,23 @@ const handlers = {
|
|
|
802
1084
|
'class': () => err('class not supported: use object literals'),
|
|
803
1085
|
'yield': () => err('generators not supported: use loops'),
|
|
804
1086
|
'debugger': () => null,
|
|
805
|
-
|
|
1087
|
+
// Static-key delete (.x, ["x"], [literal]) would change the fixed schema → reject.
|
|
1088
|
+
// Computed-key delete (obj[expr]) — including jessie's `delete ctx[k]` — lowers
|
|
1089
|
+
// to runtime __dyn_del against the per-object shadow property store.
|
|
1090
|
+
'delete'(target) {
|
|
1091
|
+
const t = prep(target)
|
|
1092
|
+
if (Array.isArray(t) && t[0] === '[]' && t.length === 3) {
|
|
1093
|
+
const key = t[2]
|
|
1094
|
+
const isLiteralKey = Array.isArray(key) && key[0] == null && key.length === 2
|
|
1095
|
+
if (!isLiteralKey) return ['delete', t[1], key]
|
|
1096
|
+
}
|
|
1097
|
+
err('delete not supported: object shape is fixed')
|
|
1098
|
+
},
|
|
806
1099
|
'in'(key, obj) { return ['in', prep(key), prep(obj)] },
|
|
807
1100
|
'instanceof': () => err('instanceof not supported: use typeof'),
|
|
808
1101
|
'with': () => err('`with` not supported: deprecated'),
|
|
809
1102
|
':': () => err('labeled statements not supported'),
|
|
1103
|
+
'label'(name, body) { return ['label', name, prep(body)] },
|
|
810
1104
|
'var': () => err('`var` not supported: use let/const'),
|
|
811
1105
|
'function': () => err('`function` not supported: use arrow functions'),
|
|
812
1106
|
|
|
@@ -828,23 +1122,60 @@ const handlers = {
|
|
|
828
1122
|
expandDestruct(lhs, tmp, stmts, decls)
|
|
829
1123
|
return prep([';', ['let', ...decls], ...stmts])
|
|
830
1124
|
}
|
|
831
|
-
//
|
|
832
|
-
//
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
1125
|
+
// Function property assignment: fn.prop = arrow → extract as top-level function fn$prop.
|
|
1126
|
+
// A property can be reassigned — esbuild/jessie wrapper-composition does
|
|
1127
|
+
// `p.s = ...; var old = p.s; p.s = () => old()...`. Each assignment extracts
|
|
1128
|
+
// its own top-level function; the property holds whichever was assigned last,
|
|
1129
|
+
// and an earlier snapshot keeps pointing at the prior one. Collide → fresh name.
|
|
1130
|
+
// The base resolves through the scope chain first so an *imported* function
|
|
1131
|
+
// (mangled to `_mod$fn`) is recognised the same as a local one — the
|
|
1132
|
+
// subscript parser's plugin model mutates `parse.step` etc. across modules,
|
|
1133
|
+
// and a reassignment in module B must mark module A's call sites mutable.
|
|
1134
|
+
if (depth === 0 && Array.isArray(lhs) && lhs[0] === '.' && typeof lhs[1] === 'string'
|
|
1135
|
+
&& Array.isArray(rhs) && rhs[0] === '=>') {
|
|
1136
|
+
const fnBase = ctx.scope.chain[lhs[1]] || lhs[1]
|
|
1137
|
+
if (hasFunc(fnBase)) {
|
|
1138
|
+
let name = `${fnBase}$${lhs[2]}`
|
|
1139
|
+
// Reassignment → the property is mutable; record it so `fn.prop()` calls
|
|
1140
|
+
// emit a dynamic property read + indirect call instead of a direct call.
|
|
1141
|
+
if (ctx.func.names.has(name)) {
|
|
1142
|
+
ctx.func.multiProp.add(`${fnBase}.${lhs[2]}`)
|
|
1143
|
+
do name = `${fnBase}$${lhs[2]}$${ctx.func.uniq++}`; while (ctx.func.names.has(name))
|
|
1144
|
+
}
|
|
1145
|
+
// Build the target `.` node directly from the resolved base — re-`prep`ing
|
|
1146
|
+
// the lhs would resolve a multiProp `fn.prop` to an rvalue (closure
|
|
1147
|
+
// materialization block), which is not a valid assignment target.
|
|
1148
|
+
if (defFunc(name, prep(rhs))) return ['=', ['.', fnBase, lhs[2]], name]
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
const staticStr = staticStringExpr(rhs)
|
|
1152
|
+
const staticArr = staticStringArrayValues(rhs)
|
|
1153
|
+
const plhs = prep(lhs)
|
|
1154
|
+
const prhs = prep(rhs)
|
|
1155
|
+
if (depth === 0 && typeof plhs === 'string' && ctx.scope.globals.has(plhs)) {
|
|
1156
|
+
// First assignment fixes the global's representation + object schema.
|
|
1157
|
+
if (!ctx.scope.globalReps?.has(plhs)) {
|
|
1158
|
+
recordGlobalRep(plhs, prhs)
|
|
1159
|
+
if (Array.isArray(prhs) && prhs[0] === '{}') {
|
|
1160
|
+
const props = staticObjectProps(prhs.slice(1))
|
|
1161
|
+
if (props) ctx.schema.vars.set(plhs, ctx.schema.register(props.names))
|
|
1162
|
+
}
|
|
839
1163
|
}
|
|
1164
|
+
// Static string/array facts hold only while every assignment is constant.
|
|
1165
|
+
if (!assignedStaticGlobals.has(plhs) && (staticStr != null || staticArr)) bindStaticGlobal(plhs, staticStr, staticArr)
|
|
1166
|
+
else deleteStaticGlobal(plhs)
|
|
1167
|
+
assignedStaticGlobals.add(plhs)
|
|
840
1168
|
}
|
|
841
|
-
//
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1169
|
+
// Local object-literal assignment to a not-yet-shaped variable — e.g. a `var`
|
|
1170
|
+
// that jzify hoisted into `let x; x = {…}`. Recording the schema here lets the
|
|
1171
|
+
// binding behave like `let x = {…}`: fixed-slot field access and for-in unroll.
|
|
1172
|
+
// First assignment fixes the shape (mirrors the global rule above).
|
|
1173
|
+
else if (typeof plhs === 'string' && Array.isArray(prhs) && prhs[0] === '{}'
|
|
1174
|
+
&& !ctx.schema.vars.has(plhs)) {
|
|
1175
|
+
const props = staticObjectProps(prhs.slice(1))
|
|
1176
|
+
if (props) ctx.schema.vars.set(plhs, ctx.schema.register(props.names))
|
|
846
1177
|
}
|
|
847
|
-
return ['=',
|
|
1178
|
+
return ['=', plhs, prhs]
|
|
848
1179
|
},
|
|
849
1180
|
|
|
850
1181
|
// try/catch/throw
|
|
@@ -877,16 +1208,14 @@ const handlers = {
|
|
|
877
1208
|
},
|
|
878
1209
|
|
|
879
1210
|
// Tagged template: tag`a${x}b` → tag(['a','b'], x)
|
|
880
|
-
// Parser drops empty string segments; reinsert them to satisfy the strings.length === exprs.length + 1 invariant.
|
|
881
1211
|
'``'(tag, ...parts) {
|
|
1212
|
+
const raw = staticStringExpr(['``', tag, ...parts])
|
|
1213
|
+
if (raw != null) return staticString(raw)
|
|
882
1214
|
const strs = [], exprs = []
|
|
883
|
-
let prev = false
|
|
884
1215
|
for (const p of parts) {
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
else { if (!prev) strs.push([null, '']); exprs.push(p); prev = false }
|
|
1216
|
+
if (Array.isArray(p) && p[0] == null && typeof p[1] === 'string') strs.push(p)
|
|
1217
|
+
else exprs.push(p)
|
|
888
1218
|
}
|
|
889
|
-
if (!prev) strs.push([null, ''])
|
|
890
1219
|
const arr = strs.length === 1 ? ['[]', strs[0]] : ['[]', [',', ...strs]]
|
|
891
1220
|
const callArgs = exprs.length === 0 ? arr : [',', arr, ...exprs]
|
|
892
1221
|
return prep(['()', tag, callArgs])
|
|
@@ -894,6 +1223,9 @@ const handlers = {
|
|
|
894
1223
|
|
|
895
1224
|
// Import
|
|
896
1225
|
'import'(fromNode) {
|
|
1226
|
+
// Bare side-effect: `import './sub.js'` → AST is ['import', [null, 'path']]
|
|
1227
|
+
if (Array.isArray(fromNode) && fromNode[0] == null && typeof fromNode[1] === 'string')
|
|
1228
|
+
return handlers['from'](null, fromNode)
|
|
897
1229
|
if (!Array.isArray(fromNode) || fromNode[0] !== 'from')
|
|
898
1230
|
return err('Dynamic import() not supported')
|
|
899
1231
|
return handlers['from'](fromNode[1], fromNode[2])
|
|
@@ -1010,9 +1342,13 @@ const handlers = {
|
|
|
1010
1342
|
err(`Unknown module '${mod}'. Provide it via { modules: { '${mod}': source } } or { imports: { '${mod}': {...} } }`)
|
|
1011
1343
|
},
|
|
1012
1344
|
|
|
1013
|
-
//
|
|
1014
|
-
|
|
1015
|
-
|
|
1345
|
+
// `===`/`!==` keep strict semantics (no coercion); emit folds a statically-known
|
|
1346
|
+
// type mismatch to false and otherwise shares the loose `==`/`!=` same-type path.
|
|
1347
|
+
// resolveTypeof still collapses `typeof x === 'type'` to a compile-time check.
|
|
1348
|
+
// Prep operands directly (not via `prep` on the node) so the strict op survives
|
|
1349
|
+
// to emit instead of re-dispatching this handler forever.
|
|
1350
|
+
'==='(a, b) { return prepStrictEq('===', a, b) },
|
|
1351
|
+
'!=='(a, b) { return prepStrictEq('!==', a, b) },
|
|
1016
1352
|
|
|
1017
1353
|
// Statements
|
|
1018
1354
|
';': (...stmts) => [';', ...stmts.map(prep).filter(x => x != null)],
|
|
@@ -1022,13 +1358,13 @@ const handlers = {
|
|
|
1022
1358
|
// Block-scoped control flow: push scope for bodies so inner let/const shadows correctly
|
|
1023
1359
|
'if': (cond, then, els) => {
|
|
1024
1360
|
const c = prep(cond)
|
|
1025
|
-
|
|
1026
|
-
if (els != null) {
|
|
1361
|
+
pushScope(); const t = prep(then); popScope()
|
|
1362
|
+
if (els != null) { pushScope(); const e = prep(els); popScope(); return ['if', c, t, e] }
|
|
1027
1363
|
return ['if', c, t]
|
|
1028
1364
|
},
|
|
1029
1365
|
'while': (cond, body) => {
|
|
1030
1366
|
const c = prep(cond)
|
|
1031
|
-
|
|
1367
|
+
pushScope(); const b = prep(body); popScope()
|
|
1032
1368
|
return ['while', c, b]
|
|
1033
1369
|
},
|
|
1034
1370
|
|
|
@@ -1111,7 +1447,7 @@ const handlers = {
|
|
|
1111
1447
|
for (const n of collectParamNames(raw)) fnScope.set(n, n)
|
|
1112
1448
|
|
|
1113
1449
|
depth++
|
|
1114
|
-
|
|
1450
|
+
pushScope(fnScope)
|
|
1115
1451
|
funcLocalNames.push(new Set(collectParamNames(raw)))
|
|
1116
1452
|
|
|
1117
1453
|
const nextParams = []
|
|
@@ -1133,6 +1469,14 @@ const handlers = {
|
|
|
1133
1469
|
}
|
|
1134
1470
|
}
|
|
1135
1471
|
let preparedBody = prep(body)
|
|
1472
|
+
// An expression-bodied arrow returning an empty object literal — `() => ({})`
|
|
1473
|
+
// — preps to a bare `['{}']`, structurally identical to an empty block body.
|
|
1474
|
+
// The grouping `()` that marked it an expression is unwrapped by then, so
|
|
1475
|
+
// wrap it in an explicit `return` — otherwise downstream block/expression
|
|
1476
|
+
// classification (compile.js `isBlockBody`) misreads it as an empty block.
|
|
1477
|
+
if (!(Array.isArray(body) && body[0] === '{}')
|
|
1478
|
+
&& Array.isArray(preparedBody) && preparedBody[0] === '{}' && preparedBody.length === 1)
|
|
1479
|
+
preparedBody = ['{}', [';', ['return', ['{}']]]]
|
|
1136
1480
|
if (bodyPrefix.length) {
|
|
1137
1481
|
const prefix = bodyPrefix.filter(x => x != null)
|
|
1138
1482
|
if (Array.isArray(preparedBody) && preparedBody[0] === '{}' && Array.isArray(preparedBody[1]) && preparedBody[1][0] === ';')
|
|
@@ -1144,7 +1488,7 @@ const handlers = {
|
|
|
1144
1488
|
}
|
|
1145
1489
|
const inner = nextParams.length === 0 ? null : nextParams.length === 1 ? nextParams[0] : [',', ...nextParams]
|
|
1146
1490
|
const result = ['=>', Array.isArray(params) && params[0] === '()' ? ['()', inner] : inner, preparedBody]
|
|
1147
|
-
|
|
1491
|
+
popScope()
|
|
1148
1492
|
funcLocalNames.pop()
|
|
1149
1493
|
depth--
|
|
1150
1494
|
return result
|
|
@@ -1176,6 +1520,8 @@ const handlers = {
|
|
|
1176
1520
|
return ['?.()', prep(callee), ...items.map(prep)]
|
|
1177
1521
|
},
|
|
1178
1522
|
// Boolean literals NaN-box as f64 — typeof at runtime returns 'number'. Fold here so the JS-spec value survives.
|
|
1523
|
+
// Unresolvable bare refs fold to 'undefined' via staticTypeofString (spec §13.5.3) —
|
|
1524
|
+
// the only place a stray identifier doesn't ReferenceError.
|
|
1179
1525
|
'typeof'(a) {
|
|
1180
1526
|
if (Array.isArray(a) && a[0] == null && typeof a[1] === 'boolean') { includeForStringOnly(); return ['str', 'boolean'] }
|
|
1181
1527
|
const known = staticTypeofString(a)
|
|
@@ -1191,7 +1537,17 @@ const handlers = {
|
|
|
1191
1537
|
includeForNumericCoercion()
|
|
1192
1538
|
return ['u+', na]
|
|
1193
1539
|
}
|
|
1194
|
-
|
|
1540
|
+
const pa = prep(a), pb = prep(b)
|
|
1541
|
+
// Compile-time fold of literal string concat. The combined bytes flow
|
|
1542
|
+
// through the `str` emitter as a single literal — SSO if ≤4 ASCII (zero
|
|
1543
|
+
// heap), otherwise one dataDedup entry (still cheaper than runtime
|
|
1544
|
+
// __str_concat_raw + heap alloc). Bottom-up, so `'a' + 'b' + 'c'` folds
|
|
1545
|
+
// left-associatively into one literal.
|
|
1546
|
+
if (Array.isArray(pa) && pa[0] === 'str' && typeof pa[1] === 'string' &&
|
|
1547
|
+
Array.isArray(pb) && pb[0] === 'str' && typeof pb[1] === 'string') {
|
|
1548
|
+
return ['str', pa[1] + pb[1]]
|
|
1549
|
+
}
|
|
1550
|
+
return ['+', pa, pb]
|
|
1195
1551
|
},
|
|
1196
1552
|
'-'(a, b) {
|
|
1197
1553
|
if (b === undefined) { const na = prep(a); return isLit(na) && typeof na[1] === 'number' ? [, -na[1]] : ['u-', na] }
|
|
@@ -1226,73 +1582,18 @@ const handlers = {
|
|
|
1226
1582
|
'()'(callee, ...args) {
|
|
1227
1583
|
// Grouping: (expr) → ['()', expr] with no args. Call: f() → ['()', 'f', null] with null arg.
|
|
1228
1584
|
if (args.length === 0) return prep(callee)
|
|
1585
|
+
if (typeof callee === 'string' && PROHIBITED[callee]) err(PROHIBITED[callee])
|
|
1229
1586
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
}
|
|
1587
|
+
// Compile-time folds: the callee names something resolvable now. Each fold
|
|
1588
|
+
// is gated by callee shape, so at most one of the three fires.
|
|
1589
|
+
const folded = foldImportMetaResolve(callee, args)
|
|
1590
|
+
?? dispatchConstructorCall(callee, args)
|
|
1591
|
+
?? foldNamespaceIntrospection(callee, args)
|
|
1592
|
+
if (folded !== undefined) return folded
|
|
1237
1593
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
if (typeof callee === 'string') {
|
|
1241
|
-
if (PROHIBITED[callee]) err(PROHIBITED[callee])
|
|
1242
|
-
if (CTORS.includes(callee)) return handlers['new'](['()', callee, ...args])
|
|
1243
|
-
|
|
1244
|
-
if (includeForNamedCall(callee)) {
|
|
1245
|
-
if (callee === 'BigInt64Array' || callee === 'BigUint64Array') {
|
|
1246
|
-
return ['()', callee, ...args.filter(a => a != null).map(prep)]
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
const local = scopes.length && isDeclared(callee)
|
|
1251
|
-
const resolved = local ? null : ctx.scope.chain[callee]
|
|
1252
|
-
if (local) callee = resolveScope(callee)
|
|
1253
|
-
else if (resolved?.includes('.')) callee = resolved
|
|
1254
|
-
else if (resolved && hasFunc(resolved)) callee = resolved
|
|
1255
|
-
else if (resolved && !resolved.includes('.')) {
|
|
1256
|
-
if (hasModule(resolved) && !ctx.module.imports.some(i => i[3]?.[1] === `$${resolved}`)) includeModule(resolved)
|
|
1257
|
-
}
|
|
1258
|
-
else if (depth > 0 && !resolved && !ctx.func.exports[callee] && !ctx.module.imports.some(i => i[3]?.[1] === `$${callee}`)) {
|
|
1259
|
-
includeForCallableValue()
|
|
1260
|
-
}
|
|
1261
|
-
} else if (Array.isArray(callee) && callee[0] === '.') {
|
|
1262
|
-
const [, obj, prop] = callee
|
|
1263
|
-
const key = typeof obj === 'string' && typeof prop === 'string' ? `${obj}.${prop}` : null
|
|
1264
|
-
if (key && ctx.module.hostImports?.[obj]?.[prop]) {
|
|
1265
|
-
const spec = ctx.module.hostImports[obj][prop]
|
|
1266
|
-
const alias = `${obj}$${prop}`
|
|
1267
|
-
addHostImport(obj, prop, alias, spec)
|
|
1268
|
-
callee = alias
|
|
1269
|
-
} else if (key && includeForNamedCall(key)) {
|
|
1270
|
-
callee = key
|
|
1271
|
-
} else if (includeForGenericMethod(prop)) {
|
|
1272
|
-
callee = prep(callee)
|
|
1273
|
-
} else {
|
|
1274
|
-
const mod = ctx.scope.chain[obj]
|
|
1275
|
-
if (typeof obj === 'string' && mod && !mod.includes('.') && hasModule(mod)) {
|
|
1276
|
-
callee = (includeModule(mod), mod + '.' + prop)
|
|
1277
|
-
} else {
|
|
1278
|
-
callee = prep(callee)
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
} else {
|
|
1282
|
-
includeForCallableValue()
|
|
1283
|
-
callee = prep(callee)
|
|
1284
|
-
}
|
|
1594
|
+
callee = resolveCallee(callee, args)
|
|
1595
|
+
args = renestSoleCommaArg(args)
|
|
1285
1596
|
|
|
1286
|
-
// Drop trailing-comma sentinel inside a comma group: `f(a, b,)` parses as
|
|
1287
|
-
// ['()', 'f', [',', a, b, null]] — without trimming, the trailing null
|
|
1288
|
-
// becomes a [, 0] literal and inflates arguments.length.
|
|
1289
|
-
if (args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',') {
|
|
1290
|
-
let end = args[0].length
|
|
1291
|
-
while (end > 1 && args[0][end - 1] == null) end--
|
|
1292
|
-
if (end < args[0].length) {
|
|
1293
|
-
args[0] = end === 2 ? args[0][1] : args[0].slice(0, end)
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
1597
|
const preppedArgs = args.filter(a => a != null).map(prep)
|
|
1297
1598
|
for (const a of preppedArgs) {
|
|
1298
1599
|
if (typeof a === 'string' && hasFunc(a)) {
|
|
@@ -1312,7 +1613,9 @@ const handlers = {
|
|
|
1312
1613
|
const inner = args[0]
|
|
1313
1614
|
includeForArrayLiteral()
|
|
1314
1615
|
if (inner == null) return ['[']
|
|
1315
|
-
|
|
1616
|
+
// jessie consumes the trailing comma itself; every remaining `null` in the
|
|
1617
|
+
// element list is a genuine elision (`[,]` → length 1, `[1,,]` → length 2).
|
|
1618
|
+
if (Array.isArray(inner) && inner[0] === ',') { const items = inner.slice(1); return ['[', ...items.map(item => item == null ? [, undefined] : prep(item))] }
|
|
1316
1619
|
return ['[', prep(inner)]
|
|
1317
1620
|
}
|
|
1318
1621
|
if (typeof args[0] === 'string' && ctx.module.namespaces?.[args[0]]) {
|
|
@@ -1332,9 +1635,9 @@ const handlers = {
|
|
|
1332
1635
|
|
|
1333
1636
|
// Bare block statement: push scope for let/const shadowing
|
|
1334
1637
|
'{'(inner) {
|
|
1335
|
-
|
|
1638
|
+
pushScope()
|
|
1336
1639
|
const result = ['{', prep(inner)]
|
|
1337
|
-
|
|
1640
|
+
popScope()
|
|
1338
1641
|
return result
|
|
1339
1642
|
},
|
|
1340
1643
|
|
|
@@ -1343,14 +1646,44 @@ const handlers = {
|
|
|
1343
1646
|
// Detect block body vs object literal
|
|
1344
1647
|
if (Array.isArray(inner) && STMT_OPS.has(inner[0])) {
|
|
1345
1648
|
// Block body: push block scope for let/const shadowing
|
|
1346
|
-
|
|
1649
|
+
pushScope()
|
|
1347
1650
|
const result = ['{}', prep(inner)]
|
|
1348
|
-
|
|
1651
|
+
popScope()
|
|
1349
1652
|
return result
|
|
1350
1653
|
}
|
|
1351
1654
|
|
|
1352
1655
|
includeForObjectLiteral()
|
|
1353
1656
|
if (inner == null) return ['{}']
|
|
1657
|
+
const items = Array.isArray(inner) && inner[0] === ','
|
|
1658
|
+
? inner.slice(1)
|
|
1659
|
+
: [inner]
|
|
1660
|
+
|
|
1661
|
+
// Computed keys: `{[k]: v}` where `k` isn't compile-time foldable. jz's
|
|
1662
|
+
// object layout is slot-based (fixed schema at the literal site), so a
|
|
1663
|
+
// truly-dynamic key can't slot in. Lower to the existing dict path:
|
|
1664
|
+
// {a:1, [k]:v, b:2} → ((__t) => (__t[k]=v, __t))({a:1, b:2})
|
|
1665
|
+
// Static-but-non-string keys still fold via `staticPropertyKey` below.
|
|
1666
|
+
const isComputed = p => Array.isArray(p) && p[0] === ':'
|
|
1667
|
+
&& typeof p[1] !== 'string' && staticPropertyKey(p[1]) == null
|
|
1668
|
+
if (items.some(isComputed)) {
|
|
1669
|
+
const staticItems = items.filter(p => !isComputed(p))
|
|
1670
|
+
const computedItems = items.filter(isComputed)
|
|
1671
|
+
const tmp = `${T}o${ctx.func.uniq++}`
|
|
1672
|
+
// Body: comma sequence of dict-sets, terminated with the tmp itself.
|
|
1673
|
+
// Computed key shape from parser is `[':', ['[]', keyExpr], valExpr]` —
|
|
1674
|
+
// unwrap the `['[]', keyExpr]` to grab keyExpr directly.
|
|
1675
|
+
const assigns = computedItems.map(p => {
|
|
1676
|
+
const keyExpr = Array.isArray(p[1]) && p[1][0] === '[]' ? p[1][1] : p[1]
|
|
1677
|
+
return ['=', ['[]', tmp, keyExpr], p[2]]
|
|
1678
|
+
})
|
|
1679
|
+
const body = [',', ...assigns, tmp]
|
|
1680
|
+
const arrow = ['=>', ['()', tmp], body]
|
|
1681
|
+
const arg = staticItems.length === 1 ? ['{}', staticItems[0]]
|
|
1682
|
+
: staticItems.length ? ['{}', [',', ...staticItems]]
|
|
1683
|
+
: ['{}']
|
|
1684
|
+
return prep(['()', arrow, arg])
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1354
1687
|
// Process properties: shorthand 'x' → [':', 'x', 'x'], or [':', key, val] → prep val only
|
|
1355
1688
|
const prop = p => {
|
|
1356
1689
|
if (typeof p === 'string') return [':', p, prep(p)]
|
|
@@ -1361,13 +1694,6 @@ const handlers = {
|
|
|
1361
1694
|
}
|
|
1362
1695
|
return prep(p)
|
|
1363
1696
|
}
|
|
1364
|
-
// Drop trailing-comma artifacts: subscript represents `{a:1, b,}` as
|
|
1365
|
-
// `[",", [":","a",1], "b", null]` — the trailing `null` would prep to a
|
|
1366
|
-
// literal-0 entry, leaving the literal carrying a phantom slot and
|
|
1367
|
-
// shifting any subsequent slot-position resolution.
|
|
1368
|
-
const items = Array.isArray(inner) && inner[0] === ','
|
|
1369
|
-
? inner.slice(1).filter(p => p != null)
|
|
1370
|
-
: [inner]
|
|
1371
1697
|
let prepped = items.map(prop)
|
|
1372
1698
|
// ES spec: duplicate keys allowed; key takes first-seen position, last-seen value.
|
|
1373
1699
|
const lastValue = new Map()
|
|
@@ -1391,7 +1717,7 @@ const handlers = {
|
|
|
1391
1717
|
|
|
1392
1718
|
// For loop
|
|
1393
1719
|
'for'(head, body) {
|
|
1394
|
-
|
|
1720
|
+
pushScope()
|
|
1395
1721
|
let r
|
|
1396
1722
|
if (Array.isArray(head) && head[0] === ';') {
|
|
1397
1723
|
let [, init, cond, step] = head
|
|
@@ -1444,30 +1770,54 @@ const handlers = {
|
|
|
1444
1770
|
if (sid != null) {
|
|
1445
1771
|
// Known schema → compile-time unrolling with string keys
|
|
1446
1772
|
const keys = ctx.schema.list[sid]
|
|
1447
|
-
if (!keys || !keys.length) {
|
|
1773
|
+
if (!keys || !keys.length) { popScope(); return null }
|
|
1448
1774
|
includeForKnownKeyIteration()
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
stmts
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1775
|
+
if (!hasLoopJump(body)) {
|
|
1776
|
+
// No break/continue → flat unroll, no loop frame needed.
|
|
1777
|
+
const stmts = []
|
|
1778
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1779
|
+
stmts.push(i === 0
|
|
1780
|
+
? ['let', ['=', varName, [, keys[i]]]]
|
|
1781
|
+
: ['=', varName, [, keys[i]]])
|
|
1782
|
+
stmts.push(cloneNode(body))
|
|
1783
|
+
}
|
|
1784
|
+
r = prep([';', ...stmts])
|
|
1785
|
+
} else {
|
|
1786
|
+
// break/continue present → an unrolled loop still needs its frames.
|
|
1787
|
+
// Wrap each iteration in a labeled block (continue target) and the
|
|
1788
|
+
// whole run in an outer labeled block (break target): `break` exits
|
|
1789
|
+
// the construct, `continue` falls through to the next iteration.
|
|
1790
|
+
const brkL = `${T}fibrk${ctx.func.uniq++}`
|
|
1791
|
+
const decl = prep(['let', ['=', varName, [, keys[0]]]])
|
|
1792
|
+
const parts = [decl]
|
|
1793
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1794
|
+
const contL = `${T}ficont${ctx.func.uniq++}`
|
|
1795
|
+
const iter = prep(i === 0
|
|
1796
|
+
? cloneNode(body)
|
|
1797
|
+
: [';', ['=', varName, [, keys[i]]], cloneNode(body)])
|
|
1798
|
+
parts.push(['label', contL, retargetLoopJumps(iter, brkL, contL)])
|
|
1799
|
+
}
|
|
1800
|
+
r = ['label', brkL, [';', ...parts]]
|
|
1455
1801
|
}
|
|
1456
|
-
r = prep([';', ...stmts])
|
|
1457
1802
|
} else {
|
|
1458
1803
|
// Dynamic object → HASH runtime iteration
|
|
1459
1804
|
includeForRuntimeKeyIteration()
|
|
1460
1805
|
r = ['for-in', varName, prep(src), prep(body)]
|
|
1461
1806
|
}
|
|
1462
1807
|
} else {
|
|
1463
|
-
|
|
1808
|
+
// Some parser/jzify shapes for `for (;;)` and `for (; cond; )` arrive
|
|
1809
|
+
// as a null or bare-condition head instead of the canonical
|
|
1810
|
+
// `[';', init, cond, step]` tuple. Normalize them before emit so they
|
|
1811
|
+
// remain ordinary for-loops, not malformed two-slot nodes.
|
|
1812
|
+
r = ['for', null, head == null ? null : prep(head), null, prep(body)]
|
|
1464
1813
|
}
|
|
1465
|
-
|
|
1814
|
+
popScope()
|
|
1466
1815
|
return r
|
|
1467
1816
|
},
|
|
1468
1817
|
|
|
1469
1818
|
// Property access - resolve namespaces or object/array properties
|
|
1470
1819
|
'.'(obj, prop) {
|
|
1820
|
+
prop = typeof prop === 'string' ? prop : staticPropertyKey(prop)
|
|
1471
1821
|
if (prop === 'caller' || prop === 'callee') err('`.caller` and `.callee` are prohibited: deprecated stack introspection')
|
|
1472
1822
|
if (prop === 'url' && isImportMeta(obj)) return staticString(importMetaUrl())
|
|
1473
1823
|
const mod = ctx.scope.chain[obj]
|
|
@@ -1505,6 +1855,20 @@ const handlers = {
|
|
|
1505
1855
|
}
|
|
1506
1856
|
}
|
|
1507
1857
|
|
|
1858
|
+
// `new RegExp("pattern", "flags?")` with string-literal pattern → compile
|
|
1859
|
+
// like a regex literal `/pattern/flags`. Dynamic pattern is not supported
|
|
1860
|
+
// (would require a runtime regex interpreter). Reported as build blocker #6.
|
|
1861
|
+
if (name === 'RegExp') {
|
|
1862
|
+
const literalArgs = ctorArgs.filter(a => a != null)
|
|
1863
|
+
const pattern = staticStringExpr(literalArgs[0])
|
|
1864
|
+
if (pattern == null)
|
|
1865
|
+
err('new RegExp() requires a string-literal pattern; dynamic regex construction is not supported')
|
|
1866
|
+
const flags = literalArgs.length > 1 ? staticStringExpr(literalArgs[1]) : ''
|
|
1867
|
+
if (flags == null)
|
|
1868
|
+
err('new RegExp() flags must be a string literal')
|
|
1869
|
+
return prep(['//', pattern, flags || undefined])
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1508
1872
|
// Wrap multi-arg ctor arg lists back into a single comma-group — the '()' op
|
|
1509
1873
|
// expects callArgs as a single element (possibly comma-grouped).
|
|
1510
1874
|
const wrapArgs = (args) => args.length === 0 ? [null]
|
|
@@ -1672,7 +2036,7 @@ function prepareModule(specifier, source) {
|
|
|
1672
2036
|
const mangled = `${prefix}$${localName}`
|
|
1673
2037
|
moduleExports.set(exportName, mangled)
|
|
1674
2038
|
const func = ctx.func.list.find(f => f.name === localName)
|
|
1675
|
-
if (func) renameFunc(func, mangled)
|
|
2039
|
+
if (func) { renameFunc(func, mangled); func._modulePrefix = prefix }
|
|
1676
2040
|
if (ctx.scope.globals.has(localName)) {
|
|
1677
2041
|
const wat = ctx.scope.globals.get(localName).replace(`$${localName}`, `$${mangled}`)
|
|
1678
2042
|
ctx.scope.globals.delete(localName)
|
|
@@ -1734,15 +2098,17 @@ function prepareModule(specifier, source) {
|
|
|
1734
2098
|
}
|
|
1735
2099
|
|
|
1736
2100
|
// Rename ALL non-exported functions created during this module's prep
|
|
1737
|
-
// (fn property assignments like f32.parse, internal helpers like cleanInt)
|
|
2101
|
+
// (fn property assignments like f32.parse, internal helpers like cleanInt).
|
|
2102
|
+
// Funcs added by nested prepareModule calls are tagged with `_modulePrefix`
|
|
2103
|
+
// by their own pass; skip those so prefixes don't stack (`a$b$name`).
|
|
1738
2104
|
for (let i = savedFuncCount; i < ctx.func.list.length; i++) {
|
|
1739
2105
|
const func = ctx.func.list[i]
|
|
1740
2106
|
if (func.raw || func.name.startsWith(prefix + '$')) continue
|
|
1741
|
-
|
|
1742
|
-
if (func.name.includes('__') && func.name.includes('$')) continue
|
|
2107
|
+
if (func._modulePrefix && func._modulePrefix !== prefix) continue
|
|
1743
2108
|
const mangled = `${prefix}$${func.name}`
|
|
1744
2109
|
moduleExports.set(func.name, mangled)
|
|
1745
2110
|
renameFunc(func, mangled)
|
|
2111
|
+
func._modulePrefix = prefix
|
|
1746
2112
|
}
|
|
1747
2113
|
|
|
1748
2114
|
// Add mangled non-exported globals to moduleExports for walk renaming
|
|
@@ -1774,6 +2140,8 @@ function prepareModule(specifier, source) {
|
|
|
1774
2140
|
for (let i = savedFuncCount; i < ctx.func.list.length; i++) {
|
|
1775
2141
|
const func = ctx.func.list[i]
|
|
1776
2142
|
if (!func.body) continue
|
|
2143
|
+
// Sub-module funcs already had their own walk; parent's rename map doesn't apply.
|
|
2144
|
+
if (func._modulePrefix && func._modulePrefix !== prefix) continue
|
|
1777
2145
|
const funcParams = new Set(func.sig?.params?.map(p => p.name) || [])
|
|
1778
2146
|
walk(func.body, funcParams)
|
|
1779
2147
|
if (func.defaults) for (const [k, v] of Object.entries(func.defaults)) func.defaults[k] = walk(v, funcParams)
|
|
@@ -1799,3 +2167,160 @@ function prepareModule(specifier, source) {
|
|
|
1799
2167
|
ctx.module.resolvedModules.set(specifier, result)
|
|
1800
2168
|
return result
|
|
1801
2169
|
}
|
|
2170
|
+
|
|
2171
|
+
// =============================================================================
|
|
2172
|
+
// AST-level fusion passes (pre-resolution)
|
|
2173
|
+
// =============================================================================
|
|
2174
|
+
// Unlike src/optimize.js (a pure WAT IR→IR rewrite, post-emission), these
|
|
2175
|
+
// rewrites need the *raw, pre-resolution* AST shape — bindings still named,
|
|
2176
|
+
// arrow bodies still inline — so they run inside prepare(), before scope
|
|
2177
|
+
// resolution and emit. They mutate the AST in place; shape guards are strict
|
|
2178
|
+
// enough that misfires are impossible.
|
|
2179
|
+
|
|
2180
|
+
/** Sparse-read .map fusion: rewrite `const b = a.map(arrow); for(...; j<b.length; ...) USE(b[j])`
|
|
2181
|
+
* into a fused for-loop that inlines `arrow(a[j])` at the read site, eliminating the materialized
|
|
2182
|
+
* intermediate array. Only fires on shapes where every use of `b` is a numeric `b[idx]` read or a
|
|
2183
|
+
* `b.length` read, the arrow is pure with a single named param, and `b` is not referenced after the
|
|
2184
|
+
* consumer for-loop. Preserves observable behavior because the arrow's pure-expression body has no
|
|
2185
|
+
* order-dependent effects. */
|
|
2186
|
+
function fuseSparseMapReads(root) {
|
|
2187
|
+
walkSparse(root)
|
|
2188
|
+
}
|
|
2189
|
+
function walkSparse(node) {
|
|
2190
|
+
if (!Array.isArray(node)) return
|
|
2191
|
+
for (let i = 1; i < node.length; i++) walkSparse(node[i])
|
|
2192
|
+
if (node[0] === ';') tryFuseInBlock(node)
|
|
2193
|
+
}
|
|
2194
|
+
function tryFuseInBlock(seq) {
|
|
2195
|
+
for (let i = 1; i < seq.length - 1; i++) {
|
|
2196
|
+
const fused = tryFusePair(seq[i], seq[i + 1], seq, i)
|
|
2197
|
+
if (fused) {
|
|
2198
|
+
seq.splice(i, 2, ...fused)
|
|
2199
|
+
i-- // re-examine same position (chained fusions)
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
function tryFusePair(decl, forNode, seq, declIdx) {
|
|
2204
|
+
if (!Array.isArray(decl) || (decl[0] !== 'const' && decl[0] !== 'let')) return null
|
|
2205
|
+
if (decl.length !== 2) return null // single binding only
|
|
2206
|
+
const bind = decl[1]
|
|
2207
|
+
if (!Array.isArray(bind) || bind[0] !== '=' || typeof bind[1] !== 'string') return null
|
|
2208
|
+
const NAME = bind[1], rhs = bind[2]
|
|
2209
|
+
if (!Array.isArray(rhs) || rhs[0] !== '()') return null
|
|
2210
|
+
const callee = rhs[1]
|
|
2211
|
+
if (!Array.isArray(callee) || callee[0] !== '.' || callee[2] !== 'map') return null
|
|
2212
|
+
const RECV = callee[1]
|
|
2213
|
+
if (typeof RECV !== 'string' || RECV === NAME) return null
|
|
2214
|
+
const arrow = rhs[2]
|
|
2215
|
+
if (!Array.isArray(arrow) || arrow[0] !== '=>') return null
|
|
2216
|
+
// Single-name param only: `x => …` or `(x) => …`
|
|
2217
|
+
const ap = arrow[1]
|
|
2218
|
+
const PARAM = typeof ap === 'string' ? ap :
|
|
2219
|
+
(Array.isArray(ap) && ap[0] === '()' && typeof ap[1] === 'string' ? ap[1] : null)
|
|
2220
|
+
if (!PARAM || PARAM === NAME || PARAM === RECV) return null
|
|
2221
|
+
// Body: single-expression arrow only (block bodies skipped — could extend later).
|
|
2222
|
+
const aBody = arrow[2]
|
|
2223
|
+
if (Array.isArray(aBody) && aBody[0] === '{}') return null
|
|
2224
|
+
if (!isPureSparseArrowBody(aBody, PARAM)) return null
|
|
2225
|
+
// For-loop: ['for', [';', initStmt, cond, inc], body]
|
|
2226
|
+
if (!Array.isArray(forNode) || forNode[0] !== 'for' || forNode.length !== 3) return null
|
|
2227
|
+
const head = forNode[1]
|
|
2228
|
+
if (!Array.isArray(head) || head[0] !== ';' || head.length !== 4) return null
|
|
2229
|
+
const cond = head[2], forBody = forNode[2]
|
|
2230
|
+
// Verify `NAME` is used only as `NAME[idx]` or `NAME.length` inside cond+forBody.
|
|
2231
|
+
if (!hasOnlySparseUses(cond, NAME)) return null
|
|
2232
|
+
if (!hasOnlySparseUses(forBody, NAME)) return null
|
|
2233
|
+
if (!hasAnyIndexedRead(forBody, NAME) && !hasAnyIndexedRead(cond, NAME)) return null
|
|
2234
|
+
// `NAME` must not be read after the for-loop in the same block.
|
|
2235
|
+
for (let k = declIdx + 2; k < seq.length; k++) {
|
|
2236
|
+
if (refsName(seq[k], NAME)) return null
|
|
2237
|
+
}
|
|
2238
|
+
// RECV must not be reassigned inside the for-loop (would invalidate substitution).
|
|
2239
|
+
if (assignsName(forNode, RECV) || assignsName(forNode, NAME)) return null
|
|
2240
|
+
// PARAM must not collide with any binding inside forBody (otherwise substitution shadows wrongly).
|
|
2241
|
+
if (bindsName(forNode, PARAM)) return null
|
|
2242
|
+
// Apply substitution: NAME.length → RECV.length; NAME[idx] → arrowBody[PARAM ← RECV[idx]].
|
|
2243
|
+
const newCond = substSparse(cond, NAME, RECV, PARAM, aBody)
|
|
2244
|
+
const newBody = substSparse(forBody, NAME, RECV, PARAM, aBody)
|
|
2245
|
+
const newHead = [';', head[1], newCond, head[3]]
|
|
2246
|
+
return [['for', newHead, newBody]]
|
|
2247
|
+
}
|
|
2248
|
+
function isPureSparseArrowBody(n, PARAM) {
|
|
2249
|
+
if (typeof n === 'string') return true
|
|
2250
|
+
if (!Array.isArray(n)) return true
|
|
2251
|
+
const op = n[0]
|
|
2252
|
+
// Calls / new / assignments / increments are unsafe for repeated-substitution semantics.
|
|
2253
|
+
if (op === '()' || op === '?.()' || op === 'new' || op === '++' || op === '--') return false
|
|
2254
|
+
if (op === '=>') return false // nested closure is opaque
|
|
2255
|
+
if (typeof op === 'string' && op !== '=>' && op !== '===' && op !== '!==' && op !== '==' && op !== '!=' && op !== '<=' && op !== '>=' && op.endsWith('=') && op !== '=') return false
|
|
2256
|
+
if (op === '=') return false
|
|
2257
|
+
for (let i = 1; i < n.length; i++) if (!isPureSparseArrowBody(n[i], PARAM)) return false
|
|
2258
|
+
return true
|
|
2259
|
+
}
|
|
2260
|
+
function hasOnlySparseUses(n, NAME) {
|
|
2261
|
+
if (typeof n === 'string') return n !== NAME
|
|
2262
|
+
if (!Array.isArray(n)) return true
|
|
2263
|
+
const op = n[0]
|
|
2264
|
+
if (op === '[]' && n.length === 3 && n[1] === NAME) return hasOnlySparseUses(n[2], NAME) // NAME[idx] — idx must not reference NAME
|
|
2265
|
+
if (op === '.' && n[1] === NAME) {
|
|
2266
|
+
if (n[2] === 'length') return true
|
|
2267
|
+
return false // any other property access on NAME is opaque
|
|
2268
|
+
}
|
|
2269
|
+
for (let i = 1; i < n.length; i++) if (!hasOnlySparseUses(n[i], NAME)) return false
|
|
2270
|
+
return true
|
|
2271
|
+
}
|
|
2272
|
+
function hasAnyIndexedRead(n, NAME) {
|
|
2273
|
+
if (!Array.isArray(n)) return false
|
|
2274
|
+
if (n[0] === '[]' && n.length === 3 && n[1] === NAME) return true
|
|
2275
|
+
for (let i = 1; i < n.length; i++) if (hasAnyIndexedRead(n[i], NAME)) return true
|
|
2276
|
+
return false
|
|
2277
|
+
}
|
|
2278
|
+
function refsName(n, NAME) {
|
|
2279
|
+
if (typeof n === 'string') return n === NAME
|
|
2280
|
+
if (!Array.isArray(n)) return false
|
|
2281
|
+
for (let i = 1; i < n.length; i++) if (refsName(n[i], NAME)) return true
|
|
2282
|
+
return false
|
|
2283
|
+
}
|
|
2284
|
+
function assignsName(n, NAME) {
|
|
2285
|
+
if (!Array.isArray(n)) return false
|
|
2286
|
+
const op = n[0]
|
|
2287
|
+
if ((op === '=' || op === '++' || op === '--' ||
|
|
2288
|
+
(typeof op === 'string' && op.endsWith('=') && op !== '==' && op !== '===' && op !== '!=' && op !== '!==' && op !== '<=' && op !== '>='))
|
|
2289
|
+
&& n[1] === NAME) return true
|
|
2290
|
+
for (let i = 1; i < n.length; i++) if (assignsName(n[i], NAME)) return true
|
|
2291
|
+
return false
|
|
2292
|
+
}
|
|
2293
|
+
function bindsName(n, NAME) {
|
|
2294
|
+
if (!Array.isArray(n)) return false
|
|
2295
|
+
const op = n[0]
|
|
2296
|
+
if ((op === 'let' || op === 'const')) {
|
|
2297
|
+
for (let i = 1; i < n.length; i++) {
|
|
2298
|
+
const bind = n[i]
|
|
2299
|
+
if (Array.isArray(bind) && bind[0] === '=' && bind[1] === NAME) return true
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
if (op === '=>') {
|
|
2303
|
+
const p = n[1]
|
|
2304
|
+
if (p === NAME) return true
|
|
2305
|
+
if (Array.isArray(p)) {
|
|
2306
|
+
if (p[0] === '()' && p[1] === NAME) return true
|
|
2307
|
+
// skip deeper destructuring forms — conservative
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
for (let i = 1; i < n.length; i++) if (bindsName(n[i], NAME)) return true
|
|
2311
|
+
return false
|
|
2312
|
+
}
|
|
2313
|
+
function substSparse(n, NAME, RECV, PARAM, arrowBody) {
|
|
2314
|
+
if (typeof n !== 'object' || n === null || !Array.isArray(n)) return n
|
|
2315
|
+
if (n[0] === '.' && n[1] === NAME && n[2] === 'length') return ['.', RECV, 'length']
|
|
2316
|
+
if (n[0] === '[]' && n.length === 3 && n[1] === NAME) {
|
|
2317
|
+
const idx = substSparse(n[2], NAME, RECV, PARAM, arrowBody)
|
|
2318
|
+
return cloneAndBind(arrowBody, PARAM, ['[]', RECV, idx])
|
|
2319
|
+
}
|
|
2320
|
+
return n.map((c, i) => i === 0 ? c : substSparse(c, NAME, RECV, PARAM, arrowBody))
|
|
2321
|
+
}
|
|
2322
|
+
function cloneAndBind(node, PARAM, replacement) {
|
|
2323
|
+
if (node === PARAM) return replacement
|
|
2324
|
+
if (!Array.isArray(node)) return node
|
|
2325
|
+
return node.map((c, i) => i === 0 ? c : cloneAndBind(c, PARAM, replacement))
|
|
2326
|
+
}
|