@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,76 @@
|
|
|
1
|
+
import type { Expr } from "../node.js";
|
|
2
|
+
import { PassStatus, type Pass, type PassResult } from "./pass.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Remove empty nodes:
|
|
6
|
+
* - seq() → removed
|
|
7
|
+
* - alt() → removed
|
|
8
|
+
* - opt(seq()) → removed
|
|
9
|
+
* - rep(alt()) → removed
|
|
10
|
+
*/
|
|
11
|
+
export const removeEmpty: Pass = {
|
|
12
|
+
name: "remove-empty",
|
|
13
|
+
apply(expr: Expr): PassResult {
|
|
14
|
+
let changed = false;
|
|
15
|
+
|
|
16
|
+
function isEmpty(node: Expr): boolean {
|
|
17
|
+
return (
|
|
18
|
+
(node.kind === "sequence" && node.attrs.nodes.length === 0) ||
|
|
19
|
+
(node.kind === "alternative" && node.attrs.alts.length === 0)
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isRemovable(node: Expr): boolean {
|
|
24
|
+
if (node.meta) return false; // Preserve metadata
|
|
25
|
+
return (
|
|
26
|
+
isEmpty(node) ||
|
|
27
|
+
((node.kind === "optional" || node.kind === "repeat") && isEmpty(node.attrs.node))
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function visit(node: Expr): Expr {
|
|
32
|
+
switch (node.kind) {
|
|
33
|
+
case "sequence": {
|
|
34
|
+
const nodes = node.attrs.nodes.map(visit).filter((child) => {
|
|
35
|
+
const removable = isRemovable(child);
|
|
36
|
+
if (removable) changed = true;
|
|
37
|
+
return !removable;
|
|
38
|
+
});
|
|
39
|
+
return { ...node, attrs: { ...node.attrs, nodes } };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
case "alternative": {
|
|
43
|
+
const alts = node.attrs.alts.map(visit).filter((child) => {
|
|
44
|
+
const removable = isRemovable(child);
|
|
45
|
+
if (removable) changed = true;
|
|
46
|
+
return !removable;
|
|
47
|
+
});
|
|
48
|
+
return { ...node, attrs: { ...node.attrs, alts } };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
case "optional":
|
|
52
|
+
return { ...node, attrs: { node: visit(node.attrs.node) } };
|
|
53
|
+
|
|
54
|
+
case "repeat":
|
|
55
|
+
return { ...node, attrs: { ...node.attrs, node: visit(node.attrs.node) } };
|
|
56
|
+
|
|
57
|
+
case "literal":
|
|
58
|
+
case "int":
|
|
59
|
+
case "float":
|
|
60
|
+
case "str":
|
|
61
|
+
case "path":
|
|
62
|
+
return node;
|
|
63
|
+
|
|
64
|
+
default: {
|
|
65
|
+
const _exhaustive: never = node;
|
|
66
|
+
return node;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
expr: visit(expr),
|
|
73
|
+
status: changed ? PassStatus.Changed : PassStatus.Unchanged,
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { NodeMeta } from "../meta.js";
|
|
2
|
+
import type { Expr } from "../node.js";
|
|
3
|
+
import { PassStatus, type Pass, type PassResult } from "./pass.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Merge two `NodeMeta` when collapsing a wrapper layer (the child is the
|
|
7
|
+
* inner/surviving node). Name, doc fields and defaultValue take the innermost
|
|
8
|
+
* value; `outputs` from both layers are concatenated so an output attached to
|
|
9
|
+
* a collapsed wrapper survives on whatever node absorbs it.
|
|
10
|
+
*/
|
|
11
|
+
function mergeMeta(parent?: NodeMeta, child?: NodeMeta): NodeMeta | undefined {
|
|
12
|
+
if (!parent && !child) return undefined;
|
|
13
|
+
if (!parent) return child;
|
|
14
|
+
if (!child) return parent;
|
|
15
|
+
|
|
16
|
+
const merged: NodeMeta = {};
|
|
17
|
+
|
|
18
|
+
const name = child.name ?? parent.name;
|
|
19
|
+
if (name !== undefined) merged.name = name;
|
|
20
|
+
|
|
21
|
+
// The variant tag is the union discriminator (the sub-command id); unlike
|
|
22
|
+
// `name` it must survive a single-field sub-command collapsing onto its inner
|
|
23
|
+
// field (whose `name` wins above), so the `@type` stays the sub-command id
|
|
24
|
+
// rather than the inner field's id. The parent (the sub-command wrapper)
|
|
25
|
+
// carries it; the inner field has none.
|
|
26
|
+
const variantTag = parent.variantTag ?? child.variantTag;
|
|
27
|
+
if (variantTag !== undefined) merged.variantTag = variantTag;
|
|
28
|
+
|
|
29
|
+
const defaultValue = child.defaultValue ?? parent.defaultValue;
|
|
30
|
+
if (defaultValue !== undefined) merged.defaultValue = defaultValue;
|
|
31
|
+
|
|
32
|
+
if (parent.doc || child.doc) {
|
|
33
|
+
merged.doc = {
|
|
34
|
+
title: child.doc?.title ?? parent.doc?.title,
|
|
35
|
+
description: child.doc?.description ?? parent.doc?.description,
|
|
36
|
+
authors: [...(parent.doc?.authors ?? []), ...(child.doc?.authors ?? [])],
|
|
37
|
+
literature: [...(parent.doc?.literature ?? []), ...(child.doc?.literature ?? [])],
|
|
38
|
+
urls: [...(parent.doc?.urls ?? []), ...(child.doc?.urls ?? [])],
|
|
39
|
+
comment: child.doc?.comment ?? parent.doc?.comment,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const outputs = [...(parent.outputs ?? []), ...(child.outputs ?? [])];
|
|
44
|
+
if (outputs.length > 0) merged.outputs = outputs;
|
|
45
|
+
|
|
46
|
+
return merged;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Simplify passes:
|
|
51
|
+
* - optional(optional(T)) -> optional(T)
|
|
52
|
+
* - repeat(repeat(T)) -> repeat(T) with merged constraints
|
|
53
|
+
* - seq(T) -> T, alt(T) -> T (singleton unwrapping)
|
|
54
|
+
* - seq(lit("a"), lit("b")) -> seq(lit("ab")) (merge consecutive literals)
|
|
55
|
+
*
|
|
56
|
+
* Every collapse that drops a wrapper layer merges that layer's `NodeMeta`
|
|
57
|
+
* into the surviving node via `mergeMeta`, so names and attached outputs are
|
|
58
|
+
* never lost.
|
|
59
|
+
*/
|
|
60
|
+
export const simplify: Pass = {
|
|
61
|
+
name: "simplify",
|
|
62
|
+
apply(expr: Expr): PassResult {
|
|
63
|
+
let changed = false;
|
|
64
|
+
|
|
65
|
+
function visit(node: Expr): Expr {
|
|
66
|
+
switch (node.kind) {
|
|
67
|
+
case "optional": {
|
|
68
|
+
const inner = visit(node.attrs.node);
|
|
69
|
+
|
|
70
|
+
// optional(optional(T)) -> optional(T)
|
|
71
|
+
if (inner.kind === "optional") {
|
|
72
|
+
changed = true;
|
|
73
|
+
return {
|
|
74
|
+
...node,
|
|
75
|
+
attrs: { node: inner.attrs.node },
|
|
76
|
+
meta: mergeMeta(node.meta, inner.meta),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { ...node, attrs: { node: inner } };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
case "repeat": {
|
|
84
|
+
const inner = visit(node.attrs.node);
|
|
85
|
+
|
|
86
|
+
// repeat(repeat(T)) -> repeat(T) with merged constraints
|
|
87
|
+
if (inner.kind === "repeat") {
|
|
88
|
+
changed = true;
|
|
89
|
+
return {
|
|
90
|
+
...node,
|
|
91
|
+
attrs: {
|
|
92
|
+
node: inner.attrs.node,
|
|
93
|
+
join: node.attrs.join ?? inner.attrs.join,
|
|
94
|
+
countMin: Math.max(node.attrs.countMin ?? 0, inner.attrs.countMin ?? 0),
|
|
95
|
+
countMax:
|
|
96
|
+
node.attrs.countMax === undefined || inner.attrs.countMax === undefined
|
|
97
|
+
? undefined
|
|
98
|
+
: Math.min(node.attrs.countMax, inner.attrs.countMax),
|
|
99
|
+
},
|
|
100
|
+
meta: mergeMeta(node.meta, inner.meta),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { ...node, attrs: { ...node.attrs, node: inner } };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case "sequence": {
|
|
108
|
+
const children = node.attrs.nodes.map(visit);
|
|
109
|
+
|
|
110
|
+
// Merge consecutive metadata-less literals, but only in an explicit
|
|
111
|
+
// concatenation context (`join === ""`). The merge is separator-free
|
|
112
|
+
// (`prev.str += child.str`), which only matches the semantics of an
|
|
113
|
+
// empty join. A sequence with no join joins its children with a space
|
|
114
|
+
// (they become separate argv tokens at the top level), so merging
|
|
115
|
+
// adjacent literals there would wrongly fuse two arguments into one
|
|
116
|
+
// (e.g. `seq(lit("wb_command"), lit("-foo"))` -> `"wb_command-foo"`).
|
|
117
|
+
// Inside a join context the backend concatenates anyway, so leaving
|
|
118
|
+
// such literals unmerged stays correct (just one node larger).
|
|
119
|
+
const nodes: Expr[] = [];
|
|
120
|
+
for (const child of children) {
|
|
121
|
+
const prev = nodes[nodes.length - 1];
|
|
122
|
+
if (
|
|
123
|
+
prev?.kind === "literal" &&
|
|
124
|
+
child.kind === "literal" &&
|
|
125
|
+
!prev.meta &&
|
|
126
|
+
!child.meta &&
|
|
127
|
+
node.attrs.join === ""
|
|
128
|
+
) {
|
|
129
|
+
changed = true;
|
|
130
|
+
prev.attrs.str += child.attrs.str;
|
|
131
|
+
} else {
|
|
132
|
+
nodes.push(child);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// seq(T) -> T, carrying the seq's meta down onto the child.
|
|
137
|
+
if (nodes.length === 1) {
|
|
138
|
+
const child = nodes[0]!;
|
|
139
|
+
changed = true;
|
|
140
|
+
const mergedMeta = mergeMeta(node.meta, child.meta);
|
|
141
|
+
return mergedMeta ? { ...child, meta: mergedMeta } : child;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { ...node, attrs: { ...node.attrs, nodes } };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
case "alternative": {
|
|
148
|
+
const alts = node.attrs.alts.map(visit);
|
|
149
|
+
|
|
150
|
+
// alt(T) -> T. Only when the alt carries no metadata of its own
|
|
151
|
+
// (otherwise the collapse would orphan it).
|
|
152
|
+
if (alts.length === 1 && !node.meta) {
|
|
153
|
+
changed = true;
|
|
154
|
+
return alts[0]!;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { ...node, attrs: { ...node.attrs, alts } };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
case "literal":
|
|
161
|
+
case "int":
|
|
162
|
+
case "float":
|
|
163
|
+
case "str":
|
|
164
|
+
case "path":
|
|
165
|
+
return node;
|
|
166
|
+
|
|
167
|
+
default: {
|
|
168
|
+
const _exhaustive: never = node;
|
|
169
|
+
return node;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
expr: visit(expr),
|
|
176
|
+
status: changed ? PassStatus.Changed : PassStatus.Unchanged,
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
};
|
package/src/ir/types.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Scalar type kinds for terminal nodes */
|
|
2
|
+
export type ScalarKind = "int" | "float" | "str" | "path";
|
|
3
|
+
|
|
4
|
+
/** Media type identifier (MIME type) */
|
|
5
|
+
export type MediaTypeIdentifier = string;
|
|
6
|
+
|
|
7
|
+
/** Human-facing documentation metadata */
|
|
8
|
+
export interface Documentation {
|
|
9
|
+
title?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
authors?: string[];
|
|
12
|
+
literature?: string[];
|
|
13
|
+
urls?: string[];
|
|
14
|
+
comment?: string;
|
|
15
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BindingRegistry,
|
|
3
|
+
OutputScope,
|
|
4
|
+
OutputValidationResult,
|
|
5
|
+
SolveResult,
|
|
6
|
+
} from "../bindings/index.js";
|
|
7
|
+
import type { AppMeta, Expr } from "../ir/index.js";
|
|
8
|
+
import type { OutputResolution } from "../solver/index.js";
|
|
9
|
+
import type { PackageMeta, ProjectMeta } from "./types.js";
|
|
10
|
+
|
|
11
|
+
export interface CodegenContext {
|
|
12
|
+
expr: Expr;
|
|
13
|
+
bindings: BindingRegistry;
|
|
14
|
+
resolve: SolveResult["resolve"];
|
|
15
|
+
outputScopes: OutputScope[];
|
|
16
|
+
outputDiagnostics: OutputValidationResult;
|
|
17
|
+
app?: AppMeta;
|
|
18
|
+
package?: PackageMeta;
|
|
19
|
+
project?: ProjectMeta;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createContext(
|
|
23
|
+
expr: Expr,
|
|
24
|
+
solveResult: SolveResult,
|
|
25
|
+
outputs: OutputResolution,
|
|
26
|
+
meta?: { app?: AppMeta; package?: PackageMeta; project?: ProjectMeta },
|
|
27
|
+
): CodegenContext {
|
|
28
|
+
return {
|
|
29
|
+
expr,
|
|
30
|
+
bindings: solveResult.bindings,
|
|
31
|
+
resolve: solveResult.resolve,
|
|
32
|
+
outputScopes: outputs.scopes,
|
|
33
|
+
outputDiagnostics: outputs.diagnostics,
|
|
34
|
+
...meta,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Documentation } from "../ir/index.js";
|
|
2
|
+
|
|
3
|
+
export interface ProjectMeta {
|
|
4
|
+
name?: string;
|
|
5
|
+
version?: string;
|
|
6
|
+
doc?: Documentation;
|
|
7
|
+
license?: Documentation;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PackageMeta {
|
|
11
|
+
name?: string;
|
|
12
|
+
version?: string;
|
|
13
|
+
docker?: string;
|
|
14
|
+
doc?: Documentation;
|
|
15
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import type { AccessPath, AccessSegment, Binding, BoundType } from "../bindings/index.js";
|
|
2
|
+
import type { Expr } from "../ir/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Attach `Binding.access` to every binding by walking the IR once, threading
|
|
6
|
+
* the same scope state the backend walkers used to re-derive independently
|
|
7
|
+
* (`arg-builder.walk` and `outputs-emit.walkAccess`). Producing the paths here,
|
|
8
|
+
* after types settle, collapses both walkers into pure `renderAccess` lookups
|
|
9
|
+
* and removes the drift class between them.
|
|
10
|
+
*
|
|
11
|
+
* The walk faithfully mirrors the (post-`ca61dc8`) arg-builder/outputs-emit
|
|
12
|
+
* scope handling for sequence/optional/alternative, and adopts the
|
|
13
|
+
* arg-builder's `repeat` recursion (which `walkAccess` lacked) so bindings
|
|
14
|
+
* inside a `repeat`-of-list get an `iter`-rooted path instead of being absent.
|
|
15
|
+
*/
|
|
16
|
+
export function assignAccessPaths(expr: Expr, resolve: (node: Expr) => Binding | undefined): void {
|
|
17
|
+
const rootBinding = resolve(expr);
|
|
18
|
+
walk(expr, resolve, { path: [], currentStructType: rootBinding?.type });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Scope state threaded down the walk, mirroring the arg-builder's `ArgContext`
|
|
23
|
+
* but carrying structured paths instead of rendered strings.
|
|
24
|
+
*/
|
|
25
|
+
interface AccessCtx {
|
|
26
|
+
/** Access path of the enclosing struct scope (the base for child fields). */
|
|
27
|
+
path: AccessPath;
|
|
28
|
+
/**
|
|
29
|
+
* When set, the next binding's value lives at this exact path rather than at
|
|
30
|
+
* `path + field(name)` - the wrapper collapsed the inner value onto its own
|
|
31
|
+
* path (`optional<scalar>`/`optional<bool>`, scalar lists). Mirrors the
|
|
32
|
+
* arg-builder's `directValue`. Not a rendered segment: it is "inherit the
|
|
33
|
+
* parent's path, append nothing".
|
|
34
|
+
*/
|
|
35
|
+
directPath?: AccessPath;
|
|
36
|
+
/** The struct type at the current scope level (prevents double-scoping). */
|
|
37
|
+
currentStructType?: BoundType;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function field(name: string): AccessSegment {
|
|
41
|
+
return { kind: "field", name };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function iter(binding: string): AccessSegment {
|
|
45
|
+
return { kind: "iter", binding };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The path at which a binding's own value lives in the current scope. */
|
|
49
|
+
function ownAccess(arg: AccessCtx, name: string): AccessPath {
|
|
50
|
+
return arg.directPath ?? [...arg.path, field(name)];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Whether a BoundType contains a struct that requires scoping when entered. */
|
|
54
|
+
function hasStructScope(type: BoundType): boolean {
|
|
55
|
+
switch (type.kind) {
|
|
56
|
+
case "optional":
|
|
57
|
+
return hasStructScope(type.inner);
|
|
58
|
+
case "list":
|
|
59
|
+
return hasStructScope(type.item);
|
|
60
|
+
case "struct":
|
|
61
|
+
return true;
|
|
62
|
+
default:
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Unwrap optional/list to find the inner struct type, if any. */
|
|
68
|
+
function unwrapToStruct(type: BoundType): Extract<BoundType, { kind: "struct" }> | undefined {
|
|
69
|
+
switch (type.kind) {
|
|
70
|
+
case "optional":
|
|
71
|
+
return unwrapToStruct(type.inner);
|
|
72
|
+
case "list":
|
|
73
|
+
return unwrapToStruct(type.item);
|
|
74
|
+
case "struct":
|
|
75
|
+
return type;
|
|
76
|
+
default:
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function walk(node: Expr, resolve: (node: Expr) => Binding | undefined, arg: AccessCtx): void {
|
|
82
|
+
const binding = resolve(node);
|
|
83
|
+
|
|
84
|
+
switch (node.kind) {
|
|
85
|
+
case "literal":
|
|
86
|
+
return;
|
|
87
|
+
|
|
88
|
+
case "int":
|
|
89
|
+
case "float":
|
|
90
|
+
case "str":
|
|
91
|
+
case "path": {
|
|
92
|
+
if (binding) binding.access = ownAccess(arg, binding.name);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
case "sequence": {
|
|
97
|
+
let childArg = arg;
|
|
98
|
+
if (binding && hasStructScope(binding.type) && binding.type !== arg.currentStructType) {
|
|
99
|
+
// A nested struct: children access fields under this binding's path.
|
|
100
|
+
const access: AccessPath = [...arg.path, field(binding.name)];
|
|
101
|
+
binding.access = access;
|
|
102
|
+
childArg = {
|
|
103
|
+
path: access,
|
|
104
|
+
currentStructType: unwrapToStruct(binding.type) ?? arg.currentStructType,
|
|
105
|
+
};
|
|
106
|
+
} else if (binding) {
|
|
107
|
+
// Root struct or a struct already scoped by an enclosing wrapper: the
|
|
108
|
+
// binding reuses the current path (collapse / already-scoped).
|
|
109
|
+
binding.access = arg.directPath ?? arg.path;
|
|
110
|
+
}
|
|
111
|
+
for (const child of node.attrs.nodes) walk(child, resolve, childArg);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
case "optional": {
|
|
116
|
+
if (!binding) {
|
|
117
|
+
walk(node.attrs.node, resolve, arg);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const access: AccessPath = [...arg.path, field(binding.name)];
|
|
121
|
+
binding.access = access;
|
|
122
|
+
// The inner node, after unwrapping the optional from the binding type.
|
|
123
|
+
const innerType = binding.type.kind === "optional" ? binding.type.inner : binding.type;
|
|
124
|
+
let childArg: AccessCtx;
|
|
125
|
+
if (binding.type === arg.currentStructType) {
|
|
126
|
+
// This optional IS the boxed single-field union variant: a collapsed
|
|
127
|
+
// single-input arm whose type the solver retyped to the variant's
|
|
128
|
+
// struct (so `binding.type === currentStructType`). It maps to the
|
|
129
|
+
// variant's lone field, so the inner value collapses onto this
|
|
130
|
+
// optional's own field access. Without this, the struct-scope branch
|
|
131
|
+
// below would add a spurious extra segment named after the inner IR
|
|
132
|
+
// node (e.g. `params.opts.exponential_options.smoothing_standard_deviation`)
|
|
133
|
+
// instead of the boxed field (`params.opts.exponential_options`). The
|
|
134
|
+
// sequence case guards the same way via its `!== currentStructType` test.
|
|
135
|
+
childArg = { ...arg, directPath: access };
|
|
136
|
+
} else if (innerType.kind === "list") {
|
|
137
|
+
// optional<list>: the inner node is a `repeat` that represents this same
|
|
138
|
+
// value and assigns its own (iter-based) child scope. It must reuse this
|
|
139
|
+
// optional's path rather than append its field again - otherwise a
|
|
140
|
+
// struct-list field whose name matches the optional's collides into a
|
|
141
|
+
// doubled path (`params.config.config`). `directPath` = "inherit this
|
|
142
|
+
// path, append nothing". Scalar lists already reached here via the
|
|
143
|
+
// optional/bool branch below; struct lists previously fell into the
|
|
144
|
+
// `hasStructScope` branch and got the doubled path.
|
|
145
|
+
childArg = { ...arg, directPath: access };
|
|
146
|
+
} else if (hasStructScope(binding.type)) {
|
|
147
|
+
childArg = {
|
|
148
|
+
path: access,
|
|
149
|
+
currentStructType: unwrapToStruct(binding.type) ?? arg.currentStructType,
|
|
150
|
+
};
|
|
151
|
+
} else if (binding.type.kind === "optional" || binding.type.kind === "bool") {
|
|
152
|
+
// Collapsed non-struct: the inner value lives at the optional's path.
|
|
153
|
+
childArg = { ...arg, directPath: access };
|
|
154
|
+
} else {
|
|
155
|
+
childArg = arg;
|
|
156
|
+
}
|
|
157
|
+
walk(node.attrs.node, resolve, childArg);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
case "repeat": {
|
|
162
|
+
// The solver always binds repeat nodes, so this is defensive only; unlike
|
|
163
|
+
// optional/alternative there are no children to recurse into without it.
|
|
164
|
+
if (!binding) return;
|
|
165
|
+
// The list/count binding lives at its own access path.
|
|
166
|
+
binding.access = ownAccess(arg, binding.name);
|
|
167
|
+
|
|
168
|
+
if (binding.type.kind === "count") {
|
|
169
|
+
// Count repeats wrap a literal (no inner binding); recurse harmlessly
|
|
170
|
+
// with the scope unchanged, mirroring the arg-builder.
|
|
171
|
+
walk(node.attrs.node, resolve, arg);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// List repeat: inner bindings are iteration-scoped. Reset the base to an
|
|
176
|
+
// `iter` segment bound to this repeat; the renderer substitutes the loop
|
|
177
|
+
// variable. Scalar lists collapse the element onto the loop var directly
|
|
178
|
+
// (directPath), struct lists scope into the element type.
|
|
179
|
+
const itemType = binding.type.kind === "list" ? binding.type.item : undefined;
|
|
180
|
+
const isScalar = !itemType || !hasStructScope(itemType);
|
|
181
|
+
const elementPath: AccessPath = [iter(binding.id)];
|
|
182
|
+
const childArg: AccessCtx = {
|
|
183
|
+
path: elementPath,
|
|
184
|
+
directPath: isScalar ? elementPath : undefined,
|
|
185
|
+
currentStructType:
|
|
186
|
+
!isScalar && itemType?.kind === "struct" ? itemType : arg.currentStructType,
|
|
187
|
+
};
|
|
188
|
+
walk(node.attrs.node, resolve, childArg);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
case "alternative": {
|
|
193
|
+
if (!binding) {
|
|
194
|
+
for (const alt of node.attrs.alts) walk(alt, resolve, arg);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const access = ownAccess(arg, binding.name);
|
|
198
|
+
binding.access = access;
|
|
199
|
+
const isComplexUnion =
|
|
200
|
+
binding.type.kind === "union" &&
|
|
201
|
+
!binding.type.variants.every((v) => v.type.kind === "literal");
|
|
202
|
+
node.attrs.alts.forEach((alt, i) => {
|
|
203
|
+
if (isComplexUnion && binding.type.kind === "union") {
|
|
204
|
+
// A complex-union variant's fields are accessed via the union's own
|
|
205
|
+
// path (the `@type` discriminant narrows it), so scope into `access`.
|
|
206
|
+
const variantType = binding.type.variants[i]?.type;
|
|
207
|
+
walk(alt, resolve, {
|
|
208
|
+
path: access,
|
|
209
|
+
currentStructType: variantType?.kind === "struct" ? variantType : arg.currentStructType,
|
|
210
|
+
});
|
|
211
|
+
} else {
|
|
212
|
+
walk(alt, resolve, arg);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|