@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
package/dist/index.cjs
CHANGED
|
@@ -206,10 +206,7 @@ var ArgdumpParser = class {
|
|
|
206
206
|
if (nargs.__argparse__ === "REMAINDER") return {
|
|
207
207
|
kind: "repeat",
|
|
208
208
|
attrs: {
|
|
209
|
-
node
|
|
210
|
-
kind: "str",
|
|
211
|
-
attrs: {}
|
|
212
|
-
},
|
|
209
|
+
node,
|
|
213
210
|
countMin: 0
|
|
214
211
|
}
|
|
215
212
|
};
|
|
@@ -580,25 +577,24 @@ var ArgdumpParser = class {
|
|
|
580
577
|
this.error(`No valid subparsers for '${action.dest}'`);
|
|
581
578
|
return null;
|
|
582
579
|
}
|
|
583
|
-
|
|
584
|
-
const alt = {
|
|
580
|
+
const node = alts.length === 1 ? alts[0] : {
|
|
585
581
|
kind: "alternative",
|
|
586
582
|
attrs: { alts }
|
|
587
583
|
};
|
|
588
|
-
|
|
584
|
+
const isRequired = action.subparsers_required === true || action.required === true;
|
|
585
|
+
const meta = this.buildNodeMeta(action);
|
|
586
|
+
if (!isRequired) {
|
|
589
587
|
const opt = {
|
|
590
588
|
kind: "optional",
|
|
591
|
-
attrs: { node
|
|
589
|
+
attrs: { node }
|
|
592
590
|
};
|
|
593
|
-
const meta = this.buildNodeMeta(action);
|
|
594
591
|
if (meta) opt.meta = meta;
|
|
595
592
|
return opt;
|
|
596
593
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
return alt;
|
|
594
|
+
if (node.kind === "alternative" && meta) node.meta = meta;
|
|
595
|
+
return node;
|
|
600
596
|
}
|
|
601
|
-
applyMutualExclusion(
|
|
597
|
+
applyMutualExclusion(groups, nodes, nodesByDest) {
|
|
602
598
|
const excluded = /* @__PURE__ */ new Set();
|
|
603
599
|
for (const group of groups) {
|
|
604
600
|
if (!isObject$3(group)) continue;
|
|
@@ -621,136 +617,1682 @@ var ArgdumpParser = class {
|
|
|
621
617
|
...inner.meta,
|
|
622
618
|
name: inner.meta?.name ?? findDeepName$1(inner) ?? dest
|
|
623
619
|
};
|
|
624
|
-
altMembers.push(inner);
|
|
625
|
-
excluded.add(dest);
|
|
620
|
+
altMembers.push(inner);
|
|
621
|
+
excluded.add(dest);
|
|
622
|
+
}
|
|
623
|
+
const alt = {
|
|
624
|
+
kind: "alternative",
|
|
625
|
+
attrs: { alts: altMembers }
|
|
626
|
+
};
|
|
627
|
+
const isRequired = group.required === true;
|
|
628
|
+
let groupNode;
|
|
629
|
+
if (isRequired) groupNode = alt;
|
|
630
|
+
else groupNode = {
|
|
631
|
+
kind: "optional",
|
|
632
|
+
attrs: { node: alt }
|
|
633
|
+
};
|
|
634
|
+
const groupName = (isString$3(group.title) ? group.title : void 0) ?? (memberDests.length === 2 ? `${memberDests[0]}_or_${memberDests[1]}` : `${memberDests[0]}_choice`);
|
|
635
|
+
groupNode.meta = {
|
|
636
|
+
...groupNode.meta,
|
|
637
|
+
name: groupName
|
|
638
|
+
};
|
|
639
|
+
const firstDest = memberDests[0];
|
|
640
|
+
const firstIdx = nodes.findIndex((n) => n === nodesByDest.get(firstDest));
|
|
641
|
+
if (firstIdx >= 0) nodes.splice(firstIdx, 0, groupNode);
|
|
642
|
+
else nodes.push(groupNode);
|
|
643
|
+
}
|
|
644
|
+
return nodes.filter((n) => {
|
|
645
|
+
for (const [dest, node] of nodesByDest) if (node === n && excluded.has(dest)) return false;
|
|
646
|
+
return true;
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
parseParserInfo(descriptor) {
|
|
650
|
+
const actions = descriptor.actions;
|
|
651
|
+
if (!isArray$3(actions)) return {
|
|
652
|
+
kind: "sequence",
|
|
653
|
+
attrs: { nodes: [] }
|
|
654
|
+
};
|
|
655
|
+
const positionals = [];
|
|
656
|
+
const optionals = [];
|
|
657
|
+
const nodesByDest = /* @__PURE__ */ new Map();
|
|
658
|
+
for (const rawAction of actions) {
|
|
659
|
+
if (!isObject$3(rawAction)) continue;
|
|
660
|
+
const node = this.buildAction(rawAction);
|
|
661
|
+
if (!node) continue;
|
|
662
|
+
const dest = rawAction.dest;
|
|
663
|
+
if (isString$3(dest)) nodesByDest.set(dest, node);
|
|
664
|
+
if (this.isPositional(rawAction) && rawAction.action_type !== "parsers") positionals.push(node);
|
|
665
|
+
else optionals.push(node);
|
|
666
|
+
}
|
|
667
|
+
let allNodes = [...positionals, ...optionals];
|
|
668
|
+
const mutexGroups = descriptor.mutually_exclusive_groups;
|
|
669
|
+
if (isArray$3(mutexGroups) && mutexGroups.length > 0) allNodes = this.applyMutualExclusion(mutexGroups, allNodes, nodesByDest);
|
|
670
|
+
return {
|
|
671
|
+
kind: "sequence",
|
|
672
|
+
attrs: { nodes: allNodes }
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
parse(source, _filename) {
|
|
676
|
+
this.reset();
|
|
677
|
+
const descriptor = this.parseJSON(source);
|
|
678
|
+
if (descriptor === null) return {
|
|
679
|
+
expr: {
|
|
680
|
+
kind: "sequence",
|
|
681
|
+
attrs: { nodes: [] }
|
|
682
|
+
},
|
|
683
|
+
errors: this.errors,
|
|
684
|
+
warnings: this.warnings
|
|
685
|
+
};
|
|
686
|
+
const meta = this.buildAppMeta(descriptor);
|
|
687
|
+
if (!meta) {
|
|
688
|
+
this.error("Descriptor is missing prog");
|
|
689
|
+
return {
|
|
690
|
+
expr: {
|
|
691
|
+
kind: "sequence",
|
|
692
|
+
attrs: { nodes: [] }
|
|
693
|
+
},
|
|
694
|
+
errors: this.errors,
|
|
695
|
+
warnings: this.warnings
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
const expr = this.parseParserInfo(descriptor);
|
|
699
|
+
if (expr === null) {
|
|
700
|
+
this.error("Failed to parse argument structure");
|
|
701
|
+
return {
|
|
702
|
+
expr: {
|
|
703
|
+
kind: "sequence",
|
|
704
|
+
attrs: { nodes: [] }
|
|
705
|
+
},
|
|
706
|
+
errors: this.errors,
|
|
707
|
+
warnings: this.warnings
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
const prog = descriptor.prog;
|
|
711
|
+
if (isString$3(prog) && prog) expr.attrs.nodes.unshift({
|
|
712
|
+
kind: "literal",
|
|
713
|
+
attrs: { str: prog }
|
|
714
|
+
});
|
|
715
|
+
if (!expr.meta?.name && meta.id) expr.meta = {
|
|
716
|
+
...expr.meta,
|
|
717
|
+
name: meta.id
|
|
718
|
+
};
|
|
719
|
+
return {
|
|
720
|
+
meta,
|
|
721
|
+
expr,
|
|
722
|
+
errors: this.errors,
|
|
723
|
+
warnings: this.warnings
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
//#endregion
|
|
729
|
+
//#region src/frontend/argtype/lexer.ts
|
|
730
|
+
const PUNCT = {
|
|
731
|
+
":": "colon",
|
|
732
|
+
"=": "eq",
|
|
733
|
+
"|": "pipe",
|
|
734
|
+
".": "dot",
|
|
735
|
+
",": "comma",
|
|
736
|
+
"(": "lparen",
|
|
737
|
+
")": "rparen"
|
|
738
|
+
};
|
|
739
|
+
function isIdentStart(ch) {
|
|
740
|
+
return /[A-Za-z_]/.test(ch);
|
|
741
|
+
}
|
|
742
|
+
function isIdentPart(ch) {
|
|
743
|
+
return /[A-Za-z0-9_]/.test(ch);
|
|
744
|
+
}
|
|
745
|
+
function isDigit(ch) {
|
|
746
|
+
return ch >= "0" && ch <= "9";
|
|
747
|
+
}
|
|
748
|
+
function lex(source) {
|
|
749
|
+
const tokens = [];
|
|
750
|
+
const errors = [];
|
|
751
|
+
let i = 0;
|
|
752
|
+
let line = 1;
|
|
753
|
+
let col = 1;
|
|
754
|
+
const peek = (o = 0) => source[i + o] ?? "";
|
|
755
|
+
const advance = () => {
|
|
756
|
+
const ch = source[i++];
|
|
757
|
+
if (ch === "\n") {
|
|
758
|
+
line++;
|
|
759
|
+
col = 1;
|
|
760
|
+
} else col++;
|
|
761
|
+
return ch;
|
|
762
|
+
};
|
|
763
|
+
const push = (kind, value, l, c) => {
|
|
764
|
+
tokens.push({
|
|
765
|
+
kind,
|
|
766
|
+
value,
|
|
767
|
+
line: l,
|
|
768
|
+
column: c
|
|
769
|
+
});
|
|
770
|
+
};
|
|
771
|
+
while (i < source.length) {
|
|
772
|
+
const ch = peek();
|
|
773
|
+
const startLine = line;
|
|
774
|
+
const startCol = col;
|
|
775
|
+
if (ch === " " || ch === " " || ch === "\r" || ch === "\n") {
|
|
776
|
+
advance();
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
if (ch === "/" && peek(1) === "/") {
|
|
780
|
+
const isDoc = peek(2) === "/";
|
|
781
|
+
advance();
|
|
782
|
+
advance();
|
|
783
|
+
if (isDoc) advance();
|
|
784
|
+
let text = "";
|
|
785
|
+
while (i < source.length && peek() !== "\n") text += advance();
|
|
786
|
+
if (isDoc) push("doc", text.trim(), startLine, startCol);
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
if (ch === "\"") {
|
|
790
|
+
advance();
|
|
791
|
+
let str = "";
|
|
792
|
+
let closed = false;
|
|
793
|
+
while (i < source.length) {
|
|
794
|
+
const c = advance();
|
|
795
|
+
if (c === "\\") {
|
|
796
|
+
const esc = i < source.length ? advance() : "";
|
|
797
|
+
str += unescape(esc);
|
|
798
|
+
} else if (c === "\"") {
|
|
799
|
+
closed = true;
|
|
800
|
+
break;
|
|
801
|
+
} else if (c === "\n") break;
|
|
802
|
+
else str += c;
|
|
803
|
+
}
|
|
804
|
+
if (!closed) errors.push({
|
|
805
|
+
message: "Unterminated string literal",
|
|
806
|
+
line: startLine,
|
|
807
|
+
column: startCol
|
|
808
|
+
});
|
|
809
|
+
push("string", str, startLine, startCol);
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
if (ch === "`") {
|
|
813
|
+
advance();
|
|
814
|
+
let body = "";
|
|
815
|
+
let closed = false;
|
|
816
|
+
while (i < source.length) {
|
|
817
|
+
const c = advance();
|
|
818
|
+
if (c === "\\") {
|
|
819
|
+
body += c;
|
|
820
|
+
if (i < source.length) body += advance();
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
if (c === "`") {
|
|
824
|
+
closed = true;
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
body += c;
|
|
828
|
+
}
|
|
829
|
+
if (!closed) errors.push({
|
|
830
|
+
message: "Unterminated template literal",
|
|
831
|
+
line: startLine,
|
|
832
|
+
column: startCol
|
|
833
|
+
});
|
|
834
|
+
push("template", body, startLine, startCol);
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
if (isDigit(ch) || ch === "-" && isDigit(peek(1))) {
|
|
838
|
+
let num = advance();
|
|
839
|
+
while (i < source.length && isDigit(peek())) num += advance();
|
|
840
|
+
if (peek() === "." && isDigit(peek(1))) {
|
|
841
|
+
num += advance();
|
|
842
|
+
while (i < source.length && isDigit(peek())) num += advance();
|
|
843
|
+
}
|
|
844
|
+
if (peek() === "e" || peek() === "E") {
|
|
845
|
+
const signed = peek(1) === "+" || peek(1) === "-";
|
|
846
|
+
if (isDigit(peek(1)) || signed && isDigit(peek(2))) {
|
|
847
|
+
num += advance();
|
|
848
|
+
if (peek() === "+" || peek() === "-") num += advance();
|
|
849
|
+
while (i < source.length && isDigit(peek())) num += advance();
|
|
850
|
+
} else {
|
|
851
|
+
num += advance();
|
|
852
|
+
errors.push({
|
|
853
|
+
message: `Malformed number '${num}': exponent has no digits`,
|
|
854
|
+
line: startLine,
|
|
855
|
+
column: startCol
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (/^\d+$/.test(num) && isIdentStart(peek())) {
|
|
860
|
+
let rest = "";
|
|
861
|
+
while (i < source.length && isIdentPart(peek())) rest += advance();
|
|
862
|
+
const full = num + rest;
|
|
863
|
+
errors.push({
|
|
864
|
+
message: `Identifier cannot start with a digit; quote it as "${full}"`,
|
|
865
|
+
line: startLine,
|
|
866
|
+
column: startCol
|
|
867
|
+
});
|
|
868
|
+
push("ident", full, startLine, startCol);
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
push("number", num, startLine, startCol);
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
if (isIdentStart(ch)) {
|
|
875
|
+
let id = advance();
|
|
876
|
+
while (i < source.length && isIdentPart(peek())) id += advance();
|
|
877
|
+
push("ident", id, startLine, startCol);
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
const punct = PUNCT[ch];
|
|
881
|
+
if (punct) {
|
|
882
|
+
advance();
|
|
883
|
+
push(punct, ch, startLine, startCol);
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
errors.push({
|
|
887
|
+
message: `Unexpected character '${ch}'`,
|
|
888
|
+
line: startLine,
|
|
889
|
+
column: startCol
|
|
890
|
+
});
|
|
891
|
+
advance();
|
|
892
|
+
}
|
|
893
|
+
push("eof", "", line, col);
|
|
894
|
+
return {
|
|
895
|
+
tokens,
|
|
896
|
+
errors
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
/** Minimal escape handling inside double-quoted strings. */
|
|
900
|
+
function unescape(esc) {
|
|
901
|
+
switch (esc) {
|
|
902
|
+
case "n": return "\n";
|
|
903
|
+
case "t": return " ";
|
|
904
|
+
case "r": return "\r";
|
|
905
|
+
case "\"": return "\"";
|
|
906
|
+
case "\\": return "\\";
|
|
907
|
+
case "": return "";
|
|
908
|
+
default: return esc;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
//#endregion
|
|
913
|
+
//#region src/frontend/argtype/frontmatter.ts
|
|
914
|
+
function splitFrontmatter(source) {
|
|
915
|
+
const lines = source.split("\n");
|
|
916
|
+
let start = 0;
|
|
917
|
+
while (start < lines.length && lines[start].trim() === "") start++;
|
|
918
|
+
if (lines[start]?.trim() !== "---") return {
|
|
919
|
+
body: source,
|
|
920
|
+
errors: []
|
|
921
|
+
};
|
|
922
|
+
let end = -1;
|
|
923
|
+
for (let i = start + 1; i < lines.length; i++) if (lines[i].trim() === "---") {
|
|
924
|
+
end = i;
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
927
|
+
if (end === -1) return {
|
|
928
|
+
body: source,
|
|
929
|
+
errors: ["Unterminated frontmatter block (missing closing '---')"]
|
|
930
|
+
};
|
|
931
|
+
const { value, errors } = parseYamlish(lines.slice(start + 1, end));
|
|
932
|
+
return {
|
|
933
|
+
frontmatter: value,
|
|
934
|
+
body: lines.map((line, i) => i >= start && i <= end ? "" : line).join("\n"),
|
|
935
|
+
errors
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
function tokenizeLines(raw) {
|
|
939
|
+
const out = [];
|
|
940
|
+
for (const line of raw) {
|
|
941
|
+
const noComment = stripComment(line);
|
|
942
|
+
if (noComment.trim() === "") continue;
|
|
943
|
+
const indent = noComment.length - noComment.trimStart().length;
|
|
944
|
+
out.push({
|
|
945
|
+
indent,
|
|
946
|
+
content: noComment.trim()
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
return out;
|
|
950
|
+
}
|
|
951
|
+
/** Strip a trailing `#` comment that is not inside quotes. Following YAML, a
|
|
952
|
+
* `#` only starts a comment at the start of the line or after whitespace, so an
|
|
953
|
+
* unquoted value containing `#` (e.g. a URL fragment `https://x/#frag`) is kept
|
|
954
|
+
* intact. */
|
|
955
|
+
function stripComment(line) {
|
|
956
|
+
let inQuote = null;
|
|
957
|
+
for (let i = 0; i < line.length; i++) {
|
|
958
|
+
const ch = line[i];
|
|
959
|
+
if (inQuote === "\"" && ch === "\\") {
|
|
960
|
+
i++;
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
if (inQuote) {
|
|
964
|
+
if (ch === inQuote) inQuote = null;
|
|
965
|
+
} else if (ch === "\"" || ch === "'") inQuote = ch;
|
|
966
|
+
else if (ch === "#" && (i === 0 || line[i - 1] === " " || line[i - 1] === " ")) return line.slice(0, i);
|
|
967
|
+
}
|
|
968
|
+
return line;
|
|
969
|
+
}
|
|
970
|
+
function parseYamlish(raw) {
|
|
971
|
+
const lines = tokenizeLines(raw);
|
|
972
|
+
const errors = [];
|
|
973
|
+
let pos = 0;
|
|
974
|
+
function parseBlock(minIndent) {
|
|
975
|
+
if (pos < lines.length && lines[pos].indent >= minIndent && lines[pos].content.startsWith("- ")) {
|
|
976
|
+
const seq = [];
|
|
977
|
+
const indent = lines[pos].indent;
|
|
978
|
+
while (pos < lines.length && lines[pos].indent === indent && lines[pos].content.startsWith("- ")) {
|
|
979
|
+
seq.push(parseScalar(lines[pos].content.slice(2).trim()));
|
|
980
|
+
pos++;
|
|
981
|
+
}
|
|
982
|
+
return seq;
|
|
983
|
+
}
|
|
984
|
+
const map = {};
|
|
985
|
+
if (pos >= lines.length) return map;
|
|
986
|
+
const indent = lines[pos].indent;
|
|
987
|
+
while (pos < lines.length && lines[pos].indent === indent && !lines[pos].content.startsWith("- ")) {
|
|
988
|
+
const { content } = lines[pos];
|
|
989
|
+
const colon = findColon(content);
|
|
990
|
+
if (colon === -1) {
|
|
991
|
+
errors.push(`Malformed frontmatter line: '${content}'`);
|
|
992
|
+
pos++;
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
const key = content.slice(0, colon).trim();
|
|
996
|
+
const valueText = content.slice(colon + 1).trim();
|
|
997
|
+
pos++;
|
|
998
|
+
if (valueText !== "") map[key] = parseScalar(valueText);
|
|
999
|
+
else if (pos < lines.length && lines[pos].indent > indent) map[key] = parseBlock(lines[pos].indent);
|
|
1000
|
+
else map[key] = null;
|
|
1001
|
+
}
|
|
1002
|
+
return map;
|
|
1003
|
+
}
|
|
1004
|
+
const value = parseBlock(0);
|
|
1005
|
+
return {
|
|
1006
|
+
value: isRecord$2(value) ? value : { _root: value },
|
|
1007
|
+
errors
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
function findColon(s) {
|
|
1011
|
+
let inQuote = null;
|
|
1012
|
+
for (let i = 0; i < s.length; i++) {
|
|
1013
|
+
const ch = s[i];
|
|
1014
|
+
if (inQuote === "\"" && ch === "\\") {
|
|
1015
|
+
i++;
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
if (inQuote) {
|
|
1019
|
+
if (ch === inQuote) inQuote = null;
|
|
1020
|
+
} else if (ch === "\"" || ch === "'") inQuote = ch;
|
|
1021
|
+
else if (ch === ":") return i;
|
|
1022
|
+
}
|
|
1023
|
+
return -1;
|
|
1024
|
+
}
|
|
1025
|
+
/** Reverse the backslash escapes an emitter applies to a quoted scalar
|
|
1026
|
+
* (`\n`, `\t`, `\r`, `\"`, `\\`), so values with newlines/quotes round-trip. */
|
|
1027
|
+
function unescapeScalar(s) {
|
|
1028
|
+
return s.replace(/\\(.)/g, (_, c) => c === "n" ? "\n" : c === "t" ? " " : c === "r" ? "\r" : c);
|
|
1029
|
+
}
|
|
1030
|
+
/** Split a flow-sequence body (`a, "b, c", [d, e]`) on top-level commas, keeping
|
|
1031
|
+
* commas inside quotes or a nested `[...]` intact. Empty segments (from a leading,
|
|
1032
|
+
* trailing, or doubled comma, or an empty `[]` body) are dropped, matching YAML;
|
|
1033
|
+
* a quoted empty string keeps its quotes and so survives. */
|
|
1034
|
+
function splitFlow(body) {
|
|
1035
|
+
const parts = [];
|
|
1036
|
+
let depth = 0;
|
|
1037
|
+
let inQuote = null;
|
|
1038
|
+
let cur = "";
|
|
1039
|
+
for (let i = 0; i < body.length; i++) {
|
|
1040
|
+
const ch = body[i];
|
|
1041
|
+
if (inQuote === "\"" && ch === "\\") {
|
|
1042
|
+
cur += ch + (body[i + 1] ?? "");
|
|
1043
|
+
i++;
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
if (inQuote) {
|
|
1047
|
+
if (ch === inQuote) inQuote = null;
|
|
1048
|
+
cur += ch;
|
|
1049
|
+
} else if (ch === "\"" || ch === "'") {
|
|
1050
|
+
inQuote = ch;
|
|
1051
|
+
cur += ch;
|
|
1052
|
+
} else if (ch === "[") {
|
|
1053
|
+
depth++;
|
|
1054
|
+
cur += ch;
|
|
1055
|
+
} else if (ch === "]") {
|
|
1056
|
+
depth--;
|
|
1057
|
+
cur += ch;
|
|
1058
|
+
} else if (ch === "," && depth === 0) {
|
|
1059
|
+
parts.push(cur);
|
|
1060
|
+
cur = "";
|
|
1061
|
+
} else cur += ch;
|
|
1062
|
+
}
|
|
1063
|
+
parts.push(cur);
|
|
1064
|
+
return parts.map((p) => p.trim()).filter((p) => p !== "");
|
|
1065
|
+
}
|
|
1066
|
+
function parseScalar(text) {
|
|
1067
|
+
if (text.startsWith("[") && text.endsWith("]") && text.length >= 2) return splitFlow(text.slice(1, -1)).map(parseScalar);
|
|
1068
|
+
if (text.startsWith("\"") && text.endsWith("\"") && text.length >= 2) return unescapeScalar(text.slice(1, -1));
|
|
1069
|
+
if (text.startsWith("'") && text.endsWith("'") && text.length >= 2) return text.slice(1, -1);
|
|
1070
|
+
if (text === "true") return true;
|
|
1071
|
+
if (text === "false") return false;
|
|
1072
|
+
if (text === "null" || text === "~") return null;
|
|
1073
|
+
if (/^-?\d+(\.\d+)?$/.test(text)) return Number(text);
|
|
1074
|
+
return text;
|
|
1075
|
+
}
|
|
1076
|
+
function isRecord$2(x) {
|
|
1077
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
//#endregion
|
|
1081
|
+
//#region src/frontend/argtype/template.ts
|
|
1082
|
+
/**
|
|
1083
|
+
* Index of the `}` that closes an interpolation opened at `open` (the position
|
|
1084
|
+
* just past the `{`), or -1 if unterminated. A naive `indexOf("}")` would stop
|
|
1085
|
+
* at the first `}` even when it sits inside a quoted ref name (`{"a}b"}`) or a
|
|
1086
|
+
* quoted operation argument (`{a.or("{b}")}`); this scan skips double-quoted
|
|
1087
|
+
* spans (honoring `\"`) and balances nested `{}` so those round-trip.
|
|
1088
|
+
*/
|
|
1089
|
+
function interpolationEnd(body, open) {
|
|
1090
|
+
let depth = 1;
|
|
1091
|
+
let inQuote = false;
|
|
1092
|
+
for (let j = open; j < body.length; j++) {
|
|
1093
|
+
const c = body[j];
|
|
1094
|
+
if (inQuote) {
|
|
1095
|
+
if (c === "\\") j++;
|
|
1096
|
+
else if (c === "\"") inQuote = false;
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
if (c === "\"") inQuote = true;
|
|
1100
|
+
else if (c === "{") depth++;
|
|
1101
|
+
else if (c === "}" && --depth === 0) return j;
|
|
1102
|
+
}
|
|
1103
|
+
return -1;
|
|
1104
|
+
}
|
|
1105
|
+
function parseTemplate(body) {
|
|
1106
|
+
const tokens = [];
|
|
1107
|
+
const errors = [];
|
|
1108
|
+
let lit = "";
|
|
1109
|
+
let i = 0;
|
|
1110
|
+
const flushLit = () => {
|
|
1111
|
+
if (lit.length > 0) {
|
|
1112
|
+
tokens.push({
|
|
1113
|
+
kind: "literal",
|
|
1114
|
+
value: lit
|
|
1115
|
+
});
|
|
1116
|
+
lit = "";
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
while (i < body.length) {
|
|
1120
|
+
const ch = body[i];
|
|
1121
|
+
if (ch === "\\" && i + 1 < body.length) {
|
|
1122
|
+
const next = body[i + 1];
|
|
1123
|
+
lit += next === "{" || next === "}" || next === "`" || next === "\\" ? next : "\\" + next;
|
|
1124
|
+
i += 2;
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
if (ch === "{") {
|
|
1128
|
+
const close = interpolationEnd(body, i + 1);
|
|
1129
|
+
if (close === -1) {
|
|
1130
|
+
errors.push("Unterminated '{' in output template");
|
|
1131
|
+
lit += body.slice(i);
|
|
1132
|
+
break;
|
|
1133
|
+
}
|
|
1134
|
+
flushLit();
|
|
1135
|
+
const { token, error } = parseRef(body.slice(i + 1, close).trim());
|
|
1136
|
+
if (error) errors.push(error);
|
|
1137
|
+
tokens.push(token);
|
|
1138
|
+
i = close + 1;
|
|
1139
|
+
} else {
|
|
1140
|
+
lit += ch;
|
|
1141
|
+
i++;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
flushLit();
|
|
1145
|
+
return {
|
|
1146
|
+
tokens,
|
|
1147
|
+
errors
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
/** Parse the inside of `{...}`: an optional name followed by `.op(arg)` calls.
|
|
1151
|
+
* The name is a bare identifier, a double-quoted string (for a non-identifier
|
|
1152
|
+
* target name, e.g. `{"4d_output"}`), or absent for a self-reference (`{}`). */
|
|
1153
|
+
function parseRef(inner) {
|
|
1154
|
+
const token = { kind: "ref" };
|
|
1155
|
+
let rest = inner;
|
|
1156
|
+
const quoted = /^"((?:[^"\\]|\\.)*)"/.exec(inner);
|
|
1157
|
+
if (quoted) {
|
|
1158
|
+
token.name = unescapeArg(quoted[1]);
|
|
1159
|
+
rest = inner.slice(quoted[0].length);
|
|
1160
|
+
} else {
|
|
1161
|
+
const nameMatch = /^[A-Za-z_][A-Za-z0-9_]*/.exec(inner);
|
|
1162
|
+
if (nameMatch) {
|
|
1163
|
+
token.name = nameMatch[0];
|
|
1164
|
+
rest = inner.slice(nameMatch[0].length);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
const opRe = /^\.\s*([A-Za-z_]+)\s*\(\s*(?:"((?:[^"\\]|\\.)*)")?\s*\)/;
|
|
1168
|
+
let error;
|
|
1169
|
+
while (rest.length > 0) {
|
|
1170
|
+
const m = opRe.exec(rest);
|
|
1171
|
+
if (!m) {
|
|
1172
|
+
error = `Unrecognized output-reference operation near '${rest}'`;
|
|
1173
|
+
break;
|
|
1174
|
+
}
|
|
1175
|
+
const op = m[1];
|
|
1176
|
+
const arg = m[2];
|
|
1177
|
+
switch (op) {
|
|
1178
|
+
case "strip_suffix":
|
|
1179
|
+
if (arg !== void 0) (token.stripSuffix ??= []).push(unescapeArg(arg));
|
|
1180
|
+
break;
|
|
1181
|
+
case "strip_prefix":
|
|
1182
|
+
if (arg !== void 0) (token.stripPrefix ??= []).push(unescapeArg(arg));
|
|
1183
|
+
break;
|
|
1184
|
+
case "basename":
|
|
1185
|
+
token.basename = true;
|
|
1186
|
+
break;
|
|
1187
|
+
case "or":
|
|
1188
|
+
if (arg !== void 0) token.or = unescapeArg(arg);
|
|
1189
|
+
break;
|
|
1190
|
+
default: error = `Unknown output-reference operation '${op}'`;
|
|
1191
|
+
}
|
|
1192
|
+
rest = rest.slice(m[0].length).trimStart();
|
|
1193
|
+
}
|
|
1194
|
+
return {
|
|
1195
|
+
token,
|
|
1196
|
+
...error && { error }
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
function unescapeArg(s) {
|
|
1200
|
+
return s.replace(/\\(.)/g, "$1");
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
//#endregion
|
|
1204
|
+
//#region src/frontend/argtype/doc.ts
|
|
1205
|
+
/**
|
|
1206
|
+
* Reflow a `///` description as Markdown-style prose: a single line break is a
|
|
1207
|
+
* soft wrap (the lines join with a space) and a blank line starts a new
|
|
1208
|
+
* paragraph (kept as `\n\n`). This lets an author wrap a long description across
|
|
1209
|
+
* several `///` lines for readability without forcing hard breaks, while real
|
|
1210
|
+
* paragraph structure survives. (It does not preserve Markdown lists or indented
|
|
1211
|
+
* code blocks - their single line breaks are joined like ordinary prose.)
|
|
1212
|
+
*/
|
|
1213
|
+
function reflowProse(text) {
|
|
1214
|
+
return text.split(/\n{2,}/).map((para) => para.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join(" ")).filter((para) => para.length > 0).join("\n\n");
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Split a `///` doc block into title + description. A leading Markdown H1
|
|
1218
|
+
* (`# Title` on the first line) is the title; everything after it is the
|
|
1219
|
+
* description. Without a leading `# `, the whole block is the description (no
|
|
1220
|
+
* title). The description is reflowed as prose (see `reflowProse`): single line
|
|
1221
|
+
* breaks soft-wrap, blank lines separate paragraphs.
|
|
1222
|
+
*/
|
|
1223
|
+
function splitDocText(raw) {
|
|
1224
|
+
const newline = raw.indexOf("\n");
|
|
1225
|
+
const firstLine = (newline === -1 ? raw : raw.slice(0, newline)).trim();
|
|
1226
|
+
if (firstLine.startsWith("# ")) {
|
|
1227
|
+
const title = firstLine.slice(2).trim();
|
|
1228
|
+
const description = reflowProse(newline === -1 ? "" : raw.slice(newline + 1));
|
|
1229
|
+
return {
|
|
1230
|
+
...title && { title },
|
|
1231
|
+
...description && { description }
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
const description = reflowProse(raw);
|
|
1235
|
+
return description ? { description } : {};
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
//#endregion
|
|
1239
|
+
//#region src/frontend/argtype/parser.ts
|
|
1240
|
+
/** Split a `///` block and attach its title/description to a node. Per the spec,
|
|
1241
|
+
* the block "is applied first and wins": for each field the block provides it
|
|
1242
|
+
* overrides a value a chained `.title()`/`.description()` already set, while a
|
|
1243
|
+
* field the block leaves unset keeps whatever the chain supplied. */
|
|
1244
|
+
function attachDocText(target, raw) {
|
|
1245
|
+
const { title, description } = splitDocText(raw);
|
|
1246
|
+
if (title !== void 0) target.title = title;
|
|
1247
|
+
if (description !== void 0) target.description = description;
|
|
1248
|
+
}
|
|
1249
|
+
const COMBINATORS = new Set([
|
|
1250
|
+
"seq",
|
|
1251
|
+
"set",
|
|
1252
|
+
"opt",
|
|
1253
|
+
"rep",
|
|
1254
|
+
"alt",
|
|
1255
|
+
"any"
|
|
1256
|
+
]);
|
|
1257
|
+
const TERMINALS = new Set([
|
|
1258
|
+
"int",
|
|
1259
|
+
"float",
|
|
1260
|
+
"str",
|
|
1261
|
+
"path"
|
|
1262
|
+
]);
|
|
1263
|
+
/** Core + supported-extension chaining methods. Anything else is an extension
|
|
1264
|
+
* we don't implement and is parsed-and-ignored (the spec's "ignorable" rule). */
|
|
1265
|
+
const KNOWN_METHODS = new Set([
|
|
1266
|
+
"name",
|
|
1267
|
+
"title",
|
|
1268
|
+
"description",
|
|
1269
|
+
"default",
|
|
1270
|
+
"min",
|
|
1271
|
+
"max",
|
|
1272
|
+
"join",
|
|
1273
|
+
"count",
|
|
1274
|
+
"countMin",
|
|
1275
|
+
"countMax",
|
|
1276
|
+
"mediaType",
|
|
1277
|
+
"mutable",
|
|
1278
|
+
"resolveParent"
|
|
1279
|
+
]);
|
|
1280
|
+
/** Levenshtein edit distance between two strings. */
|
|
1281
|
+
function editDistance(a, b) {
|
|
1282
|
+
const m = a.length;
|
|
1283
|
+
const n = b.length;
|
|
1284
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
1285
|
+
for (let i = 1; i <= m; i++) {
|
|
1286
|
+
const cur = [i];
|
|
1287
|
+
for (let j = 1; j <= n; j++) cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
1288
|
+
prev = cur;
|
|
1289
|
+
}
|
|
1290
|
+
return prev[n];
|
|
1291
|
+
}
|
|
1292
|
+
/** The closest candidate to `name` within a small edit distance, for a "did you
|
|
1293
|
+
* mean?" hint on an unknown method. Returns undefined when nothing is close
|
|
1294
|
+
* enough, so a deliberate unknown-extension method gets no spurious suggestion.
|
|
1295
|
+
* A typo (`reqires`, `mediaTpe`) lands within distance 2 of a real method; an
|
|
1296
|
+
* unrelated extension method (`conflicts`) does not. The `bestDist < name.length`
|
|
1297
|
+
* clause additionally stops a very short unknown name from matching a longer
|
|
1298
|
+
* method by pure insertion (a 1-char `.n()` is not "did you mean `.min()`"). */
|
|
1299
|
+
function suggestMethod(name, candidates) {
|
|
1300
|
+
let best;
|
|
1301
|
+
let bestDist = Infinity;
|
|
1302
|
+
for (const c of candidates) {
|
|
1303
|
+
const d = editDistance(name, c);
|
|
1304
|
+
if (d < bestDist) {
|
|
1305
|
+
bestDist = d;
|
|
1306
|
+
best = c;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
return best !== void 0 && bestDist <= 2 && bestDist < name.length ? best : void 0;
|
|
1310
|
+
}
|
|
1311
|
+
var Parser = class {
|
|
1312
|
+
tokens;
|
|
1313
|
+
pos = 0;
|
|
1314
|
+
errors = [];
|
|
1315
|
+
warnings = [];
|
|
1316
|
+
constructor(tokens) {
|
|
1317
|
+
this.tokens = tokens;
|
|
1318
|
+
}
|
|
1319
|
+
peek(o = 0) {
|
|
1320
|
+
return this.tokens[Math.min(this.pos + o, this.tokens.length - 1)];
|
|
1321
|
+
}
|
|
1322
|
+
at(kind) {
|
|
1323
|
+
return this.peek().kind === kind;
|
|
1324
|
+
}
|
|
1325
|
+
next() {
|
|
1326
|
+
return this.tokens[this.pos++] ?? this.tokens[this.tokens.length - 1];
|
|
1327
|
+
}
|
|
1328
|
+
expect(kind, what) {
|
|
1329
|
+
if (this.at(kind)) return this.next();
|
|
1330
|
+
const tok = this.peek();
|
|
1331
|
+
this.error(`Expected ${what} but found '${tok.value || tok.kind}'`, tok);
|
|
1332
|
+
return tok;
|
|
1333
|
+
}
|
|
1334
|
+
error(message, tok = this.peek()) {
|
|
1335
|
+
this.errors.push({
|
|
1336
|
+
message,
|
|
1337
|
+
line: tok.line,
|
|
1338
|
+
column: tok.column
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
warn(message, tok = this.peek()) {
|
|
1342
|
+
this.warnings.push({
|
|
1343
|
+
message,
|
|
1344
|
+
line: tok.line,
|
|
1345
|
+
column: tok.column
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
parseDocument(frontmatter) {
|
|
1349
|
+
const aliases = [];
|
|
1350
|
+
let rootName;
|
|
1351
|
+
let root;
|
|
1352
|
+
while (!this.at("eof")) {
|
|
1353
|
+
const docs = this.collectDocs();
|
|
1354
|
+
if (this.at("eof")) break;
|
|
1355
|
+
const labelLike = this.at("ident") || this.at("string");
|
|
1356
|
+
const following = this.peek(1).kind;
|
|
1357
|
+
if (labelLike && following === "eq") {
|
|
1358
|
+
const nameTok = this.next();
|
|
1359
|
+
if (nameTok.kind === "string") this.error("Alias names must be identifiers, not quoted strings", nameTok);
|
|
1360
|
+
this.next();
|
|
1361
|
+
const expr = this.parseElement();
|
|
1362
|
+
if (docs) attachDocText(expr, docs);
|
|
1363
|
+
aliases.push({
|
|
1364
|
+
name: nameTok.value,
|
|
1365
|
+
expr
|
|
1366
|
+
});
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1369
|
+
if (labelLike && following === "colon") {
|
|
1370
|
+
const nameTok = this.next();
|
|
1371
|
+
this.next();
|
|
1372
|
+
const expr = this.parseElement();
|
|
1373
|
+
if (docs) attachDocText(expr, docs);
|
|
1374
|
+
if (root) this.error(`Multiple root definitions; '${nameTok.value}' ignored (already have '${rootName ?? "<anonymous>"}')`, nameTok);
|
|
1375
|
+
else {
|
|
1376
|
+
rootName = nameTok.value;
|
|
1377
|
+
root = expr;
|
|
1378
|
+
if (!root.name) root.name = nameTok.value;
|
|
1379
|
+
}
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
if (!this.at("ident") && !this.at("string") && !this.at("lparen")) {
|
|
1383
|
+
this.error("Expected a definition (name: expr), an alias (Name = expr), or a root expression");
|
|
1384
|
+
break;
|
|
1385
|
+
}
|
|
1386
|
+
const expr = this.parseElement();
|
|
1387
|
+
if (docs) attachDocText(expr, docs);
|
|
1388
|
+
if (root) {
|
|
1389
|
+
this.error("Multiple root definitions; a second top-level expression is not allowed");
|
|
1390
|
+
break;
|
|
1391
|
+
}
|
|
1392
|
+
root = expr;
|
|
1393
|
+
}
|
|
1394
|
+
if (!root) {
|
|
1395
|
+
this.error("No root definition (expected `name: expr` or a bare root expression)");
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
return {
|
|
1399
|
+
...frontmatter && { frontmatter },
|
|
1400
|
+
aliases,
|
|
1401
|
+
...rootName !== void 0 && { rootName },
|
|
1402
|
+
root
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
/** Collect consecutive `///` doc lines, joined with newlines. */
|
|
1406
|
+
collectDocs() {
|
|
1407
|
+
const lines = [];
|
|
1408
|
+
while (this.at("doc")) lines.push(this.next().value);
|
|
1409
|
+
return lines.length > 0 ? lines.join("\n") : void 0;
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* An element is the unit inside a comma list: an optionally-named expression.
|
|
1413
|
+
* `label:` is looser than `|`, so a name binds the whole alternative that
|
|
1414
|
+
* follows it.
|
|
1415
|
+
*/
|
|
1416
|
+
parseElement() {
|
|
1417
|
+
if ((this.at("ident") || this.at("string")) && this.peek(1).kind === "colon") {
|
|
1418
|
+
const nameTok = this.next();
|
|
1419
|
+
this.next();
|
|
1420
|
+
const inner = this.parseElement();
|
|
1421
|
+
inner.name = nameTok.value;
|
|
1422
|
+
return inner;
|
|
1423
|
+
}
|
|
1424
|
+
return this.parseAlt();
|
|
1425
|
+
}
|
|
1426
|
+
parseAlt() {
|
|
1427
|
+
const first = this.parseChain();
|
|
1428
|
+
if (!this.at("pipe")) return first;
|
|
1429
|
+
const alts = [first];
|
|
1430
|
+
while (this.at("pipe")) {
|
|
1431
|
+
this.next();
|
|
1432
|
+
alts.push(this.parseChain());
|
|
1433
|
+
}
|
|
1434
|
+
return {
|
|
1435
|
+
kind: "comb",
|
|
1436
|
+
op: "alt",
|
|
1437
|
+
children: alts,
|
|
1438
|
+
...first.span && { span: first.span }
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
parseChain() {
|
|
1442
|
+
const node = this.parsePrimary();
|
|
1443
|
+
while (this.at("dot")) {
|
|
1444
|
+
this.next();
|
|
1445
|
+
this.applyMethod(node);
|
|
1446
|
+
}
|
|
1447
|
+
if (this.at("eq")) {
|
|
1448
|
+
this.next();
|
|
1449
|
+
node.default = this.parseValue();
|
|
1450
|
+
}
|
|
1451
|
+
return node;
|
|
1452
|
+
}
|
|
1453
|
+
parsePrimary() {
|
|
1454
|
+
const tok = this.peek();
|
|
1455
|
+
const span = {
|
|
1456
|
+
line: tok.line,
|
|
1457
|
+
column: tok.column
|
|
1458
|
+
};
|
|
1459
|
+
if (tok.kind === "string") {
|
|
1460
|
+
this.next();
|
|
1461
|
+
return {
|
|
1462
|
+
kind: "literal",
|
|
1463
|
+
value: tok.value,
|
|
1464
|
+
span
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
if (tok.kind === "lparen") return {
|
|
1468
|
+
kind: "comb",
|
|
1469
|
+
op: "seq",
|
|
1470
|
+
children: this.parseParenList(),
|
|
1471
|
+
span
|
|
1472
|
+
};
|
|
1473
|
+
if (tok.kind === "ident") {
|
|
1474
|
+
if (COMBINATORS.has(tok.value) && this.peek(1).kind === "lparen") {
|
|
1475
|
+
this.next();
|
|
1476
|
+
const children = this.parseParenList();
|
|
1477
|
+
return {
|
|
1478
|
+
kind: "comb",
|
|
1479
|
+
op: tok.value,
|
|
1480
|
+
children,
|
|
1481
|
+
span
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
if (TERMINALS.has(tok.value)) {
|
|
1485
|
+
this.next();
|
|
1486
|
+
return {
|
|
1487
|
+
kind: "terminal",
|
|
1488
|
+
terminal: tok.value,
|
|
1489
|
+
span
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
this.next();
|
|
1493
|
+
return {
|
|
1494
|
+
kind: "ref",
|
|
1495
|
+
refName: tok.value,
|
|
1496
|
+
span
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
this.error(`Unexpected token '${tok.value || tok.kind}' in expression`, tok);
|
|
1500
|
+
this.next();
|
|
1501
|
+
return {
|
|
1502
|
+
kind: "literal",
|
|
1503
|
+
value: "",
|
|
1504
|
+
span
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
/** Parse `( elem, elem, ... )` with optional leading docs and trailing comma. */
|
|
1508
|
+
parseParenList() {
|
|
1509
|
+
this.expect("lparen", "'('");
|
|
1510
|
+
const items = [];
|
|
1511
|
+
while (!this.at("rparen") && !this.at("eof")) {
|
|
1512
|
+
const docs = this.collectDocs();
|
|
1513
|
+
if (this.at("rparen")) break;
|
|
1514
|
+
const elem = this.parseElement();
|
|
1515
|
+
if (docs) attachDocText(elem, docs);
|
|
1516
|
+
items.push(elem);
|
|
1517
|
+
if (this.at("comma")) this.next();
|
|
1518
|
+
else break;
|
|
1519
|
+
}
|
|
1520
|
+
this.expect("rparen", "')'");
|
|
1521
|
+
return items;
|
|
1522
|
+
}
|
|
1523
|
+
applyMethod(node) {
|
|
1524
|
+
const nameTok = this.expect("ident", "a method name");
|
|
1525
|
+
const method = nameTok.value;
|
|
1526
|
+
if (method === "output") {
|
|
1527
|
+
node.outputs = [...node.outputs ?? [], ...this.parseOutputArgs()];
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
if (!KNOWN_METHODS.has(method)) {
|
|
1531
|
+
this.skipBalancedArgs();
|
|
1532
|
+
const hint = suggestMethod(method, [...KNOWN_METHODS, "output"]);
|
|
1533
|
+
this.warn(`Ignoring unsupported method '.${method}()'` + (hint ? ` (did you mean '.${hint}()'?)` : ""), nameTok);
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
const args = this.parseScalarArgs();
|
|
1537
|
+
switch (method) {
|
|
1538
|
+
case "name":
|
|
1539
|
+
if (typeof args[0] === "string") node.name = args[0];
|
|
1540
|
+
break;
|
|
1541
|
+
case "title":
|
|
1542
|
+
if (typeof args[0] === "string") node.title = args[0];
|
|
1543
|
+
break;
|
|
1544
|
+
case "description":
|
|
1545
|
+
if (typeof args[0] === "string") node.description = args[0];
|
|
1546
|
+
break;
|
|
1547
|
+
case "default":
|
|
1548
|
+
if (args[0] !== void 0) node.default = args[0];
|
|
1549
|
+
break;
|
|
1550
|
+
case "min":
|
|
1551
|
+
if (typeof args[0] === "number") node.min = args[0];
|
|
1552
|
+
break;
|
|
1553
|
+
case "max":
|
|
1554
|
+
if (typeof args[0] === "number") node.max = args[0];
|
|
1555
|
+
break;
|
|
1556
|
+
case "join":
|
|
1557
|
+
node.join = typeof args[0] === "string" ? args[0] : "";
|
|
1558
|
+
break;
|
|
1559
|
+
case "count":
|
|
1560
|
+
if (typeof args[0] === "number") {
|
|
1561
|
+
node.countMin = args[0];
|
|
1562
|
+
node.countMax = args[0];
|
|
1563
|
+
}
|
|
1564
|
+
break;
|
|
1565
|
+
case "countMin":
|
|
1566
|
+
if (typeof args[0] === "number") node.countMin = args[0];
|
|
1567
|
+
break;
|
|
1568
|
+
case "countMax":
|
|
1569
|
+
if (typeof args[0] === "number") node.countMax = args[0];
|
|
1570
|
+
break;
|
|
1571
|
+
case "mediaType":
|
|
1572
|
+
if (typeof args[0] === "string") (node.mediaTypes ??= []).push(args[0]);
|
|
1573
|
+
break;
|
|
1574
|
+
case "mutable":
|
|
1575
|
+
node.mutable = true;
|
|
1576
|
+
break;
|
|
1577
|
+
case "resolveParent":
|
|
1578
|
+
node.resolveParent = true;
|
|
1579
|
+
break;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
/** Parse a parenthesized list of plain scalar arguments (numbers / strings). */
|
|
1583
|
+
parseScalarArgs() {
|
|
1584
|
+
this.expect("lparen", "'('");
|
|
1585
|
+
const args = [];
|
|
1586
|
+
while (!this.at("rparen") && !this.at("eof")) {
|
|
1587
|
+
args.push(this.parseValue());
|
|
1588
|
+
if (this.at("comma")) this.next();
|
|
1589
|
+
else break;
|
|
1590
|
+
}
|
|
1591
|
+
this.expect("rparen", "')'");
|
|
1592
|
+
return args;
|
|
1593
|
+
}
|
|
1594
|
+
/** Consume a balanced `( ... )` group without interpreting its contents.
|
|
1595
|
+
* Used to skip the arguments of an unsupported extension method. */
|
|
1596
|
+
skipBalancedArgs() {
|
|
1597
|
+
if (!this.at("lparen")) return;
|
|
1598
|
+
let depth = 0;
|
|
1599
|
+
do {
|
|
1600
|
+
const tok = this.next();
|
|
1601
|
+
if (tok.kind === "lparen") depth++;
|
|
1602
|
+
else if (tok.kind === "rparen") depth--;
|
|
1603
|
+
else if (tok.kind === "eof") break;
|
|
1604
|
+
} while (depth > 0);
|
|
1605
|
+
}
|
|
1606
|
+
/** Parse the argument list of `.output(...)`: one or more template expressions. */
|
|
1607
|
+
parseOutputArgs() {
|
|
1608
|
+
this.expect("lparen", "'('");
|
|
1609
|
+
const outputs = [];
|
|
1610
|
+
while (!this.at("rparen") && !this.at("eof")) {
|
|
1611
|
+
const docs = this.collectDocs();
|
|
1612
|
+
if (this.at("rparen")) break;
|
|
1613
|
+
const out = this.parseOutputTemplate();
|
|
1614
|
+
if (docs) attachDocText(out, docs);
|
|
1615
|
+
outputs.push(out);
|
|
1616
|
+
if (this.at("comma")) this.next();
|
|
1617
|
+
else break;
|
|
1618
|
+
}
|
|
1619
|
+
this.expect("rparen", "')'");
|
|
1620
|
+
return outputs;
|
|
1621
|
+
}
|
|
1622
|
+
/** `label: \`tpl\`` or `\`tpl\`` followed by optional `.name(...)` / `.or(...)`. */
|
|
1623
|
+
parseOutputTemplate() {
|
|
1624
|
+
let name;
|
|
1625
|
+
if ((this.at("ident") || this.at("string")) && this.peek(1).kind === "colon") {
|
|
1626
|
+
name = this.next().value;
|
|
1627
|
+
this.next();
|
|
1628
|
+
}
|
|
1629
|
+
const out = { tokens: [] };
|
|
1630
|
+
if (this.at("template")) {
|
|
1631
|
+
const tmplTok = this.next();
|
|
1632
|
+
const { tokens, errors } = parseTemplate(tmplTok.value);
|
|
1633
|
+
out.tokens = tokens;
|
|
1634
|
+
for (const e of errors) this.error(e, tmplTok);
|
|
1635
|
+
} else this.error(`Expected an output template literal`);
|
|
1636
|
+
const CHAIN = new Set([
|
|
1637
|
+
"name",
|
|
1638
|
+
"or",
|
|
1639
|
+
"title",
|
|
1640
|
+
"description"
|
|
1641
|
+
]);
|
|
1642
|
+
while (this.at("dot")) {
|
|
1643
|
+
this.next();
|
|
1644
|
+
const m = this.expect("ident", "a template method");
|
|
1645
|
+
if (CHAIN.has(m.value)) {
|
|
1646
|
+
const arg = this.parseScalarArgs()[0];
|
|
1647
|
+
if (typeof arg === "string") if (m.value === "name") name = arg;
|
|
1648
|
+
else if (m.value === "or") out.fallback = arg;
|
|
1649
|
+
else if (m.value === "title") out.title = arg;
|
|
1650
|
+
else out.description = arg;
|
|
1651
|
+
} else {
|
|
1652
|
+
this.skipBalancedArgs();
|
|
1653
|
+
const hint = suggestMethod(m.value, CHAIN);
|
|
1654
|
+
this.warn(`Ignoring unsupported output-template method '.${m.value}()'` + (hint ? ` (did you mean '.${hint}()'?)` : ""), m);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
if (name !== void 0) out.name = name;
|
|
1658
|
+
return out;
|
|
1659
|
+
}
|
|
1660
|
+
parseValue() {
|
|
1661
|
+
const tok = this.peek();
|
|
1662
|
+
if (tok.kind === "string") {
|
|
1663
|
+
this.next();
|
|
1664
|
+
return tok.value;
|
|
1665
|
+
}
|
|
1666
|
+
if (tok.kind === "number") {
|
|
1667
|
+
this.next();
|
|
1668
|
+
return Number(tok.value);
|
|
1669
|
+
}
|
|
1670
|
+
this.error(`Expected a value (string or number) but found '${tok.value || tok.kind}'`, tok);
|
|
1671
|
+
this.next();
|
|
1672
|
+
return "";
|
|
1673
|
+
}
|
|
1674
|
+
};
|
|
1675
|
+
function parseArgtype(source) {
|
|
1676
|
+
const { frontmatter, body, errors: fmErrors } = splitFrontmatter(source);
|
|
1677
|
+
const { tokens, errors: lexErrors } = lex(body);
|
|
1678
|
+
const parser = new Parser(tokens);
|
|
1679
|
+
const doc = parser.parseDocument(frontmatter);
|
|
1680
|
+
const errors = [
|
|
1681
|
+
...fmErrors.map((m) => ({ message: m })),
|
|
1682
|
+
...lexErrors.map((e) => ({
|
|
1683
|
+
message: e.message,
|
|
1684
|
+
line: e.line,
|
|
1685
|
+
column: e.column
|
|
1686
|
+
})),
|
|
1687
|
+
...parser.errors
|
|
1688
|
+
];
|
|
1689
|
+
return {
|
|
1690
|
+
...doc && { doc },
|
|
1691
|
+
errors,
|
|
1692
|
+
warnings: parser.warnings
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
//#endregion
|
|
1697
|
+
//#region src/ir/builders.ts
|
|
1698
|
+
function lit(str) {
|
|
1699
|
+
return {
|
|
1700
|
+
kind: "literal",
|
|
1701
|
+
attrs: { str }
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
function str(meta) {
|
|
1705
|
+
return {
|
|
1706
|
+
kind: "str",
|
|
1707
|
+
attrs: {},
|
|
1708
|
+
meta: normalizeMeta(meta)
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
function int(meta) {
|
|
1712
|
+
return {
|
|
1713
|
+
kind: "int",
|
|
1714
|
+
attrs: {},
|
|
1715
|
+
meta: normalizeMeta(meta)
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
function float(meta) {
|
|
1719
|
+
return {
|
|
1720
|
+
kind: "float",
|
|
1721
|
+
attrs: {},
|
|
1722
|
+
meta: normalizeMeta(meta)
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
function path(meta) {
|
|
1726
|
+
return {
|
|
1727
|
+
kind: "path",
|
|
1728
|
+
attrs: {},
|
|
1729
|
+
meta: normalizeMeta(meta)
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
function seq(...nodes) {
|
|
1733
|
+
return {
|
|
1734
|
+
kind: "sequence",
|
|
1735
|
+
attrs: { nodes }
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
function seqJoin(join, ...nodes) {
|
|
1739
|
+
return {
|
|
1740
|
+
kind: "sequence",
|
|
1741
|
+
attrs: {
|
|
1742
|
+
nodes,
|
|
1743
|
+
join
|
|
1744
|
+
}
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
function opt(node, meta) {
|
|
1748
|
+
return {
|
|
1749
|
+
kind: "optional",
|
|
1750
|
+
attrs: { node },
|
|
1751
|
+
meta: normalizeMeta(meta)
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
function rep(node, meta) {
|
|
1755
|
+
return {
|
|
1756
|
+
kind: "repeat",
|
|
1757
|
+
attrs: { node },
|
|
1758
|
+
meta: normalizeMeta(meta)
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
function repJoin(join, node, meta) {
|
|
1762
|
+
return {
|
|
1763
|
+
kind: "repeat",
|
|
1764
|
+
attrs: {
|
|
1765
|
+
node,
|
|
1766
|
+
join
|
|
1767
|
+
},
|
|
1768
|
+
meta: normalizeMeta(meta)
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
function alt(...alts) {
|
|
1772
|
+
return {
|
|
1773
|
+
kind: "alternative",
|
|
1774
|
+
attrs: { alts }
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
function normalizeMeta(meta) {
|
|
1778
|
+
if (meta === void 0) return void 0;
|
|
1779
|
+
if (typeof meta === "string") return { name: meta };
|
|
1780
|
+
return meta;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
//#endregion
|
|
1784
|
+
//#region src/ir/meta.ts
|
|
1785
|
+
/** Construct a NodeRef from a node name. */
|
|
1786
|
+
function nodeRef(name) {
|
|
1787
|
+
return {
|
|
1788
|
+
kind: "node-ref",
|
|
1789
|
+
name
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Produce a usable name for an Output. Frontends may leave `Output.name`
|
|
1794
|
+
* unset; downstream code (resolver, validator, backends) needs a stable
|
|
1795
|
+
* identifier for diagnostics and field naming. Falls back to `output_<index>`
|
|
1796
|
+
* keyed by the output's position in tree-walk order.
|
|
1797
|
+
*/
|
|
1798
|
+
function effectiveOutputName(output, index) {
|
|
1799
|
+
return output.name ?? `output_${index}`;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
//#endregion
|
|
1803
|
+
//#region src/frontend/argtype/lower.ts
|
|
1804
|
+
/**
|
|
1805
|
+
* Lower an `AstDocument` to Styx IR (`Expr` + `AppMeta`).
|
|
1806
|
+
*
|
|
1807
|
+
* Mapping highlights:
|
|
1808
|
+
* - Combinators map 1:1 except `set` -> `sequence` (order-not-meaningful is not
|
|
1809
|
+
* modeled in the IR) and `any` -> its first branch (the spec's "emit branch 0"
|
|
1810
|
+
* rule; the interchangeable alternatives are dropped with a warning).
|
|
1811
|
+
* - Aliases are resolved by substitution with cycle detection.
|
|
1812
|
+
* - `.output(...)` declarations attach to the nearest enclosing sequence scope,
|
|
1813
|
+
* so an output nested in a repeat / subcommand keeps its list / struct shape
|
|
1814
|
+
* (per-output gating is recovered downstream from each ref binding's gate).
|
|
1815
|
+
*/
|
|
1816
|
+
/** Spread a node's span into a diagnostic (no-op when the node has no span). */
|
|
1817
|
+
function at(span) {
|
|
1818
|
+
return span ? {
|
|
1819
|
+
line: span.line,
|
|
1820
|
+
column: span.column
|
|
1821
|
+
} : {};
|
|
1822
|
+
}
|
|
1823
|
+
/** Build IR `Documentation` from an AST node's already-split title/description. */
|
|
1824
|
+
function docFrom(node) {
|
|
1825
|
+
const doc = {
|
|
1826
|
+
...node.title && { title: node.title },
|
|
1827
|
+
...node.description && { description: node.description }
|
|
1828
|
+
};
|
|
1829
|
+
return Object.keys(doc).length > 0 ? doc : void 0;
|
|
1830
|
+
}
|
|
1831
|
+
var Lowerer = class {
|
|
1832
|
+
errors = [];
|
|
1833
|
+
warnings = [];
|
|
1834
|
+
aliases = /* @__PURE__ */ new Map();
|
|
1835
|
+
constructor(aliases) {
|
|
1836
|
+
for (const a of aliases) {
|
|
1837
|
+
if (this.aliases.has(a.name)) this.warn(`Duplicate alias '${a.name}'; last definition wins`, a.expr);
|
|
1838
|
+
this.aliases.set(a.name, a.expr);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
/** Record an error, located at `node`'s span when one is available. */
|
|
1842
|
+
err(message, node) {
|
|
1843
|
+
this.errors.push({
|
|
1844
|
+
message,
|
|
1845
|
+
...at(node?.span)
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
/** Record a warning, located at `node`'s span when one is available. */
|
|
1849
|
+
warn(message, node) {
|
|
1850
|
+
this.warnings.push({
|
|
1851
|
+
message,
|
|
1852
|
+
...at(node?.span)
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
lower(doc) {
|
|
1856
|
+
const rootSink = [];
|
|
1857
|
+
const expr = this.lowerNode(doc.root, /* @__PURE__ */ new Set(), void 0, rootSink);
|
|
1858
|
+
const root = expr.kind === "sequence" ? expr : seq(expr);
|
|
1859
|
+
if (root !== expr) root.meta = { ...doc.root.name && { name: doc.root.name } };
|
|
1860
|
+
if (rootSink.length > 0) root.meta = {
|
|
1861
|
+
...root.meta,
|
|
1862
|
+
outputs: [...root.meta?.outputs ?? [], ...rootSink]
|
|
1863
|
+
};
|
|
1864
|
+
return {
|
|
1865
|
+
expr: root,
|
|
1866
|
+
errors: this.errors,
|
|
1867
|
+
warnings: this.warnings
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* @param expanding - alias names currently being expanded (cycle guard).
|
|
1872
|
+
* @param selfName - nearest enclosing named node, for `{}` self-references.
|
|
1873
|
+
* @param sink - outputs array of the nearest enclosing sequence; a `.output()`
|
|
1874
|
+
* on this node attaches here (seq/set nodes instead own their outputs).
|
|
1875
|
+
*/
|
|
1876
|
+
lowerNode(node, expanding, selfName, sink) {
|
|
1877
|
+
if (node.kind === "ref") return this.lowerRef(node, expanding, selfName, sink);
|
|
1878
|
+
const name = node.name ?? selfName;
|
|
1879
|
+
const joinable = node.kind === "comb" && (node.op === "seq" || node.op === "set" || node.op === "rep" || node.op === "opt");
|
|
1880
|
+
if (node.join !== void 0 && !joinable) this.err("`.join()` is only supported on seq/set/rep/opt", node);
|
|
1881
|
+
const isNumericTerminal = node.kind === "terminal" && (node.terminal === "int" || node.terminal === "float");
|
|
1882
|
+
const isRep = node.kind === "comb" && node.op === "rep";
|
|
1883
|
+
const isPath = node.kind === "terminal" && node.terminal === "path";
|
|
1884
|
+
if ((node.min !== void 0 || node.max !== void 0) && !isNumericTerminal) this.err("`.min()`/`.max()` is only supported on int/float terminals", node);
|
|
1885
|
+
if ((node.countMin !== void 0 || node.countMax !== void 0) && !isRep) this.err("`.count()`/`.countMin()`/`.countMax()` is only supported on rep", node);
|
|
1886
|
+
if ((node.mutable || node.resolveParent) && !isPath) this.err("`.mutable()`/`.resolveParent()` is only supported on path", node);
|
|
1887
|
+
if (node.mediaTypes?.length && !isPath) this.err("`.mediaType()` is only supported on path", node);
|
|
1888
|
+
const isStruct = node.kind === "comb" && (node.op === "seq" || node.op === "set");
|
|
1889
|
+
if (node.default !== void 0 && isStruct) {
|
|
1890
|
+
this.warn("`= value` / `.default()` is not supported on a seq/set struct; ignored", node);
|
|
1891
|
+
node.default = void 0;
|
|
1892
|
+
}
|
|
1893
|
+
const isSeqSet = node.kind === "comb" && (node.op === "seq" || node.op === "set");
|
|
1894
|
+
if (node.outputs?.length && !isSeqSet) for (const o of node.outputs) sink.push(this.toOutput(o, name));
|
|
1895
|
+
switch (node.kind) {
|
|
1896
|
+
case "literal": {
|
|
1897
|
+
const e = lit(node.value ?? "");
|
|
1898
|
+
this.applyMeta(e, node);
|
|
1899
|
+
return e;
|
|
1900
|
+
}
|
|
1901
|
+
case "terminal": return this.lowerTerminal(node);
|
|
1902
|
+
case "comb": return this.lowerComb(node, expanding, name, sink);
|
|
1903
|
+
default:
|
|
1904
|
+
this.err(`Unknown AST node kind '${node.kind}'`, node);
|
|
1905
|
+
return seq();
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
lowerRef(node, expanding, selfName, sink) {
|
|
1909
|
+
const target = node.refName;
|
|
1910
|
+
const aliasExpr = this.aliases.get(target);
|
|
1911
|
+
if (!aliasExpr) {
|
|
1912
|
+
this.err(`Unknown alias '${target}'`, node);
|
|
1913
|
+
return seq();
|
|
1914
|
+
}
|
|
1915
|
+
if (expanding.has(target)) {
|
|
1916
|
+
this.err(`Recursive alias '${target}' is not allowed`, node);
|
|
1917
|
+
return seq();
|
|
1918
|
+
}
|
|
1919
|
+
const clone = structuredClone(aliasExpr);
|
|
1920
|
+
clone.span = node.span ?? clone.span;
|
|
1921
|
+
clone.name = node.name ?? clone.name;
|
|
1922
|
+
clone.title = node.title ?? clone.title;
|
|
1923
|
+
clone.description = node.description ?? clone.description;
|
|
1924
|
+
if (node.default !== void 0) clone.default = node.default;
|
|
1925
|
+
if (node.min !== void 0) clone.min = node.min;
|
|
1926
|
+
if (node.max !== void 0) clone.max = node.max;
|
|
1927
|
+
if (node.join !== void 0) clone.join = node.join;
|
|
1928
|
+
if (node.countMin !== void 0) clone.countMin = node.countMin;
|
|
1929
|
+
if (node.countMax !== void 0) clone.countMax = node.countMax;
|
|
1930
|
+
if (node.mediaTypes?.length) clone.mediaTypes = [...clone.mediaTypes ?? [], ...node.mediaTypes];
|
|
1931
|
+
if (node.mutable) clone.mutable = true;
|
|
1932
|
+
if (node.resolveParent) clone.resolveParent = true;
|
|
1933
|
+
if (node.outputs?.length) clone.outputs = [...clone.outputs ?? [], ...node.outputs];
|
|
1934
|
+
const next = new Set(expanding);
|
|
1935
|
+
next.add(target);
|
|
1936
|
+
return this.lowerNode(clone, next, selfName, sink);
|
|
1937
|
+
}
|
|
1938
|
+
lowerTerminal(node) {
|
|
1939
|
+
switch (node.terminal) {
|
|
1940
|
+
case "int": {
|
|
1941
|
+
const e = int();
|
|
1942
|
+
this.checkBounds(node.min, node.max, "value", "min", "max", node);
|
|
1943
|
+
if (node.min !== void 0) e.attrs.minValue = node.min;
|
|
1944
|
+
if (node.max !== void 0) e.attrs.maxValue = node.max;
|
|
1945
|
+
this.applyMeta(e, node);
|
|
1946
|
+
return e;
|
|
1947
|
+
}
|
|
1948
|
+
case "float": {
|
|
1949
|
+
const e = float();
|
|
1950
|
+
this.checkBounds(node.min, node.max, "value", "min", "max", node);
|
|
1951
|
+
if (node.min !== void 0) e.attrs.minValue = node.min;
|
|
1952
|
+
if (node.max !== void 0) e.attrs.maxValue = node.max;
|
|
1953
|
+
this.applyMeta(e, node);
|
|
1954
|
+
return e;
|
|
1955
|
+
}
|
|
1956
|
+
case "str": {
|
|
1957
|
+
const e = str();
|
|
1958
|
+
this.applyMeta(e, node);
|
|
1959
|
+
return e;
|
|
1960
|
+
}
|
|
1961
|
+
case "path": {
|
|
1962
|
+
const e = path();
|
|
1963
|
+
if (node.mediaTypes?.length) e.attrs.mediaTypes = node.mediaTypes;
|
|
1964
|
+
if (node.mutable) e.attrs.mutable = true;
|
|
1965
|
+
if (node.resolveParent) e.attrs.resolveParent = true;
|
|
1966
|
+
this.applyMeta(e, node);
|
|
1967
|
+
return e;
|
|
1968
|
+
}
|
|
1969
|
+
default:
|
|
1970
|
+
this.err(`Unknown terminal '${String(node.terminal)}'`, node);
|
|
1971
|
+
return str();
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
lowerComb(node, expanding, name, sink) {
|
|
1975
|
+
const children = node.children ?? [];
|
|
1976
|
+
const lowerChildren = (s) => children.map((c) => this.lowerNode(c, expanding, name, s));
|
|
1977
|
+
switch (node.op) {
|
|
1978
|
+
case "seq":
|
|
1979
|
+
case "set": {
|
|
1980
|
+
const selfOutputs = [];
|
|
1981
|
+
for (const o of node.outputs ?? []) selfOutputs.push(this.toOutput(o, name));
|
|
1982
|
+
const lowered = lowerChildren(selfOutputs);
|
|
1983
|
+
this.dedupeSiblingNames(lowered, node);
|
|
1984
|
+
const e = seq(...lowered);
|
|
1985
|
+
if (node.join !== void 0) e.attrs.join = node.join;
|
|
1986
|
+
this.applyMeta(e, node);
|
|
1987
|
+
if (selfOutputs.length > 0) e.meta = {
|
|
1988
|
+
...e.meta,
|
|
1989
|
+
outputs: [...e.meta?.outputs ?? [], ...selfOutputs]
|
|
1990
|
+
};
|
|
1991
|
+
return e;
|
|
626
1992
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
1993
|
+
case "alt": {
|
|
1994
|
+
const arms = lowerChildren(sink);
|
|
1995
|
+
this.dedupeSiblingNames(arms, node);
|
|
1996
|
+
const e = alt(...arms);
|
|
1997
|
+
this.applyMeta(e, node);
|
|
1998
|
+
return e;
|
|
1999
|
+
}
|
|
2000
|
+
case "any": {
|
|
2001
|
+
if (children.length === 0) {
|
|
2002
|
+
this.err("`any(...)` requires at least one branch", node);
|
|
2003
|
+
return seq();
|
|
2004
|
+
}
|
|
2005
|
+
const e = this.lowerNode(children[0], expanding, name, sink);
|
|
2006
|
+
if (node.name) e.meta = {
|
|
2007
|
+
...e.meta,
|
|
2008
|
+
name: node.name
|
|
2009
|
+
};
|
|
2010
|
+
{
|
|
2011
|
+
const d = docFrom(node);
|
|
2012
|
+
if (d) e.meta = {
|
|
2013
|
+
...e.meta,
|
|
2014
|
+
doc: d
|
|
2015
|
+
};
|
|
2016
|
+
}
|
|
2017
|
+
return e;
|
|
2018
|
+
}
|
|
2019
|
+
case "opt": {
|
|
2020
|
+
const inner = this.wrapChildren(children, expanding, name, sink);
|
|
2021
|
+
if (node.join !== void 0 && (inner.kind === "sequence" || inner.kind === "repeat")) inner.attrs.join = node.join;
|
|
2022
|
+
const e = opt(inner);
|
|
2023
|
+
this.applyMeta(e, node);
|
|
2024
|
+
if (inner.kind === "literal") e.meta = {
|
|
2025
|
+
...e.meta,
|
|
2026
|
+
defaultValue: false
|
|
2027
|
+
};
|
|
2028
|
+
return e;
|
|
2029
|
+
}
|
|
2030
|
+
case "rep": {
|
|
2031
|
+
const e = rep(this.wrapChildren(children, expanding, name, sink));
|
|
2032
|
+
if (node.join !== void 0) e.attrs.join = node.join;
|
|
2033
|
+
this.checkBounds(node.countMin, node.countMax, "repetition count", "countMin", "countMax", node);
|
|
2034
|
+
if (node.countMin !== void 0) e.attrs.countMin = node.countMin;
|
|
2035
|
+
if (node.countMax !== void 0) e.attrs.countMax = node.countMax;
|
|
2036
|
+
this.applyMeta(e, node);
|
|
2037
|
+
return e;
|
|
2038
|
+
}
|
|
2039
|
+
default:
|
|
2040
|
+
this.err(`Unknown combinator '${String(node.op)}'`, node);
|
|
2041
|
+
return seq();
|
|
647
2042
|
}
|
|
648
|
-
return nodes.filter((n) => {
|
|
649
|
-
for (const [dest, node] of nodesByDest) if (node === n && excluded.has(dest)) return false;
|
|
650
|
-
return true;
|
|
651
|
-
});
|
|
652
2043
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
2044
|
+
/** Warn on an inverted `[min, max]` pair (a lower bound above its upper
|
|
2045
|
+
* bound), which yields an unsatisfiable constraint downstream. */
|
|
2046
|
+
checkBounds(min, max, what, minLabel, maxLabel, node) {
|
|
2047
|
+
if (min !== void 0 && max !== void 0 && min > max) this.warn(`Inverted ${what} bounds: ${minLabel} (${min}) > ${maxLabel} (${max})`, node);
|
|
2048
|
+
}
|
|
2049
|
+
/**
|
|
2050
|
+
* Disambiguate duplicate explicit names among the direct children of a
|
|
2051
|
+
* sequence/set (struct fields) or the arms of an alternative (union
|
|
2052
|
+
* discriminants). The solver turns each named child into a struct field / union
|
|
2053
|
+
* variant keyed by its name; two siblings with the same name would silently
|
|
2054
|
+
* collapse (the second overwrites the first, dropping a parameter). argtype is
|
|
2055
|
+
* hand-authored
|
|
2056
|
+
* and gives no uniqueness guarantee, so - like the mrtrix frontend - we rename
|
|
2057
|
+
* collisions (`name_2`, `name_3`, ...) and warn. (This catches duplicate
|
|
2058
|
+
* explicit labels; it does not detect collisions between solver-derived names
|
|
2059
|
+
* of otherwise-unnamed children, which the solver also does not guard.)
|
|
2060
|
+
*/
|
|
2061
|
+
dedupeSiblingNames(children, parent) {
|
|
2062
|
+
const used = /* @__PURE__ */ new Set();
|
|
2063
|
+
for (const child of children) {
|
|
2064
|
+
const nm = child.meta?.name;
|
|
2065
|
+
if (nm === void 0) continue;
|
|
2066
|
+
if (!used.has(nm)) {
|
|
2067
|
+
used.add(nm);
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
let n = 2;
|
|
2071
|
+
while (used.has(`${nm}_${n}`)) n++;
|
|
2072
|
+
const renamed = `${nm}_${n}`;
|
|
2073
|
+
used.add(renamed);
|
|
2074
|
+
child.meta = {
|
|
2075
|
+
...child.meta,
|
|
2076
|
+
name: renamed
|
|
2077
|
+
};
|
|
2078
|
+
this.warn(`Duplicate sibling name '${nm}'; renamed one occurrence to '${renamed}'`, parent);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
/** `opt`/`rep` with multiple children implicitly wrap them in a sequence,
|
|
2082
|
+
* which - like any sequence - is the output scope for those children. */
|
|
2083
|
+
wrapChildren(children, expanding, name, sink) {
|
|
2084
|
+
if (children.length === 1) return this.lowerNode(children[0], expanding, name, sink);
|
|
2085
|
+
const selfOutputs = [];
|
|
2086
|
+
const e = seq(...children.map((c) => this.lowerNode(c, expanding, name, selfOutputs)));
|
|
2087
|
+
if (selfOutputs.length > 0) e.meta = {
|
|
2088
|
+
...e.meta,
|
|
2089
|
+
outputs: selfOutputs
|
|
658
2090
|
};
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
2091
|
+
return e;
|
|
2092
|
+
}
|
|
2093
|
+
/** Fold an AST node's name/doc/default decorations onto an IR node's meta. */
|
|
2094
|
+
applyMeta(e, node) {
|
|
2095
|
+
const meta = {};
|
|
2096
|
+
if (node.name) meta.name = node.name;
|
|
2097
|
+
const doc = docFrom(node);
|
|
2098
|
+
if (doc) meta.doc = doc;
|
|
2099
|
+
if (node.default !== void 0) meta.defaultValue = node.default;
|
|
2100
|
+
if (Object.keys(meta).length > 0) e.meta = {
|
|
2101
|
+
...e.meta,
|
|
2102
|
+
...meta
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
toOutput(o, selfName) {
|
|
2106
|
+
const tokens = [];
|
|
2107
|
+
for (const t of o.tokens) {
|
|
2108
|
+
if (t.kind === "literal") {
|
|
2109
|
+
tokens.push({
|
|
2110
|
+
kind: "literal",
|
|
2111
|
+
value: t.value
|
|
2112
|
+
});
|
|
2113
|
+
continue;
|
|
671
2114
|
}
|
|
672
|
-
|
|
673
|
-
|
|
2115
|
+
const targetName = t.name ?? selfName;
|
|
2116
|
+
if (!targetName) {
|
|
2117
|
+
this.warn("Output self-reference '{}' has no named node to resolve to; emitting empty");
|
|
2118
|
+
tokens.push({
|
|
2119
|
+
kind: "literal",
|
|
2120
|
+
value: ""
|
|
2121
|
+
});
|
|
2122
|
+
continue;
|
|
2123
|
+
}
|
|
2124
|
+
if (t.stripPrefix?.length) this.warn("Output ref op 'strip_prefix' is not supported by the IR; ignored");
|
|
2125
|
+
if (t.basename) this.warn("Output ref op 'basename' is not supported by the IR; ignored");
|
|
2126
|
+
tokens.push({
|
|
2127
|
+
kind: "ref",
|
|
2128
|
+
target: nodeRef(targetName),
|
|
2129
|
+
...t.stripSuffix?.length && { stripExtensions: t.stripSuffix },
|
|
2130
|
+
...t.or !== void 0 && { fallback: t.or }
|
|
2131
|
+
});
|
|
674
2132
|
}
|
|
675
|
-
|
|
676
|
-
const
|
|
677
|
-
if (isArray$3(mutexGroups) && mutexGroups.length > 0) allNodes = this.applyMutualExclusion(actionsByDest, mutexGroups, allNodes, nodesByDest);
|
|
2133
|
+
if (o.fallback !== void 0) this.warn("Output-level '.or(...)' fallback is not supported by the IR; ignored");
|
|
2134
|
+
const doc = docFrom(o);
|
|
678
2135
|
return {
|
|
679
|
-
|
|
680
|
-
|
|
2136
|
+
...o.name && { name: o.name },
|
|
2137
|
+
...doc && { doc },
|
|
2138
|
+
tokens
|
|
681
2139
|
};
|
|
682
2140
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
}
|
|
691
|
-
errors: this.errors,
|
|
692
|
-
warnings: this.warnings
|
|
2141
|
+
};
|
|
2142
|
+
function buildAppMeta(fm, rootDoc, rootName) {
|
|
2143
|
+
const warnings = [];
|
|
2144
|
+
const rootTitleDesc = docFrom(rootDoc);
|
|
2145
|
+
if (!fm) {
|
|
2146
|
+
const meta = {
|
|
2147
|
+
...rootName && { id: rootName },
|
|
2148
|
+
...rootTitleDesc && { doc: rootTitleDesc }
|
|
693
2149
|
};
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
if (
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
2150
|
+
return Object.keys(meta).length > 0 ? {
|
|
2151
|
+
meta,
|
|
2152
|
+
warnings
|
|
2153
|
+
} : { warnings };
|
|
2154
|
+
}
|
|
2155
|
+
const asStr = (x) => typeof x === "string" && x.length > 0 ? x : void 0;
|
|
2156
|
+
const id = asStr(rootName) ?? asStr(fm.exe) ?? asStr(fm.id);
|
|
2157
|
+
const version = asStr(fm.version) ?? (typeof fm.version === "number" ? String(fm.version) : void 0);
|
|
2158
|
+
const strList = (x) => Array.isArray(x) ? x.filter((a) => typeof a === "string") : [];
|
|
2159
|
+
const authors = strList(fm.authors);
|
|
2160
|
+
const urls = strList(fm.urls);
|
|
2161
|
+
const references = strList(fm.references);
|
|
2162
|
+
const checkList = (key, v) => {
|
|
2163
|
+
if (v !== void 0 && v !== null && !Array.isArray(v)) warnings.push(`Frontmatter '${key}' should be a list; got a scalar - ignored`);
|
|
2164
|
+
};
|
|
2165
|
+
checkList("authors", fm.authors);
|
|
2166
|
+
checkList("urls", fm.urls);
|
|
2167
|
+
checkList("references", fm.references);
|
|
2168
|
+
if (fm.version !== void 0 && fm.version !== null && version === void 0) warnings.push(`Frontmatter 'version' should be a string or number; ignored`);
|
|
2169
|
+
if (fm.container !== void 0 && fm.container !== null) {
|
|
2170
|
+
if (!isRecord$1(fm.container)) warnings.push(`Frontmatter 'container' should be a mapping with an 'image'; ignored`);
|
|
2171
|
+
else if (!asStr(fm.container.image)) warnings.push(`Frontmatter 'container' is missing an 'image'; ignored`);
|
|
2172
|
+
}
|
|
2173
|
+
const doc = {
|
|
2174
|
+
...rootTitleDesc,
|
|
2175
|
+
...authors.length > 0 && { authors },
|
|
2176
|
+
...urls.length > 0 && { urls },
|
|
2177
|
+
...references.length > 0 && { literature: references }
|
|
2178
|
+
};
|
|
2179
|
+
const meta = {
|
|
2180
|
+
...id && { id },
|
|
2181
|
+
...version && { version },
|
|
2182
|
+
...Object.keys(doc).length > 0 && { doc }
|
|
2183
|
+
};
|
|
2184
|
+
if (isRecord$1(fm.container)) {
|
|
2185
|
+
const image = asStr(fm.container.image);
|
|
2186
|
+
const type = asStr(fm.container.type);
|
|
2187
|
+
if (image) meta.container = {
|
|
2188
|
+
image,
|
|
2189
|
+
...type === "docker" || type === "singularity" ? { type } : {}
|
|
726
2190
|
};
|
|
2191
|
+
}
|
|
2192
|
+
meta.stdout = streamFrom(fm.stdout, warnings, "stdout");
|
|
2193
|
+
meta.stderr = streamFrom(fm.stderr, warnings, "stderr");
|
|
2194
|
+
if (!meta.stdout) delete meta.stdout;
|
|
2195
|
+
if (!meta.stderr) delete meta.stderr;
|
|
2196
|
+
return {
|
|
2197
|
+
meta,
|
|
2198
|
+
warnings
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
function streamFrom(x, warnings, key) {
|
|
2202
|
+
if (x === void 0 || x === null) return void 0;
|
|
2203
|
+
if (isRecord$1(x)) {
|
|
2204
|
+
const name = typeof x.name === "string" ? x.name : void 0;
|
|
2205
|
+
if (!name) {
|
|
2206
|
+
warnings.push(`Frontmatter '${key}' is missing a 'name'; ignored`);
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
const description = typeof x.description === "string" ? x.description : void 0;
|
|
727
2210
|
return {
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
errors: this.errors,
|
|
731
|
-
warnings: this.warnings
|
|
2211
|
+
name,
|
|
2212
|
+
...description && { doc: { description } }
|
|
732
2213
|
};
|
|
733
2214
|
}
|
|
734
|
-
};
|
|
2215
|
+
if (typeof x === "string") return { name: x };
|
|
2216
|
+
warnings.push(`Frontmatter '${key}' has an unexpected shape; ignored`);
|
|
2217
|
+
}
|
|
2218
|
+
function isRecord$1(x) {
|
|
2219
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
2220
|
+
}
|
|
2221
|
+
function lowerDocument(doc) {
|
|
2222
|
+
const result = new Lowerer(doc.aliases).lower(doc);
|
|
2223
|
+
const exe = typeof doc.frontmatter?.exe === "string" ? doc.frontmatter.exe : void 0;
|
|
2224
|
+
if (exe) result.expr.attrs.nodes.unshift(lit(exe));
|
|
2225
|
+
const { meta, warnings } = buildAppMeta(doc.frontmatter, {
|
|
2226
|
+
title: doc.root.title,
|
|
2227
|
+
description: doc.root.description
|
|
2228
|
+
}, doc.rootName);
|
|
2229
|
+
return {
|
|
2230
|
+
...meta && { meta },
|
|
2231
|
+
expr: result.expr,
|
|
2232
|
+
errors: result.errors,
|
|
2233
|
+
warnings: [...result.warnings, ...warnings.map((message) => ({ message }))]
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
735
2236
|
|
|
736
2237
|
//#endregion
|
|
737
|
-
//#region src/
|
|
738
|
-
/**
|
|
739
|
-
function
|
|
2238
|
+
//#region src/frontend/argtype/parser-frontend.ts
|
|
2239
|
+
/** A fresh empty root sequence for error returns (IR passes mutate in place). */
|
|
2240
|
+
function emptyExpr$2() {
|
|
740
2241
|
return {
|
|
741
|
-
kind: "
|
|
742
|
-
|
|
2242
|
+
kind: "sequence",
|
|
2243
|
+
attrs: { nodes: [] }
|
|
743
2244
|
};
|
|
744
2245
|
}
|
|
745
2246
|
/**
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
*
|
|
749
|
-
*
|
|
2247
|
+
* Frontend for the argtype sugar DSL - the hand-authored, TypeScript-types-like
|
|
2248
|
+
* language for describing CLI argument grammars (see the argtype spec). Parses
|
|
2249
|
+
* the text into an AST, then lowers it to Styx IR + AppMeta.
|
|
2250
|
+
*
|
|
2251
|
+
* Supported today: the argtype core (combinators, terminals, literals, naming,
|
|
2252
|
+
* aliases, value constraints, `.join`/`.count`/`.default`, doc comments,
|
|
2253
|
+
* frontmatter) plus the `outputs`, `mediatypes`, and `paths` extensions. `set`
|
|
2254
|
+
* lowers to a sequence and `any` to its first branch; the `constraints`
|
|
2255
|
+
* extension is parsed-and-ignored.
|
|
750
2256
|
*/
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
2257
|
+
var ArgtypeParser = class {
|
|
2258
|
+
name = "argtype";
|
|
2259
|
+
extensions = ["argtype"];
|
|
2260
|
+
parse(source, _filename) {
|
|
2261
|
+
const { doc, errors: parseErrors, warnings: parseWarnings } = parseArgtype(source);
|
|
2262
|
+
const toLocation = (e) => e.line !== void 0 ? { location: {
|
|
2263
|
+
line: e.line,
|
|
2264
|
+
column: e.column
|
|
2265
|
+
} } : {};
|
|
2266
|
+
const errors = parseErrors.map((e) => ({
|
|
2267
|
+
message: e.message,
|
|
2268
|
+
...toLocation(e)
|
|
2269
|
+
}));
|
|
2270
|
+
const warnings = parseWarnings.map((e) => ({
|
|
2271
|
+
message: e.message,
|
|
2272
|
+
...toLocation(e)
|
|
2273
|
+
}));
|
|
2274
|
+
if (!doc) return {
|
|
2275
|
+
expr: emptyExpr$2(),
|
|
2276
|
+
errors,
|
|
2277
|
+
warnings
|
|
2278
|
+
};
|
|
2279
|
+
const lowered = lowerDocument(doc);
|
|
2280
|
+
warnings.push(...lowered.warnings.map((d) => ({
|
|
2281
|
+
message: d.message,
|
|
2282
|
+
...toLocation(d)
|
|
2283
|
+
})));
|
|
2284
|
+
errors.push(...lowered.errors.map((d) => ({
|
|
2285
|
+
message: d.message,
|
|
2286
|
+
...toLocation(d)
|
|
2287
|
+
})));
|
|
2288
|
+
return {
|
|
2289
|
+
...lowered.meta && { meta: lowered.meta },
|
|
2290
|
+
expr: lowered.expr,
|
|
2291
|
+
errors,
|
|
2292
|
+
warnings
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
};
|
|
754
2296
|
|
|
755
2297
|
//#endregion
|
|
756
2298
|
//#region src/frontend/boutiques/destruct-template.ts
|
|
@@ -772,20 +2314,26 @@ function destructTemplate(template, lookup) {
|
|
|
772
2314
|
destructed.push(x);
|
|
773
2315
|
continue;
|
|
774
2316
|
}
|
|
775
|
-
let
|
|
2317
|
+
let best = null;
|
|
776
2318
|
for (const [alias, replacement] of Object.entries(lookup)) {
|
|
2319
|
+
if (alias.length === 0) continue;
|
|
777
2320
|
const idx = x.indexOf(alias);
|
|
778
|
-
if (idx
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
2321
|
+
if (idx === -1) continue;
|
|
2322
|
+
if (best === null || idx < best.idx || idx === best.idx && alias.length > best.alias.length) best = {
|
|
2323
|
+
idx,
|
|
2324
|
+
alias,
|
|
2325
|
+
replacement
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
if (best === null) {
|
|
2329
|
+
destructed.push(x);
|
|
2330
|
+
continue;
|
|
787
2331
|
}
|
|
788
|
-
|
|
2332
|
+
const left = x.slice(0, best.idx);
|
|
2333
|
+
const right = x.slice(best.idx + best.alias.length);
|
|
2334
|
+
if (right.length > 0) stack.unshift(right);
|
|
2335
|
+
stack.unshift(best.replacement);
|
|
2336
|
+
if (left.length > 0) stack.unshift(left);
|
|
789
2337
|
}
|
|
790
2338
|
return destructed;
|
|
791
2339
|
}
|
|
@@ -1113,14 +2661,8 @@ var BoutiquesParser = class {
|
|
|
1113
2661
|
kind: "int",
|
|
1114
2662
|
attrs: {}
|
|
1115
2663
|
};
|
|
1116
|
-
if (isNumber$1(btInput.minimum))
|
|
1117
|
-
|
|
1118
|
-
if (btInput["exclusive-minimum"] === true) node.attrs.minValue += 1;
|
|
1119
|
-
}
|
|
1120
|
-
if (isNumber$1(btInput.maximum)) {
|
|
1121
|
-
node.attrs.maxValue = Math.floor(btInput.maximum);
|
|
1122
|
-
if (btInput["exclusive-maximum"] === true) node.attrs.maxValue -= 1;
|
|
1123
|
-
}
|
|
2664
|
+
if (isNumber$1(btInput.minimum)) node.attrs.minValue = btInput["exclusive-minimum"] === true ? Math.floor(btInput.minimum) + 1 : Math.ceil(btInput.minimum);
|
|
2665
|
+
if (isNumber$1(btInput.maximum)) node.attrs.maxValue = btInput["exclusive-maximum"] === true ? Math.ceil(btInput.maximum) - 1 : Math.floor(btInput.maximum);
|
|
1124
2666
|
if (meta) node.meta = meta;
|
|
1125
2667
|
return node;
|
|
1126
2668
|
}
|
|
@@ -1170,7 +2712,10 @@ var BoutiquesParser = class {
|
|
|
1170
2712
|
return null;
|
|
1171
2713
|
}
|
|
1172
2714
|
const node = this.parseDescriptor(nested);
|
|
1173
|
-
if (node && meta) node.meta =
|
|
2715
|
+
if (node && meta) node.meta = {
|
|
2716
|
+
...node.meta,
|
|
2717
|
+
...meta
|
|
2718
|
+
};
|
|
1174
2719
|
return node;
|
|
1175
2720
|
}
|
|
1176
2721
|
case InputTypePrimitive.SubCommandUnion: {
|
|
@@ -1269,13 +2814,16 @@ var BoutiquesParser = class {
|
|
|
1269
2814
|
node = this.wrapWithFlag(node, btInput);
|
|
1270
2815
|
if (inputType.isOptional) node = this.wrapWithOptional(node);
|
|
1271
2816
|
if (node !== inner && inner.meta) {
|
|
1272
|
-
const { name, ...rest } = inner.meta;
|
|
2817
|
+
const { name, outputs, ...rest } = inner.meta;
|
|
1273
2818
|
if (Object.keys(rest).length > 0) {
|
|
1274
2819
|
node.meta = {
|
|
1275
2820
|
...node.meta,
|
|
1276
2821
|
...rest
|
|
1277
2822
|
};
|
|
1278
|
-
|
|
2823
|
+
const kept = {};
|
|
2824
|
+
if (name) kept.name = name;
|
|
2825
|
+
if (outputs) kept.outputs = outputs;
|
|
2826
|
+
inner.meta = Object.keys(kept).length > 0 ? kept : void 0;
|
|
1279
2827
|
}
|
|
1280
2828
|
}
|
|
1281
2829
|
return node;
|
|
@@ -1345,123 +2893,36 @@ var BoutiquesParser = class {
|
|
|
1345
2893
|
return {
|
|
1346
2894
|
expr: {
|
|
1347
2895
|
kind: "sequence",
|
|
1348
|
-
attrs: { nodes: [] }
|
|
1349
|
-
},
|
|
1350
|
-
errors: this.errors,
|
|
1351
|
-
warnings: this.warnings
|
|
1352
|
-
};
|
|
1353
|
-
}
|
|
1354
|
-
const expr = this.parseDescriptor(bt);
|
|
1355
|
-
if (expr === null) {
|
|
1356
|
-
this.error("Failed to parse command structure");
|
|
1357
|
-
return {
|
|
1358
|
-
expr: {
|
|
1359
|
-
kind: "sequence",
|
|
1360
|
-
attrs: { nodes: [] }
|
|
1361
|
-
},
|
|
1362
|
-
errors: this.errors,
|
|
1363
|
-
warnings: this.warnings
|
|
1364
|
-
};
|
|
1365
|
-
}
|
|
1366
|
-
if (!expr.meta?.name && baseMeta?.id) expr.meta = {
|
|
1367
|
-
...expr.meta,
|
|
1368
|
-
name: baseMeta.id
|
|
1369
|
-
};
|
|
1370
|
-
return {
|
|
1371
|
-
meta: baseMeta,
|
|
1372
|
-
expr,
|
|
1373
|
-
errors: this.errors,
|
|
1374
|
-
warnings: this.warnings
|
|
1375
|
-
};
|
|
1376
|
-
}
|
|
1377
|
-
};
|
|
1378
|
-
|
|
1379
|
-
//#endregion
|
|
1380
|
-
//#region src/ir/builders.ts
|
|
1381
|
-
function lit(str) {
|
|
1382
|
-
return {
|
|
1383
|
-
kind: "literal",
|
|
1384
|
-
attrs: { str }
|
|
1385
|
-
};
|
|
1386
|
-
}
|
|
1387
|
-
function str(meta) {
|
|
1388
|
-
return {
|
|
1389
|
-
kind: "str",
|
|
1390
|
-
attrs: {},
|
|
1391
|
-
meta: normalizeMeta(meta)
|
|
1392
|
-
};
|
|
1393
|
-
}
|
|
1394
|
-
function int(meta) {
|
|
1395
|
-
return {
|
|
1396
|
-
kind: "int",
|
|
1397
|
-
attrs: {},
|
|
1398
|
-
meta: normalizeMeta(meta)
|
|
1399
|
-
};
|
|
1400
|
-
}
|
|
1401
|
-
function float(meta) {
|
|
1402
|
-
return {
|
|
1403
|
-
kind: "float",
|
|
1404
|
-
attrs: {},
|
|
1405
|
-
meta: normalizeMeta(meta)
|
|
1406
|
-
};
|
|
1407
|
-
}
|
|
1408
|
-
function path(meta) {
|
|
1409
|
-
return {
|
|
1410
|
-
kind: "path",
|
|
1411
|
-
attrs: {},
|
|
1412
|
-
meta: normalizeMeta(meta)
|
|
1413
|
-
};
|
|
1414
|
-
}
|
|
1415
|
-
function seq(...nodes) {
|
|
1416
|
-
return {
|
|
1417
|
-
kind: "sequence",
|
|
1418
|
-
attrs: { nodes }
|
|
1419
|
-
};
|
|
1420
|
-
}
|
|
1421
|
-
function seqJoin(join, ...nodes) {
|
|
1422
|
-
return {
|
|
1423
|
-
kind: "sequence",
|
|
1424
|
-
attrs: {
|
|
1425
|
-
nodes,
|
|
1426
|
-
join
|
|
2896
|
+
attrs: { nodes: [] }
|
|
2897
|
+
},
|
|
2898
|
+
errors: this.errors,
|
|
2899
|
+
warnings: this.warnings
|
|
2900
|
+
};
|
|
1427
2901
|
}
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
};
|
|
1453
|
-
}
|
|
1454
|
-
function alt(...alts) {
|
|
1455
|
-
return {
|
|
1456
|
-
kind: "alternative",
|
|
1457
|
-
attrs: { alts }
|
|
1458
|
-
};
|
|
1459
|
-
}
|
|
1460
|
-
function normalizeMeta(meta) {
|
|
1461
|
-
if (meta === void 0) return void 0;
|
|
1462
|
-
if (typeof meta === "string") return { name: meta };
|
|
1463
|
-
return meta;
|
|
1464
|
-
}
|
|
2902
|
+
const expr = this.parseDescriptor(bt);
|
|
2903
|
+
if (expr === null) {
|
|
2904
|
+
this.error("Failed to parse command structure");
|
|
2905
|
+
return {
|
|
2906
|
+
expr: {
|
|
2907
|
+
kind: "sequence",
|
|
2908
|
+
attrs: { nodes: [] }
|
|
2909
|
+
},
|
|
2910
|
+
errors: this.errors,
|
|
2911
|
+
warnings: this.warnings
|
|
2912
|
+
};
|
|
2913
|
+
}
|
|
2914
|
+
if (!expr.meta?.name && baseMeta?.id) expr.meta = {
|
|
2915
|
+
...expr.meta,
|
|
2916
|
+
name: baseMeta.id
|
|
2917
|
+
};
|
|
2918
|
+
return {
|
|
2919
|
+
meta: baseMeta,
|
|
2920
|
+
expr,
|
|
2921
|
+
errors: this.errors,
|
|
2922
|
+
warnings: this.warnings
|
|
2923
|
+
};
|
|
2924
|
+
}
|
|
2925
|
+
};
|
|
1465
2926
|
|
|
1466
2927
|
//#endregion
|
|
1467
2928
|
//#region src/backend/string-case.ts
|
|
@@ -2211,27 +3672,621 @@ function extractVersion(versionInfo) {
|
|
|
2211
3672
|
}
|
|
2212
3673
|
|
|
2213
3674
|
//#endregion
|
|
2214
|
-
//#region src/frontend/detect-format.ts
|
|
3675
|
+
//#region src/frontend/detect-format.ts
|
|
3676
|
+
/** The first non-whitespace character of a source ("" if it is all blank). */
|
|
3677
|
+
function firstNonBlankChar(source) {
|
|
3678
|
+
const match = source.match(/\S/);
|
|
3679
|
+
return match ? match[0] : "";
|
|
3680
|
+
}
|
|
3681
|
+
/**
|
|
3682
|
+
* Auto-detect the format of a descriptor source string.
|
|
3683
|
+
*
|
|
3684
|
+
* Every JSON frontend (boutiques, argdump, workbench, mrtrix) is a top-level
|
|
3685
|
+
* object, so the first non-blank character being `{` is the signal for "some
|
|
3686
|
+
* JSON format"; we then inspect its keys to pick which one. Anything else
|
|
3687
|
+
* non-blank is treated as the argtype DSL, whose sources open with a terminal
|
|
3688
|
+
* (`int`), a literal (`"hello"`), a combinator (`seq(...)`), a `name: expr`
|
|
3689
|
+
* definition, or a `---` frontmatter fence - never with `{`.
|
|
3690
|
+
*
|
|
3691
|
+
* Deciding on the leading character (rather than trying `JSON.parse` first) is
|
|
3692
|
+
* what lets standalone argtype snippets like `"hello"` or `42` be recognized:
|
|
3693
|
+
* those are *also* valid JSON scalars, so a parse-first approach would swallow
|
|
3694
|
+
* them and then reject them as "not an object".
|
|
3695
|
+
*
|
|
3696
|
+
* Returns null only when the source is blank, or opens with `{` but matches no
|
|
3697
|
+
* known JSON format (ambiguous, or still being typed).
|
|
3698
|
+
*/
|
|
3699
|
+
function detectFormat(source) {
|
|
3700
|
+
const first = firstNonBlankChar(source);
|
|
3701
|
+
if (first === "") return null;
|
|
3702
|
+
if (first !== "{") return "argtype";
|
|
3703
|
+
let parsed;
|
|
3704
|
+
try {
|
|
3705
|
+
parsed = JSON.parse(source);
|
|
3706
|
+
} catch {
|
|
3707
|
+
return null;
|
|
3708
|
+
}
|
|
3709
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
3710
|
+
const obj = parsed;
|
|
3711
|
+
if (typeof obj.$schema === "string" && obj.$schema.includes("argdump")) return "argdump";
|
|
3712
|
+
if (typeof obj.command === "string" && typeof obj.short_description === "string") return "workbench";
|
|
3713
|
+
if (typeof obj.synopsis === "string" && Array.isArray(obj.option_groups) && Array.isArray(obj.arguments)) return "mrtrix";
|
|
3714
|
+
if ("command-line" in obj || Array.isArray(obj.inputs) && "name" in obj) return "boutiques";
|
|
3715
|
+
if (Array.isArray(obj.actions) && "prog" in obj) return "argdump";
|
|
3716
|
+
return null;
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
//#endregion
|
|
3720
|
+
//#region src/backend/scope.ts
|
|
3721
|
+
/** Symbol collision avoidance for code generation. */
|
|
3722
|
+
var Scope = class Scope {
|
|
3723
|
+
reserved;
|
|
3724
|
+
used;
|
|
3725
|
+
parent;
|
|
3726
|
+
constructor(reserved = [], parent) {
|
|
3727
|
+
this.reserved = new Set(reserved);
|
|
3728
|
+
this.used = /* @__PURE__ */ new Set();
|
|
3729
|
+
this.parent = parent;
|
|
3730
|
+
}
|
|
3731
|
+
/** Check if a symbol is already taken (in this scope or any parent). */
|
|
3732
|
+
has(symbol) {
|
|
3733
|
+
return this.reserved.has(symbol) || this.used.has(symbol) || (this.parent?.has(symbol) ?? false);
|
|
3734
|
+
}
|
|
3735
|
+
/**
|
|
3736
|
+
* Add a symbol, appending a numeric suffix to avoid collisions. Returns the
|
|
3737
|
+
* safe name.
|
|
3738
|
+
*
|
|
3739
|
+
* When a `recase` transform is given, a disambiguated candidate is routed back
|
|
3740
|
+
* through it so the suffix is absorbed into the identifier's casing - e.g.
|
|
3741
|
+
* `pascalCase` folds `Config_2` into `Config2` - rather than leaving a
|
|
3742
|
+
* mixed-case `Config_2`. Uniqueness is always checked on the final emitted
|
|
3743
|
+
* form, so two hints that case-collide still get distinct names. Defaults to
|
|
3744
|
+
* identity (the bare `<name>_<n>` suffix) for callers that don't case-normalize.
|
|
3745
|
+
*/
|
|
3746
|
+
add(candidate, recase = (s) => s) {
|
|
3747
|
+
if (!this.has(candidate)) {
|
|
3748
|
+
this.used.add(candidate);
|
|
3749
|
+
return candidate;
|
|
3750
|
+
}
|
|
3751
|
+
let suffix = 2;
|
|
3752
|
+
let safe = recase(`${candidate}_${suffix}`);
|
|
3753
|
+
while (this.has(safe)) {
|
|
3754
|
+
suffix++;
|
|
3755
|
+
safe = recase(`${candidate}_${suffix}`);
|
|
3756
|
+
}
|
|
3757
|
+
this.used.add(safe);
|
|
3758
|
+
return safe;
|
|
3759
|
+
}
|
|
3760
|
+
/** Create a child scope that inherits this scope's restrictions. */
|
|
3761
|
+
child(reserved = []) {
|
|
3762
|
+
return new Scope(reserved, this);
|
|
3763
|
+
}
|
|
3764
|
+
};
|
|
3765
|
+
|
|
3766
|
+
//#endregion
|
|
3767
|
+
//#region src/backend/argtype/emit.ts
|
|
3768
|
+
const INDENT = " ";
|
|
3769
|
+
/** Target content width (excluding the `/// ` prefix and indentation) for a
|
|
3770
|
+
* wrapped `///` description line. */
|
|
3771
|
+
const DOC_WIDTH = 80;
|
|
3772
|
+
function pad(level) {
|
|
3773
|
+
return INDENT.repeat(level);
|
|
3774
|
+
}
|
|
3775
|
+
/**
|
|
3776
|
+
* Wrap a description into `///`-ready content lines. Paragraphs (separated by a
|
|
3777
|
+
* blank line) are word-wrapped to `width`; an empty string marks a paragraph
|
|
3778
|
+
* break (the caller emits it as a bare `///`).
|
|
3779
|
+
*
|
|
3780
|
+
* The argtype frontend reflows a `///` block as prose - a single line break
|
|
3781
|
+
* rejoins with a space, a blank line separates paragraphs - so wrapping here is
|
|
3782
|
+
* the inverse: it round-trips paragraph text exactly (whitespace within a
|
|
3783
|
+
* paragraph is normalized to single spaces, matching the frontend's reflow), and
|
|
3784
|
+
* keeps a long description from emitting as one giant physical line.
|
|
3785
|
+
*/
|
|
3786
|
+
function wrapDoc(text, width) {
|
|
3787
|
+
const lines = [];
|
|
3788
|
+
for (const paragraph of text.split(/\n{2,}/)) {
|
|
3789
|
+
const words = paragraph.split(/\s+/).filter((w) => w.length > 0);
|
|
3790
|
+
if (words.length === 0) continue;
|
|
3791
|
+
if (lines.length > 0) lines.push("");
|
|
3792
|
+
let cur = "";
|
|
3793
|
+
for (const word of words) if (cur === "") cur = word;
|
|
3794
|
+
else if (cur.length + 1 + word.length <= width) cur += ` ${word}`;
|
|
3795
|
+
else {
|
|
3796
|
+
lines.push(cur);
|
|
3797
|
+
cur = word;
|
|
3798
|
+
}
|
|
3799
|
+
if (cur !== "") lines.push(cur);
|
|
3800
|
+
}
|
|
3801
|
+
return lines;
|
|
3802
|
+
}
|
|
3803
|
+
function num(n) {
|
|
3804
|
+
return String(n);
|
|
3805
|
+
}
|
|
3806
|
+
/** Escape a string for a double-quoted argtype literal (matches the lexer). */
|
|
3807
|
+
function escapeString(s) {
|
|
3808
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(/\r/g, "\\r");
|
|
3809
|
+
}
|
|
3810
|
+
function quote(s) {
|
|
3811
|
+
return `"${escapeString(s)}"`;
|
|
3812
|
+
}
|
|
3813
|
+
/** A value terminal kind whose `meta.defaultValue` argtype can express. */
|
|
3814
|
+
function isValueTerminal(node) {
|
|
3815
|
+
return node.kind === "int" || node.kind === "float" || node.kind === "str" || node.kind === "path";
|
|
3816
|
+
}
|
|
3817
|
+
/**
|
|
3818
|
+
* Find the single value terminal a wrapper's default should attach to. Descends
|
|
3819
|
+
* through optional/repeat and a sequence with exactly one non-literal child
|
|
3820
|
+
* (the flag-value pattern `seq(lit, value)`). Returns undefined when the value
|
|
3821
|
+
* slot is ambiguous (multiple non-literal children) or absent (a bare literal
|
|
3822
|
+
* flag), in which case the wrapper default has no terminal to sink onto.
|
|
3823
|
+
*/
|
|
3824
|
+
function findValueTerminal(node) {
|
|
3825
|
+
switch (node.kind) {
|
|
3826
|
+
case "int":
|
|
3827
|
+
case "float":
|
|
3828
|
+
case "str":
|
|
3829
|
+
case "path": return node;
|
|
3830
|
+
case "optional": return findValueTerminal(node.attrs.node);
|
|
3831
|
+
case "repeat": return findValueTerminal(node.attrs.node);
|
|
3832
|
+
case "sequence": {
|
|
3833
|
+
const nonLiteral = node.attrs.nodes.filter((n) => n.kind !== "literal");
|
|
3834
|
+
return nonLiteral.length === 1 ? findValueTerminal(nonLiteral[0]) : void 0;
|
|
3835
|
+
}
|
|
3836
|
+
default: return;
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
/**
|
|
3840
|
+
* Push a wrapper's `meta.defaultValue` down onto its inner value terminal so it
|
|
3841
|
+
* can be emitted as `int = 5` / `.default(...)`. Boutiques (and argparse) hoist
|
|
3842
|
+
* an input's default onto the outermost wrapper (`optional` / `sequence`); the
|
|
3843
|
+
* argtype surface only carries `.default()` on a terminal, so we relocate it.
|
|
3844
|
+
* Boolean defaults are left in place: those are the `opt(literal)` flag-false
|
|
3845
|
+
* convention, which the frontend regenerates for free and which has no terminal.
|
|
3846
|
+
*/
|
|
3847
|
+
function sinkDefaults(expr) {
|
|
3848
|
+
const clone = structuredClone(expr);
|
|
3849
|
+
const visit = (node) => {
|
|
3850
|
+
switch (node.kind) {
|
|
3851
|
+
case "optional":
|
|
3852
|
+
visit(node.attrs.node);
|
|
3853
|
+
sinkInto(node);
|
|
3854
|
+
break;
|
|
3855
|
+
case "repeat":
|
|
3856
|
+
visit(node.attrs.node);
|
|
3857
|
+
sinkInto(node);
|
|
3858
|
+
break;
|
|
3859
|
+
case "sequence":
|
|
3860
|
+
for (const c of node.attrs.nodes) visit(c);
|
|
3861
|
+
sinkInto(node);
|
|
3862
|
+
break;
|
|
3863
|
+
case "alternative":
|
|
3864
|
+
for (const c of node.attrs.alts) visit(c);
|
|
3865
|
+
break;
|
|
3866
|
+
default: break;
|
|
3867
|
+
}
|
|
3868
|
+
};
|
|
3869
|
+
visit(clone);
|
|
3870
|
+
return clone;
|
|
3871
|
+
}
|
|
3872
|
+
function sinkInto(wrapper) {
|
|
3873
|
+
const dv = wrapper.meta?.defaultValue;
|
|
3874
|
+
if (dv === void 0 || typeof dv === "boolean") return;
|
|
3875
|
+
const terminal = findValueTerminal(wrapper);
|
|
3876
|
+
if (!terminal || !isValueTerminal(terminal) || terminal.meta?.defaultValue !== void 0) return;
|
|
3877
|
+
terminal.meta = {
|
|
3878
|
+
...terminal.meta,
|
|
3879
|
+
defaultValue: dv
|
|
3880
|
+
};
|
|
3881
|
+
const meta = { ...wrapper.meta };
|
|
3882
|
+
delete meta.defaultValue;
|
|
3883
|
+
wrapper.meta = Object.keys(meta).length > 0 ? meta : void 0;
|
|
3884
|
+
}
|
|
3885
|
+
/**
|
|
3886
|
+
* Detect the synthetic single-child sequence the frontend wraps a non-sequence
|
|
3887
|
+
* root in. Such a wrapper carries only `name` (matching the child's name) plus
|
|
3888
|
+
* any collected `outputs`; the doc/default live on the inner node. Returns the
|
|
3889
|
+
* inner node and the wrapper's outputs so the caller can emit the inner node as
|
|
3890
|
+
* the definition body (the frontend re-wraps it identically on re-parse).
|
|
3891
|
+
*/
|
|
3892
|
+
function syntheticWrap(root) {
|
|
3893
|
+
if (root.kind !== "sequence" || root.attrs.nodes.length !== 1 || root.attrs.join !== void 0) return;
|
|
3894
|
+
const meta = root.meta;
|
|
3895
|
+
if (meta?.doc || meta?.defaultValue !== void 0 || meta?.variantTag !== void 0) return;
|
|
3896
|
+
const child = root.attrs.nodes[0];
|
|
3897
|
+
if (child.kind === "sequence") return void 0;
|
|
3898
|
+
if ((child.meta?.name ?? void 0) !== (meta?.name ?? void 0)) return void 0;
|
|
3899
|
+
return {
|
|
3900
|
+
child,
|
|
3901
|
+
...meta?.outputs && { outputs: meta.outputs }
|
|
3902
|
+
};
|
|
3903
|
+
}
|
|
3904
|
+
const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3905
|
+
/** A name rendered where an identifier is expected: bare when it is a valid
|
|
3906
|
+
* identifier, otherwise a double-quoted form preserving it exactly. Used for
|
|
3907
|
+
* both `label:` prefixes and output-template `{ref}` targets (templates accept a
|
|
3908
|
+
* quoted name, `{"4d_output"}`), so the two always agree and no lossy
|
|
3909
|
+
* sanitization is needed. */
|
|
3910
|
+
function identOrQuoted(name) {
|
|
3911
|
+
return IDENT_RE.test(name) ? name : quote(name);
|
|
3912
|
+
}
|
|
3913
|
+
var ArgtypeEmitter = class {
|
|
3914
|
+
warnings = [];
|
|
3915
|
+
warn(message) {
|
|
3916
|
+
this.warnings.push({ message });
|
|
3917
|
+
}
|
|
3918
|
+
/** Render a name as it appears before a `:` label (bare identifier or quoted
|
|
3919
|
+
* label, e.g. `"1deval":`, `"1D":`). */
|
|
3920
|
+
labelFor(name) {
|
|
3921
|
+
return identOrQuoted(name);
|
|
3922
|
+
}
|
|
3923
|
+
/**
|
|
3924
|
+
* `///` doc lines for a `Documentation`. A title is written as a leading
|
|
3925
|
+
* Markdown H1 (`# Title`); the description follows after a blank `///` line.
|
|
3926
|
+
* Either alone emits just that part.
|
|
3927
|
+
*/
|
|
3928
|
+
docLines(doc) {
|
|
3929
|
+
if (!doc) return [];
|
|
3930
|
+
const out = [];
|
|
3931
|
+
if (doc.title) out.push(`/// # ${doc.title}`);
|
|
3932
|
+
if (doc.title && doc.description) out.push("///");
|
|
3933
|
+
if (doc.description) for (const line of wrapDoc(doc.description, DOC_WIDTH)) out.push(line === "" ? "///" : `/// ${line}`);
|
|
3934
|
+
return out;
|
|
3935
|
+
}
|
|
3936
|
+
/**
|
|
3937
|
+
* Whether a `///` block would not round-trip, so the doc must instead be
|
|
3938
|
+
* emitted as `.title()` / `.description()` chaining (which sets the fields
|
|
3939
|
+
* verbatim). Three cases:
|
|
3940
|
+
* - a multi-line title (only its first line would survive `splitDocText`);
|
|
3941
|
+
* - a description with a lone line break (a single `\n`, not a blank-line
|
|
3942
|
+
* paragraph break), which a `///` block reflows to a space - chaining keeps
|
|
3943
|
+
* the hard break intact;
|
|
3944
|
+
* - a title-less description whose first line looks like an H1 heading
|
|
3945
|
+
* (`# ...`), which would be promoted to a spurious title.
|
|
3946
|
+
* A blank-line paragraph break is safe: the frontend reflows a `///` block back
|
|
3947
|
+
* into the same paragraphs, and a description under a title is safe too
|
|
3948
|
+
* (`splitDocText` consumes only the very first line as the title).
|
|
3949
|
+
*/
|
|
3950
|
+
docNeedsChain(doc) {
|
|
3951
|
+
if (!doc) return false;
|
|
3952
|
+
if (doc.title?.includes("\n")) return true;
|
|
3953
|
+
const desc = doc.description;
|
|
3954
|
+
if (desc) {
|
|
3955
|
+
if (desc.split(/\n{2,}/).some((para) => para.includes("\n"))) return true;
|
|
3956
|
+
if (!doc.title && (desc.split("\n")[0] ?? "").trim().startsWith("# ")) return true;
|
|
3957
|
+
}
|
|
3958
|
+
return false;
|
|
3959
|
+
}
|
|
3960
|
+
/** `.title(...)` / `.description(...)` chaining for a doc that cannot round-trip
|
|
3961
|
+
* as a `///` block (see `docNeedsChain`). */
|
|
3962
|
+
docChain(doc) {
|
|
3963
|
+
if (!doc) return "";
|
|
3964
|
+
let chain = "";
|
|
3965
|
+
if (doc.title) chain += `.title(${quote(doc.title)})`;
|
|
3966
|
+
if (doc.description) chain += `.description(${quote(doc.description)})`;
|
|
3967
|
+
return chain;
|
|
3968
|
+
}
|
|
3969
|
+
/**
|
|
3970
|
+
* Warn for `Documentation` fields a node's `///` comment cannot carry. `title`
|
|
3971
|
+
* and `description` are emitted (see `docLines`); literature / comment /
|
|
3972
|
+
* authors / urls attached to an inner node have no surface, so surface them
|
|
3973
|
+
* rather than dropping silently.
|
|
3974
|
+
*/
|
|
3975
|
+
warnUnrepresentableDoc(doc, where) {
|
|
3976
|
+
if (!doc) return;
|
|
3977
|
+
const lost = [];
|
|
3978
|
+
if (doc.literature?.length) lost.push("literature");
|
|
3979
|
+
if (doc.comment) lost.push("comment");
|
|
3980
|
+
if (doc.authors?.length) lost.push("authors");
|
|
3981
|
+
if (doc.urls?.length) lost.push("urls");
|
|
3982
|
+
if (lost.length > 0) this.warn(`Documentation ${lost.join("/")} on ${where} has no argtype node surface; ignored`);
|
|
3983
|
+
}
|
|
3984
|
+
emit(expr, app) {
|
|
3985
|
+
const raw = sinkDefaults(expr);
|
|
3986
|
+
const lines = [];
|
|
3987
|
+
let exe;
|
|
3988
|
+
let root = raw;
|
|
3989
|
+
if (raw.kind === "sequence" && raw.attrs.nodes[0]?.kind === "literal") {
|
|
3990
|
+
exe = raw.attrs.nodes[0].attrs.str;
|
|
3991
|
+
root = {
|
|
3992
|
+
kind: "sequence",
|
|
3993
|
+
attrs: {
|
|
3994
|
+
...raw.attrs,
|
|
3995
|
+
nodes: raw.attrs.nodes.slice(1)
|
|
3996
|
+
}
|
|
3997
|
+
};
|
|
3998
|
+
if (raw.meta) root.meta = raw.meta;
|
|
3999
|
+
}
|
|
4000
|
+
const frontmatter = this.emitFrontmatter(app, exe);
|
|
4001
|
+
if (frontmatter) lines.push(frontmatter, "");
|
|
4002
|
+
const synthetic = syntheticWrap(root);
|
|
4003
|
+
const defNode = synthetic ? synthetic.child : root;
|
|
4004
|
+
const wrapperOutputs = synthetic ? synthetic.outputs : void 0;
|
|
4005
|
+
const rootDoc = defNode.meta?.doc ?? app?.doc;
|
|
4006
|
+
const rootChain = this.docNeedsChain(rootDoc);
|
|
4007
|
+
if (!rootChain) lines.push(...this.docLines(rootDoc));
|
|
4008
|
+
this.warnUnrepresentableDoc(defNode.meta?.doc, "the root node");
|
|
4009
|
+
const rootName = this.labelFor(defNode.meta?.name || app?.id || "tool");
|
|
4010
|
+
let body = this.emitNode(defNode, 0);
|
|
4011
|
+
if (rootChain) body += this.docChain(rootDoc);
|
|
4012
|
+
if (wrapperOutputs?.length) body += this.emitOutputs(wrapperOutputs, 0);
|
|
4013
|
+
lines.push(`${rootName}: ${body}`);
|
|
4014
|
+
return lines.join("\n") + "\n";
|
|
4015
|
+
}
|
|
4016
|
+
/**
|
|
4017
|
+
* Emit a node's core expression plus its node-local chains (value
|
|
4018
|
+
* constraints, default, join, count, media types) and any `.output(...)`.
|
|
4019
|
+
* The first line is not indented (the caller places it after a label or pad);
|
|
4020
|
+
* continuation lines are indented relative to `level`.
|
|
4021
|
+
*/
|
|
4022
|
+
emitNode(expr, level) {
|
|
4023
|
+
let core;
|
|
4024
|
+
switch (expr.kind) {
|
|
4025
|
+
case "literal":
|
|
4026
|
+
core = quote(expr.attrs.str);
|
|
4027
|
+
break;
|
|
4028
|
+
case "str":
|
|
4029
|
+
core = "str" + this.defaultSuffix(expr, false);
|
|
4030
|
+
break;
|
|
4031
|
+
case "int":
|
|
4032
|
+
case "float": {
|
|
4033
|
+
core = expr.kind;
|
|
4034
|
+
let chained = false;
|
|
4035
|
+
const min = this.finiteNum(expr.attrs.minValue, "min");
|
|
4036
|
+
if (min !== void 0) {
|
|
4037
|
+
core += `.min(${min})`;
|
|
4038
|
+
chained = true;
|
|
4039
|
+
}
|
|
4040
|
+
const max = this.finiteNum(expr.attrs.maxValue, "max");
|
|
4041
|
+
if (max !== void 0) {
|
|
4042
|
+
core += `.max(${max})`;
|
|
4043
|
+
chained = true;
|
|
4044
|
+
}
|
|
4045
|
+
core += this.defaultSuffix(expr, chained);
|
|
4046
|
+
break;
|
|
4047
|
+
}
|
|
4048
|
+
case "path": {
|
|
4049
|
+
core = "path";
|
|
4050
|
+
const media = expr.attrs.mediaTypes ?? [];
|
|
4051
|
+
for (const m of media) core += `.mediaType(${quote(m)})`;
|
|
4052
|
+
if (expr.attrs.mutable) core += ".mutable()";
|
|
4053
|
+
if (expr.attrs.resolveParent) core += ".resolveParent()";
|
|
4054
|
+
const chained = media.length > 0 || !!expr.attrs.mutable || !!expr.attrs.resolveParent;
|
|
4055
|
+
core += this.defaultSuffix(expr, chained);
|
|
4056
|
+
break;
|
|
4057
|
+
}
|
|
4058
|
+
case "sequence":
|
|
4059
|
+
core = this.emitCombinator("seq", expr.attrs.nodes, level, expr.attrs.join);
|
|
4060
|
+
core += this.structuralDefaultSuffix(expr);
|
|
4061
|
+
break;
|
|
4062
|
+
case "optional":
|
|
4063
|
+
core = this.emitOptional(expr, level) + this.structuralDefaultSuffix(expr);
|
|
4064
|
+
break;
|
|
4065
|
+
case "repeat":
|
|
4066
|
+
core = this.emitRepeat(expr, level) + this.structuralDefaultSuffix(expr);
|
|
4067
|
+
break;
|
|
4068
|
+
case "alternative":
|
|
4069
|
+
for (const arm of expr.attrs.alts) {
|
|
4070
|
+
const tag = arm.meta?.variantTag;
|
|
4071
|
+
if (tag !== void 0 && tag !== arm.meta?.name) this.warn(`Union arm discriminator '${tag}' has no argtype surface and will be re-derived from the arm name '${arm.meta?.name ?? "<unnamed>"}' on re-parse`);
|
|
4072
|
+
}
|
|
4073
|
+
core = this.emitCombinator("alt", expr.attrs.alts, level) + this.structuralDefaultSuffix(expr);
|
|
4074
|
+
break;
|
|
4075
|
+
default: core = "";
|
|
4076
|
+
}
|
|
4077
|
+
if (expr.meta?.outputs?.length) core += this.emitOutputs(expr.meta.outputs, level);
|
|
4078
|
+
return core;
|
|
4079
|
+
}
|
|
4080
|
+
/** `String(n)` when `n` is finite; otherwise undefined with a warning, since
|
|
4081
|
+
* `Infinity`/`NaN` have no argtype number literal (emitting them would produce
|
|
4082
|
+
* an identifier that fails to re-parse). */
|
|
4083
|
+
finiteNum(n, where) {
|
|
4084
|
+
if (n === void 0) return void 0;
|
|
4085
|
+
if (!Number.isFinite(n)) {
|
|
4086
|
+
this.warn(`Non-finite number (${String(n)}) on ${where} has no argtype literal; ignored`);
|
|
4087
|
+
return;
|
|
4088
|
+
}
|
|
4089
|
+
return num(n);
|
|
4090
|
+
}
|
|
4091
|
+
/** `= value` on a bare terminal, else `.default(value)` once a chain started. */
|
|
4092
|
+
defaultSuffix(expr, chained) {
|
|
4093
|
+
const dv = expr.meta?.defaultValue;
|
|
4094
|
+
if (dv === void 0 || typeof dv === "boolean") return "";
|
|
4095
|
+
let value;
|
|
4096
|
+
if (typeof dv === "number") {
|
|
4097
|
+
const n = this.finiteNum(dv, "default");
|
|
4098
|
+
if (n === void 0) return "";
|
|
4099
|
+
value = n;
|
|
4100
|
+
} else value = quote(dv);
|
|
4101
|
+
return chained ? `.default(${value})` : ` = ${value}`;
|
|
4102
|
+
}
|
|
4103
|
+
/**
|
|
4104
|
+
* A `.default(value)` chained onto a structural node (e.g. an enum
|
|
4105
|
+
* `alternative`, or a wrapper whose default could not sink onto an inner
|
|
4106
|
+
* terminal). Booleans have no argtype value literal: the only legitimate one
|
|
4107
|
+
* is the `opt(literal)` flag-false convention, which the frontend regenerates,
|
|
4108
|
+
* so it is silently dropped; any other boolean default is warned and dropped.
|
|
4109
|
+
*/
|
|
4110
|
+
structuralDefaultSuffix(expr) {
|
|
4111
|
+
const dv = expr.meta?.defaultValue;
|
|
4112
|
+
if (dv === void 0) return "";
|
|
4113
|
+
if (typeof dv === "boolean") {
|
|
4114
|
+
if (!(expr.kind === "optional" && expr.attrs.node.kind === "literal" && dv === false)) this.warn(`Boolean default on a ${expr.kind} cannot be expressed in argtype; ignored`);
|
|
4115
|
+
return "";
|
|
4116
|
+
}
|
|
4117
|
+
if (typeof dv === "number") {
|
|
4118
|
+
const n = this.finiteNum(dv, "default");
|
|
4119
|
+
return n === void 0 ? "" : `.default(${n})`;
|
|
4120
|
+
}
|
|
4121
|
+
return `.default(${quote(dv)})`;
|
|
4122
|
+
}
|
|
4123
|
+
emitOptional(expr, level) {
|
|
4124
|
+
const inner = expr.attrs.node;
|
|
4125
|
+
if (inner.kind === "sequence" && !inner.meta && inner.attrs.join === void 0) return this.emitCombinator("opt", inner.attrs.nodes, level);
|
|
4126
|
+
return this.emitCombinator("opt", [inner], level);
|
|
4127
|
+
}
|
|
4128
|
+
emitRepeat(expr, level) {
|
|
4129
|
+
const node = expr.attrs.node;
|
|
4130
|
+
let core;
|
|
4131
|
+
if (node.kind === "sequence" && !node.meta && node.attrs.join === void 0) core = this.emitCombinator("rep", node.attrs.nodes, level);
|
|
4132
|
+
else core = this.emitCombinator("rep", [node], level);
|
|
4133
|
+
if (expr.attrs.join !== void 0) core += `.join(${expr.attrs.join === "" ? "" : quote(expr.attrs.join)})`;
|
|
4134
|
+
const { countMin, countMax } = expr.attrs;
|
|
4135
|
+
if (countMin !== void 0 && countMin === countMax) core += `.count(${countMin})`;
|
|
4136
|
+
else {
|
|
4137
|
+
if (countMin !== void 0) core += `.countMin(${countMin})`;
|
|
4138
|
+
if (countMax !== void 0) core += `.countMax(${countMax})`;
|
|
4139
|
+
}
|
|
4140
|
+
return core;
|
|
4141
|
+
}
|
|
4142
|
+
emitCombinator(keyword, children, level, join) {
|
|
4143
|
+
let core;
|
|
4144
|
+
if (children.length === 0) core = `${keyword}()`;
|
|
4145
|
+
else {
|
|
4146
|
+
const items = children.map((c) => this.emitDecorated(c, level + 1));
|
|
4147
|
+
const inline = `${keyword}(${items.join(", ")})`;
|
|
4148
|
+
if (!items.some((i) => i.includes("\n")) && inline.length + level * 2 <= 80) core = inline;
|
|
4149
|
+
else core = `${keyword}(\n${items.map((i) => pad(level + 1) + i).join(",\n")},\n${pad(level)})`;
|
|
4150
|
+
}
|
|
4151
|
+
if (join !== void 0) core += `.join(${join === "" ? "" : quote(join)})`;
|
|
4152
|
+
return core;
|
|
4153
|
+
}
|
|
4154
|
+
/**
|
|
4155
|
+
* A list item: leading `///` doc lines, an optional `label:` prefix, then the
|
|
4156
|
+
* node. The first line is unindented (the caller pads it); continuation lines
|
|
4157
|
+
* are indented to `level`.
|
|
4158
|
+
*/
|
|
4159
|
+
emitDecorated(expr, level) {
|
|
4160
|
+
const doc = expr.meta?.doc;
|
|
4161
|
+
const useChain = this.docNeedsChain(doc);
|
|
4162
|
+
const parts = useChain ? [] : [...this.docLines(doc)];
|
|
4163
|
+
this.warnUnrepresentableDoc(doc, `node '${expr.meta?.name ?? expr.kind}'`);
|
|
4164
|
+
const label = expr.meta?.name ? `${this.labelFor(expr.meta.name)}: ` : "";
|
|
4165
|
+
let node = this.emitNode(expr, level);
|
|
4166
|
+
if (useChain) node += this.docChain(doc);
|
|
4167
|
+
parts.push(label + node);
|
|
4168
|
+
return parts.join("\n" + pad(level));
|
|
4169
|
+
}
|
|
4170
|
+
emitOutputs(outputs, level) {
|
|
4171
|
+
return `.output(\n${outputs.map((o) => {
|
|
4172
|
+
const useChain = this.docNeedsChain(o.doc);
|
|
4173
|
+
const docs = useChain ? [] : this.docLines(o.doc).map((l) => pad(level + 1) + l);
|
|
4174
|
+
let template = pad(level + 1) + this.emitOutputTemplate(o);
|
|
4175
|
+
if (useChain) template += this.docChain(o.doc);
|
|
4176
|
+
return [...docs, template].join("\n");
|
|
4177
|
+
}).join(",\n")},\n${pad(level)})`;
|
|
4178
|
+
}
|
|
4179
|
+
emitOutputTemplate(output) {
|
|
4180
|
+
if (output.mediaTypes?.length) this.warn(`Output '${output.name ?? "<anon>"}' has media types, which have no argtype surface; ignored`);
|
|
4181
|
+
const label = output.name ? `${this.labelFor(output.name)}: ` : "";
|
|
4182
|
+
let body = "";
|
|
4183
|
+
for (const token of output.tokens) {
|
|
4184
|
+
if (token.kind === "literal") {
|
|
4185
|
+
body += token.value.replace(/[\\{}`]/g, (c) => `\\${c}`);
|
|
4186
|
+
continue;
|
|
4187
|
+
}
|
|
4188
|
+
let ref = identOrQuoted(token.target.name);
|
|
4189
|
+
for (const ext of token.stripExtensions ?? []) ref += `.strip_suffix(${quote(ext)})`;
|
|
4190
|
+
if (token.fallback !== void 0) ref += `.or(${quote(token.fallback)})`;
|
|
4191
|
+
body += `{${ref}}`;
|
|
4192
|
+
}
|
|
4193
|
+
return `${label}\`${body}\``;
|
|
4194
|
+
}
|
|
4195
|
+
/** Quote a frontmatter scalar (double-quoted, backslash-escaped). The
|
|
4196
|
+
* frontmatter parser unescapes symmetrically, so quotes/newlines round-trip. */
|
|
4197
|
+
fmScalar(value) {
|
|
4198
|
+
return quote(value);
|
|
4199
|
+
}
|
|
4200
|
+
emitFrontmatter(app, exe) {
|
|
4201
|
+
const lines = [];
|
|
4202
|
+
if (exe !== void 0) lines.push(`exe: ${this.fmScalar(exe)}`);
|
|
4203
|
+
if (app) {
|
|
4204
|
+
if (app.version) lines.push(`version: ${this.fmScalar(app.version)}`);
|
|
4205
|
+
const authors = app.doc?.authors ?? [];
|
|
4206
|
+
if (authors.length > 0) {
|
|
4207
|
+
lines.push("authors:");
|
|
4208
|
+
for (const a of authors) lines.push(` - ${this.fmScalar(a)}`);
|
|
4209
|
+
}
|
|
4210
|
+
const urls = app.doc?.urls ?? [];
|
|
4211
|
+
if (urls.length > 0) {
|
|
4212
|
+
lines.push("urls:");
|
|
4213
|
+
for (const u of urls) lines.push(` - ${this.fmScalar(u)}`);
|
|
4214
|
+
}
|
|
4215
|
+
const references = app.doc?.literature ?? [];
|
|
4216
|
+
if (references.length > 0) {
|
|
4217
|
+
lines.push("references:");
|
|
4218
|
+
for (const rf of references) lines.push(` - ${this.fmScalar(rf)}`);
|
|
4219
|
+
}
|
|
4220
|
+
if (app.container) {
|
|
4221
|
+
lines.push("container:");
|
|
4222
|
+
lines.push(` image: ${this.fmScalar(app.container.image)}`);
|
|
4223
|
+
if (app.container.type) lines.push(` type: ${this.fmScalar(app.container.type)}`);
|
|
4224
|
+
}
|
|
4225
|
+
if (app.stdout) this.emitStream(lines, "stdout", app.stdout);
|
|
4226
|
+
if (app.stderr) this.emitStream(lines, "stderr", app.stderr);
|
|
4227
|
+
if (app.doc?.comment) this.warn("AppMeta.doc.comment has no argtype surface; ignored");
|
|
4228
|
+
}
|
|
4229
|
+
if (lines.length === 0) return void 0;
|
|
4230
|
+
return [
|
|
4231
|
+
"---",
|
|
4232
|
+
...lines,
|
|
4233
|
+
"---"
|
|
4234
|
+
].join("\n");
|
|
4235
|
+
}
|
|
4236
|
+
emitStream(lines, key, stream) {
|
|
4237
|
+
lines.push(`${key}:`);
|
|
4238
|
+
lines.push(` name: ${this.fmScalar(stream.name)}`);
|
|
4239
|
+
if (stream.doc?.description) lines.push(` description: ${this.fmScalar(stream.doc.description)}`);
|
|
4240
|
+
if (stream.doc?.title) this.warn(`${key} stream title has no argtype surface; ignored`);
|
|
4241
|
+
}
|
|
4242
|
+
};
|
|
4243
|
+
/** Emit argtype sugar-DSL source from an IR expression tree and optional `AppMeta`. */
|
|
4244
|
+
function generateArgtype(expr, app) {
|
|
4245
|
+
const emitter = new ArgtypeEmitter();
|
|
4246
|
+
return {
|
|
4247
|
+
source: emitter.emit(expr, app),
|
|
4248
|
+
warnings: emitter.warnings
|
|
4249
|
+
};
|
|
4250
|
+
}
|
|
4251
|
+
|
|
4252
|
+
//#endregion
|
|
4253
|
+
//#region src/backend/argtype/argtype.ts
|
|
2215
4254
|
/**
|
|
2216
|
-
*
|
|
2217
|
-
*
|
|
4255
|
+
* A per-tool file stem, so co-located tools in a catalog build don't clobber one
|
|
4256
|
+
* another's `descriptor.argtype`. Standalone single-tool builds (no scope) keep
|
|
4257
|
+
* the bare name. Mirrors the JSON Schema backend's `schemaStem`.
|
|
2218
4258
|
*/
|
|
2219
|
-
function
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
} catch {
|
|
2224
|
-
return null;
|
|
2225
|
-
}
|
|
2226
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
2227
|
-
const obj = parsed;
|
|
2228
|
-
if (typeof obj.$schema === "string" && obj.$schema.includes("argdump")) return "argdump";
|
|
2229
|
-
if (typeof obj.command === "string" && typeof obj.short_description === "string") return "workbench";
|
|
2230
|
-
if (typeof obj.synopsis === "string" && Array.isArray(obj.option_groups) && Array.isArray(obj.arguments)) return "mrtrix";
|
|
2231
|
-
if ("command-line" in obj || Array.isArray(obj.inputs) && "name" in obj) return "boutiques";
|
|
2232
|
-
if (Array.isArray(obj.actions) && "prog" in obj) return "argdump";
|
|
2233
|
-
return null;
|
|
4259
|
+
function argtypeStem(ctx, scope) {
|
|
4260
|
+
const id = ctx.app?.id;
|
|
4261
|
+
if (!id || !scope) return void 0;
|
|
4262
|
+
return scope.add(snakeCase(id) || "descriptor");
|
|
2234
4263
|
}
|
|
4264
|
+
/**
|
|
4265
|
+
* Serialization backend: emit argtype sugar-DSL source from the IR + `AppMeta`.
|
|
4266
|
+
*
|
|
4267
|
+
* The dogfooding / round-trip counterpart to the argtype frontend. Like the
|
|
4268
|
+
* Boutiques backend it only needs the IR (`ctx.expr`) and app metadata
|
|
4269
|
+
* (`ctx.app`), never the solved bindings, so it ignores the rest of the context.
|
|
4270
|
+
*/
|
|
4271
|
+
var ArgtypeBackend = class {
|
|
4272
|
+
name = "argtype";
|
|
4273
|
+
target = "argtype";
|
|
4274
|
+
/** One scope per package so per-tool file stems stay unique in the suite dir. */
|
|
4275
|
+
newPackageScope() {
|
|
4276
|
+
return new Scope();
|
|
4277
|
+
}
|
|
4278
|
+
emitApp(ctx, scope) {
|
|
4279
|
+
const { source, warnings } = generateArgtype(ctx.expr, ctx.app);
|
|
4280
|
+
const stem = argtypeStem(ctx, scope);
|
|
4281
|
+
const filename = stem ? `${stem}.argtype` : "descriptor.argtype";
|
|
4282
|
+
return {
|
|
4283
|
+
meta: ctx.app,
|
|
4284
|
+
files: new Map([[filename, source]]),
|
|
4285
|
+
errors: [],
|
|
4286
|
+
warnings
|
|
4287
|
+
};
|
|
4288
|
+
}
|
|
4289
|
+
};
|
|
2235
4290
|
|
|
2236
4291
|
//#endregion
|
|
2237
4292
|
//#region src/backend/find-doc.ts
|
|
@@ -2312,6 +4367,7 @@ function resolveFieldBinding(node, ctx, structType, outermost) {
|
|
|
2312
4367
|
* sequence is the actual struct owner (e.g. `seq(lit("--flag"), seq(field1, field2))`).
|
|
2313
4368
|
*/
|
|
2314
4369
|
function findStructNode(node, ctx, structType) {
|
|
4370
|
+
if (node.kind === "sequence" && Object.keys(structType.fields).length === 0) return node;
|
|
2315
4371
|
switch (node.kind) {
|
|
2316
4372
|
case "sequence":
|
|
2317
4373
|
for (const child of node.attrs.nodes) {
|
|
@@ -2517,100 +4573,6 @@ function formatType(type, indent = 0) {
|
|
|
2517
4573
|
}
|
|
2518
4574
|
}
|
|
2519
4575
|
|
|
2520
|
-
//#endregion
|
|
2521
|
-
//#region src/backend/resolve-output-tokens.ts
|
|
2522
|
-
/**
|
|
2523
|
-
* Compact consecutive literal tokens. Backends that emit string concatenation
|
|
2524
|
-
* benefit from a shorter token stream; backends that template each token
|
|
2525
|
-
* individually can ignore this and use `output.tokens` directly.
|
|
2526
|
-
*/
|
|
2527
|
-
function compactTokens(tokens) {
|
|
2528
|
-
const out = [];
|
|
2529
|
-
for (const tok of tokens) {
|
|
2530
|
-
const last = out[out.length - 1];
|
|
2531
|
-
if (tok.kind === "literal" && last && last.kind === "literal") out[out.length - 1] = {
|
|
2532
|
-
kind: "literal",
|
|
2533
|
-
value: last.value + tok.value
|
|
2534
|
-
};
|
|
2535
|
-
else out.push(tok);
|
|
2536
|
-
}
|
|
2537
|
-
return out;
|
|
2538
|
-
}
|
|
2539
|
-
function planOutput(scopeGate, output, bindings) {
|
|
2540
|
-
return {
|
|
2541
|
-
name: output.name,
|
|
2542
|
-
gate: outputGate(scopeGate, output, bindings),
|
|
2543
|
-
tokens: compactTokens(output.tokens),
|
|
2544
|
-
resolved: output
|
|
2545
|
-
};
|
|
2546
|
-
}
|
|
2547
|
-
/**
|
|
2548
|
-
* Does the output have any conditional wrapper? Equivalent to "is at least one
|
|
2549
|
-
* atom a `present` or `variant`?". `iter` alone means the output emits a list
|
|
2550
|
-
* and is not conditionally absent.
|
|
2551
|
-
*/
|
|
2552
|
-
function isGated(plan) {
|
|
2553
|
-
return plan.gate.some((a) => a.kind === "present" || a.kind === "variant");
|
|
2554
|
-
}
|
|
2555
|
-
/** Does the output iterate (emit zero-or-more values)? */
|
|
2556
|
-
function isIterated(plan) {
|
|
2557
|
-
return plan.gate.some((a) => a.kind === "iter");
|
|
2558
|
-
}
|
|
2559
|
-
/**
|
|
2560
|
-
* Convenience for backends emitting all outputs of a scope at once. The caller
|
|
2561
|
-
* provides the scope's gate (typically `bindings.get(scope.scope)?.gate ?? []`).
|
|
2562
|
-
*/
|
|
2563
|
-
function planScope(scope, scopeGate, bindings) {
|
|
2564
|
-
return scope.outputs.map((output) => planOutput(scopeGate, output, bindings));
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
|
-
//#endregion
|
|
2568
|
-
//#region src/backend/scope.ts
|
|
2569
|
-
/** Symbol collision avoidance for code generation. */
|
|
2570
|
-
var Scope = class Scope {
|
|
2571
|
-
reserved;
|
|
2572
|
-
used;
|
|
2573
|
-
parent;
|
|
2574
|
-
constructor(reserved = [], parent) {
|
|
2575
|
-
this.reserved = new Set(reserved);
|
|
2576
|
-
this.used = /* @__PURE__ */ new Set();
|
|
2577
|
-
this.parent = parent;
|
|
2578
|
-
}
|
|
2579
|
-
/** Check if a symbol is already taken (in this scope or any parent). */
|
|
2580
|
-
has(symbol) {
|
|
2581
|
-
return this.reserved.has(symbol) || this.used.has(symbol) || (this.parent?.has(symbol) ?? false);
|
|
2582
|
-
}
|
|
2583
|
-
/**
|
|
2584
|
-
* Add a symbol, appending a numeric suffix to avoid collisions. Returns the
|
|
2585
|
-
* safe name.
|
|
2586
|
-
*
|
|
2587
|
-
* When a `recase` transform is given, a disambiguated candidate is routed back
|
|
2588
|
-
* through it so the suffix is absorbed into the identifier's casing - e.g.
|
|
2589
|
-
* `pascalCase` folds `Config_2` into `Config2` - rather than leaving a
|
|
2590
|
-
* mixed-case `Config_2`. Uniqueness is always checked on the final emitted
|
|
2591
|
-
* form, so two hints that case-collide still get distinct names. Defaults to
|
|
2592
|
-
* identity (the bare `<name>_<n>` suffix) for callers that don't case-normalize.
|
|
2593
|
-
*/
|
|
2594
|
-
add(candidate, recase = (s) => s) {
|
|
2595
|
-
if (!this.has(candidate)) {
|
|
2596
|
-
this.used.add(candidate);
|
|
2597
|
-
return candidate;
|
|
2598
|
-
}
|
|
2599
|
-
let suffix = 2;
|
|
2600
|
-
let safe = recase(`${candidate}_${suffix}`);
|
|
2601
|
-
while (this.has(safe)) {
|
|
2602
|
-
suffix++;
|
|
2603
|
-
safe = recase(`${candidate}_${suffix}`);
|
|
2604
|
-
}
|
|
2605
|
-
this.used.add(safe);
|
|
2606
|
-
return safe;
|
|
2607
|
-
}
|
|
2608
|
-
/** Create a child scope that inherits this scope's restrictions. */
|
|
2609
|
-
child(reserved = []) {
|
|
2610
|
-
return new Scope(reserved, this);
|
|
2611
|
-
}
|
|
2612
|
-
};
|
|
2613
|
-
|
|
2614
4576
|
//#endregion
|
|
2615
4577
|
//#region src/backend/boutiques/boutiques.ts
|
|
2616
4578
|
var BoutiquesEmitter = class {
|
|
@@ -2826,14 +4788,12 @@ var BoutiquesEmitter = class {
|
|
|
2826
4788
|
if (flagStr) peeled.flag = flagStr;
|
|
2827
4789
|
}
|
|
2828
4790
|
const input = this.buildInput(binding, id, fieldType, valueKeyStr, peeled, fieldInfo, wrapperNode);
|
|
2829
|
-
|
|
2830
|
-
else commandParts.push(valueKeyStr);
|
|
2831
|
-
else commandParts.push(valueKeyStr);
|
|
4791
|
+
commandParts.push(valueKeyStr);
|
|
2832
4792
|
inputs.push(input);
|
|
2833
4793
|
}
|
|
2834
4794
|
bt["command-line"] = commandParts.join(" ");
|
|
2835
4795
|
bt.inputs = inputs;
|
|
2836
|
-
this.emitOutputFiles(bt,
|
|
4796
|
+
this.emitOutputFiles(bt, structNode, valueKeyByBinding, idScope);
|
|
2837
4797
|
}
|
|
2838
4798
|
emitOutputFiles(bt, scopeNode, valueKeys, idScope) {
|
|
2839
4799
|
const scopeBinding = this.ctx.resolve(scopeNode);
|
|
@@ -2926,6 +4886,7 @@ var BoutiquesEmitter = class {
|
|
|
2926
4886
|
}
|
|
2927
4887
|
finalizeInput(input) {
|
|
2928
4888
|
if (input.name === void 0) input.name = input.id;
|
|
4889
|
+
this.finalizeSubDescriptors(input);
|
|
2929
4890
|
if (input.type === "String" && typeof input["default-value"] === "boolean" && input["value-choices"] === void 0) input.type = "Flag";
|
|
2930
4891
|
if (input.type === "Flag") {
|
|
2931
4892
|
delete input["default-value"];
|
|
@@ -2952,6 +4913,15 @@ var BoutiquesEmitter = class {
|
|
|
2952
4913
|
if (Array.isArray(choices) && dv !== void 0 && !choices.some((c) => c === dv)) delete input["default-value"];
|
|
2953
4914
|
this.mergeDefaultIntoDescription(input);
|
|
2954
4915
|
}
|
|
4916
|
+
finalizeSubDescriptors(input) {
|
|
4917
|
+
const type = input.type;
|
|
4918
|
+
if (Array.isArray(type)) type.forEach((sub, i) => this.ensureSubDescriptorId(sub, `${input.id}_${i + 1}`));
|
|
4919
|
+
else if (typeof type === "object" && type !== null) this.ensureSubDescriptorId(type, input.id);
|
|
4920
|
+
}
|
|
4921
|
+
ensureSubDescriptorId(sub, fallback) {
|
|
4922
|
+
if (sub.id === void 0) sub.id = this.sanitizeId(fallback);
|
|
4923
|
+
if (sub.name === void 0) sub.name = sub.id;
|
|
4924
|
+
}
|
|
2955
4925
|
peelNode(node, type) {
|
|
2956
4926
|
const result = {
|
|
2957
4927
|
isOptional: false,
|
|
@@ -2975,6 +4945,8 @@ var BoutiquesEmitter = class {
|
|
|
2975
4945
|
this.peelNodeInner(node.attrs.node, type.kind === "list" ? type.item : type, result);
|
|
2976
4946
|
break;
|
|
2977
4947
|
case "sequence": {
|
|
4948
|
+
const structType = this.unwrapType(type);
|
|
4949
|
+
if (structType.kind === "struct" && findStructNode(node, this.ctx, structType) === node) break;
|
|
2978
4950
|
const nodes = node.attrs.nodes;
|
|
2979
4951
|
if (nodes.length === 2 && nodes[0].kind === "literal") {
|
|
2980
4952
|
const flagLit = nodes[0].attrs.str;
|
|
@@ -3063,8 +5035,7 @@ var BoutiquesEmitter = class {
|
|
|
3063
5035
|
type: "String",
|
|
3064
5036
|
valueChoices: type.variants.map((v) => v.type.kind === "literal" ? v.type.value : "")
|
|
3065
5037
|
};
|
|
3066
|
-
|
|
3067
|
-
return { type: this.buildMixedUnionAsSubCommands(type, node) };
|
|
5038
|
+
return { type: this.buildUnionSubCommands(type, node) };
|
|
3068
5039
|
}
|
|
3069
5040
|
buildSubCommand(type, node) {
|
|
3070
5041
|
const bt = {};
|
|
@@ -3075,22 +5046,7 @@ var BoutiquesEmitter = class {
|
|
|
3075
5046
|
}
|
|
3076
5047
|
return bt;
|
|
3077
5048
|
}
|
|
3078
|
-
|
|
3079
|
-
const alts = node.kind === "alternative" ? node.attrs.alts : [node];
|
|
3080
|
-
return type.variants.map((variant, i) => {
|
|
3081
|
-
const altNode = alts[i] ?? node;
|
|
3082
|
-
if (variant.type.kind === "struct") {
|
|
3083
|
-
const bt = this.buildSubCommand(variant.type, altNode);
|
|
3084
|
-
if (variant.name) {
|
|
3085
|
-
bt.name = variant.name;
|
|
3086
|
-
bt.id = this.sanitizeId(variant.name);
|
|
3087
|
-
}
|
|
3088
|
-
return bt;
|
|
3089
|
-
}
|
|
3090
|
-
return this.wrapAsDescriptor(variant, altNode);
|
|
3091
|
-
});
|
|
3092
|
-
}
|
|
3093
|
-
buildMixedUnionAsSubCommands(type, node) {
|
|
5049
|
+
buildUnionSubCommands(type, node) {
|
|
3094
5050
|
const alts = node.kind === "alternative" ? node.attrs.alts : [node];
|
|
3095
5051
|
return type.variants.map((variant, i) => {
|
|
3096
5052
|
const altNode = alts[i] ?? node;
|
|
@@ -3189,7 +5145,8 @@ var BoutiquesEmitter = class {
|
|
|
3189
5145
|
inputs: []
|
|
3190
5146
|
},
|
|
3191
5147
|
list: true,
|
|
3192
|
-
minListEntries: countMin
|
|
5148
|
+
minListEntries: countMin,
|
|
5149
|
+
...countMin === 0 && { optional: true }
|
|
3193
5150
|
};
|
|
3194
5151
|
}
|
|
3195
5152
|
};
|
|
@@ -3395,9 +5352,65 @@ function resolveTypeName(namedTypes) {
|
|
|
3395
5352
|
};
|
|
3396
5353
|
}
|
|
3397
5354
|
|
|
5355
|
+
//#endregion
|
|
5356
|
+
//#region src/backend/field-defaults.ts
|
|
5357
|
+
/**
|
|
5358
|
+
* Build the field-name -> rendered-default map for a struct root (else empty).
|
|
5359
|
+
* Includes only non-optional defaulted fields (optional fields are
|
|
5360
|
+
* presence-guarded; their default comes from the factory's kwarg signature).
|
|
5361
|
+
*
|
|
5362
|
+
* `rootType` defaults to the resolved root type; callers that already have it
|
|
5363
|
+
* (the arg-builders) pass it to avoid re-resolving.
|
|
5364
|
+
*/
|
|
5365
|
+
function collectDefaults(ctx, renderLiteral, rootType = ctx.resolve(ctx.expr)?.type) {
|
|
5366
|
+
const out = /* @__PURE__ */ new Map();
|
|
5367
|
+
if (rootType?.kind !== "struct") return out;
|
|
5368
|
+
for (const [name, fi] of collectFieldInfo(ctx, rootType)) {
|
|
5369
|
+
if (fi.defaultValue === void 0) continue;
|
|
5370
|
+
if (rootType.fields[name]?.kind === "optional") continue;
|
|
5371
|
+
out.set(name, renderLiteral(fi.defaultValue));
|
|
5372
|
+
}
|
|
5373
|
+
return out;
|
|
5374
|
+
}
|
|
5375
|
+
/** The rendered default for a binding iff it is a root-level defaulted field. */
|
|
5376
|
+
function rootFieldDefault(binding, defaults) {
|
|
5377
|
+
if (!binding) return void 0;
|
|
5378
|
+
const a = binding.access;
|
|
5379
|
+
if (a.length === 1 && a[0]?.kind === "field") return defaults.get(binding.name);
|
|
5380
|
+
}
|
|
5381
|
+
|
|
3398
5382
|
//#endregion
|
|
3399
5383
|
//#region src/backend/union-variants.ts
|
|
3400
5384
|
/**
|
|
5385
|
+
* Whether a union is "mixed": it has at least one non-struct (bare-literal) arm
|
|
5386
|
+
* alongside its struct variants (e.g. ants `n4_correction = Literal[0] | N4On`).
|
|
5387
|
+
*
|
|
5388
|
+
* The `@type` discriminator only exists on the struct arms, so indexing
|
|
5389
|
+
* `value["@type"]` on the union value is unsound (a type error, and a runtime
|
|
5390
|
+
* KeyError if the literal arm is ever hit) until the value is narrowed to a
|
|
5391
|
+
* struct at runtime. Backends that dispatch on `@type` must first emit a shape
|
|
5392
|
+
* guard (Python `isinstance(value, dict)`, TS `typeof value === "object"`) when
|
|
5393
|
+
* this is true; a pure-struct union (every arm a struct) needs no guard.
|
|
5394
|
+
*/
|
|
5395
|
+
function unionIsMixed(unionType) {
|
|
5396
|
+
return unionType.variants.some((v) => v.type.kind !== "struct");
|
|
5397
|
+
}
|
|
5398
|
+
/**
|
|
5399
|
+
* The union type bound by a `variant` gate atom, if the atom's binding resolves
|
|
5400
|
+
* to a union - used by the outputs emitters to decide whether the variant gate
|
|
5401
|
+
* needs a mixed-union shape guard before its `@type` check. Returns `undefined`
|
|
5402
|
+
* for a well-formed non-union (defensive; a variant atom should always name a
|
|
5403
|
+
* union binding).
|
|
5404
|
+
*/
|
|
5405
|
+
function variantAtomUnion(atom, bindings) {
|
|
5406
|
+
return resolveUnion(bindings.get(atom.binding)?.type);
|
|
5407
|
+
}
|
|
5408
|
+
function resolveUnion(type) {
|
|
5409
|
+
if (!type) return void 0;
|
|
5410
|
+
if (type.kind === "optional") return resolveUnion(type.inner);
|
|
5411
|
+
if (type.kind === "union") return type;
|
|
5412
|
+
}
|
|
5413
|
+
/**
|
|
3401
5414
|
* The struct variants of a union, each with its index into `variants`.
|
|
3402
5415
|
*
|
|
3403
5416
|
* A discriminated union dispatches on a unique `@type`, so two struct variants
|
|
@@ -3535,10 +5548,6 @@ function pathArg$1(node, expr) {
|
|
|
3535
5548
|
* carrying a Boutiques default. Restricted to single-segment (root) field access
|
|
3536
5549
|
* so a nested field can never accidentally pick up a same-named root default.
|
|
3537
5550
|
*/
|
|
3538
|
-
function rootFieldDefault$3(binding, defaults) {
|
|
3539
|
-
const a = binding.access;
|
|
3540
|
-
if (a.length === 1 && a[0]?.kind === "field") return defaults.get(binding.name);
|
|
3541
|
-
}
|
|
3542
5551
|
/**
|
|
3543
5552
|
* Render a binding's access for an UNCONDITIONAL value read (terminal, repeat
|
|
3544
5553
|
* loop, alternative dispatch): substitutes the field's default via
|
|
@@ -3549,7 +5558,7 @@ function rootFieldDefault$3(binding, defaults) {
|
|
|
3549
5558
|
* via `.get()`).
|
|
3550
5559
|
*/
|
|
3551
5560
|
function readAccess$1(binding, arg) {
|
|
3552
|
-
const def = rootFieldDefault
|
|
5561
|
+
const def = rootFieldDefault(binding, arg.defaults);
|
|
3553
5562
|
return accessOf$1(binding, arg, def !== void 0 ? { finalDefault: def } : {});
|
|
3554
5563
|
}
|
|
3555
5564
|
/**
|
|
@@ -3569,21 +5578,6 @@ function accessOf$1(binding, arg, opts = {}) {
|
|
|
3569
5578
|
subst: arg.valueSubst
|
|
3570
5579
|
});
|
|
3571
5580
|
}
|
|
3572
|
-
/**
|
|
3573
|
-
* Build the field-name -> rendered-default map for a struct root (else empty).
|
|
3574
|
-
* Includes only non-optional defaulted fields (optional fields are
|
|
3575
|
-
* presence-guarded; their default comes from the factory's kwarg signature).
|
|
3576
|
-
*/
|
|
3577
|
-
function collectDefaults$3(ctx, rootType) {
|
|
3578
|
-
const out = /* @__PURE__ */ new Map();
|
|
3579
|
-
if (rootType?.kind !== "struct") return out;
|
|
3580
|
-
for (const [name, fi] of collectFieldInfo(ctx, rootType)) {
|
|
3581
|
-
if (fi.defaultValue === void 0) continue;
|
|
3582
|
-
if (rootType.fields[name]?.kind === "optional") continue;
|
|
3583
|
-
out.set(name, renderPyLiteral(fi.defaultValue));
|
|
3584
|
-
}
|
|
3585
|
-
return out;
|
|
3586
|
-
}
|
|
3587
5581
|
let loopVarCounter$1 = 0;
|
|
3588
5582
|
let optVarCounter = 0;
|
|
3589
5583
|
/**
|
|
@@ -3599,7 +5593,7 @@ function buildArgs$1(rootExpr, ctx, rootType) {
|
|
|
3599
5593
|
joinDepth: 0,
|
|
3600
5594
|
loopVars: /* @__PURE__ */ new Map(),
|
|
3601
5595
|
valueSubst: /* @__PURE__ */ new Map(),
|
|
3602
|
-
defaults: collectDefaults
|
|
5596
|
+
defaults: collectDefaults(ctx, renderPyLiteral, rootType)
|
|
3603
5597
|
});
|
|
3604
5598
|
}
|
|
3605
5599
|
function walk$2(node, ctx, arg) {
|
|
@@ -3759,8 +5753,8 @@ function walkAlternative$1(node, ctx, arg) {
|
|
|
3759
5753
|
*/
|
|
3760
5754
|
function emitDocstring(cb, text) {
|
|
3761
5755
|
if (!text) return;
|
|
3762
|
-
const lines = text.split("\n");
|
|
3763
|
-
if (lines.length === 1 && !lines[0].includes("\"")) {
|
|
5756
|
+
const lines = text.replace(/"""/g, "\\\"\\\"\\\"").split("\n");
|
|
5757
|
+
if (lines.length === 1 && !lines[0].includes("\"") && !lines[0].endsWith("\\")) {
|
|
3764
5758
|
cb.line(`"""${lines[0]}"""`);
|
|
3765
5759
|
return;
|
|
3766
5760
|
}
|
|
@@ -3768,20 +5762,18 @@ function emitDocstring(cb, text) {
|
|
|
3768
5762
|
for (const line of lines) cb.line(line);
|
|
3769
5763
|
cb.line(`"""`);
|
|
3770
5764
|
}
|
|
3771
|
-
function emitImports$1(cb
|
|
3772
|
-
cb.line("import dataclasses");
|
|
5765
|
+
function emitImports$1(cb) {
|
|
3773
5766
|
cb.line("import pathlib");
|
|
3774
5767
|
cb.line("import typing");
|
|
3775
5768
|
cb.blank();
|
|
3776
|
-
|
|
5769
|
+
cb.line(`from styxdefs import ${[
|
|
3777
5770
|
"Execution",
|
|
3778
5771
|
"InputPathType",
|
|
3779
5772
|
"Metadata",
|
|
3780
5773
|
"Runner",
|
|
3781
|
-
"StyxValidationError"
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
cb.line(`from styxdefs import ${fromStyxdefs.join(", ")}, get_global_runner`);
|
|
5774
|
+
"StyxValidationError",
|
|
5775
|
+
"OutputPathType"
|
|
5776
|
+
].join(", ")}, get_global_runner`);
|
|
3785
5777
|
}
|
|
3786
5778
|
function emitMetadata$1(ctx, metaConst, cb) {
|
|
3787
5779
|
const id = ctx.app?.id ?? "unknown";
|
|
@@ -4283,9 +6275,10 @@ function renderInputTrait(p) {
|
|
|
4283
6275
|
case "str": return call("traits.Str", [...hasDef ? [renderPyLiteral(def), "usedefault=True"] : [], ...tail]);
|
|
4284
6276
|
case "enum": {
|
|
4285
6277
|
const choices = p.choices ?? [];
|
|
6278
|
+
const defChoice = hasDef && def !== void 0 && typeof def !== "boolean" && choices.includes(def) ? def : void 0;
|
|
4286
6279
|
return call("traits.Enum", [
|
|
4287
|
-
...(
|
|
4288
|
-
...
|
|
6280
|
+
...(defChoice !== void 0 ? [defChoice, ...choices.filter((c) => c !== defChoice)] : choices).map((c) => renderPyLiteral(c)),
|
|
6281
|
+
...defChoice !== void 0 ? ["usedefault=True"] : [],
|
|
4289
6282
|
...tail
|
|
4290
6283
|
]);
|
|
4291
6284
|
}
|
|
@@ -4765,7 +6758,7 @@ function emitValue$1(e, type, node, wireKey, valueExpr, expected) {
|
|
|
4765
6758
|
const itemNode = findRepeatNode(node)?.attrs.node;
|
|
4766
6759
|
const elem = e.scope.add("e");
|
|
4767
6760
|
e.cb.line(`for ${elem} in ${valueExpr}:`);
|
|
4768
|
-
e.cb.indent(() => emitValue$1(e, type.item, itemNode, wireKey, elem,
|
|
6761
|
+
e.cb.indent(() => emitValue$1(e, type.item, itemNode, wireKey, elem, expectedType$1(e, type.item)));
|
|
4769
6762
|
return;
|
|
4770
6763
|
}
|
|
4771
6764
|
case "struct":
|
|
@@ -5052,10 +7045,9 @@ function streamFieldIds$1(ctx) {
|
|
|
5052
7045
|
if (ctx.app?.stderr) res.stderr = fields[idx++].id;
|
|
5053
7046
|
return res;
|
|
5054
7047
|
}
|
|
5055
|
-
/** Emit
|
|
7048
|
+
/** Emit `class <outputsType>(typing.NamedTuple):` declaration. */
|
|
5056
7049
|
function emitOutputsClass(ctx, outputsType, cb) {
|
|
5057
|
-
cb.line(
|
|
5058
|
-
cb.line(`class ${outputsType}:`);
|
|
7050
|
+
cb.line(`class ${outputsType}(typing.NamedTuple):`);
|
|
5059
7051
|
cb.indent(() => {
|
|
5060
7052
|
emitDocstring(cb, "Output paths produced by the tool.");
|
|
5061
7053
|
const fields = collectOutputFields(ctx, pyId);
|
|
@@ -5074,26 +7066,6 @@ function emitOutputsClass(ctx, outputsType, cb) {
|
|
|
5074
7066
|
}
|
|
5075
7067
|
});
|
|
5076
7068
|
}
|
|
5077
|
-
/** The rendered default for a binding iff it is a root-level defaulted field. */
|
|
5078
|
-
function rootFieldDefault$2(binding, defaults) {
|
|
5079
|
-
if (!binding) return void 0;
|
|
5080
|
-
const a = binding.access;
|
|
5081
|
-
if (a.length === 1 && a[0]?.kind === "field") return defaults.get(binding.name);
|
|
5082
|
-
}
|
|
5083
|
-
/** Build the field-name -> rendered-default map for the struct root (else empty).
|
|
5084
|
-
* Includes only non-optional defaulted fields (optional fields are
|
|
5085
|
-
* presence-guarded; their default comes from the factory's kwarg signature). */
|
|
5086
|
-
function collectDefaults$2(ctx) {
|
|
5087
|
-
const out = /* @__PURE__ */ new Map();
|
|
5088
|
-
const rootType = ctx.resolve(ctx.expr)?.type;
|
|
5089
|
-
if (rootType?.kind !== "struct") return out;
|
|
5090
|
-
for (const [name, fi] of collectFieldInfo(ctx, rootType)) {
|
|
5091
|
-
if (fi.defaultValue === void 0) continue;
|
|
5092
|
-
if (rootType.fields[name]?.kind === "optional") continue;
|
|
5093
|
-
out.set(name, renderPyLiteral(fi.defaultValue));
|
|
5094
|
-
}
|
|
5095
|
-
return out;
|
|
5096
|
-
}
|
|
5097
7069
|
let loopCounter$1 = 0;
|
|
5098
7070
|
function renderWrapperOpen$1(atom, ec) {
|
|
5099
7071
|
if (atom.kind === "iter") {
|
|
@@ -5104,7 +7076,13 @@ function renderWrapperOpen$1(atom, ec) {
|
|
|
5104
7076
|
loopVar: v
|
|
5105
7077
|
};
|
|
5106
7078
|
}
|
|
5107
|
-
if (atom.kind === "variant")
|
|
7079
|
+
if (atom.kind === "variant") {
|
|
7080
|
+
const access = bindingAccess$1(atom.binding, ec);
|
|
7081
|
+
const check = `${access}["@type"] == ${pyStr(atom.variant)}`;
|
|
7082
|
+
const union = variantAtomUnion(atom, ec.ctx.bindings);
|
|
7083
|
+
if (union && unionIsMixed(union)) return { open: `if isinstance(${access}, dict) and ${check}:` };
|
|
7084
|
+
return { open: `if ${check}:` };
|
|
7085
|
+
}
|
|
5108
7086
|
const binding = ec.ctx.bindings.get(atom.binding);
|
|
5109
7087
|
if (binding?.type.kind === "optional") {
|
|
5110
7088
|
const subscriptAccess = bindingAccess$1(atom.binding, ec);
|
|
@@ -5160,7 +7138,7 @@ function renderToken$1(tok, ec) {
|
|
|
5160
7138
|
return renderRefValue$1(tok, ec);
|
|
5161
7139
|
}
|
|
5162
7140
|
function renderRefValue$1(tok, ec) {
|
|
5163
|
-
const def = rootFieldDefault
|
|
7141
|
+
const def = rootFieldDefault(ec.ctx.bindings.get(tok.binding), ec.defaults);
|
|
5164
7142
|
let expr = def !== void 0 && !ec.iter.has(tok.binding) ? bindingAccess$1(tok.binding, ec, false, def) : bindingAccess$1(tok.binding, ec);
|
|
5165
7143
|
if (tok.fallback !== void 0) expr = `(${expr} if ${expr} is not None else ${pyStr(tok.fallback)})`;
|
|
5166
7144
|
if (tok.stripExtensions && tok.stripExtensions.length > 0) {
|
|
@@ -5223,7 +7201,7 @@ function emitBuildOutputs$1(ctx, paramsType, outputsType, funcName, cb) {
|
|
|
5223
7201
|
ctx,
|
|
5224
7202
|
iter: /* @__PURE__ */ new Map(),
|
|
5225
7203
|
subst: /* @__PURE__ */ new Map(),
|
|
5226
|
-
defaults: collectDefaults
|
|
7204
|
+
defaults: collectDefaults(ctx, renderPyLiteral)
|
|
5227
7205
|
};
|
|
5228
7206
|
const fields = collectOutputFields(ctx, pyId);
|
|
5229
7207
|
const localVarOf = /* @__PURE__ */ new Map();
|
|
@@ -5260,11 +7238,16 @@ function emitBuildOutputs$1(ctx, paramsType, outputsType, funcName, cb) {
|
|
|
5260
7238
|
}
|
|
5261
7239
|
});
|
|
5262
7240
|
}
|
|
5263
|
-
/**
|
|
7241
|
+
/**
|
|
7242
|
+
* Sanitize an output name to a valid Python identifier. Uses a *letter*-leading
|
|
7243
|
+
* prefix (`v_`) for digit-leading / empty names, never a leading underscore:
|
|
7244
|
+
* the Outputs type is a `typing.NamedTuple`, which raises `ValueError` at import
|
|
7245
|
+
* time for a field whose name starts with `_`. Matches styx1 and `pyScrubIdent`.
|
|
7246
|
+
* (A trailing underscore for keywords is fine - only leading underscores fail.)
|
|
7247
|
+
*/
|
|
5264
7248
|
function pyId(name) {
|
|
5265
7249
|
let s = name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
5266
|
-
if (/^\d/.test(s)) s = "
|
|
5267
|
-
if (s === "") s = "_";
|
|
7250
|
+
if (/^\d/.test(s) || s === "") s = "v_" + s;
|
|
5268
7251
|
if (PY_KEYWORDS.has(s)) s = s + "_";
|
|
5269
7252
|
return s;
|
|
5270
7253
|
}
|
|
@@ -5450,13 +7433,22 @@ function buildEmitModel(ctx, scope = new Scope(PY_RESERVED)) {
|
|
|
5450
7433
|
};
|
|
5451
7434
|
}
|
|
5452
7435
|
function generatePython(ctx, packageScope) {
|
|
7436
|
+
return generatePythonModule(ctx, packageScope).code;
|
|
7437
|
+
}
|
|
7438
|
+
/**
|
|
7439
|
+
* Emit the module and, alongside it, the dispatch entrypoint carrying the
|
|
7440
|
+
* *scope-registered* execute-function name. Computing the entrypoint here (not
|
|
7441
|
+
* via the scope-blind `appEntrypoint`) keeps the suite dispatcher in sync with
|
|
7442
|
+
* the actual emitted symbol when a shared package scope suffix-bumps a collision.
|
|
7443
|
+
*/
|
|
7444
|
+
function generatePythonModule(ctx, packageScope) {
|
|
5453
7445
|
const cb = new CodeBuilder(" ");
|
|
5454
7446
|
const scope = packageScope ?? new Scope(PY_RESERVED);
|
|
5455
7447
|
const { names, rootType, rootIsStruct, namedTypes, typeDecls, rootTypeTag, paramsType, sigEntries, nestedFactories } = buildEmitModel(ctx, scope);
|
|
5456
7448
|
cb.comment("This file was auto generated by Styx.", "# ");
|
|
5457
7449
|
cb.comment("Do not edit this file directly.", "# ");
|
|
5458
7450
|
cb.blank();
|
|
5459
|
-
emitImports$1(cb
|
|
7451
|
+
emitImports$1(cb);
|
|
5460
7452
|
cb.blank();
|
|
5461
7453
|
emitMetadata$1(ctx, names.metadata, cb);
|
|
5462
7454
|
cb.blank();
|
|
@@ -5481,7 +7473,8 @@ function generatePython(ctx, packageScope) {
|
|
|
5481
7473
|
cb.blank();
|
|
5482
7474
|
emitBuildOutputs$1(ctx, paramsType, names.outputs, names.outputsFn, cb);
|
|
5483
7475
|
cb.blank();
|
|
5484
|
-
|
|
7476
|
+
const executeName = rootIsStruct ? names.execute : names.wrapper;
|
|
7477
|
+
emitWrapperFunction$1(ctx, paramsType, executeName, names.metadata, names.cargs, names.outputsFn, names.outputs, names.validate, streamFieldIds$1(ctx), cb);
|
|
5485
7478
|
cb.blank();
|
|
5486
7479
|
if (rootIsStruct) {
|
|
5487
7480
|
emitKwargWrapper$1(ctx, sigEntries, names.wrapper, names.paramsFn, names.execute, names.outputs, cb);
|
|
@@ -5489,10 +7482,10 @@ function generatePython(ctx, packageScope) {
|
|
|
5489
7482
|
}
|
|
5490
7483
|
const publicSymbols = [
|
|
5491
7484
|
names.params,
|
|
5492
|
-
|
|
7485
|
+
names.outputs,
|
|
5493
7486
|
names.metadata,
|
|
5494
7487
|
names.cargs,
|
|
5495
|
-
|
|
7488
|
+
names.outputsFn,
|
|
5496
7489
|
...rootIsStruct ? [names.paramsFn, names.execute] : [],
|
|
5497
7490
|
...nestedFactories.map((nf) => nf.funcName),
|
|
5498
7491
|
names.validate,
|
|
@@ -5503,7 +7496,16 @@ function generatePython(ctx, packageScope) {
|
|
|
5503
7496
|
for (const sym of publicSymbols) cb.line(`"${sym}",`);
|
|
5504
7497
|
});
|
|
5505
7498
|
cb.line("]");
|
|
5506
|
-
|
|
7499
|
+
const appId = ctx.app?.id;
|
|
7500
|
+
const pkg = ctx.package?.name;
|
|
7501
|
+
const entrypoint = appId && pkg ? {
|
|
7502
|
+
type: `${pkg}/${appId}`,
|
|
7503
|
+
executeFn: executeName
|
|
7504
|
+
} : void 0;
|
|
7505
|
+
return {
|
|
7506
|
+
code: cb.toString(),
|
|
7507
|
+
entrypoint
|
|
7508
|
+
};
|
|
5507
7509
|
}
|
|
5508
7510
|
/**
|
|
5509
7511
|
* Module name (file stem) for an app: snake_case of app.id, fallback `output`.
|
|
@@ -5515,22 +7517,6 @@ function appModuleName$1(meta) {
|
|
|
5515
7517
|
return pyScrubIdent(snakeCase(meta.id), PY_RESERVED);
|
|
5516
7518
|
}
|
|
5517
7519
|
/**
|
|
5518
|
-
* The dispatch entrypoint for one app: its root `@type` (`<package>/<app>`) and
|
|
5519
|
-
* the dict-style execute function name. Returns undefined when the id or package
|
|
5520
|
-
* is unknown (no stable `@type`), so the app is left out of the suite dispatcher.
|
|
5521
|
-
*/
|
|
5522
|
-
function appEntrypoint$1(ctx) {
|
|
5523
|
-
const appId = ctx.app?.id;
|
|
5524
|
-
const pkg = ctx.package?.name;
|
|
5525
|
-
if (!appId || !pkg) return void 0;
|
|
5526
|
-
const publicNames = computePublicNames$1(appId);
|
|
5527
|
-
const executeFn = pyScrubIdent(ctx.resolve(ctx.expr)?.type.kind === "struct" ? publicNames.execute : publicNames.wrapper, PY_RESERVED);
|
|
5528
|
-
return {
|
|
5529
|
-
type: `${pkg}/${appId}`,
|
|
5530
|
-
executeFn
|
|
5531
|
-
};
|
|
5532
|
-
}
|
|
5533
|
-
/**
|
|
5534
7520
|
* Generate the suite-level `__init__.py` re-export for a package containing
|
|
5535
7521
|
* multiple tool modules. Each tool module's public symbols are surfaced via
|
|
5536
7522
|
* `from .bet import *` (each tool file defines `__all__`). When apps carry a
|
|
@@ -5567,10 +7553,11 @@ function emitPackageDispatch$1(cb, dispatch) {
|
|
|
5567
7553
|
for (const e of dispatch) cb.line(`${JSON.stringify(e.type)}: ${e.executeFn},`);
|
|
5568
7554
|
});
|
|
5569
7555
|
cb.line("}");
|
|
5570
|
-
cb.line("
|
|
7556
|
+
cb.line("_type = params.get(\"@type\")");
|
|
7557
|
+
cb.line("_fn = _dispatch.get(_type) if _type is not None else None");
|
|
5571
7558
|
cb.line("if _fn is None:");
|
|
5572
7559
|
cb.indent(() => {
|
|
5573
|
-
cb.line(`raise ValueError(f"No tool registered for @type {
|
|
7560
|
+
cb.line(`raise ValueError(f"No tool registered for @type {_type!r}")`);
|
|
5574
7561
|
});
|
|
5575
7562
|
cb.line("return _fn(params, runner)");
|
|
5576
7563
|
});
|
|
@@ -5579,11 +7566,11 @@ var PythonBackend = class {
|
|
|
5579
7566
|
name = "python";
|
|
5580
7567
|
target = "python";
|
|
5581
7568
|
emitApp(ctx, scope) {
|
|
5582
|
-
const code =
|
|
7569
|
+
const { code, entrypoint } = generatePythonModule(ctx, scope);
|
|
5583
7570
|
const fileName = `${appModuleName$1(ctx.app)}.py`;
|
|
5584
7571
|
return {
|
|
5585
7572
|
meta: ctx.app,
|
|
5586
|
-
entrypoint
|
|
7573
|
+
entrypoint,
|
|
5587
7574
|
files: new Map([[fileName, code]]),
|
|
5588
7575
|
errors: [],
|
|
5589
7576
|
warnings: []
|
|
@@ -6577,6 +8564,10 @@ var JsonSchemaBackend = class {
|
|
|
6577
8564
|
|
|
6578
8565
|
//#endregion
|
|
6579
8566
|
//#region src/backend/typescript/typemap.ts
|
|
8567
|
+
const TS_IDENT_RE$1 = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
8568
|
+
function objKey(key) {
|
|
8569
|
+
return TS_IDENT_RE$1.test(key) ? key : JSON.stringify(key);
|
|
8570
|
+
}
|
|
6580
8571
|
function mapType(type, resolve) {
|
|
6581
8572
|
switch (type.kind) {
|
|
6582
8573
|
case "scalar": return {
|
|
@@ -6596,7 +8587,7 @@ function mapType(type, resolve) {
|
|
|
6596
8587
|
case "struct": {
|
|
6597
8588
|
const name = resolve(type);
|
|
6598
8589
|
if (name) return name;
|
|
6599
|
-
return `{ ${Object.entries(type.fields).filter(([, v]) => v.kind !== "literal").map(([k, v]) => `${k}: ${mapType(v, resolve)}`).join("; ")} }`;
|
|
8590
|
+
return `{ ${Object.entries(type.fields).filter(([, v]) => v.kind !== "literal").map(([k, v]) => `${objKey(k)}: ${mapType(v, resolve)}`).join("; ")} }`;
|
|
6600
8591
|
}
|
|
6601
8592
|
case "union": {
|
|
6602
8593
|
const name = resolve(type);
|
|
@@ -6758,10 +8749,6 @@ function accessOf(binding, arg) {
|
|
|
6758
8749
|
* carrying a Boutiques default. Restricted to single-segment (root) field access
|
|
6759
8750
|
* so a nested field can never accidentally pick up a same-named root default.
|
|
6760
8751
|
*/
|
|
6761
|
-
function rootFieldDefault$1(binding, defaults) {
|
|
6762
|
-
const a = binding.access;
|
|
6763
|
-
if (a.length === 1 && a[0]?.kind === "field") return defaults.get(binding.name);
|
|
6764
|
-
}
|
|
6765
8752
|
/**
|
|
6766
8753
|
* Render a binding's access for an UNCONDITIONAL value read (terminal, repeat
|
|
6767
8754
|
* loop, alternative dispatch): substitutes the field's default via
|
|
@@ -6770,22 +8757,9 @@ function rootFieldDefault$1(binding, defaults) {
|
|
|
6770
8757
|
* emitted code is byte-identical to before for the common case.
|
|
6771
8758
|
*/
|
|
6772
8759
|
function readAccess(binding, arg) {
|
|
6773
|
-
const def = rootFieldDefault
|
|
8760
|
+
const def = rootFieldDefault(binding, arg.defaults);
|
|
6774
8761
|
return def !== void 0 ? `(${accessOf(binding, arg)} ?? ${def})` : accessOf(binding, arg);
|
|
6775
8762
|
}
|
|
6776
|
-
/** Build the field-name -> rendered-default map for a struct root (else empty).
|
|
6777
|
-
* Includes only non-optional defaulted fields (optional fields are
|
|
6778
|
-
* presence-guarded; their default comes from the factory's kwarg signature). */
|
|
6779
|
-
function collectDefaults$1(ctx, rootType) {
|
|
6780
|
-
const out = /* @__PURE__ */ new Map();
|
|
6781
|
-
if (rootType?.kind !== "struct") return out;
|
|
6782
|
-
for (const [name, fi] of collectFieldInfo(ctx, rootType)) {
|
|
6783
|
-
if (fi.defaultValue === void 0) continue;
|
|
6784
|
-
if (rootType.fields[name]?.kind === "optional") continue;
|
|
6785
|
-
out.set(name, renderTsLiteral(fi.defaultValue));
|
|
6786
|
-
}
|
|
6787
|
-
return out;
|
|
6788
|
-
}
|
|
6789
8763
|
let loopVarCounter = 0;
|
|
6790
8764
|
/**
|
|
6791
8765
|
* Build arg-building code for an IR tree via recursive descent.
|
|
@@ -6798,7 +8772,7 @@ function buildArgs(rootExpr, ctx, rootType) {
|
|
|
6798
8772
|
return walk$1(rootExpr, ctx, {
|
|
6799
8773
|
joinDepth: 0,
|
|
6800
8774
|
loopVars: /* @__PURE__ */ new Map(),
|
|
6801
|
-
defaults: collectDefaults
|
|
8775
|
+
defaults: collectDefaults(ctx, renderTsLiteral, rootType)
|
|
6802
8776
|
});
|
|
6803
8777
|
}
|
|
6804
8778
|
function walk$1(node, ctx, arg) {
|
|
@@ -6953,7 +8927,7 @@ function walkAlternative(node, ctx, arg) {
|
|
|
6953
8927
|
//#region src/backend/typescript/emit.ts
|
|
6954
8928
|
function emitJsDoc(cb, description) {
|
|
6955
8929
|
if (!description) return;
|
|
6956
|
-
const lines = description.split("\n");
|
|
8930
|
+
const lines = description.replace(/\*\//g, "*\\/").split("\n");
|
|
6957
8931
|
if (lines.length === 1) cb.line(`/** ${lines[0]} */`);
|
|
6958
8932
|
else {
|
|
6959
8933
|
cb.line("/**");
|
|
@@ -6961,15 +8935,14 @@ function emitJsDoc(cb, description) {
|
|
|
6961
8935
|
cb.line(" */");
|
|
6962
8936
|
}
|
|
6963
8937
|
}
|
|
6964
|
-
function emitImports(cb
|
|
6965
|
-
|
|
8938
|
+
function emitImports(cb) {
|
|
8939
|
+
cb.line(`import type { ${[
|
|
6966
8940
|
"Runner",
|
|
6967
8941
|
"Execution",
|
|
6968
8942
|
"Metadata",
|
|
6969
|
-
"InputPathType"
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
cb.line(`import type { ${inputs.join(", ")} } from "styxdefs";`);
|
|
8943
|
+
"InputPathType",
|
|
8944
|
+
"OutputPathType"
|
|
8945
|
+
].join(", ")} } from "styxdefs";`);
|
|
6973
8946
|
cb.line("import { getGlobalRunner, StyxValidationError } from \"styxdefs\";");
|
|
6974
8947
|
}
|
|
6975
8948
|
function emitMetadata(ctx, metaConst, cb) {
|
|
@@ -7313,7 +9286,7 @@ function emitValue(e, type, node, wireKey, access, expected) {
|
|
|
7313
9286
|
const itemNode = findRepeatNode(node)?.attrs.node;
|
|
7314
9287
|
const elem = e.scope.add("el");
|
|
7315
9288
|
e.cb.line(`for (const ${elem} of ${access}) {`);
|
|
7316
|
-
e.cb.indent(() => emitValue(e, type.item, itemNode, wireKey, elem,
|
|
9289
|
+
e.cb.indent(() => emitValue(e, type.item, itemNode, wireKey, elem, expectedType(e, type.item)));
|
|
7317
9290
|
e.cb.line("}");
|
|
7318
9291
|
return;
|
|
7319
9292
|
}
|
|
@@ -7448,26 +9421,6 @@ function emitOutputsInterface(ctx, outputsType, cb) {
|
|
|
7448
9421
|
});
|
|
7449
9422
|
cb.line(`}`);
|
|
7450
9423
|
}
|
|
7451
|
-
/** The rendered default for a binding iff it is a root-level defaulted field. */
|
|
7452
|
-
function rootFieldDefault(binding, defaults) {
|
|
7453
|
-
if (!binding) return void 0;
|
|
7454
|
-
const a = binding.access;
|
|
7455
|
-
if (a.length === 1 && a[0]?.kind === "field") return defaults.get(binding.name);
|
|
7456
|
-
}
|
|
7457
|
-
/** Build the field-name -> rendered-default map for the struct root (else empty).
|
|
7458
|
-
* Includes only non-optional defaulted fields (optional fields are
|
|
7459
|
-
* presence-guarded; their default comes from the factory's kwarg signature). */
|
|
7460
|
-
function collectDefaults(ctx) {
|
|
7461
|
-
const out = /* @__PURE__ */ new Map();
|
|
7462
|
-
const rootType = ctx.resolve(ctx.expr)?.type;
|
|
7463
|
-
if (rootType?.kind !== "struct") return out;
|
|
7464
|
-
for (const [name, fi] of collectFieldInfo(ctx, rootType)) {
|
|
7465
|
-
if (fi.defaultValue === void 0) continue;
|
|
7466
|
-
if (rootType.fields[name]?.kind === "optional") continue;
|
|
7467
|
-
out.set(name, renderTsLiteral(fi.defaultValue));
|
|
7468
|
-
}
|
|
7469
|
-
return out;
|
|
7470
|
-
}
|
|
7471
9424
|
/**
|
|
7472
9425
|
* Render one output's wrapper stack and emit the assignment inside the
|
|
7473
9426
|
* innermost wrapper. Nesting is done via recursive callbacks so the
|
|
@@ -7511,10 +9464,15 @@ function renderWrapperOpen(atom, ec) {
|
|
|
7511
9464
|
loopVar: v
|
|
7512
9465
|
};
|
|
7513
9466
|
}
|
|
7514
|
-
if (atom.kind === "variant")
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
9467
|
+
if (atom.kind === "variant") {
|
|
9468
|
+
const access = bindingAccess(atom.binding, ec);
|
|
9469
|
+
const check = `${access}["@type"] === ${JSON.stringify(atom.variant)}`;
|
|
9470
|
+
const union = variantAtomUnion(atom, ec.ctx.bindings);
|
|
9471
|
+
return {
|
|
9472
|
+
open: `if (${union && unionIsMixed(union) ? `typeof ${access} === "object" && ${access} !== null && ${check}` : check}) {`,
|
|
9473
|
+
close: `}`
|
|
9474
|
+
};
|
|
9475
|
+
}
|
|
7518
9476
|
const binding = ec.ctx.bindings.get(atom.binding);
|
|
7519
9477
|
const access = bindingAccess(atom.binding, ec);
|
|
7520
9478
|
return {
|
|
@@ -7586,7 +9544,7 @@ function emitBuildOutputs(ctx, paramsType, outputsType, funcName, cb) {
|
|
|
7586
9544
|
ctx,
|
|
7587
9545
|
iter: /* @__PURE__ */ new Map(),
|
|
7588
9546
|
fieldShapes: new Map(fields.map((f) => [f.id, f.shape])),
|
|
7589
|
-
defaults: collectDefaults(ctx)
|
|
9547
|
+
defaults: collectDefaults(ctx, renderTsLiteral)
|
|
7590
9548
|
};
|
|
7591
9549
|
cb.line(`const outputs: ${outputsType} = {`);
|
|
7592
9550
|
cb.indent(() => {
|
|
@@ -7756,13 +9714,22 @@ function buildEmitModel$1(ctx, scope = new Scope(TS_RESERVED)) {
|
|
|
7756
9714
|
};
|
|
7757
9715
|
}
|
|
7758
9716
|
function generateTypeScript(ctx, packageScope) {
|
|
9717
|
+
return generateTypeScriptModule(ctx, packageScope).code;
|
|
9718
|
+
}
|
|
9719
|
+
/**
|
|
9720
|
+
* Emit the module and, alongside it, the dispatch entrypoint carrying the
|
|
9721
|
+
* *scope-registered* execute-function name. Computing the entrypoint here (not
|
|
9722
|
+
* via the scope-blind `appEntrypoint`) keeps the suite dispatcher in sync with
|
|
9723
|
+
* the actual emitted symbol when a shared package scope suffix-bumps a collision.
|
|
9724
|
+
*/
|
|
9725
|
+
function generateTypeScriptModule(ctx, packageScope) {
|
|
7759
9726
|
const cb = new CodeBuilder(" ");
|
|
7760
9727
|
const scope = packageScope ?? new Scope(TS_RESERVED);
|
|
7761
9728
|
const { appId, pkg, names, rootType, rootIsStruct, namedTypes, typeDecls, rootTypeTag, paramsType, sigEntries } = buildEmitModel$1(ctx, scope);
|
|
7762
9729
|
cb.comment("This file was auto generated by Styx.");
|
|
7763
9730
|
cb.comment("Do not edit this file directly.");
|
|
7764
9731
|
cb.blank();
|
|
7765
|
-
emitImports(cb
|
|
9732
|
+
emitImports(cb);
|
|
7766
9733
|
cb.blank();
|
|
7767
9734
|
emitMetadata(ctx, names.metadata, cb);
|
|
7768
9735
|
cb.blank();
|
|
@@ -7783,13 +9750,23 @@ function generateTypeScript(ctx, packageScope) {
|
|
|
7783
9750
|
cb.blank();
|
|
7784
9751
|
emitBuildOutputs(ctx, paramsType, names.outputs, names.outputsFn, cb);
|
|
7785
9752
|
cb.blank();
|
|
7786
|
-
|
|
9753
|
+
const executeName = rootIsStruct ? names.execute : names.wrapper;
|
|
9754
|
+
emitWrapperFunction(ctx, paramsType, executeName, names.metadata, names.cargs, names.outputsFn, names.outputs, names.validate, streamFieldIds(ctx), cb);
|
|
7787
9755
|
cb.blank();
|
|
7788
9756
|
if (rootIsStruct) {
|
|
7789
9757
|
emitKwargWrapper(ctx, sigEntries, names.wrapper, names.paramsFn, names.execute, names.outputs, cb);
|
|
7790
9758
|
cb.blank();
|
|
7791
9759
|
}
|
|
7792
|
-
|
|
9760
|
+
const entryAppId = ctx.app?.id;
|
|
9761
|
+
const entryPkg = ctx.package?.name;
|
|
9762
|
+
const entrypoint = entryAppId && entryPkg ? {
|
|
9763
|
+
type: `${entryPkg}/${entryAppId}`,
|
|
9764
|
+
executeFn: executeName
|
|
9765
|
+
} : void 0;
|
|
9766
|
+
return {
|
|
9767
|
+
code: cb.toString(),
|
|
9768
|
+
entrypoint
|
|
9769
|
+
};
|
|
7793
9770
|
}
|
|
7794
9771
|
/**
|
|
7795
9772
|
* Module name (file stem) for an app: snake_case of app.id, fallback `output`.
|
|
@@ -7871,11 +9848,11 @@ var TypeScriptBackend = class {
|
|
|
7871
9848
|
name = "typescript";
|
|
7872
9849
|
target = "typescript";
|
|
7873
9850
|
emitApp(ctx, scope) {
|
|
7874
|
-
const code =
|
|
9851
|
+
const { code, entrypoint } = generateTypeScriptModule(ctx, scope);
|
|
7875
9852
|
const fileName = `${appModuleName(ctx.app)}.ts`;
|
|
7876
9853
|
return {
|
|
7877
9854
|
meta: ctx.app,
|
|
7878
|
-
entrypoint
|
|
9855
|
+
entrypoint,
|
|
7879
9856
|
files: new Map([[fileName, code]]),
|
|
7880
9857
|
errors: [],
|
|
7881
9858
|
warnings: []
|
|
@@ -8141,6 +10118,15 @@ const canonicalize = {
|
|
|
8141
10118
|
default: return "";
|
|
8142
10119
|
}
|
|
8143
10120
|
}
|
|
10121
|
+
function identityKey(node) {
|
|
10122
|
+
const m = node.meta;
|
|
10123
|
+
const metaKey = m ? [
|
|
10124
|
+
m.name ?? "",
|
|
10125
|
+
m.variantTag ?? "",
|
|
10126
|
+
m.outputs ? JSON.stringify(m.outputs) : ""
|
|
10127
|
+
].join("|") : "";
|
|
10128
|
+
return `${structuralHash(node)}#${metaKey}`;
|
|
10129
|
+
}
|
|
8144
10130
|
function sortKey(node) {
|
|
8145
10131
|
const name = node.meta?.name ?? "";
|
|
8146
10132
|
return `${node.kind}:${name}:${structuralHash(node)}`;
|
|
@@ -8153,13 +10139,13 @@ const canonicalize = {
|
|
|
8153
10139
|
const seen = /* @__PURE__ */ new Set();
|
|
8154
10140
|
const alts = [];
|
|
8155
10141
|
for (const child of sorted) {
|
|
8156
|
-
const
|
|
8157
|
-
if (!seen.has(
|
|
8158
|
-
seen.add(
|
|
10142
|
+
const key = identityKey(child);
|
|
10143
|
+
if (!seen.has(key)) {
|
|
10144
|
+
seen.add(key);
|
|
8159
10145
|
alts.push(child);
|
|
8160
10146
|
} else changed = true;
|
|
8161
10147
|
}
|
|
8162
|
-
if (alts.length !== children.length || alts.some((alt, i) =>
|
|
10148
|
+
if (alts.length !== children.length || alts.some((alt, i) => identityKey(alt) !== identityKey(children[i]))) changed = true;
|
|
8163
10149
|
return {
|
|
8164
10150
|
...node,
|
|
8165
10151
|
attrs: {
|
|
@@ -8442,11 +10428,24 @@ const simplify = {
|
|
|
8442
10428
|
const prev = nodes[nodes.length - 1];
|
|
8443
10429
|
if (prev?.kind === "literal" && child.kind === "literal" && !prev.meta && !child.meta && node.attrs.join === "") {
|
|
8444
10430
|
changed = true;
|
|
8445
|
-
|
|
10431
|
+
nodes[nodes.length - 1] = {
|
|
10432
|
+
...prev,
|
|
10433
|
+
attrs: {
|
|
10434
|
+
...prev.attrs,
|
|
10435
|
+
str: prev.attrs.str + child.attrs.str
|
|
10436
|
+
}
|
|
10437
|
+
};
|
|
8446
10438
|
} else nodes.push(child);
|
|
8447
10439
|
}
|
|
8448
10440
|
if (nodes.length === 1) {
|
|
8449
10441
|
const child = nodes[0];
|
|
10442
|
+
if (node.meta?.outputs?.length && child.kind === "literal") return {
|
|
10443
|
+
...node,
|
|
10444
|
+
attrs: {
|
|
10445
|
+
...node.attrs,
|
|
10446
|
+
nodes
|
|
10447
|
+
}
|
|
10448
|
+
};
|
|
8450
10449
|
changed = true;
|
|
8451
10450
|
const mergedMeta = mergeMeta(node.meta, child.meta);
|
|
8452
10451
|
return mergedMeta ? {
|
|
@@ -8864,7 +10863,7 @@ function defaultNamingStrategy() {
|
|
|
8864
10863
|
function isBooleanLiteralPair(variants) {
|
|
8865
10864
|
if (variants.length !== 2 || !variants.every((v) => v.type.kind === "literal")) return false;
|
|
8866
10865
|
const [a, b] = variants.map((v) => v.type.kind === "literal" ? v.type.value : null);
|
|
8867
|
-
return a === 0 && b === 1 || a === 1 && b === 0 || a === "
|
|
10866
|
+
return a === 0 && b === 1 || a === 1 && b === 0 || a === "false" && b === "true" || a === "true" && b === "false";
|
|
8868
10867
|
}
|
|
8869
10868
|
function literalFromNode(node) {
|
|
8870
10869
|
const str = node.attrs.str;
|
|
@@ -9090,7 +11089,8 @@ function solve(expr, options) {
|
|
|
9090
11089
|
//#region src/index.ts
|
|
9091
11090
|
function compile(source, filenameOrOptions) {
|
|
9092
11091
|
const options = typeof filenameOrOptions === "string" ? { filename: filenameOrOptions } : filenameOrOptions ?? {};
|
|
9093
|
-
const
|
|
11092
|
+
const byExtension = options.filename?.endsWith(".argtype") ? "argtype" : void 0;
|
|
11093
|
+
const format = options.format ?? byExtension ?? detectFormat(source);
|
|
9094
11094
|
if (!format) return {
|
|
9095
11095
|
expr: {
|
|
9096
11096
|
kind: "sequence",
|
|
@@ -9099,10 +11099,20 @@ function compile(source, filenameOrOptions) {
|
|
|
9099
11099
|
errors: [{ message: "Could not detect input format. Specify format explicitly." }],
|
|
9100
11100
|
warnings: []
|
|
9101
11101
|
};
|
|
9102
|
-
return (
|
|
11102
|
+
return (() => {
|
|
11103
|
+
switch (format) {
|
|
11104
|
+
case "argdump": return new ArgdumpParser();
|
|
11105
|
+
case "argtype": return new ArgtypeParser();
|
|
11106
|
+
case "workbench": return new WorkbenchParser();
|
|
11107
|
+
case "mrtrix": return new MrtrixParser();
|
|
11108
|
+
case "boutiques": return new BoutiquesParser();
|
|
11109
|
+
default: return new BoutiquesParser();
|
|
11110
|
+
}
|
|
11111
|
+
})().parse(source, options.filename);
|
|
9103
11112
|
}
|
|
9104
11113
|
|
|
9105
11114
|
//#endregion
|
|
11115
|
+
exports.ArgtypeBackend = ArgtypeBackend;
|
|
9106
11116
|
exports.BoutiquesBackend = BoutiquesBackend;
|
|
9107
11117
|
exports.CodeBuilder = CodeBuilder;
|
|
9108
11118
|
exports.JsonSchemaBackend = JsonSchemaBackend;
|
|
@@ -9124,7 +11134,6 @@ exports.camelCase = camelCase;
|
|
|
9124
11134
|
exports.canonicalize = canonicalize;
|
|
9125
11135
|
exports.collectFieldInfo = collectFieldInfo;
|
|
9126
11136
|
exports.collectNamedTypes = collectNamedTypes;
|
|
9127
|
-
exports.compactTokens = compactTokens;
|
|
9128
11137
|
exports.compile = compile;
|
|
9129
11138
|
exports.compose = compose;
|
|
9130
11139
|
exports.createContext = createContext;
|
|
@@ -9141,6 +11150,7 @@ exports.flatten = flatten;
|
|
|
9141
11150
|
exports.float = float;
|
|
9142
11151
|
exports.format = format;
|
|
9143
11152
|
exports.formatSolveResult = formatSolveResult;
|
|
11153
|
+
exports.generateArgtype = generateArgtype;
|
|
9144
11154
|
exports.generateBoutiques = generateBoutiques;
|
|
9145
11155
|
exports.generateNipype = generateNipype;
|
|
9146
11156
|
exports.generateOutputsSchema = generateOutputsSchema;
|
|
@@ -9149,8 +11159,6 @@ exports.generatePython = generatePython;
|
|
|
9149
11159
|
exports.generateSchema = generateSchema;
|
|
9150
11160
|
exports.generateTypeScript = generateTypeScript;
|
|
9151
11161
|
exports.int = int;
|
|
9152
|
-
exports.isGated = isGated;
|
|
9153
|
-
exports.isIterated = isIterated;
|
|
9154
11162
|
exports.isStructural = isStructural;
|
|
9155
11163
|
exports.isTerminal = isTerminal;
|
|
9156
11164
|
exports.lit = lit;
|
|
@@ -9160,8 +11168,6 @@ exports.opt = opt;
|
|
|
9160
11168
|
exports.outputGate = outputGate;
|
|
9161
11169
|
exports.pascalCase = pascalCase;
|
|
9162
11170
|
exports.path = path;
|
|
9163
|
-
exports.planOutput = planOutput;
|
|
9164
|
-
exports.planScope = planScope;
|
|
9165
11171
|
exports.pydraNames = pydraNames;
|
|
9166
11172
|
exports.removeEmpty = removeEmpty;
|
|
9167
11173
|
exports.renderPythonCall = renderPythonCall;
|