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