@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,341 @@
|
|
|
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 type { CodeBuilder } from "../code-builder.js";
|
|
5
|
+
import type { Scope } from "../scope.js";
|
|
6
|
+
import {
|
|
7
|
+
findAlternativeNode,
|
|
8
|
+
findRangeNode,
|
|
9
|
+
findRepeatNode,
|
|
10
|
+
structFields,
|
|
11
|
+
} from "../validate-walk.js";
|
|
12
|
+
import { structVariants } from "../union-variants.js";
|
|
13
|
+
import { emitJsDoc, tsPropAccess } from "./emit.js";
|
|
14
|
+
import { mapType } from "./typemap.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Emit a `<tool>Validate(params)` function that walks the solved root
|
|
18
|
+
* `BoundType` and throws `StyxValidationError` on invalid input. Hand-rolled
|
|
19
|
+
* (no runtime deps): `typeof`/`Array.isArray` stand in for Python's
|
|
20
|
+
* `isinstance`. Mirrors the Python backend's validation behavior, inlined into
|
|
21
|
+
* a single function.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
type Resolve = (t: BoundType) => string | undefined;
|
|
25
|
+
|
|
26
|
+
interface Emit {
|
|
27
|
+
ctx: CodegenContext;
|
|
28
|
+
resolve: Resolve;
|
|
29
|
+
/** Per-function scope for generated locals (loop vars), reserving `params`. */
|
|
30
|
+
scope: Scope;
|
|
31
|
+
cb: CodeBuilder;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function emitValidate(
|
|
35
|
+
ctx: CodegenContext,
|
|
36
|
+
rootType: BoundType,
|
|
37
|
+
rootNode: Expr,
|
|
38
|
+
paramsType: string,
|
|
39
|
+
funcName: string,
|
|
40
|
+
resolve: Resolve,
|
|
41
|
+
scope: Scope,
|
|
42
|
+
cb: CodeBuilder,
|
|
43
|
+
): void {
|
|
44
|
+
const e: Emit = { ctx, resolve, scope: scope.child(["params"]), cb };
|
|
45
|
+
emitJsDoc(
|
|
46
|
+
cb,
|
|
47
|
+
`Validate untrusted parameters. Throws StyxValidationError if \`params\` is not a valid ${paramsType}; narrows it to ${paramsType} on success.`,
|
|
48
|
+
);
|
|
49
|
+
// Assertion signature over untyped input: a boundary guard usable on a parsed
|
|
50
|
+
// dict / config blob, not just an already-typed value. The body walks `params`
|
|
51
|
+
// dynamically, so the parameter is `any` (the assertion still narrows callers).
|
|
52
|
+
cb.line(`export function ${funcName}(params: any): asserts params is ${paramsType} {`);
|
|
53
|
+
cb.indent(() => emitRoot(e, rootType, rootNode));
|
|
54
|
+
cb.line("}");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function emitRoot(e: Emit, rootType: BoundType, rootNode: Expr): void {
|
|
58
|
+
if (rootType.kind === "struct") {
|
|
59
|
+
block(e, `typeof params !== "object" || params === null`, () =>
|
|
60
|
+
raise(e, "Params object has the wrong type"),
|
|
61
|
+
);
|
|
62
|
+
for (const f of structFields(e.ctx, rootType, rootNode)) {
|
|
63
|
+
emitField(e, f.name, f.type, f.node, f.hasDefault, "params");
|
|
64
|
+
}
|
|
65
|
+
} else if (rootType.kind === "union") {
|
|
66
|
+
emitUnion(e, rootType, rootNode, "params", "params");
|
|
67
|
+
} else {
|
|
68
|
+
emitValue(e, rootType, rootNode, "params", "params", expectedType(e, rootType));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Validate one struct field, handling required-presence vs optional gating. */
|
|
73
|
+
function emitField(
|
|
74
|
+
e: Emit,
|
|
75
|
+
name: string,
|
|
76
|
+
fieldType: BoundType,
|
|
77
|
+
node: Expr | undefined,
|
|
78
|
+
hasDefault: boolean,
|
|
79
|
+
base: string,
|
|
80
|
+
): void {
|
|
81
|
+
if (fieldType.kind === "literal") return;
|
|
82
|
+
|
|
83
|
+
const access = tsPropAccess(base, name);
|
|
84
|
+
const expected = expectedType(e, fieldType);
|
|
85
|
+
const valueType = fieldType.kind === "optional" ? fieldType.inner : fieldType;
|
|
86
|
+
|
|
87
|
+
// Optionals and defaulted fields/flags accept null (null = "use default"), so
|
|
88
|
+
// gate the body instead of requiring presence.
|
|
89
|
+
if (fieldType.kind === "optional" || hasDefault) {
|
|
90
|
+
e.cb.line(`if (${access} != null) {`);
|
|
91
|
+
e.cb.indent(() => emitValue(e, valueType, node, name, access, expected));
|
|
92
|
+
e.cb.line("}");
|
|
93
|
+
} else {
|
|
94
|
+
block(e, `${access} == null`, () => raise(e, "`" + name + "` must not be null"));
|
|
95
|
+
emitValue(e, valueType, node, name, access, expected);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Validate a known-present value at `access` against `type`. */
|
|
100
|
+
function emitValue(
|
|
101
|
+
e: Emit,
|
|
102
|
+
type: BoundType,
|
|
103
|
+
node: Expr | undefined,
|
|
104
|
+
wireKey: string,
|
|
105
|
+
access: string,
|
|
106
|
+
expected: string,
|
|
107
|
+
): void {
|
|
108
|
+
switch (type.kind) {
|
|
109
|
+
case "optional":
|
|
110
|
+
emitValue(e, type.inner, node, wireKey, access, expected);
|
|
111
|
+
return;
|
|
112
|
+
case "literal":
|
|
113
|
+
return;
|
|
114
|
+
case "scalar":
|
|
115
|
+
switch (type.scalar) {
|
|
116
|
+
case "str":
|
|
117
|
+
case "path":
|
|
118
|
+
checkType(e, `typeof ${access} !== "string"`, wireKey, expected);
|
|
119
|
+
return;
|
|
120
|
+
case "int":
|
|
121
|
+
case "float":
|
|
122
|
+
checkType(e, `typeof ${access} !== "number"`, wireKey, expected);
|
|
123
|
+
emitRange(e, node, wireKey, access);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
case "bool":
|
|
128
|
+
checkType(e, `typeof ${access} !== "boolean"`, wireKey, expected);
|
|
129
|
+
return;
|
|
130
|
+
case "count":
|
|
131
|
+
checkType(e, `typeof ${access} !== "number"`, wireKey, expected);
|
|
132
|
+
return;
|
|
133
|
+
case "list": {
|
|
134
|
+
checkType(e, `!Array.isArray(${access})`, wireKey, expected);
|
|
135
|
+
emitListLength(e, node, wireKey, access);
|
|
136
|
+
const itemNode = findRepeatNode(node)?.attrs.node;
|
|
137
|
+
const elem = e.scope.add("el");
|
|
138
|
+
e.cb.line(`for (const ${elem} of ${access}) {`);
|
|
139
|
+
e.cb.indent(() => emitValue(e, type.item, itemNode, wireKey, elem, expected));
|
|
140
|
+
e.cb.line("}");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
case "struct": {
|
|
144
|
+
block(e, `typeof ${access} !== "object" || ${access} === null`, () =>
|
|
145
|
+
raise(e, "Params object has the wrong type"),
|
|
146
|
+
);
|
|
147
|
+
for (const f of structFields(e.ctx, type, node)) {
|
|
148
|
+
emitField(e, f.name, f.type, f.node, f.hasDefault, access);
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
case "union":
|
|
153
|
+
emitUnion(e, type, node, wireKey, access);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function emitUnion(
|
|
159
|
+
e: Emit,
|
|
160
|
+
unionType: Extract<BoundType, { kind: "union" }>,
|
|
161
|
+
node: Expr | undefined,
|
|
162
|
+
wireKey: string,
|
|
163
|
+
access: string,
|
|
164
|
+
): void {
|
|
165
|
+
const litVariants = unionType.variants.filter((v) => v.type.kind === "literal");
|
|
166
|
+
const hasStruct = unionType.variants.some((v) => v.type.kind === "struct");
|
|
167
|
+
|
|
168
|
+
// Pure enum/choice: no struct variants, just literal values (no `@type`).
|
|
169
|
+
if (!hasStruct) {
|
|
170
|
+
const values = litVariants.map(
|
|
171
|
+
(v) => (v.type as Extract<BoundType, { kind: "literal" }>).value,
|
|
172
|
+
);
|
|
173
|
+
const allStr = values.every((x) => typeof x === "string");
|
|
174
|
+
checkType(
|
|
175
|
+
e,
|
|
176
|
+
`typeof ${access} !== ${allStr ? '"string"' : '"number"'}`,
|
|
177
|
+
wireKey,
|
|
178
|
+
expectedType(e, unionType),
|
|
179
|
+
);
|
|
180
|
+
emitLiteralMembership(e, values, wireKey, access);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const altNode = findAlternativeNode(node);
|
|
185
|
+
// Struct variants with their indices; throws if two share an `@type` (a
|
|
186
|
+
// duplicate-tagged variant is unreachable and a mypy `comparison-overlap` in
|
|
187
|
+
// the Python mirror - frontends must dodge duplicate tags before codegen).
|
|
188
|
+
// The index keeps each arm aligned with the IR `alts`.
|
|
189
|
+
const structVars = structVariants(unionType);
|
|
190
|
+
const emitStructArm = (): void => {
|
|
191
|
+
// `access` is known to be an object here.
|
|
192
|
+
block(e, `!("@type" in ${access})`, () => raise(e, "Params object is missing `@type`"));
|
|
193
|
+
const names = structVars
|
|
194
|
+
.map(({ variant }) => variant.name)
|
|
195
|
+
.filter((n): n is string => n !== undefined)
|
|
196
|
+
.map((n) => JSON.stringify(n));
|
|
197
|
+
// `.includes` rather than a `!==`-chain: the discriminant is a closed literal
|
|
198
|
+
// union and an exhaustive chain is rejected by TS as TS2367. It also leaves
|
|
199
|
+
// the union un-narrowed, so the `switch` dispatch below narrows it.
|
|
200
|
+
block(e, `![${names.join(", ")}].includes(${access}["@type"])`, () =>
|
|
201
|
+
raise(e, "Parameter `" + wireKey + "`s `@type` must be one of [" + names.join(", ") + "]"),
|
|
202
|
+
);
|
|
203
|
+
// `switch` (mirroring the cargs builder): each `case` narrows correctly,
|
|
204
|
+
// whereas an `if`/`else if` chain on `===` accumulates narrowing and TS
|
|
205
|
+
// rejects later arms.
|
|
206
|
+
e.cb.line(`switch (${access}["@type"]) {`);
|
|
207
|
+
e.cb.indent(() => {
|
|
208
|
+
structVars.forEach(({ variant, i }) => {
|
|
209
|
+
const vt = variant.type as Extract<BoundType, { kind: "struct" }>;
|
|
210
|
+
e.cb.line(`case ${JSON.stringify(variant.name ?? "")}: {`);
|
|
211
|
+
e.cb.indent(() => {
|
|
212
|
+
const fields = structFields(e.ctx, vt, altNode?.attrs.alts[i]).filter(
|
|
213
|
+
(f) => f.type.kind !== "literal",
|
|
214
|
+
);
|
|
215
|
+
for (const f of fields) emitField(e, f.name, f.type, f.node, f.hasDefault, access);
|
|
216
|
+
e.cb.line("break;");
|
|
217
|
+
});
|
|
218
|
+
e.cb.line("}");
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
e.cb.line("}");
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Pure discriminated union: every variant is a struct with an `@type`.
|
|
225
|
+
if (litVariants.length === 0) {
|
|
226
|
+
block(e, `typeof ${access} !== "object" || ${access} === null`, () =>
|
|
227
|
+
raise(e, "Params object has the wrong type"),
|
|
228
|
+
);
|
|
229
|
+
emitStructArm();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Mixed union: a value is either a struct (dict with `@type`) or a bare
|
|
234
|
+
// literal. Branch on the runtime shape - `typeof === "object"` narrows to the
|
|
235
|
+
// struct members, the `else` to the literal members.
|
|
236
|
+
e.cb.line(`if (typeof ${access} === "object" && ${access} !== null) {`);
|
|
237
|
+
e.cb.indent(emitStructArm);
|
|
238
|
+
e.cb.line(`} else {`);
|
|
239
|
+
e.cb.indent(() => {
|
|
240
|
+
const values = litVariants.map(
|
|
241
|
+
(v) => (v.type as Extract<BoundType, { kind: "literal" }>).value,
|
|
242
|
+
);
|
|
243
|
+
emitLiteralMembership(e, values, wireKey, access);
|
|
244
|
+
});
|
|
245
|
+
e.cb.line("}");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Emit a `.includes`-based membership check over literal values. */
|
|
249
|
+
function emitLiteralMembership(
|
|
250
|
+
e: Emit,
|
|
251
|
+
values: (string | number)[],
|
|
252
|
+
wireKey: string,
|
|
253
|
+
access: string,
|
|
254
|
+
): void {
|
|
255
|
+
const rendered = values.map(renderLiteral);
|
|
256
|
+
block(e, `![${rendered.join(", ")}].includes(${access})`, () =>
|
|
257
|
+
raise(e, "Parameter `" + wireKey + "` must be one of [" + rendered.join(", ") + "]"),
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function checkType(e: Emit, condition: string, wireKey: string, expected: string): void {
|
|
262
|
+
block(e, condition, () =>
|
|
263
|
+
raise(e, "`" + wireKey + "` has the wrong type (expected " + expected + ")"),
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function emitRange(e: Emit, node: Expr | undefined, wireKey: string, access: string): void {
|
|
268
|
+
const term = findRangeNode(node);
|
|
269
|
+
if (!term) return;
|
|
270
|
+
const { minValue, maxValue } = term.attrs;
|
|
271
|
+
if (minValue !== undefined && maxValue !== undefined) {
|
|
272
|
+
block(e, `!(${tsNum(minValue)} <= ${access} && ${access} <= ${tsNum(maxValue)})`, () =>
|
|
273
|
+
raise(e, `Parameter \`${wireKey}\` must be between ${minValue} and ${maxValue} (inclusive)`),
|
|
274
|
+
);
|
|
275
|
+
} else if (minValue !== undefined) {
|
|
276
|
+
block(e, `${access} < ${tsNum(minValue)}`, () =>
|
|
277
|
+
raise(e, `Parameter \`${wireKey}\` must be at least ${minValue}`),
|
|
278
|
+
);
|
|
279
|
+
} else if (maxValue !== undefined) {
|
|
280
|
+
block(e, `${access} > ${tsNum(maxValue)}`, () =>
|
|
281
|
+
raise(e, `Parameter \`${wireKey}\` must be at most ${maxValue}`),
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function emitListLength(e: Emit, node: Expr | undefined, wireKey: string, access: string): void {
|
|
287
|
+
const rep = findRepeatNode(node);
|
|
288
|
+
if (!rep) return;
|
|
289
|
+
const { countMin, countMax } = rep.attrs;
|
|
290
|
+
if (countMin !== undefined && countMax !== undefined) {
|
|
291
|
+
block(e, `!(${countMin} <= ${access}.length && ${access}.length <= ${countMax})`, () =>
|
|
292
|
+
raise(
|
|
293
|
+
e,
|
|
294
|
+
`Parameter \`${wireKey}\` must contain between ${countMin} and ${countMax} elements (inclusive)`,
|
|
295
|
+
),
|
|
296
|
+
);
|
|
297
|
+
} else if (countMin !== undefined) {
|
|
298
|
+
block(e, `${access}.length < ${countMin}`, () =>
|
|
299
|
+
raise(
|
|
300
|
+
e,
|
|
301
|
+
`Parameter \`${wireKey}\` must contain at least ${countMin} ${plural("element", countMin)}`,
|
|
302
|
+
),
|
|
303
|
+
);
|
|
304
|
+
} else if (countMax !== undefined) {
|
|
305
|
+
block(e, `${access}.length > ${countMax}`, () =>
|
|
306
|
+
raise(
|
|
307
|
+
e,
|
|
308
|
+
`Parameter \`${wireKey}\` must contain at most ${countMax} ${plural("element", countMax)}`,
|
|
309
|
+
),
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// -- Emit helpers --
|
|
315
|
+
|
|
316
|
+
/** Emit `if (<cond>) { <body> }`. */
|
|
317
|
+
function block(e: Emit, condition: string, body: () => void): void {
|
|
318
|
+
e.cb.line(`if (${condition}) {`);
|
|
319
|
+
e.cb.indent(body);
|
|
320
|
+
e.cb.line("}");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function raise(e: Emit, message: string): void {
|
|
324
|
+
e.cb.line(`throw new StyxValidationError(${JSON.stringify(message)});`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function expectedType(e: Emit, type: BoundType): string {
|
|
328
|
+
return mapType(type, e.resolve);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function renderLiteral(value: string | number): string {
|
|
332
|
+
return typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function tsNum(n: number): string {
|
|
336
|
+
return Number.isFinite(n) ? String(n) : "NaN";
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function plural(word: string, count: number): string {
|
|
340
|
+
return count === 1 ? word : `${word}s`;
|
|
341
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { BoundType, BoundVariant } from "../bindings/index.js";
|
|
2
|
+
|
|
3
|
+
/** A union's struct variant paired with its index into the union's `variants`
|
|
4
|
+
* array (which stays parallel to the IR `alts` and the per-arm results a caller
|
|
5
|
+
* builds, so the index must be threaded through). */
|
|
6
|
+
export interface IndexedStructVariant {
|
|
7
|
+
variant: BoundVariant;
|
|
8
|
+
i: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The struct variants of a union, each with its index into `variants`.
|
|
13
|
+
*
|
|
14
|
+
* A discriminated union dispatches on a unique `@type`, so two struct variants
|
|
15
|
+
* sharing a tag are a malformed union: the second is unreachable at runtime and
|
|
16
|
+
* emits a dead branch (a mypy `comparison-overlap` in the Python backend's
|
|
17
|
+
* `if`/`elif` chain). Producing well-formed, unique-tagged unions is a frontend
|
|
18
|
+
* responsibility - the Boutiques frontend dodges duplicate sub-command ids
|
|
19
|
+
* (`orient` -> `orient_2`). This asserts that invariant at the backend boundary
|
|
20
|
+
* and throws if it is violated, rather than silently emitting a dead branch.
|
|
21
|
+
*/
|
|
22
|
+
export function structVariants(
|
|
23
|
+
unionType: Extract<BoundType, { kind: "union" }>,
|
|
24
|
+
): IndexedStructVariant[] {
|
|
25
|
+
const seen = new Set<string>();
|
|
26
|
+
const out: IndexedStructVariant[] = [];
|
|
27
|
+
unionType.variants.forEach((variant, i) => {
|
|
28
|
+
if (variant.type.kind !== "struct") return;
|
|
29
|
+
const tag = variant.name ?? "";
|
|
30
|
+
if (seen.has(tag)) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`duplicate union variant @type ${JSON.stringify(tag)}: a discriminated ` +
|
|
33
|
+
`union dispatches on a unique @type, so a second variant with the same ` +
|
|
34
|
+
`tag is unreachable. Frontends must dodge duplicate variant tags before ` +
|
|
35
|
+
`codegen (the Boutiques frontend renames duplicate sub-command ids).`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
seen.add(tag);
|
|
39
|
+
out.push({ variant, i });
|
|
40
|
+
});
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { BoundType } from "../bindings/index.js";
|
|
2
|
+
import type { Expr, Float, Int, Repeat } from "../ir/index.js";
|
|
3
|
+
import type { CodegenContext } from "../manifest/index.js";
|
|
4
|
+
import { collectFieldInfo } from "./collect-field-info.js";
|
|
5
|
+
import { findStructNode } from "./find-struct-node.js";
|
|
6
|
+
import { resolveFieldBinding } from "./resolve-field-binding.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Shared, language-agnostic tree-walk helpers for validation emit.
|
|
10
|
+
*
|
|
11
|
+
* Validation walks the solved root `BoundType` for the data shape, but the
|
|
12
|
+
* runtime constraints it checks (int/float range, list length) live on the
|
|
13
|
+
* underlying IR nodes, not the `BoundType`. These helpers bridge the two: they
|
|
14
|
+
* map a struct's fields to their IR binding nodes and locate the IR node that
|
|
15
|
+
* carries a given constraint within a field's subtree.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** A struct field paired with its BoundType and (when resolvable) its IR node. */
|
|
19
|
+
export interface FieldEntry {
|
|
20
|
+
name: string;
|
|
21
|
+
type: BoundType;
|
|
22
|
+
/** The IR node the field's binding was solved from. `undefined` if it could
|
|
23
|
+
* not be resolved (constraint lookups then degrade gracefully). */
|
|
24
|
+
node: Expr | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Whether the field has a default value. Such fields (and flags) accept
|
|
27
|
+
* `None`/`null` to mean "use the default", so validation gates them like
|
|
28
|
+
* optionals rather than requiring presence - matching v1 niwrap.
|
|
29
|
+
*/
|
|
30
|
+
hasDefault: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Enumerate a struct type's fields in declaration order, each paired with the
|
|
35
|
+
* IR node its binding resolved from. `searchRoot` is the IR subtree known to
|
|
36
|
+
* contain the struct (the root expr for the top-level struct, or a field /
|
|
37
|
+
* union-arm node for nested ones).
|
|
38
|
+
*/
|
|
39
|
+
export function structFields(
|
|
40
|
+
ctx: CodegenContext,
|
|
41
|
+
structType: Extract<BoundType, { kind: "struct" }>,
|
|
42
|
+
searchRoot: Expr | undefined,
|
|
43
|
+
): FieldEntry[] {
|
|
44
|
+
const nodeByName = new Map<string, Expr>();
|
|
45
|
+
if (searchRoot) {
|
|
46
|
+
const structNode = findStructNode(searchRoot, ctx, structType);
|
|
47
|
+
if (structNode) {
|
|
48
|
+
for (const child of structNode.attrs.nodes) {
|
|
49
|
+
const match = resolveFieldBinding(child, ctx, structType);
|
|
50
|
+
if (match) nodeByName.set(match.binding.name, match.binding.node);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const fieldInfo = collectFieldInfo(ctx, structType);
|
|
55
|
+
return Object.entries(structType.fields).map(([name, type]) => ({
|
|
56
|
+
name,
|
|
57
|
+
type,
|
|
58
|
+
node: nodeByName.get(name),
|
|
59
|
+
hasDefault: fieldInfo.get(name)?.defaultValue !== undefined,
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Depth-first search for the first node satisfying `pred`, descending through
|
|
65
|
+
* the transparent structural wrappers the solver may bury a binding under
|
|
66
|
+
* (sequence/optional/repeat/alternative).
|
|
67
|
+
*/
|
|
68
|
+
export function findNode(node: Expr | undefined, pred: (n: Expr) => boolean): Expr | undefined {
|
|
69
|
+
if (!node) return undefined;
|
|
70
|
+
if (pred(node)) return node;
|
|
71
|
+
switch (node.kind) {
|
|
72
|
+
case "sequence":
|
|
73
|
+
for (const child of node.attrs.nodes) {
|
|
74
|
+
const r = findNode(child, pred);
|
|
75
|
+
if (r) return r;
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
case "optional":
|
|
79
|
+
return findNode(node.attrs.node, pred);
|
|
80
|
+
case "repeat":
|
|
81
|
+
return findNode(node.attrs.node, pred);
|
|
82
|
+
case "alternative":
|
|
83
|
+
for (const alt of node.attrs.alts) {
|
|
84
|
+
const r = findNode(alt, pred);
|
|
85
|
+
if (r) return r;
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
default:
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Locate the int/float node carrying a scalar field's numeric range. */
|
|
94
|
+
export function findRangeNode(node: Expr | undefined): Int | Float | undefined {
|
|
95
|
+
const found = findNode(node, (n) => n.kind === "int" || n.kind === "float");
|
|
96
|
+
return found as Int | Float | undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Locate the repeat node carrying a list field's length bounds and item. */
|
|
100
|
+
export function findRepeatNode(node: Expr | undefined): Repeat | undefined {
|
|
101
|
+
const found = findNode(node, (n) => n.kind === "repeat");
|
|
102
|
+
return found as Repeat | undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Locate the alternative node backing a union field, to map arms to variants. */
|
|
106
|
+
export function findAlternativeNode(
|
|
107
|
+
node: Expr | undefined,
|
|
108
|
+
): Extract<Expr, { kind: "alternative" }> | undefined {
|
|
109
|
+
const found = findNode(node, (n) => n.kind === "alternative");
|
|
110
|
+
return found as Extract<Expr, { kind: "alternative" }> | undefined;
|
|
111
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Expr } from "../ir/index.js";
|
|
2
|
+
import type { GateAtom } from "./resolved-output.js";
|
|
3
|
+
import type { BoundType } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export type BindingId = string;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One step in a binding's access path relative to top-level `params`.
|
|
9
|
+
*
|
|
10
|
+
* - `field`: descend into a named field of the enclosing struct scope. Renders
|
|
11
|
+
* as `params.name` (TS) / `params["name"]` (Python).
|
|
12
|
+
* - `iter`: the access base resets to the per-element loop variable bound to
|
|
13
|
+
* `binding` (a `repeat`-of-list). The renderer substitutes the active loop
|
|
14
|
+
* variable at emit time (from the `iter` gate atom's loop, or the
|
|
15
|
+
* arg-builder's local loop), so segments after an `iter` build off the
|
|
16
|
+
* element rather than off `params`.
|
|
17
|
+
*
|
|
18
|
+
* There is deliberately no `variant` or `directValue` segment: complex-union
|
|
19
|
+
* variant fields are plain `field` segments off the union's own path (the
|
|
20
|
+
* `@type` discriminant lives in `GateAtom`, not the access path), and the
|
|
21
|
+
* solver's wrapper collapses (`optional<scalar>`, scalar lists) are expressed
|
|
22
|
+
* by a binding simply inheriting its parent wrapper's path.
|
|
23
|
+
*/
|
|
24
|
+
export type AccessSegment = { kind: "field"; name: string } | { kind: "iter"; binding: BindingId };
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A binding's location relative to top-level `params`, as a structured segment
|
|
28
|
+
* sequence. Computed once by the solver (`assignAccessPaths`) and rendered by
|
|
29
|
+
* each backend's `renderAccess`, so the arg-builder and outputs emitter share
|
|
30
|
+
* one source of truth instead of each re-deriving paths.
|
|
31
|
+
*/
|
|
32
|
+
export type AccessPath = AccessSegment[];
|
|
33
|
+
|
|
34
|
+
export interface Binding {
|
|
35
|
+
id: BindingId;
|
|
36
|
+
node: Expr;
|
|
37
|
+
name: string;
|
|
38
|
+
type: BoundType;
|
|
39
|
+
/**
|
|
40
|
+
* Wrapper layers on the path from the root to this binding, root-to-leaf.
|
|
41
|
+
* Captures the optional/repeat/alternative ancestors as `present`/`iter`/
|
|
42
|
+
* `variant` atoms. Backends nest wrappers in array order; "is this binding
|
|
43
|
+
* conditionally active?" reduces to `gate.length > 0`.
|
|
44
|
+
*/
|
|
45
|
+
gate: GateAtom[];
|
|
46
|
+
/**
|
|
47
|
+
* Access path relative to top-level `params`, assigned by the solver's
|
|
48
|
+
* `assignAccessPaths` pass after types settle. Backends render it via
|
|
49
|
+
* `renderAccess` rather than re-walking the IR to recompute where this
|
|
50
|
+
* binding lives.
|
|
51
|
+
*/
|
|
52
|
+
access: AccessPath;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type BindingRegistry = Map<BindingId, Binding>;
|
|
56
|
+
|
|
57
|
+
export type OutputDiagnosticLevel = "error" | "warning";
|
|
58
|
+
|
|
59
|
+
export interface OutputDiagnostic {
|
|
60
|
+
output: string;
|
|
61
|
+
message: string;
|
|
62
|
+
level: OutputDiagnosticLevel;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface OutputValidationResult {
|
|
66
|
+
errors: OutputDiagnostic[];
|
|
67
|
+
warnings: OutputDiagnostic[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface SolveResult {
|
|
71
|
+
bindings: BindingRegistry;
|
|
72
|
+
resolve: (node: Expr) => Binding | undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createRegistry(): BindingRegistry {
|
|
76
|
+
return new Map();
|
|
77
|
+
}
|