jz 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +315 -318
- package/bench/README.md +369 -0
- package/bench/bench.svg +102 -0
- package/cli.js +104 -30
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +362 -84
- package/interop.js +159 -186
- package/jz.svg +5 -0
- package/jzify/arguments.js +97 -0
- package/jzify/bundler.js +382 -0
- package/jzify/classes.js +328 -0
- package/jzify/hoist-vars.js +177 -0
- package/jzify/index.js +51 -0
- package/jzify/names.js +37 -0
- package/jzify/switch.js +106 -0
- package/jzify/transform.js +349 -0
- package/layout.js +184 -0
- package/module/array.js +377 -176
- package/module/collection.js +639 -145
- package/module/console.js +55 -43
- package/module/core.js +264 -153
- package/module/date.js +15 -3
- package/module/function.js +73 -6
- package/module/index.js +2 -1
- package/module/json.js +226 -61
- package/module/math.js +551 -187
- package/module/number.js +327 -60
- package/module/object.js +474 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +117 -0
- package/module/string.js +621 -226
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +459 -73
- package/package.json +58 -14
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +29 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +824 -0
- package/src/compile/analyze.js +1600 -0
- package/src/compile/emit-assign.js +411 -0
- package/src/compile/emit.js +3512 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +778 -128
- package/src/{infer.js → compile/infer.js} +62 -98
- package/src/compile/loop-divmod.js +155 -0
- package/src/{narrow.js → compile/narrow.js} +409 -106
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +370 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +122 -0
- package/src/compile/plan/inline.js +682 -0
- package/src/compile/plan/literals.js +1199 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +649 -0
- package/src/compile/program-facts.js +423 -0
- package/src/ctx.js +220 -64
- package/src/ir.js +589 -172
- package/src/kind-traits.js +132 -0
- package/src/kind.js +524 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1432 -473
- package/src/optimize/vectorize.js +4660 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +649 -205
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +116 -0
- package/src/resolve.js +12 -3
- package/src/static.js +208 -0
- package/src/type.js +651 -0
- package/src/{assemble.js → wat/assemble.js} +275 -55
- package/src/wat/optimize.js +3938 -0
- package/transform.js +21 -0
- package/wasi.js +47 -5
- package/src/analyze.js +0 -3818
- package/src/emit.js +0 -3040
- package/src/jzify.js +0 -1580
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /package/src/{codegen.js → wat/codegen.js} +0 -0
|
@@ -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
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ValueRep storage + VAL lattice lookups (ctx-backed, cycle-free).
|
|
3
|
+
*
|
|
4
|
+
* Thin accessors shared by ir, emit, stdlib, and analyze. Keeps heavy AST
|
|
5
|
+
* walkers in analyze.js without pulling them into ir.js.
|
|
6
|
+
*
|
|
7
|
+
* ## Lookup priority (lookupValType / lookupNotString)
|
|
8
|
+
*
|
|
9
|
+
* A single binding can carry several pieces of type knowledge, set at different
|
|
10
|
+
* lifecycle phases. Accessors resolve them in this fixed order — first hit wins:
|
|
11
|
+
*
|
|
12
|
+
* 1. `ctx.func.refinements` flow-sensitive (typeof/instanceof guard)
|
|
13
|
+
* 2. `ctx.func.localValTypesOverlay` call-site / loop-iter overlay (transient)
|
|
14
|
+
* 3. `ctx.func.localReps` per-function plan/analyze fact (durable)
|
|
15
|
+
* 4. `ctx.scope.globalValTypes` module-level binding (durable)
|
|
16
|
+
*
|
|
17
|
+
* Writes go through `updateRep` (#3 mutator) / `updateGlobalRep` (#4 mutator).
|
|
18
|
+
* Refinements (#1) are managed by `withRefinements` in emit; overlay (#2) is
|
|
19
|
+
* scoped by call/loop-emit code and torn down when the scope exits.
|
|
20
|
+
*
|
|
21
|
+
* Mutation sites by phase:
|
|
22
|
+
* plan.js — initial reps from prepare-pass typing
|
|
23
|
+
* analyze.js — boxing decisions, schema bindings, sched facts
|
|
24
|
+
* compile/index.js — closure-arg upgrades, propagation across calls
|
|
25
|
+
* emit.js — withRefinements / overlay, transient narrowing only
|
|
26
|
+
*
|
|
27
|
+
* @module reps
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { ctx } from './ctx.js'
|
|
31
|
+
|
|
32
|
+
/** Value kinds — method dispatch, schema, carrier selection. */
|
|
33
|
+
export const VAL = {
|
|
34
|
+
NUMBER: 'number', ARRAY: 'array', STRING: 'string',
|
|
35
|
+
OBJECT: 'object', HASH: 'hash', SET: 'set', MAP: 'map',
|
|
36
|
+
CLOSURE: 'closure', TYPED: 'typed', REGEX: 'regex',
|
|
37
|
+
BIGINT: 'bigint', BUFFER: 'buffer', DATE: 'date',
|
|
38
|
+
BOOL: 'boolean',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A binding's inferred representation. Every field optional; absence = "unknown".
|
|
43
|
+
* Written only through updateRep / updateGlobalRep, plus a few direct r.wasm /
|
|
44
|
+
* r.typedCtor mutations in narrow.js's signature fixpoint. This is the *closed*
|
|
45
|
+
* shape: REP_FIELDS is the single source of truth — it gates updateRep in debug
|
|
46
|
+
* mode and drives repView, so a typo'd key surfaces loudly instead of silently
|
|
47
|
+
* vanishing into the open `{...prev, ...fields}` spread.
|
|
48
|
+
*
|
|
49
|
+
* @typedef {Object} ValueRep
|
|
50
|
+
* @property {string} [val] VAL.* kind (number/array/string/…).
|
|
51
|
+
* @property {number} [ptrKind] PTR.* pointer class for NaN-box rebox.
|
|
52
|
+
* @property {number} [ptrAux] aux bits in the NaN-box (schema id / elem type).
|
|
53
|
+
* @property {number} [schemaId] object-shape id (OBJECT kind).
|
|
54
|
+
* @property {number} [intConst] proven constant integer value.
|
|
55
|
+
* @property {boolean} [intCertain] integer-valued on every path.
|
|
56
|
+
* @property {boolean} [notString] proven not a string (skips string-path guards).
|
|
57
|
+
* @property {number} [arrayElemSchema] element object-schema id for arrays.
|
|
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} [carrier] abi carrier id override (e.g. 'jsstring').
|
|
61
|
+
* @property {boolean} [unsigned] i32 carries an unsigned value (`>>>` result).
|
|
62
|
+
* @property {*} [jsonShape] inferred shape for the JSON.stringify fast path.
|
|
63
|
+
* @property {string} [typedCtor] TypedArray ctor name (TYPED kind); null = bimorphic.
|
|
64
|
+
* @property {string} [wasm] wasm storage type 'i32'|'f64' (narrow.js fixpoint).
|
|
65
|
+
* @property {boolean} [nullable] binding can hold null/undefined on some path
|
|
66
|
+
* (init or an assignment was a nullish literal) — suppresses the `=== null` /
|
|
67
|
+
* `=== undefined` constant-fold even when `val` is a definite non-null kind.
|
|
68
|
+
*/
|
|
69
|
+
export const REP_FIELDS = new Set([
|
|
70
|
+
'val', 'ptrKind', 'ptrAux', 'schemaId', 'intConst', 'intCertain', 'notString',
|
|
71
|
+
'arrayElemSchema', 'arrayElemValType', 'arrayElemElemValType', 'carrier', 'unsigned', 'jsonShape',
|
|
72
|
+
'typedCtor', 'wasm', 'nullable', 'neverGrown',
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
const DBG_REPS = typeof process !== 'undefined' && process.env?.JZ_DEBUG_INVARIANTS === '1'
|
|
76
|
+
const assertRepFields = (name, fields) => {
|
|
77
|
+
for (const k in fields)
|
|
78
|
+
if (!REP_FIELDS.has(k))
|
|
79
|
+
throw new Error(`updateRep('${name}', {${k}}): unknown ValueRep field — typo, or add it to REP_FIELDS in reps.js`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** @returns {ValueRep|undefined} */
|
|
83
|
+
export const repOf = name => ctx.func.localReps?.get(name)
|
|
84
|
+
|
|
85
|
+
export const updateRep = (name, fields) => {
|
|
86
|
+
if (DBG_REPS) assertRepFields(name, fields)
|
|
87
|
+
const m = ctx.func.localReps ||= new Map()
|
|
88
|
+
const prev = m.get(name) || {}
|
|
89
|
+
const next = { ...prev, ...fields }
|
|
90
|
+
for (const k of Object.keys(next)) if (next[k] === undefined) delete next[k]
|
|
91
|
+
if (Object.keys(next).length === 0) m.delete(name)
|
|
92
|
+
else m.set(name, next)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const repOfGlobal = name => ctx.scope.globalReps?.get(name)
|
|
96
|
+
|
|
97
|
+
export const updateGlobalRep = (name, fields) => {
|
|
98
|
+
if (DBG_REPS) assertRepFields(name, fields)
|
|
99
|
+
const m = ctx.scope.globalReps ||= new Map()
|
|
100
|
+
const prev = m.get(name)
|
|
101
|
+
m.set(name, prev ? { ...prev, ...fields } : { ...fields })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const lookupValType = name => {
|
|
105
|
+
const r = ctx.func.refinements
|
|
106
|
+
if (r?.size) { const v = r.get(name)?.val; if (v) return v }
|
|
107
|
+
const ov = ctx.func.localValTypesOverlay
|
|
108
|
+
if (ov) { const v = ov.get(name); if (v) return v }
|
|
109
|
+
return ctx.func.localReps?.get(name)?.val || ctx.scope.globalValTypes?.get(name) || null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const lookupNotString = name => {
|
|
113
|
+
const r = ctx.func.refinements
|
|
114
|
+
if (r?.size && r.get(name)?.notString) return true
|
|
115
|
+
return ctx.func.localReps?.get(name)?.notString === true
|
|
116
|
+
}
|
package/src/resolve.js
CHANGED
|
@@ -14,9 +14,18 @@ import { readFileSync, existsSync } from 'fs'
|
|
|
14
14
|
import { dirname, resolve, join } from 'path'
|
|
15
15
|
import { execFileSync } from 'child_process'
|
|
16
16
|
|
|
17
|
-
// Matches
|
|
18
|
-
//
|
|
19
|
-
|
|
17
|
+
// Matches real module imports/exports at statement position — and ONLY those:
|
|
18
|
+
// import 'x' (bare side-effect import)
|
|
19
|
+
// import … from 'x' (default/named/namespace)
|
|
20
|
+
// export … from 'x' / export * from (re-export)
|
|
21
|
+
// The specifier MUST follow `from` (or be a bare `import`'s string). The old
|
|
22
|
+
// `(?:import|export)\s+[^'"]*?['"]…` matched any post-keyword string because
|
|
23
|
+
// `[^'"]` spans newlines, so `export const X = [⏎ 'lit'` was read as `export …
|
|
24
|
+
// 'lit'` — bundling then rewrote the bare string literal `'lit'` to a module path.
|
|
25
|
+
// (Self-host fallout: `PASS_NAMES = ['watr', …]` had `'watr'` rewritten to
|
|
26
|
+
// `…/node_modules/watr/watr.js`, corrupting every `'watr'` constant — and so the
|
|
27
|
+
// kernel's whole optimize config, since `cfg.watr` then read that path.)
|
|
28
|
+
const importRe = /^\s*(?:import\b[^'"]*?\bfrom\s*|import\s+|export\b[^'"]*?\bfrom\s*)['"]([^'"]+)['"]/gm
|
|
20
29
|
|
|
21
30
|
/**
|
|
22
31
|
* @param {string} entryFile - absolute or cwd-relative path to the entry module.
|
package/src/static.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-time static evaluation — literals, property keys, schema ids.
|
|
3
|
+
* @module static
|
|
4
|
+
*/
|
|
5
|
+
import { I32_MIN, I32_MAX } from './ast.js'
|
|
6
|
+
import { ctx } from './ctx.js'
|
|
7
|
+
import { repOf, VAL } from './reps.js'
|
|
8
|
+
|
|
9
|
+
/** Extract integer value from AST literal node. Returns null if not a 32-bit integer. */
|
|
10
|
+
export function intLiteralValue(expr) {
|
|
11
|
+
let v = null
|
|
12
|
+
if (typeof expr === 'number') v = expr
|
|
13
|
+
else if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'number') v = expr[1]
|
|
14
|
+
else if (Array.isArray(expr) && expr[0] === 'u-' && typeof expr[1] === 'number') v = -expr[1]
|
|
15
|
+
else if (typeof expr === 'string') v = repOf(expr)?.intConst ?? ctx.scope.constInts?.get(expr) ?? null
|
|
16
|
+
return v != null && Number.isInteger(v) && v >= I32_MIN && v <= I32_MAX ? v : null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Non-negative integer literal — used for string/typed-array index bounds. */
|
|
20
|
+
export const nonNegIntLiteral = (node) => { const n = intLiteralValue(node); return n != null && n >= 0 ? n : null }
|
|
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
|
+
|
|
31
|
+
/** Fold compile-time integer expressions (literals, const bindings, + - * <<). */
|
|
32
|
+
export function constIntExpr(node) {
|
|
33
|
+
let lit = intLiteralValue(node)
|
|
34
|
+
if (lit == null && typeof node === 'number' && Number.isInteger(node)) lit = node
|
|
35
|
+
if (lit == null && Array.isArray(node) && node[0] == null && Number.isInteger(node[1])) lit = node[1]
|
|
36
|
+
if (lit != null) return lit
|
|
37
|
+
if (typeof node === 'string') return repOf(node)?.intConst ?? ctx.scope.constInts?.get(node) ?? null
|
|
38
|
+
if (!Array.isArray(node)) return null
|
|
39
|
+
const op = node[0]
|
|
40
|
+
if (op === 'u-') {
|
|
41
|
+
const v = constIntExpr(node[1])
|
|
42
|
+
return v == null ? null : -v
|
|
43
|
+
}
|
|
44
|
+
if (node.length !== 3) return null
|
|
45
|
+
const a = constIntExpr(node[1]), b = constIntExpr(node[2])
|
|
46
|
+
if (a == null || b == null) return null
|
|
47
|
+
if (op === '+') return a + b
|
|
48
|
+
if (op === '-') return a - b
|
|
49
|
+
if (op === '*') return a * b
|
|
50
|
+
if (op === '<<') return a << b
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
export const NO_VALUE = Symbol('no-static-property-key')
|
|
56
|
+
|
|
57
|
+
export function staticPropertyKey(node) {
|
|
58
|
+
const value = staticValue(node)
|
|
59
|
+
return value === NO_VALUE ? null : String(value)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function staticValue(node) {
|
|
63
|
+
if (node === undefined) return undefined
|
|
64
|
+
if (node === null || typeof node === 'number' || typeof node === 'boolean') return node
|
|
65
|
+
if (typeof node === 'string') return ctx.scope.constStrs?.get(node) ?? NO_VALUE
|
|
66
|
+
if (!Array.isArray(node)) return NO_VALUE
|
|
67
|
+
|
|
68
|
+
const [op, ...args] = node
|
|
69
|
+
if (op == null) return args.length ? args[0] : undefined
|
|
70
|
+
if (op === 'str') return args[0]
|
|
71
|
+
// Self-host kernel boundary marks a literal bool as `['bool', 1|0]` (interop
|
|
72
|
+
// normalizeBigints), where native keeps `[, true]` (caught by op==null above).
|
|
73
|
+
// Recover the boolean from its 0/1 carrier so const-folded keys/conditions
|
|
74
|
+
// resolve on the kernel leg (e.g. `{ [true ? 3 : 4]: 5 }`).
|
|
75
|
+
if (op === 'bool') { const c = staticValue(args[0]); return c === NO_VALUE ? NO_VALUE : !!c }
|
|
76
|
+
if (op === '[]' && args.length === 1) return staticValue(args[0])
|
|
77
|
+
if (op === '()' && args[0] === 'String' && args.length === 2) {
|
|
78
|
+
const value = staticValue(args[1])
|
|
79
|
+
return value === NO_VALUE ? NO_VALUE : String(value)
|
|
80
|
+
}
|
|
81
|
+
if (op === '()' && args[0] === 'Number' && args.length === 2) {
|
|
82
|
+
const value = staticValue(args[1])
|
|
83
|
+
return value === NO_VALUE ? NO_VALUE : Number(value)
|
|
84
|
+
}
|
|
85
|
+
if (op === '?:' || op === '?') {
|
|
86
|
+
const cond = staticValue(args[0])
|
|
87
|
+
return cond === NO_VALUE ? NO_VALUE : staticValue(cond ? args[1] : args[2])
|
|
88
|
+
}
|
|
89
|
+
if (op === '&&' || op === '||') {
|
|
90
|
+
const left = staticValue(args[0])
|
|
91
|
+
if (left === NO_VALUE) return NO_VALUE
|
|
92
|
+
return op === '&&' ? (left ? staticValue(args[1]) : left) : (left ? left : staticValue(args[1]))
|
|
93
|
+
}
|
|
94
|
+
if (op === '??') {
|
|
95
|
+
const left = staticValue(args[0])
|
|
96
|
+
return left === NO_VALUE ? NO_VALUE : left == null ? staticValue(args[1]) : left
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (args.length === 1) {
|
|
100
|
+
const value = staticValue(args[0])
|
|
101
|
+
if (value === NO_VALUE) return NO_VALUE
|
|
102
|
+
// Parser emits raw `-`/`+` for both unary and binary; prep later normalizes
|
|
103
|
+
// unary to `u-`/`u+`, but staticPropertyKey runs on raw parser AST.
|
|
104
|
+
if (op === 'u+' || op === '+') return +value
|
|
105
|
+
if (op === 'u-' || op === '-') return -value
|
|
106
|
+
if (op === '!') return !value
|
|
107
|
+
if (op === '~') return ~value
|
|
108
|
+
return NO_VALUE
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (args.length === 2) {
|
|
112
|
+
const left = staticValue(args[0])
|
|
113
|
+
const right = staticValue(args[1])
|
|
114
|
+
if (left === NO_VALUE || right === NO_VALUE) return NO_VALUE
|
|
115
|
+
switch (op) {
|
|
116
|
+
case '+': return typeof left === 'string' || typeof right === 'string' ? String(left) + String(right) : Number(left) + Number(right)
|
|
117
|
+
case '-': return Number(left) - Number(right)
|
|
118
|
+
case '*': return Number(left) * Number(right)
|
|
119
|
+
case '/': return Number(left) / Number(right)
|
|
120
|
+
case '%': return Number(left) % Number(right)
|
|
121
|
+
case '**': return Number(left) ** Number(right)
|
|
122
|
+
case '&': return Number(left) & Number(right)
|
|
123
|
+
case '|': return Number(left) | Number(right)
|
|
124
|
+
case '^': return Number(left) ^ Number(right)
|
|
125
|
+
case '<<': return Number(left) << Number(right)
|
|
126
|
+
case '>>': return Number(left) >> Number(right)
|
|
127
|
+
case '>>>': return Number(left) >>> Number(right)
|
|
128
|
+
default: return NO_VALUE
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return NO_VALUE
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Decode a `['{}', ...]` AST's children into `{names, values}`, or null if any
|
|
136
|
+
* property is non-static-key (computed key, spread, shorthand). Matches the
|
|
137
|
+
* emitter's flatten rule for comma-grouped props. Used by collectProgramFacts,
|
|
138
|
+
* narrowSignatures, and objLiteralSchemaId; the emitter (module/object.js)
|
|
139
|
+
* does its own decoding because it must handle the spread/computed-key paths. */
|
|
140
|
+
export function staticObjectProps(args) {
|
|
141
|
+
const raw = args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',' ? args[0].slice(1) : args
|
|
142
|
+
const names = [], values = []
|
|
143
|
+
for (const p of raw) {
|
|
144
|
+
if (!Array.isArray(p) || p[0] !== ':' || typeof p[1] !== 'string') return null
|
|
145
|
+
names.push(p[1]); values.push(p[2])
|
|
146
|
+
}
|
|
147
|
+
return names.length ? { names, values } : null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function staticArrayElems(expr) {
|
|
151
|
+
if (!Array.isArray(expr)) return null
|
|
152
|
+
if (expr[0] === '[') return expr.slice(1)
|
|
153
|
+
if (expr[0] !== '[]' || expr.length >= 3) return null
|
|
154
|
+
const arg = expr[1]
|
|
155
|
+
if (arg == null) return []
|
|
156
|
+
return Array.isArray(arg) && arg[0] === ',' ? arg.slice(1) : [arg]
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Schema-id for an object literal expression. Returns null on dynamic keys, spread, shorthand. */
|
|
160
|
+
export function objLiteralSchemaId(expr) {
|
|
161
|
+
if (!Array.isArray(expr) || expr[0] !== '{}' || !ctx.schema?.register) return null
|
|
162
|
+
const parsed = staticObjectProps(expr.slice(1))
|
|
163
|
+
return parsed ? ctx.schema.register(parsed.names) : null
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Resolve schemaId of an expression, given a per-function schemaId map for locals.
|
|
167
|
+
* Used for both intra-function arr elem-schema observation and func.arrayElemSchema
|
|
168
|
+
* return inference. Recognizes: object literals, var names with bound schemaId,
|
|
169
|
+
* user fn calls with narrowed result schema, ?: / && / || when both branches agree. */
|
|
170
|
+
export function exprSchemaId(expr, localSchemaMap) {
|
|
171
|
+
if (typeof expr === 'string') {
|
|
172
|
+
if (localSchemaMap?.has(expr)) return localSchemaMap.get(expr)
|
|
173
|
+
return ctx.schema?.idOf?.(expr) ?? null
|
|
174
|
+
}
|
|
175
|
+
if (!Array.isArray(expr)) return null
|
|
176
|
+
const op = expr[0]
|
|
177
|
+
if (op === '{}') return objLiteralSchemaId(expr)
|
|
178
|
+
if (op === '()' && typeof expr[1] === 'string') {
|
|
179
|
+
const f = ctx.func.map?.get(expr[1])
|
|
180
|
+
if (f?.valResult === VAL.OBJECT && f.sig?.ptrAux != null) return f.sig.ptrAux
|
|
181
|
+
return null
|
|
182
|
+
}
|
|
183
|
+
if (op === '?:') {
|
|
184
|
+
const a = exprSchemaId(expr[2], localSchemaMap)
|
|
185
|
+
const b = exprSchemaId(expr[3], localSchemaMap)
|
|
186
|
+
return a != null && a === b ? a : null
|
|
187
|
+
}
|
|
188
|
+
if (op === '&&' || op === '||') {
|
|
189
|
+
const a = exprSchemaId(expr[1], localSchemaMap)
|
|
190
|
+
const b = exprSchemaId(expr[2], localSchemaMap)
|
|
191
|
+
return a != null && a === b ? a : null
|
|
192
|
+
}
|
|
193
|
+
return null
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function inlineArraySid(name) {
|
|
197
|
+
if (typeof name !== 'string') return null
|
|
198
|
+
// structInline is keyed on the per-function `localReps` rep, so it is only
|
|
199
|
+
// consistent for a *function-local* array — a write site and a read site in the
|
|
200
|
+
// same frame agree. A module-global array is read across functions whose frames
|
|
201
|
+
// carry no rep for it, so the carrier would diverge: `G.push({a,b})` in one
|
|
202
|
+
// function flattens the struct into K cells, while `G.length` / `G[i].a` in
|
|
203
|
+
// another sees a plain array (K=1) and reads garbage. Never inline a global's
|
|
204
|
+
// element struct — the plain Array<ptr> representation is consistent everywhere.
|
|
205
|
+
if (ctx.scope.globals?.has(name)) return null
|
|
206
|
+
const sid = ctx.func.localReps?.get(name)?.arrayElemSchema
|
|
207
|
+
return sid != null && ctx.schema.inlineArray?.has(sid) ? sid : null
|
|
208
|
+
}
|