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