@styx-api/core 0.6.1 → 0.7.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 +2079 -147
- package/dist/index.d.cts +41 -4
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +41 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2078 -148
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/backend/argtype/__fixtures__/microsyntax.argtype +15 -0
- package/src/backend/argtype/__fixtures__/subcommands.argtype +25 -0
- package/src/backend/argtype/argtype.ts +45 -0
- package/src/backend/argtype/emit.ts +725 -0
- package/src/backend/argtype/index.ts +2 -0
- package/src/backend/index.ts +1 -0
- package/src/frontend/argtype/__fixtures__/bet.argtype +103 -0
- package/src/frontend/argtype/ast.ts +101 -0
- package/src/frontend/argtype/doc.ts +56 -0
- package/src/frontend/argtype/frontmatter.ts +193 -0
- package/src/frontend/argtype/index.ts +1 -0
- package/src/frontend/argtype/lexer.ts +244 -0
- package/src/frontend/argtype/lower.ts +561 -0
- package/src/frontend/argtype/parser-frontend.ts +51 -0
- package/src/frontend/argtype/parser.ts +487 -0
- package/src/frontend/argtype/template.ts +147 -0
- package/src/frontend/detect-format.ts +28 -3
- package/src/index.ts +23 -9
package/dist/index.mjs
CHANGED
|
@@ -732,6 +732,972 @@ var ArgdumpParser = class {
|
|
|
732
732
|
}
|
|
733
733
|
};
|
|
734
734
|
|
|
735
|
+
//#endregion
|
|
736
|
+
//#region src/frontend/argtype/lexer.ts
|
|
737
|
+
const PUNCT = {
|
|
738
|
+
":": "colon",
|
|
739
|
+
"=": "eq",
|
|
740
|
+
"|": "pipe",
|
|
741
|
+
".": "dot",
|
|
742
|
+
",": "comma",
|
|
743
|
+
"(": "lparen",
|
|
744
|
+
")": "rparen"
|
|
745
|
+
};
|
|
746
|
+
function isIdentStart(ch) {
|
|
747
|
+
return /[A-Za-z_]/.test(ch);
|
|
748
|
+
}
|
|
749
|
+
function isIdentPart(ch) {
|
|
750
|
+
return /[A-Za-z0-9_]/.test(ch);
|
|
751
|
+
}
|
|
752
|
+
function isDigit(ch) {
|
|
753
|
+
return ch >= "0" && ch <= "9";
|
|
754
|
+
}
|
|
755
|
+
function lex(source) {
|
|
756
|
+
const tokens = [];
|
|
757
|
+
const errors = [];
|
|
758
|
+
let i = 0;
|
|
759
|
+
let line = 1;
|
|
760
|
+
let col = 1;
|
|
761
|
+
const peek = (o = 0) => source[i + o] ?? "";
|
|
762
|
+
const advance = () => {
|
|
763
|
+
const ch = source[i++];
|
|
764
|
+
if (ch === "\n") {
|
|
765
|
+
line++;
|
|
766
|
+
col = 1;
|
|
767
|
+
} else col++;
|
|
768
|
+
return ch;
|
|
769
|
+
};
|
|
770
|
+
const push = (kind, value, l, c) => {
|
|
771
|
+
tokens.push({
|
|
772
|
+
kind,
|
|
773
|
+
value,
|
|
774
|
+
line: l,
|
|
775
|
+
column: c
|
|
776
|
+
});
|
|
777
|
+
};
|
|
778
|
+
while (i < source.length) {
|
|
779
|
+
const ch = peek();
|
|
780
|
+
const startLine = line;
|
|
781
|
+
const startCol = col;
|
|
782
|
+
if (ch === " " || ch === " " || ch === "\r" || ch === "\n") {
|
|
783
|
+
advance();
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
if (ch === "/" && peek(1) === "/") {
|
|
787
|
+
const isDoc = peek(2) === "/";
|
|
788
|
+
advance();
|
|
789
|
+
advance();
|
|
790
|
+
if (isDoc) advance();
|
|
791
|
+
let text = "";
|
|
792
|
+
while (i < source.length && peek() !== "\n") text += advance();
|
|
793
|
+
if (isDoc) push("doc", text.trim(), startLine, startCol);
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
if (ch === "\"") {
|
|
797
|
+
advance();
|
|
798
|
+
let str = "";
|
|
799
|
+
let closed = false;
|
|
800
|
+
while (i < source.length) {
|
|
801
|
+
const c = advance();
|
|
802
|
+
if (c === "\\") {
|
|
803
|
+
const esc = i < source.length ? advance() : "";
|
|
804
|
+
str += unescape(esc);
|
|
805
|
+
} else if (c === "\"") {
|
|
806
|
+
closed = true;
|
|
807
|
+
break;
|
|
808
|
+
} else if (c === "\n") break;
|
|
809
|
+
else str += c;
|
|
810
|
+
}
|
|
811
|
+
if (!closed) errors.push({
|
|
812
|
+
message: "Unterminated string literal",
|
|
813
|
+
line: startLine,
|
|
814
|
+
column: startCol
|
|
815
|
+
});
|
|
816
|
+
push("string", str, startLine, startCol);
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
if (ch === "`") {
|
|
820
|
+
advance();
|
|
821
|
+
let body = "";
|
|
822
|
+
let closed = false;
|
|
823
|
+
while (i < source.length) {
|
|
824
|
+
const c = advance();
|
|
825
|
+
if (c === "\\") {
|
|
826
|
+
body += c;
|
|
827
|
+
if (i < source.length) body += advance();
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
if (c === "`") {
|
|
831
|
+
closed = true;
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
834
|
+
body += c;
|
|
835
|
+
}
|
|
836
|
+
if (!closed) errors.push({
|
|
837
|
+
message: "Unterminated template literal",
|
|
838
|
+
line: startLine,
|
|
839
|
+
column: startCol
|
|
840
|
+
});
|
|
841
|
+
push("template", body, startLine, startCol);
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
if (isDigit(ch) || ch === "-" && isDigit(peek(1))) {
|
|
845
|
+
let num = advance();
|
|
846
|
+
while (i < source.length && isDigit(peek())) num += advance();
|
|
847
|
+
if (peek() === "." && isDigit(peek(1))) {
|
|
848
|
+
num += advance();
|
|
849
|
+
while (i < source.length && isDigit(peek())) num += advance();
|
|
850
|
+
}
|
|
851
|
+
if (peek() === "e" || peek() === "E") {
|
|
852
|
+
const signed = peek(1) === "+" || peek(1) === "-";
|
|
853
|
+
if (isDigit(peek(1)) || signed && isDigit(peek(2))) {
|
|
854
|
+
num += advance();
|
|
855
|
+
if (peek() === "+" || peek() === "-") num += advance();
|
|
856
|
+
while (i < source.length && isDigit(peek())) num += advance();
|
|
857
|
+
} else {
|
|
858
|
+
num += advance();
|
|
859
|
+
errors.push({
|
|
860
|
+
message: `Malformed number '${num}': exponent has no digits`,
|
|
861
|
+
line: startLine,
|
|
862
|
+
column: startCol
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
push("number", num, startLine, startCol);
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
if (isIdentStart(ch)) {
|
|
870
|
+
let id = advance();
|
|
871
|
+
while (i < source.length && isIdentPart(peek())) id += advance();
|
|
872
|
+
push("ident", id, startLine, startCol);
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
const punct = PUNCT[ch];
|
|
876
|
+
if (punct) {
|
|
877
|
+
advance();
|
|
878
|
+
push(punct, ch, startLine, startCol);
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
errors.push({
|
|
882
|
+
message: `Unexpected character '${ch}'`,
|
|
883
|
+
line: startLine,
|
|
884
|
+
column: startCol
|
|
885
|
+
});
|
|
886
|
+
advance();
|
|
887
|
+
}
|
|
888
|
+
push("eof", "", line, col);
|
|
889
|
+
return {
|
|
890
|
+
tokens,
|
|
891
|
+
errors
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
/** Minimal escape handling inside double-quoted strings. */
|
|
895
|
+
function unescape(esc) {
|
|
896
|
+
switch (esc) {
|
|
897
|
+
case "n": return "\n";
|
|
898
|
+
case "t": return " ";
|
|
899
|
+
case "r": return "\r";
|
|
900
|
+
case "\"": return "\"";
|
|
901
|
+
case "\\": return "\\";
|
|
902
|
+
case "": return "";
|
|
903
|
+
default: return esc;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
//#endregion
|
|
908
|
+
//#region src/frontend/argtype/frontmatter.ts
|
|
909
|
+
function splitFrontmatter(source) {
|
|
910
|
+
const lines = source.split("\n");
|
|
911
|
+
let start = 0;
|
|
912
|
+
while (start < lines.length && lines[start].trim() === "") start++;
|
|
913
|
+
if (lines[start]?.trim() !== "---") return {
|
|
914
|
+
body: source,
|
|
915
|
+
errors: []
|
|
916
|
+
};
|
|
917
|
+
let end = -1;
|
|
918
|
+
for (let i = start + 1; i < lines.length; i++) if (lines[i].trim() === "---") {
|
|
919
|
+
end = i;
|
|
920
|
+
break;
|
|
921
|
+
}
|
|
922
|
+
if (end === -1) return {
|
|
923
|
+
body: source,
|
|
924
|
+
errors: ["Unterminated frontmatter block (missing closing '---')"]
|
|
925
|
+
};
|
|
926
|
+
const { value, errors } = parseYamlish(lines.slice(start + 1, end));
|
|
927
|
+
return {
|
|
928
|
+
frontmatter: value,
|
|
929
|
+
body: lines.map((line, i) => i >= start && i <= end ? "" : line).join("\n"),
|
|
930
|
+
errors
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
function tokenizeLines(raw) {
|
|
934
|
+
const out = [];
|
|
935
|
+
for (const line of raw) {
|
|
936
|
+
const noComment = stripComment(line);
|
|
937
|
+
if (noComment.trim() === "") continue;
|
|
938
|
+
const indent = noComment.length - noComment.trimStart().length;
|
|
939
|
+
out.push({
|
|
940
|
+
indent,
|
|
941
|
+
content: noComment.trim()
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
return out;
|
|
945
|
+
}
|
|
946
|
+
/** Strip a trailing `#` comment that is not inside quotes. Following YAML, a
|
|
947
|
+
* `#` only starts a comment at the start of the line or after whitespace, so an
|
|
948
|
+
* unquoted value containing `#` (e.g. a URL fragment `https://x/#frag`) is kept
|
|
949
|
+
* intact. */
|
|
950
|
+
function stripComment(line) {
|
|
951
|
+
let inQuote = null;
|
|
952
|
+
for (let i = 0; i < line.length; i++) {
|
|
953
|
+
const ch = line[i];
|
|
954
|
+
if (inQuote === "\"" && ch === "\\") {
|
|
955
|
+
i++;
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
if (inQuote) {
|
|
959
|
+
if (ch === inQuote) inQuote = null;
|
|
960
|
+
} else if (ch === "\"" || ch === "'") inQuote = ch;
|
|
961
|
+
else if (ch === "#" && (i === 0 || line[i - 1] === " " || line[i - 1] === " ")) return line.slice(0, i);
|
|
962
|
+
}
|
|
963
|
+
return line;
|
|
964
|
+
}
|
|
965
|
+
function parseYamlish(raw) {
|
|
966
|
+
const lines = tokenizeLines(raw);
|
|
967
|
+
const errors = [];
|
|
968
|
+
let pos = 0;
|
|
969
|
+
function parseBlock(minIndent) {
|
|
970
|
+
if (pos < lines.length && lines[pos].indent >= minIndent && lines[pos].content.startsWith("- ")) {
|
|
971
|
+
const seq = [];
|
|
972
|
+
const indent = lines[pos].indent;
|
|
973
|
+
while (pos < lines.length && lines[pos].indent === indent && lines[pos].content.startsWith("- ")) {
|
|
974
|
+
seq.push(parseScalar(lines[pos].content.slice(2).trim()));
|
|
975
|
+
pos++;
|
|
976
|
+
}
|
|
977
|
+
return seq;
|
|
978
|
+
}
|
|
979
|
+
const map = {};
|
|
980
|
+
if (pos >= lines.length) return map;
|
|
981
|
+
const indent = lines[pos].indent;
|
|
982
|
+
while (pos < lines.length && lines[pos].indent === indent && !lines[pos].content.startsWith("- ")) {
|
|
983
|
+
const { content } = lines[pos];
|
|
984
|
+
const colon = findColon(content);
|
|
985
|
+
if (colon === -1) {
|
|
986
|
+
errors.push(`Malformed frontmatter line: '${content}'`);
|
|
987
|
+
pos++;
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
const key = content.slice(0, colon).trim();
|
|
991
|
+
const valueText = content.slice(colon + 1).trim();
|
|
992
|
+
pos++;
|
|
993
|
+
if (valueText !== "") map[key] = parseScalar(valueText);
|
|
994
|
+
else if (pos < lines.length && lines[pos].indent > indent) map[key] = parseBlock(lines[pos].indent);
|
|
995
|
+
else map[key] = null;
|
|
996
|
+
}
|
|
997
|
+
return map;
|
|
998
|
+
}
|
|
999
|
+
const value = parseBlock(0);
|
|
1000
|
+
return {
|
|
1001
|
+
value: isRecord$2(value) ? value : { _root: value },
|
|
1002
|
+
errors
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
function findColon(s) {
|
|
1006
|
+
let inQuote = null;
|
|
1007
|
+
for (let i = 0; i < s.length; i++) {
|
|
1008
|
+
const ch = s[i];
|
|
1009
|
+
if (inQuote === "\"" && ch === "\\") {
|
|
1010
|
+
i++;
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
if (inQuote) {
|
|
1014
|
+
if (ch === inQuote) inQuote = null;
|
|
1015
|
+
} else if (ch === "\"" || ch === "'") inQuote = ch;
|
|
1016
|
+
else if (ch === ":") return i;
|
|
1017
|
+
}
|
|
1018
|
+
return -1;
|
|
1019
|
+
}
|
|
1020
|
+
/** Reverse the backslash escapes an emitter applies to a quoted scalar
|
|
1021
|
+
* (`\n`, `\t`, `\r`, `\"`, `\\`), so values with newlines/quotes round-trip. */
|
|
1022
|
+
function unescapeScalar(s) {
|
|
1023
|
+
return s.replace(/\\(.)/g, (_, c) => c === "n" ? "\n" : c === "t" ? " " : c === "r" ? "\r" : c);
|
|
1024
|
+
}
|
|
1025
|
+
function parseScalar(text) {
|
|
1026
|
+
if (text.startsWith("\"") && text.endsWith("\"") && text.length >= 2) return unescapeScalar(text.slice(1, -1));
|
|
1027
|
+
if (text.startsWith("'") && text.endsWith("'") && text.length >= 2) return text.slice(1, -1);
|
|
1028
|
+
if (text === "true") return true;
|
|
1029
|
+
if (text === "false") return false;
|
|
1030
|
+
if (text === "null" || text === "~") return null;
|
|
1031
|
+
if (/^-?\d+(\.\d+)?$/.test(text)) return Number(text);
|
|
1032
|
+
return text;
|
|
1033
|
+
}
|
|
1034
|
+
function isRecord$2(x) {
|
|
1035
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
//#endregion
|
|
1039
|
+
//#region src/frontend/argtype/template.ts
|
|
1040
|
+
/**
|
|
1041
|
+
* Index of the `}` that closes an interpolation opened at `open` (the position
|
|
1042
|
+
* just past the `{`), or -1 if unterminated. A naive `indexOf("}")` would stop
|
|
1043
|
+
* at the first `}` even when it sits inside a quoted ref name (`{"a}b"}`) or a
|
|
1044
|
+
* quoted operation argument (`{a.or("{b}")}`); this scan skips double-quoted
|
|
1045
|
+
* spans (honoring `\"`) and balances nested `{}` so those round-trip.
|
|
1046
|
+
*/
|
|
1047
|
+
function interpolationEnd(body, open) {
|
|
1048
|
+
let depth = 1;
|
|
1049
|
+
let inQuote = false;
|
|
1050
|
+
for (let j = open; j < body.length; j++) {
|
|
1051
|
+
const c = body[j];
|
|
1052
|
+
if (inQuote) {
|
|
1053
|
+
if (c === "\\") j++;
|
|
1054
|
+
else if (c === "\"") inQuote = false;
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
if (c === "\"") inQuote = true;
|
|
1058
|
+
else if (c === "{") depth++;
|
|
1059
|
+
else if (c === "}" && --depth === 0) return j;
|
|
1060
|
+
}
|
|
1061
|
+
return -1;
|
|
1062
|
+
}
|
|
1063
|
+
function parseTemplate(body) {
|
|
1064
|
+
const tokens = [];
|
|
1065
|
+
const errors = [];
|
|
1066
|
+
let lit = "";
|
|
1067
|
+
let i = 0;
|
|
1068
|
+
const flushLit = () => {
|
|
1069
|
+
if (lit.length > 0) {
|
|
1070
|
+
tokens.push({
|
|
1071
|
+
kind: "literal",
|
|
1072
|
+
value: lit
|
|
1073
|
+
});
|
|
1074
|
+
lit = "";
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
while (i < body.length) {
|
|
1078
|
+
const ch = body[i];
|
|
1079
|
+
if (ch === "\\" && i + 1 < body.length) {
|
|
1080
|
+
const next = body[i + 1];
|
|
1081
|
+
lit += next === "{" || next === "}" || next === "`" || next === "\\" ? next : "\\" + next;
|
|
1082
|
+
i += 2;
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
if (ch === "{") {
|
|
1086
|
+
const close = interpolationEnd(body, i + 1);
|
|
1087
|
+
if (close === -1) {
|
|
1088
|
+
errors.push("Unterminated '{' in output template");
|
|
1089
|
+
lit += body.slice(i);
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
1092
|
+
flushLit();
|
|
1093
|
+
const { token, error } = parseRef(body.slice(i + 1, close).trim());
|
|
1094
|
+
if (error) errors.push(error);
|
|
1095
|
+
tokens.push(token);
|
|
1096
|
+
i = close + 1;
|
|
1097
|
+
} else {
|
|
1098
|
+
lit += ch;
|
|
1099
|
+
i++;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
flushLit();
|
|
1103
|
+
return {
|
|
1104
|
+
tokens,
|
|
1105
|
+
errors
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
/** Parse the inside of `{...}`: an optional name followed by `.op(arg)` calls.
|
|
1109
|
+
* The name is a bare identifier, a double-quoted string (for a non-identifier
|
|
1110
|
+
* target name, e.g. `{"4d_output"}`), or absent for a self-reference (`{}`). */
|
|
1111
|
+
function parseRef(inner) {
|
|
1112
|
+
const token = { kind: "ref" };
|
|
1113
|
+
let rest = inner;
|
|
1114
|
+
const quoted = /^"((?:[^"\\]|\\.)*)"/.exec(inner);
|
|
1115
|
+
if (quoted) {
|
|
1116
|
+
token.name = unescapeArg(quoted[1]);
|
|
1117
|
+
rest = inner.slice(quoted[0].length);
|
|
1118
|
+
} else {
|
|
1119
|
+
const nameMatch = /^[A-Za-z_][A-Za-z0-9_]*/.exec(inner);
|
|
1120
|
+
if (nameMatch) {
|
|
1121
|
+
token.name = nameMatch[0];
|
|
1122
|
+
rest = inner.slice(nameMatch[0].length);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
const opRe = /^\.\s*([A-Za-z_]+)\s*\(\s*(?:"((?:[^"\\]|\\.)*)")?\s*\)/;
|
|
1126
|
+
let error;
|
|
1127
|
+
while (rest.length > 0) {
|
|
1128
|
+
const m = opRe.exec(rest);
|
|
1129
|
+
if (!m) {
|
|
1130
|
+
error = `Unrecognized output-reference operation near '${rest}'`;
|
|
1131
|
+
break;
|
|
1132
|
+
}
|
|
1133
|
+
const op = m[1];
|
|
1134
|
+
const arg = m[2];
|
|
1135
|
+
switch (op) {
|
|
1136
|
+
case "strip_suffix":
|
|
1137
|
+
if (arg !== void 0) (token.stripSuffix ??= []).push(unescapeArg(arg));
|
|
1138
|
+
break;
|
|
1139
|
+
case "strip_prefix":
|
|
1140
|
+
if (arg !== void 0) (token.stripPrefix ??= []).push(unescapeArg(arg));
|
|
1141
|
+
break;
|
|
1142
|
+
case "basename":
|
|
1143
|
+
token.basename = true;
|
|
1144
|
+
break;
|
|
1145
|
+
case "or":
|
|
1146
|
+
if (arg !== void 0) token.or = unescapeArg(arg);
|
|
1147
|
+
break;
|
|
1148
|
+
default: error = `Unknown output-reference operation '${op}'`;
|
|
1149
|
+
}
|
|
1150
|
+
rest = rest.slice(m[0].length).trimStart();
|
|
1151
|
+
}
|
|
1152
|
+
return {
|
|
1153
|
+
token,
|
|
1154
|
+
...error && { error }
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
function unescapeArg(s) {
|
|
1158
|
+
return s.replace(/\\(.)/g, "$1");
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
//#endregion
|
|
1162
|
+
//#region src/frontend/argtype/doc.ts
|
|
1163
|
+
/**
|
|
1164
|
+
* Reflow a `///` description as Markdown-style prose: a single line break is a
|
|
1165
|
+
* soft wrap (the lines join with a space) and a blank line starts a new
|
|
1166
|
+
* paragraph (kept as `\n\n`). This lets an author wrap a long description across
|
|
1167
|
+
* several `///` lines for readability without forcing hard breaks, while real
|
|
1168
|
+
* paragraph structure survives. (It does not preserve Markdown lists or indented
|
|
1169
|
+
* code blocks - their single line breaks are joined like ordinary prose.)
|
|
1170
|
+
*/
|
|
1171
|
+
function reflowProse(text) {
|
|
1172
|
+
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");
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Split a `///` doc block into title + description. A leading Markdown H1
|
|
1176
|
+
* (`# Title` on the first line) is the title; everything after it is the
|
|
1177
|
+
* description. Without a leading `# `, the whole block is the description (no
|
|
1178
|
+
* title). The description is reflowed as prose (see `reflowProse`): single line
|
|
1179
|
+
* breaks soft-wrap, blank lines separate paragraphs.
|
|
1180
|
+
*/
|
|
1181
|
+
function splitDocText(raw) {
|
|
1182
|
+
const newline = raw.indexOf("\n");
|
|
1183
|
+
const firstLine = (newline === -1 ? raw : raw.slice(0, newline)).trim();
|
|
1184
|
+
if (firstLine.startsWith("# ")) {
|
|
1185
|
+
const title = firstLine.slice(2).trim();
|
|
1186
|
+
const description = reflowProse(newline === -1 ? "" : raw.slice(newline + 1));
|
|
1187
|
+
return {
|
|
1188
|
+
...title && { title },
|
|
1189
|
+
...description && { description }
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
const description = reflowProse(raw);
|
|
1193
|
+
return description ? { description } : {};
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
//#endregion
|
|
1197
|
+
//#region src/frontend/argtype/parser.ts
|
|
1198
|
+
/** Split a `///` block and attach its title/description to a node. Per the spec,
|
|
1199
|
+
* the block "is applied first and wins": for each field the block provides it
|
|
1200
|
+
* overrides a value a chained `.title()`/`.description()` already set, while a
|
|
1201
|
+
* field the block leaves unset keeps whatever the chain supplied. */
|
|
1202
|
+
function attachDocText(target, raw) {
|
|
1203
|
+
const { title, description } = splitDocText(raw);
|
|
1204
|
+
if (title !== void 0) target.title = title;
|
|
1205
|
+
if (description !== void 0) target.description = description;
|
|
1206
|
+
}
|
|
1207
|
+
const COMBINATORS = new Set([
|
|
1208
|
+
"seq",
|
|
1209
|
+
"set",
|
|
1210
|
+
"opt",
|
|
1211
|
+
"rep",
|
|
1212
|
+
"alt",
|
|
1213
|
+
"any"
|
|
1214
|
+
]);
|
|
1215
|
+
const TERMINALS = new Set([
|
|
1216
|
+
"int",
|
|
1217
|
+
"float",
|
|
1218
|
+
"str",
|
|
1219
|
+
"path"
|
|
1220
|
+
]);
|
|
1221
|
+
/** Core + supported-extension chaining methods. Anything else is an extension
|
|
1222
|
+
* we don't implement and is parsed-and-ignored (the spec's "ignorable" rule). */
|
|
1223
|
+
const KNOWN_METHODS = new Set([
|
|
1224
|
+
"name",
|
|
1225
|
+
"title",
|
|
1226
|
+
"description",
|
|
1227
|
+
"default",
|
|
1228
|
+
"min",
|
|
1229
|
+
"max",
|
|
1230
|
+
"join",
|
|
1231
|
+
"count",
|
|
1232
|
+
"countMin",
|
|
1233
|
+
"countMax",
|
|
1234
|
+
"mediaType",
|
|
1235
|
+
"mutable",
|
|
1236
|
+
"resolveParent"
|
|
1237
|
+
]);
|
|
1238
|
+
var Parser = class {
|
|
1239
|
+
tokens;
|
|
1240
|
+
pos = 0;
|
|
1241
|
+
errors = [];
|
|
1242
|
+
warnings = [];
|
|
1243
|
+
constructor(tokens) {
|
|
1244
|
+
this.tokens = tokens;
|
|
1245
|
+
}
|
|
1246
|
+
peek(o = 0) {
|
|
1247
|
+
return this.tokens[Math.min(this.pos + o, this.tokens.length - 1)];
|
|
1248
|
+
}
|
|
1249
|
+
at(kind) {
|
|
1250
|
+
return this.peek().kind === kind;
|
|
1251
|
+
}
|
|
1252
|
+
next() {
|
|
1253
|
+
return this.tokens[this.pos++] ?? this.tokens[this.tokens.length - 1];
|
|
1254
|
+
}
|
|
1255
|
+
expect(kind, what) {
|
|
1256
|
+
if (this.at(kind)) return this.next();
|
|
1257
|
+
const tok = this.peek();
|
|
1258
|
+
this.error(`Expected ${what} but found '${tok.value || tok.kind}'`, tok);
|
|
1259
|
+
return tok;
|
|
1260
|
+
}
|
|
1261
|
+
error(message, tok = this.peek()) {
|
|
1262
|
+
this.errors.push({
|
|
1263
|
+
message,
|
|
1264
|
+
line: tok.line,
|
|
1265
|
+
column: tok.column
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
warn(message, tok = this.peek()) {
|
|
1269
|
+
this.warnings.push({
|
|
1270
|
+
message,
|
|
1271
|
+
line: tok.line,
|
|
1272
|
+
column: tok.column
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
parseDocument(frontmatter) {
|
|
1276
|
+
const aliases = [];
|
|
1277
|
+
let rootName;
|
|
1278
|
+
let root;
|
|
1279
|
+
while (!this.at("eof")) {
|
|
1280
|
+
const docs = this.collectDocs();
|
|
1281
|
+
if (this.at("eof")) break;
|
|
1282
|
+
const labelLike = this.at("ident") || this.at("string");
|
|
1283
|
+
const following = this.peek(1).kind;
|
|
1284
|
+
if (labelLike && following === "eq") {
|
|
1285
|
+
const nameTok = this.next();
|
|
1286
|
+
if (nameTok.kind === "string") this.error("Alias names must be identifiers, not quoted strings", nameTok);
|
|
1287
|
+
this.next();
|
|
1288
|
+
const expr = this.parseElement();
|
|
1289
|
+
if (docs) attachDocText(expr, docs);
|
|
1290
|
+
aliases.push({
|
|
1291
|
+
name: nameTok.value,
|
|
1292
|
+
expr
|
|
1293
|
+
});
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
if (labelLike && following === "colon") {
|
|
1297
|
+
const nameTok = this.next();
|
|
1298
|
+
this.next();
|
|
1299
|
+
const expr = this.parseElement();
|
|
1300
|
+
if (docs) attachDocText(expr, docs);
|
|
1301
|
+
if (root) this.error(`Multiple root definitions; '${nameTok.value}' ignored (already have '${rootName ?? "<anonymous>"}')`, nameTok);
|
|
1302
|
+
else {
|
|
1303
|
+
rootName = nameTok.value;
|
|
1304
|
+
root = expr;
|
|
1305
|
+
if (!root.name) root.name = nameTok.value;
|
|
1306
|
+
}
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
if (!this.at("ident") && !this.at("string") && !this.at("lparen")) {
|
|
1310
|
+
this.error("Expected a definition (name: expr), an alias (Name = expr), or a root expression");
|
|
1311
|
+
break;
|
|
1312
|
+
}
|
|
1313
|
+
const expr = this.parseElement();
|
|
1314
|
+
if (docs) attachDocText(expr, docs);
|
|
1315
|
+
if (root) {
|
|
1316
|
+
this.error("Multiple root definitions; a second top-level expression is not allowed");
|
|
1317
|
+
break;
|
|
1318
|
+
}
|
|
1319
|
+
root = expr;
|
|
1320
|
+
}
|
|
1321
|
+
if (!root) {
|
|
1322
|
+
this.error("No root definition (expected `name: expr` or a bare root expression)");
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
return {
|
|
1326
|
+
...frontmatter && { frontmatter },
|
|
1327
|
+
aliases,
|
|
1328
|
+
...rootName !== void 0 && { rootName },
|
|
1329
|
+
root
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
/** Collect consecutive `///` doc lines, joined with newlines. */
|
|
1333
|
+
collectDocs() {
|
|
1334
|
+
const lines = [];
|
|
1335
|
+
while (this.at("doc")) lines.push(this.next().value);
|
|
1336
|
+
return lines.length > 0 ? lines.join("\n") : void 0;
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* An element is the unit inside a comma list: an optionally-named expression.
|
|
1340
|
+
* `label:` is looser than `|`, so a name binds the whole alternative that
|
|
1341
|
+
* follows it.
|
|
1342
|
+
*/
|
|
1343
|
+
parseElement() {
|
|
1344
|
+
if ((this.at("ident") || this.at("string")) && this.peek(1).kind === "colon") {
|
|
1345
|
+
const nameTok = this.next();
|
|
1346
|
+
this.next();
|
|
1347
|
+
const inner = this.parseElement();
|
|
1348
|
+
inner.name = nameTok.value;
|
|
1349
|
+
return inner;
|
|
1350
|
+
}
|
|
1351
|
+
return this.parseAlt();
|
|
1352
|
+
}
|
|
1353
|
+
parseAlt() {
|
|
1354
|
+
const first = this.parseChain();
|
|
1355
|
+
if (!this.at("pipe")) return first;
|
|
1356
|
+
const alts = [first];
|
|
1357
|
+
while (this.at("pipe")) {
|
|
1358
|
+
this.next();
|
|
1359
|
+
alts.push(this.parseChain());
|
|
1360
|
+
}
|
|
1361
|
+
return {
|
|
1362
|
+
kind: "comb",
|
|
1363
|
+
op: "alt",
|
|
1364
|
+
children: alts
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
parseChain() {
|
|
1368
|
+
const node = this.parsePrimary();
|
|
1369
|
+
let chained = false;
|
|
1370
|
+
while (this.at("dot")) {
|
|
1371
|
+
this.next();
|
|
1372
|
+
this.applyMethod(node);
|
|
1373
|
+
chained = true;
|
|
1374
|
+
}
|
|
1375
|
+
if (this.at("eq")) {
|
|
1376
|
+
const eqTok = this.next();
|
|
1377
|
+
const value = this.parseValue();
|
|
1378
|
+
if (node.kind === "terminal" && !chained) node.default = value;
|
|
1379
|
+
else this.error("`= value` default is only allowed on a bare terminal; use `.default(...)` after a method chain", eqTok);
|
|
1380
|
+
}
|
|
1381
|
+
return node;
|
|
1382
|
+
}
|
|
1383
|
+
parsePrimary() {
|
|
1384
|
+
const tok = this.peek();
|
|
1385
|
+
if (tok.kind === "string") {
|
|
1386
|
+
this.next();
|
|
1387
|
+
return {
|
|
1388
|
+
kind: "literal",
|
|
1389
|
+
value: tok.value
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
if (tok.kind === "lparen") return {
|
|
1393
|
+
kind: "comb",
|
|
1394
|
+
op: "seq",
|
|
1395
|
+
children: this.parseParenList()
|
|
1396
|
+
};
|
|
1397
|
+
if (tok.kind === "ident") {
|
|
1398
|
+
if (COMBINATORS.has(tok.value) && this.peek(1).kind === "lparen") {
|
|
1399
|
+
this.next();
|
|
1400
|
+
const children = this.parseParenList();
|
|
1401
|
+
return {
|
|
1402
|
+
kind: "comb",
|
|
1403
|
+
op: tok.value,
|
|
1404
|
+
children
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
if (TERMINALS.has(tok.value)) {
|
|
1408
|
+
this.next();
|
|
1409
|
+
return {
|
|
1410
|
+
kind: "terminal",
|
|
1411
|
+
terminal: tok.value
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
this.next();
|
|
1415
|
+
return {
|
|
1416
|
+
kind: "ref",
|
|
1417
|
+
refName: tok.value
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
this.error(`Unexpected token '${tok.value || tok.kind}' in expression`, tok);
|
|
1421
|
+
this.next();
|
|
1422
|
+
return {
|
|
1423
|
+
kind: "literal",
|
|
1424
|
+
value: ""
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
/** Parse `( elem, elem, ... )` with optional leading docs and trailing comma. */
|
|
1428
|
+
parseParenList() {
|
|
1429
|
+
this.expect("lparen", "'('");
|
|
1430
|
+
const items = [];
|
|
1431
|
+
while (!this.at("rparen") && !this.at("eof")) {
|
|
1432
|
+
const docs = this.collectDocs();
|
|
1433
|
+
if (this.at("rparen")) break;
|
|
1434
|
+
const elem = this.parseElement();
|
|
1435
|
+
if (docs) attachDocText(elem, docs);
|
|
1436
|
+
items.push(elem);
|
|
1437
|
+
if (this.at("comma")) this.next();
|
|
1438
|
+
else break;
|
|
1439
|
+
}
|
|
1440
|
+
this.expect("rparen", "')'");
|
|
1441
|
+
return items;
|
|
1442
|
+
}
|
|
1443
|
+
applyMethod(node) {
|
|
1444
|
+
const nameTok = this.expect("ident", "a method name");
|
|
1445
|
+
const method = nameTok.value;
|
|
1446
|
+
if (method === "output") {
|
|
1447
|
+
node.outputs = [...node.outputs ?? [], ...this.parseOutputArgs()];
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
if (!KNOWN_METHODS.has(method)) {
|
|
1451
|
+
this.skipBalancedArgs();
|
|
1452
|
+
this.warn(`Ignoring unsupported method '.${method}()'`, nameTok);
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
const args = this.parseScalarArgs();
|
|
1456
|
+
switch (method) {
|
|
1457
|
+
case "name":
|
|
1458
|
+
if (typeof args[0] === "string") node.name = args[0];
|
|
1459
|
+
break;
|
|
1460
|
+
case "title":
|
|
1461
|
+
if (typeof args[0] === "string") node.title = args[0];
|
|
1462
|
+
break;
|
|
1463
|
+
case "description":
|
|
1464
|
+
if (typeof args[0] === "string") node.description = args[0];
|
|
1465
|
+
break;
|
|
1466
|
+
case "default":
|
|
1467
|
+
if (args[0] !== void 0) node.default = args[0];
|
|
1468
|
+
break;
|
|
1469
|
+
case "min":
|
|
1470
|
+
if (typeof args[0] === "number") node.min = args[0];
|
|
1471
|
+
break;
|
|
1472
|
+
case "max":
|
|
1473
|
+
if (typeof args[0] === "number") node.max = args[0];
|
|
1474
|
+
break;
|
|
1475
|
+
case "join":
|
|
1476
|
+
node.join = typeof args[0] === "string" ? args[0] : "";
|
|
1477
|
+
break;
|
|
1478
|
+
case "count":
|
|
1479
|
+
if (typeof args[0] === "number") {
|
|
1480
|
+
node.countMin = args[0];
|
|
1481
|
+
node.countMax = args[0];
|
|
1482
|
+
}
|
|
1483
|
+
break;
|
|
1484
|
+
case "countMin":
|
|
1485
|
+
if (typeof args[0] === "number") node.countMin = args[0];
|
|
1486
|
+
break;
|
|
1487
|
+
case "countMax":
|
|
1488
|
+
if (typeof args[0] === "number") node.countMax = args[0];
|
|
1489
|
+
break;
|
|
1490
|
+
case "mediaType":
|
|
1491
|
+
if (typeof args[0] === "string") (node.mediaTypes ??= []).push(args[0]);
|
|
1492
|
+
break;
|
|
1493
|
+
case "mutable":
|
|
1494
|
+
node.mutable = true;
|
|
1495
|
+
break;
|
|
1496
|
+
case "resolveParent":
|
|
1497
|
+
node.resolveParent = true;
|
|
1498
|
+
break;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
/** Parse a parenthesized list of plain scalar arguments (numbers / strings). */
|
|
1502
|
+
parseScalarArgs() {
|
|
1503
|
+
this.expect("lparen", "'('");
|
|
1504
|
+
const args = [];
|
|
1505
|
+
while (!this.at("rparen") && !this.at("eof")) {
|
|
1506
|
+
args.push(this.parseValue());
|
|
1507
|
+
if (this.at("comma")) this.next();
|
|
1508
|
+
else break;
|
|
1509
|
+
}
|
|
1510
|
+
this.expect("rparen", "')'");
|
|
1511
|
+
return args;
|
|
1512
|
+
}
|
|
1513
|
+
/** Consume a balanced `( ... )` group without interpreting its contents.
|
|
1514
|
+
* Used to skip the arguments of an unsupported extension method. */
|
|
1515
|
+
skipBalancedArgs() {
|
|
1516
|
+
if (!this.at("lparen")) return;
|
|
1517
|
+
let depth = 0;
|
|
1518
|
+
do {
|
|
1519
|
+
const tok = this.next();
|
|
1520
|
+
if (tok.kind === "lparen") depth++;
|
|
1521
|
+
else if (tok.kind === "rparen") depth--;
|
|
1522
|
+
else if (tok.kind === "eof") break;
|
|
1523
|
+
} while (depth > 0);
|
|
1524
|
+
}
|
|
1525
|
+
/** Parse the argument list of `.output(...)`: one or more template expressions. */
|
|
1526
|
+
parseOutputArgs() {
|
|
1527
|
+
this.expect("lparen", "'('");
|
|
1528
|
+
const outputs = [];
|
|
1529
|
+
while (!this.at("rparen") && !this.at("eof")) {
|
|
1530
|
+
const docs = this.collectDocs();
|
|
1531
|
+
if (this.at("rparen")) break;
|
|
1532
|
+
const out = this.parseOutputTemplate();
|
|
1533
|
+
if (docs) attachDocText(out, docs);
|
|
1534
|
+
outputs.push(out);
|
|
1535
|
+
if (this.at("comma")) this.next();
|
|
1536
|
+
else break;
|
|
1537
|
+
}
|
|
1538
|
+
this.expect("rparen", "')'");
|
|
1539
|
+
return outputs;
|
|
1540
|
+
}
|
|
1541
|
+
/** `label: \`tpl\`` or `\`tpl\`` followed by optional `.name(...)` / `.or(...)`. */
|
|
1542
|
+
parseOutputTemplate() {
|
|
1543
|
+
let name;
|
|
1544
|
+
if ((this.at("ident") || this.at("string")) && this.peek(1).kind === "colon") {
|
|
1545
|
+
name = this.next().value;
|
|
1546
|
+
this.next();
|
|
1547
|
+
}
|
|
1548
|
+
const out = { tokens: [] };
|
|
1549
|
+
if (this.at("template")) {
|
|
1550
|
+
const tmplTok = this.next();
|
|
1551
|
+
const { tokens, errors } = parseTemplate(tmplTok.value);
|
|
1552
|
+
out.tokens = tokens;
|
|
1553
|
+
for (const e of errors) this.error(e, tmplTok);
|
|
1554
|
+
} else this.error(`Expected an output template literal`);
|
|
1555
|
+
const CHAIN = new Set([
|
|
1556
|
+
"name",
|
|
1557
|
+
"or",
|
|
1558
|
+
"title",
|
|
1559
|
+
"description"
|
|
1560
|
+
]);
|
|
1561
|
+
while (this.at("dot")) {
|
|
1562
|
+
this.next();
|
|
1563
|
+
const m = this.expect("ident", "a template method");
|
|
1564
|
+
if (CHAIN.has(m.value)) {
|
|
1565
|
+
const arg = this.parseScalarArgs()[0];
|
|
1566
|
+
if (typeof arg === "string") if (m.value === "name") name = arg;
|
|
1567
|
+
else if (m.value === "or") out.fallback = arg;
|
|
1568
|
+
else if (m.value === "title") out.title = arg;
|
|
1569
|
+
else out.description = arg;
|
|
1570
|
+
} else {
|
|
1571
|
+
this.skipBalancedArgs();
|
|
1572
|
+
this.warn(`Ignoring unsupported output-template method '.${m.value}()'`, m);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
if (name !== void 0) out.name = name;
|
|
1576
|
+
return out;
|
|
1577
|
+
}
|
|
1578
|
+
parseValue() {
|
|
1579
|
+
const tok = this.peek();
|
|
1580
|
+
if (tok.kind === "string") {
|
|
1581
|
+
this.next();
|
|
1582
|
+
return tok.value;
|
|
1583
|
+
}
|
|
1584
|
+
if (tok.kind === "number") {
|
|
1585
|
+
this.next();
|
|
1586
|
+
return Number(tok.value);
|
|
1587
|
+
}
|
|
1588
|
+
this.error(`Expected a value (string or number) but found '${tok.value || tok.kind}'`, tok);
|
|
1589
|
+
this.next();
|
|
1590
|
+
return "";
|
|
1591
|
+
}
|
|
1592
|
+
};
|
|
1593
|
+
function parseArgtype(source) {
|
|
1594
|
+
const { frontmatter, body, errors: fmErrors } = splitFrontmatter(source);
|
|
1595
|
+
const { tokens, errors: lexErrors } = lex(body);
|
|
1596
|
+
const parser = new Parser(tokens);
|
|
1597
|
+
const doc = parser.parseDocument(frontmatter);
|
|
1598
|
+
const errors = [
|
|
1599
|
+
...fmErrors.map((m) => ({ message: m })),
|
|
1600
|
+
...lexErrors.map((e) => ({
|
|
1601
|
+
message: e.message,
|
|
1602
|
+
line: e.line,
|
|
1603
|
+
column: e.column
|
|
1604
|
+
})),
|
|
1605
|
+
...parser.errors
|
|
1606
|
+
];
|
|
1607
|
+
return {
|
|
1608
|
+
...doc && { doc },
|
|
1609
|
+
errors,
|
|
1610
|
+
warnings: parser.warnings
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
//#endregion
|
|
1615
|
+
//#region src/ir/builders.ts
|
|
1616
|
+
function lit(str) {
|
|
1617
|
+
return {
|
|
1618
|
+
kind: "literal",
|
|
1619
|
+
attrs: { str }
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
function str(meta) {
|
|
1623
|
+
return {
|
|
1624
|
+
kind: "str",
|
|
1625
|
+
attrs: {},
|
|
1626
|
+
meta: normalizeMeta(meta)
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
function int(meta) {
|
|
1630
|
+
return {
|
|
1631
|
+
kind: "int",
|
|
1632
|
+
attrs: {},
|
|
1633
|
+
meta: normalizeMeta(meta)
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
function float(meta) {
|
|
1637
|
+
return {
|
|
1638
|
+
kind: "float",
|
|
1639
|
+
attrs: {},
|
|
1640
|
+
meta: normalizeMeta(meta)
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
function path(meta) {
|
|
1644
|
+
return {
|
|
1645
|
+
kind: "path",
|
|
1646
|
+
attrs: {},
|
|
1647
|
+
meta: normalizeMeta(meta)
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
function seq(...nodes) {
|
|
1651
|
+
return {
|
|
1652
|
+
kind: "sequence",
|
|
1653
|
+
attrs: { nodes }
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
function seqJoin(join, ...nodes) {
|
|
1657
|
+
return {
|
|
1658
|
+
kind: "sequence",
|
|
1659
|
+
attrs: {
|
|
1660
|
+
nodes,
|
|
1661
|
+
join
|
|
1662
|
+
}
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
function opt(node, meta) {
|
|
1666
|
+
return {
|
|
1667
|
+
kind: "optional",
|
|
1668
|
+
attrs: { node },
|
|
1669
|
+
meta: normalizeMeta(meta)
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
function rep(node, meta) {
|
|
1673
|
+
return {
|
|
1674
|
+
kind: "repeat",
|
|
1675
|
+
attrs: { node },
|
|
1676
|
+
meta: normalizeMeta(meta)
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
function repJoin(join, node, meta) {
|
|
1680
|
+
return {
|
|
1681
|
+
kind: "repeat",
|
|
1682
|
+
attrs: {
|
|
1683
|
+
node,
|
|
1684
|
+
join
|
|
1685
|
+
},
|
|
1686
|
+
meta: normalizeMeta(meta)
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
function alt(...alts) {
|
|
1690
|
+
return {
|
|
1691
|
+
kind: "alternative",
|
|
1692
|
+
attrs: { alts }
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
function normalizeMeta(meta) {
|
|
1696
|
+
if (meta === void 0) return void 0;
|
|
1697
|
+
if (typeof meta === "string") return { name: meta };
|
|
1698
|
+
return meta;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
735
1701
|
//#endregion
|
|
736
1702
|
//#region src/ir/meta.ts
|
|
737
1703
|
/** Construct a NodeRef from a node name. */
|
|
@@ -751,6 +1717,473 @@ function effectiveOutputName(output, index) {
|
|
|
751
1717
|
return output.name ?? `output_${index}`;
|
|
752
1718
|
}
|
|
753
1719
|
|
|
1720
|
+
//#endregion
|
|
1721
|
+
//#region src/frontend/argtype/lower.ts
|
|
1722
|
+
/**
|
|
1723
|
+
* Lower an `AstDocument` to Styx IR (`Expr` + `AppMeta`).
|
|
1724
|
+
*
|
|
1725
|
+
* Mapping highlights:
|
|
1726
|
+
* - Combinators map 1:1 except `set` -> `sequence` (order-not-meaningful is not
|
|
1727
|
+
* modeled in the IR) and `any` -> its first branch (the spec's "emit branch 0"
|
|
1728
|
+
* rule; the interchangeable alternatives are dropped with a warning).
|
|
1729
|
+
* - Aliases are resolved by substitution with cycle detection.
|
|
1730
|
+
* - `.output(...)` declarations attach to the nearest enclosing sequence scope,
|
|
1731
|
+
* so an output nested in a repeat / subcommand keeps its list / struct shape
|
|
1732
|
+
* (per-output gating is recovered downstream from each ref binding's gate).
|
|
1733
|
+
*/
|
|
1734
|
+
/** Build IR `Documentation` from an AST node's already-split title/description. */
|
|
1735
|
+
function docFrom(node) {
|
|
1736
|
+
const doc = {
|
|
1737
|
+
...node.title && { title: node.title },
|
|
1738
|
+
...node.description && { description: node.description }
|
|
1739
|
+
};
|
|
1740
|
+
return Object.keys(doc).length > 0 ? doc : void 0;
|
|
1741
|
+
}
|
|
1742
|
+
var Lowerer = class {
|
|
1743
|
+
errors = [];
|
|
1744
|
+
warnings = [];
|
|
1745
|
+
/** Extensions whose annotations the document actually uses, collected during
|
|
1746
|
+
* lowering so `lowerDocument` can flag any used-but-undeclared in frontmatter. */
|
|
1747
|
+
usedExtensions = /* @__PURE__ */ new Set();
|
|
1748
|
+
aliases = /* @__PURE__ */ new Map();
|
|
1749
|
+
constructor(aliases) {
|
|
1750
|
+
for (const a of aliases) {
|
|
1751
|
+
if (this.aliases.has(a.name)) this.warnings.push(`Duplicate alias '${a.name}'; last definition wins`);
|
|
1752
|
+
this.aliases.set(a.name, a.expr);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
lower(doc) {
|
|
1756
|
+
const rootSink = [];
|
|
1757
|
+
const expr = this.lowerNode(doc.root, /* @__PURE__ */ new Set(), void 0, rootSink);
|
|
1758
|
+
const root = expr.kind === "sequence" ? expr : seq(expr);
|
|
1759
|
+
if (root !== expr) root.meta = { ...doc.root.name && { name: doc.root.name } };
|
|
1760
|
+
if (rootSink.length > 0) root.meta = {
|
|
1761
|
+
...root.meta,
|
|
1762
|
+
outputs: [...root.meta?.outputs ?? [], ...rootSink]
|
|
1763
|
+
};
|
|
1764
|
+
return {
|
|
1765
|
+
expr: root,
|
|
1766
|
+
errors: this.errors,
|
|
1767
|
+
warnings: this.warnings
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* @param expanding - alias names currently being expanded (cycle guard).
|
|
1772
|
+
* @param selfName - nearest enclosing named node, for `{}` self-references.
|
|
1773
|
+
* @param sink - outputs array of the nearest enclosing sequence; a `.output()`
|
|
1774
|
+
* on this node attaches here (seq/set nodes instead own their outputs).
|
|
1775
|
+
*/
|
|
1776
|
+
lowerNode(node, expanding, selfName, sink) {
|
|
1777
|
+
if (node.kind === "ref") return this.lowerRef(node, expanding, selfName, sink);
|
|
1778
|
+
const name = node.name ?? selfName;
|
|
1779
|
+
const joinable = node.kind === "comb" && (node.op === "seq" || node.op === "set" || node.op === "rep" || node.op === "opt");
|
|
1780
|
+
if (node.join !== void 0 && !joinable) this.warnings.push("`.join()` is only supported on seq/set/rep/opt; ignored here");
|
|
1781
|
+
const isNumericTerminal = node.kind === "terminal" && (node.terminal === "int" || node.terminal === "float");
|
|
1782
|
+
const isRep = node.kind === "comb" && node.op === "rep";
|
|
1783
|
+
const isPath = node.kind === "terminal" && node.terminal === "path";
|
|
1784
|
+
if ((node.min !== void 0 || node.max !== void 0) && !isNumericTerminal) this.warnings.push("`.min()`/`.max()` is only supported on int/float terminals; ignored here");
|
|
1785
|
+
if ((node.countMin !== void 0 || node.countMax !== void 0) && !isRep) this.warnings.push("`.count()`/`.countMin()`/`.countMax()` is only supported on rep; ignored here");
|
|
1786
|
+
if ((node.mutable || node.resolveParent) && !isPath) this.warnings.push("`.mutable()`/`.resolveParent()` is only supported on path; ignored here");
|
|
1787
|
+
if (node.mediaTypes?.length && !isPath) this.warnings.push("`.mediaType()` is only supported on path; ignored here");
|
|
1788
|
+
if (node.outputs?.length) this.usedExtensions.add("outputs");
|
|
1789
|
+
if (node.mediaTypes?.length && isPath) this.usedExtensions.add("mediatypes");
|
|
1790
|
+
if ((node.mutable || node.resolveParent) && isPath) this.usedExtensions.add("paths");
|
|
1791
|
+
const isSeqSet = node.kind === "comb" && (node.op === "seq" || node.op === "set");
|
|
1792
|
+
if (node.outputs?.length && !isSeqSet) for (const o of node.outputs) sink.push(this.toOutput(o, name));
|
|
1793
|
+
switch (node.kind) {
|
|
1794
|
+
case "literal": {
|
|
1795
|
+
const e = lit(node.value ?? "");
|
|
1796
|
+
this.applyMeta(e, node);
|
|
1797
|
+
return e;
|
|
1798
|
+
}
|
|
1799
|
+
case "terminal": return this.lowerTerminal(node);
|
|
1800
|
+
case "comb": return this.lowerComb(node, expanding, name, sink);
|
|
1801
|
+
default:
|
|
1802
|
+
this.errors.push(`Unknown AST node kind '${node.kind}'`);
|
|
1803
|
+
return seq();
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
lowerRef(node, expanding, selfName, sink) {
|
|
1807
|
+
const target = node.refName;
|
|
1808
|
+
const aliasExpr = this.aliases.get(target);
|
|
1809
|
+
if (!aliasExpr) {
|
|
1810
|
+
this.errors.push(`Unknown alias '${target}'`);
|
|
1811
|
+
return seq();
|
|
1812
|
+
}
|
|
1813
|
+
if (expanding.has(target)) {
|
|
1814
|
+
this.errors.push(`Recursive alias '${target}' is not allowed`);
|
|
1815
|
+
return seq();
|
|
1816
|
+
}
|
|
1817
|
+
const clone = structuredClone(aliasExpr);
|
|
1818
|
+
clone.name = node.name ?? clone.name;
|
|
1819
|
+
clone.title = node.title ?? clone.title;
|
|
1820
|
+
clone.description = node.description ?? clone.description;
|
|
1821
|
+
if (node.default !== void 0) clone.default = node.default;
|
|
1822
|
+
if (node.min !== void 0) clone.min = node.min;
|
|
1823
|
+
if (node.max !== void 0) clone.max = node.max;
|
|
1824
|
+
if (node.join !== void 0) clone.join = node.join;
|
|
1825
|
+
if (node.countMin !== void 0) clone.countMin = node.countMin;
|
|
1826
|
+
if (node.countMax !== void 0) clone.countMax = node.countMax;
|
|
1827
|
+
if (node.mediaTypes?.length) clone.mediaTypes = [...clone.mediaTypes ?? [], ...node.mediaTypes];
|
|
1828
|
+
if (node.mutable) clone.mutable = true;
|
|
1829
|
+
if (node.resolveParent) clone.resolveParent = true;
|
|
1830
|
+
if (node.outputs?.length) clone.outputs = [...clone.outputs ?? [], ...node.outputs];
|
|
1831
|
+
const next = new Set(expanding);
|
|
1832
|
+
next.add(target);
|
|
1833
|
+
return this.lowerNode(clone, next, selfName, sink);
|
|
1834
|
+
}
|
|
1835
|
+
lowerTerminal(node) {
|
|
1836
|
+
switch (node.terminal) {
|
|
1837
|
+
case "int": {
|
|
1838
|
+
const e = int();
|
|
1839
|
+
this.checkBounds(node.min, node.max, "value", "min", "max");
|
|
1840
|
+
if (node.min !== void 0) e.attrs.minValue = node.min;
|
|
1841
|
+
if (node.max !== void 0) e.attrs.maxValue = node.max;
|
|
1842
|
+
this.applyMeta(e, node);
|
|
1843
|
+
return e;
|
|
1844
|
+
}
|
|
1845
|
+
case "float": {
|
|
1846
|
+
const e = float();
|
|
1847
|
+
this.checkBounds(node.min, node.max, "value", "min", "max");
|
|
1848
|
+
if (node.min !== void 0) e.attrs.minValue = node.min;
|
|
1849
|
+
if (node.max !== void 0) e.attrs.maxValue = node.max;
|
|
1850
|
+
this.applyMeta(e, node);
|
|
1851
|
+
return e;
|
|
1852
|
+
}
|
|
1853
|
+
case "str": {
|
|
1854
|
+
const e = str();
|
|
1855
|
+
this.applyMeta(e, node);
|
|
1856
|
+
return e;
|
|
1857
|
+
}
|
|
1858
|
+
case "path": {
|
|
1859
|
+
const e = path();
|
|
1860
|
+
if (node.mediaTypes?.length) e.attrs.mediaTypes = node.mediaTypes;
|
|
1861
|
+
if (node.mutable) e.attrs.mutable = true;
|
|
1862
|
+
if (node.resolveParent) e.attrs.resolveParent = true;
|
|
1863
|
+
this.applyMeta(e, node);
|
|
1864
|
+
return e;
|
|
1865
|
+
}
|
|
1866
|
+
default:
|
|
1867
|
+
this.errors.push(`Unknown terminal '${String(node.terminal)}'`);
|
|
1868
|
+
return str();
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
lowerComb(node, expanding, name, sink) {
|
|
1872
|
+
const children = node.children ?? [];
|
|
1873
|
+
const lowerChildren = (s) => children.map((c) => this.lowerNode(c, expanding, name, s));
|
|
1874
|
+
switch (node.op) {
|
|
1875
|
+
case "seq":
|
|
1876
|
+
case "set": {
|
|
1877
|
+
const selfOutputs = [];
|
|
1878
|
+
for (const o of node.outputs ?? []) selfOutputs.push(this.toOutput(o, name));
|
|
1879
|
+
const lowered = lowerChildren(selfOutputs);
|
|
1880
|
+
this.dedupeSiblingNames(lowered);
|
|
1881
|
+
const e = seq(...lowered);
|
|
1882
|
+
if (node.join !== void 0) e.attrs.join = node.join;
|
|
1883
|
+
this.applyMeta(e, node);
|
|
1884
|
+
if (selfOutputs.length > 0) e.meta = {
|
|
1885
|
+
...e.meta,
|
|
1886
|
+
outputs: [...e.meta?.outputs ?? [], ...selfOutputs]
|
|
1887
|
+
};
|
|
1888
|
+
return e;
|
|
1889
|
+
}
|
|
1890
|
+
case "alt": {
|
|
1891
|
+
const arms = lowerChildren(sink);
|
|
1892
|
+
this.dedupeSiblingNames(arms);
|
|
1893
|
+
const e = alt(...arms);
|
|
1894
|
+
this.applyMeta(e, node);
|
|
1895
|
+
return e;
|
|
1896
|
+
}
|
|
1897
|
+
case "any": {
|
|
1898
|
+
if (children.length === 0) {
|
|
1899
|
+
this.errors.push("`any(...)` requires at least one branch");
|
|
1900
|
+
return seq();
|
|
1901
|
+
}
|
|
1902
|
+
const e = this.lowerNode(children[0], expanding, name, sink);
|
|
1903
|
+
if (node.name) e.meta = {
|
|
1904
|
+
...e.meta,
|
|
1905
|
+
name: node.name
|
|
1906
|
+
};
|
|
1907
|
+
{
|
|
1908
|
+
const d = docFrom(node);
|
|
1909
|
+
if (d) e.meta = {
|
|
1910
|
+
...e.meta,
|
|
1911
|
+
doc: d
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
return e;
|
|
1915
|
+
}
|
|
1916
|
+
case "opt": {
|
|
1917
|
+
const inner = this.wrapChildren(children, expanding, name, sink);
|
|
1918
|
+
if (node.join !== void 0 && (inner.kind === "sequence" || inner.kind === "repeat")) inner.attrs.join = node.join;
|
|
1919
|
+
const e = opt(inner);
|
|
1920
|
+
this.applyMeta(e, node);
|
|
1921
|
+
if (inner.kind === "literal") e.meta = {
|
|
1922
|
+
...e.meta,
|
|
1923
|
+
defaultValue: false
|
|
1924
|
+
};
|
|
1925
|
+
return e;
|
|
1926
|
+
}
|
|
1927
|
+
case "rep": {
|
|
1928
|
+
const e = rep(this.wrapChildren(children, expanding, name, sink));
|
|
1929
|
+
if (node.join !== void 0) e.attrs.join = node.join;
|
|
1930
|
+
this.checkBounds(node.countMin, node.countMax, "repetition count", "countMin", "countMax");
|
|
1931
|
+
if (node.countMin !== void 0) e.attrs.countMin = node.countMin;
|
|
1932
|
+
if (node.countMax !== void 0) e.attrs.countMax = node.countMax;
|
|
1933
|
+
this.applyMeta(e, node);
|
|
1934
|
+
return e;
|
|
1935
|
+
}
|
|
1936
|
+
default:
|
|
1937
|
+
this.errors.push(`Unknown combinator '${String(node.op)}'`);
|
|
1938
|
+
return seq();
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
/** Warn on an inverted `[min, max]` pair (a lower bound above its upper
|
|
1942
|
+
* bound), which yields an unsatisfiable constraint downstream. */
|
|
1943
|
+
checkBounds(min, max, what, minLabel, maxLabel) {
|
|
1944
|
+
if (min !== void 0 && max !== void 0 && min > max) this.warnings.push(`Inverted ${what} bounds: ${minLabel} (${min}) > ${maxLabel} (${max})`);
|
|
1945
|
+
}
|
|
1946
|
+
/**
|
|
1947
|
+
* Disambiguate duplicate explicit names among the direct children of a
|
|
1948
|
+
* sequence/set (struct fields) or the arms of an alternative (union
|
|
1949
|
+
* discriminants). The solver turns each named child into a struct field / union
|
|
1950
|
+
* variant keyed by its name; two siblings with the same name would silently
|
|
1951
|
+
* collapse (the second overwrites the first, dropping a parameter). argtype is
|
|
1952
|
+
* hand-authored
|
|
1953
|
+
* and gives no uniqueness guarantee, so - like the mrtrix frontend - we rename
|
|
1954
|
+
* collisions (`name_2`, `name_3`, ...) and warn. (This catches duplicate
|
|
1955
|
+
* explicit labels; it does not detect collisions between solver-derived names
|
|
1956
|
+
* of otherwise-unnamed children, which the solver also does not guard.)
|
|
1957
|
+
*/
|
|
1958
|
+
dedupeSiblingNames(children) {
|
|
1959
|
+
const used = /* @__PURE__ */ new Set();
|
|
1960
|
+
for (const child of children) {
|
|
1961
|
+
const nm = child.meta?.name;
|
|
1962
|
+
if (nm === void 0) continue;
|
|
1963
|
+
if (!used.has(nm)) {
|
|
1964
|
+
used.add(nm);
|
|
1965
|
+
continue;
|
|
1966
|
+
}
|
|
1967
|
+
let n = 2;
|
|
1968
|
+
while (used.has(`${nm}_${n}`)) n++;
|
|
1969
|
+
const renamed = `${nm}_${n}`;
|
|
1970
|
+
used.add(renamed);
|
|
1971
|
+
child.meta = {
|
|
1972
|
+
...child.meta,
|
|
1973
|
+
name: renamed
|
|
1974
|
+
};
|
|
1975
|
+
this.warnings.push(`Duplicate sibling name '${nm}'; renamed one occurrence to '${renamed}'`);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
/** `opt`/`rep` with multiple children implicitly wrap them in a sequence,
|
|
1979
|
+
* which - like any sequence - is the output scope for those children. */
|
|
1980
|
+
wrapChildren(children, expanding, name, sink) {
|
|
1981
|
+
if (children.length === 1) return this.lowerNode(children[0], expanding, name, sink);
|
|
1982
|
+
const selfOutputs = [];
|
|
1983
|
+
const e = seq(...children.map((c) => this.lowerNode(c, expanding, name, selfOutputs)));
|
|
1984
|
+
if (selfOutputs.length > 0) e.meta = {
|
|
1985
|
+
...e.meta,
|
|
1986
|
+
outputs: selfOutputs
|
|
1987
|
+
};
|
|
1988
|
+
return e;
|
|
1989
|
+
}
|
|
1990
|
+
/** Fold an AST node's name/doc/default decorations onto an IR node's meta. */
|
|
1991
|
+
applyMeta(e, node) {
|
|
1992
|
+
const meta = {};
|
|
1993
|
+
if (node.name) meta.name = node.name;
|
|
1994
|
+
const doc = docFrom(node);
|
|
1995
|
+
if (doc) meta.doc = doc;
|
|
1996
|
+
if (node.default !== void 0) meta.defaultValue = node.default;
|
|
1997
|
+
if (Object.keys(meta).length > 0) e.meta = {
|
|
1998
|
+
...e.meta,
|
|
1999
|
+
...meta
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
toOutput(o, selfName) {
|
|
2003
|
+
const tokens = [];
|
|
2004
|
+
for (const t of o.tokens) {
|
|
2005
|
+
if (t.kind === "literal") {
|
|
2006
|
+
tokens.push({
|
|
2007
|
+
kind: "literal",
|
|
2008
|
+
value: t.value
|
|
2009
|
+
});
|
|
2010
|
+
continue;
|
|
2011
|
+
}
|
|
2012
|
+
const targetName = t.name ?? selfName;
|
|
2013
|
+
if (!targetName) {
|
|
2014
|
+
this.warnings.push("Output self-reference '{}' has no named node to resolve to; emitting empty");
|
|
2015
|
+
tokens.push({
|
|
2016
|
+
kind: "literal",
|
|
2017
|
+
value: ""
|
|
2018
|
+
});
|
|
2019
|
+
continue;
|
|
2020
|
+
}
|
|
2021
|
+
if (t.stripPrefix?.length) this.warnings.push("Output ref op 'strip_prefix' is not supported by the IR; ignored");
|
|
2022
|
+
if (t.basename) this.warnings.push("Output ref op 'basename' is not supported by the IR; ignored");
|
|
2023
|
+
tokens.push({
|
|
2024
|
+
kind: "ref",
|
|
2025
|
+
target: nodeRef(targetName),
|
|
2026
|
+
...t.stripSuffix?.length && { stripExtensions: t.stripSuffix },
|
|
2027
|
+
...t.or !== void 0 && { fallback: t.or }
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
if (o.fallback !== void 0) this.warnings.push("Output-level '.or(...)' fallback is not supported by the IR; ignored");
|
|
2031
|
+
const doc = docFrom(o);
|
|
2032
|
+
return {
|
|
2033
|
+
...o.name && { name: o.name },
|
|
2034
|
+
...doc && { doc },
|
|
2035
|
+
tokens
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
};
|
|
2039
|
+
function buildAppMeta(fm, rootDoc, rootName) {
|
|
2040
|
+
const warnings = [];
|
|
2041
|
+
const rootTitleDesc = docFrom(rootDoc);
|
|
2042
|
+
if (!fm) {
|
|
2043
|
+
const meta = {
|
|
2044
|
+
...rootName && { id: rootName },
|
|
2045
|
+
...rootTitleDesc && { doc: rootTitleDesc }
|
|
2046
|
+
};
|
|
2047
|
+
return Object.keys(meta).length > 0 ? {
|
|
2048
|
+
meta,
|
|
2049
|
+
warnings
|
|
2050
|
+
} : { warnings };
|
|
2051
|
+
}
|
|
2052
|
+
const asStr = (x) => typeof x === "string" && x.length > 0 ? x : void 0;
|
|
2053
|
+
const id = asStr(rootName) ?? asStr(fm.exe) ?? asStr(fm.id);
|
|
2054
|
+
const version = asStr(fm.version) ?? (typeof fm.version === "number" ? String(fm.version) : void 0);
|
|
2055
|
+
const strList = (x) => Array.isArray(x) ? x.filter((a) => typeof a === "string") : [];
|
|
2056
|
+
const authors = strList(fm.authors);
|
|
2057
|
+
const urls = strList(fm.urls);
|
|
2058
|
+
const references = strList(fm.references);
|
|
2059
|
+
const doc = {
|
|
2060
|
+
...rootTitleDesc,
|
|
2061
|
+
...authors.length > 0 && { authors },
|
|
2062
|
+
...urls.length > 0 && { urls },
|
|
2063
|
+
...references.length > 0 && { literature: references }
|
|
2064
|
+
};
|
|
2065
|
+
const meta = {
|
|
2066
|
+
...id && { id },
|
|
2067
|
+
...version && { version },
|
|
2068
|
+
...Object.keys(doc).length > 0 && { doc }
|
|
2069
|
+
};
|
|
2070
|
+
if (isRecord$1(fm.container)) {
|
|
2071
|
+
const image = asStr(fm.container.image);
|
|
2072
|
+
const type = asStr(fm.container.type);
|
|
2073
|
+
if (image) meta.container = {
|
|
2074
|
+
image,
|
|
2075
|
+
...type === "docker" || type === "singularity" ? { type } : {}
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
meta.stdout = streamFrom(fm.stdout, warnings, "stdout");
|
|
2079
|
+
meta.stderr = streamFrom(fm.stderr, warnings, "stderr");
|
|
2080
|
+
if (!meta.stdout) delete meta.stdout;
|
|
2081
|
+
if (!meta.stderr) delete meta.stderr;
|
|
2082
|
+
return {
|
|
2083
|
+
meta,
|
|
2084
|
+
warnings
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
function streamFrom(x, warnings, key) {
|
|
2088
|
+
if (x === void 0 || x === null) return void 0;
|
|
2089
|
+
if (isRecord$1(x)) {
|
|
2090
|
+
const name = typeof x.name === "string" ? x.name : void 0;
|
|
2091
|
+
if (!name) {
|
|
2092
|
+
warnings.push(`Frontmatter '${key}' is missing a 'name'; ignored`);
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
const description = typeof x.description === "string" ? x.description : void 0;
|
|
2096
|
+
return {
|
|
2097
|
+
name,
|
|
2098
|
+
...description && { doc: { description } }
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
if (typeof x === "string") return { name: x };
|
|
2102
|
+
warnings.push(`Frontmatter '${key}' has an unexpected shape; ignored`);
|
|
2103
|
+
}
|
|
2104
|
+
function isRecord$1(x) {
|
|
2105
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
2106
|
+
}
|
|
2107
|
+
function lowerDocument(doc) {
|
|
2108
|
+
const lowerer = new Lowerer(doc.aliases);
|
|
2109
|
+
const result = lowerer.lower(doc);
|
|
2110
|
+
const exe = typeof doc.frontmatter?.exe === "string" ? doc.frontmatter.exe : void 0;
|
|
2111
|
+
if (exe) result.expr.attrs.nodes.unshift(lit(exe));
|
|
2112
|
+
const { meta, warnings } = buildAppMeta(doc.frontmatter, {
|
|
2113
|
+
title: doc.root.title,
|
|
2114
|
+
description: doc.root.description
|
|
2115
|
+
}, doc.rootName);
|
|
2116
|
+
const extWarnings = [];
|
|
2117
|
+
if (doc.frontmatter) {
|
|
2118
|
+
const declared = new Set(Array.isArray(doc.frontmatter.extensions) ? doc.frontmatter.extensions.filter((e) => typeof e === "string") : []);
|
|
2119
|
+
for (const used of lowerer.usedExtensions) if (!declared.has(used)) extWarnings.push(`Uses the '${used}' extension but does not declare it in frontmatter (add it to 'extensions:')`);
|
|
2120
|
+
}
|
|
2121
|
+
return {
|
|
2122
|
+
...meta && { meta },
|
|
2123
|
+
expr: result.expr,
|
|
2124
|
+
errors: result.errors,
|
|
2125
|
+
warnings: [
|
|
2126
|
+
...result.warnings,
|
|
2127
|
+
...warnings,
|
|
2128
|
+
...extWarnings
|
|
2129
|
+
]
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
//#endregion
|
|
2134
|
+
//#region src/frontend/argtype/parser-frontend.ts
|
|
2135
|
+
/** A fresh empty root sequence for error returns (IR passes mutate in place). */
|
|
2136
|
+
function emptyExpr$2() {
|
|
2137
|
+
return {
|
|
2138
|
+
kind: "sequence",
|
|
2139
|
+
attrs: { nodes: [] }
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
/**
|
|
2143
|
+
* Frontend for the argtype sugar DSL - the hand-authored, TypeScript-types-like
|
|
2144
|
+
* language for describing CLI argument grammars (see the argtype spec). Parses
|
|
2145
|
+
* the text into an AST, then lowers it to Styx IR + AppMeta.
|
|
2146
|
+
*
|
|
2147
|
+
* Supported today: the argtype core (combinators, terminals, literals, naming,
|
|
2148
|
+
* aliases, value constraints, `.join`/`.count`/`.default`, doc comments,
|
|
2149
|
+
* frontmatter) plus the `outputs`, `mediatypes`, and `paths` extensions. `set`
|
|
2150
|
+
* lowers to a sequence and `any` to its first branch; the `constraints`
|
|
2151
|
+
* extension is parsed-and-ignored.
|
|
2152
|
+
*/
|
|
2153
|
+
var ArgtypeParser = class {
|
|
2154
|
+
name = "argtype";
|
|
2155
|
+
extensions = ["argtype"];
|
|
2156
|
+
parse(source, _filename) {
|
|
2157
|
+
const { doc, errors: parseErrors, warnings: parseWarnings } = parseArgtype(source);
|
|
2158
|
+
const toLocation = (e) => e.line !== void 0 ? { location: {
|
|
2159
|
+
line: e.line,
|
|
2160
|
+
column: e.column
|
|
2161
|
+
} } : {};
|
|
2162
|
+
const errors = parseErrors.map((e) => ({
|
|
2163
|
+
message: e.message,
|
|
2164
|
+
...toLocation(e)
|
|
2165
|
+
}));
|
|
2166
|
+
const warnings = parseWarnings.map((e) => ({
|
|
2167
|
+
message: e.message,
|
|
2168
|
+
...toLocation(e)
|
|
2169
|
+
}));
|
|
2170
|
+
if (!doc) return {
|
|
2171
|
+
expr: emptyExpr$2(),
|
|
2172
|
+
errors,
|
|
2173
|
+
warnings
|
|
2174
|
+
};
|
|
2175
|
+
const lowered = lowerDocument(doc);
|
|
2176
|
+
warnings.push(...lowered.warnings.map((message) => ({ message })));
|
|
2177
|
+
errors.push(...lowered.errors.map((message) => ({ message })));
|
|
2178
|
+
return {
|
|
2179
|
+
...lowered.meta && { meta: lowered.meta },
|
|
2180
|
+
expr: lowered.expr,
|
|
2181
|
+
errors,
|
|
2182
|
+
warnings
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
};
|
|
2186
|
+
|
|
754
2187
|
//#endregion
|
|
755
2188
|
//#region src/frontend/boutiques/destruct-template.ts
|
|
756
2189
|
/**
|
|
@@ -1365,102 +2798,15 @@ var BoutiquesParser = class {
|
|
|
1365
2798
|
if (!expr.meta?.name && baseMeta?.id) expr.meta = {
|
|
1366
2799
|
...expr.meta,
|
|
1367
2800
|
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
|
|
1426
|
-
}
|
|
1427
|
-
};
|
|
1428
|
-
}
|
|
1429
|
-
function opt(node, meta) {
|
|
1430
|
-
return {
|
|
1431
|
-
kind: "optional",
|
|
1432
|
-
attrs: { node },
|
|
1433
|
-
meta: normalizeMeta(meta)
|
|
1434
|
-
};
|
|
1435
|
-
}
|
|
1436
|
-
function rep(node, meta) {
|
|
1437
|
-
return {
|
|
1438
|
-
kind: "repeat",
|
|
1439
|
-
attrs: { node },
|
|
1440
|
-
meta: normalizeMeta(meta)
|
|
1441
|
-
};
|
|
1442
|
-
}
|
|
1443
|
-
function repJoin(join, node, meta) {
|
|
1444
|
-
return {
|
|
1445
|
-
kind: "repeat",
|
|
1446
|
-
attrs: {
|
|
1447
|
-
node,
|
|
1448
|
-
join
|
|
1449
|
-
},
|
|
1450
|
-
meta: normalizeMeta(meta)
|
|
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
|
-
}
|
|
2801
|
+
};
|
|
2802
|
+
return {
|
|
2803
|
+
meta: baseMeta,
|
|
2804
|
+
expr,
|
|
2805
|
+
errors: this.errors,
|
|
2806
|
+
warnings: this.warnings
|
|
2807
|
+
};
|
|
2808
|
+
}
|
|
2809
|
+
};
|
|
1464
2810
|
|
|
1465
2811
|
//#endregion
|
|
1466
2812
|
//#region src/backend/string-case.ts
|
|
@@ -2211,11 +3557,33 @@ function extractVersion(versionInfo) {
|
|
|
2211
3557
|
|
|
2212
3558
|
//#endregion
|
|
2213
3559
|
//#region src/frontend/detect-format.ts
|
|
3560
|
+
/** The first non-whitespace character of a source ("" if it is all blank). */
|
|
3561
|
+
function firstNonBlankChar(source) {
|
|
3562
|
+
const match = source.match(/\S/);
|
|
3563
|
+
return match ? match[0] : "";
|
|
3564
|
+
}
|
|
2214
3565
|
/**
|
|
2215
|
-
* Auto-detect the format of a
|
|
2216
|
-
*
|
|
3566
|
+
* Auto-detect the format of a descriptor source string.
|
|
3567
|
+
*
|
|
3568
|
+
* Every JSON frontend (boutiques, argdump, workbench, mrtrix) is a top-level
|
|
3569
|
+
* object, so the first non-blank character being `{` is the signal for "some
|
|
3570
|
+
* JSON format"; we then inspect its keys to pick which one. Anything else
|
|
3571
|
+
* non-blank is treated as the argtype DSL, whose sources open with a terminal
|
|
3572
|
+
* (`int`), a literal (`"hello"`), a combinator (`seq(...)`), a `name: expr`
|
|
3573
|
+
* definition, or a `---` frontmatter fence - never with `{`.
|
|
3574
|
+
*
|
|
3575
|
+
* Deciding on the leading character (rather than trying `JSON.parse` first) is
|
|
3576
|
+
* what lets standalone argtype snippets like `"hello"` or `42` be recognized:
|
|
3577
|
+
* those are *also* valid JSON scalars, so a parse-first approach would swallow
|
|
3578
|
+
* them and then reject them as "not an object".
|
|
3579
|
+
*
|
|
3580
|
+
* Returns null only when the source is blank, or opens with `{` but matches no
|
|
3581
|
+
* known JSON format (ambiguous, or still being typed).
|
|
2217
3582
|
*/
|
|
2218
3583
|
function detectFormat(source) {
|
|
3584
|
+
const first = firstNonBlankChar(source);
|
|
3585
|
+
if (first === "") return null;
|
|
3586
|
+
if (first !== "{") return "argtype";
|
|
2219
3587
|
let parsed;
|
|
2220
3588
|
try {
|
|
2221
3589
|
parsed = JSON.parse(source);
|
|
@@ -2232,6 +3600,605 @@ function detectFormat(source) {
|
|
|
2232
3600
|
return null;
|
|
2233
3601
|
}
|
|
2234
3602
|
|
|
3603
|
+
//#endregion
|
|
3604
|
+
//#region src/backend/scope.ts
|
|
3605
|
+
/** Symbol collision avoidance for code generation. */
|
|
3606
|
+
var Scope = class Scope {
|
|
3607
|
+
reserved;
|
|
3608
|
+
used;
|
|
3609
|
+
parent;
|
|
3610
|
+
constructor(reserved = [], parent) {
|
|
3611
|
+
this.reserved = new Set(reserved);
|
|
3612
|
+
this.used = /* @__PURE__ */ new Set();
|
|
3613
|
+
this.parent = parent;
|
|
3614
|
+
}
|
|
3615
|
+
/** Check if a symbol is already taken (in this scope or any parent). */
|
|
3616
|
+
has(symbol) {
|
|
3617
|
+
return this.reserved.has(symbol) || this.used.has(symbol) || (this.parent?.has(symbol) ?? false);
|
|
3618
|
+
}
|
|
3619
|
+
/**
|
|
3620
|
+
* Add a symbol, appending a numeric suffix to avoid collisions. Returns the
|
|
3621
|
+
* safe name.
|
|
3622
|
+
*
|
|
3623
|
+
* When a `recase` transform is given, a disambiguated candidate is routed back
|
|
3624
|
+
* through it so the suffix is absorbed into the identifier's casing - e.g.
|
|
3625
|
+
* `pascalCase` folds `Config_2` into `Config2` - rather than leaving a
|
|
3626
|
+
* mixed-case `Config_2`. Uniqueness is always checked on the final emitted
|
|
3627
|
+
* form, so two hints that case-collide still get distinct names. Defaults to
|
|
3628
|
+
* identity (the bare `<name>_<n>` suffix) for callers that don't case-normalize.
|
|
3629
|
+
*/
|
|
3630
|
+
add(candidate, recase = (s) => s) {
|
|
3631
|
+
if (!this.has(candidate)) {
|
|
3632
|
+
this.used.add(candidate);
|
|
3633
|
+
return candidate;
|
|
3634
|
+
}
|
|
3635
|
+
let suffix = 2;
|
|
3636
|
+
let safe = recase(`${candidate}_${suffix}`);
|
|
3637
|
+
while (this.has(safe)) {
|
|
3638
|
+
suffix++;
|
|
3639
|
+
safe = recase(`${candidate}_${suffix}`);
|
|
3640
|
+
}
|
|
3641
|
+
this.used.add(safe);
|
|
3642
|
+
return safe;
|
|
3643
|
+
}
|
|
3644
|
+
/** Create a child scope that inherits this scope's restrictions. */
|
|
3645
|
+
child(reserved = []) {
|
|
3646
|
+
return new Scope(reserved, this);
|
|
3647
|
+
}
|
|
3648
|
+
};
|
|
3649
|
+
|
|
3650
|
+
//#endregion
|
|
3651
|
+
//#region src/backend/argtype/emit.ts
|
|
3652
|
+
const INDENT = " ";
|
|
3653
|
+
/** Target content width (excluding the `/// ` prefix and indentation) for a
|
|
3654
|
+
* wrapped `///` description line. */
|
|
3655
|
+
const DOC_WIDTH = 80;
|
|
3656
|
+
function pad(level) {
|
|
3657
|
+
return INDENT.repeat(level);
|
|
3658
|
+
}
|
|
3659
|
+
/**
|
|
3660
|
+
* Wrap a description into `///`-ready content lines. Paragraphs (separated by a
|
|
3661
|
+
* blank line) are word-wrapped to `width`; an empty string marks a paragraph
|
|
3662
|
+
* break (the caller emits it as a bare `///`).
|
|
3663
|
+
*
|
|
3664
|
+
* The argtype frontend reflows a `///` block as prose - a single line break
|
|
3665
|
+
* rejoins with a space, a blank line separates paragraphs - so wrapping here is
|
|
3666
|
+
* the inverse: it round-trips paragraph text exactly (whitespace within a
|
|
3667
|
+
* paragraph is normalized to single spaces, matching the frontend's reflow), and
|
|
3668
|
+
* keeps a long description from emitting as one giant physical line.
|
|
3669
|
+
*/
|
|
3670
|
+
function wrapDoc(text, width) {
|
|
3671
|
+
const lines = [];
|
|
3672
|
+
for (const paragraph of text.split(/\n{2,}/)) {
|
|
3673
|
+
const words = paragraph.split(/\s+/).filter((w) => w.length > 0);
|
|
3674
|
+
if (words.length === 0) continue;
|
|
3675
|
+
if (lines.length > 0) lines.push("");
|
|
3676
|
+
let cur = "";
|
|
3677
|
+
for (const word of words) if (cur === "") cur = word;
|
|
3678
|
+
else if (cur.length + 1 + word.length <= width) cur += ` ${word}`;
|
|
3679
|
+
else {
|
|
3680
|
+
lines.push(cur);
|
|
3681
|
+
cur = word;
|
|
3682
|
+
}
|
|
3683
|
+
if (cur !== "") lines.push(cur);
|
|
3684
|
+
}
|
|
3685
|
+
return lines;
|
|
3686
|
+
}
|
|
3687
|
+
function num(n) {
|
|
3688
|
+
return String(n);
|
|
3689
|
+
}
|
|
3690
|
+
/** Escape a string for a double-quoted argtype literal (matches the lexer). */
|
|
3691
|
+
function escapeString(s) {
|
|
3692
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(/\r/g, "\\r");
|
|
3693
|
+
}
|
|
3694
|
+
function quote(s) {
|
|
3695
|
+
return `"${escapeString(s)}"`;
|
|
3696
|
+
}
|
|
3697
|
+
/** A value terminal kind whose `meta.defaultValue` argtype can express. */
|
|
3698
|
+
function isValueTerminal(node) {
|
|
3699
|
+
return node.kind === "int" || node.kind === "float" || node.kind === "str" || node.kind === "path";
|
|
3700
|
+
}
|
|
3701
|
+
/**
|
|
3702
|
+
* Find the single value terminal a wrapper's default should attach to. Descends
|
|
3703
|
+
* through optional/repeat and a sequence with exactly one non-literal child
|
|
3704
|
+
* (the flag-value pattern `seq(lit, value)`). Returns undefined when the value
|
|
3705
|
+
* slot is ambiguous (multiple non-literal children) or absent (a bare literal
|
|
3706
|
+
* flag), in which case the wrapper default has no terminal to sink onto.
|
|
3707
|
+
*/
|
|
3708
|
+
function findValueTerminal(node) {
|
|
3709
|
+
switch (node.kind) {
|
|
3710
|
+
case "int":
|
|
3711
|
+
case "float":
|
|
3712
|
+
case "str":
|
|
3713
|
+
case "path": return node;
|
|
3714
|
+
case "optional": return findValueTerminal(node.attrs.node);
|
|
3715
|
+
case "repeat": return findValueTerminal(node.attrs.node);
|
|
3716
|
+
case "sequence": {
|
|
3717
|
+
const nonLiteral = node.attrs.nodes.filter((n) => n.kind !== "literal");
|
|
3718
|
+
return nonLiteral.length === 1 ? findValueTerminal(nonLiteral[0]) : void 0;
|
|
3719
|
+
}
|
|
3720
|
+
default: return;
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
/**
|
|
3724
|
+
* Push a wrapper's `meta.defaultValue` down onto its inner value terminal so it
|
|
3725
|
+
* can be emitted as `int = 5` / `.default(...)`. Boutiques (and argparse) hoist
|
|
3726
|
+
* an input's default onto the outermost wrapper (`optional` / `sequence`); the
|
|
3727
|
+
* argtype surface only carries `.default()` on a terminal, so we relocate it.
|
|
3728
|
+
* Boolean defaults are left in place: those are the `opt(literal)` flag-false
|
|
3729
|
+
* convention, which the frontend regenerates for free and which has no terminal.
|
|
3730
|
+
*/
|
|
3731
|
+
function sinkDefaults(expr) {
|
|
3732
|
+
const clone = structuredClone(expr);
|
|
3733
|
+
const visit = (node) => {
|
|
3734
|
+
switch (node.kind) {
|
|
3735
|
+
case "optional":
|
|
3736
|
+
visit(node.attrs.node);
|
|
3737
|
+
sinkInto(node);
|
|
3738
|
+
break;
|
|
3739
|
+
case "repeat":
|
|
3740
|
+
visit(node.attrs.node);
|
|
3741
|
+
sinkInto(node);
|
|
3742
|
+
break;
|
|
3743
|
+
case "sequence":
|
|
3744
|
+
for (const c of node.attrs.nodes) visit(c);
|
|
3745
|
+
sinkInto(node);
|
|
3746
|
+
break;
|
|
3747
|
+
case "alternative":
|
|
3748
|
+
for (const c of node.attrs.alts) visit(c);
|
|
3749
|
+
break;
|
|
3750
|
+
default: break;
|
|
3751
|
+
}
|
|
3752
|
+
};
|
|
3753
|
+
visit(clone);
|
|
3754
|
+
return clone;
|
|
3755
|
+
}
|
|
3756
|
+
function sinkInto(wrapper) {
|
|
3757
|
+
const dv = wrapper.meta?.defaultValue;
|
|
3758
|
+
if (dv === void 0 || typeof dv === "boolean") return;
|
|
3759
|
+
const terminal = findValueTerminal(wrapper);
|
|
3760
|
+
if (!terminal || !isValueTerminal(terminal) || terminal.meta?.defaultValue !== void 0) return;
|
|
3761
|
+
terminal.meta = {
|
|
3762
|
+
...terminal.meta,
|
|
3763
|
+
defaultValue: dv
|
|
3764
|
+
};
|
|
3765
|
+
const meta = { ...wrapper.meta };
|
|
3766
|
+
delete meta.defaultValue;
|
|
3767
|
+
wrapper.meta = Object.keys(meta).length > 0 ? meta : void 0;
|
|
3768
|
+
}
|
|
3769
|
+
/**
|
|
3770
|
+
* Detect the synthetic single-child sequence the frontend wraps a non-sequence
|
|
3771
|
+
* root in. Such a wrapper carries only `name` (matching the child's name) plus
|
|
3772
|
+
* any collected `outputs`; the doc/default live on the inner node. Returns the
|
|
3773
|
+
* inner node and the wrapper's outputs so the caller can emit the inner node as
|
|
3774
|
+
* the definition body (the frontend re-wraps it identically on re-parse).
|
|
3775
|
+
*/
|
|
3776
|
+
function syntheticWrap(root) {
|
|
3777
|
+
if (root.kind !== "sequence" || root.attrs.nodes.length !== 1 || root.attrs.join !== void 0) return;
|
|
3778
|
+
const meta = root.meta;
|
|
3779
|
+
if (meta?.doc || meta?.defaultValue !== void 0 || meta?.variantTag !== void 0) return;
|
|
3780
|
+
const child = root.attrs.nodes[0];
|
|
3781
|
+
if (child.kind === "sequence") return void 0;
|
|
3782
|
+
if ((child.meta?.name ?? void 0) !== (meta?.name ?? void 0)) return void 0;
|
|
3783
|
+
return {
|
|
3784
|
+
child,
|
|
3785
|
+
...meta?.outputs && { outputs: meta.outputs }
|
|
3786
|
+
};
|
|
3787
|
+
}
|
|
3788
|
+
function hasOutputs(expr) {
|
|
3789
|
+
return walkSome(expr, (n) => (n.meta?.outputs?.length ?? 0) > 0);
|
|
3790
|
+
}
|
|
3791
|
+
function hasMediaTypes(expr) {
|
|
3792
|
+
return walkSome(expr, (n) => n.kind === "path" && (n.attrs.mediaTypes?.length ?? 0) > 0);
|
|
3793
|
+
}
|
|
3794
|
+
function hasPaths(expr) {
|
|
3795
|
+
return walkSome(expr, (n) => n.kind === "path" && (!!n.attrs.mutable || !!n.attrs.resolveParent));
|
|
3796
|
+
}
|
|
3797
|
+
function walkSome(node, pred) {
|
|
3798
|
+
if (pred(node)) return true;
|
|
3799
|
+
switch (node.kind) {
|
|
3800
|
+
case "sequence": return node.attrs.nodes.some((n) => walkSome(n, pred));
|
|
3801
|
+
case "alternative": return node.attrs.alts.some((n) => walkSome(n, pred));
|
|
3802
|
+
case "optional": return walkSome(node.attrs.node, pred);
|
|
3803
|
+
case "repeat": return walkSome(node.attrs.node, pred);
|
|
3804
|
+
default: return false;
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3808
|
+
/** A name rendered where an identifier is expected: bare when it is a valid
|
|
3809
|
+
* identifier, otherwise a double-quoted form preserving it exactly. Used for
|
|
3810
|
+
* both `label:` prefixes and output-template `{ref}` targets (templates accept a
|
|
3811
|
+
* quoted name, `{"4d_output"}`), so the two always agree and no lossy
|
|
3812
|
+
* sanitization is needed. */
|
|
3813
|
+
function identOrQuoted(name) {
|
|
3814
|
+
return IDENT_RE.test(name) ? name : quote(name);
|
|
3815
|
+
}
|
|
3816
|
+
var ArgtypeEmitter = class {
|
|
3817
|
+
warnings = [];
|
|
3818
|
+
warn(message) {
|
|
3819
|
+
this.warnings.push({ message });
|
|
3820
|
+
}
|
|
3821
|
+
/** Render a name as it appears before a `:` label (bare identifier or quoted
|
|
3822
|
+
* label, e.g. `"1deval":`, `"1D":`). */
|
|
3823
|
+
labelFor(name) {
|
|
3824
|
+
return identOrQuoted(name);
|
|
3825
|
+
}
|
|
3826
|
+
/**
|
|
3827
|
+
* `///` doc lines for a `Documentation`. A title is written as a leading
|
|
3828
|
+
* Markdown H1 (`# Title`); the description follows after a blank `///` line.
|
|
3829
|
+
* Either alone emits just that part.
|
|
3830
|
+
*/
|
|
3831
|
+
docLines(doc) {
|
|
3832
|
+
if (!doc) return [];
|
|
3833
|
+
const out = [];
|
|
3834
|
+
if (doc.title) out.push(`/// # ${doc.title}`);
|
|
3835
|
+
if (doc.title && doc.description) out.push("///");
|
|
3836
|
+
if (doc.description) for (const line of wrapDoc(doc.description, DOC_WIDTH)) out.push(line === "" ? "///" : `/// ${line}`);
|
|
3837
|
+
return out;
|
|
3838
|
+
}
|
|
3839
|
+
/**
|
|
3840
|
+
* Whether a `///` block would not round-trip, so the doc must instead be
|
|
3841
|
+
* emitted as `.title()` / `.description()` chaining (which sets the fields
|
|
3842
|
+
* verbatim). Three cases:
|
|
3843
|
+
* - a multi-line title (only its first line would survive `splitDocText`);
|
|
3844
|
+
* - a description with a lone line break (a single `\n`, not a blank-line
|
|
3845
|
+
* paragraph break), which a `///` block reflows to a space - chaining keeps
|
|
3846
|
+
* the hard break intact;
|
|
3847
|
+
* - a title-less description whose first line looks like an H1 heading
|
|
3848
|
+
* (`# ...`), which would be promoted to a spurious title.
|
|
3849
|
+
* A blank-line paragraph break is safe: the frontend reflows a `///` block back
|
|
3850
|
+
* into the same paragraphs, and a description under a title is safe too
|
|
3851
|
+
* (`splitDocText` consumes only the very first line as the title).
|
|
3852
|
+
*/
|
|
3853
|
+
docNeedsChain(doc) {
|
|
3854
|
+
if (!doc) return false;
|
|
3855
|
+
if (doc.title?.includes("\n")) return true;
|
|
3856
|
+
const desc = doc.description;
|
|
3857
|
+
if (desc) {
|
|
3858
|
+
if (desc.split(/\n{2,}/).some((para) => para.includes("\n"))) return true;
|
|
3859
|
+
if (!doc.title && (desc.split("\n")[0] ?? "").trim().startsWith("# ")) return true;
|
|
3860
|
+
}
|
|
3861
|
+
return false;
|
|
3862
|
+
}
|
|
3863
|
+
/** `.title(...)` / `.description(...)` chaining for a doc that cannot round-trip
|
|
3864
|
+
* as a `///` block (see `docNeedsChain`). */
|
|
3865
|
+
docChain(doc) {
|
|
3866
|
+
if (!doc) return "";
|
|
3867
|
+
let chain = "";
|
|
3868
|
+
if (doc.title) chain += `.title(${quote(doc.title)})`;
|
|
3869
|
+
if (doc.description) chain += `.description(${quote(doc.description)})`;
|
|
3870
|
+
return chain;
|
|
3871
|
+
}
|
|
3872
|
+
/**
|
|
3873
|
+
* Warn for `Documentation` fields a node's `///` comment cannot carry. `title`
|
|
3874
|
+
* and `description` are emitted (see `docLines`); literature / comment /
|
|
3875
|
+
* authors / urls attached to an inner node have no surface, so surface them
|
|
3876
|
+
* rather than dropping silently.
|
|
3877
|
+
*/
|
|
3878
|
+
warnUnrepresentableDoc(doc, where) {
|
|
3879
|
+
if (!doc) return;
|
|
3880
|
+
const lost = [];
|
|
3881
|
+
if (doc.literature?.length) lost.push("literature");
|
|
3882
|
+
if (doc.comment) lost.push("comment");
|
|
3883
|
+
if (doc.authors?.length) lost.push("authors");
|
|
3884
|
+
if (doc.urls?.length) lost.push("urls");
|
|
3885
|
+
if (lost.length > 0) this.warn(`Documentation ${lost.join("/")} on ${where} has no argtype node surface; ignored`);
|
|
3886
|
+
}
|
|
3887
|
+
emit(expr, app) {
|
|
3888
|
+
const raw = sinkDefaults(expr);
|
|
3889
|
+
const lines = [];
|
|
3890
|
+
let exe;
|
|
3891
|
+
let root = raw;
|
|
3892
|
+
if (raw.kind === "sequence" && raw.attrs.nodes[0]?.kind === "literal") {
|
|
3893
|
+
exe = raw.attrs.nodes[0].attrs.str;
|
|
3894
|
+
root = {
|
|
3895
|
+
kind: "sequence",
|
|
3896
|
+
attrs: {
|
|
3897
|
+
...raw.attrs,
|
|
3898
|
+
nodes: raw.attrs.nodes.slice(1)
|
|
3899
|
+
}
|
|
3900
|
+
};
|
|
3901
|
+
if (raw.meta) root.meta = raw.meta;
|
|
3902
|
+
}
|
|
3903
|
+
const frontmatter = this.emitFrontmatter(app, exe, hasOutputs(root), hasMediaTypes(root), hasPaths(root));
|
|
3904
|
+
if (frontmatter) lines.push(frontmatter, "");
|
|
3905
|
+
const synthetic = syntheticWrap(root);
|
|
3906
|
+
const defNode = synthetic ? synthetic.child : root;
|
|
3907
|
+
const wrapperOutputs = synthetic ? synthetic.outputs : void 0;
|
|
3908
|
+
const rootDoc = defNode.meta?.doc ?? app?.doc;
|
|
3909
|
+
const rootChain = this.docNeedsChain(rootDoc);
|
|
3910
|
+
if (!rootChain) lines.push(...this.docLines(rootDoc));
|
|
3911
|
+
this.warnUnrepresentableDoc(defNode.meta?.doc, "the root node");
|
|
3912
|
+
const rootName = this.labelFor(defNode.meta?.name || app?.id || "tool");
|
|
3913
|
+
let body = this.emitNode(defNode, 0);
|
|
3914
|
+
if (rootChain) body += this.docChain(rootDoc);
|
|
3915
|
+
if (wrapperOutputs?.length) body += this.emitOutputs(wrapperOutputs, 0);
|
|
3916
|
+
lines.push(`${rootName}: ${body}`);
|
|
3917
|
+
return lines.join("\n") + "\n";
|
|
3918
|
+
}
|
|
3919
|
+
/**
|
|
3920
|
+
* Emit a node's core expression plus its node-local chains (value
|
|
3921
|
+
* constraints, default, join, count, media types) and any `.output(...)`.
|
|
3922
|
+
* The first line is not indented (the caller places it after a label or pad);
|
|
3923
|
+
* continuation lines are indented relative to `level`.
|
|
3924
|
+
*/
|
|
3925
|
+
emitNode(expr, level) {
|
|
3926
|
+
let core;
|
|
3927
|
+
switch (expr.kind) {
|
|
3928
|
+
case "literal":
|
|
3929
|
+
core = quote(expr.attrs.str);
|
|
3930
|
+
break;
|
|
3931
|
+
case "str":
|
|
3932
|
+
core = "str" + this.defaultSuffix(expr, false);
|
|
3933
|
+
break;
|
|
3934
|
+
case "int":
|
|
3935
|
+
case "float": {
|
|
3936
|
+
core = expr.kind;
|
|
3937
|
+
let chained = false;
|
|
3938
|
+
const min = this.finiteNum(expr.attrs.minValue, "min");
|
|
3939
|
+
if (min !== void 0) {
|
|
3940
|
+
core += `.min(${min})`;
|
|
3941
|
+
chained = true;
|
|
3942
|
+
}
|
|
3943
|
+
const max = this.finiteNum(expr.attrs.maxValue, "max");
|
|
3944
|
+
if (max !== void 0) {
|
|
3945
|
+
core += `.max(${max})`;
|
|
3946
|
+
chained = true;
|
|
3947
|
+
}
|
|
3948
|
+
core += this.defaultSuffix(expr, chained);
|
|
3949
|
+
break;
|
|
3950
|
+
}
|
|
3951
|
+
case "path": {
|
|
3952
|
+
core = "path";
|
|
3953
|
+
const media = expr.attrs.mediaTypes ?? [];
|
|
3954
|
+
for (const m of media) core += `.mediaType(${quote(m)})`;
|
|
3955
|
+
if (expr.attrs.mutable) core += ".mutable()";
|
|
3956
|
+
if (expr.attrs.resolveParent) core += ".resolveParent()";
|
|
3957
|
+
const chained = media.length > 0 || !!expr.attrs.mutable || !!expr.attrs.resolveParent;
|
|
3958
|
+
core += this.defaultSuffix(expr, chained);
|
|
3959
|
+
break;
|
|
3960
|
+
}
|
|
3961
|
+
case "sequence":
|
|
3962
|
+
core = this.emitCombinator("seq", expr.attrs.nodes, level, expr.attrs.join);
|
|
3963
|
+
core += this.structuralDefaultSuffix(expr);
|
|
3964
|
+
break;
|
|
3965
|
+
case "optional":
|
|
3966
|
+
core = this.emitOptional(expr, level) + this.structuralDefaultSuffix(expr);
|
|
3967
|
+
break;
|
|
3968
|
+
case "repeat":
|
|
3969
|
+
core = this.emitRepeat(expr, level) + this.structuralDefaultSuffix(expr);
|
|
3970
|
+
break;
|
|
3971
|
+
case "alternative":
|
|
3972
|
+
for (const arm of expr.attrs.alts) {
|
|
3973
|
+
const tag = arm.meta?.variantTag;
|
|
3974
|
+
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`);
|
|
3975
|
+
}
|
|
3976
|
+
core = this.emitCombinator("alt", expr.attrs.alts, level) + this.structuralDefaultSuffix(expr);
|
|
3977
|
+
break;
|
|
3978
|
+
default: core = "";
|
|
3979
|
+
}
|
|
3980
|
+
if (expr.meta?.outputs?.length) core += this.emitOutputs(expr.meta.outputs, level);
|
|
3981
|
+
return core;
|
|
3982
|
+
}
|
|
3983
|
+
/** `String(n)` when `n` is finite; otherwise undefined with a warning, since
|
|
3984
|
+
* `Infinity`/`NaN` have no argtype number literal (emitting them would produce
|
|
3985
|
+
* an identifier that fails to re-parse). */
|
|
3986
|
+
finiteNum(n, where) {
|
|
3987
|
+
if (n === void 0) return void 0;
|
|
3988
|
+
if (!Number.isFinite(n)) {
|
|
3989
|
+
this.warn(`Non-finite number (${String(n)}) on ${where} has no argtype literal; ignored`);
|
|
3990
|
+
return;
|
|
3991
|
+
}
|
|
3992
|
+
return num(n);
|
|
3993
|
+
}
|
|
3994
|
+
/** `= value` on a bare terminal, else `.default(value)` once a chain started. */
|
|
3995
|
+
defaultSuffix(expr, chained) {
|
|
3996
|
+
const dv = expr.meta?.defaultValue;
|
|
3997
|
+
if (dv === void 0 || typeof dv === "boolean") return "";
|
|
3998
|
+
let value;
|
|
3999
|
+
if (typeof dv === "number") {
|
|
4000
|
+
const n = this.finiteNum(dv, "default");
|
|
4001
|
+
if (n === void 0) return "";
|
|
4002
|
+
value = n;
|
|
4003
|
+
} else value = quote(dv);
|
|
4004
|
+
return chained ? `.default(${value})` : ` = ${value}`;
|
|
4005
|
+
}
|
|
4006
|
+
/**
|
|
4007
|
+
* A `.default(value)` chained onto a structural node (e.g. an enum
|
|
4008
|
+
* `alternative`, or a wrapper whose default could not sink onto an inner
|
|
4009
|
+
* terminal). Booleans have no argtype value literal: the only legitimate one
|
|
4010
|
+
* is the `opt(literal)` flag-false convention, which the frontend regenerates,
|
|
4011
|
+
* so it is silently dropped; any other boolean default is warned and dropped.
|
|
4012
|
+
*/
|
|
4013
|
+
structuralDefaultSuffix(expr) {
|
|
4014
|
+
const dv = expr.meta?.defaultValue;
|
|
4015
|
+
if (dv === void 0) return "";
|
|
4016
|
+
if (typeof dv === "boolean") {
|
|
4017
|
+
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`);
|
|
4018
|
+
return "";
|
|
4019
|
+
}
|
|
4020
|
+
if (typeof dv === "number") {
|
|
4021
|
+
const n = this.finiteNum(dv, "default");
|
|
4022
|
+
return n === void 0 ? "" : `.default(${n})`;
|
|
4023
|
+
}
|
|
4024
|
+
return `.default(${quote(dv)})`;
|
|
4025
|
+
}
|
|
4026
|
+
emitOptional(expr, level) {
|
|
4027
|
+
const inner = expr.attrs.node;
|
|
4028
|
+
if (inner.kind === "sequence" && !inner.meta && inner.attrs.join === void 0) return this.emitCombinator("opt", inner.attrs.nodes, level);
|
|
4029
|
+
return this.emitCombinator("opt", [inner], level);
|
|
4030
|
+
}
|
|
4031
|
+
emitRepeat(expr, level) {
|
|
4032
|
+
const node = expr.attrs.node;
|
|
4033
|
+
let core;
|
|
4034
|
+
if (node.kind === "sequence" && !node.meta && node.attrs.join === void 0) core = this.emitCombinator("rep", node.attrs.nodes, level);
|
|
4035
|
+
else core = this.emitCombinator("rep", [node], level);
|
|
4036
|
+
if (expr.attrs.join !== void 0) core += `.join(${expr.attrs.join === "" ? "" : quote(expr.attrs.join)})`;
|
|
4037
|
+
const { countMin, countMax } = expr.attrs;
|
|
4038
|
+
if (countMin !== void 0 && countMin === countMax) core += `.count(${countMin})`;
|
|
4039
|
+
else {
|
|
4040
|
+
if (countMin !== void 0) core += `.countMin(${countMin})`;
|
|
4041
|
+
if (countMax !== void 0) core += `.countMax(${countMax})`;
|
|
4042
|
+
}
|
|
4043
|
+
return core;
|
|
4044
|
+
}
|
|
4045
|
+
emitCombinator(keyword, children, level, join) {
|
|
4046
|
+
let core;
|
|
4047
|
+
if (children.length === 0) core = `${keyword}()`;
|
|
4048
|
+
else {
|
|
4049
|
+
const items = children.map((c) => this.emitDecorated(c, level + 1));
|
|
4050
|
+
const inline = `${keyword}(${items.join(", ")})`;
|
|
4051
|
+
if (!items.some((i) => i.includes("\n")) && inline.length + level * 2 <= 80) core = inline;
|
|
4052
|
+
else core = `${keyword}(\n${items.map((i) => pad(level + 1) + i).join(",\n")},\n${pad(level)})`;
|
|
4053
|
+
}
|
|
4054
|
+
if (join !== void 0) core += `.join(${join === "" ? "" : quote(join)})`;
|
|
4055
|
+
return core;
|
|
4056
|
+
}
|
|
4057
|
+
/**
|
|
4058
|
+
* A list item: leading `///` doc lines, an optional `label:` prefix, then the
|
|
4059
|
+
* node. The first line is unindented (the caller pads it); continuation lines
|
|
4060
|
+
* are indented to `level`.
|
|
4061
|
+
*/
|
|
4062
|
+
emitDecorated(expr, level) {
|
|
4063
|
+
const doc = expr.meta?.doc;
|
|
4064
|
+
const useChain = this.docNeedsChain(doc);
|
|
4065
|
+
const parts = useChain ? [] : [...this.docLines(doc)];
|
|
4066
|
+
this.warnUnrepresentableDoc(doc, `node '${expr.meta?.name ?? expr.kind}'`);
|
|
4067
|
+
const label = expr.meta?.name ? `${this.labelFor(expr.meta.name)}: ` : "";
|
|
4068
|
+
let node = this.emitNode(expr, level);
|
|
4069
|
+
if (useChain) node += this.docChain(doc);
|
|
4070
|
+
parts.push(label + node);
|
|
4071
|
+
return parts.join("\n" + pad(level));
|
|
4072
|
+
}
|
|
4073
|
+
emitOutputs(outputs, level) {
|
|
4074
|
+
return `.output(\n${outputs.map((o) => {
|
|
4075
|
+
const useChain = this.docNeedsChain(o.doc);
|
|
4076
|
+
const docs = useChain ? [] : this.docLines(o.doc).map((l) => pad(level + 1) + l);
|
|
4077
|
+
let template = pad(level + 1) + this.emitOutputTemplate(o);
|
|
4078
|
+
if (useChain) template += this.docChain(o.doc);
|
|
4079
|
+
return [...docs, template].join("\n");
|
|
4080
|
+
}).join(",\n")},\n${pad(level)})`;
|
|
4081
|
+
}
|
|
4082
|
+
emitOutputTemplate(output) {
|
|
4083
|
+
if (output.mediaTypes?.length) this.warn(`Output '${output.name ?? "<anon>"}' has media types, which have no argtype surface; ignored`);
|
|
4084
|
+
const label = output.name ? `${this.labelFor(output.name)}: ` : "";
|
|
4085
|
+
let body = "";
|
|
4086
|
+
for (const token of output.tokens) {
|
|
4087
|
+
if (token.kind === "literal") {
|
|
4088
|
+
body += token.value.replace(/[\\{}`]/g, (c) => `\\${c}`);
|
|
4089
|
+
continue;
|
|
4090
|
+
}
|
|
4091
|
+
let ref = identOrQuoted(token.target.name);
|
|
4092
|
+
for (const ext of token.stripExtensions ?? []) ref += `.strip_suffix(${quote(ext)})`;
|
|
4093
|
+
if (token.fallback !== void 0) ref += `.or(${quote(token.fallback)})`;
|
|
4094
|
+
body += `{${ref}}`;
|
|
4095
|
+
}
|
|
4096
|
+
return `${label}\`${body}\``;
|
|
4097
|
+
}
|
|
4098
|
+
/** Quote a frontmatter scalar (double-quoted, backslash-escaped). The
|
|
4099
|
+
* frontmatter parser unescapes symmetrically, so quotes/newlines round-trip. */
|
|
4100
|
+
fmScalar(value) {
|
|
4101
|
+
return quote(value);
|
|
4102
|
+
}
|
|
4103
|
+
emitFrontmatter(app, exe, outputs, mediaTypes, paths) {
|
|
4104
|
+
const lines = [];
|
|
4105
|
+
if (exe !== void 0) lines.push(`exe: ${this.fmScalar(exe)}`);
|
|
4106
|
+
if (app) {
|
|
4107
|
+
if (app.version) lines.push(`version: ${this.fmScalar(app.version)}`);
|
|
4108
|
+
const authors = app.doc?.authors ?? [];
|
|
4109
|
+
if (authors.length > 0) {
|
|
4110
|
+
lines.push("authors:");
|
|
4111
|
+
for (const a of authors) lines.push(` - ${this.fmScalar(a)}`);
|
|
4112
|
+
}
|
|
4113
|
+
const urls = app.doc?.urls ?? [];
|
|
4114
|
+
if (urls.length > 0) {
|
|
4115
|
+
lines.push("urls:");
|
|
4116
|
+
for (const u of urls) lines.push(` - ${this.fmScalar(u)}`);
|
|
4117
|
+
}
|
|
4118
|
+
const references = app.doc?.literature ?? [];
|
|
4119
|
+
if (references.length > 0) {
|
|
4120
|
+
lines.push("references:");
|
|
4121
|
+
for (const rf of references) lines.push(` - ${this.fmScalar(rf)}`);
|
|
4122
|
+
}
|
|
4123
|
+
if (app.container) {
|
|
4124
|
+
lines.push("container:");
|
|
4125
|
+
lines.push(` image: ${this.fmScalar(app.container.image)}`);
|
|
4126
|
+
if (app.container.type) lines.push(` type: ${this.fmScalar(app.container.type)}`);
|
|
4127
|
+
}
|
|
4128
|
+
if (app.stdout) this.emitStream(lines, "stdout", app.stdout);
|
|
4129
|
+
if (app.stderr) this.emitStream(lines, "stderr", app.stderr);
|
|
4130
|
+
if (app.doc?.comment) this.warn("AppMeta.doc.comment has no argtype surface; ignored");
|
|
4131
|
+
}
|
|
4132
|
+
const extensions = [];
|
|
4133
|
+
if (outputs) extensions.push("outputs");
|
|
4134
|
+
if (mediaTypes) extensions.push("mediatypes");
|
|
4135
|
+
if (paths) extensions.push("paths");
|
|
4136
|
+
if (extensions.length > 0) {
|
|
4137
|
+
lines.push("extensions:");
|
|
4138
|
+
for (const e of extensions) lines.push(` - ${e}`);
|
|
4139
|
+
}
|
|
4140
|
+
if (lines.length === 0) return void 0;
|
|
4141
|
+
return [
|
|
4142
|
+
"---",
|
|
4143
|
+
...lines,
|
|
4144
|
+
"---"
|
|
4145
|
+
].join("\n");
|
|
4146
|
+
}
|
|
4147
|
+
emitStream(lines, key, stream) {
|
|
4148
|
+
lines.push(`${key}:`);
|
|
4149
|
+
lines.push(` name: ${this.fmScalar(stream.name)}`);
|
|
4150
|
+
if (stream.doc?.description) lines.push(` description: ${this.fmScalar(stream.doc.description)}`);
|
|
4151
|
+
if (stream.doc?.title) this.warn(`${key} stream title has no argtype surface; ignored`);
|
|
4152
|
+
}
|
|
4153
|
+
};
|
|
4154
|
+
/** Emit argtype sugar-DSL source from an IR expression tree and optional `AppMeta`. */
|
|
4155
|
+
function generateArgtype(expr, app) {
|
|
4156
|
+
const emitter = new ArgtypeEmitter();
|
|
4157
|
+
return {
|
|
4158
|
+
source: emitter.emit(expr, app),
|
|
4159
|
+
warnings: emitter.warnings
|
|
4160
|
+
};
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4163
|
+
//#endregion
|
|
4164
|
+
//#region src/backend/argtype/argtype.ts
|
|
4165
|
+
/**
|
|
4166
|
+
* A per-tool file stem, so co-located tools in a catalog build don't clobber one
|
|
4167
|
+
* another's `descriptor.argtype`. Standalone single-tool builds (no scope) keep
|
|
4168
|
+
* the bare name. Mirrors the JSON Schema backend's `schemaStem`.
|
|
4169
|
+
*/
|
|
4170
|
+
function argtypeStem(ctx, scope) {
|
|
4171
|
+
const id = ctx.app?.id;
|
|
4172
|
+
if (!id || !scope) return void 0;
|
|
4173
|
+
return scope.add(snakeCase(id) || "descriptor");
|
|
4174
|
+
}
|
|
4175
|
+
/**
|
|
4176
|
+
* Serialization backend: emit argtype sugar-DSL source from the IR + `AppMeta`.
|
|
4177
|
+
*
|
|
4178
|
+
* The dogfooding / round-trip counterpart to the argtype frontend. Like the
|
|
4179
|
+
* Boutiques backend it only needs the IR (`ctx.expr`) and app metadata
|
|
4180
|
+
* (`ctx.app`), never the solved bindings, so it ignores the rest of the context.
|
|
4181
|
+
*/
|
|
4182
|
+
var ArgtypeBackend = class {
|
|
4183
|
+
name = "argtype";
|
|
4184
|
+
target = "argtype";
|
|
4185
|
+
/** One scope per package so per-tool file stems stay unique in the suite dir. */
|
|
4186
|
+
newPackageScope() {
|
|
4187
|
+
return new Scope();
|
|
4188
|
+
}
|
|
4189
|
+
emitApp(ctx, scope) {
|
|
4190
|
+
const { source, warnings } = generateArgtype(ctx.expr, ctx.app);
|
|
4191
|
+
const stem = argtypeStem(ctx, scope);
|
|
4192
|
+
const filename = stem ? `${stem}.argtype` : "descriptor.argtype";
|
|
4193
|
+
return {
|
|
4194
|
+
meta: ctx.app,
|
|
4195
|
+
files: new Map([[filename, source]]),
|
|
4196
|
+
errors: [],
|
|
4197
|
+
warnings
|
|
4198
|
+
};
|
|
4199
|
+
}
|
|
4200
|
+
};
|
|
4201
|
+
|
|
2235
4202
|
//#endregion
|
|
2236
4203
|
//#region src/backend/find-doc.ts
|
|
2237
4204
|
/**
|
|
@@ -2563,53 +4530,6 @@ function planScope(scope, scopeGate, bindings) {
|
|
|
2563
4530
|
return scope.outputs.map((output) => planOutput(scopeGate, output, bindings));
|
|
2564
4531
|
}
|
|
2565
4532
|
|
|
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
4533
|
//#endregion
|
|
2614
4534
|
//#region src/backend/boutiques/boutiques.ts
|
|
2615
4535
|
var BoutiquesEmitter = class {
|
|
@@ -9089,7 +11009,8 @@ function solve(expr, options) {
|
|
|
9089
11009
|
//#region src/index.ts
|
|
9090
11010
|
function compile(source, filenameOrOptions) {
|
|
9091
11011
|
const options = typeof filenameOrOptions === "string" ? { filename: filenameOrOptions } : filenameOrOptions ?? {};
|
|
9092
|
-
const
|
|
11012
|
+
const byExtension = options.filename?.endsWith(".argtype") ? "argtype" : void 0;
|
|
11013
|
+
const format = options.format ?? byExtension ?? detectFormat(source);
|
|
9093
11014
|
if (!format) return {
|
|
9094
11015
|
expr: {
|
|
9095
11016
|
kind: "sequence",
|
|
@@ -9098,9 +11019,18 @@ function compile(source, filenameOrOptions) {
|
|
|
9098
11019
|
errors: [{ message: "Could not detect input format. Specify format explicitly." }],
|
|
9099
11020
|
warnings: []
|
|
9100
11021
|
};
|
|
9101
|
-
return (
|
|
11022
|
+
return (() => {
|
|
11023
|
+
switch (format) {
|
|
11024
|
+
case "argdump": return new ArgdumpParser();
|
|
11025
|
+
case "argtype": return new ArgtypeParser();
|
|
11026
|
+
case "workbench": return new WorkbenchParser();
|
|
11027
|
+
case "mrtrix": return new MrtrixParser();
|
|
11028
|
+
case "boutiques": return new BoutiquesParser();
|
|
11029
|
+
default: return new BoutiquesParser();
|
|
11030
|
+
}
|
|
11031
|
+
})().parse(source, options.filename);
|
|
9102
11032
|
}
|
|
9103
11033
|
|
|
9104
11034
|
//#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, compactTokens, compile, compose, createContext, createPipeline, createRegistry, defaultNamingStrategy, defaultPipeline, detectFormat, effectiveOutputName, findDoc, findStructNode, fixpoint, flatten, float, format, formatSolveResult, generateBoutiques, generateNipype, generateOutputsSchema, generatePydra, generatePython, generateSchema, generateTypeScript, int, isGated, isIterated, isStructural, isTerminal, lit, nipypeNames, nodeRef, opt, outputGate, pascalCase, path, planOutput, planScope, pydraNames, removeEmpty, renderPythonCall, renderStructLiteral, renderTypeScriptCall, renderValue, rep, repJoin, resolveFieldBinding, resolveOutputs, resolveTypeName, screamingSnakeCase, seq, seqJoin, simplify, snakeCase, solve, str, structKey, typeKey, unionKey };
|
|
11035
|
+
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, compactTokens, 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, isGated, isIterated, isStructural, isTerminal, lit, nipypeNames, nodeRef, opt, outputGate, pascalCase, path, planOutput, planScope, pydraNames, removeEmpty, renderPythonCall, renderStructLiteral, renderTypeScriptCall, renderValue, rep, repJoin, resolveFieldBinding, resolveOutputs, resolveTypeName, screamingSnakeCase, seq, seqJoin, simplify, snakeCase, solve, str, structKey, typeKey, unionKey };
|
|
9106
11036
|
//# sourceMappingURL=index.mjs.map
|