jz 0.5.1 → 0.6.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 (80) hide show
  1. package/README.md +288 -314
  2. package/bench/README.md +319 -0
  3. package/bench/bench.svg +112 -0
  4. package/cli.js +32 -24
  5. package/index.js +177 -55
  6. package/interop.js +88 -159
  7. package/jz.svg +5 -0
  8. package/jzify/arguments.js +97 -0
  9. package/jzify/bundler.js +382 -0
  10. package/jzify/classes.js +328 -0
  11. package/jzify/hoist-vars.js +177 -0
  12. package/jzify/index.js +51 -0
  13. package/jzify/names.js +37 -0
  14. package/jzify/switch.js +106 -0
  15. package/jzify/transform.js +349 -0
  16. package/layout.js +179 -0
  17. package/module/array.js +322 -153
  18. package/module/collection.js +603 -145
  19. package/module/console.js +55 -43
  20. package/module/core.js +266 -153
  21. package/module/date.js +15 -3
  22. package/module/function.js +73 -6
  23. package/module/index.js +2 -1
  24. package/module/json.js +226 -61
  25. package/module/math.js +414 -186
  26. package/module/number.js +306 -60
  27. package/module/object.js +448 -184
  28. package/module/regex.js +255 -25
  29. package/module/schema.js +24 -6
  30. package/module/simd.js +85 -0
  31. package/module/string.js +586 -220
  32. package/module/symbol.js +1 -1
  33. package/module/timer.js +9 -14
  34. package/module/typedarray.js +45 -48
  35. package/package.json +41 -12
  36. package/src/abi/index.js +39 -23
  37. package/src/abi/string.js +38 -41
  38. package/src/ast.js +460 -0
  39. package/src/autoload.js +26 -24
  40. package/src/bridge.js +111 -0
  41. package/src/compile/analyze-scans.js +661 -0
  42. package/src/compile/analyze.js +1565 -0
  43. package/src/compile/emit-assign.js +408 -0
  44. package/src/compile/emit.js +3201 -0
  45. package/src/compile/flow-types.js +103 -0
  46. package/src/{compile.js → compile/index.js} +497 -125
  47. package/src/{infer.js → compile/infer.js} +27 -98
  48. package/src/{narrow.js → compile/narrow.js} +302 -96
  49. package/src/compile/plan/advise.js +316 -0
  50. package/src/compile/plan/common.js +150 -0
  51. package/src/compile/plan/index.js +118 -0
  52. package/src/compile/plan/inline.js +679 -0
  53. package/src/compile/plan/literals.js +984 -0
  54. package/src/compile/plan/loops.js +472 -0
  55. package/src/compile/plan/scope.js +573 -0
  56. package/src/compile/program-facts.js +404 -0
  57. package/src/ctx.js +176 -58
  58. package/src/ir.js +540 -171
  59. package/src/kind-traits.js +105 -0
  60. package/src/kind.js +462 -0
  61. package/src/op-policy.js +57 -0
  62. package/src/{optimize.js → optimize/index.js} +1106 -446
  63. package/src/optimize/vectorize.js +1874 -0
  64. package/src/param-reps.js +65 -0
  65. package/src/parse.js +44 -0
  66. package/src/{prepare.js → prepare/index.js} +589 -202
  67. package/src/reps.js +115 -0
  68. package/src/resolve.js +12 -3
  69. package/src/static.js +199 -0
  70. package/src/type.js +647 -0
  71. package/src/{assemble.js → wat/assemble.js} +86 -48
  72. package/src/wat/optimize.js +3760 -0
  73. package/transform.js +21 -0
  74. package/wasi.js +47 -5
  75. package/src/analyze.js +0 -3818
  76. package/src/emit.js +0 -3040
  77. package/src/jzify.js +0 -1580
  78. package/src/plan.js +0 -2132
  79. package/src/vectorize.js +0 -1088
  80. /package/src/{codegen.js → wat/codegen.js} +0 -0
@@ -0,0 +1,105 @@
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
+ return null
98
+ }
99
+
100
+ export function typedCtorElemValType(ctor) {
101
+ if (!ctor) return null
102
+ const isView = ctor.endsWith('.view')
103
+ const name = isView ? ctor.slice(4, -5) : ctor.slice(4)
104
+ return name === 'BigInt64Array' || name === 'BigUint64Array' ? VAL.BIGINT : VAL.NUMBER
105
+ }
package/src/kind.js ADDED
@@ -0,0 +1,462 @@
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 } from './static.js'
12
+ import {
13
+ BOOL_OPS, NUMERIC_BINARY_OPS, NUMERIC_UNARY_OPS, COMPOUND_NUMERIC_OPS,
14
+ calleeValType, methodValType, 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
+ VT['&&'] = VT['||'] = (args) => {
149
+ const ta = valTypeOf(args[0]), tb = valTypeOf(args[1])
150
+ if (ta && ta === tb) return ta
151
+ if (ta === VAL.BOOL && tb && tb !== VAL.BOOL) return tb
152
+ if (tb === VAL.BOOL && ta && ta !== VAL.BOOL) return ta
153
+ return null
154
+ }
155
+
156
+ // `[]` op covers both array literals (1 arg) and index access (2 args).
157
+ // Array literal: `[]` → ['[]', null]; `[1,2]` → ['[]', [',', ...]]; `[x]` → ['[]', x].
158
+ // Index access: `arr[i]` → ['[]', arr, i].
159
+ VT['[]'] = (args) => {
160
+ if (args.length < 2) return VAL.ARRAY
161
+ // Indexed read on a known typed-array receiver yields Number except for
162
+ // BigInt64Array/BigUint64Array, whose i64 carriers must stay BigInt-typed.
163
+ if (typeof args[0] === 'string' && lookupValType(args[0]) === VAL.TYPED)
164
+ return typedCtorElemValType(ctx.types.typedElem?.get(args[0])) || VAL.NUMBER
165
+ // Indexed read on a STRING returns a 1-char string (SSO at runtime).
166
+ if (typeof args[0] === 'string' && lookupValType(args[0]) === VAL.STRING) return VAL.STRING
167
+ if (Array.isArray(args[0]) && valTypeOf(args[0]) === VAL.STRING) return VAL.STRING
168
+ // Indexed read on a known Array<VAL> receiver: bind by rep.arrayElemValType.
169
+ // Set by analyzeValTypes from body observations + emitFunc preseed for params.
170
+ if (typeof args[0] === 'string') {
171
+ const elemVt = ctx.func.localReps?.get(args[0])?.arrayElemValType
172
+ if (elemVt) return elemVt
173
+ }
174
+ // Indexed read on an inline all-numeric array literal — `[2,4,2,9][i]` (floatbeat
175
+ // chord/pattern tables; literal op is `[`, elements inline). Every element is a
176
+ // Number, so the load is a Number; this lets toNumF64 skip __to_num on the result
177
+ // and propagates numericness outward (e.g. a closure arg that then marks its param
178
+ // numeric, or the surrounding `-arr[i]` that feeds a numeric accumulator).
179
+ if (Array.isArray(args[0]) && args[0][0] === '[' && args[0].length > 1
180
+ && args[0].slice(1).every(e => valTypeOf(e) === VAL.NUMBER)) return VAL.NUMBER
181
+ return null
182
+ }
183
+
184
+ VT['.'] = (args) => {
185
+ if (typeof args[1] !== 'string') return null
186
+ // Schema slot read: when `varName` has a bound schemaId and `.prop` resolves
187
+ // to a slot whose VAL kind is monomorphic across program-wide observations,
188
+ // return that kind. Lets `+`, `===`, method dispatch skip runtime str-key
189
+ // checks on numeric properties of known shapes. Precise-only — see
190
+ // ctx.schema.slotVT for why structural subtyping is intentionally off.
191
+ if (ctx.schema?.slotVT) {
192
+ const slotVT = ctx.schema.slotVT(args[0], args[1])
193
+ if (slotVT) return slotVT
194
+ }
195
+ // OBJECT `.prop` propagation: when the receiver chain roots at a binding
196
+ // sourced from `JSON.parse(stringConst)`, walk the shape tree to recover the
197
+ // child's val-type. Generic for any compile-time-known JSON literal.
198
+ const sh = shapeOf(args[0])
199
+ if (sh?.val === VAL.OBJECT || sh?.val === VAL.HASH) {
200
+ const child = sh.props[args[1]]
201
+ if (child) return child.val
202
+ }
203
+ return null
204
+ }
205
+
206
+ // Arithmetic expressions: BigInt if either operand is BigInt, else number.
207
+ const numericBinaryVT = (args) =>
208
+ valTypeOf(args[0]) === VAL.BIGINT || valTypeOf(args[1]) === VAL.BIGINT ? VAL.BIGINT : VAL.NUMBER
209
+ for (const op of NUMERIC_BINARY_OPS) VT[op] = numericBinaryVT
210
+ // `~`, `++`, `--`, `**` preserve/propagate BigInt…
211
+ const numericUnaryVT = (args) =>
212
+ valTypeOf(args[0]) === VAL.BIGINT || (args[1] != null && valTypeOf(args[1]) === VAL.BIGINT) ? VAL.BIGINT : VAL.NUMBER
213
+ for (const op of NUMERIC_UNARY_OPS) VT[op] = numericUnaryVT
214
+ // …while `>>>` and unary-plus throw on bigint operands so they always yield Number.
215
+ VT['>>>'] = VT['u+'] = () => VAL.NUMBER
216
+
217
+ VT['+'] = (args) => {
218
+ const ta = valTypeOf(args[0]), tb = valTypeOf(args[1])
219
+ if (ta === VAL.STRING || tb === VAL.STRING) return VAL.STRING
220
+ if (ta === VAL.BIGINT || tb === VAL.BIGINT) return VAL.BIGINT
221
+ return VAL.NUMBER
222
+ }
223
+
224
+ // Assignment & compound-assign expressions return the rhs value. Without this,
225
+ // `(a = x*x) + (b = y*y)` falls through to null and `+` emits the polymorphic
226
+ // string-concat dispatch on two pure-numeric subexpressions.
227
+ VT['='] = (args) => valTypeOf(args[1])
228
+ VT['+='] = (args) => {
229
+ const ta = typeof args[0] === 'string' ? lookupValType(args[0]) : null
230
+ const tb = valTypeOf(args[1])
231
+ if (ta === VAL.STRING || tb === VAL.STRING) return VAL.STRING
232
+ if (ta === VAL.BIGINT || tb === VAL.BIGINT) return VAL.BIGINT
233
+ return VAL.NUMBER
234
+ }
235
+ const compoundNumericVT = (args) => {
236
+ const ta = typeof args[0] === 'string' ? lookupValType(args[0]) : null
237
+ return ta === VAL.BIGINT || valTypeOf(args[1]) === VAL.BIGINT ? VAL.BIGINT : VAL.NUMBER
238
+ }
239
+ for (const op of COMPOUND_NUMERIC_OPS) VT[op] = compoundNumericVT
240
+
241
+ VT['()'] = (args) => {
242
+ const callee = args[0]
243
+ // __iter_arr normalizes an iterable to an index-iterable Array: Set→keys,
244
+ // Map→[k,v], while Array/String/TypedArray pass through unchanged. The result
245
+ // type drives the downstream arr[i]/.length dispatch, so a Set/Map source
246
+ // becomes ARRAY and everything else keeps the source's own type.
247
+ if (callee === '__iter_arr') {
248
+ const t = valTypeOf(args[1])
249
+ return t === VAL.SET || t === VAL.MAP ? VAL.ARRAY : t
250
+ }
251
+ // Ternary is parsed as call to '?' operator: ['()', ['?', cond, a, b]]
252
+ if (Array.isArray(callee) && callee[0] === '?') {
253
+ const truthy = literalTruthiness(callee[1])
254
+ if (truthy != null) return valTypeOf(truthy ? callee[2] : callee[3])
255
+ const ta = valTypeOf(callee[2]), tb = valTypeOf(callee[3])
256
+ return ta && ta === tb ? ta : null
257
+ }
258
+ // Constructor results + user function return-type inference
259
+ if (typeof callee === 'string') {
260
+ if (callee === 'JSON.parse') {
261
+ const src = jsonConstString(args[1])
262
+ if (src != null) {
263
+ const c = src.trimStart()[0]
264
+ if (c === '{') return VAL.OBJECT
265
+ if (c === '[') return VAL.ARRAY
266
+ if (c === '"') return VAL.STRING
267
+ if (c === 't' || c === 'f' || c === '-' || (c >= '0' && c <= '9')) return VAL.NUMBER
268
+ }
269
+ } else {
270
+ const vt = calleeValType(callee, args, ctx)
271
+ if (vt != null) return vt
272
+ }
273
+ }
274
+ if (Array.isArray(callee) && callee[0] === '.') {
275
+ const [, obj, method] = callee
276
+ const vt = methodValType(method, obj, valTypeOf(obj), ctx)
277
+ if (vt != null) return vt
278
+ }
279
+ return null
280
+ }
281
+
282
+ export function valTypeOf(expr) {
283
+ if (expr == null) return null
284
+ if (typeof expr === 'number') return VAL.NUMBER
285
+ if (typeof expr === 'boolean') return VAL.BOOL
286
+ if (typeof expr === 'bigint') return VAL.BIGINT
287
+ if (typeof expr === 'string') return lookupValType(expr)
288
+ if (!Array.isArray(expr)) return null
289
+
290
+ const [op, ...args] = expr
291
+ if (op == null) {
292
+ // Literal forms: [] = undefined, [null, null] = null, [null, n] = number/bigint, [, bool] = boolean
293
+ if (args.length === 0) return null // undefined literal
294
+ if (args[0] == null) return null // null literal
295
+ if (typeof args[0] === 'boolean') return VAL.BOOL
296
+ if (typeof args[0] === 'symbol') return null // prepared null sentinel
297
+ return typeof args[0] === 'bigint' ? VAL.BIGINT : VAL.NUMBER
298
+ }
299
+ return VT[op]?.(args) ?? null
300
+ }
301
+
302
+ export function jsonConstString(expr) {
303
+ if (Array.isArray(expr) && expr[0] === 'str' && typeof expr[1] === 'string') return expr[1]
304
+ if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'string') return expr[1]
305
+ if (typeof expr === 'string') {
306
+ return ctx.scope.shapeStrs?.get(expr) ?? ctx.scope.constStrs?.get(expr) ?? null
307
+ }
308
+ return null
309
+ }
310
+
311
+ function jsonShapeStrings(expr) {
312
+ const single = jsonConstString(expr)
313
+ if (single != null) return [single]
314
+ if (Array.isArray(expr) && expr[0] === '[]' && typeof expr[1] === 'string') return ctx.scope.shapeStrArrays?.get(expr[1]) ?? null
315
+ return null
316
+ }
317
+
318
+ /** Build a structural shape tree from a parsed JSON value. Each node is
319
+ * `{ val, props?, elem? }` — `val` is the inferred VAL kind (matches
320
+ * rep.val in localReps entries). Lets `valTypeOf` propagate VAL kinds
321
+ * through `.prop` chains and `[i]` reads on bindings sourced from
322
+ * `JSON.parse` of a compile-time-known string. Polymorphic arrays drop
323
+ * their `elem`. */
324
+ function shapeOfJsonValue(v) {
325
+ if (v === null || v === undefined) return null
326
+ if (typeof v === 'number') return { val: VAL.NUMBER }
327
+ if (typeof v === 'string') return { val: VAL.STRING }
328
+ if (typeof v === 'boolean') return { val: VAL.NUMBER }
329
+ if (Array.isArray(v)) {
330
+ let elem = null
331
+ for (const x of v) {
332
+ const s = shapeOfJsonValue(x)
333
+ if (!s) { elem = null; break }
334
+ if (!elem) elem = s
335
+ else if (!shapeUnifies(elem, s)) { elem = null; break }
336
+ }
337
+ return { val: VAL.ARRAY, elem }
338
+ }
339
+ if (typeof v === 'object') {
340
+ const props = Object.create(null)
341
+ const names = Object.keys(v)
342
+ for (const k of names) {
343
+ const s = shapeOfJsonValue(v[k])
344
+ if (s) props[k] = s
345
+ }
346
+ return { val: VAL.OBJECT, props, names }
347
+ }
348
+ return null
349
+ }
350
+
351
+ function shapeUnifies(a, b) {
352
+ if (!a || !b || a.val !== b.val) return false
353
+ if (a.val === VAL.OBJECT || a.val === VAL.HASH) {
354
+ const ak = Object.keys(a.props), bk = Object.keys(b.props)
355
+ if (ak.length !== bk.length) return false
356
+ for (const k of ak) {
357
+ if (!b.props[k] || !shapeUnifies(a.props[k], b.props[k])) return false
358
+ }
359
+ }
360
+ if (a.val === VAL.ARRAY) {
361
+ if ((a.elem == null) !== (b.elem == null)) return false
362
+ if (a.elem && !shapeUnifies(a.elem, b.elem)) return false
363
+ }
364
+ return true
365
+ }
366
+
367
+ function shapeLayoutUnifies(a, b) {
368
+ if (!shapeUnifies(a, b)) return false
369
+ if (a.val === VAL.OBJECT || a.val === VAL.HASH) {
370
+ if (a.names?.length !== b.names?.length) return false
371
+ for (let i = 0; i < a.names.length; i++) if (a.names[i] !== b.names[i]) return false
372
+ }
373
+ if (a.val === VAL.ARRAY && a.elem) return shapeLayoutUnifies(a.elem, b.elem)
374
+ return true
375
+ }
376
+
377
+ function parseJsonShape(src) {
378
+ if (typeof src !== 'string') return null
379
+ let parsed
380
+ try { parsed = JSON.parse(src) } catch { return null }
381
+ return shapeOfJsonValue(parsed)
382
+ }
383
+
384
+ function parseUnifiedJsonShape(srcs) {
385
+ if (!srcs?.length) return null
386
+ let out = null
387
+ for (const src of srcs) {
388
+ const sh = parseJsonShape(src)
389
+ if (!sh) return null
390
+ if (!out) out = sh
391
+ else if (!shapeLayoutUnifies(out, sh)) return null
392
+ }
393
+ return out
394
+ }
395
+
396
+ /** Resolve the json shape for an expression by walking name → rep.jsonShape and
397
+ * `.prop` / `[i]` indirection. Returns null when shape is unknown at this site. */
398
+ export function shapeOf(expr) {
399
+ if (typeof expr === 'string')
400
+ return ctx.func.localReps?.get(expr)?.jsonShape
401
+ ?? ctx.scope.globalReps?.get(expr)?.jsonShape
402
+ ?? null
403
+ if (!Array.isArray(expr)) return null
404
+ const [op, ...args] = expr
405
+ if (op === '()' && args[0] === 'JSON.parse') {
406
+ const srcs = jsonShapeStrings(args[1])
407
+ if (srcs) return parseUnifiedJsonShape(srcs)
408
+ }
409
+ if (op === '.' && typeof args[1] === 'string') {
410
+ const parent = shapeOf(args[0])
411
+ if (parent?.val === VAL.OBJECT || parent?.val === VAL.HASH) return parent.props[args[1]] || null
412
+ }
413
+ if (op === '[]' && args.length === 2) {
414
+ const parent = shapeOf(args[0])
415
+ if (parent?.val === VAL.ARRAY) return parent.elem || null
416
+ }
417
+ return null
418
+ }
419
+
420
+ /** Spread source's static schema (key list) or null if unknown at compile time.
421
+ * Mirrors module/object.js `resolveSchema` so kind inference predicts the same
422
+ * OBJECT-vs-HASH decision emitObjectSpread makes (kept here to keep kind.js
423
+ * cycle-free — it must not import the object stdlib module). */
424
+ function spreadSchema(obj) {
425
+ // A parameter's compile-time schema is an inferred/union guess (and is unbound
426
+ // during this body's analysis but bound by emit) — see resolveSchema in
427
+ // module/object.js. Treat params as unknown so the spread result is HASH-typed
428
+ // consistently across analyze and emit; otherwise reads misdispatch.
429
+ if (typeof obj === 'string') {
430
+ if (ctx.func.current?.params?.some(p => p.name === obj)) return null
431
+ return ctx.schema?.resolve?.(obj)
432
+ }
433
+ if (Array.isArray(obj) && obj[0] === '{}')
434
+ return obj.slice(1).filter(p => Array.isArray(p) && p[0] === ':').map(p => p[1])
435
+ const sh = shapeOf(obj)
436
+ return (sh?.val === VAL.OBJECT && sh.names) ? sh.names : null
437
+ }
438
+
439
+ /** Build a structural shape from a `{}` AST node — recursive for nested
440
+ * object/array literals + propagating shapes through identifier references
441
+ * (so `let G = {…}; let H = {x: G}` carries G's shape under H.x). Returns
442
+ * null when any property breaks the static-shape contract (computed key,
443
+ * spread, non-shape value). Only called from `recordGlobalRep` — local
444
+ * bindings keep relying on `shapeOf` whose narrower contract (JSON.parse /
445
+ * traversal only) lets `Object.assign(a, …)` extend `a`'s schema without
446
+ * locking a static jsonShape onto it. */
447
+ export function shapeOfObjectLiteralAst(expr) {
448
+ if (typeof expr === 'string') return shapeOf(expr)
449
+ if (!Array.isArray(expr) || expr[0] !== '{}') return shapeOf(expr)
450
+ const raw = expr.length === 2 && Array.isArray(expr[1]) && expr[1][0] === ','
451
+ ? expr[1].slice(1)
452
+ : expr.slice(1)
453
+ const props = Object.create(null)
454
+ const names = []
455
+ for (const p of raw) {
456
+ if (!Array.isArray(p) || p[0] !== ':' || typeof p[1] !== 'string') return null
457
+ names.push(p[1])
458
+ const child = shapeOfObjectLiteralAst(p[2])
459
+ if (child) props[p[1]] = child
460
+ }
461
+ return names.length ? { val: VAL.OBJECT, props, names } : null
462
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Shared op policy for jzify (transform) and prepare (reject).
3
+ *
4
+ * `.jz` source bypasses jzify, so prepare must enforce reject rules for both paths.
5
+ * jzify lowers `var`/`function`/`class`/`arguments` before prepare; prepare rejects
6
+ * survivors. Identifiers (`this`, `eval`, …) have no safe lowering in either pass.
7
+ *
8
+ * @module op-policy
9
+ */
10
+
11
+ /** Ops prepare rejects when they appear in the AST (handler or identifier). */
12
+ export const REJECT_OPS = {
13
+ async: 'async/await not supported: WASM is synchronous',
14
+ await: 'async/await not supported: WASM is synchronous',
15
+ class: 'class not supported: use object literals',
16
+ yield: 'generators not supported: use loops',
17
+ instanceof: 'instanceof not supported: use typeof',
18
+ with: '`with` not supported: deprecated',
19
+ ':': 'labeled statements not supported',
20
+ var: '`var` not supported: use let/const',
21
+ function: '`function` not supported: use arrow functions',
22
+ }
23
+
24
+ /** Bare identifiers prepare rejects (no jzify lowering). */
25
+ export const REJECT_IDENTS = {
26
+ with: '`with` not supported',
27
+ class: '`class` not supported',
28
+ yield: '`yield` not supported',
29
+ this: '`this` not supported: use explicit parameter',
30
+ super: '`super` not supported: no class inheritance',
31
+ arguments: '`arguments` not supported: use rest params',
32
+ eval: '`eval` not supported',
33
+ // `const` is an always-reserved word (like `class`/`with` above) — never a valid
34
+ // binding/identifier name in any mode; subscript parses it as a plain identifier,
35
+ // so reject it. NOT `let`: `let` is only *strict-mode* reserved and is a legal
36
+ // identifier in sloppy JS (`var let = 5`), so rejecting it would refuse valid JS
37
+ // (test262 language/expressions/object/let-non-strict-*).
38
+ const: '`const` is a reserved word, not a valid name',
39
+ }
40
+
41
+ /** jzify-only errors for class lowering (no prepare counterpart). */
42
+ export const JZIFY_CLASS_ERRORS = {
43
+ computedMember: 'non-constant computed class member names are not supported',
44
+ computedStaticField: 'non-constant computed static class fields are not supported',
45
+ computedField: 'non-constant computed/destructured class fields are not supported',
46
+ computedStaticMember: 'non-constant computed static class member names are not supported',
47
+ accessor: 'class getters/setters are not supported — jz objects have no accessors',
48
+ staticMember: '`static` class members are not supported yet',
49
+ superProp: '`super` property access is not supported yet',
50
+ }
51
+
52
+ /** Build prepare handler map from shared reject messages. */
53
+ export function rejectHandlers(errFn) {
54
+ const out = {}
55
+ for (const [op, msg] of Object.entries(REJECT_OPS)) out[op] = () => errFn(msg)
56
+ return out
57
+ }