@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.
- package/dist/index.cjs +7947 -0
- package/dist/index.d.cts +1143 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +1143 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +7877 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +55 -0
- package/src/backend/backend.ts +95 -0
- package/src/backend/boutiques/boutiques.ts +1049 -0
- package/src/backend/boutiques/index.ts +1 -0
- package/src/backend/code-builder.ts +49 -0
- package/src/backend/collect-field-info.ts +50 -0
- package/src/backend/collect-named-types.ts +103 -0
- package/src/backend/collect-output-fields.ts +222 -0
- package/src/backend/find-doc.ts +38 -0
- package/src/backend/find-struct-node.ts +66 -0
- package/src/backend/index.ts +39 -0
- package/src/backend/python/arg-builder.ts +454 -0
- package/src/backend/python/emit.ts +638 -0
- package/src/backend/python/index.ts +9 -0
- package/src/backend/python/outputs-emit.ts +430 -0
- package/src/backend/python/packaging.ts +173 -0
- package/src/backend/python/python.ts +558 -0
- package/src/backend/python/snippet.ts +84 -0
- package/src/backend/python/typemap.ts +131 -0
- package/src/backend/python/types.ts +8 -0
- package/src/backend/python/validate-emit.ts +356 -0
- package/src/backend/resolve-field-binding.ts +41 -0
- package/src/backend/resolve-output-tokens.ts +80 -0
- package/src/backend/schema/index.ts +2 -0
- package/src/backend/schema/jsonschema.ts +303 -0
- package/src/backend/scope.ts +50 -0
- package/src/backend/sig-entries.ts +97 -0
- package/src/backend/snippet-core.ts +185 -0
- package/src/backend/string-case.ts +30 -0
- package/src/backend/styxdefs-compat.ts +21 -0
- package/src/backend/type-keys.ts +52 -0
- package/src/backend/typescript/arg-builder.ts +420 -0
- package/src/backend/typescript/emit.ts +450 -0
- package/src/backend/typescript/index.ts +10 -0
- package/src/backend/typescript/outputs-emit.ts +389 -0
- package/src/backend/typescript/packaging.ts +130 -0
- package/src/backend/typescript/snippet.ts +60 -0
- package/src/backend/typescript/typemap.ts +47 -0
- package/src/backend/typescript/types.ts +8 -0
- package/src/backend/typescript/typescript.ts +507 -0
- package/src/backend/typescript/validate-emit.ts +341 -0
- package/src/backend/union-variants.ts +42 -0
- package/src/backend/validate-walk.ts +111 -0
- package/src/bindings/binding.ts +77 -0
- package/src/bindings/format.ts +176 -0
- package/src/bindings/index.ts +16 -0
- package/src/bindings/output-gate.ts +50 -0
- package/src/bindings/resolved-output.ts +56 -0
- package/src/bindings/types.ts +16 -0
- package/src/frontend/argdump/index.ts +1 -0
- package/src/frontend/argdump/parser.ts +914 -0
- package/src/frontend/boutiques/destruct-template.ts +50 -0
- package/src/frontend/boutiques/index.ts +1 -0
- package/src/frontend/boutiques/parser.ts +676 -0
- package/src/frontend/boutiques/split-command.ts +69 -0
- package/src/frontend/detect-format.ts +42 -0
- package/src/frontend/frontend.ts +31 -0
- package/src/frontend/index.ts +9 -0
- package/src/frontend/workbench/index.ts +1 -0
- package/src/frontend/workbench/parser.ts +351 -0
- package/src/index.ts +41 -0
- package/src/ir/builders.ts +69 -0
- package/src/ir/format.ts +157 -0
- package/src/ir/index.ts +32 -0
- package/src/ir/meta.ts +91 -0
- package/src/ir/node.ts +95 -0
- package/src/ir/passes/canonicalize.ts +108 -0
- package/src/ir/passes/flatten.ts +73 -0
- package/src/ir/passes/index.ts +7 -0
- package/src/ir/passes/pass.ts +86 -0
- package/src/ir/passes/pipeline.ts +21 -0
- package/src/ir/passes/remove-empty.ts +76 -0
- package/src/ir/passes/simplify.ts +179 -0
- package/src/ir/types.ts +15 -0
- package/src/manifest/context.ts +36 -0
- package/src/manifest/index.ts +3 -0
- package/src/manifest/types.ts +15 -0
- package/src/solver/assign-access.ts +218 -0
- package/src/solver/index.ts +4 -0
- package/src/solver/resolve-outputs.ts +233 -0
- package/src/solver/solver.ts +319 -0
package/src/ir/format.ts
ADDED
|
@@ -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
|
+
}
|
package/src/ir/index.ts
ADDED
|
@@ -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
|
+
}
|