jz 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +315 -318
  2. package/bench/README.md +369 -0
  3. package/bench/bench.svg +102 -0
  4. package/cli.js +104 -30
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6024 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +362 -84
  9. package/interop.js +159 -186
  10. package/jz.svg +5 -0
  11. package/jzify/arguments.js +97 -0
  12. package/jzify/bundler.js +382 -0
  13. package/jzify/classes.js +328 -0
  14. package/jzify/hoist-vars.js +177 -0
  15. package/jzify/index.js +51 -0
  16. package/jzify/names.js +37 -0
  17. package/jzify/switch.js +106 -0
  18. package/jzify/transform.js +349 -0
  19. package/layout.js +184 -0
  20. package/module/array.js +377 -176
  21. package/module/collection.js +639 -145
  22. package/module/console.js +55 -43
  23. package/module/core.js +264 -153
  24. package/module/date.js +15 -3
  25. package/module/function.js +73 -6
  26. package/module/index.js +2 -1
  27. package/module/json.js +226 -61
  28. package/module/math.js +551 -187
  29. package/module/number.js +327 -60
  30. package/module/object.js +474 -184
  31. package/module/regex.js +255 -25
  32. package/module/schema.js +24 -6
  33. package/module/simd.js +117 -0
  34. package/module/string.js +621 -226
  35. package/module/symbol.js +1 -1
  36. package/module/timer.js +9 -14
  37. package/module/typedarray.js +459 -73
  38. package/package.json +58 -14
  39. package/src/abi/index.js +39 -23
  40. package/src/abi/string.js +38 -41
  41. package/src/ast.js +460 -0
  42. package/src/autoload.js +29 -24
  43. package/src/bridge.js +111 -0
  44. package/src/compile/analyze-scans.js +824 -0
  45. package/src/compile/analyze.js +1600 -0
  46. package/src/compile/emit-assign.js +411 -0
  47. package/src/compile/emit.js +3512 -0
  48. package/src/compile/flow-types.js +103 -0
  49. package/src/{compile.js → compile/index.js} +778 -128
  50. package/src/{infer.js → compile/infer.js} +62 -98
  51. package/src/compile/loop-divmod.js +155 -0
  52. package/src/{narrow.js → compile/narrow.js} +409 -106
  53. package/src/compile/peel-stencil.js +261 -0
  54. package/src/compile/plan/advise.js +370 -0
  55. package/src/compile/plan/common.js +150 -0
  56. package/src/compile/plan/index.js +122 -0
  57. package/src/compile/plan/inline.js +682 -0
  58. package/src/compile/plan/literals.js +1199 -0
  59. package/src/compile/plan/loops.js +472 -0
  60. package/src/compile/plan/scope.js +649 -0
  61. package/src/compile/program-facts.js +423 -0
  62. package/src/ctx.js +220 -64
  63. package/src/ir.js +589 -172
  64. package/src/kind-traits.js +132 -0
  65. package/src/kind.js +524 -0
  66. package/src/op-policy.js +57 -0
  67. package/src/{optimize.js → optimize/index.js} +1432 -473
  68. package/src/optimize/vectorize.js +4660 -0
  69. package/src/param-reps.js +65 -0
  70. package/src/parse.js +44 -0
  71. package/src/{prepare.js → prepare/index.js} +649 -205
  72. package/src/prepare/lift-iife.js +149 -0
  73. package/src/reps.js +116 -0
  74. package/src/resolve.js +12 -3
  75. package/src/static.js +208 -0
  76. package/src/type.js +651 -0
  77. package/src/{assemble.js → wat/assemble.js} +275 -55
  78. package/src/wat/optimize.js +3938 -0
  79. package/transform.js +21 -0
  80. package/wasi.js +47 -5
  81. package/src/analyze.js +0 -3818
  82. package/src/emit.js +0 -3040
  83. package/src/jzify.js +0 -1580
  84. package/src/plan.js +0 -2132
  85. package/src/vectorize.js +0 -1088
  86. /package/src/{codegen.js → wat/codegen.js} +0 -0
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Shared AST utilities for plan/ subsystems.
3
+ *
4
+ * Helpers used by multiple subfiles (scalarize/loops/inline). Single-use helpers
5
+ * stay with their consumer.
6
+ *
7
+ * @module compile/plan/common
8
+ */
9
+
10
+ import { ctx } from '../../ctx.js'
11
+ import { ASSIGN_OPS, some, callArgs } from '../../ast.js'
12
+ import { constIntExpr } from '../../static.js'
13
+ import { typedElemCtor } from '../../type.js'
14
+ import { PASS_NAMES } from '../../optimize/index.js'
15
+
16
+ /** True iff the optimizer is active (any non-falsy pass under `ctx.transform.optimize`). */
17
+ export const optimizing = () => { const c = ctx.transform.optimize; return !!c && PASS_NAMES.some(n => c[n]) }
18
+
19
+ /** Ops whose body opens a new loop scope. (`for-in`/`for-of` excluded — they
20
+ * bind a fresh per-iter local on each entry, so jz lowers them differently.) */
21
+ export const LOOP_OPS = new Set(['for', 'while', 'do', 'do-while'])
22
+
23
+ /** Inline-substitution argument check — pure, side-effect-free, captures nothing. */
24
+ export const isSimpleArg = node => {
25
+ if (typeof node === 'string' || typeof node === 'number') return true
26
+ if (!Array.isArray(node)) return false
27
+ if (node[0] == null) return typeof node[1] === 'number'
28
+ if (node[0] === 'str') return typeof node[1] === 'string'
29
+ if (node[0] === 'u-' || (node[0] === '-' && node.length === 2)) return isSimpleArg(node[1])
30
+ if (['+', '-', '*', '/', '%', '&', '|', '^', '<<', '>>', '>>>'].includes(node[0]))
31
+ return isSimpleArg(node[1]) && isSimpleArg(node[2])
32
+ return false
33
+ }
34
+
35
+ /** Maximum loop nesting under `node`. Closures (`=>`) opaque — their loops don't count. */
36
+ export const loopDepth = (node, depth) => {
37
+ if (!Array.isArray(node)) return depth
38
+ if (node[0] === '=>') return depth
39
+ const here = LOOP_OPS.has(node[0]) ? depth + 1 : depth
40
+ let max = here
41
+ for (let i = 1; i < node.length; i++) {
42
+ const d = loopDepth(node[i], here)
43
+ if (d > max) max = d
44
+ }
45
+ return max
46
+ }
47
+
48
+ /** Node-count weight — drives inline cost heuristics. */
49
+ export const nodeSize = (node) => {
50
+ if (!Array.isArray(node)) return 1
51
+ let n = 1
52
+ for (let i = 1; i < node.length; i++) n += nodeSize(node[i])
53
+ return n
54
+ }
55
+
56
+ /** Collect every binder name in `node` into `out` (lexical-scope set; doesn't
57
+ * descend into nested arrow bodies — those open a new scope). */
58
+ export const collectBindings = (node, out) => {
59
+ if (!Array.isArray(node)) return
60
+ const op = node[0]
61
+ if (op === '=>') return
62
+ if (op === 'let' || op === 'const') {
63
+ for (let i = 1; i < node.length; i++) collectBindingTarget(node[i], out)
64
+ }
65
+ for (let i = 1; i < node.length; i++) collectBindings(node[i], out)
66
+ }
67
+
68
+ const collectBindingTarget = (node, out) => {
69
+ if (typeof node === 'string') { out.add(node); return }
70
+ if (!Array.isArray(node)) return
71
+ if (node[0] === '=') collectBindingTarget(node[1], out)
72
+ else if (node[0] === '...' && typeof node[1] === 'string') out.add(node[1])
73
+ else if (node[0] === ',' || node[0] === '[]' || node[0] === '{}')
74
+ for (let i = 1; i < node.length; i++) collectBindingTarget(node[i], out)
75
+ }
76
+
77
+ /** True iff `node` writes to any name in `names` (incl. `++`/`--` and compound assigns). */
78
+ export const mutatesAny = (node, names) => some(node, n => {
79
+ const op = n[0]
80
+ if ((op === '++' || op === '--') && typeof n[1] === 'string') return names.has(n[1])
81
+ return ASSIGN_OPS.has(op) && typeof n[1] === 'string' && names.has(n[1])
82
+ })
83
+
84
+ /** Deep-clone array-tree AST. Plain values pass through by identity. */
85
+ export const clonePlain = node => Array.isArray(node) ? node.map(clonePlain) : node
86
+
87
+ /** AST index of a `for` loop's body, or null if `stmt` isn't a `for`.
88
+ * Handles both `['for', cond, body]` (3-arg while-like) and the full
89
+ * `['for', init, cond, step, body]` C-style form. */
90
+ export const forLoopBodyIndex = (stmt) =>
91
+ Array.isArray(stmt) && stmt[0] === 'for' ? (stmt.length === 3 ? 2 : 4) : null
92
+
93
+ /** Reconstruct a `for` node with its body replaced. Preserves arity. */
94
+ export const withForLoopBody = (stmt, body) =>
95
+ stmt.length === 3 ? ['for', stmt[1], body] : ['for', stmt[1], stmt[2], stmt[3], body]
96
+
97
+ // === Fixed-size typed-array recognition ===
98
+ // Shared by scalarize (replaces the literal with N locals) and inline (filters
99
+ // inlinings that would trample a typed-array param). Tables/thresholds live here
100
+ // so both subsystems agree on what "scalarizable" means.
101
+
102
+ /** Fixed-size typed-array ctors eligible for scalar replacement, mapped to the
103
+ * element store-coercion kind ('' = none, i.e. Float64Array's f64-identity).
104
+ * Excluded: Float32Array (Math.fround pulls module), Uint32Array (range > i32),
105
+ * Uint8ClampedArray (round-half-even clamp). Coerced (truthy) types are only
106
+ * scalarized when fully local. */
107
+ export const SCALAR_TYPED_COERCE = {
108
+ 'new.Float64Array': '',
109
+ 'new.Int32Array': 'i32',
110
+ 'new.Int16Array': 'i16', 'new.Uint16Array': 'u16',
111
+ 'new.Int8Array': 'i8', 'new.Uint8Array': 'u8',
112
+ }
113
+
114
+ // Default 64 covers the 8×8 block kernels (DCT/JPEG-shaped). Measured at 64
115
+ // elements: scalarized form is ~2.2× SMALLER (stores fold away; local refs
116
+ // out-LEB the memory ops they replace) and 2.5× faster than the memory form —
117
+ // there is no LEB128 cliff in practice.
118
+ export const maxScalarTypedArrayLen = () => ctx.transform.optimize?.scalarTypedArrayLen ?? 64
119
+
120
+ /** Recognize `new TypedArrayCtor(N)` with `N` a static small integer.
121
+ * Returns `{len, coerce}` or null. */
122
+ export const fixedScalarTypedArray = (expr) => {
123
+ const ctor = typedElemCtor(expr)
124
+ if (ctor == null || !(ctor in SCALAR_TYPED_COERCE)) return null
125
+ const args = callArgs(expr)
126
+ if (!args || args.length !== 1) return null
127
+ const len = constIntExpr(args[0])
128
+ return len != null && len >= 0 && len <= maxScalarTypedArrayLen()
129
+ ? { len, coerce: SCALAR_TYPED_COERCE[ctor] } : null
130
+ }
131
+
132
+ /** Map of name → {len, coerce} for every fixed-size typed-array binding declared
133
+ * via `let`/`const` directly in `body` (no descent into nested arrows). */
134
+ export const fixedTypedArraysInBody = (body) => {
135
+ const out = new Map()
136
+ const walk = node => {
137
+ if (!Array.isArray(node) || node[0] === '=>') return
138
+ if (node[0] === 'let' || node[0] === 'const') {
139
+ for (let i = 1; i < node.length; i++) {
140
+ const d = node[i]
141
+ if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
142
+ const fixed = fixedScalarTypedArray(d[2])
143
+ if (fixed != null) out.set(d[1], fixed)
144
+ }
145
+ }
146
+ for (let i = 1; i < node.length; i++) walk(node[i])
147
+ }
148
+ walk(body)
149
+ return out
150
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Pre-emit compile planning: bridges prepare (AST shape) and emit (wasm bytes).
3
+ *
4
+ * # Stage contract
5
+ * IN: populated `ctx` from prepare.js (functions, schemas, scopes, modules)
6
+ * plus the prepared AST.
7
+ * OUT: returns a `programFacts` object; mutates `ctx` so each function has
8
+ * narrowed signatures, finalized global reps, and per-call decisions.
9
+ *
10
+ * # Pipeline (top-level `plan(ast)`)
11
+ * 1. unboxConstTypedGlobals — finalize global storage. (Global value facts
12
+ * themselves are seeded by prepare via `infer.recordGlobalRep`.)
13
+ * 2. collectProgramFacts — sweep arrow bodies for typed-elem usage, key sets,
14
+ * loop depth, control-transfer shapes; rerun if hot inlining changes the AST.
15
+ * 3. materializeAutoBoxSchemas / resolveClosureWidth — settle layout decisions.
16
+ * 4. Whole-program narrowing (skipped on simple programs):
17
+ * - narrowSignatures — pick a specialization per function from call sites
18
+ * - specializeBimorphicTyped — split typed-elem hot paths into two variants
19
+ * when callers diverge between two ctors
20
+ * - refineDynKeys — tighten dynamic property-key sets
21
+ *
22
+ * No bytes are emitted here; emit.js consumes the planned ctx + programFacts.
23
+ *
24
+ * @module plan
25
+ */
26
+
27
+ import { ctx } from '../../ctx.js'
28
+ import { collectProgramFacts, refreshProgramFacts } from '../program-facts.js'
29
+ import narrowSignatures, {
30
+ specializeBimorphicTyped, refineDynKeys,
31
+ applyJsstringBoundaryCarrierStandalone, narrowBoolResults,
32
+ strictBoundaryTypeCheck,
33
+ } from '../narrow.js'
34
+
35
+ import { optimizing } from './common.js'
36
+ import { adviseProgram } from './advise.js'
37
+ import {
38
+ inferModuleLetTypes, unboxConstTypedGlobals, inferModuleIntGlobals,
39
+ flattenFuncNamespaces, devirtGlobalCalls,
40
+ materializeAutoBoxSchemas, resolveClosureWidth, canSkipWholeProgramNarrowing,
41
+ } from './scope.js'
42
+ import { inlineHotInternalCalls, inlineLocalLambdas, specializeFixedRestCalls } from './inline.js'
43
+ import { bindNestedRowLengths, unrollRowLenPadLoops, splitCharScanLoops } from './loops.js'
44
+ import {
45
+ scalarizeFunctionTypedArrays, scalarizeFunctionArrayLiterals,
46
+ promoteIntArrayLiterals, scalarizeFunctionObjectLiterals,
47
+ } from './literals.js'
48
+
49
+ export default function plan(ast, profiler) {
50
+ // Per-pass timing under `plan:` — the plan stage is the compile pipeline's
51
+ // multi-pass hot spot (each mutating pass triggers a whole-program fact
52
+ // refresh), so the profile must show WHICH pass and refresh dominate.
53
+ const t = profiler?.time ? (name, fn) => profiler.time(`plan:${name}`, fn) : (_, fn) => fn()
54
+ // AST-mutating pass: run timed; on change, re-sweep program facts (timed
55
+ // separately — the refreshes are usually the cost, not the passes).
56
+ let programFacts
57
+ const sweep = (name, pass) => {
58
+ if (t(name, pass)) programFacts = t('refreshFacts', () => refreshProgramFacts(ast, programFacts))
59
+ }
60
+
61
+ t('inferModuleLetTypes', () => inferModuleLetTypes(ast))
62
+ t('unboxConstTypedGlobals', unboxConstTypedGlobals)
63
+ t('inferModuleIntGlobals', () => inferModuleIntGlobals(ast))
64
+
65
+ programFacts = t('collectFacts', () => collectProgramFacts(ast))
66
+ // Function-namespace SROA — dissolve reassigned `f.prop` slots into module
67
+ // globals before inlining/narrowing, so all downstream passes see plain
68
+ // globals instead of the dynamic property machinery.
69
+ sweep('flattenFuncNamespaces', () => flattenFuncNamespaces(ast))
70
+ // Devirtualize calls through init-constant function globals (closure
71
+ // devirtualization) — must follow the SROA above, which creates the globals.
72
+ t('devirtGlobalCalls', () => devirtGlobalCalls(ast))
73
+ sweep('bindNestedRowLengths', bindNestedRowLengths)
74
+ sweep('unrollRowLenPadLoops', unrollRowLenPadLoops)
75
+ // The call-inlining family (`inlineHotInternalCalls` self-gates on `sourceInline`)
76
+ // is a pure speed optimization — the un-inlined calls emit correctly. Scalar
77
+ // replacement (`scalarize*`) and array promotion gate on `optimizing()`: off only
78
+ // under a fully-disabled optimizer, on for every enabled preset (incl. the
79
+ // `optimize:{sourceInline:false}` heap-elision-test form, which is level-2 based).
80
+ sweep('inlineHotInternalCalls', () => inlineHotInternalCalls(programFacts, ast))
81
+ sweep('bindNestedRowLengths', bindNestedRowLengths)
82
+ sweep('unrollRowLenPadLoops', unrollRowLenPadLoops)
83
+ sweep('inlineLocalLambdas', inlineLocalLambdas)
84
+ sweep('specializeFixedRestCalls', () => specializeFixedRestCalls(programFacts))
85
+ if (optimizing()) {
86
+ sweep('splitCharScan', splitCharScanLoops)
87
+ sweep('scalarizeArrayLiterals', scalarizeFunctionArrayLiterals)
88
+ sweep('scalarizeObjectLiterals', scalarizeFunctionObjectLiterals)
89
+ // Promotion runs AFTER literal scalarization (those that fully reduce to scalars
90
+ // are gone) and BEFORE typed-array scalarization (so a freshly-promoted array's
91
+ // fixed-length-typed-of-known-size variant could still participate in loop
92
+ // unrolling — currently it can't, since promotion produces the `[...]`-arg
93
+ // form rather than `new Int32Array(N)`, but the ordering keeps the door open).
94
+ sweep('promoteIntArrayLiterals', promoteIntArrayLiterals)
95
+ sweep('scalarizeTypedArrays', () => scalarizeFunctionTypedArrays(programFacts))
96
+ }
97
+ ctx.types.dynKeyVars = programFacts.dynVars
98
+ ctx.types.dynWriteVars = programFacts.dynWriteVars
99
+ ctx.types.anyDynKey = programFacts.anyDyn
100
+
101
+ t('materializeAutoBoxSchemas', () => materializeAutoBoxSchemas(programFacts))
102
+ t('resolveClosureWidth', () => resolveClosureWidth(programFacts))
103
+ if (canSkipWholeProgramNarrowing(programFacts)) {
104
+ // Phase J (jsstring boundary opt-in) is body-local and call-site-independent;
105
+ // run it even when the rest of narrowing is skipped so simple `export let
106
+ // f = (s) => s.length` still flips to externref. Likewise the boolean-result
107
+ // fact, so `export let f = (a) => a > 2` boxes its boundary atom.
108
+ applyJsstringBoundaryCarrierStandalone(programFacts)
109
+ narrowBoolResults()
110
+ strictBoundaryTypeCheck(programFacts)
111
+ adviseProgram(programFacts)
112
+ return programFacts
113
+ }
114
+
115
+ t('narrowSignatures', () => narrowSignatures(programFacts, ast))
116
+ t('specializeBimorphicTyped', () => specializeBimorphicTyped(programFacts))
117
+ t('refineDynKeys', () => refineDynKeys(programFacts))
118
+ strictBoundaryTypeCheck(programFacts)
119
+
120
+ adviseProgram(programFacts)
121
+ return programFacts
122
+ }