@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,454 @@
|
|
|
1
|
+
import type { Binding, BindingId, BoundType, BoundVariant } from "../../bindings/index.js";
|
|
2
|
+
import { collectFieldInfo } from "../collect-field-info.js";
|
|
3
|
+
import type { Expr } from "../../ir/index.js";
|
|
4
|
+
import type { CodegenContext } from "../../manifest/index.js";
|
|
5
|
+
import { CodeBuilder } from "../code-builder.js";
|
|
6
|
+
import { structVariants } from "../union-variants.js";
|
|
7
|
+
import { pyStr, renderAccess, renderPyLiteral } from "./typemap.js";
|
|
8
|
+
|
|
9
|
+
// -- Result types --
|
|
10
|
+
|
|
11
|
+
interface Expr_ {
|
|
12
|
+
expr: string;
|
|
13
|
+
}
|
|
14
|
+
interface Stmt {
|
|
15
|
+
stmt: string;
|
|
16
|
+
}
|
|
17
|
+
export type ArgResult = Expr_ | Stmt;
|
|
18
|
+
|
|
19
|
+
function isExpr(r: ArgResult): r is Expr_ {
|
|
20
|
+
return "expr" in r;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resultToStmt(r: ArgResult): string {
|
|
24
|
+
return isExpr(r) ? `cargs.append(${r.expr})` : r.stmt;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function appendLines(cb: CodeBuilder, code: string): void {
|
|
28
|
+
for (const line of code.split("\n")) cb.line(line);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// -- Type helpers --
|
|
32
|
+
|
|
33
|
+
function toStringExpr(node: Expr, type: BoundType, expr: string): string {
|
|
34
|
+
if (type.kind === "scalar") {
|
|
35
|
+
if (type.scalar === "str") return expr;
|
|
36
|
+
if (type.scalar === "path") return pathArg(node, expr);
|
|
37
|
+
}
|
|
38
|
+
return `str(${expr})`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Command-line value for a path input via `execution.input_file`, threading the
|
|
43
|
+
* path node's `resolve_parent` / `mutable` attrs as keyword args. `mutable=True`
|
|
44
|
+
* tells the runner to stage a writable COPY (original untouched) and return the
|
|
45
|
+
* copy's command-line path; the outputs builder surfaces that same copy's host
|
|
46
|
+
* path via `execution.mutable_copy`.
|
|
47
|
+
*/
|
|
48
|
+
function pathArg(node: Expr, expr: string): string {
|
|
49
|
+
if (node.kind !== "path") return `execution.input_file(${expr})`;
|
|
50
|
+
let extra = "";
|
|
51
|
+
if (node.attrs.resolveParent) extra += ", resolve_parent=True";
|
|
52
|
+
if (node.attrs.mutable) extra += ", mutable=True";
|
|
53
|
+
return `execution.input_file(${expr}${extra})`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// -- Context passed down through recursion --
|
|
57
|
+
|
|
58
|
+
interface ArgContext {
|
|
59
|
+
/** Join nesting depth (controls ternary vs if-statement for optionals). */
|
|
60
|
+
joinDepth: number;
|
|
61
|
+
/**
|
|
62
|
+
* Loop variables bound by enclosing `repeat`-of-list nodes, keyed by the
|
|
63
|
+
* repeat binding's id. `renderAccess` consults this to resolve the `iter`
|
|
64
|
+
* segments in a binding's solver-assigned access path.
|
|
65
|
+
*/
|
|
66
|
+
loopVars: ReadonlyMap<BindingId, string>;
|
|
67
|
+
/**
|
|
68
|
+
* Prefix substitutions for optional fields narrowed by an enclosing presence
|
|
69
|
+
* guard: maps a rendered access prefix to the `.get()`-narrowed local that
|
|
70
|
+
* holds it. Threaded into `renderAccess` so inner reads use the local (one
|
|
71
|
+
* lookup, absent-safe, mypy-narrowable) instead of re-subscripting.
|
|
72
|
+
*/
|
|
73
|
+
valueSubst: ReadonlyMap<string, string>;
|
|
74
|
+
/**
|
|
75
|
+
* Rendered Python default literals for root-level NON-OPTIONAL fields that
|
|
76
|
+
* carry a Boutiques default (e.g. `maskfile -> "img_bet"`), keyed by field
|
|
77
|
+
* name. Such a field is `NotRequired` (a hand-authored config may omit it) yet
|
|
78
|
+
* is read unconditionally - so every read of its value becomes
|
|
79
|
+
* `.get(key, <default>)` to substitute the default instead of raising
|
|
80
|
+
* `KeyError`. Optional fields are excluded: they are presence-guarded (their
|
|
81
|
+
* default, if any, is supplied by the factory's kwarg signature), so reading
|
|
82
|
+
* them with a default here would both be wrong and collide with `valueSubst`.
|
|
83
|
+
*/
|
|
84
|
+
defaults: ReadonlyMap<string, string>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The rendered default for a binding iff it is a root-level NON-OPTIONAL field
|
|
89
|
+
* carrying a Boutiques default. Restricted to single-segment (root) field access
|
|
90
|
+
* so a nested field can never accidentally pick up a same-named root default.
|
|
91
|
+
*/
|
|
92
|
+
function rootFieldDefault(
|
|
93
|
+
binding: Binding,
|
|
94
|
+
defaults: ReadonlyMap<string, string>,
|
|
95
|
+
): string | undefined {
|
|
96
|
+
const a = binding.access;
|
|
97
|
+
if (a.length === 1 && a[0]?.kind === "field") return defaults.get(binding.name);
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Render a binding's access for an UNCONDITIONAL value read (terminal, repeat
|
|
103
|
+
* loop, alternative dispatch): substitutes the field's default via
|
|
104
|
+
* `.get(key, default)` when it is a defaulted non-optional field, else the plain
|
|
105
|
+
* access. Returns plain access for every non-defaulted field, so the emitted
|
|
106
|
+
* code is byte-identical to before for the common case. Not used by
|
|
107
|
+
* `walkOptional` (its bare access is the `valueSubst` key and its guard reads
|
|
108
|
+
* via `.get()`).
|
|
109
|
+
*/
|
|
110
|
+
function readAccess(binding: Binding, arg: ArgContext): string {
|
|
111
|
+
const def = rootFieldDefault(binding, arg.defaults);
|
|
112
|
+
return accessOf(binding, arg, def !== undefined ? { finalDefault: def } : {});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface AccessOpts {
|
|
116
|
+
/** Render the final field segment as `.get(key)` (absent key -> None). */
|
|
117
|
+
finalGet?: boolean;
|
|
118
|
+
/** Render the final field segment as `.get(key, default)` (absent key -> default). */
|
|
119
|
+
finalDefault?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Render a binding's solver-assigned access path in the current loop scope.
|
|
124
|
+
* `finalGet` renders the final field segment as `.get(key)` (used when binding
|
|
125
|
+
* an optional's value to a narrowed local); `finalDefault` renders it as
|
|
126
|
+
* `.get(key, default)` (used for absent-safe reads of a defaulted field).
|
|
127
|
+
*/
|
|
128
|
+
function accessOf(binding: Binding, arg: ArgContext, opts: AccessOpts = {}): string {
|
|
129
|
+
return renderAccess(
|
|
130
|
+
binding.access,
|
|
131
|
+
(b) => {
|
|
132
|
+
const v = arg.loopVars.get(b);
|
|
133
|
+
if (v === undefined) throw new Error(`arg-builder: unbound loop variable for binding ${b}`);
|
|
134
|
+
return v;
|
|
135
|
+
},
|
|
136
|
+
{ finalFieldGet: opts.finalGet, finalFieldDefault: opts.finalDefault, subst: arg.valueSubst },
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Build the field-name -> rendered-default map for a struct root (else empty).
|
|
142
|
+
* Includes only non-optional defaulted fields (optional fields are
|
|
143
|
+
* presence-guarded; their default comes from the factory's kwarg signature).
|
|
144
|
+
*/
|
|
145
|
+
function collectDefaults(ctx: CodegenContext, rootType?: BoundType): Map<string, string> {
|
|
146
|
+
const out = new Map<string, string>();
|
|
147
|
+
if (rootType?.kind !== "struct") return out;
|
|
148
|
+
for (const [name, fi] of collectFieldInfo(ctx, rootType)) {
|
|
149
|
+
if (fi.defaultValue === undefined) continue;
|
|
150
|
+
if (rootType.fields[name]?.kind === "optional") continue;
|
|
151
|
+
out.set(name, renderPyLiteral(fi.defaultValue));
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// -- Recursive descent --
|
|
157
|
+
|
|
158
|
+
let loopVarCounter = 0;
|
|
159
|
+
let optVarCounter = 0;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Build arg-building code for an IR tree via recursive descent.
|
|
163
|
+
*
|
|
164
|
+
* Mirrors the TypeScript backend's `walk` structurally so the emitted Python
|
|
165
|
+
* has the same shape (and the same correctness story) as the TS output.
|
|
166
|
+
*/
|
|
167
|
+
export function buildArgs(rootExpr: Expr, ctx: CodegenContext, rootType?: BoundType): ArgResult {
|
|
168
|
+
loopVarCounter = 0;
|
|
169
|
+
optVarCounter = 0;
|
|
170
|
+
const initialCtx: ArgContext = {
|
|
171
|
+
joinDepth: 0,
|
|
172
|
+
loopVars: new Map(),
|
|
173
|
+
valueSubst: new Map(),
|
|
174
|
+
defaults: collectDefaults(ctx, rootType),
|
|
175
|
+
};
|
|
176
|
+
return walk(rootExpr, ctx, initialCtx);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function walk(node: Expr, ctx: CodegenContext, arg: ArgContext): ArgResult {
|
|
180
|
+
switch (node.kind) {
|
|
181
|
+
case "literal":
|
|
182
|
+
return { expr: pyStr(node.attrs.str) };
|
|
183
|
+
|
|
184
|
+
case "int":
|
|
185
|
+
case "float":
|
|
186
|
+
case "str":
|
|
187
|
+
case "path":
|
|
188
|
+
return walkTerminal(node, ctx, arg);
|
|
189
|
+
|
|
190
|
+
case "sequence":
|
|
191
|
+
return walkSequence(node, ctx, arg);
|
|
192
|
+
|
|
193
|
+
case "optional":
|
|
194
|
+
return walkOptional(node, ctx, arg);
|
|
195
|
+
|
|
196
|
+
case "repeat":
|
|
197
|
+
return walkRepeat(node, ctx, arg);
|
|
198
|
+
|
|
199
|
+
case "alternative":
|
|
200
|
+
return walkAlternative(node, ctx, arg);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function walkTerminal(node: Expr, ctx: CodegenContext, arg: ArgContext): ArgResult {
|
|
205
|
+
const binding = ctx.resolve(node);
|
|
206
|
+
if (!binding) throw new Error(`Missing binding for terminal node: ${node.kind}`);
|
|
207
|
+
// A root-level defaulted field (e.g. an output basename `maskfile="img_bet"`)
|
|
208
|
+
// is read here unconditionally but is `NotRequired`, so `readAccess`
|
|
209
|
+
// substitutes the default for an absent key via `.get(key, default)`.
|
|
210
|
+
return { expr: toStringExpr(node, binding.type, readAccess(binding, arg)) };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function walkSequence(
|
|
214
|
+
node: Extract<Expr, { kind: "sequence" }>,
|
|
215
|
+
ctx: CodegenContext,
|
|
216
|
+
arg: ArgContext,
|
|
217
|
+
): ArgResult {
|
|
218
|
+
// A non-join sequence inside an outer join must concatenate (rather than
|
|
219
|
+
// push separate args) so it can stand in as a single Expr element of the
|
|
220
|
+
// outer join. Boutiques produces this shape for `command-line-flag` inputs
|
|
221
|
+
// nested under a parent join template (e.g. `[OUTPUT][FLAG]` -> seqJoin('')
|
|
222
|
+
// around an opt(seq(lit(FLAG), value))).
|
|
223
|
+
const join = node.attrs.join ?? (arg.joinDepth > 0 ? "" : undefined);
|
|
224
|
+
|
|
225
|
+
// Struct scoping is already baked into each child's access path by the
|
|
226
|
+
// solver; here we only thread join depth (a codegen concern).
|
|
227
|
+
const childArg: ArgContext = join !== undefined ? { ...arg, joinDepth: arg.joinDepth + 1 } : arg;
|
|
228
|
+
|
|
229
|
+
const parts = node.attrs.nodes.map((child) => walk(child, ctx, childArg));
|
|
230
|
+
|
|
231
|
+
if (join !== undefined) {
|
|
232
|
+
const exprs = parts.map((p) => (isExpr(p) ? p.expr : p.stmt));
|
|
233
|
+
if (exprs.length === 1) return { expr: exprs[0]! };
|
|
234
|
+
return { expr: `${pyStr(join)}.join([${exprs.join(", ")}])` };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return { stmt: parts.map(resultToStmt).join("\n") };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function walkOptional(
|
|
241
|
+
node: Extract<Expr, { kind: "optional" }>,
|
|
242
|
+
ctx: CodegenContext,
|
|
243
|
+
arg: ArgContext,
|
|
244
|
+
): ArgResult {
|
|
245
|
+
const binding = ctx.resolve(node);
|
|
246
|
+
if (!binding) throw new Error("Missing binding for optional node");
|
|
247
|
+
const isOpt = binding.type.kind === "optional";
|
|
248
|
+
const access = accessOf(binding, arg);
|
|
249
|
+
|
|
250
|
+
// For a nullable optional, bind the value to a narrowed local read via `.get()`
|
|
251
|
+
// (the key is `NotRequired` - the factory omits it when None, so a bare
|
|
252
|
+
// subscript would KeyError). Inner reads of this access (and anything nested
|
|
253
|
+
// under it) are redirected to the local via `valueSubst`: one lookup, absent-
|
|
254
|
+
// safe, and mypy can narrow the local (it cannot narrow a re-subscript or a
|
|
255
|
+
// fresh `.get()`). Bool-flag optionals are also `NotRequired` (default false),
|
|
256
|
+
// so the truthy guard reads via `.get()` too (absent key -> None -> flag off).
|
|
257
|
+
let childArg = arg;
|
|
258
|
+
let local: string | undefined;
|
|
259
|
+
let getAccess: string | undefined;
|
|
260
|
+
if (isOpt) {
|
|
261
|
+
local = `v_${optVarCounter++}`;
|
|
262
|
+
getAccess = accessOf(binding, arg, { finalGet: true });
|
|
263
|
+
childArg = { ...arg, valueSubst: new Map(arg.valueSubst).set(access, local) };
|
|
264
|
+
}
|
|
265
|
+
// Absent-safe truthy guard for the bool-flag case.
|
|
266
|
+
const boolGuard = accessOf(binding, arg, { finalGet: true });
|
|
267
|
+
|
|
268
|
+
// The inner node's access path is solver-assigned (it either inherits this
|
|
269
|
+
// optional's path on a collapse, or scopes into it for a struct); we thread the
|
|
270
|
+
// loop scope, join depth, and the optional's value substitution.
|
|
271
|
+
const inner = walk(node.attrs.node, ctx, childArg);
|
|
272
|
+
|
|
273
|
+
// Inside a join context, emit as ternary expression.
|
|
274
|
+
if (arg.joinDepth > 0 && isExpr(inner)) {
|
|
275
|
+
if (isOpt) {
|
|
276
|
+
// Walrus binds the narrowed local inside the lazy ternary; the inner expr
|
|
277
|
+
// (which references `local`) only evaluates when the key is present.
|
|
278
|
+
return { expr: `(${inner.expr} if (${local} := ${getAccess}) is not None else "")` };
|
|
279
|
+
}
|
|
280
|
+
return { expr: `(${inner.expr} if ${boolGuard} else "")` };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const cb = new CodeBuilder(" ");
|
|
284
|
+
const innerStmt = resultToStmt(inner);
|
|
285
|
+
if (isOpt) {
|
|
286
|
+
cb.line(`${local} = ${getAccess}`);
|
|
287
|
+
cb.line(`if ${local} is not None:`);
|
|
288
|
+
cb.indent(() => appendLines(cb, innerStmt));
|
|
289
|
+
} else {
|
|
290
|
+
cb.line(`if ${boolGuard}:`);
|
|
291
|
+
cb.indent(() => appendLines(cb, innerStmt));
|
|
292
|
+
}
|
|
293
|
+
return { stmt: cb.toString() };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function walkRepeat(
|
|
297
|
+
node: Extract<Expr, { kind: "repeat" }>,
|
|
298
|
+
ctx: CodegenContext,
|
|
299
|
+
arg: ArgContext,
|
|
300
|
+
): ArgResult {
|
|
301
|
+
const binding = ctx.resolve(node);
|
|
302
|
+
if (!binding) throw new Error("Missing binding for repeat node");
|
|
303
|
+
// A non-join repeat inside an outer join concatenates rather than pushing
|
|
304
|
+
// separate args, mirroring walkSequence's handling of bare non-join seqs.
|
|
305
|
+
const join = node.attrs.join ?? (arg.joinDepth > 0 ? "" : undefined);
|
|
306
|
+
// Unconditional read (the count/list value) - `readAccess` substitutes a
|
|
307
|
+
// defaulted non-optional field's default for an absent key.
|
|
308
|
+
const access = readAccess(binding, arg);
|
|
309
|
+
|
|
310
|
+
// Count repeat: emit a counted for-loop. Inside a join the for-loop would
|
|
311
|
+
// be dropped into a list literal as raw text, so emit a comprehension.
|
|
312
|
+
if (binding.type.kind === "count") {
|
|
313
|
+
const inner = walk(node.attrs.node, ctx, arg);
|
|
314
|
+
const v = `_i${loopVarCounter++}`;
|
|
315
|
+
if (join !== undefined && isExpr(inner)) {
|
|
316
|
+
return { expr: `${pyStr(join)}.join([${inner.expr} for ${v} in range(${access})])` };
|
|
317
|
+
}
|
|
318
|
+
const cb = new CodeBuilder(" ");
|
|
319
|
+
cb.line(`for ${v} in range(${access}):`);
|
|
320
|
+
cb.indent(() => appendLines(cb, resultToStmt(inner)));
|
|
321
|
+
return { stmt: cb.toString() };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// List repeat: emit a for-in loop or generator-join. The loop variable is
|
|
325
|
+
// registered under this repeat's binding id so inner bindings' `iter`
|
|
326
|
+
// segments resolve to it via `renderAccess`.
|
|
327
|
+
const loopVar = `item${loopVarCounter++}`;
|
|
328
|
+
const childArg: ArgContext = {
|
|
329
|
+
...arg,
|
|
330
|
+
loopVars: new Map(arg.loopVars).set(binding.id, loopVar),
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const inner = walk(node.attrs.node, ctx, childArg);
|
|
334
|
+
|
|
335
|
+
if (join !== undefined && isExpr(inner)) {
|
|
336
|
+
return {
|
|
337
|
+
expr: `${pyStr(join)}.join([${inner.expr} for ${loopVar} in ${access}])`,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const cb = new CodeBuilder(" ");
|
|
342
|
+
cb.line(`for ${loopVar} in ${access}:`);
|
|
343
|
+
cb.indent(() => appendLines(cb, resultToStmt(inner)));
|
|
344
|
+
return { stmt: cb.toString() };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function walkAlternative(
|
|
348
|
+
node: Extract<Expr, { kind: "alternative" }>,
|
|
349
|
+
ctx: CodegenContext,
|
|
350
|
+
arg: ArgContext,
|
|
351
|
+
): ArgResult {
|
|
352
|
+
const binding = ctx.resolve(node);
|
|
353
|
+
if (!binding) throw new Error("Missing binding for alternative node");
|
|
354
|
+
// Unconditional read (the enum value / bool guard / union discriminator) -
|
|
355
|
+
// `readAccess` substitutes a defaulted non-optional field's default for an
|
|
356
|
+
// absent key (e.g. a `value-choices` String with a default).
|
|
357
|
+
const access = readAccess(binding, arg);
|
|
358
|
+
|
|
359
|
+
// Complex-union variant fields already carry the union's path in their
|
|
360
|
+
// solver-assigned access, so arms walk with the current context unchanged.
|
|
361
|
+
const variants = node.attrs.alts.map((alt) => walk(alt, ctx, arg));
|
|
362
|
+
|
|
363
|
+
if (
|
|
364
|
+
binding.type.kind === "union" &&
|
|
365
|
+
binding.type.variants.every((v: BoundVariant) => v.type.kind === "literal")
|
|
366
|
+
) {
|
|
367
|
+
return { expr: `str(${access})` };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (binding.type.kind === "bool") {
|
|
371
|
+
// Inside a join, the alternative's output must be an expression, not a
|
|
372
|
+
// statement: an `if/else` block dropped into a `"".join([...])` list
|
|
373
|
+
// literal is not valid Python. Emit a ternary when both arms are exprs.
|
|
374
|
+
if (arg.joinDepth > 0) {
|
|
375
|
+
if (!variants[1]) {
|
|
376
|
+
throw new Error(
|
|
377
|
+
"single-arm bool alternative inside a join: cannot produce an expression " +
|
|
378
|
+
"without ambiguous semantics (omitting the entry vs emitting empty string)",
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
if (!variants.every(isExpr)) {
|
|
382
|
+
throw new Error(
|
|
383
|
+
"bool alternative inside a join has statement-shaped variants; " +
|
|
384
|
+
"expected all arms to fold to expressions",
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
const v0 = (variants[0] as Expr_).expr;
|
|
388
|
+
const v1 = (variants[1] as Expr_).expr;
|
|
389
|
+
return { expr: `(${v0} if ${access} else ${v1})` };
|
|
390
|
+
}
|
|
391
|
+
const cb = new CodeBuilder(" ");
|
|
392
|
+
cb.line(`if ${access}:`);
|
|
393
|
+
cb.indent(() => appendLines(cb, resultToStmt(variants[0]!)));
|
|
394
|
+
if (variants[1]) {
|
|
395
|
+
cb.line(`else:`);
|
|
396
|
+
cb.indent(() => appendLines(cb, resultToStmt(variants[1]!)));
|
|
397
|
+
}
|
|
398
|
+
return { stmt: cb.toString() };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (binding.type.kind === "union") {
|
|
402
|
+
const unionType = binding.type;
|
|
403
|
+
// A union may be pure-discriminated (every variant a struct with `@type`) or
|
|
404
|
+
// mixed (struct variants plus bare-literal variants, e.g. ants
|
|
405
|
+
// `Interpolation = "Linear" | MultiLabel | ...`). Pure-enum unions returned
|
|
406
|
+
// above. Dispatch struct variants on `@type`; a bare literal is its own value.
|
|
407
|
+
// Struct variants with their indices; throws if two share an `@type` (a
|
|
408
|
+
// duplicate-tagged variant is unreachable and a mypy `comparison-overlap` -
|
|
409
|
+
// frontends must dodge duplicate tags before codegen).
|
|
410
|
+
const structVars = structVariants(unionType);
|
|
411
|
+
const hasLiteral = unionType.variants.some((v) => v.type.kind === "literal");
|
|
412
|
+
|
|
413
|
+
// Inside a join: chained ternary, same reason as bool above.
|
|
414
|
+
if (arg.joinDepth > 0) {
|
|
415
|
+
if (!variants.every(isExpr)) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
"union alternative inside a join has statement-shaped variants; " +
|
|
418
|
+
"expected all arms to fold to expressions",
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
let structExpr = pyStr("");
|
|
422
|
+
for (let k = structVars.length - 1; k >= 0; k--) {
|
|
423
|
+
const { variant, i } = structVars[k]!;
|
|
424
|
+
const v = (variants[i] as Expr_).expr;
|
|
425
|
+
structExpr = `(${v} if ${access}["@type"] == ${pyStr(variant.name ?? "")} else ${structExpr})`;
|
|
426
|
+
}
|
|
427
|
+
if (!hasLiteral) return { expr: structExpr };
|
|
428
|
+
// Mixed: a dict value dispatches by `@type`; a bare literal is itself.
|
|
429
|
+
return { expr: `(${structExpr} if isinstance(${access}, dict) else str(${access}))` };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const cb = new CodeBuilder(" ");
|
|
433
|
+
const emitStructDispatch = (): void => {
|
|
434
|
+
structVars.forEach(({ variant, i }, k) => {
|
|
435
|
+
const keyword = k === 0 ? "if" : "elif";
|
|
436
|
+
cb.line(`${keyword} ${access}["@type"] == ${pyStr(variant.name ?? "")}:`);
|
|
437
|
+
cb.indent(() => appendLines(cb, resultToStmt(variants[i]!)));
|
|
438
|
+
});
|
|
439
|
+
};
|
|
440
|
+
if (!hasLiteral) {
|
|
441
|
+
emitStructDispatch();
|
|
442
|
+
return { stmt: cb.toString() };
|
|
443
|
+
}
|
|
444
|
+
// Mixed union: branch on runtime shape (dict -> `@type` dispatch; else a
|
|
445
|
+
// bare literal used directly), mirroring the validator.
|
|
446
|
+
cb.line(`if isinstance(${access}, dict):`);
|
|
447
|
+
cb.indent(emitStructDispatch);
|
|
448
|
+
cb.line(`else:`);
|
|
449
|
+
cb.indent(() => appendLines(cb, resultToStmt({ expr: `str(${access})` })));
|
|
450
|
+
return { stmt: cb.toString() };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return { stmt: variants.map(resultToStmt).join("\n") };
|
|
454
|
+
}
|