@styx-api/core 0.7.0 → 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.
Files changed (44) hide show
  1. package/dist/index.cjs +430 -356
  2. package/dist/index.d.cts +1 -34
  3. package/dist/index.d.cts.map +1 -1
  4. package/dist/index.d.mts +1 -34
  5. package/dist/index.d.mts.map +1 -1
  6. package/dist/index.mjs +431 -352
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +2 -1
  9. package/src/backend/argtype/__fixtures__/microsyntax.argtype +0 -2
  10. package/src/backend/argtype/emit.ts +2 -51
  11. package/src/backend/boutiques/boutiques.ts +55 -47
  12. package/src/backend/field-defaults.ts +40 -0
  13. package/src/backend/find-struct-node.ts +7 -0
  14. package/src/backend/index.ts +1 -9
  15. package/src/backend/nipype/emit.ts +11 -3
  16. package/src/backend/python/arg-builder.ts +2 -27
  17. package/src/backend/python/emit.ts +18 -6
  18. package/src/backend/python/outputs-emit.ts +27 -42
  19. package/src/backend/python/python.ts +46 -22
  20. package/src/backend/python/validate-emit.ts +4 -1
  21. package/src/backend/resolve-output-tokens.ts +0 -76
  22. package/src/backend/typescript/arg-builder.ts +2 -24
  23. package/src/backend/typescript/emit.ts +7 -4
  24. package/src/backend/typescript/outputs-emit.ts +15 -39
  25. package/src/backend/typescript/typemap.ts +9 -1
  26. package/src/backend/typescript/typescript.ts +41 -18
  27. package/src/backend/typescript/validate-emit.ts +4 -1
  28. package/src/backend/union-variants.ts +41 -1
  29. package/src/frontend/argdump/parser.ts +20 -19
  30. package/src/frontend/argtype/__fixtures__/afni-3dtstat.argtype +41 -0
  31. package/src/frontend/argtype/__fixtures__/bet.argtype +0 -2
  32. package/src/frontend/argtype/__fixtures__/fsl-flirt.argtype +50 -0
  33. package/src/frontend/argtype/__fixtures__/wb-command-sub.argtype +39 -0
  34. package/src/frontend/argtype/ast.ts +14 -1
  35. package/src/frontend/argtype/frontmatter.ts +49 -5
  36. package/src/frontend/argtype/lexer.ts +20 -0
  37. package/src/frontend/argtype/lower.ts +115 -75
  38. package/src/frontend/argtype/parser-frontend.ts +2 -2
  39. package/src/frontend/argtype/parser.ts +70 -24
  40. package/src/frontend/boutiques/destruct-template.ts +24 -17
  41. package/src/frontend/boutiques/parser.ts +27 -8
  42. package/src/ir/passes/canonicalize.ts +18 -5
  43. package/src/ir/passes/simplify.ts +17 -2
  44. package/src/solver/solver.ts +2 -2
@@ -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(str())
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: { kind: "str", attrs: {} }, countMin: 0 },
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
- if (alts.length === 1) {
704
- return alts[0]!;
705
- }
706
-
707
- const alt: Alternative = { kind: "alternative", attrs: { alts } };
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
- if (!isRequired) {
711
- const opt: Optional = { kind: "optional", attrs: { node: alt } };
714
+ const meta = this.buildNodeMeta(action);
712
715
 
713
- const meta = this.buildNodeMeta(action);
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
- const meta = this.buildNodeMeta(action);
720
- if (meta) alt.meta = meta;
721
-
722
- return alt;
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(actionsByDest, mutexGroups, allNodes, nodesByDest);
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
+ )
@@ -5,8 +5,6 @@ authors:
5
5
  - "FMRIB Analysis Group, University of Oxford"
6
6
  urls:
7
7
  - "https://fsl.fmrib.ox.ac.uk/fsl/fslwiki"
8
- extensions:
9
- - outputs
10
8
  ---
11
9
 
12
10
  /// Automated brain extraction tool for FSL
@@ -0,0 +1,50 @@
1
+ ---
2
+ exe: "flirt"
3
+ version: "6.0.4"
4
+ authors:
5
+ - "FMRIB Analysis Group, University of Oxford"
6
+ urls:
7
+ - "https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/FLIRT"
8
+ ---
9
+
10
+ /// FMRIB's Linear Image Registration Tool (affine intra-modal registration).
11
+ flirt: seq(
12
+ /// Input volume to register.
13
+ opt("-in", infile: path),
14
+
15
+ /// Reference volume that defines the target space.
16
+ opt("-ref", reference: path),
17
+
18
+ set(
19
+ /// Registered output volume.
20
+ opt("-out", outbase: str).output(outvol: `{outbase}.nii.gz`),
21
+
22
+ /// Text file the estimated affine transform is written to.
23
+ opt("-omat", outmatrix: str),
24
+
25
+ /// Number of histogram bins used for the cost function.
26
+ opt("-bins", bins: int.min(1) = 256),
27
+
28
+ /// Degrees of freedom for the transform (6 rigid, 12 full affine).
29
+ opt("-dof", dof: int.min(3).max(12) = 12),
30
+
31
+ /// Cost function to optimize.
32
+ opt("-cost", cost: alt(
33
+ "mutualinfo",
34
+ "corratio",
35
+ "normcorr",
36
+ "normmi",
37
+ "leastsq",
38
+ ) = "corratio"),
39
+
40
+ /// Final interpolation applied when writing `-out`. The union carries a
41
+ /// default variant (`trilinear`), which the frontend keeps as the node
42
+ /// default.
43
+ opt("-interp", interp: alt(
44
+ "trilinear",
45
+ "nearestneighbour",
46
+ "sinc",
47
+ "spline",
48
+ ) = "trilinear"),
49
+ ),
50
+ )
@@ -0,0 +1,39 @@
1
+ ---
2
+ exe: "wb_command"
3
+ version: "1.5.0"
4
+ authors:
5
+ - "Washington University School of Medicine"
6
+ urls:
7
+ - "https://www.humanconnectome.org/software/connectome-workbench"
8
+ ---
9
+
10
+ /// Reduce a volume file along the map axis to a single output map.
11
+ ///
12
+ /// The executable is `wb_command` and the subcommand is `-volume-reduce`, so
13
+ /// the tool id (`volume_reduce`) deliberately differs from the exe.
14
+ volume_reduce: seq(
15
+ "-volume-reduce",
16
+
17
+ /// The input volume.
18
+ volume_in: path,
19
+
20
+ /// The reduction operation applied across the maps.
21
+ operation: alt(
22
+ "MAX",
23
+ "MIN",
24
+ "MEAN",
25
+ "SUM",
26
+ "STDEV",
27
+ "VARIANCE",
28
+ "TSNR",
29
+ ),
30
+
31
+ /// The output volume.
32
+ volume_out: str,
33
+
34
+ /// Exclude outliers by standard deviation before reducing.
35
+ opt("-exclude-outliers", sigma_below: float.min(0), sigma_above: float.min(0)),
36
+ ).output(
37
+ /// The reduced output volume.
38
+ reduced: `{volume_out}`,
39
+ )
@@ -15,9 +15,21 @@ export type Combinator = "seq" | "set" | "opt" | "rep" | "alt" | "any";
15
15
  /** A terminal keyword. */
16
16
  export type Terminal = "int" | "float" | "str" | "path";
17
17
 
18
+ /** 1-based source position of a construct, for diagnostics. Points at the node's
19
+ * primary token (a terminal/combinator keyword, a literal token, or the opening
20
+ * paren of a group), which is where a misplaced modifier or bad decoration is
21
+ * most usefully reported. */
22
+ export interface SourceSpan {
23
+ line: number;
24
+ column: number;
25
+ }
26
+
18
27
  export interface AstNode {
19
28
  kind: "literal" | "terminal" | "comb" | "ref";
20
29
 
30
+ /** Source position of the node's primary token, if known. */
31
+ span?: SourceSpan;
32
+
21
33
  /** kind === "literal": the fixed token string. */
22
34
  value?: string;
23
35
  /** kind === "terminal": which terminal. */
@@ -36,7 +48,8 @@ export interface AstNode {
36
48
  title?: string;
37
49
  /** Description: a `///` block (minus the title), or `.description("...")`. */
38
50
  description?: string;
39
- /** `= value` sugar / `.default(...)` (terminal only). */
51
+ /** `= value` sugar / `.default(...)`. Meaningful on a terminal and on
52
+ * `opt`/`alt`/`rep`; dropped with a warning on a `seq`/`set` struct. */
40
53
  default?: string | number;
41
54
  /** `.min(n)` (int/float only). */
42
55
  min?: number;
@@ -2,11 +2,11 @@
2
2
  * Frontmatter handling for argtype documents.
3
3
  *
4
4
  * A document may begin with a `---` fenced block of YAML-ish metadata (`exe`,
5
- * `version`, `extensions`, `container`, `authors`, `stdout`, ...). Rather than
6
- * pull in a full YAML dependency, this parses the small subset the format uses:
7
- * scalar `key: value`, block sequences (`- item`), and one level of nested
8
- * mappings. That covers every key the lowering step consumes; anything more
9
- * exotic is read as a raw string and ignored downstream.
5
+ * `version`, `container`, `authors`, `stdout`, ...). Rather than pull in a full
6
+ * YAML dependency, this parses the small subset the format uses: scalar
7
+ * `key: value`, block sequences (`- item`), inline flow sequences (`[a, b]`),
8
+ * and one level of nested mappings. That covers every key the lowering step
9
+ * consumes; anything more exotic is read as a raw string and ignored downstream.
10
10
  */
11
11
 
12
12
  export interface SplitResult {
@@ -174,7 +174,51 @@ function unescapeScalar(s: string): string {
174
174
  );
175
175
  }
176
176
 
177
+ /** Split a flow-sequence body (`a, "b, c", [d, e]`) on top-level commas, keeping
178
+ * commas inside quotes or a nested `[...]` intact. Empty segments (from a leading,
179
+ * trailing, or doubled comma, or an empty `[]` body) are dropped, matching YAML;
180
+ * a quoted empty string keeps its quotes and so survives. */
181
+ function splitFlow(body: string): string[] {
182
+ const parts: string[] = [];
183
+ let depth = 0;
184
+ let inQuote: string | null = null;
185
+ let cur = "";
186
+ for (let i = 0; i < body.length; i++) {
187
+ const ch = body[i]!;
188
+ if (inQuote === '"' && ch === "\\") {
189
+ cur += ch + (body[i + 1] ?? "");
190
+ i++;
191
+ continue;
192
+ }
193
+ if (inQuote) {
194
+ if (ch === inQuote) inQuote = null;
195
+ cur += ch;
196
+ } else if (ch === '"' || ch === "'") {
197
+ inQuote = ch;
198
+ cur += ch;
199
+ } else if (ch === "[") {
200
+ depth++;
201
+ cur += ch;
202
+ } else if (ch === "]") {
203
+ depth--;
204
+ cur += ch;
205
+ } else if (ch === "," && depth === 0) {
206
+ parts.push(cur);
207
+ cur = "";
208
+ } else {
209
+ cur += ch;
210
+ }
211
+ }
212
+ parts.push(cur);
213
+ return parts.map((p) => p.trim()).filter((p) => p !== "");
214
+ }
215
+
177
216
  function parseScalar(text: string): unknown {
217
+ // Inline flow sequence: `[a, b, c]` (valid YAML the block-list parser would
218
+ // otherwise read as a bare string). `[]` is the empty list.
219
+ if (text.startsWith("[") && text.endsWith("]") && text.length >= 2) {
220
+ return splitFlow(text.slice(1, -1)).map(parseScalar);
221
+ }
178
222
  if (text.startsWith('"') && text.endsWith('"') && text.length >= 2) {
179
223
  return unescapeScalar(text.slice(1, -1));
180
224
  }
@@ -195,6 +195,26 @@ export function lex(source: string): LexResult {
195
195
  });
196
196
  }
197
197
  }
198
+ // A plain integer run abutting a letter/underscore (`3dTstat`, all of
199
+ // AFNI's `3d*` tools) is a digit-led identifier: not a valid number and
200
+ // not a valid bare identifier. Consume the whole run and emit the fix
201
+ // directly rather than splitting into `number` + `ident` (which surfaces
202
+ // as a baffling "expected a definition"); recover as an `ident` so the
203
+ // rest still parses. Only a pure integer run qualifies: a decimal,
204
+ // signed, or malformed-exponent `num` is a broken number, not a name, so
205
+ // it keeps its own number/error path instead of being quoted as an ident.
206
+ if (/^\d+$/.test(num) && isIdentStart(peek())) {
207
+ let rest = "";
208
+ while (i < source.length && isIdentPart(peek())) rest += advance();
209
+ const full = num + rest;
210
+ errors.push({
211
+ message: `Identifier cannot start with a digit; quote it as "${full}"`,
212
+ line: startLine,
213
+ column: startCol,
214
+ });
215
+ push("ident", full, startLine, startCol);
216
+ continue;
217
+ }
198
218
  push("number", num, startLine, startCol);
199
219
  continue;
200
220
  }