@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { BoutiquesBackend, generateBoutiques } from "./boutiques.js";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Line-buffer abstraction for code emission. */
|
|
2
|
+
export class CodeBuilder {
|
|
3
|
+
private readonly lines: string[] = [];
|
|
4
|
+
private depth = 0;
|
|
5
|
+
private readonly indentStr: string;
|
|
6
|
+
|
|
7
|
+
constructor(indent = " ") {
|
|
8
|
+
this.indentStr = indent;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Append a line at the current indentation level. */
|
|
12
|
+
line(text: string): this {
|
|
13
|
+
this.lines.push(this.indentStr.repeat(this.depth) + text);
|
|
14
|
+
return this;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Append a blank line. */
|
|
18
|
+
blank(): this {
|
|
19
|
+
this.lines.push("");
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Append a comment line. */
|
|
24
|
+
comment(text: string, prefix = "// "): this {
|
|
25
|
+
return this.line(`${prefix}${text}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Run a callback with increased indentation. */
|
|
29
|
+
indent(fn: () => void): this {
|
|
30
|
+
this.depth++;
|
|
31
|
+
fn();
|
|
32
|
+
this.depth--;
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Append all lines from another CodeBuilder at the current indentation level. */
|
|
37
|
+
append(other: CodeBuilder): this {
|
|
38
|
+
const prefix = this.indentStr.repeat(this.depth);
|
|
39
|
+
for (const line of other.lines) {
|
|
40
|
+
this.lines.push(line === "" ? "" : prefix + line);
|
|
41
|
+
}
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Return the built code as a string. */
|
|
46
|
+
toString(): string {
|
|
47
|
+
return this.lines.join("\n");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { BoundType } from "../bindings/index.js";
|
|
2
|
+
import type { CodegenContext } from "../manifest/index.js";
|
|
3
|
+
import { findDoc } from "./find-doc.js";
|
|
4
|
+
import { findStructNode } from "./find-struct-node.js";
|
|
5
|
+
import { resolveFieldBinding } from "./resolve-field-binding.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Metadata extracted for each field of a struct type.
|
|
9
|
+
*
|
|
10
|
+
* `doc` is the field's description, recovered from wrapper nodes via `findDoc`.
|
|
11
|
+
* `defaultValue` is pulled from the wrapper or binding node's metadata.
|
|
12
|
+
*/
|
|
13
|
+
export interface FieldInfo {
|
|
14
|
+
doc?: string;
|
|
15
|
+
defaultValue?: string | number | boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Collect field metadata (doc, defaultValue) for each field of a struct type.
|
|
20
|
+
*
|
|
21
|
+
* Walks the IR tree to find the sequence node containing the struct's fields,
|
|
22
|
+
* then resolves each child to its field binding. Metadata is recovered from both
|
|
23
|
+
* the wrapper node (where the parser hoists doc) and the binding node (where the
|
|
24
|
+
* solver places the binding after sequence collapse).
|
|
25
|
+
*/
|
|
26
|
+
export function collectFieldInfo(
|
|
27
|
+
ctx: CodegenContext,
|
|
28
|
+
structType: Extract<BoundType, { kind: "struct" }>,
|
|
29
|
+
): Map<string, FieldInfo> {
|
|
30
|
+
const info = new Map<string, FieldInfo>();
|
|
31
|
+
|
|
32
|
+
const structNode = findStructNode(ctx.expr, ctx, structType);
|
|
33
|
+
if (!structNode) return info;
|
|
34
|
+
|
|
35
|
+
for (const child of structNode.attrs.nodes) {
|
|
36
|
+
const match = resolveFieldBinding(child, ctx, structType);
|
|
37
|
+
if (!match) continue;
|
|
38
|
+
const { binding, wrapperNode } = match;
|
|
39
|
+
const fieldInfo: FieldInfo = {};
|
|
40
|
+
const fieldType = structType.fields[binding.name]!;
|
|
41
|
+
// Check wrapper node first (doc may be hoisted there), then binding node
|
|
42
|
+
const doc = findDoc(wrapperNode, fieldType) ?? findDoc(binding.node, fieldType);
|
|
43
|
+
if (doc) fieldInfo.doc = doc;
|
|
44
|
+
const defaultValue = wrapperNode.meta?.defaultValue ?? binding.node.meta?.defaultValue;
|
|
45
|
+
if (defaultValue !== undefined) fieldInfo.defaultValue = defaultValue;
|
|
46
|
+
info.set(binding.name, fieldInfo);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return info;
|
|
50
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { BoundType } from "../bindings/index.js";
|
|
2
|
+
import { Scope } from "./scope.js";
|
|
3
|
+
import { structKey, unionKey } from "./type-keys.js";
|
|
4
|
+
|
|
5
|
+
/** A named compound type (struct or union) discovered during type collection. */
|
|
6
|
+
export interface NamedType {
|
|
7
|
+
name: string;
|
|
8
|
+
type: BoundType;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Recursively collect all struct/union types and assign unique names.
|
|
13
|
+
*
|
|
14
|
+
* Walks the BoundType tree depth-first, using structural keys (`structKey`,
|
|
15
|
+
* `unionKey`) to deduplicate types that appear multiple times in the tree.
|
|
16
|
+
* Each unique struct/union gets a name generated by applying `nameTransform`
|
|
17
|
+
* to a hint string (derived from the root name or field/variant names).
|
|
18
|
+
*
|
|
19
|
+
* @param rootType - The root BoundType to walk.
|
|
20
|
+
* @param rootName - The hint for the root type's name (already tool-qualified).
|
|
21
|
+
* @param scope - Symbol scope for collision avoidance.
|
|
22
|
+
* @param nameTransform - Converts a hint string to a type name (e.g. pascalCase, snake_case).
|
|
23
|
+
* @param nestedPrefix - Prepended to every NON-root type name so a suite's flat
|
|
24
|
+
* barrel (`export * from`/`from .x import *`) can't collide two tools' types
|
|
25
|
+
* that share a local field/variant name (e.g. `Outputtype`). Pass the tool's
|
|
26
|
+
* type prefix (its root name); defaults to "" for single-tool emission.
|
|
27
|
+
* @returns `namedTypes` maps structural keys to assigned names, `typeDecls` lists declarations in order.
|
|
28
|
+
*/
|
|
29
|
+
export function collectNamedTypes(
|
|
30
|
+
rootType: BoundType,
|
|
31
|
+
rootName: string,
|
|
32
|
+
scope: Scope,
|
|
33
|
+
nameTransform: (hint: string) => string,
|
|
34
|
+
nestedPrefix = "",
|
|
35
|
+
): { namedTypes: Map<string, string>; typeDecls: NamedType[] } {
|
|
36
|
+
const namedTypes = new Map<string, string>();
|
|
37
|
+
const typeDecls: NamedType[] = [];
|
|
38
|
+
|
|
39
|
+
function nameFor(hint: string, isRoot: boolean): string {
|
|
40
|
+
const cased = isRoot ? nameTransform(hint) : nestedPrefix + nameTransform(hint);
|
|
41
|
+
// Dedup in the cased (emitted) namespace, folding any disambiguation suffix
|
|
42
|
+
// back through nameTransform so a collision yields e.g. `Config2`, not the
|
|
43
|
+
// mixed-case `Config_2`. Uniqueness is still checked on the final form, so
|
|
44
|
+
// two hints that case-collide get distinct names.
|
|
45
|
+
return scope.add(cased, nameTransform);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function visit(type: BoundType, hint: string, isRoot: boolean): void {
|
|
49
|
+
switch (type.kind) {
|
|
50
|
+
case "struct": {
|
|
51
|
+
const key = structKey(type);
|
|
52
|
+
if (!namedTypes.has(key)) {
|
|
53
|
+
const name = nameFor(hint, isRoot);
|
|
54
|
+
namedTypes.set(key, name);
|
|
55
|
+
typeDecls.push({ name, type });
|
|
56
|
+
for (const [fieldName, fieldType] of Object.entries(type.fields)) {
|
|
57
|
+
visit(fieldType, fieldName, false);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case "union": {
|
|
63
|
+
const key = unionKey(type);
|
|
64
|
+
if (!namedTypes.has(key)) {
|
|
65
|
+
const name = nameFor(hint, isRoot);
|
|
66
|
+
namedTypes.set(key, name);
|
|
67
|
+
typeDecls.push({ name, type });
|
|
68
|
+
for (const v of type.variants) {
|
|
69
|
+
visit(v.type, v.name ?? hint, false);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
// Wrappers are transparent: an optional/list at the root still names its
|
|
75
|
+
// inner type as the root (no prefix), matching the pre-prefix behavior.
|
|
76
|
+
case "optional":
|
|
77
|
+
visit(type.inner, hint, isRoot);
|
|
78
|
+
break;
|
|
79
|
+
case "list":
|
|
80
|
+
visit(type.item, hint, isRoot);
|
|
81
|
+
break;
|
|
82
|
+
default:
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
visit(rootType, rootName, true);
|
|
88
|
+
return { namedTypes, typeDecls };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create a type name resolver from a namedTypes map.
|
|
93
|
+
* Returns a function that maps a BoundType to its assigned name (if any).
|
|
94
|
+
*/
|
|
95
|
+
export function resolveTypeName(
|
|
96
|
+
namedTypes: Map<string, string>,
|
|
97
|
+
): (type: BoundType) => string | undefined {
|
|
98
|
+
return (type) => {
|
|
99
|
+
if (type.kind === "struct") return namedTypes.get(structKey(type));
|
|
100
|
+
if (type.kind === "union") return namedTypes.get(unionKey(type));
|
|
101
|
+
return undefined;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type { BindingId, GateAtom, ResolvedOutput } from "../bindings/index.js";
|
|
2
|
+
import { outputGate } from "../bindings/index.js";
|
|
3
|
+
import type { Documentation, Expr } from "../ir/index.js";
|
|
4
|
+
import type { CodegenContext } from "../manifest/index.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Language-agnostic collection of a tool's Outputs object: the set of output
|
|
8
|
+
* files it produces (resolved outputs + mutable inputs surfaced as outputs)
|
|
9
|
+
* plus its captured stdout/stderr streams. Both the Python and TypeScript
|
|
10
|
+
* backends, and the JSON Schema backend, consume this single source of truth so
|
|
11
|
+
* the three describe the same Outputs shape (previously this logic was
|
|
12
|
+
* near-duplicated between the two `outputs-emit.ts` files). Identifier
|
|
13
|
+
* sanitization is the one language-specific concern, so callers pass an `idOf`
|
|
14
|
+
* mapping a raw output name to their target language's field identifier; the
|
|
15
|
+
* dedup space is keyed by that id, matching each backend's own field set.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A `ResolvedOutput` plus a `mutable` marker. A mutable input file is surfaced
|
|
20
|
+
* as an output: its single ref token points at the input binding, and the
|
|
21
|
+
* backend emits a writable-copy call (`mutable_copy` / `mutableCopy`) - the
|
|
22
|
+
* host path of the copy the runner staged for the matching mutable input -
|
|
23
|
+
* instead of `output_file` / `outputFile`.
|
|
24
|
+
*/
|
|
25
|
+
export type EmittedOutput = ResolvedOutput & { mutable?: boolean };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The synthetic `root` output: the runner's output directory itself, surfaced as
|
|
29
|
+
* an always-present, non-gated `OutputPathType` field. Modeled as a regular
|
|
30
|
+
* `ResolvedOutput` with a single literal `"."` token so it flows through the
|
|
31
|
+
* exact same collection and emit machinery as every declared output - its empty
|
|
32
|
+
* gate makes it a required single, rendered as `output_file(".")` /
|
|
33
|
+
* `outputFile(".")`. Because every tool emits it, every tool returns a
|
|
34
|
+
* non-empty Outputs object (matching styx v1, which always carried `root`).
|
|
35
|
+
*/
|
|
36
|
+
export function rootOutput(ctx: CodegenContext, idOf: (name: string) => string): EmittedOutput {
|
|
37
|
+
// Reserve a non-colliding field id. If a tool genuinely declares an output (or
|
|
38
|
+
// a mutable input surfaced as an output) whose id sanitizes to "root", the
|
|
39
|
+
// synthetic output-dir field dodges with a trailing "_" so it never silently
|
|
40
|
+
// clobbers that real output (or flips it optional via shape merging).
|
|
41
|
+
const taken = new Set<string>();
|
|
42
|
+
for (const scope of ctx.outputScopes) for (const o of scope.outputs) taken.add(idOf(o.name));
|
|
43
|
+
for (const o of collectMutableOutputs(ctx)) taken.add(idOf(o.name));
|
|
44
|
+
let name = "root";
|
|
45
|
+
while (taken.has(idOf(name))) name += "_";
|
|
46
|
+
return { name, tokens: [{ kind: "literal", value: "." }] };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Field shape for a single resolved output.
|
|
51
|
+
*
|
|
52
|
+
* - `single`: emitted at most once. Optional iff any `present`/`variant` atom
|
|
53
|
+
* appears in the gate (the value may be absent under that gate).
|
|
54
|
+
* - `list`: emitted once per element of an iterated binding (any `iter` atom in
|
|
55
|
+
* the gate). A gated list still types as a list - the empty list stands for
|
|
56
|
+
* "nothing produced".
|
|
57
|
+
*/
|
|
58
|
+
export type OutputShape = { kind: "single"; optional: boolean } | { kind: "list" };
|
|
59
|
+
|
|
60
|
+
export function outputShape(gate: GateAtom[]): OutputShape {
|
|
61
|
+
const iter = gate.some((a) => a.kind === "iter");
|
|
62
|
+
if (iter) return { kind: "list" };
|
|
63
|
+
const optional = gate.some((a) => a.kind === "present" || a.kind === "variant");
|
|
64
|
+
return { kind: "single", optional };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Merge two shapes for outputs that share a field name across scopes/variants.
|
|
69
|
+
* Any iterated contributor makes the field a list; otherwise it is a single
|
|
70
|
+
* field that is optional if any contributor is gated.
|
|
71
|
+
*/
|
|
72
|
+
export function mergeShape(a: OutputShape, b: OutputShape): OutputShape {
|
|
73
|
+
if (a.kind === "list" || b.kind === "list") return { kind: "list" };
|
|
74
|
+
return { kind: "single", optional: a.optional || b.optional };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** One collected Outputs field, deduped across same-named outputs. */
|
|
78
|
+
export interface OutputField {
|
|
79
|
+
/** Backend-sanitized field identifier (via the caller's `idOf`). */
|
|
80
|
+
id: string;
|
|
81
|
+
/** First-seen raw output name (the descriptor's output id). */
|
|
82
|
+
name: string;
|
|
83
|
+
shape: OutputShape;
|
|
84
|
+
doc?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Collect the unique Outputs fields in first-seen order, merging the shape and
|
|
89
|
+
* doc of any outputs that resolve to the same field id. Multiple scopes (e.g.
|
|
90
|
+
* the arms of a union output) routinely declare the same output name; without
|
|
91
|
+
* deduping a backend would emit duplicate fields (a Python SyntaxError, a TS
|
|
92
|
+
* duplicate member). `idOf` maps a raw output name to the target language's
|
|
93
|
+
* sanitized identifier, so two raw names that collapse to the same identifier
|
|
94
|
+
* merge into one field - exactly the field set the backend will emit.
|
|
95
|
+
*/
|
|
96
|
+
export function collectOutputFields(
|
|
97
|
+
ctx: CodegenContext,
|
|
98
|
+
idOf: (name: string) => string,
|
|
99
|
+
): OutputField[] {
|
|
100
|
+
const byId = new Map<string, OutputField>();
|
|
101
|
+
const add = (output: EmittedOutput, scopeGate: GateAtom[]): void => {
|
|
102
|
+
const gate = outputGate(scopeGate, output, ctx.bindings);
|
|
103
|
+
const shape = outputShape(gate);
|
|
104
|
+
const id = idOf(output.name);
|
|
105
|
+
const doc = output.doc?.description ?? output.doc?.title;
|
|
106
|
+
const existing = byId.get(id);
|
|
107
|
+
if (existing) {
|
|
108
|
+
existing.shape = mergeShape(existing.shape, shape);
|
|
109
|
+
if (!existing.doc && doc) existing.doc = doc;
|
|
110
|
+
} else {
|
|
111
|
+
byId.set(id, { id, name: output.name, shape, doc });
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
// The output directory itself, always present and ungated, listed first.
|
|
115
|
+
add(rootOutput(ctx, idOf), []);
|
|
116
|
+
for (const scope of ctx.outputScopes) {
|
|
117
|
+
const scopeBinding = ctx.bindings.get(scope.scope);
|
|
118
|
+
const scopeGate = scopeBinding?.gate ?? [];
|
|
119
|
+
for (const output of scope.outputs) add(output, scopeGate);
|
|
120
|
+
}
|
|
121
|
+
// Mutable inputs surface as outputs; their binding gate is absolute (rooted),
|
|
122
|
+
// so the scope gate is empty.
|
|
123
|
+
for (const output of collectMutableOutputs(ctx)) add(output, []);
|
|
124
|
+
return [...byId.values()];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Has any scope in the context attached at least one output? */
|
|
128
|
+
export function hasAnyOutputs(ctx: CodegenContext): boolean {
|
|
129
|
+
return ctx.outputScopes.some((s) => s.outputs.length > 0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** A captured stream (stdout/stderr), surfaced as a list-of-lines Outputs field. */
|
|
133
|
+
export interface StreamField {
|
|
134
|
+
/** First-seen raw stream name, bumped with a trailing `_` to dodge collisions. */
|
|
135
|
+
name: string;
|
|
136
|
+
/** Backend-sanitized identifier for `name` (via the caller's `idOf`). */
|
|
137
|
+
id: string;
|
|
138
|
+
doc?: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The stdout/stderr fields declared by the app metadata, in declaration order
|
|
143
|
+
* (stdout before stderr). Stream outputs are app-level: never gated (always
|
|
144
|
+
* present when the tool runs), so they bypass the solver/gating machinery and
|
|
145
|
+
* surface as plain list-of-string fields the wrapper appends to via the
|
|
146
|
+
* `handle_stdout` / `handle_stderr` (Python) or `handleStdout` / `handleStderr`
|
|
147
|
+
* (TS) callbacks. A stream whose sanitized id collides with a real output's id
|
|
148
|
+
* is bumped (raw name gains a trailing `_`, re-sanitized) so it never shadows a
|
|
149
|
+
* file output / emits a duplicate field.
|
|
150
|
+
*/
|
|
151
|
+
export function streamFields(ctx: CodegenContext, idOf: (name: string) => string): StreamField[] {
|
|
152
|
+
const out: StreamField[] = [];
|
|
153
|
+
// Seed with the file/mutable output field ids so a stream whose name collides
|
|
154
|
+
// with a real output (e.g. an output literally named "stdout") is bumped
|
|
155
|
+
// rather than emitting a duplicate field / repeated constructor argument.
|
|
156
|
+
const used = new Set<string>(collectOutputFields(ctx, idOf).map((f) => f.id));
|
|
157
|
+
const add = (rawName: string, doc?: string): void => {
|
|
158
|
+
let name = rawName;
|
|
159
|
+
while (used.has(idOf(name))) name += "_";
|
|
160
|
+
used.add(idOf(name));
|
|
161
|
+
out.push({ name, id: idOf(name), doc });
|
|
162
|
+
};
|
|
163
|
+
const so = ctx.app?.stdout;
|
|
164
|
+
const se = ctx.app?.stderr;
|
|
165
|
+
if (so) add(so.name, so.doc?.description ?? so.doc?.title);
|
|
166
|
+
if (se) add(se.name, se.doc?.description ?? se.doc?.title);
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Does the app declare any stdout/stderr stream output? */
|
|
171
|
+
export function hasStreamOutputs(ctx: CodegenContext): boolean {
|
|
172
|
+
return !!(ctx.app?.stdout || ctx.app?.stderr);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Synthesize one output per mutable file input. Each is a `ResolvedOutput` with
|
|
177
|
+
* a single ref token to the input binding and the `mutable` marker. The input
|
|
178
|
+
* binding's solver-assigned gate fully encodes its ancestry (optional/variant/
|
|
179
|
+
* iterated), so `outputGate([], ...)` yields the correct shape and gating for
|
|
180
|
+
* free - no scope bucket needed.
|
|
181
|
+
*/
|
|
182
|
+
export function collectMutableOutputs(ctx: CodegenContext): EmittedOutput[] {
|
|
183
|
+
const out: EmittedOutput[] = [];
|
|
184
|
+
const seen = new Set<BindingId>();
|
|
185
|
+
const walk = (node: Expr, inheritedDoc?: Documentation): void => {
|
|
186
|
+
if (node.kind === "path") {
|
|
187
|
+
if (node.attrs.mutable) {
|
|
188
|
+
const binding = ctx.resolve(node);
|
|
189
|
+
if (binding && !seen.has(binding.id)) {
|
|
190
|
+
seen.add(binding.id);
|
|
191
|
+
const doc = node.meta?.doc ?? inheritedDoc;
|
|
192
|
+
out.push({
|
|
193
|
+
name: binding.name,
|
|
194
|
+
tokens: [{ kind: "ref", binding: binding.id }],
|
|
195
|
+
...(doc && { doc }),
|
|
196
|
+
mutable: true,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
switch (node.kind) {
|
|
203
|
+
case "sequence":
|
|
204
|
+
for (const child of node.attrs.nodes) walk(child);
|
|
205
|
+
break;
|
|
206
|
+
case "optional":
|
|
207
|
+
case "repeat":
|
|
208
|
+
walk(node.attrs.node, node.meta?.doc ?? inheritedDoc);
|
|
209
|
+
break;
|
|
210
|
+
case "alternative":
|
|
211
|
+
for (const alt of node.attrs.alts) walk(alt, node.meta?.doc ?? inheritedDoc);
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
walk(ctx.expr);
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Does the tool have any mutable file input (surfaced as an output)? */
|
|
220
|
+
export function hasMutableInputs(ctx: CodegenContext): boolean {
|
|
221
|
+
return collectMutableOutputs(ctx).length > 0;
|
|
222
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { BoundType } from "../bindings/index.js";
|
|
2
|
+
import type { Expr } from "../ir/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Find a description from an IR node, traversing through wrapper nodes.
|
|
6
|
+
*
|
|
7
|
+
* The parser's `wrapNode` hoists doc metadata to the outermost wrapper node,
|
|
8
|
+
* but the solver's simplify pass can collapse sequences, burying descriptions
|
|
9
|
+
* deeper in the tree.
|
|
10
|
+
*
|
|
11
|
+
* This traversal is type-aware: it only enters sequences when the corresponding
|
|
12
|
+
* BoundType is not a struct. Struct sequences have their own field collection
|
|
13
|
+
* call, so entering them would steal nested struct children's descriptions.
|
|
14
|
+
*
|
|
15
|
+
* @param node - The IR node to search for a description.
|
|
16
|
+
* @param fieldType - The BoundType of the field, used to determine traversal boundaries.
|
|
17
|
+
*/
|
|
18
|
+
export function findDoc(node: Expr, fieldType: BoundType): string | undefined {
|
|
19
|
+
if (node.meta?.doc?.description) return node.meta.doc.description;
|
|
20
|
+
switch (node.kind) {
|
|
21
|
+
case "optional":
|
|
22
|
+
return findDoc(node.attrs.node, fieldType.kind === "optional" ? fieldType.inner : fieldType);
|
|
23
|
+
case "repeat":
|
|
24
|
+
return findDoc(node.attrs.node, fieldType.kind === "list" ? fieldType.item : fieldType);
|
|
25
|
+
case "sequence": {
|
|
26
|
+
// Only traverse into sequences that were collapsed (non-struct field types).
|
|
27
|
+
// Struct sequences have their own collectFieldInfo call for their children.
|
|
28
|
+
if (fieldType.kind === "struct") return undefined;
|
|
29
|
+
for (const child of node.attrs.nodes) {
|
|
30
|
+
const doc = findDoc(child, fieldType);
|
|
31
|
+
if (doc) return doc;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
default:
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { BoundType } from "../bindings/index.js";
|
|
2
|
+
import type { Expr } from "../ir/index.js";
|
|
3
|
+
import type { CodegenContext } from "../manifest/index.js";
|
|
4
|
+
import { resolveFieldBinding } from "./resolve-field-binding.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Find the sequence node whose child bindings match a struct type's fields.
|
|
8
|
+
*
|
|
9
|
+
* Traverses through optional, repeat, and alternative wrappers to find the
|
|
10
|
+
* sequence that directly contains the struct's field bindings. This is necessary
|
|
11
|
+
* because the solver may collapse `seq(lit("--flag"), terminal)` into the terminal,
|
|
12
|
+
* burying bindings deeper in the tree.
|
|
13
|
+
*
|
|
14
|
+
* Uses a two-phase check for sequences:
|
|
15
|
+
* 1. Direct binding check (`ctx.resolve`) - matches when bindings are on immediate children
|
|
16
|
+
* 2. Recursive binding check (`resolveFieldBinding`) - matches when solver collapsed
|
|
17
|
+
* a seq(lit, terminal) and the binding is buried deeper
|
|
18
|
+
*
|
|
19
|
+
* Phase 1 is tried first to avoid falsely matching an outer sequence when an inner
|
|
20
|
+
* sequence is the actual struct owner (e.g. `seq(lit("--flag"), seq(field1, field2))`).
|
|
21
|
+
*/
|
|
22
|
+
export function findStructNode(
|
|
23
|
+
node: Expr,
|
|
24
|
+
ctx: CodegenContext,
|
|
25
|
+
structType: Extract<BoundType, { kind: "struct" }>,
|
|
26
|
+
): Extract<Expr, { kind: "sequence" }> | undefined {
|
|
27
|
+
switch (node.kind) {
|
|
28
|
+
case "sequence": {
|
|
29
|
+
// Phase 1: Check if any direct child has a binding matching a struct field
|
|
30
|
+
for (const child of node.attrs.nodes) {
|
|
31
|
+
const binding = ctx.resolve(child);
|
|
32
|
+
if (
|
|
33
|
+
binding &&
|
|
34
|
+
binding.name in structType.fields &&
|
|
35
|
+
binding.type === structType.fields[binding.name]
|
|
36
|
+
) {
|
|
37
|
+
return node;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Recurse into child nodes first (prefer deeper matches)
|
|
41
|
+
for (const child of node.attrs.nodes) {
|
|
42
|
+
const result = findStructNode(child, ctx, structType);
|
|
43
|
+
if (result) return result;
|
|
44
|
+
}
|
|
45
|
+
// Phase 2: Check via resolveFieldBinding for collapsed sequences
|
|
46
|
+
// where bindings are buried inside collapsed seq(lit, terminal)
|
|
47
|
+
for (const child of node.attrs.nodes) {
|
|
48
|
+
if (resolveFieldBinding(child, ctx, structType)) return node;
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
case "optional":
|
|
53
|
+
return findStructNode(node.attrs.node, ctx, structType);
|
|
54
|
+
case "repeat":
|
|
55
|
+
return findStructNode(node.attrs.node, ctx, structType);
|
|
56
|
+
case "alternative": {
|
|
57
|
+
for (const alt of node.attrs.alts) {
|
|
58
|
+
const result = findStructNode(alt, ctx, structType);
|
|
59
|
+
if (result) return result;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
default:
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
Backend,
|
|
3
|
+
EmitError,
|
|
4
|
+
EmitResult,
|
|
5
|
+
EmittedApp,
|
|
6
|
+
EmittedPackage,
|
|
7
|
+
EmitWarning,
|
|
8
|
+
TypeMap,
|
|
9
|
+
} from "./backend.js";
|
|
10
|
+
export { BoutiquesBackend, generateBoutiques } from "./boutiques/index.js";
|
|
11
|
+
export { CodeBuilder } from "./code-builder.js";
|
|
12
|
+
export type { FieldInfo } from "./collect-field-info.js";
|
|
13
|
+
export { collectFieldInfo } from "./collect-field-info.js";
|
|
14
|
+
export type { NamedType } from "./collect-named-types.js";
|
|
15
|
+
export { collectNamedTypes, resolveTypeName } from "./collect-named-types.js";
|
|
16
|
+
export { findDoc } from "./find-doc.js";
|
|
17
|
+
export { findStructNode } from "./find-struct-node.js";
|
|
18
|
+
export { resolveFieldBinding } from "./resolve-field-binding.js";
|
|
19
|
+
export type { OutputEmitPlan } from "./resolve-output-tokens.js";
|
|
20
|
+
export {
|
|
21
|
+
compactTokens,
|
|
22
|
+
isGated,
|
|
23
|
+
isIterated,
|
|
24
|
+
outputGate,
|
|
25
|
+
planOutput,
|
|
26
|
+
planScope,
|
|
27
|
+
} from "./resolve-output-tokens.js";
|
|
28
|
+
export { generatePython, PythonBackend, renderPythonCall } from "./python/index.js";
|
|
29
|
+
export { Scope } from "./scope.js";
|
|
30
|
+
export type { JsonSchema } from "./schema/index.js";
|
|
31
|
+
export { generateOutputsSchema, generateSchema, JsonSchemaBackend } from "./schema/index.js";
|
|
32
|
+
export { buildSigEntries } from "./sig-entries.js";
|
|
33
|
+
export type { SigEntry, SigOptions } from "./sig-entries.js";
|
|
34
|
+
export { renderStructLiteral, renderValue } from "./snippet-core.js";
|
|
35
|
+
export type { SnippetDialect, SnippetOptions } from "./snippet-core.js";
|
|
36
|
+
export { camelCase, pascalCase, screamingSnakeCase, snakeCase } from "./string-case.js";
|
|
37
|
+
export { PYTHON_RUNNER_DEPS, STYXDEFS_COMPAT } from "./styxdefs-compat.js";
|
|
38
|
+
export { structKey, typeKey, unionKey } from "./type-keys.js";
|
|
39
|
+
export { generateTypeScript, renderTypeScriptCall, TypeScriptBackend } from "./typescript/index.js";
|