@aigne/ash 0.0.1
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/DESIGN.md +41 -0
- package/dist/ai-dev-loop/ash-run-result.cjs +12 -0
- package/dist/ai-dev-loop/ash-run-result.d.cts +28 -0
- package/dist/ai-dev-loop/ash-run-result.d.cts.map +1 -0
- package/dist/ai-dev-loop/ash-run-result.d.mts +28 -0
- package/dist/ai-dev-loop/ash-run-result.d.mts.map +1 -0
- package/dist/ai-dev-loop/ash-run-result.mjs +11 -0
- package/dist/ai-dev-loop/ash-run-result.mjs.map +1 -0
- package/dist/ai-dev-loop/ash-typed-error.cjs +51 -0
- package/dist/ai-dev-loop/ash-typed-error.d.cts +54 -0
- package/dist/ai-dev-loop/ash-typed-error.d.cts.map +1 -0
- package/dist/ai-dev-loop/ash-typed-error.d.mts +54 -0
- package/dist/ai-dev-loop/ash-typed-error.d.mts.map +1 -0
- package/dist/ai-dev-loop/ash-typed-error.mjs +50 -0
- package/dist/ai-dev-loop/ash-typed-error.mjs.map +1 -0
- package/dist/ai-dev-loop/ash-validate.cjs +27 -0
- package/dist/ai-dev-loop/ash-validate.d.cts +7 -0
- package/dist/ai-dev-loop/ash-validate.d.cts.map +1 -0
- package/dist/ai-dev-loop/ash-validate.d.mts +7 -0
- package/dist/ai-dev-loop/ash-validate.d.mts.map +1 -0
- package/dist/ai-dev-loop/ash-validate.mjs +28 -0
- package/dist/ai-dev-loop/ash-validate.mjs.map +1 -0
- package/dist/ai-dev-loop/dev-loop.cjs +134 -0
- package/dist/ai-dev-loop/dev-loop.d.cts +28 -0
- package/dist/ai-dev-loop/dev-loop.d.cts.map +1 -0
- package/dist/ai-dev-loop/dev-loop.d.mts +28 -0
- package/dist/ai-dev-loop/dev-loop.d.mts.map +1 -0
- package/dist/ai-dev-loop/dev-loop.mjs +135 -0
- package/dist/ai-dev-loop/dev-loop.mjs.map +1 -0
- package/dist/ai-dev-loop/index.cjs +24 -0
- package/dist/ai-dev-loop/index.d.cts +9 -0
- package/dist/ai-dev-loop/index.d.mts +9 -0
- package/dist/ai-dev-loop/index.mjs +10 -0
- package/dist/ai-dev-loop/live-mode.cjs +17 -0
- package/dist/ai-dev-loop/live-mode.d.cts +24 -0
- package/dist/ai-dev-loop/live-mode.d.cts.map +1 -0
- package/dist/ai-dev-loop/live-mode.d.mts +24 -0
- package/dist/ai-dev-loop/live-mode.d.mts.map +1 -0
- package/dist/ai-dev-loop/live-mode.mjs +17 -0
- package/dist/ai-dev-loop/live-mode.mjs.map +1 -0
- package/dist/ai-dev-loop/meta-tools.cjs +123 -0
- package/dist/ai-dev-loop/meta-tools.d.cts +24 -0
- package/dist/ai-dev-loop/meta-tools.d.cts.map +1 -0
- package/dist/ai-dev-loop/meta-tools.d.mts +24 -0
- package/dist/ai-dev-loop/meta-tools.d.mts.map +1 -0
- package/dist/ai-dev-loop/meta-tools.mjs +120 -0
- package/dist/ai-dev-loop/meta-tools.mjs.map +1 -0
- package/dist/ai-dev-loop/structured-runner.cjs +154 -0
- package/dist/ai-dev-loop/structured-runner.d.cts +12 -0
- package/dist/ai-dev-loop/structured-runner.d.cts.map +1 -0
- package/dist/ai-dev-loop/structured-runner.d.mts +12 -0
- package/dist/ai-dev-loop/structured-runner.d.mts.map +1 -0
- package/dist/ai-dev-loop/structured-runner.mjs +155 -0
- package/dist/ai-dev-loop/structured-runner.mjs.map +1 -0
- package/dist/ai-dev-loop/system-prompt.cjs +55 -0
- package/dist/ai-dev-loop/system-prompt.d.cts +20 -0
- package/dist/ai-dev-loop/system-prompt.d.cts.map +1 -0
- package/dist/ai-dev-loop/system-prompt.d.mts +20 -0
- package/dist/ai-dev-loop/system-prompt.d.mts.map +1 -0
- package/dist/ai-dev-loop/system-prompt.mjs +54 -0
- package/dist/ai-dev-loop/system-prompt.mjs.map +1 -0
- package/dist/ast.d.cts +140 -0
- package/dist/ast.d.cts.map +1 -0
- package/dist/ast.d.mts +140 -0
- package/dist/ast.d.mts.map +1 -0
- package/dist/compiler.cjs +802 -0
- package/dist/compiler.d.cts +103 -0
- package/dist/compiler.d.cts.map +1 -0
- package/dist/compiler.d.mts +103 -0
- package/dist/compiler.d.mts.map +1 -0
- package/dist/compiler.mjs +802 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/index.cjs +14 -0
- package/dist/index.d.cts +7 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +7 -0
- package/dist/lexer.cjs +451 -0
- package/dist/lexer.d.cts +14 -0
- package/dist/lexer.d.cts.map +1 -0
- package/dist/lexer.d.mts +14 -0
- package/dist/lexer.d.mts.map +1 -0
- package/dist/lexer.mjs +451 -0
- package/dist/lexer.mjs.map +1 -0
- package/dist/parser.cjs +734 -0
- package/dist/parser.d.cts +40 -0
- package/dist/parser.d.cts.map +1 -0
- package/dist/parser.d.mts +40 -0
- package/dist/parser.d.mts.map +1 -0
- package/dist/parser.mjs +734 -0
- package/dist/parser.mjs.map +1 -0
- package/dist/reference.cjs +130 -0
- package/dist/reference.d.cts +11 -0
- package/dist/reference.d.cts.map +1 -0
- package/dist/reference.d.mts +11 -0
- package/dist/reference.d.mts.map +1 -0
- package/dist/reference.mjs +130 -0
- package/dist/reference.mjs.map +1 -0
- package/dist/template.cjs +85 -0
- package/dist/template.mjs +84 -0
- package/dist/template.mjs.map +1 -0
- package/dist/type-checker.cjs +582 -0
- package/dist/type-checker.d.cts +31 -0
- package/dist/type-checker.d.cts.map +1 -0
- package/dist/type-checker.d.mts +31 -0
- package/dist/type-checker.d.mts.map +1 -0
- package/dist/type-checker.mjs +573 -0
- package/dist/type-checker.mjs.map +1 -0
- package/package.json +29 -0
- package/src/ai-dev-loop/ash-run-result.test.ts +113 -0
- package/src/ai-dev-loop/ash-run-result.ts +46 -0
- package/src/ai-dev-loop/ash-typed-error.test.ts +136 -0
- package/src/ai-dev-loop/ash-typed-error.ts +50 -0
- package/src/ai-dev-loop/ash-validate.test.ts +54 -0
- package/src/ai-dev-loop/ash-validate.ts +34 -0
- package/src/ai-dev-loop/dev-loop.test.ts +364 -0
- package/src/ai-dev-loop/dev-loop.ts +156 -0
- package/src/ai-dev-loop/dry-run.test.ts +107 -0
- package/src/ai-dev-loop/e2e-multi-fix.test.ts +473 -0
- package/src/ai-dev-loop/e2e.test.ts +324 -0
- package/src/ai-dev-loop/index.ts +15 -0
- package/src/ai-dev-loop/invariants.test.ts +253 -0
- package/src/ai-dev-loop/live-mode.test.ts +63 -0
- package/src/ai-dev-loop/live-mode.ts +33 -0
- package/src/ai-dev-loop/meta-tools.test.ts +120 -0
- package/src/ai-dev-loop/meta-tools.ts +142 -0
- package/src/ai-dev-loop/structured-runner.test.ts +159 -0
- package/src/ai-dev-loop/structured-runner.ts +209 -0
- package/src/ai-dev-loop/system-prompt.test.ts +102 -0
- package/src/ai-dev-loop/system-prompt.ts +81 -0
- package/src/ast.ts +186 -0
- package/src/compiler.test.ts +2933 -0
- package/src/compiler.ts +1103 -0
- package/src/e2e.test.ts +552 -0
- package/src/index.ts +16 -0
- package/src/lexer.test.ts +538 -0
- package/src/lexer.ts +222 -0
- package/src/parser.test.ts +1024 -0
- package/src/parser.ts +835 -0
- package/src/reference.test.ts +166 -0
- package/src/reference.ts +125 -0
- package/src/template.test.ts +210 -0
- package/src/template.ts +139 -0
- package/src/type-checker.test.ts +1494 -0
- package/src/type-checker.ts +785 -0
- package/tsconfig.json +9 -0
- package/tsdown.config.ts +12 -0
package/dist/parser.mjs
ADDED
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
//#region src/parser.ts
|
|
2
|
+
/** Annotation names that are script-level: propagate to all jobs when declared before any statement. */
|
|
3
|
+
const SCRIPT_LEVEL_ANNOTATIONS = new Set(["caps", "budget"]);
|
|
4
|
+
/** Merge script-level annotations with job-specific ones. Job overrides script-level by name. */
|
|
5
|
+
function mergeAnnotations(script, job) {
|
|
6
|
+
if (script.length === 0) return job;
|
|
7
|
+
if (job.length === 0) return [...script];
|
|
8
|
+
const jobNames = new Set(job.map((a) => a.name));
|
|
9
|
+
return [...script.filter((a) => !jobNames.has(a.name)), ...job];
|
|
10
|
+
}
|
|
11
|
+
var AshParser = class AshParser {
|
|
12
|
+
tokens = [];
|
|
13
|
+
pos = 0;
|
|
14
|
+
letNames = /* @__PURE__ */ new Set();
|
|
15
|
+
paramNames = /* @__PURE__ */ new Set();
|
|
16
|
+
static RESERVED_KEYWORDS = new Set([
|
|
17
|
+
"job",
|
|
18
|
+
"find",
|
|
19
|
+
"where",
|
|
20
|
+
"map",
|
|
21
|
+
"save",
|
|
22
|
+
"publish",
|
|
23
|
+
"tee",
|
|
24
|
+
"fanout",
|
|
25
|
+
"input",
|
|
26
|
+
"output",
|
|
27
|
+
"let",
|
|
28
|
+
"action",
|
|
29
|
+
"route",
|
|
30
|
+
"lookup",
|
|
31
|
+
"param",
|
|
32
|
+
"on"
|
|
33
|
+
]);
|
|
34
|
+
parse(tokens) {
|
|
35
|
+
this.tokens = tokens;
|
|
36
|
+
this.pos = 0;
|
|
37
|
+
this.letNames = /* @__PURE__ */ new Set();
|
|
38
|
+
this.paramNames = /* @__PURE__ */ new Set();
|
|
39
|
+
const statements = [];
|
|
40
|
+
this.skipNewlines();
|
|
41
|
+
const initialAnnotations = this.parseAnnotations();
|
|
42
|
+
const scriptAnnotations = initialAnnotations.filter((a) => SCRIPT_LEVEL_ANNOTATIONS.has(a.name));
|
|
43
|
+
const firstJobOnly = initialAnnotations.filter((a) => !SCRIPT_LEVEL_ANNOTATIONS.has(a.name));
|
|
44
|
+
let firstStatement = true;
|
|
45
|
+
while (!this.isAtEnd()) {
|
|
46
|
+
const jobAnnotations = this.parseAnnotations();
|
|
47
|
+
if (this.isAtEnd()) break;
|
|
48
|
+
const t = this.peek();
|
|
49
|
+
if (t.type === "JOB") {
|
|
50
|
+
const merged = mergeAnnotations(scriptAnnotations, firstStatement ? [...firstJobOnly, ...jobAnnotations] : jobAnnotations);
|
|
51
|
+
firstStatement = false;
|
|
52
|
+
statements.push(this.parseJob(merged));
|
|
53
|
+
} else if (t.type === "OUTPUT" && jobAnnotations.length === 0) {
|
|
54
|
+
firstStatement = false;
|
|
55
|
+
this.advance();
|
|
56
|
+
const msg = this.expect("STRING", "Expected string after 'output'");
|
|
57
|
+
statements.push({
|
|
58
|
+
kind: "output",
|
|
59
|
+
message: msg.value
|
|
60
|
+
});
|
|
61
|
+
} else if (t.type === "LET" && jobAnnotations.length === 0) {
|
|
62
|
+
firstStatement = false;
|
|
63
|
+
statements.push(this.parseLet());
|
|
64
|
+
} else if (t.type === "PARAM" && jobAnnotations.length === 0) {
|
|
65
|
+
firstStatement = false;
|
|
66
|
+
statements.push(this.parseParam());
|
|
67
|
+
} else throw new Error(`Expected 'job', 'output', 'let', or 'param' at top level, got ${t.type} at line ${t.line}, column ${t.column}`);
|
|
68
|
+
this.skipNewlines();
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
statements,
|
|
72
|
+
jobs: statements.filter((s) => s.kind === "job")
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
parseParam() {
|
|
76
|
+
const paramToken = this.advance();
|
|
77
|
+
if (this.isAtEnd()) throw new Error(`Expected parameter name after 'param' at line ${paramToken.line}, column ${paramToken.column}`);
|
|
78
|
+
const nameToken = this.peek();
|
|
79
|
+
if (nameToken.type !== "IDENTIFIER") throw new Error(`Cannot use reserved keyword '${nameToken.value}' as param name at line ${nameToken.line}, column ${nameToken.column}`);
|
|
80
|
+
this.advance();
|
|
81
|
+
if (AshParser.RESERVED_KEYWORDS.has(nameToken.value)) throw new Error(`Cannot use reserved keyword '${nameToken.value}' as param name at line ${nameToken.line}, column ${nameToken.column}`);
|
|
82
|
+
if (this.paramNames.has(nameToken.value)) throw new Error(`Duplicate param name '${nameToken.value}' at line ${nameToken.line}, column ${nameToken.column}`);
|
|
83
|
+
if (this.letNames.has(nameToken.value)) throw new Error(`Param '${nameToken.value}' conflicts with existing let variable at line ${nameToken.line}, column ${nameToken.column}`);
|
|
84
|
+
this.expect("ASSIGN", "Expected '=' after param name");
|
|
85
|
+
const valToken = this.advance();
|
|
86
|
+
let defaultValue;
|
|
87
|
+
if (valToken.type === "NUMBER") defaultValue = parseFloat(valToken.value);
|
|
88
|
+
else if (valToken.type === "STRING") defaultValue = valToken.value;
|
|
89
|
+
else if (valToken.type === "PATH") defaultValue = valToken.value;
|
|
90
|
+
else if (valToken.type === "IDENTIFIER") defaultValue = valToken.value;
|
|
91
|
+
else throw new Error(`Expected default value after '=' at line ${valToken.line}, column ${valToken.column}`);
|
|
92
|
+
this.paramNames.add(nameToken.value);
|
|
93
|
+
return {
|
|
94
|
+
kind: "param",
|
|
95
|
+
name: nameToken.value,
|
|
96
|
+
defaultValue
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
static PIPELINE_STARTERS = new Set([
|
|
100
|
+
"FIND",
|
|
101
|
+
"ACTION",
|
|
102
|
+
"LOOKUP",
|
|
103
|
+
"INPUT"
|
|
104
|
+
]);
|
|
105
|
+
parseLet() {
|
|
106
|
+
const letToken = this.advance();
|
|
107
|
+
if (this.isAtEnd() || this.peek().type !== "IDENTIFIER") throw new Error(`Expected variable name after 'let' at line ${letToken.line}, column ${letToken.column}`);
|
|
108
|
+
const nameToken = this.advance();
|
|
109
|
+
if (AshParser.RESERVED_KEYWORDS.has(nameToken.value)) throw new Error(`Cannot use reserved keyword '${nameToken.value}' as variable name at line ${nameToken.line}, column ${nameToken.column}`);
|
|
110
|
+
if (this.letNames.has(nameToken.value)) throw new Error(`Duplicate variable name '${nameToken.value}' at line ${nameToken.line}, column ${nameToken.column}`);
|
|
111
|
+
if (this.paramNames.has(nameToken.value)) throw new Error(`Let variable '${nameToken.value}' conflicts with existing param at line ${nameToken.line}, column ${nameToken.column}`);
|
|
112
|
+
this.expect("ASSIGN", "Expected '=' after variable name");
|
|
113
|
+
if (!this.isAtEnd() && AshParser.PIPELINE_STARTERS.has(this.peek().type)) {
|
|
114
|
+
const pipeline = this.parseLetPipeline();
|
|
115
|
+
this.letNames.add(nameToken.value);
|
|
116
|
+
return {
|
|
117
|
+
kind: "let",
|
|
118
|
+
name: nameToken.value,
|
|
119
|
+
value: 0,
|
|
120
|
+
pipeline
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const valToken = this.advance();
|
|
124
|
+
let value;
|
|
125
|
+
if (valToken.type === "NUMBER") value = parseFloat(valToken.value);
|
|
126
|
+
else if (valToken.type === "STRING") value = valToken.value;
|
|
127
|
+
else if (valToken.type === "IDENTIFIER") value = valToken.value;
|
|
128
|
+
else throw new Error(`Expected value after '=' at line ${valToken.line}, column ${valToken.column}`);
|
|
129
|
+
this.letNames.add(nameToken.value);
|
|
130
|
+
return {
|
|
131
|
+
kind: "let",
|
|
132
|
+
name: nameToken.value,
|
|
133
|
+
value
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
parseLetPipeline() {
|
|
137
|
+
const stages = [];
|
|
138
|
+
while (!this.isAtEnd()) {
|
|
139
|
+
const t = this.peek();
|
|
140
|
+
if (t.type === "NEWLINE" || t.type === "JOB" || t.type === "LET" || t.type === "OUTPUT" || t.type === "AT") break;
|
|
141
|
+
if (t.type === "PIPE") {
|
|
142
|
+
this.advance();
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
stages.push(this.parseStage());
|
|
146
|
+
}
|
|
147
|
+
return stages;
|
|
148
|
+
}
|
|
149
|
+
parseAnnotations() {
|
|
150
|
+
const annotations = [];
|
|
151
|
+
while (!this.isAtEnd() && this.peek().type === "AT") {
|
|
152
|
+
const at = this.advance();
|
|
153
|
+
if (this.isAtEnd() || this.peek().type !== "IDENTIFIER") throw new Error(`Expected annotation name after '@' at line ${at.line}, column ${at.column}`);
|
|
154
|
+
const nameToken = this.advance();
|
|
155
|
+
const args = [];
|
|
156
|
+
if (!this.isAtEnd() && this.peek().type === "LPAREN") {
|
|
157
|
+
this.advance();
|
|
158
|
+
while (!this.isAtEnd() && this.peek().type !== "RPAREN") {
|
|
159
|
+
const arg = this.advance();
|
|
160
|
+
args.push(arg.value);
|
|
161
|
+
if (!this.isAtEnd() && this.peek().type === "COMMA") this.advance();
|
|
162
|
+
}
|
|
163
|
+
if (this.isAtEnd()) throw new Error(`Unclosed annotation parenthesis at line ${at.line}, column ${at.column}`);
|
|
164
|
+
this.advance();
|
|
165
|
+
}
|
|
166
|
+
annotations.push({
|
|
167
|
+
name: nameToken.value,
|
|
168
|
+
args,
|
|
169
|
+
line: at.line,
|
|
170
|
+
column: at.column
|
|
171
|
+
});
|
|
172
|
+
this.skipNewlines();
|
|
173
|
+
}
|
|
174
|
+
return annotations;
|
|
175
|
+
}
|
|
176
|
+
parseJob(annotations) {
|
|
177
|
+
this.advance();
|
|
178
|
+
this.skipNewlines();
|
|
179
|
+
if (this.isAtEnd()) throw new Error("Expected job name");
|
|
180
|
+
const nameToken = this.advance();
|
|
181
|
+
if (AshParser.RESERVED_KEYWORDS.has(nameToken.value)) throw new Error(`Cannot use reserved keyword '${nameToken.value}' as job name at line ${nameToken.line}, column ${nameToken.column}`);
|
|
182
|
+
this.skipNewlines();
|
|
183
|
+
let trigger;
|
|
184
|
+
if (!this.isAtEnd() && this.peek().type === "ON") {
|
|
185
|
+
const onToken = this.advance();
|
|
186
|
+
if (!this.isAtEnd() && this.peek().type === "IDENTIFIER" && this.peek().value === "cron") {
|
|
187
|
+
this.advance();
|
|
188
|
+
if (this.isAtEnd() || this.peek().type !== "LPAREN") throw new Error(`Expected '(' after 'cron' at line ${onToken.line}, column ${onToken.column}`);
|
|
189
|
+
this.advance();
|
|
190
|
+
if (this.isAtEnd() || this.peek().type !== "STRING") throw new Error(`Expected cron expression string after 'cron(' at line ${onToken.line}, column ${onToken.column}`);
|
|
191
|
+
const exprToken = this.advance();
|
|
192
|
+
if (this.isAtEnd() || this.peek().type !== "RPAREN") throw new Error(`Expected ')' after cron expression at line ${onToken.line}, column ${onToken.column}`);
|
|
193
|
+
this.advance();
|
|
194
|
+
trigger = {
|
|
195
|
+
kind: "cron",
|
|
196
|
+
expression: exprToken.value
|
|
197
|
+
};
|
|
198
|
+
} else {
|
|
199
|
+
if (this.isAtEnd() || this.peek().type !== "PATH") throw new Error(`Expected path after 'on' at line ${onToken.line}, column ${onToken.column}`);
|
|
200
|
+
const pathToken = this.advance();
|
|
201
|
+
if (this.isAtEnd() || this.peek().type !== "COLON") throw new Error(`Expected ':event' after trigger path at line ${pathToken.line}, column ${pathToken.column}`);
|
|
202
|
+
this.advance();
|
|
203
|
+
if (this.isAtEnd() || this.peek().type !== "IDENTIFIER") throw new Error(`Expected event name after ':' at line ${pathToken.line}, column ${pathToken.column}`);
|
|
204
|
+
const eventToken = this.advance();
|
|
205
|
+
trigger = {
|
|
206
|
+
kind: "event",
|
|
207
|
+
path: pathToken.value,
|
|
208
|
+
event: eventToken.value
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
this.skipNewlines();
|
|
212
|
+
}
|
|
213
|
+
if (this.isAtEnd() || this.peek().type !== "LBRACE") {
|
|
214
|
+
const t = this.isAtEnd() ? nameToken : this.peek();
|
|
215
|
+
throw new Error(`Expected '{' after job name at line ${t.line}, column ${t.column}`);
|
|
216
|
+
}
|
|
217
|
+
this.advance();
|
|
218
|
+
this.skipNewlines();
|
|
219
|
+
const pipeline = this.parsePipeline();
|
|
220
|
+
this.skipNewlines();
|
|
221
|
+
if (this.isAtEnd() || this.peek().type !== "RBRACE") throw new Error("Expected '}' to close job body");
|
|
222
|
+
this.advance();
|
|
223
|
+
return {
|
|
224
|
+
kind: "job",
|
|
225
|
+
name: nameToken.value,
|
|
226
|
+
annotations,
|
|
227
|
+
pipeline,
|
|
228
|
+
trigger
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
parsePipeline() {
|
|
232
|
+
const stages = [];
|
|
233
|
+
while (!this.isAtEnd() && this.peek().type !== "RBRACE") {
|
|
234
|
+
if (this.peek().type === "PIPE") {
|
|
235
|
+
if (stages.length === 0) {
|
|
236
|
+
const t = this.peek();
|
|
237
|
+
throw new Error(`Unexpected PIPE at line ${t.line}, column ${t.column}`);
|
|
238
|
+
}
|
|
239
|
+
this.advance();
|
|
240
|
+
this.skipNewlines();
|
|
241
|
+
if (this.isAtEnd() || this.peek().type === "RBRACE") throw new Error("Unexpected end after pipe operator");
|
|
242
|
+
}
|
|
243
|
+
this.skipNewlines();
|
|
244
|
+
if (this.isAtEnd() || this.peek().type === "RBRACE") break;
|
|
245
|
+
const stage = this.parseStage();
|
|
246
|
+
stages.push(stage);
|
|
247
|
+
this.skipNewlines();
|
|
248
|
+
}
|
|
249
|
+
return stages;
|
|
250
|
+
}
|
|
251
|
+
parseStage() {
|
|
252
|
+
const t = this.peek();
|
|
253
|
+
switch (t.type) {
|
|
254
|
+
case "FIND": {
|
|
255
|
+
this.advance();
|
|
256
|
+
const path = this.expect("PATH", "Expected path after 'find'");
|
|
257
|
+
let query;
|
|
258
|
+
if (!this.isAtEnd() && this.peek().type === "WHERE") {
|
|
259
|
+
this.advance();
|
|
260
|
+
const where = this.parseWhere();
|
|
261
|
+
query = {
|
|
262
|
+
field: where.left,
|
|
263
|
+
op: where.op,
|
|
264
|
+
value: where.right
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
kind: "find",
|
|
269
|
+
path: path.value,
|
|
270
|
+
query
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
case "WHERE":
|
|
274
|
+
this.advance();
|
|
275
|
+
return this.parseWhere();
|
|
276
|
+
case "MAP":
|
|
277
|
+
this.advance();
|
|
278
|
+
return this.parseMapBody();
|
|
279
|
+
case "SAVE":
|
|
280
|
+
this.advance();
|
|
281
|
+
return {
|
|
282
|
+
kind: "save",
|
|
283
|
+
path: this.expect("PATH", "Expected path after 'save'").value
|
|
284
|
+
};
|
|
285
|
+
case "PUBLISH":
|
|
286
|
+
this.advance();
|
|
287
|
+
return {
|
|
288
|
+
kind: "publish",
|
|
289
|
+
path: this.expect("PATH", "Expected path after 'publish'").value
|
|
290
|
+
};
|
|
291
|
+
case "TEE":
|
|
292
|
+
this.advance();
|
|
293
|
+
return {
|
|
294
|
+
kind: "tee",
|
|
295
|
+
path: this.expect("PATH", "Expected path after 'tee'").value
|
|
296
|
+
};
|
|
297
|
+
case "FANOUT":
|
|
298
|
+
this.advance();
|
|
299
|
+
return this.parseFanout();
|
|
300
|
+
case "OUTPUT": {
|
|
301
|
+
this.advance();
|
|
302
|
+
const expr = this.parseExpression();
|
|
303
|
+
if (expr.kind === "literal" && typeof expr.value === "string") return {
|
|
304
|
+
kind: "output",
|
|
305
|
+
message: expr.value
|
|
306
|
+
};
|
|
307
|
+
return {
|
|
308
|
+
kind: "output",
|
|
309
|
+
message: "",
|
|
310
|
+
expression: expr
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
case "INPUT":
|
|
314
|
+
this.advance();
|
|
315
|
+
return {
|
|
316
|
+
kind: "input",
|
|
317
|
+
prompt: this.expect("STRING", "Expected string after 'input'").value
|
|
318
|
+
};
|
|
319
|
+
case "COUNT":
|
|
320
|
+
this.advance();
|
|
321
|
+
return { kind: "count" };
|
|
322
|
+
case "GROUP_BY":
|
|
323
|
+
this.advance();
|
|
324
|
+
if (this.isAtEnd() || this.peek().type === "PIPE" || this.peek().type === "RBRACE" || this.peek().type === "NEWLINE") throw new Error("Expected field name after 'group-by'");
|
|
325
|
+
return {
|
|
326
|
+
kind: "group-by",
|
|
327
|
+
field: this.parseFieldAccess()
|
|
328
|
+
};
|
|
329
|
+
case "ACTION": return this.parseAction();
|
|
330
|
+
case "ROUTE": return this.parseRoute();
|
|
331
|
+
case "LOOKUP": return this.parseLookup();
|
|
332
|
+
default: throw new Error(`Unexpected token ${t.type} "${t.value}" at line ${t.line}, column ${t.column}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
parseWhere() {
|
|
336
|
+
const left = this.parseFieldAccess();
|
|
337
|
+
const opToken = this.advance();
|
|
338
|
+
const op = {
|
|
339
|
+
"==": "==",
|
|
340
|
+
"!=": "!=",
|
|
341
|
+
">": ">",
|
|
342
|
+
"<": "<",
|
|
343
|
+
">=": ">=",
|
|
344
|
+
"<=": "<="
|
|
345
|
+
}[opToken.value];
|
|
346
|
+
if (!op) throw new Error(`Expected comparison operator, got ${opToken.type} at line ${opToken.line}, column ${opToken.column}`);
|
|
347
|
+
let right;
|
|
348
|
+
if (!this.isAtEnd() && this.peek().type === "DOLLAR") {
|
|
349
|
+
this.advance();
|
|
350
|
+
right = "$" + this.advance().value;
|
|
351
|
+
} else {
|
|
352
|
+
const rightToken = this.advance();
|
|
353
|
+
if (rightToken.type === "NUMBER") right = parseFloat(rightToken.value);
|
|
354
|
+
else if (rightToken.type === "STRING") right = rightToken.value;
|
|
355
|
+
else if (rightToken.type === "IDENTIFIER" || rightToken.type === "PATH") {
|
|
356
|
+
let val = rightToken.value;
|
|
357
|
+
while (!this.isAtEnd() && this.peek().type === "DOT") {
|
|
358
|
+
this.advance();
|
|
359
|
+
const next = this.advance();
|
|
360
|
+
val += "." + next.value;
|
|
361
|
+
}
|
|
362
|
+
right = val;
|
|
363
|
+
} else right = rightToken.value;
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
kind: "where",
|
|
367
|
+
left,
|
|
368
|
+
op,
|
|
369
|
+
right
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
parseFieldAccess() {
|
|
373
|
+
let field = this.advance().value;
|
|
374
|
+
while (!this.isAtEnd() && this.peek().type === "DOT") {
|
|
375
|
+
this.advance();
|
|
376
|
+
const next = this.advance();
|
|
377
|
+
field += "." + next.value;
|
|
378
|
+
}
|
|
379
|
+
return field;
|
|
380
|
+
}
|
|
381
|
+
parseMapBody() {
|
|
382
|
+
if (this.isAtEnd() || this.peek().type === "PIPE" || this.peek().type === "RBRACE" || this.peek().type === "NEWLINE") throw new Error("Expected field or expression after 'map'");
|
|
383
|
+
if (this.peek().type !== "LBRACE") {
|
|
384
|
+
const expr = this.parseExpression();
|
|
385
|
+
if (expr.kind === "field_access") return {
|
|
386
|
+
kind: "map",
|
|
387
|
+
field: expr.path
|
|
388
|
+
};
|
|
389
|
+
return {
|
|
390
|
+
kind: "map",
|
|
391
|
+
field: "",
|
|
392
|
+
expression: expr
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
this.advance();
|
|
396
|
+
this.skipNewlines();
|
|
397
|
+
if (this.isAtEnd() || this.peek().type === "RBRACE") {
|
|
398
|
+
this.advance();
|
|
399
|
+
return {
|
|
400
|
+
kind: "map",
|
|
401
|
+
field: ""
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
const savedPos = this.pos;
|
|
405
|
+
this.advance();
|
|
406
|
+
const hasColon = !this.isAtEnd() && this.peek().type === "COLON";
|
|
407
|
+
this.pos = savedPos;
|
|
408
|
+
if (hasColon) {
|
|
409
|
+
const exprMappings = Object.create(null);
|
|
410
|
+
const seenExprKeys = /* @__PURE__ */ new Set();
|
|
411
|
+
while (!this.isAtEnd() && this.peek().type !== "RBRACE") {
|
|
412
|
+
const keyToken = this.expect("IDENTIFIER", "Expected key in map expression");
|
|
413
|
+
if (seenExprKeys.has(keyToken.value)) throw new Error(`Duplicate map key '${keyToken.value}' at line ${keyToken.line}, column ${keyToken.column}`);
|
|
414
|
+
seenExprKeys.add(keyToken.value);
|
|
415
|
+
this.expect("COLON", "Expected ':' after key in map expression");
|
|
416
|
+
const expr = this.parseExpression();
|
|
417
|
+
exprMappings[keyToken.value] = expr;
|
|
418
|
+
this.skipNewlines();
|
|
419
|
+
if (!this.isAtEnd() && this.peek().type === "COMMA") {
|
|
420
|
+
this.advance();
|
|
421
|
+
this.skipNewlines();
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (this.isAtEnd()) throw new Error("Expected '}' to close map expression");
|
|
425
|
+
this.advance();
|
|
426
|
+
return {
|
|
427
|
+
kind: "map",
|
|
428
|
+
field: "",
|
|
429
|
+
exprMappings
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
const mappings = Object.create(null);
|
|
433
|
+
const seenMapKeys = /* @__PURE__ */ new Set();
|
|
434
|
+
while (!this.isAtEnd() && this.peek().type !== "RBRACE") {
|
|
435
|
+
const key = this.advance();
|
|
436
|
+
if (seenMapKeys.has(key.value)) throw new Error(`Duplicate map key '${key.value}' at line ${key.line}, column ${key.column}`);
|
|
437
|
+
seenMapKeys.add(key.value);
|
|
438
|
+
const field = this.advance();
|
|
439
|
+
mappings[key.value] = field.value;
|
|
440
|
+
this.skipNewlines();
|
|
441
|
+
if (!this.isAtEnd() && this.peek().type === "COMMA") {
|
|
442
|
+
this.advance();
|
|
443
|
+
this.skipNewlines();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (this.isAtEnd()) throw new Error("Expected '}' to close map object");
|
|
447
|
+
this.advance();
|
|
448
|
+
return {
|
|
449
|
+
kind: "map",
|
|
450
|
+
field: Object.keys(mappings)[0] ?? "",
|
|
451
|
+
mappings
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
parseExpression() {
|
|
455
|
+
let left = this.parseTerm();
|
|
456
|
+
while (!this.isAtEnd() && (this.peek().type === "PLUS" || this.peek().type === "MINUS")) {
|
|
457
|
+
const opToken = this.advance();
|
|
458
|
+
const right = this.parseTerm();
|
|
459
|
+
left = {
|
|
460
|
+
kind: "binary",
|
|
461
|
+
op: opToken.value,
|
|
462
|
+
left,
|
|
463
|
+
right
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
return left;
|
|
467
|
+
}
|
|
468
|
+
parseTerm() {
|
|
469
|
+
let left = this.parseFactor();
|
|
470
|
+
while (!this.isAtEnd() && (this.peek().type === "STAR" || this.peek().type === "SLASH")) {
|
|
471
|
+
const opToken = this.advance();
|
|
472
|
+
const right = this.parseFactor();
|
|
473
|
+
left = {
|
|
474
|
+
kind: "binary",
|
|
475
|
+
op: opToken.value,
|
|
476
|
+
left,
|
|
477
|
+
right
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
return left;
|
|
481
|
+
}
|
|
482
|
+
parseFactor() {
|
|
483
|
+
if (this.isAtEnd()) throw new Error("Expected expression");
|
|
484
|
+
const t = this.peek();
|
|
485
|
+
if (t.type === "LPAREN") {
|
|
486
|
+
this.advance();
|
|
487
|
+
const expr = this.parseExpression();
|
|
488
|
+
this.expect("RPAREN", "Expected ')' after expression");
|
|
489
|
+
return expr;
|
|
490
|
+
}
|
|
491
|
+
if (t.type === "DOLLAR") {
|
|
492
|
+
this.advance();
|
|
493
|
+
return {
|
|
494
|
+
kind: "var_ref",
|
|
495
|
+
name: this.expect("IDENTIFIER", "Expected variable name after '$'").value
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
if (t.type === "STRING") {
|
|
499
|
+
this.advance();
|
|
500
|
+
return {
|
|
501
|
+
kind: "literal",
|
|
502
|
+
value: t.value
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
if (t.type === "NUMBER") {
|
|
506
|
+
this.advance();
|
|
507
|
+
return {
|
|
508
|
+
kind: "literal",
|
|
509
|
+
value: parseFloat(t.value)
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
if (t.type === "IDENTIFIER") {
|
|
513
|
+
let field = this.advance().value;
|
|
514
|
+
while (!this.isAtEnd() && this.peek().type === "DOT") {
|
|
515
|
+
this.advance();
|
|
516
|
+
const next = this.advance();
|
|
517
|
+
field += "." + next.value;
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
kind: "field_access",
|
|
521
|
+
path: field
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
throw new Error(`Unexpected token in expression: ${t.type} "${t.value}" at line ${t.line}, column ${t.column}`);
|
|
525
|
+
}
|
|
526
|
+
parseAction() {
|
|
527
|
+
const actionToken = this.advance();
|
|
528
|
+
if (this.isAtEnd()) throw new Error(`Expected path or action name after 'action' at line ${actionToken.line}, column ${actionToken.column}`);
|
|
529
|
+
const nextToken = this.peek();
|
|
530
|
+
let path;
|
|
531
|
+
let relative;
|
|
532
|
+
if (nextToken.type === "PATH") path = this.advance().value;
|
|
533
|
+
else if (nextToken.type === "IDENTIFIER") {
|
|
534
|
+
path = this.advance().value;
|
|
535
|
+
relative = true;
|
|
536
|
+
} else throw new Error(`Expected path or action name after 'action' at line ${actionToken.line}, column ${actionToken.column}`);
|
|
537
|
+
let params;
|
|
538
|
+
if (!this.isAtEnd() && this.peek().type === "LBRACE") {
|
|
539
|
+
this.advance();
|
|
540
|
+
params = {};
|
|
541
|
+
this.skipNewlines();
|
|
542
|
+
while (!this.isAtEnd() && this.peek().type !== "RBRACE") {
|
|
543
|
+
const keyToken = this.expect("IDENTIFIER", "Expected parameter name in action params");
|
|
544
|
+
this.expect("COLON", "Expected ':' after parameter name");
|
|
545
|
+
if (this.isAtEnd()) throw new Error(`Expected value after ':' at line ${keyToken.line}, column ${keyToken.column}`);
|
|
546
|
+
if (this.peek().type === "IDENTIFIER" && this.peek().value === "json") {
|
|
547
|
+
const jsonToken = this.advance();
|
|
548
|
+
this.expect("LPAREN", `Expected '(' after json at line ${jsonToken.line}, column ${jsonToken.column}`);
|
|
549
|
+
params[keyToken.value] = this.parseJsonValue();
|
|
550
|
+
this.expect("RPAREN", "Expected ')' to close json()");
|
|
551
|
+
} else {
|
|
552
|
+
const valToken = this.advance();
|
|
553
|
+
if (valToken.type === "STRING") params[keyToken.value] = valToken.value;
|
|
554
|
+
else if (valToken.type === "NUMBER") params[keyToken.value] = parseFloat(valToken.value);
|
|
555
|
+
else throw new Error(`Expected string or number value for parameter '${keyToken.value}' at line ${valToken.line}, column ${valToken.column}`);
|
|
556
|
+
}
|
|
557
|
+
this.skipNewlines();
|
|
558
|
+
if (!this.isAtEnd() && this.peek().type === "COMMA") {
|
|
559
|
+
this.advance();
|
|
560
|
+
this.skipNewlines();
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (this.isAtEnd()) throw new Error("Expected '}' to close action params");
|
|
564
|
+
this.advance();
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
kind: "action",
|
|
568
|
+
path,
|
|
569
|
+
relative,
|
|
570
|
+
params
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
parseRoute() {
|
|
574
|
+
const routeToken = this.advance();
|
|
575
|
+
if (this.isAtEnd() || this.peek().type !== "IDENTIFIER" && this.peek().type !== "DOT") throw new Error(`Expected field name after 'route' at line ${routeToken.line}, column ${routeToken.column}`);
|
|
576
|
+
const field = this.parseFieldAccess();
|
|
577
|
+
this.expect("LBRACE", "Expected '{' after route field");
|
|
578
|
+
this.skipNewlines();
|
|
579
|
+
const branches = [];
|
|
580
|
+
let fallback;
|
|
581
|
+
while (!this.isAtEnd() && this.peek().type !== "RBRACE") {
|
|
582
|
+
let value;
|
|
583
|
+
if (this.peek().type === "STRING") value = this.advance().value;
|
|
584
|
+
else if (this.peek().type === "IDENTIFIER" && this.peek().value === "_") {
|
|
585
|
+
value = "_";
|
|
586
|
+
this.advance();
|
|
587
|
+
} else throw new Error(`Expected string value or '_' in route branch at line ${this.peek().line}, column ${this.peek().column}`);
|
|
588
|
+
this.expect("ARROW", "Expected '->' in route branch");
|
|
589
|
+
this.expect("JOB", "Expected 'job' keyword in route branch target");
|
|
590
|
+
const targetToken = this.advance();
|
|
591
|
+
if (value === "_") fallback = targetToken.value;
|
|
592
|
+
else branches.push({
|
|
593
|
+
value,
|
|
594
|
+
targetJob: targetToken.value
|
|
595
|
+
});
|
|
596
|
+
this.skipNewlines();
|
|
597
|
+
if (!this.isAtEnd() && this.peek().type === "COMMA") {
|
|
598
|
+
this.advance();
|
|
599
|
+
this.skipNewlines();
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (branches.length === 0 && !fallback) throw new Error(`Route must have at least one branch at line ${routeToken.line}, column ${routeToken.column}`);
|
|
603
|
+
if (this.isAtEnd()) throw new Error("Expected '}' to close route");
|
|
604
|
+
this.advance();
|
|
605
|
+
return {
|
|
606
|
+
kind: "route",
|
|
607
|
+
field,
|
|
608
|
+
branches,
|
|
609
|
+
fallback
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
parseLookup() {
|
|
613
|
+
const lookupToken = this.advance();
|
|
614
|
+
if (this.isAtEnd() || this.peek().type !== "PATH") throw new Error(`Expected path after 'lookup' at line ${lookupToken.line}, column ${lookupToken.column}`);
|
|
615
|
+
const pathToken = this.advance();
|
|
616
|
+
if (this.isAtEnd() || this.peek().type !== "ON") throw new Error(`Expected 'on' keyword after lookup path at line ${lookupToken.line}, column ${lookupToken.column}`);
|
|
617
|
+
this.advance();
|
|
618
|
+
const joinKey = this.parseFieldAccess();
|
|
619
|
+
return {
|
|
620
|
+
kind: "lookup",
|
|
621
|
+
path: pathToken.value,
|
|
622
|
+
joinKey
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
parseFanout() {
|
|
626
|
+
this.expect("LBRACE", "Expected '{' after 'fanout'");
|
|
627
|
+
const branches = [];
|
|
628
|
+
this.skipNewlines();
|
|
629
|
+
while (!this.isAtEnd() && this.peek().type !== "RBRACE") {
|
|
630
|
+
const branch = [];
|
|
631
|
+
while (!this.isAtEnd() && this.peek().type !== "COMMA" && this.peek().type !== "RBRACE" && this.peek().type !== "NEWLINE") {
|
|
632
|
+
if (this.peek().type === "PIPE") this.advance();
|
|
633
|
+
branch.push(this.parseStage());
|
|
634
|
+
}
|
|
635
|
+
branches.push(branch);
|
|
636
|
+
this.skipNewlines();
|
|
637
|
+
if (!this.isAtEnd() && this.peek().type === "COMMA") {
|
|
638
|
+
this.advance();
|
|
639
|
+
this.skipNewlines();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (this.isAtEnd()) throw new Error("Expected '}' to close fanout");
|
|
643
|
+
this.advance();
|
|
644
|
+
return {
|
|
645
|
+
kind: "fanout",
|
|
646
|
+
branches
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
/** Recursive descent JSON value parser using existing lexer tokens. */
|
|
650
|
+
parseJsonValue() {
|
|
651
|
+
if (this.isAtEnd()) throw new Error("Expected JSON value (unexpected EOF)");
|
|
652
|
+
const t = this.peek();
|
|
653
|
+
if (t.type === "LBRACE") {
|
|
654
|
+
this.advance();
|
|
655
|
+
const obj = {};
|
|
656
|
+
this.skipNewlines();
|
|
657
|
+
while (!this.isAtEnd() && this.peek().type !== "RBRACE") {
|
|
658
|
+
const keyToken = this.advance();
|
|
659
|
+
if (keyToken.type !== "STRING" && keyToken.type !== "IDENTIFIER") throw new Error(`Expected string key in JSON object at line ${keyToken.line}, column ${keyToken.column}`);
|
|
660
|
+
this.expect("COLON", "Expected ':' after key in JSON object");
|
|
661
|
+
obj[keyToken.value] = this.parseJsonValue();
|
|
662
|
+
this.skipNewlines();
|
|
663
|
+
if (!this.isAtEnd() && this.peek().type === "COMMA") {
|
|
664
|
+
this.advance();
|
|
665
|
+
this.skipNewlines();
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (this.isAtEnd()) throw new Error("Expected '}' to close JSON object");
|
|
669
|
+
this.advance();
|
|
670
|
+
return obj;
|
|
671
|
+
}
|
|
672
|
+
if (t.type === "LBRACKET") {
|
|
673
|
+
this.advance();
|
|
674
|
+
const arr = [];
|
|
675
|
+
this.skipNewlines();
|
|
676
|
+
while (!this.isAtEnd() && this.peek().type !== "RBRACKET") {
|
|
677
|
+
arr.push(this.parseJsonValue());
|
|
678
|
+
this.skipNewlines();
|
|
679
|
+
if (!this.isAtEnd() && this.peek().type === "COMMA") {
|
|
680
|
+
this.advance();
|
|
681
|
+
this.skipNewlines();
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (this.isAtEnd()) throw new Error("Expected ']' to close JSON array");
|
|
685
|
+
this.advance();
|
|
686
|
+
return arr;
|
|
687
|
+
}
|
|
688
|
+
if (t.type === "STRING") {
|
|
689
|
+
this.advance();
|
|
690
|
+
return t.value;
|
|
691
|
+
}
|
|
692
|
+
if (t.type === "NUMBER") {
|
|
693
|
+
this.advance();
|
|
694
|
+
return parseFloat(t.value);
|
|
695
|
+
}
|
|
696
|
+
if (t.type === "IDENTIFIER") {
|
|
697
|
+
if (t.value === "true") {
|
|
698
|
+
this.advance();
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
if (t.value === "false") {
|
|
702
|
+
this.advance();
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
if (t.value === "null") {
|
|
706
|
+
this.advance();
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
throw new Error(`Unexpected token in json(): ${t.type} "${t.value}" at line ${t.line}, column ${t.column}`);
|
|
711
|
+
}
|
|
712
|
+
peek() {
|
|
713
|
+
return this.tokens[this.pos];
|
|
714
|
+
}
|
|
715
|
+
advance() {
|
|
716
|
+
return this.tokens[this.pos++];
|
|
717
|
+
}
|
|
718
|
+
expect(type, message) {
|
|
719
|
+
if (this.isAtEnd()) throw new Error(message + " (unexpected EOF)");
|
|
720
|
+
const t = this.peek();
|
|
721
|
+
if (t.type !== type) throw new Error(`${message}, got ${t.type} at line ${t.line}, column ${t.column}`);
|
|
722
|
+
return this.advance();
|
|
723
|
+
}
|
|
724
|
+
isAtEnd() {
|
|
725
|
+
return this.pos >= this.tokens.length || this.tokens[this.pos].type === "EOF";
|
|
726
|
+
}
|
|
727
|
+
skipNewlines() {
|
|
728
|
+
while (!this.isAtEnd() && this.peek().type === "NEWLINE") this.pos++;
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
//#endregion
|
|
733
|
+
export { AshParser };
|
|
734
|
+
//# sourceMappingURL=parser.mjs.map
|