jz 0.8.0 → 0.9.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 +56 -9
- package/bench/README.md +121 -50
- package/bench/bench.svg +27 -27
- package/cli.js +3 -1
- package/dist/interop.js +1 -1
- package/dist/jz.js +7541 -6178
- package/index.js +165 -74
- package/interop.js +189 -17
- package/jzify/arguments.js +32 -1
- package/jzify/async.js +409 -0
- package/jzify/classes.js +136 -18
- package/jzify/generators.js +639 -0
- package/jzify/hoist-vars.js +73 -1
- package/jzify/index.js +123 -4
- package/jzify/names.js +1 -0
- package/jzify/transform.js +239 -1
- package/layout.js +48 -3
- package/module/array.js +318 -43
- package/module/atomics.js +144 -0
- package/module/collection.js +1429 -155
- package/module/core.js +431 -40
- package/module/date.js +142 -120
- package/module/fs.js +144 -0
- package/module/function.js +6 -3
- package/module/index.js +4 -1
- package/module/json.js +270 -49
- package/module/math.js +1032 -145
- package/module/number.js +532 -163
- package/module/object.js +353 -95
- package/module/regex.js +157 -7
- package/module/schema.js +100 -2
- package/module/string.js +428 -93
- package/module/typedarray.js +264 -72
- package/module/web.js +36 -0
- package/package.json +5 -5
- package/src/abi/string.js +82 -11
- package/src/ast.js +11 -5
- package/src/autoload.js +28 -1
- package/src/bridge.js +7 -2
- package/src/compile/analyze-scans.js +22 -7
- package/src/compile/analyze.js +136 -30
- package/src/compile/dyn-closure-tables.js +277 -0
- package/src/compile/emit-assign.js +281 -15
- package/src/compile/emit.js +1055 -70
- package/src/compile/index.js +277 -29
- package/src/compile/infer.js +42 -4
- package/src/compile/inplace-store.js +329 -0
- package/src/compile/loop-recurrence.js +167 -0
- package/src/compile/narrow.js +555 -18
- package/src/compile/peel-stencil.js +5 -0
- package/src/compile/plan/index.js +45 -3
- package/src/compile/plan/inline.js +8 -1
- package/src/compile/plan/literals.js +39 -0
- package/src/compile/plan/scope.js +430 -23
- package/src/compile/program-facts.js +732 -38
- package/src/ctx.js +64 -9
- package/src/helper-counters.js +7 -1
- package/src/ir.js +113 -5
- package/src/kind-traits.js +66 -3
- package/src/kind.js +84 -12
- package/src/op-policy.js +7 -4
- package/src/optimize/index.js +1179 -704
- package/src/optimize/recurse.js +2 -2
- package/src/optimize/vectorize.js +982 -67
- package/src/prepare/index.js +809 -67
- package/src/prepare/math-kernel.js +331 -0
- package/src/prepare/pre-eval.js +714 -0
- package/src/snapshot.js +194 -0
- package/src/type.js +1170 -56
- package/src/wat/assemble.js +403 -65
- package/src/wat/codegen.js +74 -14
- package/transform.js +113 -4
- package/wasi.js +3 -0
package/src/wat/codegen.js
CHANGED
|
@@ -20,6 +20,19 @@ function wrapBlock(node, depth) {
|
|
|
20
20
|
return '{ ' + codegen(node, depth) + '; }'
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// Effective precedence of a node in expression position: binaries from `prec`,
|
|
24
|
+
// ternary/arrow/comma below them, atoms/calls/members bind tightest.
|
|
25
|
+
const ASSOC = new Set(['&&', '||', '+', '*', '&', '|', '^'])
|
|
26
|
+
const cprec = (n) => !Array.isArray(n) || n[0] == null ? Infinity
|
|
27
|
+
: prec[n[0]] != null ? prec[n[0]]
|
|
28
|
+
: n[0] === '?' || n[0] === '?:' ? 1.5
|
|
29
|
+
: n[0] === '=>' ? 0.7
|
|
30
|
+
: n[0] === ',' ? 0.1
|
|
31
|
+
: Infinity
|
|
32
|
+
|
|
33
|
+
/** Print child, parenthesized when it binds looser than the context. */
|
|
34
|
+
const paren = (n, ctx) => cprec(n) < ctx ? '(' + codegen(n) + ')' : codegen(n)
|
|
35
|
+
|
|
23
36
|
/** Generate jz source from AST. Enforces semicolons. */
|
|
24
37
|
export function codegen(node, depth = 0) {
|
|
25
38
|
if (node == null) return ''
|
|
@@ -31,8 +44,13 @@ export function codegen(node, depth = 0) {
|
|
|
31
44
|
const [op, ...a] = node
|
|
32
45
|
const ind = INDENT.repeat(depth), ind1 = INDENT.repeat(depth + 1)
|
|
33
46
|
|
|
34
|
-
// Literal: [, value]
|
|
35
|
-
|
|
47
|
+
// Literal: [, value]. `[]` (no payload) is subscript's undefined encoding —
|
|
48
|
+
// print it as `undefined`, not `null` (distinct values through a round-trip).
|
|
49
|
+
if (op == null) return typeof a[0] === 'string' ? JSON.stringify(a[0]) : a[0] === null ? 'null' : a[0] === undefined ? 'undefined' : String(a[0]) + (typeof a[0] === 'bigint' ? 'n' : '')
|
|
50
|
+
// ['nan'] — jz's parse encodes NaN as a self-describing marker (src/parse.js)
|
|
51
|
+
if (op === 'nan') return 'NaN'
|
|
52
|
+
// ['bool', 1|0] — true/false parse to a self-describing marker (src/parse.js)
|
|
53
|
+
if (op === 'bool') return a[0] ? 'true' : 'false'
|
|
36
54
|
|
|
37
55
|
// Statements
|
|
38
56
|
if (op === ';') return a.map(s => codegen(s, depth)).filter(Boolean).join(';\n' + ind) + ';'
|
|
@@ -97,6 +115,17 @@ export function codegen(node, depth = 0) {
|
|
|
97
115
|
if (a.length === 3) return 'try ' + codegen(a[0], depth) + ' catch (' + a[1] + ') ' + codegen(a[2], depth)
|
|
98
116
|
return 'try ' + codegen(a[0], depth) + ' catch ' + codegen(a[1], depth)
|
|
99
117
|
}
|
|
118
|
+
// Parser shape: ['try', body, ['catch', param|null, cbody]?, ['finally', fbody]?] —
|
|
119
|
+
// bodies arrive unbraced (bare statement or `;`-list); JS requires the blocks back.
|
|
120
|
+
if (op === 'try') {
|
|
121
|
+
let s = 'try ' + wrapBlock(a[0], depth)
|
|
122
|
+
for (const c of a.slice(1)) {
|
|
123
|
+
if (!Array.isArray(c)) continue
|
|
124
|
+
if (c[0] === 'catch') s += (c[1] != null ? ' catch (' + codegen(c[1]) + ') ' : ' catch ') + wrapBlock(c[2], depth)
|
|
125
|
+
else if (c[0] === 'finally') s += ' finally ' + wrapBlock(c[1], depth)
|
|
126
|
+
}
|
|
127
|
+
return s
|
|
128
|
+
}
|
|
100
129
|
|
|
101
130
|
// Arrow
|
|
102
131
|
if (op === '=>') {
|
|
@@ -114,11 +143,21 @@ export function codegen(node, depth = 0) {
|
|
|
114
143
|
// Grouping parens / function call
|
|
115
144
|
if (op === '()') {
|
|
116
145
|
if (a.length === 1) return '(' + (a[0] == null ? '' : codegen(a[0])) + ')'
|
|
117
|
-
|
|
146
|
+
// An arrow/ternary/binary callee binds looser than the call — parenthesize
|
|
147
|
+
// (IIFE: `(() => {…})()`), else `... => body()` re-parses into the body.
|
|
148
|
+
const calleeNeedsParens = Array.isArray(a[0]) && (a[0][0] === '=>' || a[0][0] === '?' || prec[a[0][0]] != null)
|
|
149
|
+
const callee = calleeNeedsParens ? '(' + codegen(a[0]) + ')' : codegen(a[0])
|
|
150
|
+
return callee + '(' + a.slice(1).map(x => codegen(x)).join(', ') + ')'
|
|
118
151
|
}
|
|
119
152
|
|
|
120
|
-
// Property access
|
|
121
|
-
|
|
153
|
+
// Property access. Canonicalized well-known-symbol props ('@@iterator' …)
|
|
154
|
+
// are not valid dot syntax — print the computed [Symbol.X] source form,
|
|
155
|
+
// which canonSymbols folds back to the same '@@X' on re-parse.
|
|
156
|
+
if (op === '.') {
|
|
157
|
+
if (typeof a[1] === 'string' && a[1].startsWith('@@'))
|
|
158
|
+
return codegen(a[0]) + '[Symbol.' + a[1].slice(2) + ']'
|
|
159
|
+
return codegen(a[0]) + '.' + a[1]
|
|
160
|
+
}
|
|
122
161
|
if (op === '?.') return codegen(a[0]) + '?.' + a[1]
|
|
123
162
|
if (op === '?.[]') return codegen(a[0]) + '?.[' + codegen(a[1]) + ']'
|
|
124
163
|
if (op === '?.()') return codegen(a[0]) + '?.(' + a.slice(1).map(x => codegen(x)).join(', ') + ')'
|
|
@@ -134,7 +173,13 @@ export function codegen(node, depth = 0) {
|
|
|
134
173
|
// Subscript: ['[]', obj, idx]
|
|
135
174
|
return codegen(a[0]) + '[' + codegen(a[1]) + ']'
|
|
136
175
|
}
|
|
137
|
-
if (op === ':')
|
|
176
|
+
if (op === ':') {
|
|
177
|
+
if (typeof a[0] === 'string' && a[0].startsWith('@@'))
|
|
178
|
+
return '[Symbol.' + a[0].slice(2) + ']: ' + codegen(a[1])
|
|
179
|
+
// a '@@X' key needs the quoted form ('@@iterator': v) only when written
|
|
180
|
+
// literally; the computed form above is the canonical print.
|
|
181
|
+
return codegen(a[0]) + ': ' + codegen(a[1])
|
|
182
|
+
}
|
|
138
183
|
if (op === 'str') return JSON.stringify(a[0])
|
|
139
184
|
if (op === '//') return '/' + a[0] + '/' + (a[1] || '')
|
|
140
185
|
|
|
@@ -157,20 +202,35 @@ export function codegen(node, depth = 0) {
|
|
|
157
202
|
// Unary prefix
|
|
158
203
|
if (a.length === 1) {
|
|
159
204
|
if (op === '++' || op === '--') return a[0] == null ? op : op + codegen(a[0])
|
|
160
|
-
if (op === 'typeof') return 'typeof ' +
|
|
161
|
-
if (op === 'u-') return '-' +
|
|
162
|
-
if (op === 'u+') return '+' +
|
|
163
|
-
return op +
|
|
205
|
+
if (op === 'typeof') return 'typeof ' + paren(a[0], 14)
|
|
206
|
+
if (op === 'u-') return '-' + paren(a[0], 14)
|
|
207
|
+
if (op === 'u+') return '+' + paren(a[0], 14)
|
|
208
|
+
return op + paren(a[0], 14)
|
|
164
209
|
}
|
|
165
210
|
|
|
166
211
|
// Postfix
|
|
167
212
|
if (a.length === 2 && a[1] === null) return codegen(a[0]) + op
|
|
168
213
|
|
|
169
|
-
// Binary
|
|
170
|
-
|
|
214
|
+
// Binary — parenthesize looser-binding children: parse preserves user parens as
|
|
215
|
+
// '()' group nodes, but jzify/canon-synthesized trees are bare, so `&&` over a
|
|
216
|
+
// synthesized `||` must print `a && (b || c)`, not re-associate on re-parse.
|
|
217
|
+
if (a.length === 2 && prec[op]) {
|
|
218
|
+
const P = prec[op]
|
|
219
|
+
// `??` may not mix with bare `&&`/`||` at all (JS SyntaxError) — force parens.
|
|
220
|
+
const force = (n) => op === '??' && Array.isArray(n) && (n[0] === '&&' || n[0] === '||')
|
|
221
|
+
const left = force(a[0]) || cprec(a[0]) < P || (cprec(a[0]) === P && op === '**') // ** is right-assoc
|
|
222
|
+
? '(' + codegen(a[0]) + ')' : codegen(a[0])
|
|
223
|
+
// Assignment RHS takes any assignment-expression bare (arrows, ternaries,
|
|
224
|
+
// chained `=`) — only a comma-sequence needs parens there.
|
|
225
|
+
const rmin = P === 1 ? 0.5 : P
|
|
226
|
+
const right = force(a[1]) || cprec(a[1]) < rmin || (cprec(a[1]) === rmin && !(ASSOC.has(op) && a[1][0] === op))
|
|
227
|
+
? '(' + codegen(a[1]) + ')' : codegen(a[1])
|
|
228
|
+
return left + ' ' + op + ' ' + right
|
|
229
|
+
}
|
|
171
230
|
|
|
172
|
-
// Ternary
|
|
173
|
-
|
|
231
|
+
// Ternary — only the condition slot needs guarding (branches take any
|
|
232
|
+
// assignment-expression; ?: is right-associative)
|
|
233
|
+
if (op === '?' || op === '?:') return paren(a[0], 2) + ' ? ' + codegen(a[1]) + ' : ' + codegen(a[2])
|
|
174
234
|
|
|
175
235
|
// Fallback
|
|
176
236
|
return op + '(' + a.map(x => codegen(x)).join(', ') + ')'
|
package/transform.js
CHANGED
|
@@ -8,14 +8,123 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module jz/transform
|
|
10
10
|
*/
|
|
11
|
-
import { parse } from '
|
|
11
|
+
import { parse } from './src/parse.js'
|
|
12
12
|
import jzify from './jzify/index.js'
|
|
13
13
|
import { codegen } from './src/wat/codegen.js'
|
|
14
|
+
import { initWarnings, warn } from './src/ctx.js'
|
|
15
|
+
|
|
16
|
+
// jzify mints temps under private-use-area prefixes (jzify/names.js) so they can
|
|
17
|
+
// never collide with user names — but printed source must be plain identifiers
|
|
18
|
+
// (the compiler REJECTS the reserved prefix in user code). Strip the prefix and
|
|
19
|
+
// uniquify against every name present in the program.
|
|
20
|
+
const PUA = /^[-]/
|
|
21
|
+
function renameSynthetic(ast) {
|
|
22
|
+
const names = new Set(), synth = new Set()
|
|
23
|
+
const collect = (n) => {
|
|
24
|
+
if (typeof n === 'string') (PUA.test(n) ? synth : names).add(n)
|
|
25
|
+
else if (Array.isArray(n) && n[0] != null) for (let i = 0; i < n.length; i++) collect(n[i])
|
|
26
|
+
}
|
|
27
|
+
collect(ast)
|
|
28
|
+
if (!synth.size) return ast
|
|
29
|
+
const map = new Map()
|
|
30
|
+
for (const s of synth) {
|
|
31
|
+
let base = s.replace(PUA, ''), name = base
|
|
32
|
+
for (let i = 2; names.has(name); i++) name = base + '_' + i
|
|
33
|
+
names.add(name); map.set(s, name)
|
|
34
|
+
}
|
|
35
|
+
const rename = (n) => {
|
|
36
|
+
if (Array.isArray(n) && n[0] != null)
|
|
37
|
+
for (let i = 0; i < n.length; i++) {
|
|
38
|
+
if (typeof n[i] === 'string' && map.has(n[i])) n[i] = map.get(n[i])
|
|
39
|
+
else rename(n[i])
|
|
40
|
+
}
|
|
41
|
+
return n
|
|
42
|
+
}
|
|
43
|
+
return rename(ast)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Canonical equality — converter-only (the compile path keeps JS-loose `==`).
|
|
47
|
+
// jz's `==` follows JS: statically-known mixed types coerce (`1 == "1"` is
|
|
48
|
+
// true), so a blanket `===` swap would change behavior. Rewrite by proof:
|
|
49
|
+
// x == null / == undefined → x === null || x === undefined (exact, incl. dynamic)
|
|
50
|
+
// typeof x == "s", same-type literals → strict op (provably identical)
|
|
51
|
+
// mixed-type literals → folded constant (the JS loose result)
|
|
52
|
+
// anything else → strict op + `eqeq` advisory (identical unless
|
|
53
|
+
// the operands mix types at runtime — the case Crockford bans `==` over)
|
|
54
|
+
// jzify-synthesized comparisons carry no source `loc` and rewrite silently.
|
|
55
|
+
const isLit = n => Array.isArray(n) && n.length === 2 && n[0] == null
|
|
56
|
+
const isNullish = n => n === 'null' || n === 'undefined' || (isLit(n) && n[1] == null)
|
|
57
|
+
const isTypeof = n => Array.isArray(n) && n[0] === 'typeof'
|
|
58
|
+
const isStrLit = n => isLit(n) && typeof n[1] === 'string'
|
|
59
|
+
// Re-evaluable without effects — jz has no getters/proxies, so member and
|
|
60
|
+
// index reads are pure; calls and assignments are not.
|
|
61
|
+
const isPure = n => typeof n === 'string' || isLit(n)
|
|
62
|
+
|| (Array.isArray(n) && (n[0] === '.' || n[0] === '?.') && isPure(n[1]))
|
|
63
|
+
|| (Array.isArray(n) && (n[0] === '[]' || n[0] === '?.[]') && n.length === 3 && isPure(n[1]) && isPure(n[2]))
|
|
64
|
+
|
|
65
|
+
let eqTemp = 0
|
|
66
|
+
function canonEqNode(node, source) {
|
|
67
|
+
const [op, a, b] = node, eq = op === '=='
|
|
68
|
+
const strict = eq ? '===' : '!=='
|
|
69
|
+
if (isNullish(a) && isNullish(b)) return [null, eq]
|
|
70
|
+
if (isNullish(a) || isNullish(b)) {
|
|
71
|
+
const x = isNullish(a) ? b : a
|
|
72
|
+
const test = v => eq ? ['||', ['===', v, 'null'], ['===', v, 'undefined']]
|
|
73
|
+
: ['&&', ['!==', v, 'null'], ['!==', v, 'undefined']]
|
|
74
|
+
if (isPure(x)) return test(x)
|
|
75
|
+
const t = `eq${eqTemp++}` // effectful operand — bind once
|
|
76
|
+
return ['()', ['=>', t, test(t)], x]
|
|
77
|
+
}
|
|
78
|
+
if ((isTypeof(a) && (isTypeof(b) || isStrLit(b))) || (isTypeof(b) && isStrLit(a)))
|
|
79
|
+
return [strict, a, b]
|
|
80
|
+
if (isLit(a) && isLit(b)) {
|
|
81
|
+
if (typeof a[1] === typeof b[1]) return [strict, a, b]
|
|
82
|
+
const r = +a[1] === +b[1] // JS loose result for mixed number/string/boolean/bigint literals
|
|
83
|
+
return [null, eq ? r : !r]
|
|
84
|
+
}
|
|
85
|
+
// `.prototype` comparisons are bundler idioms jzify folds away — no advisory.
|
|
86
|
+
const protoish = n => Array.isArray(n) && n[0] === '.' && (n[2] === 'prototype' || protoish(n[1]))
|
|
87
|
+
if (source != null && node.loc != null && !protoish(a) && !protoish(b)) {
|
|
88
|
+
const before = source.slice(0, node.loc)
|
|
89
|
+
warn('eqeq', `\`${eq ? '==' : '!='}\` rewritten to \`${strict}\` — identical unless the operands mix types at runtime (loose \`${eq ? '==' : '!='}\` coerces, e.g. \`1 == "1"\`); review if coercion was intended`,
|
|
90
|
+
{ line: before.split('\n').length, column: node.loc - before.lastIndexOf('\n') })
|
|
91
|
+
}
|
|
92
|
+
return [strict, a, b]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Two passes: pre-jzify on the raw parse (source locs intact → advisories point
|
|
96
|
+
// at real lines) and post-jzify silent (`source: null`) — jzify both rebuilds
|
|
97
|
+
// user nodes (dropping loc) and synthesizes its own `==`/`!=` (dispose guards,
|
|
98
|
+
// protocol probes, prepended runtimes), all of which must still canonicalize.
|
|
99
|
+
function canonEq(n, source) {
|
|
100
|
+
if (!Array.isArray(n) || n[0] == null) return n
|
|
101
|
+
for (let i = 1; i < n.length; i++) n[i] = canonEq(n[i], source)
|
|
102
|
+
return (n[0] === '==' || n[0] === '!=') ? canonEqNode(n, source) : n
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// AST fingerprint for no-change detection (bigints aren't JSON-stringifiable)
|
|
106
|
+
const key = (ast) => JSON.stringify(ast, (_, v) => typeof v === 'bigint' ? v + 'n' : v)
|
|
14
107
|
|
|
15
108
|
/**
|
|
16
109
|
* @param {string} code - full-JS source
|
|
17
|
-
* @
|
|
110
|
+
* @param {object} [opts]
|
|
111
|
+
* @param {boolean} [opts.onlyLowered] - return null when jzify changes nothing
|
|
112
|
+
* (source is already canonical jz) — lets a REPL keep the user's original
|
|
113
|
+
* bytes, comments and formatting intact, and rewrite only real lowerings.
|
|
114
|
+
* @param {{entries: Array}} [opts.warnings] - advisory sink (mirrors compile's
|
|
115
|
+
* `opts.warnings`) — collects `eqeq` rewrite advisories etc.
|
|
116
|
+
* @returns {string|null} canonical jz source
|
|
18
117
|
*/
|
|
19
|
-
export default function transform(code) {
|
|
20
|
-
|
|
118
|
+
export default function transform(code, { onlyLowered = false, warnings = null } = {}) {
|
|
119
|
+
initWarnings(warnings)
|
|
120
|
+
eqTemp = 0
|
|
121
|
+
try {
|
|
122
|
+
if (onlyLowered) {
|
|
123
|
+
const before = key(parse(code))
|
|
124
|
+
const lowered = canonEq(jzify(canonEq(parse(code), code)), null) // fresh parse — jzify mutates its input
|
|
125
|
+
if (key(lowered) === before) return null
|
|
126
|
+
return codegen(renameSynthetic(lowered))
|
|
127
|
+
}
|
|
128
|
+
return codegen(renameSynthetic(canonEq(jzify(canonEq(parse(code), code)), null)))
|
|
129
|
+
} finally { initWarnings(null) }
|
|
21
130
|
}
|
package/wasi.js
CHANGED
|
@@ -131,5 +131,8 @@ export function instantiate(wasm, opts = {}) {
|
|
|
131
131
|
const imports = wasi(opts)
|
|
132
132
|
const inst = new WebAssembly.Instance(new WebAssembly.Module(wasm), imports)
|
|
133
133
|
imports._setMemory(inst.exports.memory)
|
|
134
|
+
// WASI reactor convention: module init lives in `_initialize` (not a wasm start
|
|
135
|
+
// section — WASI calls there would fire before memory is wired above).
|
|
136
|
+
inst.exports._initialize?.()
|
|
134
137
|
return inst
|
|
135
138
|
}
|