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,132 @@
1
+ /**
2
+ * Declarative value-kind traits — tables consumed by kind.js valTypeOf.
3
+ * @module kind-traits
4
+ */
5
+
6
+ import { VAL } from './reps.js'
7
+
8
+ export const BOOL_OPS = new Set([
9
+ '!', '<', '<=', '>', '>=', '==', '!=', '===', '!==', 'in', 'instanceof',
10
+ ])
11
+
12
+ export const NUMERIC_BINARY_OPS = ['-', 'u-', '*', '/', '%', '&', '|', '^', '<<', '>>']
13
+ export const NUMERIC_UNARY_OPS = new Set(['**', '++', '--', '~', '>>>', 'u+'])
14
+ export const COMPOUND_NUMERIC_OPS = new Set([
15
+ '-=', '*=', '/=', '%=', '**=', '&=', '|=', '^=', '<<=', '>>=', '>>>=',
16
+ ])
17
+
18
+ export const STRING_METHODS = new Set([
19
+ 'toUpperCase', 'toLowerCase', 'toLocaleLowerCase', 'trim', 'trimStart', 'trimEnd',
20
+ 'repeat', 'padStart', 'padEnd', 'replace', 'replaceAll', 'charAt', 'substring',
21
+ ])
22
+
23
+ export const NUMBER_METHODS = new Set(['charCodeAt', 'codePointAt'])
24
+
25
+ // Methods whose result is a boolean. Classifying them VAL.BOOL lets the export
26
+ // boundary materialize the 0/1 carrier as a real boolean (host sees true/false,
27
+ // not 1/0) and lets typeof/String/JSON observe it faithfully — internal branch/
28
+ // arithmetic positions still ride the cheap 0/1 carrier. (`has`/`delete` are
29
+ // guarded on a proven Map/Set receiver below, like `add`/`set`.)
30
+ export const BOOL_METHODS = new Set([
31
+ 'includes', 'some', 'every', 'startsWith', 'endsWith', 'test',
32
+ ])
33
+
34
+ export const CALLEE_VAL = {
35
+ 'new.Set': VAL.SET,
36
+ 'new.Map': VAL.MAP,
37
+ 'new.Date': VAL.DATE,
38
+ 'new.ArrayBuffer': VAL.BUFFER,
39
+ 'new.Array': VAL.ARRAY,
40
+ 'String.fromCharCode': VAL.STRING,
41
+ String: VAL.STRING,
42
+ Boolean: VAL.BOOL,
43
+ BigInt: VAL.BIGINT,
44
+ 'BigInt.asIntN': VAL.BIGINT,
45
+ 'BigInt.asUintN': VAL.BIGINT,
46
+ 'performance.now': VAL.NUMBER,
47
+ 'Date.now': VAL.NUMBER,
48
+ }
49
+
50
+ export function calleeValType(callee, _args, ctx) {
51
+ if (typeof callee !== 'string') return null
52
+ if (callee in CALLEE_VAL) return CALLEE_VAL[callee]
53
+ if (callee.startsWith('new.')) return VAL.TYPED
54
+ if (callee.startsWith('math.')) return VAL.NUMBER
55
+ const hostVT = ctx.module.hostImportValTypes?.get(callee)
56
+ if (hostVT) return hostVT
57
+ // A direct-dispatched local closure proven to return a plain number: its f64 result
58
+ // is never a NaN-boxed pointer, so `toNumF64` can skip the `__to_num` wrapper at the
59
+ // call site. (Populated by closure.make as bodies are emitted, in decl order.)
60
+ const closBody = ctx.func.directClosures?.get(callee)
61
+ if (closBody && ctx.closure?.numericReturn?.has(closBody)) return VAL.NUMBER
62
+ const f = ctx.func.map?.get(callee)
63
+ if (f?.valResult) return f.valResult
64
+ return null
65
+ }
66
+
67
+ export function methodValType(method, obj, objType, ctx) {
68
+ if (method === 'map' || method === 'filter') {
69
+ if (objType === VAL.TYPED) return VAL.TYPED
70
+ if (objType === VAL.ARRAY) return VAL.ARRAY
71
+ return null
72
+ }
73
+ if (method === 'push') return VAL.ARRAY
74
+ if ((method === 'shift' || method === 'pop') && typeof obj === 'string') {
75
+ const elemVt = ctx.func.localReps?.get(obj)?.arrayElemValType
76
+ if (elemVt) return elemVt
77
+ }
78
+ // `.add`/`.set` return their receiver (Set/Map, chainable) — but only when the
79
+ // receiver is *proven* that collection. An unknown receiver is NOT assumed
80
+ // Set/Map: a plain object, user class, or the self-host value-tracker `store`
81
+ // carries an own `add`/`set` closure whose result is whatever it returns;
82
+ // claiming SET/MAP would box that f64 result as a tagged pointer (corrupt read
83
+ // on use). A genuine Map/Set value is proven VAL.MAP/SET and still chains; an
84
+ // untyped Map's `.set` simply yields an untyped (but correct) pointer value.
85
+ // Mirrors the objType guard on map/filter/slice/concat.
86
+ if (method === 'add') return objType === VAL.SET ? VAL.SET : null
87
+ if (method === 'set') return objType === VAL.MAP ? VAL.MAP : null
88
+ if (BOOL_METHODS.has(method)) return VAL.BOOL
89
+ if ((method === 'has' || method === 'delete') && (objType === VAL.MAP || objType === VAL.SET)) return VAL.BOOL
90
+ if (STRING_METHODS.has(method)) return VAL.STRING
91
+ if (NUMBER_METHODS.has(method)) return VAL.NUMBER
92
+ if (method === 'split') return VAL.ARRAY
93
+ if (method === 'slice' || method === 'concat') {
94
+ if (objType === VAL.STRING || objType === VAL.ARRAY || objType === VAL.TYPED) return objType
95
+ return null
96
+ }
97
+ // .subarray / .toReversed / .toSorted / .with all return a typed array of the same kind.
98
+ if (method === 'subarray' || method === 'toReversed' || method === 'toSorted' || method === 'with')
99
+ return objType === VAL.TYPED ? VAL.TYPED : null
100
+ return null
101
+ }
102
+
103
+ // Built-in PROPERTY val-types — the property-read mirror of methodValType.
104
+ // These are language invariants: `.length` is always a number on the sized
105
+ // value kinds, `.size` on Set/Map, `.byteLength`/`.byteOffset` on typed arrays.
106
+ // Without this, `arr.length + x` sees `.length` as untyped and routes `+`
107
+ // through the __is_str_key string-concat dispatch — even though `.length` can
108
+ // never be a string on a known sized kind. (Object schema slots override this
109
+ // earlier in VT['.'], so `{length:'x'}.length` keeps its true slot type.)
110
+ //
111
+ // Gate on a known objType: an untyped receiver could be an object with a
112
+ // string-valued shadow of the same name, so leave it null there (conservative).
113
+ // null-proto: user code reads `.valueOf`, `.toString` etc. — a plain `{}` would
114
+ // expose inherited Object.prototype members as bogus "numeric props".
115
+ const NUMERIC_PROPS = Object.assign(Object.create(null), {
116
+ length: new Set([VAL.STRING, VAL.ARRAY, VAL.TYPED]),
117
+ byteLength: new Set([VAL.TYPED, VAL.BUFFER]),
118
+ byteOffset: new Set([VAL.TYPED]),
119
+ size: new Set([VAL.SET, VAL.MAP]),
120
+ })
121
+ export function propValType(prop, objType) {
122
+ if (objType == null) return null
123
+ const kinds = NUMERIC_PROPS[prop]
124
+ return kinds && kinds.has(objType) ? VAL.NUMBER : null
125
+ }
126
+
127
+ export function typedCtorElemValType(ctor) {
128
+ if (!ctor) return null
129
+ const isView = ctor.endsWith('.view')
130
+ const name = isView ? ctor.slice(4, -5) : ctor.slice(4)
131
+ return name === 'BigInt64Array' || name === 'BigUint64Array' ? VAL.BIGINT : VAL.NUMBER
132
+ }
package/src/kind.js ADDED
@@ -0,0 +1,524 @@
1
+ /**
2
+ * Expression value KIND inference (STRING, ARRAY, …) + JSON shape propagation.
3
+ *
4
+ * Cycle-free w.r.t. analyze.js body walkers — reads ctx + reps only.
5
+ *
6
+ * @module kind
7
+ */
8
+
9
+ import { ctx } from './ctx.js'
10
+ import { VAL, lookupValType } from './reps.js'
11
+ import { intLiteralValue, staticIndexKey } from './static.js'
12
+ import {
13
+ BOOL_OPS, NUMERIC_BINARY_OPS, NUMERIC_UNARY_OPS, COMPOUND_NUMERIC_OPS,
14
+ calleeValType, methodValType, propValType, typedCtorElemValType,
15
+ } from './kind-traits.js'
16
+
17
+ export { typedCtorElemValType } from './kind-traits.js'
18
+
19
+ function literalTruthiness(expr) {
20
+ if (typeof expr === 'number') return expr !== 0 && expr === expr
21
+ if (typeof expr === 'boolean') return expr
22
+ if (typeof expr === 'bigint') return expr !== 0n
23
+ if (typeof expr === 'string') {
24
+ const value = intLiteralValue(expr)
25
+ if (value != null) return value !== 0
26
+ }
27
+ if (Array.isArray(expr)) {
28
+ const [op, ...args] = expr
29
+ if (op == null) {
30
+ if (args.length === 0 || args[0] == null) return false
31
+ return literalTruthiness(args[0])
32
+ }
33
+ if (op === 'bool') return literalTruthiness(args[0])
34
+ if (op === 'nan') return false
35
+ if (op === 'str' && typeof args[0] === 'string') return args[0].length !== 0
36
+ if (op === '()' && expr.length === 2) return literalTruthiness(args[0])
37
+ if (BOOL_OPS.has(op)) {
38
+ const result = literalBool(expr)
39
+ if (result != null) return result
40
+ }
41
+ if (op === '?:' || op === '?') {
42
+ const truthy = literalTruthiness(args[0])
43
+ if (truthy != null) return literalTruthiness(truthy ? args[1] : args[2])
44
+ const thenTruthy = literalTruthiness(args[1])
45
+ const elseTruthy = literalTruthiness(args[2])
46
+ if (thenTruthy != null && thenTruthy === elseTruthy) return thenTruthy
47
+ }
48
+ if (op === '()' && Array.isArray(args[0]) && args[0][0] === '?') {
49
+ const truthy = literalTruthiness(args[0][1])
50
+ if (truthy != null) return literalTruthiness(truthy ? args[0][2] : args[0][3])
51
+ const thenTruthy = literalTruthiness(args[0][2])
52
+ const elseTruthy = literalTruthiness(args[0][3])
53
+ if (thenTruthy != null && thenTruthy === elseTruthy) return thenTruthy
54
+ }
55
+ }
56
+ return null
57
+ }
58
+
59
+ function literalValue(expr) {
60
+ if (expr == null || typeof expr === 'number' || typeof expr === 'boolean' || typeof expr === 'bigint') return expr
61
+ if (!Array.isArray(expr)) return undefined
62
+ const [op, ...args] = expr
63
+ if (op == null) return args.length ? args[0] : undefined
64
+ if (op === 'nan') return NaN
65
+ if (op === 'str') return args[0]
66
+ if (op === 'bool') {
67
+ const truthy = literalTruthiness(args[0])
68
+ return truthy == null ? undefined : truthy
69
+ }
70
+ if (op === '()' && expr.length === 2) return literalValue(args[0])
71
+ return undefined
72
+ }
73
+
74
+ function literalBool(expr) {
75
+ if (!Array.isArray(expr)) return null
76
+ const [op, left, right] = expr
77
+ if (op === '!') {
78
+ const truthy = literalTruthiness(left)
79
+ return truthy == null ? null : !truthy
80
+ }
81
+ if (!['<', '<=', '>', '>=', '==', '!=', '===', '!=='].includes(op)) return null
82
+ const a = literalValue(left), b = literalValue(right)
83
+ if (a === undefined || b === undefined) return null
84
+ switch (op) {
85
+ case '<': return a < b
86
+ case '<=': return a <= b
87
+ case '>': return a > b
88
+ case '>=': return a >= b
89
+ case '==': return a == b
90
+ case '!=': return a != b
91
+ case '===': return a === b
92
+ case '!==': return a !== b
93
+ }
94
+ return null
95
+ }
96
+
97
+ /**
98
+ * Per-op val-type rules — the dispatch table behind `valTypeOf`. Each entry
99
+ * takes the op's args and returns a VAL kind or undefined (→ null). Set-driven
100
+ * families (BOOL_OPS, NUMERIC_*) enroll at module init, so adding an operator
101
+ * is a kind-traits table entry, not a new branch here.
102
+ */
103
+ const VT = Object.create(null)
104
+
105
+ // Self-describing boolean literal from the host→kernel AST boundary (normalizeBigints).
106
+ VT.bool = () => VAL.BOOL
107
+ // Boolean-result operators: relational/equality compares and logical-not always
108
+ // yield a boolean. (`&&`/`||` are value-preserving, not boolean — excluded.)
109
+ for (const op of BOOL_OPS) VT[op] = VT.bool
110
+ // Self-describing bigint literal (`normalizeBigints`) — same VAL as a raw `255n`.
111
+ VT.bigint = () => VAL.BIGINT
112
+ VT['['] = () => VAL.ARRAY
113
+ VT.str = VT.strcat = () => VAL.STRING
114
+ VT['=>'] = () => VAL.CLOSURE
115
+ VT['//'] = () => VAL.REGEX
116
+
117
+ VT['{}'] = (args) => {
118
+ const hasSpread = args.some(p => Array.isArray(p) && p[0] === '...')
119
+ if (!hasSpread) return args[0]?.[0] === ':' ? VAL.OBJECT : null
120
+ // Spread literal — mirror emitObjectSpread (module/object.js). When every
121
+ // spread source has a compile-time schema, emit builds a fixed-shape OBJECT
122
+ // and the existing schema-by-name read path resolves props with no val-type
123
+ // tag, so leave it untyped (tagging OBJECT here regresses it — the merged
124
+ // schema isn't bound to this name). When any source's schema is unknown, emit
125
+ // builds a dynamic HASH (emitDynamicSpread); that result carries no schema, so
126
+ // the binding MUST be HASH-typed or computed/static reads silently misdispatch
127
+ // (fixed-slot / array index) and return undefined — the bug this fixes.
128
+ for (const p of args)
129
+ if (Array.isArray(p) && p[0] === '...' && !spreadSchema(p[1])) {
130
+ // `{ ...src }` with a single unknown spread aliases src — carry its type.
131
+ return args.length === 1 ? valTypeOf(args[0][1]) : VAL.HASH
132
+ }
133
+ return null
134
+ }
135
+
136
+ VT['?:'] = (args) => {
137
+ const truthy = literalTruthiness(args[0])
138
+ if (truthy != null) return valTypeOf(truthy ? args[1] : args[2])
139
+ const ta = valTypeOf(args[1]), tb = valTypeOf(args[2])
140
+ return ta && ta === tb ? ta : null
141
+ }
142
+
143
+ // Value-preserving logical: `&&`/`||` return one of their operands.
144
+ // When both sides share a type, return it. When one side is boolean
145
+ // (a condition/guard) and the other has a known non-boolean type,
146
+ // return the non-boolean type — common in `condition && numericValue`
147
+ // guard patterns where the falsey boolean is coerced to 0 in numeric context.
148
+ // `a && b` / `a || b` / `a ?? b` all yield one of the two operands, so the result
149
+ // type is their common type (else unknown). Giving `??` a type — not just ||/&& —
150
+ // lets `numA ?? numB` read NaN-safe (value-typed NUMBER → f64.eq) instead of routing
151
+ // through the bit-comparing __is_truthy, which mis-reads a non-canonical NaN.
152
+ VT['&&'] = VT['||'] = VT['??'] = (args) => {
153
+ const ta = valTypeOf(args[0]), tb = valTypeOf(args[1])
154
+ if (ta && ta === tb) return ta
155
+ if (ta === VAL.BOOL && tb && tb !== VAL.BOOL) return tb
156
+ if (tb === VAL.BOOL && ta && ta !== VAL.BOOL) return ta
157
+ return null
158
+ }
159
+
160
+ // `[]` op covers both array literals (1 arg) and index access (2 args).
161
+ // Array literal: `[]` → ['[]', null]; `[1,2]` → ['[]', [',', ...]]; `[x]` → ['[]', x].
162
+ // Index access: `arr[i]` → ['[]', arr, i].
163
+ VT['[]'] = (args) => {
164
+ if (args.length < 2) return VAL.ARRAY
165
+ // SRoA flat-array slot read: `a[k]` (static index) where `a` dissolved into
166
+ // scalar `a#i` locals (scanFlatObjects). A write-once slot's value-type is its
167
+ // element literal's — same numeric-binding as the `VT['.']` object case, so
168
+ // `a[0] * 2` stays a plain f64 op instead of the polymorphic ToNumber battery.
169
+ if (typeof args[0] === 'string') {
170
+ const flat = ctx.func.flatObjects?.get(args[0])
171
+ if (flat) {
172
+ const k = staticIndexKey(args[1])
173
+ if (k != null && !flat.written?.has(k)) {
174
+ const i = flat.names.indexOf(k)
175
+ if (i >= 0 && flat.values[i] !== undefined) return valTypeOf(flat.values[i])
176
+ }
177
+ }
178
+ }
179
+ // Indexed read on a known typed-array receiver yields Number except for
180
+ // BigInt64Array/BigUint64Array, whose i64 carriers must stay BigInt-typed.
181
+ if (typeof args[0] === 'string' && lookupValType(args[0]) === VAL.TYPED)
182
+ return typedCtorElemValType(ctx.types.typedElem?.get(args[0])) || VAL.NUMBER
183
+ // Indexed read on a STRING returns a 1-char string (SSO at runtime).
184
+ if (typeof args[0] === 'string' && lookupValType(args[0]) === VAL.STRING) return VAL.STRING
185
+ if (Array.isArray(args[0]) && valTypeOf(args[0]) === VAL.STRING) return VAL.STRING
186
+ // Indexed read on a known Array<VAL> receiver: bind by rep.arrayElemValType.
187
+ // Set by analyzeValTypes from body observations + emitFunc preseed for params.
188
+ if (typeof args[0] === 'string') {
189
+ const elemVt = ctx.func.localReps?.get(args[0])?.arrayElemValType
190
+ if (elemVt) return elemVt
191
+ // Module-level const array (a numeric/uniform table): its element val-type was
192
+ // recorded on the global rep at decl time. Trust it only when no function element-
193
+ // writes the array — dynWriteVars holds every var written via a non-named-property
194
+ // index, so a `X[i]=str` anywhere disables this and falls back to the untyped read.
195
+ if (!ctx.func.localReps?.has(args[0])) {
196
+ const gElem = ctx.scope.globalReps?.get(args[0])?.arrayElemValType
197
+ if (gElem && !ctx.types?.dynWriteVars?.has(args[0])) return gElem
198
+ }
199
+ }
200
+ // Direct double-index on a module-level nested numeric table — `C[i][j]` where
201
+ // `C = [[…number…], …]`. The receiver is itself a single-index read of a global
202
+ // array whose nested element kind was recorded at decl time. Same dynWriteVars
203
+ // guard (now root-aware, so a `C[i][j]=…` write anywhere disables it).
204
+ if (Array.isArray(args[0]) && args[0][0] === '[]' && args[0].length === 3 && typeof args[0][1] === 'string') {
205
+ const base = args[0][1]
206
+ if (!ctx.func.localReps?.has(base)) {
207
+ const gNested = ctx.scope.globalReps?.get(base)?.arrayElemElemValType
208
+ if (gNested && !ctx.types?.dynWriteVars?.has(base)) return gNested
209
+ }
210
+ }
211
+ // Indexed read on an inline all-numeric array literal — `[2,4,2,9][i]` (floatbeat
212
+ // chord/pattern tables; literal op is `[`, elements inline). Every element is a
213
+ // Number, so the load is a Number; this lets toNumF64 skip __to_num on the result
214
+ // and propagates numericness outward (e.g. a closure arg that then marks its param
215
+ // numeric, or the surrounding `-arr[i]` that feeds a numeric accumulator).
216
+ if (Array.isArray(args[0]) && args[0][0] === '[' && args[0].length > 1
217
+ && args[0].slice(1).every(e => valTypeOf(e) === VAL.NUMBER)) return VAL.NUMBER
218
+ return null
219
+ }
220
+
221
+ VT['.'] = (args) => {
222
+ if (typeof args[1] !== 'string') return null
223
+ // SRoA flat-object slot read: `p.x` where `p` dissolved into scalar `p#i`
224
+ // locals (scanFlatObjects). A write-once slot's value-type IS its literal
225
+ // initializer's, so bind by it — exactly as a plain `let slot = value` local
226
+ // would. Without this `p.x * 2` looks like "could be anything" and pulls the
227
+ // ToNumber + string-format battery, though it can only be numeric. Computed
228
+ // on-demand (not cached at analyze time) because param val-types — `{x:n}`'s
229
+ // `n` is numeric-by-divergence — are only seeded at emit. A reassigned slot
230
+ // (`p.x = …`) stays untyped: its runtime value may differ from the literal.
231
+ if (typeof args[0] === 'string') {
232
+ const flat = ctx.func.flatObjects?.get(args[0])
233
+ if (flat && !flat.written?.has(args[1])) {
234
+ const i = flat.names.indexOf(args[1])
235
+ if (i >= 0 && flat.values[i] !== undefined) return valTypeOf(flat.values[i])
236
+ }
237
+ }
238
+ // Schema slot read: when `varName` has a bound schemaId and `.prop` resolves
239
+ // to a slot whose VAL kind is monomorphic across program-wide observations,
240
+ // return that kind. Lets `+`, `===`, method dispatch skip runtime str-key
241
+ // checks on numeric properties of known shapes. Precise-only — see
242
+ // ctx.schema.slotVT for why structural subtyping is intentionally off.
243
+ if (ctx.schema?.slotVT) {
244
+ const slotVT = ctx.schema.slotVT(args[0], args[1])
245
+ if (slotVT) return slotVT
246
+ }
247
+ // OBJECT `.prop` propagation: when the receiver chain roots at a binding
248
+ // sourced from `JSON.parse(stringConst)`, walk the shape tree to recover the
249
+ // child's val-type. Generic for any compile-time-known JSON literal.
250
+ const sh = shapeOf(args[0])
251
+ if (sh?.val === VAL.OBJECT || sh?.val === VAL.HASH) {
252
+ const child = sh.props[args[1]]
253
+ if (child) return child.val
254
+ }
255
+ // Built-in property on a known sized kind — `.length` on STRING/ARRAY/TYPED,
256
+ // `.size` on SET/MAP, `.byteLength`/`.byteOffset` on TYPED/BUFFER. These are
257
+ // language invariants (the property is always a number on that kind), so typing
258
+ // them NUMBER lets `+` skip the string-concat dispatch. Object schema slots
259
+ // resolved above override this, keeping user-defined same-name slots sound.
260
+ const objType = typeof args[0] === 'string' ? lookupValType(args[0]) : valTypeOf(args[0])
261
+ const pvt = propValType(args[1], objType)
262
+ if (pvt) return pvt
263
+ return null
264
+ }
265
+
266
+ // Arithmetic expressions: BigInt if either operand is BigInt, else number.
267
+ const numericBinaryVT = (args) =>
268
+ valTypeOf(args[0]) === VAL.BIGINT || valTypeOf(args[1]) === VAL.BIGINT ? VAL.BIGINT : VAL.NUMBER
269
+ for (const op of NUMERIC_BINARY_OPS) VT[op] = numericBinaryVT
270
+ // `~`, `++`, `--`, `**` preserve/propagate BigInt…
271
+ const numericUnaryVT = (args) =>
272
+ valTypeOf(args[0]) === VAL.BIGINT || (args[1] != null && valTypeOf(args[1]) === VAL.BIGINT) ? VAL.BIGINT : VAL.NUMBER
273
+ for (const op of NUMERIC_UNARY_OPS) VT[op] = numericUnaryVT
274
+ // …while `>>>` and unary-plus throw on bigint operands so they always yield Number.
275
+ VT['>>>'] = VT['u+'] = () => VAL.NUMBER
276
+
277
+ VT['+'] = (args) => {
278
+ const ta = valTypeOf(args[0]), tb = valTypeOf(args[1])
279
+ if (ta === VAL.STRING || tb === VAL.STRING) return VAL.STRING
280
+ if (ta === VAL.BIGINT || tb === VAL.BIGINT) return VAL.BIGINT
281
+ return VAL.NUMBER
282
+ }
283
+
284
+ // Assignment & compound-assign expressions return the rhs value. Without this,
285
+ // `(a = x*x) + (b = y*y)` falls through to null and `+` emits the polymorphic
286
+ // string-concat dispatch on two pure-numeric subexpressions.
287
+ VT['='] = (args) => valTypeOf(args[1])
288
+ VT['+='] = (args) => {
289
+ const ta = typeof args[0] === 'string' ? lookupValType(args[0]) : null
290
+ const tb = valTypeOf(args[1])
291
+ if (ta === VAL.STRING || tb === VAL.STRING) return VAL.STRING
292
+ if (ta === VAL.BIGINT || tb === VAL.BIGINT) return VAL.BIGINT
293
+ return VAL.NUMBER
294
+ }
295
+ const compoundNumericVT = (args) => {
296
+ const ta = typeof args[0] === 'string' ? lookupValType(args[0]) : null
297
+ return ta === VAL.BIGINT || valTypeOf(args[1]) === VAL.BIGINT ? VAL.BIGINT : VAL.NUMBER
298
+ }
299
+ for (const op of COMPOUND_NUMERIC_OPS) VT[op] = compoundNumericVT
300
+
301
+ VT['()'] = (args) => {
302
+ const callee = args[0]
303
+ // __iter_arr normalizes an iterable to an index-iterable Array: Set→keys,
304
+ // Map→[k,v], while Array/String/TypedArray pass through unchanged. The result
305
+ // type drives the downstream arr[i]/.length dispatch, so a Set/Map source
306
+ // becomes ARRAY and everything else keeps the source's own type.
307
+ if (callee === '__iter_arr') {
308
+ const t = valTypeOf(args[1])
309
+ return t === VAL.SET || t === VAL.MAP ? VAL.ARRAY : t
310
+ }
311
+ // for-in's read-only key list (src/prepare) — always an Array of key strings.
312
+ if (callee === '__keys_ro') return VAL.ARRAY
313
+ // Ternary is parsed as call to '?' operator: ['()', ['?', cond, a, b]]
314
+ if (Array.isArray(callee) && callee[0] === '?') {
315
+ const truthy = literalTruthiness(callee[1])
316
+ if (truthy != null) return valTypeOf(truthy ? callee[2] : callee[3])
317
+ const ta = valTypeOf(callee[2]), tb = valTypeOf(callee[3])
318
+ return ta && ta === tb ? ta : null
319
+ }
320
+ // Constructor results + user function return-type inference
321
+ if (typeof callee === 'string') {
322
+ if (callee === 'JSON.parse') {
323
+ const src = jsonConstString(args[1])
324
+ if (src != null) {
325
+ const c = src.trimStart()[0]
326
+ if (c === '{') return VAL.OBJECT
327
+ if (c === '[') return VAL.ARRAY
328
+ if (c === '"') return VAL.STRING
329
+ if (c === 't' || c === 'f' || c === '-' || (c >= '0' && c <= '9')) return VAL.NUMBER
330
+ }
331
+ } else {
332
+ const vt = calleeValType(callee, args, ctx)
333
+ if (vt != null) return vt
334
+ }
335
+ }
336
+ if (Array.isArray(callee) && callee[0] === '.') {
337
+ const [, obj, method] = callee
338
+ const vt = methodValType(method, obj, valTypeOf(obj), ctx)
339
+ if (vt != null) return vt
340
+ }
341
+ return null
342
+ }
343
+
344
+ export function valTypeOf(expr) {
345
+ if (expr == null) return null
346
+ if (typeof expr === 'number') return VAL.NUMBER
347
+ if (typeof expr === 'boolean') return VAL.BOOL
348
+ if (typeof expr === 'bigint') return VAL.BIGINT
349
+ if (typeof expr === 'string') return lookupValType(expr)
350
+ if (!Array.isArray(expr)) return null
351
+
352
+ const [op, ...args] = expr
353
+ if (op == null) {
354
+ // Literal forms: [] = undefined, [null, null] = null, [null, n] = number/bigint, [, bool] = boolean
355
+ if (args.length === 0) return null // undefined literal
356
+ if (args[0] == null) return null // null literal
357
+ if (typeof args[0] === 'boolean') return VAL.BOOL
358
+ if (typeof args[0] === 'symbol') return null // prepared null sentinel
359
+ return typeof args[0] === 'bigint' ? VAL.BIGINT : VAL.NUMBER
360
+ }
361
+ return VT[op]?.(args) ?? null
362
+ }
363
+
364
+ export function jsonConstString(expr) {
365
+ if (Array.isArray(expr) && expr[0] === 'str' && typeof expr[1] === 'string') return expr[1]
366
+ if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'string') return expr[1]
367
+ if (typeof expr === 'string') {
368
+ return ctx.scope.shapeStrs?.get(expr) ?? ctx.scope.constStrs?.get(expr) ?? null
369
+ }
370
+ return null
371
+ }
372
+
373
+ function jsonShapeStrings(expr) {
374
+ const single = jsonConstString(expr)
375
+ if (single != null) return [single]
376
+ if (Array.isArray(expr) && expr[0] === '[]' && typeof expr[1] === 'string') return ctx.scope.shapeStrArrays?.get(expr[1]) ?? null
377
+ return null
378
+ }
379
+
380
+ /** Build a structural shape tree from a parsed JSON value. Each node is
381
+ * `{ val, props?, elem? }` — `val` is the inferred VAL kind (matches
382
+ * rep.val in localReps entries). Lets `valTypeOf` propagate VAL kinds
383
+ * through `.prop` chains and `[i]` reads on bindings sourced from
384
+ * `JSON.parse` of a compile-time-known string. Polymorphic arrays drop
385
+ * their `elem`. */
386
+ function shapeOfJsonValue(v) {
387
+ if (v === null || v === undefined) return null
388
+ if (typeof v === 'number') return { val: VAL.NUMBER }
389
+ if (typeof v === 'string') return { val: VAL.STRING }
390
+ if (typeof v === 'boolean') return { val: VAL.NUMBER }
391
+ if (Array.isArray(v)) {
392
+ let elem = null
393
+ for (const x of v) {
394
+ const s = shapeOfJsonValue(x)
395
+ if (!s) { elem = null; break }
396
+ if (!elem) elem = s
397
+ else if (!shapeUnifies(elem, s)) { elem = null; break }
398
+ }
399
+ return { val: VAL.ARRAY, elem }
400
+ }
401
+ if (typeof v === 'object') {
402
+ const props = Object.create(null)
403
+ const names = Object.keys(v)
404
+ for (const k of names) {
405
+ const s = shapeOfJsonValue(v[k])
406
+ if (s) props[k] = s
407
+ }
408
+ return { val: VAL.OBJECT, props, names }
409
+ }
410
+ return null
411
+ }
412
+
413
+ function shapeUnifies(a, b) {
414
+ if (!a || !b || a.val !== b.val) return false
415
+ if (a.val === VAL.OBJECT || a.val === VAL.HASH) {
416
+ const ak = Object.keys(a.props), bk = Object.keys(b.props)
417
+ if (ak.length !== bk.length) return false
418
+ for (const k of ak) {
419
+ if (!b.props[k] || !shapeUnifies(a.props[k], b.props[k])) return false
420
+ }
421
+ }
422
+ if (a.val === VAL.ARRAY) {
423
+ if ((a.elem == null) !== (b.elem == null)) return false
424
+ if (a.elem && !shapeUnifies(a.elem, b.elem)) return false
425
+ }
426
+ return true
427
+ }
428
+
429
+ function shapeLayoutUnifies(a, b) {
430
+ if (!shapeUnifies(a, b)) return false
431
+ if (a.val === VAL.OBJECT || a.val === VAL.HASH) {
432
+ if (a.names?.length !== b.names?.length) return false
433
+ for (let i = 0; i < a.names.length; i++) if (a.names[i] !== b.names[i]) return false
434
+ }
435
+ if (a.val === VAL.ARRAY && a.elem) return shapeLayoutUnifies(a.elem, b.elem)
436
+ return true
437
+ }
438
+
439
+ function parseJsonShape(src) {
440
+ if (typeof src !== 'string') return null
441
+ let parsed
442
+ try { parsed = JSON.parse(src) } catch { return null }
443
+ return shapeOfJsonValue(parsed)
444
+ }
445
+
446
+ function parseUnifiedJsonShape(srcs) {
447
+ if (!srcs?.length) return null
448
+ let out = null
449
+ for (const src of srcs) {
450
+ const sh = parseJsonShape(src)
451
+ if (!sh) return null
452
+ if (!out) out = sh
453
+ else if (!shapeLayoutUnifies(out, sh)) return null
454
+ }
455
+ return out
456
+ }
457
+
458
+ /** Resolve the json shape for an expression by walking name → rep.jsonShape and
459
+ * `.prop` / `[i]` indirection. Returns null when shape is unknown at this site. */
460
+ export function shapeOf(expr) {
461
+ if (typeof expr === 'string')
462
+ return ctx.func.localReps?.get(expr)?.jsonShape
463
+ ?? ctx.scope.globalReps?.get(expr)?.jsonShape
464
+ ?? null
465
+ if (!Array.isArray(expr)) return null
466
+ const [op, ...args] = expr
467
+ if (op === '()' && args[0] === 'JSON.parse') {
468
+ const srcs = jsonShapeStrings(args[1])
469
+ if (srcs) return parseUnifiedJsonShape(srcs)
470
+ }
471
+ if (op === '.' && typeof args[1] === 'string') {
472
+ const parent = shapeOf(args[0])
473
+ if (parent?.val === VAL.OBJECT || parent?.val === VAL.HASH) return parent.props[args[1]] || null
474
+ }
475
+ if (op === '[]' && args.length === 2) {
476
+ const parent = shapeOf(args[0])
477
+ if (parent?.val === VAL.ARRAY) return parent.elem || null
478
+ }
479
+ return null
480
+ }
481
+
482
+ /** Spread source's static schema (key list) or null if unknown at compile time.
483
+ * Mirrors module/object.js `resolveSchema` so kind inference predicts the same
484
+ * OBJECT-vs-HASH decision emitObjectSpread makes (kept here to keep kind.js
485
+ * cycle-free — it must not import the object stdlib module). */
486
+ function spreadSchema(obj) {
487
+ // A parameter's compile-time schema is an inferred/union guess (and is unbound
488
+ // during this body's analysis but bound by emit) — see resolveSchema in
489
+ // module/object.js. Treat params as unknown so the spread result is HASH-typed
490
+ // consistently across analyze and emit; otherwise reads misdispatch.
491
+ if (typeof obj === 'string') {
492
+ if (ctx.func.current?.params?.some(p => p.name === obj)) return null
493
+ return ctx.schema?.resolve?.(obj)
494
+ }
495
+ if (Array.isArray(obj) && obj[0] === '{}')
496
+ return obj.slice(1).filter(p => Array.isArray(p) && p[0] === ':').map(p => p[1])
497
+ const sh = shapeOf(obj)
498
+ return (sh?.val === VAL.OBJECT && sh.names) ? sh.names : null
499
+ }
500
+
501
+ /** Build a structural shape from a `{}` AST node — recursive for nested
502
+ * object/array literals + propagating shapes through identifier references
503
+ * (so `let G = {…}; let H = {x: G}` carries G's shape under H.x). Returns
504
+ * null when any property breaks the static-shape contract (computed key,
505
+ * spread, non-shape value). Only called from `recordGlobalRep` — local
506
+ * bindings keep relying on `shapeOf` whose narrower contract (JSON.parse /
507
+ * traversal only) lets `Object.assign(a, …)` extend `a`'s schema without
508
+ * locking a static jsonShape onto it. */
509
+ export function shapeOfObjectLiteralAst(expr) {
510
+ if (typeof expr === 'string') return shapeOf(expr)
511
+ if (!Array.isArray(expr) || expr[0] !== '{}') return shapeOf(expr)
512
+ const raw = expr.length === 2 && Array.isArray(expr[1]) && expr[1][0] === ','
513
+ ? expr[1].slice(1)
514
+ : expr.slice(1)
515
+ const props = Object.create(null)
516
+ const names = []
517
+ for (const p of raw) {
518
+ if (!Array.isArray(p) || p[0] !== ':' || typeof p[1] !== 'string') return null
519
+ names.push(p[1])
520
+ const child = shapeOfObjectLiteralAst(p[2])
521
+ if (child) props[p[1]] = child
522
+ }
523
+ return names.length ? { val: VAL.OBJECT, props, names } : null
524
+ }