@styx-api/core 0.1.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 (88) hide show
  1. package/dist/index.cjs +7947 -0
  2. package/dist/index.d.cts +1143 -0
  3. package/dist/index.d.cts.map +1 -0
  4. package/dist/index.d.mts +1143 -0
  5. package/dist/index.d.mts.map +1 -0
  6. package/dist/index.mjs +7877 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/package.json +55 -0
  9. package/src/backend/backend.ts +95 -0
  10. package/src/backend/boutiques/boutiques.ts +1049 -0
  11. package/src/backend/boutiques/index.ts +1 -0
  12. package/src/backend/code-builder.ts +49 -0
  13. package/src/backend/collect-field-info.ts +50 -0
  14. package/src/backend/collect-named-types.ts +103 -0
  15. package/src/backend/collect-output-fields.ts +222 -0
  16. package/src/backend/find-doc.ts +38 -0
  17. package/src/backend/find-struct-node.ts +66 -0
  18. package/src/backend/index.ts +39 -0
  19. package/src/backend/python/arg-builder.ts +454 -0
  20. package/src/backend/python/emit.ts +638 -0
  21. package/src/backend/python/index.ts +9 -0
  22. package/src/backend/python/outputs-emit.ts +430 -0
  23. package/src/backend/python/packaging.ts +173 -0
  24. package/src/backend/python/python.ts +558 -0
  25. package/src/backend/python/snippet.ts +84 -0
  26. package/src/backend/python/typemap.ts +131 -0
  27. package/src/backend/python/types.ts +8 -0
  28. package/src/backend/python/validate-emit.ts +356 -0
  29. package/src/backend/resolve-field-binding.ts +41 -0
  30. package/src/backend/resolve-output-tokens.ts +80 -0
  31. package/src/backend/schema/index.ts +2 -0
  32. package/src/backend/schema/jsonschema.ts +303 -0
  33. package/src/backend/scope.ts +50 -0
  34. package/src/backend/sig-entries.ts +97 -0
  35. package/src/backend/snippet-core.ts +185 -0
  36. package/src/backend/string-case.ts +30 -0
  37. package/src/backend/styxdefs-compat.ts +21 -0
  38. package/src/backend/type-keys.ts +52 -0
  39. package/src/backend/typescript/arg-builder.ts +420 -0
  40. package/src/backend/typescript/emit.ts +450 -0
  41. package/src/backend/typescript/index.ts +10 -0
  42. package/src/backend/typescript/outputs-emit.ts +389 -0
  43. package/src/backend/typescript/packaging.ts +130 -0
  44. package/src/backend/typescript/snippet.ts +60 -0
  45. package/src/backend/typescript/typemap.ts +47 -0
  46. package/src/backend/typescript/types.ts +8 -0
  47. package/src/backend/typescript/typescript.ts +507 -0
  48. package/src/backend/typescript/validate-emit.ts +341 -0
  49. package/src/backend/union-variants.ts +42 -0
  50. package/src/backend/validate-walk.ts +111 -0
  51. package/src/bindings/binding.ts +77 -0
  52. package/src/bindings/format.ts +176 -0
  53. package/src/bindings/index.ts +16 -0
  54. package/src/bindings/output-gate.ts +50 -0
  55. package/src/bindings/resolved-output.ts +56 -0
  56. package/src/bindings/types.ts +16 -0
  57. package/src/frontend/argdump/index.ts +1 -0
  58. package/src/frontend/argdump/parser.ts +914 -0
  59. package/src/frontend/boutiques/destruct-template.ts +50 -0
  60. package/src/frontend/boutiques/index.ts +1 -0
  61. package/src/frontend/boutiques/parser.ts +676 -0
  62. package/src/frontend/boutiques/split-command.ts +69 -0
  63. package/src/frontend/detect-format.ts +42 -0
  64. package/src/frontend/frontend.ts +31 -0
  65. package/src/frontend/index.ts +9 -0
  66. package/src/frontend/workbench/index.ts +1 -0
  67. package/src/frontend/workbench/parser.ts +351 -0
  68. package/src/index.ts +41 -0
  69. package/src/ir/builders.ts +69 -0
  70. package/src/ir/format.ts +157 -0
  71. package/src/ir/index.ts +32 -0
  72. package/src/ir/meta.ts +91 -0
  73. package/src/ir/node.ts +95 -0
  74. package/src/ir/passes/canonicalize.ts +108 -0
  75. package/src/ir/passes/flatten.ts +73 -0
  76. package/src/ir/passes/index.ts +7 -0
  77. package/src/ir/passes/pass.ts +86 -0
  78. package/src/ir/passes/pipeline.ts +21 -0
  79. package/src/ir/passes/remove-empty.ts +76 -0
  80. package/src/ir/passes/simplify.ts +179 -0
  81. package/src/ir/types.ts +15 -0
  82. package/src/manifest/context.ts +36 -0
  83. package/src/manifest/index.ts +3 -0
  84. package/src/manifest/types.ts +15 -0
  85. package/src/solver/assign-access.ts +218 -0
  86. package/src/solver/index.ts +4 -0
  87. package/src/solver/resolve-outputs.ts +233 -0
  88. package/src/solver/solver.ts +319 -0
@@ -0,0 +1,157 @@
1
+ import type { AppMeta, Output, OutputToken } from "./meta.js";
2
+ import type { Expr } from "./node.js";
3
+
4
+ export function format(expr: Expr, meta?: AppMeta): string {
5
+ const lines: string[] = [];
6
+
7
+ if (meta) {
8
+ lines.push(`app ${meta.id ?? "unknown"}${meta.version ? `@${meta.version}` : ""}`);
9
+ if (meta.doc?.description) {
10
+ lines.push(` "${meta.doc.description}"`);
11
+ }
12
+ if (meta.doc?.authors?.length) {
13
+ lines.push(` authors: ${meta.doc.authors.join(", ")}`);
14
+ }
15
+ if (meta.container) {
16
+ lines.push(` container: ${meta.container.image}`);
17
+ }
18
+ if (meta.stdout) {
19
+ lines.push(` stdout: ${meta.stdout.name}`);
20
+ }
21
+ if (meta.stderr) {
22
+ lines.push(` stderr: ${meta.stderr.name}`);
23
+ }
24
+ lines.push("");
25
+ }
26
+
27
+ lines.push(formatExpr(expr, 0));
28
+ return lines.join("\n");
29
+ }
30
+
31
+ function formatOutputToken(token: OutputToken): string {
32
+ if (token.kind === "literal") return JSON.stringify(token.value);
33
+ const flags = [
34
+ token.stripExtensions?.length && `strip=${JSON.stringify(token.stripExtensions)}`,
35
+ token.fallback !== undefined && `fallback=${JSON.stringify(token.fallback)}`,
36
+ ].filter(Boolean);
37
+ const suffix = flags.length > 0 ? ` {${flags.join(", ")}}` : "";
38
+ return `ref(${token.target.name})${suffix}`;
39
+ }
40
+
41
+ function formatOutputsBlock(outputs: Output[], indent: number): string {
42
+ const pad = " ".repeat(indent);
43
+ const lines = [`${pad}outputs:`];
44
+ for (const out of outputs) {
45
+ const name = out.name ?? "<anon>";
46
+ const media = out.mediaTypes?.length ? ` (${out.mediaTypes.join(", ")})` : "";
47
+ const tokens = out.tokens.map(formatOutputToken).join(" + ") || `""`;
48
+ lines.push(`${pad} ${name}${media}: ${tokens}`);
49
+ }
50
+ return lines.join("\n");
51
+ }
52
+
53
+ // Splice the outputs block in right after the node's header line (its first
54
+ // line), before any child lines, so outputs read naturally as belonging to
55
+ // the node they decorate.
56
+ function spliceOutputs(body: string, outputsBlock: string): string {
57
+ if (!outputsBlock) return body;
58
+ const nl = body.indexOf("\n");
59
+ if (nl === -1) return `${body}\n${outputsBlock}`;
60
+ return `${body.slice(0, nl)}\n${outputsBlock}${body.slice(nl)}`;
61
+ }
62
+
63
+ function formatExpr(expr: Expr, indent: number): string {
64
+ const pad = " ".repeat(indent);
65
+ const name = expr.meta?.name ? ` [${expr.meta.name}]` : "";
66
+ const outputsBlock = expr.meta?.outputs?.length
67
+ ? formatOutputsBlock(expr.meta.outputs, indent + 1)
68
+ : "";
69
+
70
+ let body: string;
71
+ switch (expr.kind) {
72
+ case "literal":
73
+ body = `${pad}literal${name} "${expr.attrs.str}"`;
74
+ break;
75
+
76
+ case "str":
77
+ body = `${pad}str${name}`;
78
+ break;
79
+
80
+ case "int": {
81
+ const { minValue, maxValue } = expr.attrs;
82
+ const range =
83
+ minValue !== undefined || maxValue !== undefined
84
+ ? ` (${minValue ?? ""}..${maxValue ?? ""})`
85
+ : "";
86
+ body = `${pad}int${name}${range}`;
87
+ break;
88
+ }
89
+
90
+ case "float": {
91
+ const { minValue, maxValue } = expr.attrs;
92
+ const range =
93
+ minValue !== undefined || maxValue !== undefined
94
+ ? ` (${minValue ?? ""}..${maxValue ?? ""})`
95
+ : "";
96
+ body = `${pad}float${name}${range}`;
97
+ break;
98
+ }
99
+
100
+ case "path": {
101
+ const flags = [
102
+ expr.attrs.resolveParent && "resolveParent",
103
+ expr.attrs.mutable && "mutable",
104
+ ].filter(Boolean);
105
+ const suffix = flags.length > 0 ? ` {${flags.join(", ")}}` : "";
106
+ body = `${pad}path${name}${suffix}`;
107
+ break;
108
+ }
109
+
110
+ case "sequence": {
111
+ const join = expr.attrs.join !== undefined ? ` join="${expr.attrs.join}"` : "";
112
+ const header = `${pad}sequence${name}${join}`;
113
+ if (expr.attrs.nodes.length === 0) {
114
+ body = `${header} (empty)`;
115
+ } else {
116
+ const children = expr.attrs.nodes.map((n) => formatExpr(n, indent + 1)).join("\n");
117
+ body = `${header}\n${children}`;
118
+ }
119
+ break;
120
+ }
121
+
122
+ case "alternative": {
123
+ const header = `${pad}alternative${name}`;
124
+ const children = expr.attrs.alts.map((n) => formatExpr(n, indent + 1)).join("\n");
125
+ body = `${header}\n${children}`;
126
+ break;
127
+ }
128
+
129
+ case "optional": {
130
+ const header = `${pad}optional${name}`;
131
+ const child = formatExpr(expr.attrs.node, indent + 1);
132
+ body = `${header}\n${child}`;
133
+ break;
134
+ }
135
+
136
+ case "repeat": {
137
+ const { join, countMin, countMax } = expr.attrs;
138
+ const parts = [
139
+ join !== undefined && `join="${join}"`,
140
+ countMin !== undefined && `min=${countMin}`,
141
+ countMax !== undefined && `max=${countMax}`,
142
+ ].filter(Boolean);
143
+ const suffix = parts.length > 0 ? ` {${parts.join(", ")}}` : "";
144
+ const header = `${pad}repeat${name}${suffix}`;
145
+ const child = formatExpr(expr.attrs.node, indent + 1);
146
+ body = `${header}\n${child}`;
147
+ break;
148
+ }
149
+
150
+ default: {
151
+ const _exhaustive: never = expr;
152
+ body = `${pad}unknown`;
153
+ }
154
+ }
155
+
156
+ return spliceOutputs(body, outputsBlock);
157
+ }
@@ -0,0 +1,32 @@
1
+ export { alt, float, int, lit, opt, path, rep, repJoin, seq, seqJoin, str } from "./builders.js";
2
+ export { format } from "./format.js";
3
+ export type { AppMeta, NodeMeta, NodeRef, Output, OutputToken, StreamOutput } from "./meta.js";
4
+ export { effectiveOutputName, nodeRef } from "./meta.js";
5
+ export type {
6
+ Alternative,
7
+ Expr,
8
+ Float,
9
+ Int,
10
+ Literal,
11
+ Optional,
12
+ Path,
13
+ Repeat,
14
+ Sequence,
15
+ Str,
16
+ StructuralNode,
17
+ Terminal,
18
+ } from "./node.js";
19
+ export { isStructural, isTerminal } from "./node.js";
20
+ export type { Pass, PassResult } from "./passes/index.js";
21
+ export {
22
+ canonicalize,
23
+ compose,
24
+ createPipeline,
25
+ defaultPipeline,
26
+ fixpoint,
27
+ flatten,
28
+ PassStatus,
29
+ simplify,
30
+ removeEmpty,
31
+ } from "./passes/index.js";
32
+ export type { Documentation, MediaTypeIdentifier, ScalarKind } from "./types.js";
package/src/ir/meta.ts ADDED
@@ -0,0 +1,91 @@
1
+ import type { Documentation, MediaTypeIdentifier } from "./types.js";
2
+
3
+ /**
4
+ * Opaque, name-based reference to an IR node by its `NodeMeta.name`.
5
+ *
6
+ * Resolved post-solve against the binding registry, not within IR passes.
7
+ * Names survive optimization (pointers don't), so token refs stay valid even
8
+ * after passes rewrite the tree. See `memory/design_outputs.md`.
9
+ */
10
+ export interface NodeRef {
11
+ kind: "node-ref";
12
+ name: string;
13
+ }
14
+
15
+ /** Construct a NodeRef from a node name. */
16
+ export function nodeRef(name: string): NodeRef {
17
+ return { kind: "node-ref", name };
18
+ }
19
+
20
+ /** Output token: literal text or a parameter reference. */
21
+ export type OutputToken =
22
+ | { kind: "literal"; value: string }
23
+ | {
24
+ kind: "ref";
25
+ target: NodeRef;
26
+ stripExtensions?: string[];
27
+ fallback?: string;
28
+ };
29
+
30
+ /**
31
+ * Specification for a file the tool produces. Lives on `NodeMeta.outputs` of
32
+ * the struct node (root sequence or subcommand sequence) that declared it.
33
+ * Per-output gating is derived downstream from the scope's binding gate and
34
+ * each ref binding's gate, so no host node or `optional` flag is stored.
35
+ */
36
+ export interface Output {
37
+ name?: string;
38
+ doc?: Documentation;
39
+ tokens: OutputToken[];
40
+ mediaTypes?: MediaTypeIdentifier[];
41
+ }
42
+
43
+ /** Metadata attached to any IR node. */
44
+ export interface NodeMeta {
45
+ /** Name identifier for this node (used by solver for binding names). */
46
+ name?: string;
47
+ /**
48
+ * The discriminator (`@type`) tag for this node when it is a union arm (a
49
+ * Boutiques sub-command). Kept separate from `name`: a single-field
50
+ * sub-command collapses onto its inner field, whose `name` then wins, so the
51
+ * tag would otherwise become the inner field's id - e.g. two distinct
52
+ * sub-commands `VariousString`/`VariousFile` both wrapping an `obj` field
53
+ * would collide on `@type: "obj"` (and the second arm would be unreachable).
54
+ * `mergeMeta` preserves this through the collapse and the solver prefers it
55
+ * for the variant tag, so distinct sub-commands keep distinct, reachable
56
+ * `@type`s.
57
+ */
58
+ variantTag?: string;
59
+ doc?: Documentation;
60
+ defaultValue?: string | number | boolean;
61
+ /** Files produced when this node is active. See `Output`. */
62
+ outputs?: Output[];
63
+ }
64
+
65
+ /** Application-level metadata for the root node. */
66
+ export interface AppMeta {
67
+ id?: string;
68
+ version?: string;
69
+ doc?: Documentation;
70
+ container?: {
71
+ image: string;
72
+ type?: "docker" | "singularity";
73
+ };
74
+ stdout?: StreamOutput;
75
+ stderr?: StreamOutput;
76
+ }
77
+
78
+ export interface StreamOutput {
79
+ name: string;
80
+ doc?: Documentation;
81
+ }
82
+
83
+ /**
84
+ * Produce a usable name for an Output. Frontends may leave `Output.name`
85
+ * unset; downstream code (resolver, validator, backends) needs a stable
86
+ * identifier for diagnostics and field naming. Falls back to `output_<index>`
87
+ * keyed by the output's position in tree-walk order.
88
+ */
89
+ export function effectiveOutputName(output: Output, index: number): string {
90
+ return output.name ?? `output_${index}`;
91
+ }
package/src/ir/node.ts ADDED
@@ -0,0 +1,95 @@
1
+ import type { NodeMeta } from "./meta.js";
2
+ import type { MediaTypeIdentifier } from "./types.js";
3
+
4
+ /** Base structure for all IR nodes */
5
+ interface BaseNode<K extends string, Attrs> {
6
+ kind: K;
7
+ meta?: NodeMeta;
8
+ attrs: Attrs;
9
+ }
10
+
11
+ // Structural nodes
12
+
13
+ export type Literal = BaseNode<
14
+ "literal",
15
+ {
16
+ str: string;
17
+ }
18
+ >;
19
+
20
+ export type Sequence = BaseNode<
21
+ "sequence",
22
+ {
23
+ nodes: Expr[];
24
+ join?: string;
25
+ }
26
+ >;
27
+
28
+ export type Optional = BaseNode<
29
+ "optional",
30
+ {
31
+ node: Expr;
32
+ }
33
+ >;
34
+
35
+ export type Alternative = BaseNode<
36
+ "alternative",
37
+ {
38
+ alts: Expr[];
39
+ }
40
+ >;
41
+
42
+ export type Repeat = BaseNode<
43
+ "repeat",
44
+ {
45
+ node: Expr;
46
+ join?: string;
47
+ countMin?: number;
48
+ countMax?: number;
49
+ }
50
+ >;
51
+
52
+ // Terminal nodes
53
+
54
+ export type Int = BaseNode<
55
+ "int",
56
+ {
57
+ minValue?: number;
58
+ maxValue?: number;
59
+ }
60
+ >;
61
+
62
+ export type Float = BaseNode<
63
+ "float",
64
+ {
65
+ minValue?: number;
66
+ maxValue?: number;
67
+ }
68
+ >;
69
+
70
+ export type Str = BaseNode<"str", Record<string, never>>;
71
+
72
+ export type Path = BaseNode<
73
+ "path",
74
+ {
75
+ resolveParent?: boolean;
76
+ mutable?: boolean;
77
+ mediaTypes?: MediaTypeIdentifier[];
78
+ }
79
+ >;
80
+
81
+ // Union types
82
+
83
+ export type StructuralNode = Sequence | Optional | Alternative | Repeat;
84
+ export type Terminal = Literal | Int | Float | Str | Path;
85
+ export type Expr = StructuralNode | Terminal;
86
+
87
+ // Type guards
88
+
89
+ export function isTerminal(expr: Expr): expr is Terminal {
90
+ return ["literal", "int", "float", "str", "path"].includes(expr.kind);
91
+ }
92
+
93
+ export function isStructural(expr: Expr): expr is StructuralNode {
94
+ return ["sequence", "optional", "alternative", "repeat"].includes(expr.kind);
95
+ }
@@ -0,0 +1,108 @@
1
+ import type { Expr } from "../node.js";
2
+ import { PassStatus, type Pass, type PassResult } from "./pass.js";
3
+
4
+ /**
5
+ * Canonicalize IR for consistent representation:
6
+ * - Sort alternatives by kind, then name, then structure
7
+ * - Deduplicate identical alternatives
8
+ */
9
+ export const canonicalize: Pass = {
10
+ name: "canonicalize",
11
+ apply(expr: Expr): PassResult {
12
+ let changed = false;
13
+
14
+ function structuralHash(node: Expr): string {
15
+ switch (node.kind) {
16
+ case "literal":
17
+ return `lit:${node.attrs.str}`;
18
+ case "int":
19
+ return `int:${node.attrs.minValue ?? ""}:${node.attrs.maxValue ?? ""}`;
20
+ case "float":
21
+ return `float:${node.attrs.minValue ?? ""}:${node.attrs.maxValue ?? ""}`;
22
+ case "str":
23
+ return "str";
24
+ case "path":
25
+ return `path:${node.attrs.resolveParent ?? ""}:${node.attrs.mutable ?? ""}`;
26
+ case "optional":
27
+ return `opt:${structuralHash(node.attrs.node)}`;
28
+ case "repeat":
29
+ return `rep:${node.attrs.join ?? ""}:${structuralHash(node.attrs.node)}`;
30
+ case "sequence":
31
+ return `seq:${node.attrs.join ?? ""}:${node.attrs.nodes.map(structuralHash).join(",")}`;
32
+ case "alternative":
33
+ return `alt:${node.attrs.alts.map(structuralHash).join(",")}`;
34
+ default: {
35
+ const _exhaustive: never = node;
36
+ return "";
37
+ }
38
+ }
39
+ }
40
+
41
+ function sortKey(node: Expr): string {
42
+ const name = node.meta?.name ?? "";
43
+ return `${node.kind}:${name}:${structuralHash(node)}`;
44
+ }
45
+
46
+ function visit(node: Expr): Expr {
47
+ switch (node.kind) {
48
+ case "alternative": {
49
+ const children = node.attrs.alts.map(visit);
50
+
51
+ // Sort alternatives
52
+ const sorted = [...children].sort((a, b) => sortKey(a).localeCompare(sortKey(b)));
53
+
54
+ // Deduplicate by structural hash
55
+ const seen = new Set<string>();
56
+ const alts: Expr[] = [];
57
+ for (const child of sorted) {
58
+ const hash = structuralHash(child);
59
+ if (!seen.has(hash)) {
60
+ seen.add(hash);
61
+ alts.push(child);
62
+ } else {
63
+ changed = true;
64
+ }
65
+ }
66
+
67
+ // Check if order changed
68
+ if (
69
+ alts.length !== children.length ||
70
+ alts.some((alt, i) => structuralHash(alt) !== structuralHash(children[i]!))
71
+ ) {
72
+ changed = true;
73
+ }
74
+
75
+ return { ...node, attrs: { ...node.attrs, alts } };
76
+ }
77
+
78
+ case "sequence": {
79
+ const nodes = node.attrs.nodes.map(visit);
80
+ return { ...node, attrs: { ...node.attrs, nodes } };
81
+ }
82
+
83
+ case "optional":
84
+ return { ...node, attrs: { node: visit(node.attrs.node) } };
85
+
86
+ case "repeat":
87
+ return { ...node, attrs: { ...node.attrs, node: visit(node.attrs.node) } };
88
+
89
+ case "literal":
90
+ case "int":
91
+ case "float":
92
+ case "str":
93
+ case "path":
94
+ return node;
95
+
96
+ default: {
97
+ const _exhaustive: never = node;
98
+ return node;
99
+ }
100
+ }
101
+ }
102
+
103
+ return {
104
+ expr: visit(expr),
105
+ status: changed ? PassStatus.Changed : PassStatus.Unchanged,
106
+ };
107
+ },
108
+ };
@@ -0,0 +1,73 @@
1
+ import type { Expr } from "../node.js";
2
+ import { PassStatus, type Pass, type PassResult } from "./pass.js";
3
+
4
+ /**
5
+ * Flatten passes
6
+ * - seq(a, seq(b, c)) -> seq(a, b, c)
7
+ * - alt(a, alt(b, c)) -> alt(a, b, c)
8
+ */
9
+ export const flatten: Pass = {
10
+ name: "flatten",
11
+ apply(expr: Expr): PassResult {
12
+ let changed = false;
13
+
14
+ function visit(node: Expr): Expr {
15
+ switch (node.kind) {
16
+ case "sequence": {
17
+ const children = node.attrs.nodes.map(visit);
18
+ const nodes: Expr[] = [];
19
+
20
+ for (const child of children) {
21
+ if (child.kind === "sequence" && child.attrs.join === node.attrs.join && !child.meta) {
22
+ changed = true;
23
+ nodes.push(...child.attrs.nodes);
24
+ } else {
25
+ nodes.push(child);
26
+ }
27
+ }
28
+
29
+ return { ...node, attrs: { ...node.attrs, nodes } };
30
+ }
31
+
32
+ case "alternative": {
33
+ const children = node.attrs.alts.map(visit);
34
+ const alts: Expr[] = [];
35
+
36
+ for (const child of children) {
37
+ if (child.kind === "alternative" && !child.meta) {
38
+ changed = true;
39
+ alts.push(...child.attrs.alts);
40
+ } else {
41
+ alts.push(child);
42
+ }
43
+ }
44
+
45
+ return { ...node, attrs: { ...node.attrs, alts } };
46
+ }
47
+
48
+ case "optional":
49
+ return { ...node, attrs: { node: visit(node.attrs.node) } };
50
+
51
+ case "repeat":
52
+ return { ...node, attrs: { ...node.attrs, node: visit(node.attrs.node) } };
53
+
54
+ case "literal":
55
+ case "int":
56
+ case "float":
57
+ case "str":
58
+ case "path":
59
+ return node;
60
+
61
+ default: {
62
+ const _exhaustive: never = node;
63
+ return node;
64
+ }
65
+ }
66
+ }
67
+
68
+ return {
69
+ expr: visit(expr),
70
+ status: changed ? PassStatus.Changed : PassStatus.Unchanged,
71
+ };
72
+ },
73
+ };
@@ -0,0 +1,7 @@
1
+ export { canonicalize } from "./canonicalize.js";
2
+ export { flatten } from "./flatten.js";
3
+ export type { Pass, PassResult } from "./pass.js";
4
+ export { compose, fixpoint, PassStatus } from "./pass.js";
5
+ export { createPipeline, defaultPipeline } from "./pipeline.js";
6
+ export { simplify } from "./simplify.js";
7
+ export { removeEmpty } from "./remove-empty.js";
@@ -0,0 +1,86 @@
1
+ import type { Expr } from "../node.js";
2
+
3
+ export enum PassStatus {
4
+ Unchanged = "unchanged",
5
+ Changed = "changed",
6
+ ChangedNeedsRerun = "changed-needs-rerun",
7
+ }
8
+
9
+ export interface PassResult {
10
+ expr: Expr;
11
+ status: PassStatus;
12
+ warnings?: string[];
13
+ }
14
+
15
+ export interface Pass {
16
+ readonly name: string;
17
+ apply(expr: Expr): PassResult;
18
+ }
19
+
20
+ export function compose(...passes: Pass[]): Pass {
21
+ return {
22
+ name: passes.map((p) => p.name).join(" → "),
23
+ apply(expr: Expr): PassResult {
24
+ let current = expr;
25
+ let overallStatus = PassStatus.Unchanged;
26
+ const warnings: string[] = [];
27
+
28
+ for (const pass of passes) {
29
+ const result = pass.apply(current);
30
+ current = result.expr;
31
+
32
+ if (result.status !== PassStatus.Unchanged) {
33
+ overallStatus = result.status;
34
+ }
35
+ if (result.warnings) {
36
+ warnings.push(...result.warnings);
37
+ }
38
+ }
39
+
40
+ return {
41
+ expr: current,
42
+ status: overallStatus,
43
+ ...(warnings.length > 0 && { warnings }),
44
+ };
45
+ },
46
+ };
47
+ }
48
+
49
+ export function fixpoint(pass: Pass, maxIterations = 10): Pass {
50
+ return {
51
+ name: `fixpoint(${pass.name})`,
52
+ apply(expr: Expr): PassResult {
53
+ let current = expr;
54
+ const allWarnings: string[] = [];
55
+
56
+ for (let i = 0; i < maxIterations; ++i) {
57
+ const result = pass.apply(current);
58
+
59
+ if (result.warnings) {
60
+ allWarnings.push(...result.warnings);
61
+ }
62
+
63
+ // Stop if unchanged or no rerun needed
64
+ if (result.status !== PassStatus.ChangedNeedsRerun) {
65
+ return {
66
+ expr: result.expr,
67
+ status:
68
+ result.status === PassStatus.Unchanged ? PassStatus.Unchanged : PassStatus.Changed,
69
+ ...(allWarnings.length > 0 && { warnings: allWarnings }),
70
+ };
71
+ }
72
+
73
+ current = result.expr;
74
+ }
75
+
76
+ return {
77
+ expr: current,
78
+ status: PassStatus.Changed,
79
+ warnings: [
80
+ ...allWarnings,
81
+ `${pass.name} did not converge after ${maxIterations} iterations`,
82
+ ],
83
+ };
84
+ },
85
+ };
86
+ }
@@ -0,0 +1,21 @@
1
+ // import { canonicalize } from "./canonicalize.js";
2
+ import { flatten } from "./flatten.js";
3
+ import type { Pass } from "./pass.js";
4
+ import { compose, fixpoint } from "./pass.js";
5
+ import { removeEmpty } from "./remove-empty.js";
6
+ import { simplify } from "./simplify.js";
7
+
8
+ export const defaultPipeline: Pass = fixpoint(
9
+ compose(flatten, removeEmpty, simplify /* canonicalize */),
10
+ );
11
+
12
+ export function createPipeline(
13
+ passes: Pass[],
14
+ options?: { fixpoint?: boolean; maxIterations?: number },
15
+ ): Pass {
16
+ const composed = compose(...passes);
17
+ if (options?.fixpoint) {
18
+ return fixpoint(composed, options.maxIterations);
19
+ }
20
+ return composed;
21
+ }