jz 0.6.0 → 0.8.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 +99 -72
- package/bench/README.md +253 -100
- package/bench/bench.svg +58 -81
- package/cli.js +85 -12
- package/dist/interop.js +1 -0
- package/dist/jz.js +6196 -0
- package/index.d.ts +126 -0
- package/index.js +222 -35
- package/interop.js +240 -141
- package/layout.js +34 -18
- package/module/array.js +99 -111
- package/module/collection.js +124 -30
- package/module/console.js +1 -1
- package/module/core.js +163 -19
- package/module/json.js +3 -3
- package/module/math.js +162 -3
- package/module/number.js +268 -13
- package/module/object.js +37 -5
- package/module/regex.js +8 -7
- package/module/simd.js +37 -5
- package/module/string.js +203 -168
- package/module/typedarray.js +565 -112
- package/package.json +26 -7
- package/src/abi/string.js +29 -29
- package/src/ast.js +19 -2
- package/src/autoload.js +3 -0
- package/src/compile/analyze-scans.js +174 -11
- package/src/compile/analyze.js +101 -4
- package/src/compile/cse-load.js +200 -0
- package/src/compile/emit-assign.js +82 -20
- package/src/compile/emit.js +592 -51
- package/src/compile/index.js +504 -89
- package/src/compile/infer.js +36 -1
- package/src/compile/loop-divmod.js +109 -0
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +275 -41
- package/src/compile/peel-stencil.js +215 -0
- package/src/compile/plan/advise.js +55 -1
- package/src/compile/plan/common.js +29 -0
- package/src/compile/plan/index.js +8 -1
- package/src/compile/plan/inline.js +180 -22
- package/src/compile/plan/literals.js +313 -24
- package/src/compile/plan/scope.js +115 -39
- package/src/compile/program-facts.js +21 -2
- package/src/ctx.js +96 -19
- package/src/helper-counters.js +137 -0
- package/src/ir.js +157 -20
- package/src/kind-traits.js +34 -3
- package/src/kind.js +79 -4
- package/src/op-policy.js +5 -2
- package/src/ops.js +119 -0
- package/src/optimize/index.js +1274 -151
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +4187 -253
- package/src/prepare/index.js +75 -14
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +6 -2
- package/src/static.js +9 -0
- package/src/type.js +63 -51
- package/src/wat/assemble.js +286 -21
- package/src/widen.js +21 -0
- package/src/wat/optimize.js +0 -3760
package/src/prepare/index.js
CHANGED
|
@@ -44,7 +44,7 @@ import { recordGlobalRep } from '../compile/infer.js'
|
|
|
44
44
|
import { isFuncRef } from '../ir.js'
|
|
45
45
|
import {
|
|
46
46
|
CTORS, COLLECTION_CTORS, TIMER_NAMES,
|
|
47
|
-
hasModule, includeModule,
|
|
47
|
+
hasModule, includeModule, includeMods,
|
|
48
48
|
includeForArrayAccess, includeForArrayLiteral, includeForArrayPattern, includeForCallableValue,
|
|
49
49
|
includeForGenericMethod, includeForKnownKeyIteration, includeForNamedCall, includeForNumericCoercion,
|
|
50
50
|
includeForObjectLiteral, includeForObjectPattern, includeForOp, includeForProperty, includeForRuntimeCtor,
|
|
@@ -169,7 +169,7 @@ const addHostImport = (mod, name, alias, spec) => {
|
|
|
169
169
|
// ABI — record it so references fold to an f64 literal (see prep's identifier resolution) instead
|
|
170
170
|
// of emitting a 0-arg func import that can't be read as a value ("'PI' is not in scope").
|
|
171
171
|
if (typeof spec === 'number') {
|
|
172
|
-
if (!ctx.scope.hostConsts) ctx.scope.hostConsts =
|
|
172
|
+
if (!ctx.scope.hostConsts) ctx.scope.hostConsts = Object.create(null) // name-keyed: prototype-less (see derive)
|
|
173
173
|
ctx.scope.hostConsts[alias] = spec
|
|
174
174
|
return
|
|
175
175
|
}
|
|
@@ -321,6 +321,30 @@ function lookupStaticStringArray(name) {
|
|
|
321
321
|
return ctx.scope.shapeStrArrays?.get(resolved) ?? null
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
/** Evaluate a constant numeric expression (number literals + basic arithmetic) for
|
|
325
|
+
* compile-time string/template folding. Returns null when it isn't a pure-number
|
|
326
|
+
* constant — string `+` and dynamic parts fall through to the caller's runtime path. */
|
|
327
|
+
function constNum(node) {
|
|
328
|
+
if (Array.isArray(node) && node[0] == null && typeof node[1] === 'number') return node[1]
|
|
329
|
+
if (!Array.isArray(node)) return null
|
|
330
|
+
const [op, a, b] = node
|
|
331
|
+
if ((op === 'u-' || op === '-' || op === '+') && b === undefined) {
|
|
332
|
+
const x = constNum(a)
|
|
333
|
+
return x == null ? null : op === 'u-' || op === '-' ? -x : +x
|
|
334
|
+
}
|
|
335
|
+
const x = constNum(a), y = constNum(b)
|
|
336
|
+
if (x == null || y == null) return null
|
|
337
|
+
switch (op) {
|
|
338
|
+
case '+': return x + y
|
|
339
|
+
case '-': return x - y
|
|
340
|
+
case '*': return x * y
|
|
341
|
+
case '/': return y === 0 ? null : x / y
|
|
342
|
+
case '%': return y === 0 ? null : x % y
|
|
343
|
+
case '**': return x ** y
|
|
344
|
+
}
|
|
345
|
+
return null
|
|
346
|
+
}
|
|
347
|
+
|
|
324
348
|
function staticStringExpr(node) {
|
|
325
349
|
const lit = stringValue(node)
|
|
326
350
|
if (lit != null) return lit
|
|
@@ -341,7 +365,11 @@ function staticStringExpr(node) {
|
|
|
341
365
|
if (op === '`') {
|
|
342
366
|
let out = ''
|
|
343
367
|
for (const part of args) {
|
|
344
|
-
|
|
368
|
+
let s = staticStringExpr(part)
|
|
369
|
+
// A numeric interpolation (`${123}`, `${1+2}`) is a constant in string context —
|
|
370
|
+
// ToString it so a fully-static template folds to one literal instead of a runtime
|
|
371
|
+
// concat. (Only the template case stringifies numbers; `+` stays polymorphic.)
|
|
372
|
+
if (s == null) { const n = constNum(part); if (n != null) s = String(n) }
|
|
345
373
|
if (s == null) return null
|
|
346
374
|
out += s
|
|
347
375
|
}
|
|
@@ -383,7 +411,7 @@ function resolveImportMeta(spec) {
|
|
|
383
411
|
|
|
384
412
|
function recordModuleInitFacts(root) {
|
|
385
413
|
const facts = ctx.module.initFacts ||= {
|
|
386
|
-
dynVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
|
|
414
|
+
dynVars: new Set(), dynWriteVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
|
|
387
415
|
hasFuncValue: false, timerNames: new Set(),
|
|
388
416
|
maxDef: 0, maxCall: 0, hasRest: false, hasSpread: false,
|
|
389
417
|
writtenProps: new Set(),
|
|
@@ -458,6 +486,11 @@ export default function prepare(node) {
|
|
|
458
486
|
// import would cycle (autoload imports every module via module/index.js).
|
|
459
487
|
ctx.module.include = includeModule
|
|
460
488
|
includeModule('core')
|
|
489
|
+
// Empty or whitespace-only source parses to a bare '' — an empty program, not an
|
|
490
|
+
// identifier reference. Normalize to an empty statement so it compiles to a bare
|
|
491
|
+
// `(module)` instead of a `(local.get $)` against a zero-length name. (A non-empty
|
|
492
|
+
// bare identifier like `foo` parses to `'foo'` and stays a real reference.)
|
|
493
|
+
if (node === '') node = [';']
|
|
461
494
|
validateCoalesceMixing(node) // ES2020: reject unparenthesized `??` mixed with `||`/`&&`
|
|
462
495
|
normalizeIdents(node)
|
|
463
496
|
fuseSparseMapReads(node) // AST-level fusion; needs pre-resolution shape — defined at end of file
|
|
@@ -557,9 +590,13 @@ export default function prepare(node) {
|
|
|
557
590
|
|
|
558
591
|
// Named constants → numeric literals. The JZ_NULL/JZ_UNDEF atom sentinels live
|
|
559
592
|
// in ast.js — shared with emit without crossing the prepare↔compile boundary.
|
|
560
|
-
|
|
593
|
+
// Prototype-less (Object.create(null)): a plain `{}` inherits Object.prototype in V8, so
|
|
594
|
+
// `'valueOf' in CONSTANTS` / `CONSTANTS['toString']` would hit an inherited method and
|
|
595
|
+
// mis-resolve a user identifier named like an Object method (jz.js-only — kernel objects
|
|
596
|
+
// are already prototype-less). Same reason on F64_CONSTANTS / GLOBALS / REJECT_IDENTS.
|
|
597
|
+
const CONSTANTS = Object.assign(Object.create(null), { 'true': true, 'false': false, 'null': JZ_NULL, 'undefined': JZ_UNDEF })
|
|
561
598
|
// NaN/Infinity stay as special f64 values in emit()
|
|
562
|
-
const F64_CONSTANTS = { 'NaN': NaN, 'Infinity': Infinity }
|
|
599
|
+
const F64_CONSTANTS = Object.assign(Object.create(null), { 'NaN': NaN, 'Infinity': Infinity })
|
|
563
600
|
|
|
564
601
|
/** Resolve variable name through block scope chain (innermost rename wins). */
|
|
565
602
|
function resolveScope(name) {
|
|
@@ -815,7 +852,7 @@ function prep(node) {
|
|
|
815
852
|
// Not actually "implicit imports" — these are ambient globals that exist in every jz/JS
|
|
816
853
|
// program (they do not live in any module). jzify auto-injecting imports would still
|
|
817
854
|
// need a list of these names to know what to emit, so the table lives here either way.
|
|
818
|
-
export const GLOBALS = {
|
|
855
|
+
export const GLOBALS = Object.assign(Object.create(null), {
|
|
819
856
|
Math: 'math',
|
|
820
857
|
Number: 'Number',
|
|
821
858
|
Array: 'Array',
|
|
@@ -845,7 +882,7 @@ export const GLOBALS = {
|
|
|
845
882
|
BigInt: 'BigInt',
|
|
846
883
|
TextEncoder: 'TextEncoder',
|
|
847
884
|
TextDecoder: 'TextDecoder',
|
|
848
|
-
}
|
|
885
|
+
})
|
|
849
886
|
|
|
850
887
|
const patternItems = (node) => node?.[0] === ',' ? node.slice(1) : [node]
|
|
851
888
|
const isDestructPattern = (node) => Array.isArray(node) && (node[0] === '[]' || node[0] === '{}')
|
|
@@ -1246,7 +1283,7 @@ function foldNamespaceIntrospection(callee, args) {
|
|
|
1246
1283
|
// Compiler-internal synthetic callees: emit-handled intrinsics, never user
|
|
1247
1284
|
// function values — so a bare reference must not pull in the callable-value
|
|
1248
1285
|
// (function table / closure) machinery.
|
|
1249
|
-
const INTRINSIC_CALLEES = new Set(['__iter_arr'])
|
|
1286
|
+
const INTRINSIC_CALLEES = new Set(['__iter_arr', '__keys_ro'])
|
|
1250
1287
|
|
|
1251
1288
|
function resolveCallee(callee, args) {
|
|
1252
1289
|
if (typeof callee === 'string') {
|
|
@@ -1427,6 +1464,10 @@ const handlers = {
|
|
|
1427
1464
|
|
|
1428
1465
|
// Template literal: [``, part, ...] → fused single-allocation string concat.
|
|
1429
1466
|
'`'(...parts) {
|
|
1467
|
+
// Fully-static template (`a${123}b`, `hello ${1+2} world`) folds to a single string
|
|
1468
|
+
// literal — a static data segment / SSO box, no runtime concat and no heap machinery.
|
|
1469
|
+
const folded = staticStringExpr(['`', ...parts])
|
|
1470
|
+
if (folded != null) return staticString(folded)
|
|
1430
1471
|
includeForStringValue()
|
|
1431
1472
|
const nodes = parts.map(p =>
|
|
1432
1473
|
Array.isArray(p) && p[0] == null && typeof p[1] === 'string' ? ['str', p[1]] : prep(p))
|
|
@@ -1546,7 +1587,7 @@ const handlers = {
|
|
|
1546
1587
|
if (Array.isArray(specifiers) && specifiers[0] === 'as' && specifiers[1] === '*') {
|
|
1547
1588
|
const alias = specifiers[2]
|
|
1548
1589
|
// Store namespace mapping so '.' handler can resolve X.prop → mangled name
|
|
1549
|
-
if (!ctx.module.namespaces) ctx.module.namespaces =
|
|
1590
|
+
if (!ctx.module.namespaces) ctx.module.namespaces = Object.create(null) // name-keyed: prototype-less (see derive)
|
|
1550
1591
|
ctx.module.namespaces[alias] = resolved.exports
|
|
1551
1592
|
return null
|
|
1552
1593
|
}
|
|
@@ -1628,6 +1669,15 @@ const handlers = {
|
|
|
1628
1669
|
for (const i of decl.slice(1))
|
|
1629
1670
|
if (Array.isArray(i) && i[0] === '=' && typeof i[1] === 'string')
|
|
1630
1671
|
ctx.func.exports[i[1]] = true
|
|
1672
|
+
// export name → bare-identifier re-export (shorthand for `export { name }`).
|
|
1673
|
+
// Register the binding and emit nothing; without this the name falls through
|
|
1674
|
+
// to `prep(decl)` below and compiles as a dead `global.get; drop` statement
|
|
1675
|
+
// while the export itself is silently lost.
|
|
1676
|
+
if (typeof decl === 'string') {
|
|
1677
|
+
const resolved = ctx.scope.chain[decl]
|
|
1678
|
+
ctx.func.exports[decl] = (resolved && resolved !== decl) ? resolved : decl
|
|
1679
|
+
return null
|
|
1680
|
+
}
|
|
1631
1681
|
// export { name, name as alias } from './mod' or export * from './mod'
|
|
1632
1682
|
if (Array.isArray(decl) && decl[0] === 'from') {
|
|
1633
1683
|
const mod = decl[2]?.[1]
|
|
@@ -1821,7 +1871,11 @@ const handlers = {
|
|
|
1821
1871
|
// alone wrongly folds `-5n`, and negating the bigint here yields garbage (-2^63+5).
|
|
1822
1872
|
// `typeof !== 'bigint'` excludes it in both engines (real JS: 'bigint'; jz: matches
|
|
1823
1873
|
// 'bigint'). Bigint negation then flows to emit's i64.sub(0,·) path correctly.
|
|
1824
|
-
|
|
1874
|
+
// `-0` is NOT folded: the self-host kernel evaluates the constant `-na[1]` with
|
|
1875
|
+
// i32 negation (i32 has no signed zero), collapsing -0→+0 — observable via sort's
|
|
1876
|
+
// -0<+0 tiebreak, Object.is, and 1/x. Leaving it as a runtime `u-` emits f64.neg,
|
|
1877
|
+
// which preserves the sign in both engines; V8 re-folds it, so no native cost.
|
|
1878
|
+
if (b === undefined) { const na = prep(a); return isLit(na) && typeof na[1] === 'number' && typeof na[1] !== 'bigint' && na[1] !== 0 ? [, -na[1]] : ['u-', na] }
|
|
1825
1879
|
return ['-', prep(a), prep(b)]
|
|
1826
1880
|
},
|
|
1827
1881
|
|
|
@@ -2123,10 +2177,17 @@ const handlers = {
|
|
|
2123
2177
|
// `[null, null]`, `undefined` is `[null]` (empty value slot). A numeric/string literal
|
|
2124
2178
|
// `[null, v]` has a non-nullish value slot, so `src[1] == null` discriminates them.
|
|
2125
2179
|
const nullish = Array.isArray(src) && src[0] == null && src[1] == null
|
|
2180
|
+
// `__keys_ro` is for-in's read-only key list: identical to Object.keys, but
|
|
2181
|
+
// when the receiver has a complete static schema the keys are a compile-time
|
|
2182
|
+
// constant, so it pools ONE static-data array instead of allocating a fresh
|
|
2183
|
+
// one each evaluation — the per-iteration heap-growth cliff (jz#deopt-forin).
|
|
2184
|
+
// Sound only because for-in reads ks[i]/ks.length and never mutates (unlike
|
|
2185
|
+
// user Object.keys, which permits in-place `.sort()`/`.reverse()`).
|
|
2186
|
+
includeMods('core', 'object', 'string')
|
|
2126
2187
|
const keysExpr = nullish ? ['[]', null]
|
|
2127
2188
|
: typeof src === 'string'
|
|
2128
|
-
? ['?', ['==', src, [null, null]], ['[]', null], ['()',
|
|
2129
|
-
: ['()',
|
|
2189
|
+
? ['?', ['==', src, [null, null]], ['[]', null], ['()', '__keys_ro', src]]
|
|
2190
|
+
: ['()', '__keys_ro', src]
|
|
2130
2191
|
const ks = `${T}fik${ctx.func.uniq++}`, ix = `${T}fii${ctx.func.uniq++}`, lenV = `${T}fil${ctx.func.uniq++}`
|
|
2131
2192
|
const decls = ['let',
|
|
2132
2193
|
['=', ks, keysExpr],
|
|
@@ -2409,7 +2470,7 @@ function prepareModule(specifier, source) {
|
|
|
2409
2470
|
const savedFuncCount = ctx.func.list.length // track new funcs from this module
|
|
2410
2471
|
const savedModulePrefix = ctx.module.currentPrefix
|
|
2411
2472
|
ctx.scope.chain = derive(savedScope) // inherit parent scope
|
|
2412
|
-
ctx.func.exports =
|
|
2473
|
+
ctx.func.exports = Object.create(null) // name-keyed: prototype-less (see derive)
|
|
2413
2474
|
ctx.module.currentPrefix = prefix
|
|
2414
2475
|
|
|
2415
2476
|
try {
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lambda-lift immediately-invoked arrow literals (IIFEs).
|
|
3
|
+
*
|
|
4
|
+
* `(params => body)(args)` is lowered by jz's default path as a CLOSURE value invoked
|
|
5
|
+
* via `call_indirect` through the uniform-f64 closure ABI. That ABI can't carry a
|
|
6
|
+
* `v128`, so a SIMD IIFE — `(() => f32x4.…)()` — emits invalid wasm; it also pays a
|
|
7
|
+
* closure alloc + indirect call for what is really a direct, single call.
|
|
8
|
+
*
|
|
9
|
+
* This pass rewrites each such IIFE into a TOP-LEVEL function whose free variables are
|
|
10
|
+
* appended as parameters, and replaces the call with a direct call passing those vars:
|
|
11
|
+
*
|
|
12
|
+
* let r = (() => { let t = f32x4.mul(a, a); return f32x4.add(t, …) })() // captures a
|
|
13
|
+
* ⇓
|
|
14
|
+
* let ⟨lift⟩ = (a) => { let t = f32x4.mul(a, a); return f32x4.add(t, …) } // hoisted to top
|
|
15
|
+
* let r = ⟨lift⟩(a) // direct call
|
|
16
|
+
*
|
|
17
|
+
* The lifted function flows through jz's monomorphic typed-function path (like any
|
|
18
|
+
* `let f = (x) => …; f(v)`), so a captured/returned `v128` is typed `v128` from the
|
|
19
|
+
* single call site — and `inlineOnce` then folds the single-caller body back in, so
|
|
20
|
+
* there's no residual call. Capture is BY VALUE, which is exact for a synchronous
|
|
21
|
+
* immediate invocation (the value at the call instant == what the closure would read);
|
|
22
|
+
* the one divergence — a capture MUTATED inside the body, which a closure writes back
|
|
23
|
+
* through its cell — is the bail below.
|
|
24
|
+
*
|
|
25
|
+
* Bails (leaving the closure path untouched) on: non-plain params (rest / default /
|
|
26
|
+
* destructured), or a body that assigns any captured variable. Everything else folds.
|
|
27
|
+
*
|
|
28
|
+
* @module prepare/lift-iife
|
|
29
|
+
*/
|
|
30
|
+
import { T, extractParams, classifyParam } from '../ast.js'
|
|
31
|
+
import { findFreeVars, findMutations } from '../compile/analyze-scans.js'
|
|
32
|
+
|
|
33
|
+
// Build a comma-list operand node (the parser's shape) from an array of nodes.
|
|
34
|
+
const commaList = (items) =>
|
|
35
|
+
items.length === 0 ? null : items.length === 1 ? items[0] : [',', ...items]
|
|
36
|
+
|
|
37
|
+
// Unwrap parenthesis nodes (`['()', x]`, length 2) to reach the inner expression.
|
|
38
|
+
const unwrapParens = (n) => {
|
|
39
|
+
while (Array.isArray(n) && n[0] === '()' && n.length === 2) n = n[1]
|
|
40
|
+
return n
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// `['()', callee, args]` (length 3) whose callee unwraps to an arrow literal → that arrow.
|
|
44
|
+
const iifeArrow = (n) => {
|
|
45
|
+
if (!Array.isArray(n) || n[0] !== '()' || n.length !== 3) return null
|
|
46
|
+
const callee = unwrapParens(n[1])
|
|
47
|
+
return Array.isArray(callee) && callee[0] === '=>' ? callee : null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Plain-name params only; anything else (rest/default/destructure) → null (bail).
|
|
51
|
+
const plainParamNames = (arrow) => {
|
|
52
|
+
const out = []
|
|
53
|
+
for (const p of extractParams(arrow[1])) {
|
|
54
|
+
const c = classifyParam(p)
|
|
55
|
+
if (c.kind !== 'plain' || typeof c.name !== 'string') return null
|
|
56
|
+
out.push(c.name)
|
|
57
|
+
}
|
|
58
|
+
return out
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Names a function binds: its plain params + every let/const name in its body, NOT
|
|
62
|
+
// descending into nested arrows (their locals are their own). Over-approximating
|
|
63
|
+
// within a frame is safe — a free ident only resolves to one of these if it's
|
|
64
|
+
// actually in scope at the reference. Used to scope captures to enclosing LOCALS
|
|
65
|
+
// (module-level names stay global refs in the lifted body, never captured).
|
|
66
|
+
const collectDeclNames = (decl, out) => {
|
|
67
|
+
if (!Array.isArray(decl)) return
|
|
68
|
+
if (decl[0] === '=' && typeof decl[1] === 'string') out.add(decl[1])
|
|
69
|
+
else if (decl[0] === '=' && Array.isArray(decl[1])) collectPatternNames(decl[1], out)
|
|
70
|
+
else if (typeof decl[1] === 'string') out.add(decl[1])
|
|
71
|
+
}
|
|
72
|
+
const collectPatternNames = (pat, out) => {
|
|
73
|
+
if (typeof pat === 'string') { out.add(pat); return }
|
|
74
|
+
if (!Array.isArray(pat)) return
|
|
75
|
+
for (let i = 1; i < pat.length; i++) collectPatternNames(pat[i], out)
|
|
76
|
+
}
|
|
77
|
+
function functionLocals(paramNodes, body) {
|
|
78
|
+
const names = new Set()
|
|
79
|
+
for (const p of paramNodes) { const c = classifyParam(p); if (typeof c.name === 'string') names.add(c.name); else if (c.pattern) collectPatternNames(c.pattern, names) }
|
|
80
|
+
const scan = (n) => {
|
|
81
|
+
if (!Array.isArray(n)) return
|
|
82
|
+
if (n[0] === '=>') return
|
|
83
|
+
if (n[0] === 'let' || n[0] === 'const') for (const d of n.slice(1)) collectDeclNames(d, names)
|
|
84
|
+
for (let i = 1; i < n.length; i++) scan(n[i])
|
|
85
|
+
}
|
|
86
|
+
scan(body)
|
|
87
|
+
return names
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function liftIIFEs(ast) {
|
|
91
|
+
if (!Array.isArray(ast)) return ast
|
|
92
|
+
const lifted = [] // hoisted `['let', ['=', name, arrow]]` decls
|
|
93
|
+
let uid = 0
|
|
94
|
+
|
|
95
|
+
// Copy non-index node metadata (parser `.loc`, etc.) onto a rebuilt node so error
|
|
96
|
+
// source-locations survive the transform.
|
|
97
|
+
const copyMeta = (to, from) => { for (const k of Object.keys(from)) if (isNaN(+k)) to[k] = from[k]; return to }
|
|
98
|
+
|
|
99
|
+
// `locals` = union of every enclosing FUNCTION frame's bound names. The transform
|
|
100
|
+
// is post-order (inner IIFEs fold first) so an outer lift sees already-direct inner
|
|
101
|
+
// calls; scope tracking is top-down so each IIFE sees the right enclosing locals.
|
|
102
|
+
// Identity-preserving: a node with no lifted descendant is returned unchanged.
|
|
103
|
+
const visit = (node, locals) => {
|
|
104
|
+
if (!Array.isArray(node)) return node
|
|
105
|
+
|
|
106
|
+
// Descend into an arrow body with the frame extended by this arrow's locals.
|
|
107
|
+
if (node[0] === '=>') {
|
|
108
|
+
const inner = new Set(locals)
|
|
109
|
+
for (const n of functionLocals(extractParams(node[1]), node[2])) inner.add(n)
|
|
110
|
+
let changed = false
|
|
111
|
+
const out = node.map((c, i) => { if (i === 0) return c; const v = visit(c, inner); if (v !== c) changed = true; return v })
|
|
112
|
+
return changed ? copyMeta(out, node) : node
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Transform children first (post-order).
|
|
116
|
+
let changed = false
|
|
117
|
+
const mapped = node.map((c, i) => { if (i === 0) return c; const v = visit(c, locals); if (v !== c) changed = true; return v })
|
|
118
|
+
const base = changed ? copyMeta(mapped, node) : node
|
|
119
|
+
|
|
120
|
+
const arrow = iifeArrow(base)
|
|
121
|
+
if (!arrow) return base
|
|
122
|
+
|
|
123
|
+
const paramNames = plainParamNames(arrow)
|
|
124
|
+
if (paramNames === null) return base // bail: non-plain params
|
|
125
|
+
|
|
126
|
+
const callArgs = base[2] == null ? [] : (Array.isArray(base[2]) && base[2][0] === ',') ? base[2].slice(1) : [base[2]]
|
|
127
|
+
if (callArgs.some(a => Array.isArray(a) && a[0] === '...')) return base // bail: spread into a fixed-arity direct call
|
|
128
|
+
|
|
129
|
+
const body = arrow[2]
|
|
130
|
+
const captures = []
|
|
131
|
+
findFreeVars(body, new Set(paramNames), captures, locals)
|
|
132
|
+
|
|
133
|
+
const mutated = new Set()
|
|
134
|
+
findMutations(body, new Set(captures), mutated)
|
|
135
|
+
if (mutated.size) return base // bail: a captured var is written
|
|
136
|
+
|
|
137
|
+
// Lift: `(…params, …captures) => body`, hoisted to module top; call passes captures.
|
|
138
|
+
const name = `${T}lift${uid++}`
|
|
139
|
+
lifted.push(['let', ['=', name, ['=>', ['()', commaList([...paramNames, ...captures])], body]]])
|
|
140
|
+
return copyMeta(['()', name, commaList([...callArgs, ...captures])], base)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const result = visit(ast, new Set())
|
|
144
|
+
if (!lifted.length) return result
|
|
145
|
+
|
|
146
|
+
// Prepend the lifted decls to the module body (`[';', …stmts]`).
|
|
147
|
+
if (Array.isArray(result) && result[0] === ';') return [';', ...lifted, ...result.slice(1)]
|
|
148
|
+
return [';', ...lifted, result]
|
|
149
|
+
}
|
package/src/reps.js
CHANGED
|
@@ -56,6 +56,10 @@ export const VAL = {
|
|
|
56
56
|
* @property {boolean} [notString] proven not a string (skips string-path guards).
|
|
57
57
|
* @property {number} [arrayElemSchema] element object-schema id for arrays.
|
|
58
58
|
* @property {string} [arrayElemValType] element VAL.* kind for arrays.
|
|
59
|
+
* @property {string} [arrayElemElemValType] nested element VAL.* kind (`X[i][j]`) for arrays of arrays.
|
|
60
|
+
* @property {string} [arrayElemTypedCtor] element TypedArray ctor (`new.Float32Array`) for an
|
|
61
|
+
* array whose elements are all typed arrays of one ctor (`Array.from(n,()=>new Float32Array())`),
|
|
62
|
+
* so `arr[i]` is a known typed array and `arr[i][j]` inlines instead of runtime aux-dispatch.
|
|
59
63
|
* @property {string} [carrier] abi carrier id override (e.g. 'jsstring').
|
|
60
64
|
* @property {boolean} [unsigned] i32 carries an unsigned value (`>>>` result).
|
|
61
65
|
* @property {*} [jsonShape] inferred shape for the JSON.stringify fast path.
|
|
@@ -67,8 +71,8 @@ export const VAL = {
|
|
|
67
71
|
*/
|
|
68
72
|
export const REP_FIELDS = new Set([
|
|
69
73
|
'val', 'ptrKind', 'ptrAux', 'schemaId', 'intConst', 'intCertain', 'notString',
|
|
70
|
-
'arrayElemSchema', 'arrayElemValType', 'carrier', 'unsigned', 'jsonShape',
|
|
71
|
-
'typedCtor', 'wasm', 'nullable',
|
|
74
|
+
'arrayElemSchema', 'arrayElemValType', 'arrayElemElemValType', 'arrayElemTypedCtor', 'carrier', 'unsigned', 'jsonShape',
|
|
75
|
+
'typedCtor', 'wasm', 'nullable', 'neverGrown',
|
|
72
76
|
])
|
|
73
77
|
|
|
74
78
|
const DBG_REPS = typeof process !== 'undefined' && process.env?.JZ_DEBUG_INVARIANTS === '1'
|
package/src/static.js
CHANGED
|
@@ -19,6 +19,15 @@ export function intLiteralValue(expr) {
|
|
|
19
19
|
/** Non-negative integer literal — used for string/typed-array index bounds. */
|
|
20
20
|
export const nonNegIntLiteral = (node) => { const n = intLiteralValue(node); return n != null && n >= 0 ? n : null }
|
|
21
21
|
|
|
22
|
+
/** Flat-array slot key for a *bare* non-negative integer index literal `[null, k]`
|
|
23
|
+
* — returns the stringified index ("0","1",…) so an array `a[k]` resolves through
|
|
24
|
+
* the same SRoA `name#i` machinery as an object `o.key`. Only a literal index
|
|
25
|
+
* qualifies (not a const-folded identifier): the key must be unambiguous at scan
|
|
26
|
+
* time, before any rep is known. Null for dynamic / non-integer / huge indices. */
|
|
27
|
+
export const staticIndexKey = (node) =>
|
|
28
|
+
Array.isArray(node) && node[0] == null && Number.isInteger(node[1]) && node[1] >= 0 && node[1] < 0x100000000
|
|
29
|
+
? String(node[1]) : null
|
|
30
|
+
|
|
22
31
|
/** Fold compile-time integer expressions (literals, const bindings, + - * <<). */
|
|
23
32
|
export function constIntExpr(node) {
|
|
24
33
|
let lit = intLiteralValue(node)
|
package/src/type.js
CHANGED
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { isI32, isReassigned } from './ast.js'
|
|
14
14
|
import { ctx } from './ctx.js'
|
|
15
|
+
import { FITS_I32_MAX } from './widen.js'
|
|
15
16
|
import { VAL, lookupValType } from './reps.js'
|
|
16
17
|
import { valTypeOf } from './kind.js'
|
|
18
|
+
import { propValType, CMP_OPS } from './kind-traits.js'
|
|
17
19
|
import { NO_VALUE, staticValue, intLiteralValue } from './static.js'
|
|
18
20
|
import { typedElemAux } from '../layout.js'
|
|
19
21
|
|
|
@@ -362,9 +364,24 @@ export function isTerminator(body) {
|
|
|
362
364
|
return false
|
|
363
365
|
}
|
|
364
366
|
|
|
365
|
-
|
|
367
|
+
// Resolve a name's typed-array element ctor: in-progress local overlay (analyzeBody) →
|
|
368
|
+
// per-func map (post-analyze) → module-global registry. The global fallback matters during
|
|
369
|
+
// analyzeBody/narrow when the per-func map is null, so a read of a *global* typed array
|
|
370
|
+
// (`DX[i]` with `let DX = new Int32Array(...)` at module scope) resolves its element type
|
|
371
|
+
// instead of defaulting to f64. Guard against local shadows / dynamic rewrites (cf. kind.js).
|
|
372
|
+
const typedElemCtorOf = (name, locals) =>
|
|
373
|
+
ctx.func.localTypedElemsOverlay?.get(name) ?? ctx.types.typedElem?.get(name)
|
|
374
|
+
?? (!locals?.has?.(name) && !ctx.types?.dynWriteVars?.has?.(name)
|
|
375
|
+
? ctx.scope?.globalTypedElem?.get(name) : undefined)
|
|
376
|
+
|
|
377
|
+
// An expression whose i32 value carries the unsigned [0, 2^32) magnitude (not a signed i32):
|
|
378
|
+
// `>>>`, an unsigned-result call, or a Uint32Array read (aux 5 — the only typed array whose
|
|
379
|
+
// element can exceed signed-i32 range). The +/-/*/% rules widen these to f64 so `U[i] + 1`
|
|
380
|
+
// near 2^32 doesn't wrap; bitwise/store consumers are ToInt32-exact and keep the i32 bits.
|
|
381
|
+
const isUnsignedI32Expr = (e, locals) => Array.isArray(e) && (
|
|
366
382
|
e[0] === '>>>' ||
|
|
367
|
-
(e[0] === '()' && typeof e[1] === 'string' && ctx.func.map?.get(e[1])?.sig?.unsignedResult === true)
|
|
383
|
+
(e[0] === '()' && typeof e[1] === 'string' && ctx.func.map?.get(e[1])?.sig?.unsignedResult === true) ||
|
|
384
|
+
(e[0] === '[]' && typeof e[1] === 'string' && typedElemAux(typedElemCtorOf(e[1], locals)) === 5)
|
|
368
385
|
)
|
|
369
386
|
|
|
370
387
|
/**
|
|
@@ -380,12 +397,16 @@ export function exprType(expr, locals) {
|
|
|
380
397
|
if (locals?.has?.(expr)) return locals.get(expr)
|
|
381
398
|
const paramType = ctx.func.current?.params?.find(p => p.name === expr)?.type
|
|
382
399
|
if (paramType) return paramType
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
|
|
400
|
+
// A module-level INTEGER const (`const N = 16384`) is an integer compile-time
|
|
401
|
+
// constant — type it i32 when it fits, regardless of the global's f64 (NaN-box)
|
|
402
|
+
// storage. Otherwise a counter bounded by it (`for (i=0; i<N; i++)`) widens to
|
|
403
|
+
// f64 and `x % N` / `x & N` / `x / N` take the f64 round-trip instead of the
|
|
404
|
+
// native integer path (i32.rem_s / i32.and / i32.shr). Mirrors a literal int.
|
|
405
|
+
const ci = ctx.scope?.constInts?.get?.(expr)
|
|
406
|
+
if (ci != null && isI32(ci)) return 'i32'
|
|
407
|
+
// Module-level numeric consts emitted as wasm globals with a known wasm type.
|
|
408
|
+
// Only propagate primitive numeric kinds — i64 globals are reserved for the
|
|
409
|
+
// NaN-box carrier ABI and shouldn't influence local typing.
|
|
389
410
|
const gt = ctx.scope?.globalTypes?.get?.(expr)
|
|
390
411
|
if (gt === 'i32' || gt === 'f64') return gt
|
|
391
412
|
return 'f64'
|
|
@@ -401,44 +422,41 @@ export function exprType(expr, locals) {
|
|
|
401
422
|
|
|
402
423
|
// Always f64
|
|
403
424
|
if (op === '/' || op === '**' || op === '[' || op === '{}' || op === 'str') return 'f64'
|
|
404
|
-
// arr[i] — typed
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
425
|
+
// arr[i] — integer typed arrays (Int8/Uint8/Int16/Uint16/Int32/Uint32, aux 0..5) read as i32:
|
|
426
|
+
// the element IS a 32-bit machine integer, so a binding used in integer/bitwise ops stays i32
|
|
427
|
+
// instead of round-tripping i32.load → f64 → trunc back (the deopt that made packed-pixel fade
|
|
428
|
+
// loops like lorenz slow). Uint32 reads carry the full 0..2^32-1 range as the i32 bit-pattern;
|
|
429
|
+
// ToInt32-coercing uses (& | ^ << >> >>>, i32.store) are bit-exact, and value uses that need the
|
|
430
|
+
// unsigned magnitude (compare, f64 convert) go through the elem-aux's unsigned path. Floats
|
|
431
|
+
// (Float32/Float64, aux 6/7) genuinely yield f64. typedElems: in-progress reads come from
|
|
432
|
+
// localTypedElemsOverlay during analyzeBody; post-analyze passes read ctx.types.typedElem.
|
|
408
433
|
if (op === '[]') {
|
|
409
434
|
if (typeof args[0] === 'string') {
|
|
410
|
-
|
|
435
|
+
// Resolve the element ctor across local overlay → per-func map → module-global registry
|
|
436
|
+
// (the global fallback is why `DX[i]` on a module-scope Int32Array types as i32 instead of
|
|
437
|
+
// f64-round-tripping integer accumulation like `ax = ax + DX[i]`). See typedElemCtorOf.
|
|
438
|
+
const ctor = typedElemCtorOf(args[0], locals)
|
|
411
439
|
if (ctor) {
|
|
412
440
|
const aux = typedElemAux(ctor)
|
|
413
|
-
if (aux != null && (aux & 7) <=
|
|
441
|
+
if (aux != null && (aux & 7) <= 5) return 'i32'
|
|
414
442
|
}
|
|
415
443
|
}
|
|
416
444
|
return 'f64'
|
|
417
445
|
}
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
//
|
|
446
|
+
// A sized built-in property on a statically-known receiver (`.length` on
|
|
447
|
+
// STRING/ARRAY/TYPED, `.size` on SET/MAP, `.byteLength`/`.byteOffset` on
|
|
448
|
+
// TYPED/BUFFER) returns i32 directly (`__len`/`__str_byteLen` return i32).
|
|
449
|
+
// Keeping it i32 lets analyzeBody keep the counter local i32, eliminating the
|
|
450
|
+
// per-iteration `f64.convert_i32_s` widen and matching `arr[i]`/`i*k` truncs.
|
|
451
|
+
// The membership lives in one place — `propValType` (src/kind-traits.js).
|
|
423
452
|
if (op === '.') {
|
|
424
|
-
if (args[
|
|
425
|
-
const vt = lookupValType(args[0])
|
|
426
|
-
if (vt === VAL.TYPED || vt === VAL.ARRAY || vt === VAL.STRING || vt === VAL.BUFFER) return 'i32'
|
|
427
|
-
}
|
|
428
|
-
if (args[1] === 'size' && typeof args[0] === 'string') {
|
|
429
|
-
const vt = lookupValType(args[0])
|
|
430
|
-
if (vt === VAL.SET || vt === VAL.MAP) return 'i32'
|
|
431
|
-
}
|
|
432
|
-
if (args[1] === 'byteLength' && typeof args[0] === 'string') {
|
|
433
|
-
const vt = lookupValType(args[0])
|
|
434
|
-
if (vt === VAL.BUFFER || vt === VAL.TYPED) return 'i32'
|
|
435
|
-
}
|
|
453
|
+
if (typeof args[0] === 'string' && propValType(args[1], lookupValType(args[0])) === VAL.NUMBER) return 'i32'
|
|
436
454
|
return 'f64'
|
|
437
455
|
}
|
|
438
456
|
// Comparisons, logical-not, and unsigned shift always yield an i32 — a boolean,
|
|
439
457
|
// or a ToUint32 result. True even on BigInt operands (`>>>` throws on bigint, so
|
|
440
458
|
// it never reaches here with one).
|
|
441
|
-
if (
|
|
459
|
+
if (CMP_OPS.has(op) || op === '>>>') return 'i32'
|
|
442
460
|
// Bitwise & signed-shift: i32 on numbers, but f64 when operands are BigInt — the
|
|
443
461
|
// result is a bigint carried in the i64-bits-as-f64 ABI, not a 32-bit int.
|
|
444
462
|
if (['&', '|', '^', '~', '<<', '>>'].includes(op))
|
|
@@ -451,7 +469,7 @@ export function exprType(expr, locals) {
|
|
|
451
469
|
// A uint32 operand ([0, 2^32)) makes the result exceed signed i32 range, so
|
|
452
470
|
// emit widens to f64 (see emit.js `+`/`-`). exprType must agree — else
|
|
453
471
|
// narrowing the result back to i32 would trunc_sat-saturate the f64 to INT32_MAX.
|
|
454
|
-
if (isUnsignedI32Expr(args[0]) || (args[1] != null && isUnsignedI32Expr(args[1]))) return 'f64'
|
|
472
|
+
if (isUnsignedI32Expr(args[0], locals) || (args[1] != null && isUnsignedI32Expr(args[1], locals))) return 'f64'
|
|
455
473
|
return 'i32'
|
|
456
474
|
}
|
|
457
475
|
// `%` is i32 only when emit takes the i32.rem_s path: both operands i32, neither
|
|
@@ -461,7 +479,7 @@ export function exprType(expr, locals) {
|
|
|
461
479
|
if (op === '%') {
|
|
462
480
|
const ta = exprType(args[0], locals), tb = exprType(args[1], locals)
|
|
463
481
|
if (ta !== 'i32' || tb !== 'i32') return 'f64'
|
|
464
|
-
if (isUnsignedI32Expr(args[0]) || isUnsignedI32Expr(args[1])) return 'f64'
|
|
482
|
+
if (isUnsignedI32Expr(args[0], locals) || isUnsignedI32Expr(args[1], locals)) return 'f64'
|
|
465
483
|
const dv = staticValue(args[1])
|
|
466
484
|
return (dv !== NO_VALUE && typeof dv === 'number' && dv !== 0 && Number.isInteger(dv)) ? 'i32' : 'f64'
|
|
467
485
|
}
|
|
@@ -474,11 +492,15 @@ export function exprType(expr, locals) {
|
|
|
474
492
|
const ta = exprType(args[0], locals), tb = exprType(args[1], locals)
|
|
475
493
|
if (ta !== 'i32' || tb !== 'i32') return 'f64'
|
|
476
494
|
// uint32 operand: product can exceed i32; emit widens to f64 (see emit.js `*`).
|
|
477
|
-
if (isUnsignedI32Expr(args[0]) || isUnsignedI32Expr(args[1])) return 'f64'
|
|
495
|
+
if (isUnsignedI32Expr(args[0], locals) || isUnsignedI32Expr(args[1], locals)) return 'f64'
|
|
478
496
|
if (sv !== NO_VALUE && typeof sv === 'number') return isI32(sv) ? 'i32' : 'f64'
|
|
497
|
+
// Shared FITS_I32_MAX threshold (widen.js) keeps this in lock-step with emit's
|
|
498
|
+
// `mulFitsI32`. exprType only proves the static-literal case — a strict SUBSET of
|
|
499
|
+
// emit's i32 verdict (emit also admits masked-bound operands), which is the safe
|
|
500
|
+
// direction: never claim i32 where emit might widen to f64.
|
|
479
501
|
const small = e => {
|
|
480
502
|
const v = staticValue(e)
|
|
481
|
-
return v !== NO_VALUE && typeof v === 'number' && Math.abs(v) <=
|
|
503
|
+
return v !== NO_VALUE && typeof v === 'number' && Math.abs(v) <= FITS_I32_MAX
|
|
482
504
|
}
|
|
483
505
|
return small(args[0]) || small(args[1]) ? 'i32' : 'f64'
|
|
484
506
|
}
|
|
@@ -501,7 +523,7 @@ export function exprType(expr, locals) {
|
|
|
501
523
|
// hand a scalar back (i32x4.lane / v128.anyTrue / v128.allTrue → i32;
|
|
502
524
|
// f32x4.lane → f64). See module/simd.js.
|
|
503
525
|
if (typeof args[0] === 'string' && (args[0].startsWith('f32x4.') || args[0].startsWith('i32x4.') || args[0].startsWith('f64x2.') || args[0].startsWith('v128.'))) {
|
|
504
|
-
if (args[0] === 'f32x4.lane') return 'f64'
|
|
526
|
+
if (args[0] === 'f32x4.lane' || args[0] === 'f64x2.lane') return 'f64'
|
|
505
527
|
if (args[0] === 'i32x4.lane' || args[0] === 'v128.anyTrue' || args[0] === 'v128.allTrue') return 'i32'
|
|
506
528
|
return 'v128'
|
|
507
529
|
}
|
|
@@ -527,7 +549,6 @@ export function exprType(expr, locals) {
|
|
|
527
549
|
// === Integer-certainty fixpoint (shared by analyzeIntCertain + program-facts) ===
|
|
528
550
|
|
|
529
551
|
const INT_BIT_OPS = new Set(['|', '&', '^', '~', '<<', '>>', '>>>'])
|
|
530
|
-
const INT_CMP_OPS = new Set(['<', '>', '<=', '>=', '==', '!=', '===', '!==', '!'])
|
|
531
552
|
const INT_CLOSED_OPS = new Set(['+', '-', '*']) // `%` handled separately — int only for nonzero divisor
|
|
532
553
|
const INT_MATH_FNS = new Set(['imul', 'clz32', 'floor', 'ceil', 'round', 'trunc'])
|
|
533
554
|
|
|
@@ -548,7 +569,7 @@ function collectIntDefs(body) {
|
|
|
548
569
|
} else if (op === '=' && typeof args[0] === 'string') {
|
|
549
570
|
pushDef(args[0], args[1])
|
|
550
571
|
} else if (typeof op === 'string' && op.length > 1 && op.endsWith('=') &&
|
|
551
|
-
!
|
|
572
|
+
!CMP_OPS.has(op) && op !== '=>' && typeof args[0] === 'string') {
|
|
552
573
|
pushDef(args[0], [op.slice(0, -1), args[0], args[1]])
|
|
553
574
|
} else if ((op === '++' || op === '--') && typeof args[0] === 'string') {
|
|
554
575
|
pushDef(args[0], [op === '++' ? '+' : '-', args[0], [null, 1]])
|
|
@@ -574,18 +595,9 @@ function makeIsIntExpr(intCertain) {
|
|
|
574
595
|
if (typeof v === 'boolean') return true
|
|
575
596
|
return false
|
|
576
597
|
}
|
|
577
|
-
if (INT_BIT_OPS.has(op) ||
|
|
578
|
-
if (op === '.')
|
|
579
|
-
|
|
580
|
-
const vt = lookupValType(args[0])
|
|
581
|
-
return vt === VAL.TYPED || vt === VAL.ARRAY || vt === VAL.STRING || vt === VAL.BUFFER
|
|
582
|
-
}
|
|
583
|
-
if (args[1] === 'size' && typeof args[0] === 'string') {
|
|
584
|
-
const vt = lookupValType(args[0])
|
|
585
|
-
return vt === VAL.SET || vt === VAL.MAP
|
|
586
|
-
}
|
|
587
|
-
return false
|
|
588
|
-
}
|
|
598
|
+
if (INT_BIT_OPS.has(op) || CMP_OPS.has(op)) return true
|
|
599
|
+
if (op === '.')
|
|
600
|
+
return typeof args[0] === 'string' && propValType(args[1], lookupValType(args[0])) === VAL.NUMBER
|
|
589
601
|
if (INT_CLOSED_OPS.has(op)) {
|
|
590
602
|
const a = isIntExpr(args[0])
|
|
591
603
|
const b = args[1] != null ? isIntExpr(args[1]) : a
|