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.
Files changed (86) hide show
  1. package/README.md +315 -318
  2. package/bench/README.md +369 -0
  3. package/bench/bench.svg +102 -0
  4. package/cli.js +104 -30
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6024 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +362 -84
  9. package/interop.js +159 -186
  10. package/jz.svg +5 -0
  11. package/jzify/arguments.js +97 -0
  12. package/jzify/bundler.js +382 -0
  13. package/jzify/classes.js +328 -0
  14. package/jzify/hoist-vars.js +177 -0
  15. package/jzify/index.js +51 -0
  16. package/jzify/names.js +37 -0
  17. package/jzify/switch.js +106 -0
  18. package/jzify/transform.js +349 -0
  19. package/layout.js +184 -0
  20. package/module/array.js +377 -176
  21. package/module/collection.js +639 -145
  22. package/module/console.js +55 -43
  23. package/module/core.js +264 -153
  24. package/module/date.js +15 -3
  25. package/module/function.js +73 -6
  26. package/module/index.js +2 -1
  27. package/module/json.js +226 -61
  28. package/module/math.js +551 -187
  29. package/module/number.js +327 -60
  30. package/module/object.js +474 -184
  31. package/module/regex.js +255 -25
  32. package/module/schema.js +24 -6
  33. package/module/simd.js +117 -0
  34. package/module/string.js +621 -226
  35. package/module/symbol.js +1 -1
  36. package/module/timer.js +9 -14
  37. package/module/typedarray.js +459 -73
  38. package/package.json +58 -14
  39. package/src/abi/index.js +39 -23
  40. package/src/abi/string.js +38 -41
  41. package/src/ast.js +460 -0
  42. package/src/autoload.js +29 -24
  43. package/src/bridge.js +111 -0
  44. package/src/compile/analyze-scans.js +824 -0
  45. package/src/compile/analyze.js +1600 -0
  46. package/src/compile/emit-assign.js +411 -0
  47. package/src/compile/emit.js +3512 -0
  48. package/src/compile/flow-types.js +103 -0
  49. package/src/{compile.js → compile/index.js} +778 -128
  50. package/src/{infer.js → compile/infer.js} +62 -98
  51. package/src/compile/loop-divmod.js +155 -0
  52. package/src/{narrow.js → compile/narrow.js} +409 -106
  53. package/src/compile/peel-stencil.js +261 -0
  54. package/src/compile/plan/advise.js +370 -0
  55. package/src/compile/plan/common.js +150 -0
  56. package/src/compile/plan/index.js +122 -0
  57. package/src/compile/plan/inline.js +682 -0
  58. package/src/compile/plan/literals.js +1199 -0
  59. package/src/compile/plan/loops.js +472 -0
  60. package/src/compile/plan/scope.js +649 -0
  61. package/src/compile/program-facts.js +423 -0
  62. package/src/ctx.js +220 -64
  63. package/src/ir.js +589 -172
  64. package/src/kind-traits.js +132 -0
  65. package/src/kind.js +524 -0
  66. package/src/op-policy.js +57 -0
  67. package/src/{optimize.js → optimize/index.js} +1432 -473
  68. package/src/optimize/vectorize.js +4660 -0
  69. package/src/param-reps.js +65 -0
  70. package/src/parse.js +44 -0
  71. package/src/{prepare.js → prepare/index.js} +649 -205
  72. package/src/prepare/lift-iife.js +149 -0
  73. package/src/reps.js +116 -0
  74. package/src/resolve.js +12 -3
  75. package/src/static.js +208 -0
  76. package/src/type.js +651 -0
  77. package/src/{assemble.js → wat/assemble.js} +275 -55
  78. package/src/wat/optimize.js +3938 -0
  79. package/transform.js +21 -0
  80. package/wasi.js +47 -5
  81. package/src/analyze.js +0 -3818
  82. package/src/emit.js +0 -3040
  83. package/src/jzify.js +0 -1580
  84. package/src/plan.js +0 -2132
  85. package/src/vectorize.js +0 -1088
  86. /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 both `import` and `export ... from` statements (statement position).
18
- // Lazy `[^'"]*?` consumes whatever sits between the keyword and the first quote.
19
- const importRe = /^\s*(?:import|export)\s+[^'"]*?['"]([^'"]+)['"]/gm
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
+ }