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.
- package/README.md +288 -314
- package/bench/README.md +319 -0
- package/bench/bench.svg +112 -0
- package/cli.js +32 -24
- package/index.js +177 -55
- package/interop.js +88 -159
- package/jz.svg +5 -0
- package/jzify/arguments.js +97 -0
- package/jzify/bundler.js +382 -0
- package/jzify/classes.js +328 -0
- package/jzify/hoist-vars.js +177 -0
- package/jzify/index.js +51 -0
- package/jzify/names.js +37 -0
- package/jzify/switch.js +106 -0
- package/jzify/transform.js +349 -0
- package/layout.js +179 -0
- package/module/array.js +322 -153
- package/module/collection.js +603 -145
- package/module/console.js +55 -43
- package/module/core.js +266 -153
- package/module/date.js +15 -3
- package/module/function.js +73 -6
- package/module/index.js +2 -1
- package/module/json.js +226 -61
- package/module/math.js +414 -186
- package/module/number.js +306 -60
- package/module/object.js +448 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +85 -0
- package/module/string.js +586 -220
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +45 -48
- package/package.json +41 -12
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +26 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +661 -0
- package/src/compile/analyze.js +1565 -0
- package/src/compile/emit-assign.js +408 -0
- package/src/compile/emit.js +3201 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +497 -125
- package/src/{infer.js → compile/infer.js} +27 -98
- package/src/{narrow.js → compile/narrow.js} +302 -96
- package/src/compile/plan/advise.js +316 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +118 -0
- package/src/compile/plan/inline.js +679 -0
- package/src/compile/plan/literals.js +984 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +573 -0
- package/src/compile/program-facts.js +404 -0
- package/src/ctx.js +176 -58
- package/src/ir.js +540 -171
- package/src/kind-traits.js +105 -0
- package/src/kind.js +462 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1106 -446
- package/src/optimize/vectorize.js +1874 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +589 -202
- package/src/reps.js +115 -0
- package/src/resolve.js +12 -3
- package/src/static.js +199 -0
- package/src/type.js +647 -0
- package/src/{assemble.js → wat/assemble.js} +86 -48
- package/src/wat/optimize.js +3760 -0
- package/transform.js +21 -0
- package/wasi.js +47 -5
- package/src/analyze.js +0 -3818
- package/src/emit.js +0 -3040
- package/src/jzify.js +0 -1580
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /package/src/{codegen.js → wat/codegen.js} +0 -0
|
@@ -0,0 +1,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
|
+
}
|
package/src/op-policy.js
ADDED
|
@@ -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
|
+
}
|