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.
Files changed (73) hide show
  1. package/README.md +56 -9
  2. package/bench/README.md +121 -50
  3. package/bench/bench.svg +27 -27
  4. package/cli.js +3 -1
  5. package/dist/interop.js +1 -1
  6. package/dist/jz.js +7541 -6178
  7. package/index.js +165 -74
  8. package/interop.js +189 -17
  9. package/jzify/arguments.js +32 -1
  10. package/jzify/async.js +409 -0
  11. package/jzify/classes.js +136 -18
  12. package/jzify/generators.js +639 -0
  13. package/jzify/hoist-vars.js +73 -1
  14. package/jzify/index.js +123 -4
  15. package/jzify/names.js +1 -0
  16. package/jzify/transform.js +239 -1
  17. package/layout.js +48 -3
  18. package/module/array.js +318 -43
  19. package/module/atomics.js +144 -0
  20. package/module/collection.js +1429 -155
  21. package/module/core.js +431 -40
  22. package/module/date.js +142 -120
  23. package/module/fs.js +144 -0
  24. package/module/function.js +6 -3
  25. package/module/index.js +4 -1
  26. package/module/json.js +270 -49
  27. package/module/math.js +1032 -145
  28. package/module/number.js +532 -163
  29. package/module/object.js +353 -95
  30. package/module/regex.js +157 -7
  31. package/module/schema.js +100 -2
  32. package/module/string.js +428 -93
  33. package/module/typedarray.js +264 -72
  34. package/module/web.js +36 -0
  35. package/package.json +5 -5
  36. package/src/abi/string.js +82 -11
  37. package/src/ast.js +11 -5
  38. package/src/autoload.js +28 -1
  39. package/src/bridge.js +7 -2
  40. package/src/compile/analyze-scans.js +22 -7
  41. package/src/compile/analyze.js +136 -30
  42. package/src/compile/dyn-closure-tables.js +277 -0
  43. package/src/compile/emit-assign.js +281 -15
  44. package/src/compile/emit.js +1055 -70
  45. package/src/compile/index.js +277 -29
  46. package/src/compile/infer.js +42 -4
  47. package/src/compile/inplace-store.js +329 -0
  48. package/src/compile/loop-recurrence.js +167 -0
  49. package/src/compile/narrow.js +555 -18
  50. package/src/compile/peel-stencil.js +5 -0
  51. package/src/compile/plan/index.js +45 -3
  52. package/src/compile/plan/inline.js +8 -1
  53. package/src/compile/plan/literals.js +39 -0
  54. package/src/compile/plan/scope.js +430 -23
  55. package/src/compile/program-facts.js +732 -38
  56. package/src/ctx.js +64 -9
  57. package/src/helper-counters.js +7 -1
  58. package/src/ir.js +113 -5
  59. package/src/kind-traits.js +66 -3
  60. package/src/kind.js +84 -12
  61. package/src/op-policy.js +7 -4
  62. package/src/optimize/index.js +1179 -704
  63. package/src/optimize/recurse.js +2 -2
  64. package/src/optimize/vectorize.js +982 -67
  65. package/src/prepare/index.js +809 -67
  66. package/src/prepare/math-kernel.js +331 -0
  67. package/src/prepare/pre-eval.js +714 -0
  68. package/src/snapshot.js +194 -0
  69. package/src/type.js +1170 -56
  70. package/src/wat/assemble.js +403 -65
  71. package/src/wat/codegen.js +74 -14
  72. package/transform.js +113 -4
  73. package/wasi.js +3 -0
@@ -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
- if (op == null) return typeof a[0] === 'string' ? JSON.stringify(a[0]) : a[0] == null ? 'null' : String(a[0]) + (typeof a[0] === 'bigint' ? 'n' : '')
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
- return codegen(a[0]) + '(' + a.slice(1).map(x => codegen(x)).join(', ') + ')'
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
- if (op === '.') return codegen(a[0]) + '.' + a[1]
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 === ':') return codegen(a[0]) + ': ' + codegen(a[1])
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 ' + codegen(a[0])
161
- if (op === 'u-') return '-' + codegen(a[0])
162
- if (op === 'u+') return '+' + codegen(a[0])
163
- return op + codegen(a[0])
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
- if (a.length === 2 && prec[op]) return codegen(a[0]) + ' ' + op + ' ' + codegen(a[1])
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
- if (op === '?' || op === '?:') return codegen(a[0]) + ' ? ' + codegen(a[1]) + ' : ' + codegen(a[2])
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 'subscript/feature/jessie'
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
- * @returns {string} canonical jz source
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
- return codegen(jzify(parse(code)))
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
  }