@styx-api/core 0.6.1 → 0.8.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 +2559 -553
- package/dist/index.d.cts +41 -37
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +41 -37
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2558 -549
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/backend/argtype/__fixtures__/microsyntax.argtype +13 -0
- package/src/backend/argtype/__fixtures__/subcommands.argtype +25 -0
- package/src/backend/argtype/argtype.ts +45 -0
- package/src/backend/argtype/emit.ts +676 -0
- package/src/backend/argtype/index.ts +2 -0
- package/src/backend/boutiques/boutiques.ts +55 -47
- package/src/backend/field-defaults.ts +40 -0
- package/src/backend/find-struct-node.ts +7 -0
- package/src/backend/index.ts +2 -9
- package/src/backend/nipype/emit.ts +11 -3
- package/src/backend/python/arg-builder.ts +2 -27
- package/src/backend/python/emit.ts +18 -6
- package/src/backend/python/outputs-emit.ts +27 -42
- package/src/backend/python/python.ts +46 -22
- package/src/backend/python/validate-emit.ts +4 -1
- package/src/backend/resolve-output-tokens.ts +0 -76
- package/src/backend/typescript/arg-builder.ts +2 -24
- package/src/backend/typescript/emit.ts +7 -4
- package/src/backend/typescript/outputs-emit.ts +15 -39
- package/src/backend/typescript/typemap.ts +9 -1
- package/src/backend/typescript/typescript.ts +41 -18
- package/src/backend/typescript/validate-emit.ts +4 -1
- package/src/backend/union-variants.ts +41 -1
- package/src/frontend/argdump/parser.ts +20 -19
- package/src/frontend/argtype/__fixtures__/afni-3dtstat.argtype +41 -0
- package/src/frontend/argtype/__fixtures__/bet.argtype +101 -0
- package/src/frontend/argtype/__fixtures__/fsl-flirt.argtype +50 -0
- package/src/frontend/argtype/__fixtures__/wb-command-sub.argtype +39 -0
- package/src/frontend/argtype/ast.ts +114 -0
- package/src/frontend/argtype/doc.ts +56 -0
- package/src/frontend/argtype/frontmatter.ts +237 -0
- package/src/frontend/argtype/index.ts +1 -0
- package/src/frontend/argtype/lexer.ts +264 -0
- package/src/frontend/argtype/lower.ts +601 -0
- package/src/frontend/argtype/parser-frontend.ts +51 -0
- package/src/frontend/argtype/parser.ts +533 -0
- package/src/frontend/argtype/template.ts +147 -0
- package/src/frontend/boutiques/destruct-template.ts +24 -17
- package/src/frontend/boutiques/parser.ts +27 -8
- package/src/frontend/detect-format.ts +28 -3
- package/src/index.ts +23 -9
- package/src/ir/passes/canonicalize.ts +18 -5
- package/src/ir/passes/simplify.ts +17 -2
- package/src/solver/solver.ts +2 -2
|
@@ -1,80 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
BindingRegistry,
|
|
3
|
-
GateAtom,
|
|
4
|
-
OutputScope,
|
|
5
|
-
ResolvedOutput,
|
|
6
|
-
ResolvedToken,
|
|
7
|
-
} from "../bindings/index.js";
|
|
8
1
|
import { outputGate } from "../bindings/index.js";
|
|
9
2
|
|
|
10
3
|
// Re-export the core helper so backends have a single entry point.
|
|
11
4
|
export { outputGate };
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Compact consecutive literal tokens. Backends that emit string concatenation
|
|
15
|
-
* benefit from a shorter token stream; backends that template each token
|
|
16
|
-
* individually can ignore this and use `output.tokens` directly.
|
|
17
|
-
*/
|
|
18
|
-
export function compactTokens(tokens: ResolvedToken[]): ResolvedToken[] {
|
|
19
|
-
const out: ResolvedToken[] = [];
|
|
20
|
-
for (const tok of tokens) {
|
|
21
|
-
const last = out[out.length - 1];
|
|
22
|
-
if (tok.kind === "literal" && last && last.kind === "literal") {
|
|
23
|
-
out[out.length - 1] = { kind: "literal", value: last.value + tok.value };
|
|
24
|
-
} else {
|
|
25
|
-
out.push(tok);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return out;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* One output ready for codegen. `gate` is the wrapper sequence (outermost
|
|
33
|
-
* first); the backend renders each atom as the appropriate scope-introducing
|
|
34
|
-
* statement, then emits the path expression inside the innermost layer.
|
|
35
|
-
*/
|
|
36
|
-
export interface OutputEmitPlan {
|
|
37
|
-
name: string;
|
|
38
|
-
gate: GateAtom[];
|
|
39
|
-
tokens: ResolvedToken[];
|
|
40
|
-
resolved: ResolvedOutput;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function planOutput(
|
|
44
|
-
scopeGate: GateAtom[],
|
|
45
|
-
output: ResolvedOutput,
|
|
46
|
-
bindings: BindingRegistry,
|
|
47
|
-
): OutputEmitPlan {
|
|
48
|
-
return {
|
|
49
|
-
name: output.name,
|
|
50
|
-
gate: outputGate(scopeGate, output, bindings),
|
|
51
|
-
tokens: compactTokens(output.tokens),
|
|
52
|
-
resolved: output,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Does the output have any conditional wrapper? Equivalent to "is at least one
|
|
58
|
-
* atom a `present` or `variant`?". `iter` alone means the output emits a list
|
|
59
|
-
* and is not conditionally absent.
|
|
60
|
-
*/
|
|
61
|
-
export function isGated(plan: OutputEmitPlan): boolean {
|
|
62
|
-
return plan.gate.some((a) => a.kind === "present" || a.kind === "variant");
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Does the output iterate (emit zero-or-more values)? */
|
|
66
|
-
export function isIterated(plan: OutputEmitPlan): boolean {
|
|
67
|
-
return plan.gate.some((a) => a.kind === "iter");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Convenience for backends emitting all outputs of a scope at once. The caller
|
|
72
|
-
* provides the scope's gate (typically `bindings.get(scope.scope)?.gate ?? []`).
|
|
73
|
-
*/
|
|
74
|
-
export function planScope(
|
|
75
|
-
scope: OutputScope,
|
|
76
|
-
scopeGate: GateAtom[],
|
|
77
|
-
bindings: BindingRegistry,
|
|
78
|
-
): OutputEmitPlan[] {
|
|
79
|
-
return scope.outputs.map((output) => planOutput(scopeGate, output, bindings));
|
|
80
|
-
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Binding, BindingId, BoundType, BoundVariant } from "../../bindings/index.js";
|
|
2
|
-
import {
|
|
2
|
+
import { collectDefaults, rootFieldDefault } from "../field-defaults.js";
|
|
3
3
|
import type { Expr } from "../../ir/index.js";
|
|
4
4
|
import type { CodegenContext } from "../../manifest/index.js";
|
|
5
5
|
import { CodeBuilder } from "../code-builder.js";
|
|
@@ -92,14 +92,6 @@ function accessOf(binding: Binding, arg: ArgContext): string {
|
|
|
92
92
|
* carrying a Boutiques default. Restricted to single-segment (root) field access
|
|
93
93
|
* so a nested field can never accidentally pick up a same-named root default.
|
|
94
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
95
|
|
|
104
96
|
/**
|
|
105
97
|
* Render a binding's access for an UNCONDITIONAL value read (terminal, repeat
|
|
@@ -113,20 +105,6 @@ function readAccess(binding: Binding, arg: ArgContext): string {
|
|
|
113
105
|
return def !== undefined ? `(${accessOf(binding, arg)} ?? ${def})` : accessOf(binding, arg);
|
|
114
106
|
}
|
|
115
107
|
|
|
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
108
|
// -- Recursive descent --
|
|
131
109
|
|
|
132
110
|
let loopVarCounter = 0;
|
|
@@ -142,7 +120,7 @@ export function buildArgs(rootExpr: Expr, ctx: CodegenContext, rootType?: BoundT
|
|
|
142
120
|
const initialCtx: ArgContext = {
|
|
143
121
|
joinDepth: 0,
|
|
144
122
|
loopVars: new Map(),
|
|
145
|
-
defaults: collectDefaults(ctx, rootType),
|
|
123
|
+
defaults: collectDefaults(ctx, renderTsLiteral, rootType),
|
|
146
124
|
};
|
|
147
125
|
return walk(rootExpr, ctx, initialCtx);
|
|
148
126
|
}
|
|
@@ -10,7 +10,10 @@ import { collectFieldInfo, resolveTypeName } from "./types.js";
|
|
|
10
10
|
|
|
11
11
|
export function emitJsDoc(cb: CodeBuilder, description?: string): void {
|
|
12
12
|
if (!description) return;
|
|
13
|
-
|
|
13
|
+
// Escape `*/` so a description containing it can't terminate the block comment
|
|
14
|
+
// early (`*\/` is not a comment terminator; the backslash is inert in comments).
|
|
15
|
+
const safe = description.replace(/\*\//g, "*\\/");
|
|
16
|
+
const lines = safe.split("\n");
|
|
14
17
|
if (lines.length === 1) {
|
|
15
18
|
cb.line(`/** ${lines[0]} */`);
|
|
16
19
|
} else {
|
|
@@ -22,9 +25,9 @@ export function emitJsDoc(cb: CodeBuilder, description?: string): void {
|
|
|
22
25
|
}
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
export function emitImports(cb: CodeBuilder
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
export function emitImports(cb: CodeBuilder): void {
|
|
29
|
+
// Every tool emits an Outputs object, so OutputPathType is always needed.
|
|
30
|
+
const inputs = ["Runner", "Execution", "Metadata", "InputPathType", "OutputPathType"];
|
|
28
31
|
cb.line(`import type { ${inputs.join(", ")} } from "styxdefs";`);
|
|
29
32
|
cb.line('import { getGlobalRunner, StyxValidationError } from "styxdefs";');
|
|
30
33
|
}
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Binding,
|
|
3
|
-
BindingId,
|
|
4
|
-
BoundType,
|
|
5
|
-
GateAtom,
|
|
6
|
-
ResolvedToken,
|
|
7
|
-
} from "../../bindings/index.js";
|
|
1
|
+
import type { BindingId, BoundType, GateAtom, ResolvedToken } from "../../bindings/index.js";
|
|
8
2
|
import { outputGate } from "../../bindings/index.js";
|
|
9
|
-
import {
|
|
3
|
+
import { collectDefaults, rootFieldDefault } from "../field-defaults.js";
|
|
10
4
|
import type { CodegenContext } from "../../manifest/index.js";
|
|
11
5
|
import { CodeBuilder } from "../code-builder.js";
|
|
12
6
|
import {
|
|
@@ -18,6 +12,7 @@ import {
|
|
|
18
12
|
rootOutput,
|
|
19
13
|
streamFields,
|
|
20
14
|
} from "../collect-output-fields.js";
|
|
15
|
+
import { unionIsMixed, variantAtomUnion } from "../union-variants.js";
|
|
21
16
|
import { emitJsDoc, renderAccess, tsPropAccess } from "./emit.js";
|
|
22
17
|
import { renderTsLiteral } from "./typemap.js";
|
|
23
18
|
|
|
@@ -97,32 +92,6 @@ interface OutputEmitCtx {
|
|
|
97
92
|
defaults: ReadonlyMap<string, string>;
|
|
98
93
|
}
|
|
99
94
|
|
|
100
|
-
/** The rendered default for a binding iff it is a root-level defaulted field. */
|
|
101
|
-
function rootFieldDefault(
|
|
102
|
-
binding: Binding | undefined,
|
|
103
|
-
defaults: ReadonlyMap<string, string>,
|
|
104
|
-
): string | undefined {
|
|
105
|
-
if (!binding) return undefined;
|
|
106
|
-
const a = binding.access;
|
|
107
|
-
if (a.length === 1 && a[0]?.kind === "field") return defaults.get(binding.name);
|
|
108
|
-
return undefined;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Build the field-name -> rendered-default map for the struct root (else empty).
|
|
112
|
-
* Includes only non-optional defaulted fields (optional fields are
|
|
113
|
-
* presence-guarded; their default comes from the factory's kwarg signature). */
|
|
114
|
-
function collectDefaults(ctx: CodegenContext): Map<string, string> {
|
|
115
|
-
const out = new Map<string, string>();
|
|
116
|
-
const rootType = ctx.resolve(ctx.expr)?.type;
|
|
117
|
-
if (rootType?.kind !== "struct") return out;
|
|
118
|
-
for (const [name, fi] of collectFieldInfo(ctx, rootType)) {
|
|
119
|
-
if (fi.defaultValue === undefined) continue;
|
|
120
|
-
if (rootType.fields[name]?.kind === "optional") continue;
|
|
121
|
-
out.set(name, renderTsLiteral(fi.defaultValue));
|
|
122
|
-
}
|
|
123
|
-
return out;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
95
|
/**
|
|
127
96
|
* Render one output's wrapper stack and emit the assignment inside the
|
|
128
97
|
* innermost wrapper. Nesting is done via recursive callbacks so the
|
|
@@ -201,10 +170,17 @@ function renderWrapperOpen(atom: GateAtom, ec: OutputEmitCtx): WrapperRender {
|
|
|
201
170
|
}
|
|
202
171
|
if (atom.kind === "variant") {
|
|
203
172
|
const access = bindingAccess(atom.binding, ec);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
173
|
+
const check = `${access}["@type"] === ${JSON.stringify(atom.variant)}`;
|
|
174
|
+
// A mixed union (`0 | Struct`) only carries `@type` on its struct arms; guard
|
|
175
|
+
// on the object shape first so TS narrows off the bare-literal arm before the
|
|
176
|
+
// subscript (which would otherwise be a "property does not exist" error).
|
|
177
|
+
// Mirrors the cargs builder and validator. Pure-struct unions need no guard.
|
|
178
|
+
const union = variantAtomUnion(atom, ec.ctx.bindings);
|
|
179
|
+
const cond =
|
|
180
|
+
union && unionIsMixed(union)
|
|
181
|
+
? `typeof ${access} === "object" && ${access} !== null && ${check}`
|
|
182
|
+
: check;
|
|
183
|
+
return { open: `if (${cond}) {`, close: `}` };
|
|
208
184
|
}
|
|
209
185
|
// present
|
|
210
186
|
const binding = ec.ctx.bindings.get(atom.binding);
|
|
@@ -321,7 +297,7 @@ export function emitBuildOutputs(
|
|
|
321
297
|
ctx,
|
|
322
298
|
iter: new Map(),
|
|
323
299
|
fieldShapes: new Map(fields.map((f) => [f.id, f.shape])),
|
|
324
|
-
defaults: collectDefaults(ctx),
|
|
300
|
+
defaults: collectDefaults(ctx, renderTsLiteral),
|
|
325
301
|
};
|
|
326
302
|
|
|
327
303
|
// Initialize the outputs object with defaults so wrapper code can assign or
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { BoundType } from "../../bindings/index.js";
|
|
2
2
|
|
|
3
|
+
// A valid TS identifier can be an unquoted object key; anything else (e.g. a
|
|
4
|
+
// wire key like `4d_input`) must be quoted. Kept local to avoid a cycle with
|
|
5
|
+
// emit.ts (which imports this module).
|
|
6
|
+
const TS_IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
7
|
+
function objKey(key: string): string {
|
|
8
|
+
return TS_IDENT_RE.test(key) ? key : JSON.stringify(key);
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
export function mapType(type: BoundType, resolve: (type: BoundType) => string | undefined): string {
|
|
4
12
|
switch (type.kind) {
|
|
5
13
|
case "scalar":
|
|
@@ -27,7 +35,7 @@ export function mapType(type: BoundType, resolve: (type: BoundType) => string |
|
|
|
27
35
|
if (name) return name;
|
|
28
36
|
const fields = Object.entries(type.fields)
|
|
29
37
|
.filter(([, v]) => v.kind !== "literal")
|
|
30
|
-
.map(([k, v]) => `${k}: ${mapType(v, resolve)}`)
|
|
38
|
+
.map(([k, v]) => `${objKey(k)}: ${mapType(v, resolve)}`)
|
|
31
39
|
.join("; ");
|
|
32
40
|
return `{ ${fields} }`;
|
|
33
41
|
}
|
|
@@ -256,6 +256,19 @@ export function buildEmitModel(
|
|
|
256
256
|
}
|
|
257
257
|
|
|
258
258
|
export function generateTypeScript(ctx: CodegenContext, packageScope?: Scope): string {
|
|
259
|
+
return generateTypeScriptModule(ctx, packageScope).code;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Emit the module and, alongside it, the dispatch entrypoint carrying the
|
|
264
|
+
* *scope-registered* execute-function name. Computing the entrypoint here (not
|
|
265
|
+
* via the scope-blind `appEntrypoint`) keeps the suite dispatcher in sync with
|
|
266
|
+
* the actual emitted symbol when a shared package scope suffix-bumps a collision.
|
|
267
|
+
*/
|
|
268
|
+
function generateTypeScriptModule(
|
|
269
|
+
ctx: CodegenContext,
|
|
270
|
+
packageScope?: Scope,
|
|
271
|
+
): { code: string; entrypoint: AppEntrypoint | undefined } {
|
|
259
272
|
const cb = new CodeBuilder(" ");
|
|
260
273
|
// A package-shared scope keeps top-level names unique across every tool in the
|
|
261
274
|
// suite barrel; without one (standalone emit) a per-tool scope is enough.
|
|
@@ -282,9 +295,7 @@ export function generateTypeScript(ctx: CodegenContext, packageScope?: Scope): s
|
|
|
282
295
|
// Every tool emits an Outputs object: at minimum the synthetic `root` output
|
|
283
296
|
// directory (outputFile(".")), plus any declared file/mutable outputs and
|
|
284
297
|
// stdout/stderr stream fields. OutputPathType is therefore always imported.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
emitImports(cb, true);
|
|
298
|
+
emitImports(cb);
|
|
288
299
|
cb.blank();
|
|
289
300
|
|
|
290
301
|
emitMetadata(ctx, names.metadata, cb);
|
|
@@ -292,12 +303,10 @@ export function generateTypeScript(ctx: CodegenContext, packageScope?: Scope): s
|
|
|
292
303
|
|
|
293
304
|
emitTypeDeclarations(typeDecls, namedTypes, ctx, names.params, appId, pkg, cb);
|
|
294
305
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
cb.blank();
|
|
298
|
-
}
|
|
306
|
+
emitOutputsInterface(ctx, names.outputs, cb);
|
|
307
|
+
cb.blank();
|
|
299
308
|
|
|
300
|
-
if (
|
|
309
|
+
if (needsStripExtensionsHelper(ctx)) {
|
|
301
310
|
emitStripExtensionsHelper(cb);
|
|
302
311
|
cb.blank();
|
|
303
312
|
}
|
|
@@ -327,10 +336,8 @@ export function generateTypeScript(ctx: CodegenContext, packageScope?: Scope): s
|
|
|
327
336
|
emitBuildCargs(ctx, rootType, paramsType, names.cargs, cb);
|
|
328
337
|
cb.blank();
|
|
329
338
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
cb.blank();
|
|
333
|
-
}
|
|
339
|
+
emitBuildOutputs(ctx, paramsType, names.outputs, names.outputsFn, cb);
|
|
340
|
+
cb.blank();
|
|
334
341
|
|
|
335
342
|
// Dict-style execute function. For struct roots it's the internal
|
|
336
343
|
// `<tool>Execute`; for other roots it doubles as the user-facing wrapper.
|
|
@@ -341,8 +348,8 @@ export function generateTypeScript(ctx: CodegenContext, packageScope?: Scope): s
|
|
|
341
348
|
executeName,
|
|
342
349
|
names.metadata,
|
|
343
350
|
names.cargs,
|
|
344
|
-
|
|
345
|
-
|
|
351
|
+
names.outputsFn,
|
|
352
|
+
names.outputs,
|
|
346
353
|
names.validate,
|
|
347
354
|
streamFieldIds(ctx),
|
|
348
355
|
cb,
|
|
@@ -357,13 +364,24 @@ export function generateTypeScript(ctx: CodegenContext, packageScope?: Scope): s
|
|
|
357
364
|
names.wrapper,
|
|
358
365
|
names.paramsFn,
|
|
359
366
|
names.execute,
|
|
360
|
-
|
|
367
|
+
names.outputs,
|
|
361
368
|
cb,
|
|
362
369
|
);
|
|
363
370
|
cb.blank();
|
|
364
371
|
}
|
|
365
372
|
|
|
366
|
-
|
|
373
|
+
// `executeName` is the scope-registered symbol actually emitted above, so the
|
|
374
|
+
// dispatcher references the real name even after a collision suffix-bump. Guard
|
|
375
|
+
// on `ctx.package?.name` (not the destructured `pkg`, which falls back to
|
|
376
|
+
// "unknown") so a package-less app yields no entrypoint, as before.
|
|
377
|
+
const entryAppId = ctx.app?.id;
|
|
378
|
+
const entryPkg = ctx.package?.name;
|
|
379
|
+
const entrypoint: AppEntrypoint | undefined =
|
|
380
|
+
entryAppId && entryPkg
|
|
381
|
+
? { type: `${entryPkg}/${entryAppId}`, executeFn: executeName }
|
|
382
|
+
: undefined;
|
|
383
|
+
|
|
384
|
+
return { code: cb.toString(), entrypoint };
|
|
367
385
|
}
|
|
368
386
|
|
|
369
387
|
/**
|
|
@@ -381,6 +399,9 @@ export function appModuleName(meta: AppMeta | undefined): string {
|
|
|
381
399
|
* the dict-style execute function name. Returns undefined when the id or package
|
|
382
400
|
* is unknown (no stable `@type`), so the app is left out of the suite dispatcher.
|
|
383
401
|
*/
|
|
402
|
+
// Note: this recomputes the execute name without a package scope, so it reflects
|
|
403
|
+
// the un-bumped name. The emitter (`emitApp`) instead takes the entrypoint from
|
|
404
|
+
// the scope-aware emit pass; use that path when a shared package scope is in play.
|
|
384
405
|
export function appEntrypoint(ctx: CodegenContext): AppEntrypoint | undefined {
|
|
385
406
|
const appId = ctx.app?.id;
|
|
386
407
|
const pkg = ctx.package?.name;
|
|
@@ -469,11 +490,13 @@ export class TypeScriptBackend implements Backend {
|
|
|
469
490
|
readonly target = "typescript";
|
|
470
491
|
|
|
471
492
|
emitApp(ctx: CodegenContext, scope?: Scope): EmittedApp {
|
|
472
|
-
|
|
493
|
+
// Take the entrypoint from the same pass that emitted the module, so its
|
|
494
|
+
// execute-function name reflects any shared-scope suffix-bump.
|
|
495
|
+
const { code, entrypoint } = generateTypeScriptModule(ctx, scope);
|
|
473
496
|
const fileName = `${appModuleName(ctx.app)}.ts`;
|
|
474
497
|
return {
|
|
475
498
|
meta: ctx.app,
|
|
476
|
-
entrypoint
|
|
499
|
+
entrypoint,
|
|
477
500
|
files: new Map([[fileName, code]]),
|
|
478
501
|
errors: [],
|
|
479
502
|
warnings: [],
|
|
@@ -136,7 +136,10 @@ function emitValue(
|
|
|
136
136
|
const itemNode = findRepeatNode(node)?.attrs.node;
|
|
137
137
|
const elem = e.scope.add("el");
|
|
138
138
|
e.cb.line(`for (const ${elem} of ${access}) {`);
|
|
139
|
-
|
|
139
|
+
// Report the element type (e.g. `int`), not the list type (`int[]`).
|
|
140
|
+
e.cb.indent(() =>
|
|
141
|
+
emitValue(e, type.item, itemNode, wireKey, elem, expectedType(e, type.item)),
|
|
142
|
+
);
|
|
140
143
|
e.cb.line("}");
|
|
141
144
|
return;
|
|
142
145
|
}
|
|
@@ -1,4 +1,44 @@
|
|
|
1
|
-
import type { BoundType, BoundVariant } from "../bindings/index.js";
|
|
1
|
+
import type { BindingRegistry, BoundType, BoundVariant, GateAtom } from "../bindings/index.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether a union is "mixed": it has at least one non-struct (bare-literal) arm
|
|
5
|
+
* alongside its struct variants (e.g. ants `n4_correction = Literal[0] | N4On`).
|
|
6
|
+
*
|
|
7
|
+
* The `@type` discriminator only exists on the struct arms, so indexing
|
|
8
|
+
* `value["@type"]` on the union value is unsound (a type error, and a runtime
|
|
9
|
+
* KeyError if the literal arm is ever hit) until the value is narrowed to a
|
|
10
|
+
* struct at runtime. Backends that dispatch on `@type` must first emit a shape
|
|
11
|
+
* guard (Python `isinstance(value, dict)`, TS `typeof value === "object"`) when
|
|
12
|
+
* this is true; a pure-struct union (every arm a struct) needs no guard.
|
|
13
|
+
*/
|
|
14
|
+
export function unionIsMixed(unionType: Extract<BoundType, { kind: "union" }>): boolean {
|
|
15
|
+
return unionType.variants.some((v) => v.type.kind !== "struct");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The union type bound by a `variant` gate atom, if the atom's binding resolves
|
|
20
|
+
* to a union - used by the outputs emitters to decide whether the variant gate
|
|
21
|
+
* needs a mixed-union shape guard before its `@type` check. Returns `undefined`
|
|
22
|
+
* for a well-formed non-union (defensive; a variant atom should always name a
|
|
23
|
+
* union binding).
|
|
24
|
+
*/
|
|
25
|
+
export function variantAtomUnion(
|
|
26
|
+
atom: Extract<GateAtom, { kind: "variant" }>,
|
|
27
|
+
bindings: BindingRegistry,
|
|
28
|
+
): Extract<BoundType, { kind: "union" }> | undefined {
|
|
29
|
+
return resolveUnion(bindings.get(atom.binding)?.type);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveUnion(
|
|
33
|
+
type: BoundType | undefined,
|
|
34
|
+
): Extract<BoundType, { kind: "union" }> | undefined {
|
|
35
|
+
if (!type) return undefined;
|
|
36
|
+
// An `optional<union>` binding can carry a variant gate (the optional wrapper
|
|
37
|
+
// and the union collapse onto one access path); unwrap to reach the union.
|
|
38
|
+
if (type.kind === "optional") return resolveUnion(type.inner);
|
|
39
|
+
if (type.kind === "union") return type;
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
2
42
|
|
|
3
43
|
/** A union's struct variant paired with its index into the union's `variants`
|
|
4
44
|
* array (which stays parallel to the IR `alts` and the per-arm results a caller
|
|
@@ -270,10 +270,12 @@ export class ArgdumpParser implements Frontend {
|
|
|
270
270
|
|
|
271
271
|
if (isArgparseMarker(nargs)) {
|
|
272
272
|
if (nargs.__argparse__ === "REMAINDER") {
|
|
273
|
-
// REMAINDER -> rep(
|
|
273
|
+
// REMAINDER -> rep(node), preserving the terminal's meta (name/doc/
|
|
274
|
+
// default) so the positional's `dest` name survives instead of being
|
|
275
|
+
// dropped for a solver-derived placeholder.
|
|
274
276
|
const rep: Repeat = {
|
|
275
277
|
kind: "repeat",
|
|
276
|
-
attrs: { node
|
|
278
|
+
attrs: { node, countMin: 0 },
|
|
277
279
|
};
|
|
278
280
|
return rep;
|
|
279
281
|
}
|
|
@@ -700,32 +702,33 @@ export class ArgdumpParser implements Frontend {
|
|
|
700
702
|
return null;
|
|
701
703
|
}
|
|
702
704
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
705
|
+
// A single choice needs no `alternative` wrapper, but must still honor the
|
|
706
|
+
// subparsers' required-ness (a lone-choice non-required subparser is still
|
|
707
|
+
// optional) rather than being returned bare as a mandatory node.
|
|
708
|
+
const node: Expr =
|
|
709
|
+
alts.length === 1
|
|
710
|
+
? alts[0]!
|
|
711
|
+
: ({ kind: "alternative", attrs: { alts } } satisfies Alternative);
|
|
708
712
|
|
|
709
713
|
const isRequired = action.subparsers_required === true || action.required === true;
|
|
710
|
-
|
|
711
|
-
const opt: Optional = { kind: "optional", attrs: { node: alt } };
|
|
714
|
+
const meta = this.buildNodeMeta(action);
|
|
712
715
|
|
|
713
|
-
|
|
716
|
+
if (!isRequired) {
|
|
717
|
+
const opt: Optional = { kind: "optional", attrs: { node } };
|
|
714
718
|
if (meta) opt.meta = meta;
|
|
715
|
-
|
|
716
719
|
return opt;
|
|
717
720
|
}
|
|
718
721
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
722
|
+
// Required: hang the subparsers meta on the alternative wrapper when there is
|
|
723
|
+
// one; a single bare arm keeps its own sub-command meta instead of having it
|
|
724
|
+
// clobbered by the action's dest name.
|
|
725
|
+
if (node.kind === "alternative" && meta) node.meta = meta;
|
|
726
|
+
return node;
|
|
723
727
|
}
|
|
724
728
|
|
|
725
729
|
// Mutual exclusion
|
|
726
730
|
|
|
727
731
|
private applyMutualExclusion(
|
|
728
|
-
actionsByDest: Map<string, AdAction>,
|
|
729
732
|
groups: unknown[],
|
|
730
733
|
nodes: Expr[],
|
|
731
734
|
nodesByDest: Map<string, Expr>,
|
|
@@ -825,7 +828,6 @@ export class ArgdumpParser implements Frontend {
|
|
|
825
828
|
|
|
826
829
|
const positionals: Expr[] = [];
|
|
827
830
|
const optionals: Expr[] = [];
|
|
828
|
-
const actionsByDest = new Map<string, AdAction>();
|
|
829
831
|
const nodesByDest = new Map<string, Expr>();
|
|
830
832
|
|
|
831
833
|
for (const rawAction of actions) {
|
|
@@ -836,7 +838,6 @@ export class ArgdumpParser implements Frontend {
|
|
|
836
838
|
|
|
837
839
|
const dest = rawAction.dest;
|
|
838
840
|
if (isString(dest)) {
|
|
839
|
-
actionsByDest.set(dest, rawAction);
|
|
840
841
|
nodesByDest.set(dest, node);
|
|
841
842
|
}
|
|
842
843
|
|
|
@@ -853,7 +854,7 @@ export class ArgdumpParser implements Frontend {
|
|
|
853
854
|
// Apply mutual exclusion groups
|
|
854
855
|
const mutexGroups = descriptor.mutually_exclusive_groups;
|
|
855
856
|
if (isArray(mutexGroups) && mutexGroups.length > 0) {
|
|
856
|
-
allNodes = this.applyMutualExclusion(
|
|
857
|
+
allNodes = this.applyMutualExclusion(mutexGroups, allNodes, nodesByDest);
|
|
857
858
|
}
|
|
858
859
|
|
|
859
860
|
return { kind: "sequence", attrs: { nodes: allNodes } };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
exe: "3dTstat"
|
|
3
|
+
version: "24.2.06"
|
|
4
|
+
authors:
|
|
5
|
+
- "RW Cox"
|
|
6
|
+
urls:
|
|
7
|
+
- "https://afni.nimh.nih.gov/"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
/// Compute voxelwise statistics across the time axis of a 3D+time dataset.
|
|
11
|
+
///
|
|
12
|
+
/// All of AFNI is named with a leading digit (3d*, 1d*, 2d*), so the tool
|
|
13
|
+
/// label has to be quoted - a bare identifier cannot start with a digit.
|
|
14
|
+
"3dTstat": seq(
|
|
15
|
+
set(
|
|
16
|
+
/// Output dataset prefix (the view and extension are added automatically).
|
|
17
|
+
opt("-prefix", prefix: str = "stat"),
|
|
18
|
+
|
|
19
|
+
/// Compute the mean across time (the default when no statistic is given).
|
|
20
|
+
mean: opt("-mean"),
|
|
21
|
+
|
|
22
|
+
/// Compute the standard deviation across time.
|
|
23
|
+
stdev: opt("-stdev"),
|
|
24
|
+
|
|
25
|
+
/// Compute the median across time.
|
|
26
|
+
median: opt("-median"),
|
|
27
|
+
|
|
28
|
+
/// Percentiles to compute, emitted as a single colon-joined argument
|
|
29
|
+
/// (for example `25:50:75`).
|
|
30
|
+
opt("-percentile", percentiles: rep(int.min(0).max(100)).join(":")),
|
|
31
|
+
|
|
32
|
+
/// Number of leading time points to ignore.
|
|
33
|
+
opt("-ignore", ignore_count: int.min(0)),
|
|
34
|
+
),
|
|
35
|
+
|
|
36
|
+
/// Input 3D+time dataset.
|
|
37
|
+
input: path,
|
|
38
|
+
).output(
|
|
39
|
+
/// The statistics dataset header written by `-prefix`.
|
|
40
|
+
statfile: `{prefix}+orig.HEAD`,
|
|
41
|
+
)
|