@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,914 @@
|
|
|
1
|
+
import type { AppMeta, NodeMeta } from "../../ir/meta.js";
|
|
2
|
+
import type {
|
|
3
|
+
Alternative,
|
|
4
|
+
Expr,
|
|
5
|
+
Float,
|
|
6
|
+
Int,
|
|
7
|
+
Literal,
|
|
8
|
+
Optional,
|
|
9
|
+
Path,
|
|
10
|
+
Repeat,
|
|
11
|
+
Sequence,
|
|
12
|
+
Str,
|
|
13
|
+
} from "../../ir/node.js";
|
|
14
|
+
import type {
|
|
15
|
+
Frontend,
|
|
16
|
+
ParseError,
|
|
17
|
+
ParseResult,
|
|
18
|
+
ParseWarning,
|
|
19
|
+
SourceLocation,
|
|
20
|
+
} from "../frontend.js";
|
|
21
|
+
|
|
22
|
+
// Find the deepest existing name in a subtree, mirroring solver semantics.
|
|
23
|
+
// Used by the mutex code so the synthesized inner name matches the binding
|
|
24
|
+
// name the solver will produce for the same subtree.
|
|
25
|
+
function findDeepName(node: Expr): string | undefined {
|
|
26
|
+
if (node.meta?.name) return node.meta.name;
|
|
27
|
+
if (node.kind === "optional" || node.kind === "repeat") {
|
|
28
|
+
return findDeepName(node.attrs.node);
|
|
29
|
+
}
|
|
30
|
+
if (node.kind === "sequence") {
|
|
31
|
+
for (const child of node.attrs.nodes) {
|
|
32
|
+
if (child.kind === "literal") continue;
|
|
33
|
+
const name = findDeepName(child);
|
|
34
|
+
if (name) return name;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Type guards
|
|
41
|
+
|
|
42
|
+
function isObject(x: unknown): x is Record<string, unknown> {
|
|
43
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isString(x: unknown): x is string {
|
|
47
|
+
return typeof x === "string";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isNumber(x: unknown): x is number {
|
|
51
|
+
return typeof x === "number";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isArray(x: unknown): x is unknown[] {
|
|
55
|
+
return Array.isArray(x);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Argdump types
|
|
59
|
+
|
|
60
|
+
type AdAction = Record<string, unknown>;
|
|
61
|
+
type AdDescriptor = Record<string, unknown>;
|
|
62
|
+
|
|
63
|
+
interface ArgparseMarker {
|
|
64
|
+
__argparse__: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isArgparseMarker(x: unknown): x is ArgparseMarker {
|
|
68
|
+
return isObject(x) && isString(x.__argparse__);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isSuppressed(x: unknown): boolean {
|
|
72
|
+
return isArgparseMarker(x) && x.__argparse__ === "SUPPRESS";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Parser
|
|
76
|
+
|
|
77
|
+
export class ArgdumpParser implements Frontend {
|
|
78
|
+
readonly name = "argdump";
|
|
79
|
+
readonly extensions = ["json"];
|
|
80
|
+
|
|
81
|
+
private errors: ParseError[] = [];
|
|
82
|
+
private warnings: ParseWarning[] = [];
|
|
83
|
+
|
|
84
|
+
private reset(): void {
|
|
85
|
+
this.errors = [];
|
|
86
|
+
this.warnings = [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private error(message: string, location?: SourceLocation): void {
|
|
90
|
+
this.errors.push({ message, location });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private warn(message: string, location?: SourceLocation): void {
|
|
94
|
+
this.warnings.push({ message, location });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// JSON parsing
|
|
98
|
+
|
|
99
|
+
private parseJSON(source: string): AdDescriptor | null {
|
|
100
|
+
let parsed: unknown;
|
|
101
|
+
try {
|
|
102
|
+
parsed = JSON.parse(source);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
this.error(e instanceof SyntaxError ? e.message : "Invalid JSON");
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!isObject(parsed)) {
|
|
109
|
+
this.error("JSON source is not an object");
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return parsed;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Metadata building
|
|
117
|
+
|
|
118
|
+
private buildAppMeta(descriptor: AdDescriptor): AppMeta | undefined {
|
|
119
|
+
const prog = descriptor.prog;
|
|
120
|
+
if (!isString(prog)) return undefined;
|
|
121
|
+
|
|
122
|
+
// Use prog as id, fall back to first word of description
|
|
123
|
+
let id = prog || undefined;
|
|
124
|
+
if (!id) {
|
|
125
|
+
const desc = descriptor.description;
|
|
126
|
+
if (isString(desc)) {
|
|
127
|
+
// Extract tool name: first word, strip trailing punctuation
|
|
128
|
+
const match = desc.match(/^(\S+)/);
|
|
129
|
+
if (match) id = match[1]!.replace(/[:;,]+$/, "") || undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (!id) return undefined;
|
|
133
|
+
|
|
134
|
+
const description = descriptor.description;
|
|
135
|
+
const epilog = descriptor.epilog;
|
|
136
|
+
// Try to extract version from a version action
|
|
137
|
+
let versionStr: string | undefined;
|
|
138
|
+
const actions = descriptor.actions;
|
|
139
|
+
if (isArray(actions)) {
|
|
140
|
+
for (const action of actions) {
|
|
141
|
+
if (isObject(action) && action.action_type === "version" && isString(action.version)) {
|
|
142
|
+
// Strip %(prog)s prefix if present
|
|
143
|
+
versionStr = action.version.replace(/%%?\(prog\)s\s*/g, "").trim();
|
|
144
|
+
if (!versionStr) versionStr = undefined;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
id,
|
|
152
|
+
...(versionStr && { version: versionStr }),
|
|
153
|
+
...((isString(description) || isString(epilog)) && {
|
|
154
|
+
doc: {
|
|
155
|
+
...(isString(description) && { description }),
|
|
156
|
+
...(isString(epilog) && { comment: epilog }),
|
|
157
|
+
},
|
|
158
|
+
}),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Terminal node building
|
|
163
|
+
|
|
164
|
+
private resolveTerminal(action: AdAction): Expr | null {
|
|
165
|
+
const typeInfo = action.type_info;
|
|
166
|
+
const fileTypeInfo = action.file_type_info;
|
|
167
|
+
const choices = action.choices;
|
|
168
|
+
|
|
169
|
+
// Choices -> enum alternative
|
|
170
|
+
if (isArray(choices) && choices.length > 0) {
|
|
171
|
+
const alts: Literal[] = [];
|
|
172
|
+
for (const choice of choices) {
|
|
173
|
+
if (isString(choice) || isNumber(choice)) {
|
|
174
|
+
alts.push({ kind: "literal", attrs: { str: String(choice) } });
|
|
175
|
+
} else {
|
|
176
|
+
this.warn(`Ignoring non-string/number choice: ${JSON.stringify(choice)}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (alts.length === 0) return null;
|
|
180
|
+
return { kind: "alternative", attrs: { alts } };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// FileType -> path
|
|
184
|
+
if (isObject(fileTypeInfo)) {
|
|
185
|
+
return { kind: "path", attrs: {} } satisfies Path;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// type_info-based resolution
|
|
189
|
+
if (isObject(typeInfo)) {
|
|
190
|
+
const name = typeInfo.name;
|
|
191
|
+
|
|
192
|
+
if (!isString(name)) {
|
|
193
|
+
return this.inferFromSamples(action) ?? ({ kind: "str", attrs: {} } satisfies Str);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Non-serializable type -> infer from samples, else str + warning
|
|
197
|
+
if (typeInfo.serializable === false) {
|
|
198
|
+
const inferred = this.inferFromSamples(action);
|
|
199
|
+
const fallback = inferred ? inferred.kind : "string";
|
|
200
|
+
this.warn(`Non-serializable type '${name}' for '${action.dest}', treating as ${fallback}`);
|
|
201
|
+
return inferred ?? ({ kind: "str", attrs: {} } satisfies Str);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
switch (name) {
|
|
205
|
+
case "int":
|
|
206
|
+
return { kind: "int", attrs: {} } satisfies Int;
|
|
207
|
+
case "float":
|
|
208
|
+
return { kind: "float", attrs: {} } satisfies Float;
|
|
209
|
+
case "Path":
|
|
210
|
+
case "PosixPath":
|
|
211
|
+
case "WindowsPath":
|
|
212
|
+
return { kind: "path", attrs: {} } satisfies Path;
|
|
213
|
+
default: {
|
|
214
|
+
const moduleHit = this.resolveByModule(typeInfo.module);
|
|
215
|
+
if (moduleHit) return moduleHit;
|
|
216
|
+
// Unknown type -> infer from samples, else str
|
|
217
|
+
const inferred = this.inferFromSamples(action);
|
|
218
|
+
if (name !== "str") {
|
|
219
|
+
const fallback = inferred ? inferred.kind : "string";
|
|
220
|
+
this.warn(`Unknown type '${name}' for '${action.dest}', treating as ${fallback}`);
|
|
221
|
+
}
|
|
222
|
+
return inferred ?? ({ kind: "str", attrs: {} } satisfies Str);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// No type_info -> infer from samples, else str (argparse default)
|
|
228
|
+
return this.inferFromSamples(action) ?? ({ kind: "str", attrs: {} } satisfies Str);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private resolveByModule(mod: unknown): Path | Float | null {
|
|
232
|
+
if (!isString(mod)) return null;
|
|
233
|
+
if (mod === "pathlib" || mod === "os.path" || mod.includes("path")) {
|
|
234
|
+
return { kind: "path", attrs: {} } satisfies Path;
|
|
235
|
+
}
|
|
236
|
+
if (mod === "decimal" || mod === "fractions") {
|
|
237
|
+
return { kind: "float", attrs: {} } satisfies Float;
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Infer numeric type from sample values: default (incl. list elements) or const. */
|
|
243
|
+
private inferFromSamples(action: AdAction): Int | Float | null {
|
|
244
|
+
const samples: unknown[] = [];
|
|
245
|
+
const def = action.default;
|
|
246
|
+
if (isArray(def)) samples.push(...def);
|
|
247
|
+
else samples.push(def);
|
|
248
|
+
samples.push(action.const);
|
|
249
|
+
|
|
250
|
+
let sawNumber = false;
|
|
251
|
+
let sawNonInteger = false;
|
|
252
|
+
for (const s of samples) {
|
|
253
|
+
if (typeof s !== "number" || !Number.isFinite(s)) continue;
|
|
254
|
+
sawNumber = true;
|
|
255
|
+
if (!Number.isInteger(s)) sawNonInteger = true;
|
|
256
|
+
}
|
|
257
|
+
if (!sawNumber) return null;
|
|
258
|
+
return sawNonInteger
|
|
259
|
+
? ({ kind: "float", attrs: {} } satisfies Float)
|
|
260
|
+
: ({ kind: "int", attrs: {} } satisfies Int);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Nargs wrapping
|
|
264
|
+
|
|
265
|
+
private wrapWithNargs(node: Expr, nargs: unknown): Expr {
|
|
266
|
+
if (nargs === null || nargs === undefined) {
|
|
267
|
+
// No nargs -> bare value
|
|
268
|
+
return node;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (isArgparseMarker(nargs)) {
|
|
272
|
+
if (nargs.__argparse__ === "REMAINDER") {
|
|
273
|
+
// REMAINDER -> rep(str())
|
|
274
|
+
const rep: Repeat = {
|
|
275
|
+
kind: "repeat",
|
|
276
|
+
attrs: { node: { kind: "str", attrs: {} }, countMin: 0 },
|
|
277
|
+
};
|
|
278
|
+
return rep;
|
|
279
|
+
}
|
|
280
|
+
if (nargs.__argparse__ === "SUPPRESS") {
|
|
281
|
+
return node;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (nargs === "?") {
|
|
286
|
+
const opt: Optional = { kind: "optional", attrs: { node } };
|
|
287
|
+
return opt;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (nargs === "*") {
|
|
291
|
+
const rep: Repeat = { kind: "repeat", attrs: { node, countMin: 0 } };
|
|
292
|
+
return rep;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (nargs === "+") {
|
|
296
|
+
const rep: Repeat = { kind: "repeat", attrs: { node, countMin: 1 } };
|
|
297
|
+
return rep;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (isNumber(nargs) && Number.isInteger(nargs) && nargs >= 0) {
|
|
301
|
+
if (nargs === 1) return node;
|
|
302
|
+
const rep: Repeat = {
|
|
303
|
+
kind: "repeat",
|
|
304
|
+
attrs: { node, countMin: nargs, countMax: nargs },
|
|
305
|
+
};
|
|
306
|
+
return rep;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return node;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Action building
|
|
313
|
+
|
|
314
|
+
private buildNodeMeta(action: AdAction): NodeMeta | undefined {
|
|
315
|
+
const dest = action.dest;
|
|
316
|
+
const help = action.help;
|
|
317
|
+
const defaultVal = action.default;
|
|
318
|
+
const name = this.preferredName(action) ?? (isString(dest) ? dest : undefined);
|
|
319
|
+
|
|
320
|
+
const hasName = name !== undefined;
|
|
321
|
+
const hasHelp = isString(help) && !isSuppressed(help);
|
|
322
|
+
const hasDefault =
|
|
323
|
+
(isString(defaultVal) || isNumber(defaultVal) || typeof defaultVal === "boolean") &&
|
|
324
|
+
!isSuppressed(defaultVal);
|
|
325
|
+
|
|
326
|
+
if (!hasName && !hasHelp && !hasDefault) return undefined;
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
...(hasName && { name }),
|
|
330
|
+
...(hasHelp && { doc: { description: help } }),
|
|
331
|
+
...(hasDefault && { defaultValue: defaultVal }),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private getOptionFlag(action: AdAction): string | null {
|
|
336
|
+
const optionStrings = action.option_strings;
|
|
337
|
+
if (!isArray(optionStrings) || optionStrings.length === 0) return null;
|
|
338
|
+
|
|
339
|
+
// Prefer long option, fallback to first
|
|
340
|
+
for (const opt of optionStrings) {
|
|
341
|
+
if (isString(opt) && opt.startsWith("--")) return opt;
|
|
342
|
+
}
|
|
343
|
+
const first = optionStrings[0];
|
|
344
|
+
return isString(first) ? first : null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Prefer the first --long flag (without leading dashes) over dest. */
|
|
348
|
+
private preferredName(action: AdAction): string | undefined {
|
|
349
|
+
const optionStrings = action.option_strings;
|
|
350
|
+
if (!isArray(optionStrings)) return undefined;
|
|
351
|
+
for (const opt of optionStrings) {
|
|
352
|
+
if (isString(opt) && opt.startsWith("--") && opt.length > 2) {
|
|
353
|
+
return opt.slice(2);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private isPositional(action: AdAction): boolean {
|
|
360
|
+
const optionStrings = action.option_strings;
|
|
361
|
+
return !isArray(optionStrings) || optionStrings.length === 0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private buildAction(action: AdAction): Expr | null {
|
|
365
|
+
const actionType = action.action_type ?? "store";
|
|
366
|
+
|
|
367
|
+
switch (actionType) {
|
|
368
|
+
case "store":
|
|
369
|
+
return this.buildStore(action);
|
|
370
|
+
case "store_true":
|
|
371
|
+
return this.buildStoreTrue(action);
|
|
372
|
+
case "store_false":
|
|
373
|
+
return this.buildStoreFalse(action);
|
|
374
|
+
case "store_const":
|
|
375
|
+
return this.buildStoreConst(action);
|
|
376
|
+
case "boolean_optional":
|
|
377
|
+
return this.buildBooleanOptional(action);
|
|
378
|
+
case "count":
|
|
379
|
+
return this.buildCount(action);
|
|
380
|
+
case "append":
|
|
381
|
+
case "extend":
|
|
382
|
+
return this.buildAppendExtend(action);
|
|
383
|
+
case "append_const":
|
|
384
|
+
return this.buildAppendConst(action);
|
|
385
|
+
case "parsers":
|
|
386
|
+
return this.buildSubparsers(action);
|
|
387
|
+
case "help":
|
|
388
|
+
case "version":
|
|
389
|
+
// Skip help/version actions - not part of the tool interface
|
|
390
|
+
return null;
|
|
391
|
+
case "unknown":
|
|
392
|
+
this.warn(
|
|
393
|
+
`Unknown action type for '${action.dest}'` +
|
|
394
|
+
(isString(action.custom_action_class)
|
|
395
|
+
? ` (custom class: ${action.custom_action_class})`
|
|
396
|
+
: "") +
|
|
397
|
+
", treating as store",
|
|
398
|
+
);
|
|
399
|
+
return this.buildStore(action);
|
|
400
|
+
default:
|
|
401
|
+
this.warn(`Unrecognized action_type '${actionType}' for '${action.dest}', skipping`);
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private buildStore(action: AdAction): Expr | null {
|
|
407
|
+
const terminal = this.resolveTerminal(action);
|
|
408
|
+
if (!terminal) return null;
|
|
409
|
+
|
|
410
|
+
const meta = this.buildNodeMeta(action);
|
|
411
|
+
if (meta) terminal.meta = meta;
|
|
412
|
+
|
|
413
|
+
const node: Expr = this.wrapWithNargs(terminal, action.nargs);
|
|
414
|
+
|
|
415
|
+
if (this.isPositional(action)) {
|
|
416
|
+
// Hoist metadata from terminal to outermost wrapper
|
|
417
|
+
if (node !== terminal && terminal.meta) {
|
|
418
|
+
const { name, ...rest } = terminal.meta;
|
|
419
|
+
if (Object.keys(rest).length > 0) {
|
|
420
|
+
node.meta = { ...node.meta, ...rest };
|
|
421
|
+
terminal.meta = name ? { name } : undefined;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return node;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Optional argument: seq(lit(flag), value)
|
|
428
|
+
const flag = this.getOptionFlag(action);
|
|
429
|
+
if (!flag) {
|
|
430
|
+
this.error(`Optional argument '${action.dest}' has no option strings`);
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const flagLit: Literal = { kind: "literal", attrs: { str: flag } };
|
|
435
|
+
const seq: Sequence = { kind: "sequence", attrs: { nodes: [flagLit, node] } };
|
|
436
|
+
|
|
437
|
+
// Wrap in optional unless explicitly required
|
|
438
|
+
const isRequired = action.required === true;
|
|
439
|
+
let result: Expr;
|
|
440
|
+
if (isRequired) {
|
|
441
|
+
result = seq;
|
|
442
|
+
} else {
|
|
443
|
+
const opt: Optional = { kind: "optional", attrs: { node: seq } };
|
|
444
|
+
result = opt;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Hoist metadata to outermost node
|
|
448
|
+
if (terminal.meta) {
|
|
449
|
+
const { name, ...rest } = terminal.meta;
|
|
450
|
+
if (Object.keys(rest).length > 0) {
|
|
451
|
+
result.meta = { ...result.meta, ...rest };
|
|
452
|
+
terminal.meta = name ? { name } : undefined;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return result;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private buildStoreTrue(action: AdAction): Expr | null {
|
|
460
|
+
const flag = this.getOptionFlag(action);
|
|
461
|
+
if (!flag) {
|
|
462
|
+
this.error(`store_true action '${action.dest}' has no option strings`);
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const literal: Literal = { kind: "literal", attrs: { str: flag } };
|
|
467
|
+
const opt: Optional = { kind: "optional", attrs: { node: literal } };
|
|
468
|
+
|
|
469
|
+
const meta = this.buildNodeMeta(action);
|
|
470
|
+
const flagMeta = meta ?? {};
|
|
471
|
+
if (flagMeta.defaultValue === undefined) flagMeta.defaultValue = false;
|
|
472
|
+
opt.meta = flagMeta;
|
|
473
|
+
|
|
474
|
+
return opt;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private buildStoreFalse(action: AdAction): Expr | null {
|
|
478
|
+
const flag = this.getOptionFlag(action);
|
|
479
|
+
if (!flag) {
|
|
480
|
+
this.error(`store_false action '${action.dest}' has no option strings`);
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const literal: Literal = { kind: "literal", attrs: { str: flag } };
|
|
485
|
+
const opt: Optional = { kind: "optional", attrs: { node: literal } };
|
|
486
|
+
|
|
487
|
+
const meta = this.buildNodeMeta(action);
|
|
488
|
+
const flagMeta = meta ?? {};
|
|
489
|
+
if (flagMeta.defaultValue === undefined) flagMeta.defaultValue = true;
|
|
490
|
+
opt.meta = flagMeta;
|
|
491
|
+
|
|
492
|
+
return opt;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private buildStoreConst(action: AdAction): Expr | null {
|
|
496
|
+
const flag = this.getOptionFlag(action);
|
|
497
|
+
if (!flag) {
|
|
498
|
+
this.error(`store_const action '${action.dest}' has no option strings`);
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const literal: Literal = { kind: "literal", attrs: { str: flag } };
|
|
503
|
+
const opt: Optional = { kind: "optional", attrs: { node: literal } };
|
|
504
|
+
|
|
505
|
+
const meta = this.buildNodeMeta(action);
|
|
506
|
+
if (meta) opt.meta = meta;
|
|
507
|
+
|
|
508
|
+
return opt;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private buildBooleanOptional(action: AdAction): Expr | null {
|
|
512
|
+
const optionStrings = action.option_strings;
|
|
513
|
+
if (!isArray(optionStrings) || optionStrings.length === 0) {
|
|
514
|
+
this.error(`boolean_optional action '${action.dest}' has no option strings`);
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Find --flag and --no-flag forms
|
|
519
|
+
let posFlag: string | null = null;
|
|
520
|
+
let negFlag: string | null = null;
|
|
521
|
+
|
|
522
|
+
for (const opt of optionStrings) {
|
|
523
|
+
if (!isString(opt)) continue;
|
|
524
|
+
if (opt.startsWith("--no-")) {
|
|
525
|
+
negFlag = opt;
|
|
526
|
+
} else if (opt.startsWith("--")) {
|
|
527
|
+
posFlag = opt;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (!posFlag) {
|
|
532
|
+
// Fallback: use first two option strings
|
|
533
|
+
posFlag = isString(optionStrings[0]) ? optionStrings[0] : null;
|
|
534
|
+
negFlag = isString(optionStrings[1]) ? optionStrings[1] : null;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (!posFlag) {
|
|
538
|
+
this.error(`boolean_optional action '${action.dest}' has no valid option strings`);
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const posLit: Literal = { kind: "literal", attrs: { str: posFlag } };
|
|
543
|
+
|
|
544
|
+
let innerNode: Expr;
|
|
545
|
+
if (negFlag) {
|
|
546
|
+
const negLit: Literal = { kind: "literal", attrs: { str: negFlag } };
|
|
547
|
+
const alt: Alternative = { kind: "alternative", attrs: { alts: [posLit, negLit] } };
|
|
548
|
+
innerNode = alt;
|
|
549
|
+
} else {
|
|
550
|
+
innerNode = posLit;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const opt: Optional = { kind: "optional", attrs: { node: innerNode } };
|
|
554
|
+
|
|
555
|
+
const meta = this.buildNodeMeta(action);
|
|
556
|
+
const flagMeta = meta ?? {};
|
|
557
|
+
if (flagMeta.defaultValue === undefined) flagMeta.defaultValue = false;
|
|
558
|
+
opt.meta = flagMeta;
|
|
559
|
+
|
|
560
|
+
return opt;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private buildCount(action: AdAction): Expr | null {
|
|
564
|
+
const flag = this.getOptionFlag(action);
|
|
565
|
+
if (!flag) {
|
|
566
|
+
this.error(`count action '${action.dest}' has no option strings`);
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const literal: Literal = { kind: "literal", attrs: { str: flag } };
|
|
571
|
+
const rep: Repeat = { kind: "repeat", attrs: { node: literal, countMin: 0 } };
|
|
572
|
+
|
|
573
|
+
const meta = this.buildNodeMeta(action);
|
|
574
|
+
if (meta) rep.meta = meta;
|
|
575
|
+
|
|
576
|
+
return rep;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
private buildAppendExtend(action: AdAction): Expr | null {
|
|
580
|
+
const terminal = this.resolveTerminal(action);
|
|
581
|
+
if (!terminal) return null;
|
|
582
|
+
|
|
583
|
+
const meta = this.buildNodeMeta(action);
|
|
584
|
+
if (meta) terminal.meta = meta;
|
|
585
|
+
|
|
586
|
+
// Inner value may have nargs
|
|
587
|
+
const nargsWrapped: Expr = this.wrapWithNargs(terminal, action.nargs);
|
|
588
|
+
|
|
589
|
+
// Always wrap in repeat (append/extend accumulates)
|
|
590
|
+
const inner: Expr =
|
|
591
|
+
nargsWrapped.kind === "repeat"
|
|
592
|
+
? nargsWrapped
|
|
593
|
+
: ({ kind: "repeat", attrs: { node: nargsWrapped, countMin: 0 } } satisfies Repeat);
|
|
594
|
+
|
|
595
|
+
if (this.isPositional(action)) {
|
|
596
|
+
// Hoist metadata
|
|
597
|
+
if (inner !== terminal && terminal.meta) {
|
|
598
|
+
const { name, ...rest } = terminal.meta;
|
|
599
|
+
if (Object.keys(rest).length > 0) {
|
|
600
|
+
inner.meta = { ...inner.meta, ...rest };
|
|
601
|
+
terminal.meta = name ? { name } : undefined;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return inner;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Optional argument with flag
|
|
608
|
+
const flag = this.getOptionFlag(action);
|
|
609
|
+
if (!flag) {
|
|
610
|
+
this.error(`append/extend action '${action.dest}' has no option strings`);
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// For append with flag: rep(seq(lit(flag), value))
|
|
615
|
+
// Unwrap the repeat we just added
|
|
616
|
+
const valueNode = inner.kind === "repeat" ? inner.attrs.node : inner;
|
|
617
|
+
const flagLit: Literal = { kind: "literal", attrs: { str: flag } };
|
|
618
|
+
const flagSeq: Sequence = { kind: "sequence", attrs: { nodes: [flagLit, valueNode] } };
|
|
619
|
+
const outerRep: Repeat = { kind: "repeat", attrs: { node: flagSeq, countMin: 0 } };
|
|
620
|
+
|
|
621
|
+
// Hoist metadata
|
|
622
|
+
if (terminal.meta) {
|
|
623
|
+
const { name, ...rest } = terminal.meta;
|
|
624
|
+
if (Object.keys(rest).length > 0) {
|
|
625
|
+
outerRep.meta = { ...outerRep.meta, ...rest };
|
|
626
|
+
terminal.meta = name ? { name } : undefined;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return outerRep;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
private buildAppendConst(action: AdAction): Expr | null {
|
|
634
|
+
const flag = this.getOptionFlag(action);
|
|
635
|
+
if (!flag) {
|
|
636
|
+
this.error(`append_const action '${action.dest}' has no option strings`);
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Similar to count - repeated flag
|
|
641
|
+
const literal: Literal = { kind: "literal", attrs: { str: flag } };
|
|
642
|
+
const rep: Repeat = { kind: "repeat", attrs: { node: literal, countMin: 0 } };
|
|
643
|
+
|
|
644
|
+
const meta = this.buildNodeMeta(action);
|
|
645
|
+
if (meta) rep.meta = meta;
|
|
646
|
+
|
|
647
|
+
return rep;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
private buildSubparsers(action: AdAction): Expr | null {
|
|
651
|
+
const subparsers = action.subparsers;
|
|
652
|
+
if (!isObject(subparsers)) {
|
|
653
|
+
this.error(`parsers action '${action.dest}' has no subparsers`);
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const aliases = isObject(action.subparsers_aliases) ? action.subparsers_aliases : {};
|
|
658
|
+
const alts: Expr[] = [];
|
|
659
|
+
|
|
660
|
+
for (const [name, parserInfo] of Object.entries(subparsers)) {
|
|
661
|
+
if (!isObject(parserInfo)) {
|
|
662
|
+
this.warn(`Skipping non-object subparser '${name}'`);
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const subExpr = this.parseParserInfo(parserInfo);
|
|
667
|
+
if (!subExpr) continue;
|
|
668
|
+
|
|
669
|
+
// Prepend subcommand literal
|
|
670
|
+
const cmdLit: Literal = { kind: "literal", attrs: { str: name } };
|
|
671
|
+
const seq: Sequence = {
|
|
672
|
+
kind: "sequence",
|
|
673
|
+
attrs: { nodes: [cmdLit, ...subExpr.attrs.nodes] },
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
// Attach name and aliases as doc
|
|
677
|
+
const subAliases = aliases[name];
|
|
678
|
+
const aliasDoc =
|
|
679
|
+
isArray(subAliases) && subAliases.length > 0
|
|
680
|
+
? ` (aliases: ${subAliases.filter(isString).join(", ")})`
|
|
681
|
+
: "";
|
|
682
|
+
|
|
683
|
+
const description = isString(parserInfo.description) ? parserInfo.description : undefined;
|
|
684
|
+
const docStr = description
|
|
685
|
+
? description + aliasDoc
|
|
686
|
+
: aliasDoc
|
|
687
|
+
? aliasDoc.slice(1) // Remove leading space
|
|
688
|
+
: undefined;
|
|
689
|
+
|
|
690
|
+
seq.meta = {
|
|
691
|
+
name,
|
|
692
|
+
...(docStr && { doc: { description: docStr } }),
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
alts.push(seq);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (alts.length === 0) {
|
|
699
|
+
this.error(`No valid subparsers for '${action.dest}'`);
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (alts.length === 1) {
|
|
704
|
+
return alts[0]!;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const alt: Alternative = { kind: "alternative", attrs: { alts } };
|
|
708
|
+
|
|
709
|
+
const isRequired = action.subparsers_required === true || action.required === true;
|
|
710
|
+
if (!isRequired) {
|
|
711
|
+
const opt: Optional = { kind: "optional", attrs: { node: alt } };
|
|
712
|
+
|
|
713
|
+
const meta = this.buildNodeMeta(action);
|
|
714
|
+
if (meta) opt.meta = meta;
|
|
715
|
+
|
|
716
|
+
return opt;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const meta = this.buildNodeMeta(action);
|
|
720
|
+
if (meta) alt.meta = meta;
|
|
721
|
+
|
|
722
|
+
return alt;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Mutual exclusion
|
|
726
|
+
|
|
727
|
+
private applyMutualExclusion(
|
|
728
|
+
actionsByDest: Map<string, AdAction>,
|
|
729
|
+
groups: unknown[],
|
|
730
|
+
nodes: Expr[],
|
|
731
|
+
nodesByDest: Map<string, Expr>,
|
|
732
|
+
): Expr[] {
|
|
733
|
+
const excluded = new Set<string>();
|
|
734
|
+
|
|
735
|
+
for (const group of groups) {
|
|
736
|
+
if (!isObject(group)) continue;
|
|
737
|
+
const groupActions = group.actions;
|
|
738
|
+
if (!isArray(groupActions)) continue;
|
|
739
|
+
|
|
740
|
+
const memberDests: string[] = [];
|
|
741
|
+
for (const dest of groupActions) {
|
|
742
|
+
if (isString(dest) && nodesByDest.has(dest)) {
|
|
743
|
+
memberDests.push(dest);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (memberDests.length < 2) continue;
|
|
748
|
+
|
|
749
|
+
// Build alt from member nodes. Unwrapping the optional drops its meta
|
|
750
|
+
// (doc/default), so merge it onto the inner node and tag the inner
|
|
751
|
+
// with the dest so backends can derive a per-variant name.
|
|
752
|
+
const altMembers: Expr[] = [];
|
|
753
|
+
for (const dest of memberDests) {
|
|
754
|
+
const node = nodesByDest.get(dest)!;
|
|
755
|
+
let inner: Expr;
|
|
756
|
+
let outerMeta: NodeMeta | undefined;
|
|
757
|
+
if (node.kind === "optional") {
|
|
758
|
+
inner = node.attrs.node;
|
|
759
|
+
outerMeta = node.meta;
|
|
760
|
+
} else {
|
|
761
|
+
inner = node;
|
|
762
|
+
}
|
|
763
|
+
inner.meta = {
|
|
764
|
+
...outerMeta,
|
|
765
|
+
...inner.meta,
|
|
766
|
+
// Prefer the deepest existing name in the subtree so the synthesized
|
|
767
|
+
// name matches the binding the solver derives for the same node.
|
|
768
|
+
// Otherwise findDeepName short-circuits on the inner's new name and
|
|
769
|
+
// the variant struct's field key drifts from the binding name.
|
|
770
|
+
name: inner.meta?.name ?? findDeepName(inner) ?? dest,
|
|
771
|
+
};
|
|
772
|
+
altMembers.push(inner);
|
|
773
|
+
excluded.add(dest);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const alt: Alternative = { kind: "alternative", attrs: { alts: altMembers } };
|
|
777
|
+
|
|
778
|
+
const isRequired = group.required === true;
|
|
779
|
+
let groupNode: Expr;
|
|
780
|
+
if (isRequired) {
|
|
781
|
+
groupNode = alt;
|
|
782
|
+
} else {
|
|
783
|
+
groupNode = { kind: "optional", attrs: { node: alt } } satisfies Optional;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Synthesize a name so backends can derive a meaningful id instead of
|
|
787
|
+
// a Scope-generated placeholder. Prefer an explicit title if surfaced
|
|
788
|
+
// by argdump, otherwise concat dests for 2-member groups, fall back
|
|
789
|
+
// to a "_choice" suffix for larger groups.
|
|
790
|
+
const title = isString(group.title) ? group.title : undefined;
|
|
791
|
+
const groupName =
|
|
792
|
+
title ??
|
|
793
|
+
(memberDests.length === 2
|
|
794
|
+
? `${memberDests[0]}_or_${memberDests[1]}`
|
|
795
|
+
: `${memberDests[0]}_choice`);
|
|
796
|
+
groupNode.meta = { ...groupNode.meta, name: groupName };
|
|
797
|
+
|
|
798
|
+
// Insert at position of first member
|
|
799
|
+
const firstDest = memberDests[0]!;
|
|
800
|
+
const firstIdx = nodes.findIndex((n) => n === nodesByDest.get(firstDest));
|
|
801
|
+
if (firstIdx >= 0) {
|
|
802
|
+
nodes.splice(firstIdx, 0, groupNode);
|
|
803
|
+
} else {
|
|
804
|
+
nodes.push(groupNode);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Remove excluded nodes
|
|
809
|
+
return nodes.filter((n) => {
|
|
810
|
+
// Find which dest this node corresponds to
|
|
811
|
+
for (const [dest, node] of nodesByDest) {
|
|
812
|
+
if (node === n && excluded.has(dest)) return false;
|
|
813
|
+
}
|
|
814
|
+
return true;
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Main parser
|
|
819
|
+
|
|
820
|
+
private parseParserInfo(descriptor: AdDescriptor): Sequence | null {
|
|
821
|
+
const actions = descriptor.actions;
|
|
822
|
+
if (!isArray(actions)) {
|
|
823
|
+
return { kind: "sequence", attrs: { nodes: [] } };
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const positionals: Expr[] = [];
|
|
827
|
+
const optionals: Expr[] = [];
|
|
828
|
+
const actionsByDest = new Map<string, AdAction>();
|
|
829
|
+
const nodesByDest = new Map<string, Expr>();
|
|
830
|
+
|
|
831
|
+
for (const rawAction of actions) {
|
|
832
|
+
if (!isObject(rawAction)) continue;
|
|
833
|
+
|
|
834
|
+
const node = this.buildAction(rawAction);
|
|
835
|
+
if (!node) continue;
|
|
836
|
+
|
|
837
|
+
const dest = rawAction.dest;
|
|
838
|
+
if (isString(dest)) {
|
|
839
|
+
actionsByDest.set(dest, rawAction);
|
|
840
|
+
nodesByDest.set(dest, node);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (this.isPositional(rawAction) && rawAction.action_type !== "parsers") {
|
|
844
|
+
positionals.push(node);
|
|
845
|
+
} else {
|
|
846
|
+
optionals.push(node);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Assemble: positionals first, then optionals
|
|
851
|
+
let allNodes = [...positionals, ...optionals];
|
|
852
|
+
|
|
853
|
+
// Apply mutual exclusion groups
|
|
854
|
+
const mutexGroups = descriptor.mutually_exclusive_groups;
|
|
855
|
+
if (isArray(mutexGroups) && mutexGroups.length > 0) {
|
|
856
|
+
allNodes = this.applyMutualExclusion(actionsByDest, mutexGroups, allNodes, nodesByDest);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
return { kind: "sequence", attrs: { nodes: allNodes } };
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Public API
|
|
863
|
+
|
|
864
|
+
parse(source: string, _filename?: string): ParseResult {
|
|
865
|
+
this.reset();
|
|
866
|
+
|
|
867
|
+
const descriptor = this.parseJSON(source);
|
|
868
|
+
if (descriptor === null) {
|
|
869
|
+
return {
|
|
870
|
+
expr: { kind: "sequence", attrs: { nodes: [] } },
|
|
871
|
+
errors: this.errors,
|
|
872
|
+
warnings: this.warnings,
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const meta = this.buildAppMeta(descriptor);
|
|
877
|
+
if (!meta) {
|
|
878
|
+
this.error("Descriptor is missing prog");
|
|
879
|
+
return {
|
|
880
|
+
expr: { kind: "sequence", attrs: { nodes: [] } },
|
|
881
|
+
errors: this.errors,
|
|
882
|
+
warnings: this.warnings,
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const expr = this.parseParserInfo(descriptor);
|
|
887
|
+
if (expr === null) {
|
|
888
|
+
this.error("Failed to parse argument structure");
|
|
889
|
+
return {
|
|
890
|
+
expr: { kind: "sequence", attrs: { nodes: [] } },
|
|
891
|
+
errors: this.errors,
|
|
892
|
+
warnings: this.warnings,
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Prepend prog as command literal (like Boutiques' command-line prefix)
|
|
897
|
+
const prog = descriptor.prog;
|
|
898
|
+
if (isString(prog) && prog) {
|
|
899
|
+
expr.attrs.nodes.unshift({ kind: "literal", attrs: { str: prog } });
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Set root struct name
|
|
903
|
+
if (!expr.meta?.name && meta.id) {
|
|
904
|
+
expr.meta = { ...expr.meta, name: meta.id };
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
return {
|
|
908
|
+
meta,
|
|
909
|
+
expr,
|
|
910
|
+
errors: this.errors,
|
|
911
|
+
warnings: this.warnings,
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
}
|