@hatchingpoint/point 0.0.3 → 0.0.6
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/README.md +18 -5
- package/package.json +1 -1
- package/src/core/ast.ts +46 -1
- package/src/core/check.ts +217 -69
- package/src/core/cli.ts +332 -39
- package/src/core/context.ts +213 -36
- package/src/core/emit-javascript.ts +124 -0
- package/src/core/emit-typescript.ts +39 -5
- package/src/core/format.ts +4 -101
- package/src/core/incremental.ts +53 -0
- package/src/core/index.ts +5 -0
- package/src/core/lexer.ts +15 -5
- package/src/core/parser.ts +11 -362
- package/src/core/semantic-source.ts +26 -0
- package/src/core/serialize.ts +18 -0
- package/src/core/test-only/core-text-parser.ts +415 -0
- package/src/core/test-only/format-core.ts +120 -0
- package/src/core/test-only/index.ts +3 -0
- package/src/core/test-only/legacy-lowering.ts +1047 -0
- package/src/semantic/ast.ts +230 -0
- package/src/semantic/callables.ts +51 -0
- package/src/semantic/context.ts +347 -0
- package/src/semantic/desugar.ts +665 -0
- package/src/semantic/expressions.ts +347 -0
- package/src/semantic/format.ts +222 -0
- package/src/semantic/index.ts +10 -0
- package/src/semantic/metadata.ts +37 -0
- package/src/semantic/naming.ts +33 -0
- package/src/semantic/parse.ts +945 -0
- package/src/semantic/serialize.ts +18 -0
|
@@ -0,0 +1,945 @@
|
|
|
1
|
+
import type { PointSourceSpan } from "../core/ast.ts";
|
|
2
|
+
import type {
|
|
3
|
+
PointSemanticActionDeclaration,
|
|
4
|
+
PointSemanticActionStatement,
|
|
5
|
+
PointSemanticCalculationDeclaration,
|
|
6
|
+
PointSemanticCalculationStatement,
|
|
7
|
+
PointSemanticCommandDeclaration,
|
|
8
|
+
PointSemanticCommandStatement,
|
|
9
|
+
PointSemanticDeclaration,
|
|
10
|
+
PointSemanticExternalDeclaration,
|
|
11
|
+
PointSemanticExternalFunction,
|
|
12
|
+
PointSemanticField,
|
|
13
|
+
PointSemanticLabelDeclaration,
|
|
14
|
+
PointSemanticLabelStatement,
|
|
15
|
+
PointSemanticMutationStatement,
|
|
16
|
+
PointSemanticOutputBinding,
|
|
17
|
+
PointSemanticPolicyDeclaration,
|
|
18
|
+
PointSemanticPolicyStatement,
|
|
19
|
+
PointSemanticProgram,
|
|
20
|
+
PointSemanticRecordDeclaration,
|
|
21
|
+
PointSemanticRouteDeclaration,
|
|
22
|
+
PointSemanticRouteStatement,
|
|
23
|
+
PointSemanticRuleDeclaration,
|
|
24
|
+
PointSemanticRuleStatement,
|
|
25
|
+
PointSemanticUseDeclaration,
|
|
26
|
+
PointSemanticViewDeclaration,
|
|
27
|
+
PointSemanticViewStatement,
|
|
28
|
+
PointSemanticWorkflowDeclaration,
|
|
29
|
+
PointSemanticWorkflowStatement,
|
|
30
|
+
PointSemanticBinding,
|
|
31
|
+
} from "./ast.ts";
|
|
32
|
+
import {
|
|
33
|
+
buildExpressionContext,
|
|
34
|
+
lineSpan,
|
|
35
|
+
parseSemanticExpression,
|
|
36
|
+
parseSemanticTypeExpression,
|
|
37
|
+
} from "./expressions.ts";
|
|
38
|
+
import { collectSemanticCallables } from "./callables.ts";
|
|
39
|
+
|
|
40
|
+
function parseLineExpression(
|
|
41
|
+
expressionSource: string,
|
|
42
|
+
context: ReturnType<typeof buildExpressionContext>,
|
|
43
|
+
fileSource: string,
|
|
44
|
+
lineNumber: number,
|
|
45
|
+
) {
|
|
46
|
+
return parseSemanticExpression(expressionSource, context, lineSpan(fileSource, lineNumber));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isPointSemanticAstEnabled(): boolean {
|
|
50
|
+
return process.env.POINT_LEGACY_LOWER !== "1";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Parse semantic `.point` source to semantic AST. */
|
|
54
|
+
export function parsePointSourceV2(source: string): PointSemanticProgram {
|
|
55
|
+
return parseSemanticSource(source);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function parseSemanticSource(source: string): PointSemanticProgram {
|
|
59
|
+
const lines = source.split(/\r?\n/);
|
|
60
|
+
const records = new Map<string, Map<string, string>>();
|
|
61
|
+
const callables = collectSemanticCallables(source);
|
|
62
|
+
const uses: PointSemanticUseDeclaration[] = [];
|
|
63
|
+
const declarations: PointSemanticDeclaration[] = [];
|
|
64
|
+
let moduleName: string | undefined;
|
|
65
|
+
let index = 0;
|
|
66
|
+
|
|
67
|
+
while (index < lines.length) {
|
|
68
|
+
const lineNumber = index + 1;
|
|
69
|
+
const trimmed = (lines[index] ?? "").trim();
|
|
70
|
+
if (!trimmed) {
|
|
71
|
+
index += 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (trimmed.startsWith("module ")) {
|
|
75
|
+
moduleName = trimmed.slice("module ".length).trim();
|
|
76
|
+
index += 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (trimmed.startsWith("use ")) {
|
|
80
|
+
uses.push(parseUseDeclaration(trimmed, lineNumber));
|
|
81
|
+
index += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (trimmed.startsWith("record ")) {
|
|
85
|
+
const parsed = parseRecord(lines, index, source, records);
|
|
86
|
+
declarations.push(parsed.declaration);
|
|
87
|
+
index = parsed.next;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (trimmed.startsWith("calculation ")) {
|
|
91
|
+
const parsed = parseCalculation(lines, index, source, records, callables);
|
|
92
|
+
declarations.push(parsed.declaration);
|
|
93
|
+
index = parsed.next;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (trimmed.startsWith("rule ")) {
|
|
97
|
+
const parsed = parseRule(lines, index, source, records, callables);
|
|
98
|
+
declarations.push(parsed.declaration);
|
|
99
|
+
index = parsed.next;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (trimmed.startsWith("label ")) {
|
|
103
|
+
const parsed = parseLabel(lines, index, source, records, callables);
|
|
104
|
+
declarations.push(parsed.declaration);
|
|
105
|
+
index = parsed.next;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (trimmed.startsWith("external ")) {
|
|
109
|
+
const parsed = parseExternal(lines, index, source);
|
|
110
|
+
declarations.push(parsed.declaration);
|
|
111
|
+
index = parsed.next;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (trimmed.startsWith("action ")) {
|
|
115
|
+
const parsed = parseAction(lines, index, source, records, callables);
|
|
116
|
+
declarations.push(parsed.declaration);
|
|
117
|
+
index = parsed.next;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (trimmed.startsWith("policy ")) {
|
|
121
|
+
const parsed = parsePolicy(lines, index, source, records, callables);
|
|
122
|
+
declarations.push(parsed.declaration);
|
|
123
|
+
index = parsed.next;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (trimmed.startsWith("view ")) {
|
|
127
|
+
const parsed = parseView(lines, index, source, records, callables);
|
|
128
|
+
declarations.push(parsed.declaration);
|
|
129
|
+
index = parsed.next;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (trimmed.startsWith("route ")) {
|
|
133
|
+
const parsed = parseRoute(lines, index, source, records, callables);
|
|
134
|
+
declarations.push(parsed.declaration);
|
|
135
|
+
index = parsed.next;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (trimmed.startsWith("workflow ")) {
|
|
139
|
+
const parsed = parseWorkflow(lines, index, source, records, callables);
|
|
140
|
+
declarations.push(parsed.declaration);
|
|
141
|
+
index = parsed.next;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (trimmed.startsWith("command ")) {
|
|
145
|
+
const parsed = parseCommand(lines, index, source, records, callables);
|
|
146
|
+
declarations.push(parsed.declaration);
|
|
147
|
+
index = parsed.next;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
throw new Error(`Unknown semantic top-level declaration at ${lineNumber}: ${trimmed}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
kind: "semanticProgram",
|
|
155
|
+
module: moduleName,
|
|
156
|
+
uses,
|
|
157
|
+
declarations,
|
|
158
|
+
span: lineSpan(source, 1),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function parseUseDeclaration(line: string, lineNumber: number): PointSemanticUseDeclaration {
|
|
163
|
+
const match = line.match(/^use\s+([A-Za-z][A-Za-z0-9]*(?:\.[A-Za-z][A-Za-z0-9]*)*)(?:\s+from\s+"([^"]+)")?$/);
|
|
164
|
+
if (!match) throw new Error(`Invalid use declaration: ${line}`);
|
|
165
|
+
return {
|
|
166
|
+
kind: "use",
|
|
167
|
+
moduleName: match[1] ?? "",
|
|
168
|
+
from: match[2],
|
|
169
|
+
span: lineSpanFromLine(lineNumber, line),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function parseRecord(
|
|
174
|
+
lines: string[],
|
|
175
|
+
start: number,
|
|
176
|
+
source: string,
|
|
177
|
+
records: Map<string, Map<string, string>>,
|
|
178
|
+
): { declaration: PointSemanticRecordDeclaration; next: number } {
|
|
179
|
+
const lineNumber = start + 1;
|
|
180
|
+
const name = (lines[start] ?? "").trim().slice("record ".length).trim();
|
|
181
|
+
const fields: PointSemanticField[] = [];
|
|
182
|
+
const fieldMap = new Map<string, string>();
|
|
183
|
+
let index = start + 1;
|
|
184
|
+
for (; index < lines.length; index += 1) {
|
|
185
|
+
const trimmed = (lines[index] ?? "").trim();
|
|
186
|
+
if (!trimmed) continue;
|
|
187
|
+
if (isSemanticTopLevel(trimmed)) break;
|
|
188
|
+
const colon = trimmed.indexOf(":");
|
|
189
|
+
if (colon === -1) throw new Error(`Expected field type in record ${name}: ${trimmed}`);
|
|
190
|
+
const label = trimmed.slice(0, colon).trim();
|
|
191
|
+
fields.push({
|
|
192
|
+
label,
|
|
193
|
+
type: parseSemanticTypeExpression(trimmed.slice(colon + 1).trim()),
|
|
194
|
+
span: lineSpan(source, index + 1),
|
|
195
|
+
});
|
|
196
|
+
fieldMap.set(label, toIdentifier(label));
|
|
197
|
+
}
|
|
198
|
+
records.set(toPascalCase(name), fieldMap);
|
|
199
|
+
return {
|
|
200
|
+
declaration: { kind: "record", name, fields, span: lineSpan(source, lineNumber) },
|
|
201
|
+
next: index,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function parseCalculation(
|
|
206
|
+
lines: string[],
|
|
207
|
+
start: number,
|
|
208
|
+
source: string,
|
|
209
|
+
records: Map<string, Map<string, string>>,
|
|
210
|
+
callables: string[],
|
|
211
|
+
): { declaration: PointSemanticCalculationDeclaration; next: number } {
|
|
212
|
+
const name = (lines[start] ?? "").trim().slice("calculation ".length).trim();
|
|
213
|
+
const body = collectSemanticBody(lines, start + 1);
|
|
214
|
+
const inputs: PointSemanticBinding[] = [];
|
|
215
|
+
const paramTypes = new Map<string, string>();
|
|
216
|
+
const bindings: string[] = [];
|
|
217
|
+
let output: PointSemanticOutputBinding = { name: "result", type: { kind: "typeRef", name: "Void", args: [] } };
|
|
218
|
+
const statements: PointSemanticCalculationStatement[] = [];
|
|
219
|
+
|
|
220
|
+
for (let lineIndex = 0; lineIndex < body.lines.length; lineIndex += 1) {
|
|
221
|
+
const line = body.lines[lineIndex] ?? "";
|
|
222
|
+
const lineNumber = body.lineNumbers[lineIndex] ?? start + 2;
|
|
223
|
+
if (line.startsWith("input ")) {
|
|
224
|
+
const binding = parseInputBinding(line.slice("input ".length), source, lineNumber);
|
|
225
|
+
inputs.push(binding);
|
|
226
|
+
paramTypes.set(binding.label, typeLabel(binding.type));
|
|
227
|
+
bindings.push(binding.label);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (line.startsWith("output ")) {
|
|
231
|
+
output = parseOutputBinding(line.slice("output ".length));
|
|
232
|
+
bindings.push(output.name);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const context = buildExpressionContext({ bindings, paramTypes, recordFields: records, callables });
|
|
236
|
+
const loop = line.match(/^for each (.+) in (.+)$/);
|
|
237
|
+
if (loop) {
|
|
238
|
+
const item = loop[1]?.trim() ?? "";
|
|
239
|
+
const iterable = parseLineExpression(loop[2] ?? "", context, source, lineNumber);
|
|
240
|
+
const loopBody: PointSemanticMutationStatement[] = [];
|
|
241
|
+
const loopParamTypes = new Map(paramTypes);
|
|
242
|
+
loopParamTypes.set(item, listItemType(loop[2] ?? "", paramTypes));
|
|
243
|
+
const loopBindings = [...bindings, item];
|
|
244
|
+
while (body.lines[lineIndex + 1] && !isLoopBoundary(body.lines[lineIndex + 1]!)) {
|
|
245
|
+
lineIndex += 1;
|
|
246
|
+
const mutation = parseMutationStatement(
|
|
247
|
+
body.lines[lineIndex]!,
|
|
248
|
+
buildExpressionContext({ bindings: loopBindings, paramTypes: loopParamTypes, recordFields: records, callables }),
|
|
249
|
+
source,
|
|
250
|
+
body.lineNumbers[lineIndex] ?? lineNumber,
|
|
251
|
+
);
|
|
252
|
+
if (mutation) loopBody.push(mutation);
|
|
253
|
+
}
|
|
254
|
+
statements.push({ kind: "forEach", item, iterable, body: loopBody, span: lineSpan(source, lineNumber) });
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const isExpr = line.match(/^(.+) is (.+)$/);
|
|
258
|
+
if (isExpr) {
|
|
259
|
+
statements.push({
|
|
260
|
+
kind: "assignIs",
|
|
261
|
+
name: isExpr[1]?.trim() ?? "",
|
|
262
|
+
value: parseLineExpression(isExpr[2] ?? "", context, source, lineNumber),
|
|
263
|
+
span: lineSpan(source, lineNumber),
|
|
264
|
+
});
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const startsAt = line.match(/^(.+) starts at (.+)$/);
|
|
268
|
+
if (startsAt) {
|
|
269
|
+
statements.push({
|
|
270
|
+
kind: "startsAt",
|
|
271
|
+
name: startsAt[1]?.trim() ?? "",
|
|
272
|
+
value: parseLineExpression(startsAt[2] ?? "", context, source, lineNumber),
|
|
273
|
+
span: lineSpan(source, lineNumber),
|
|
274
|
+
});
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const startsAs = line.match(/^(.+) starts as (.+)$/);
|
|
278
|
+
if (startsAs) {
|
|
279
|
+
statements.push({
|
|
280
|
+
kind: "startsAs",
|
|
281
|
+
name: startsAs[1]?.trim() ?? "",
|
|
282
|
+
value: parseLineExpression(startsAs[2] ?? "", context, source, lineNumber),
|
|
283
|
+
span: lineSpan(source, lineNumber),
|
|
284
|
+
});
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const mutation = parseMutationStatement(line, context, source, lineNumber);
|
|
288
|
+
if (mutation) {
|
|
289
|
+
statements.push(mutation);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (line.startsWith("return ")) {
|
|
293
|
+
statements.push({
|
|
294
|
+
kind: "return",
|
|
295
|
+
value: parseLineExpression(line.slice("return ".length), context, source, lineNumber),
|
|
296
|
+
span: lineSpan(source, lineNumber),
|
|
297
|
+
});
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
throw new Error(`Unknown calculation statement: ${line}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
declaration: {
|
|
305
|
+
kind: "calculation",
|
|
306
|
+
name,
|
|
307
|
+
inputs,
|
|
308
|
+
output,
|
|
309
|
+
body: statements,
|
|
310
|
+
span: lineSpan(source, start + 1),
|
|
311
|
+
},
|
|
312
|
+
next: body.next,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function parseRule(
|
|
317
|
+
lines: string[],
|
|
318
|
+
start: number,
|
|
319
|
+
source: string,
|
|
320
|
+
records: Map<string, Map<string, string>>,
|
|
321
|
+
callables: string[],
|
|
322
|
+
): { declaration: PointSemanticRuleDeclaration; next: number } {
|
|
323
|
+
const name = (lines[start] ?? "").trim().slice("rule ".length).trim();
|
|
324
|
+
const body = collectSemanticBody(lines, start + 1);
|
|
325
|
+
const inputs: PointSemanticBinding[] = [];
|
|
326
|
+
const paramTypes = new Map<string, string>();
|
|
327
|
+
const bindings: string[] = [];
|
|
328
|
+
let output: PointSemanticOutputBinding = { name: "result", type: { kind: "typeRef", name: "Void", args: [] } };
|
|
329
|
+
const statements: PointSemanticRuleStatement[] = [];
|
|
330
|
+
|
|
331
|
+
for (let lineIndex = 0; lineIndex < body.lines.length; lineIndex += 1) {
|
|
332
|
+
const line = body.lines[lineIndex] ?? "";
|
|
333
|
+
const lineNumber = body.lineNumbers[lineIndex] ?? start + 2;
|
|
334
|
+
if (line.startsWith("input ")) {
|
|
335
|
+
const binding = parseInputBinding(line.slice("input ".length), source, lineNumber);
|
|
336
|
+
inputs.push(binding);
|
|
337
|
+
paramTypes.set(binding.label, typeLabel(binding.type));
|
|
338
|
+
bindings.push(binding.label);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (line.startsWith("output ")) {
|
|
342
|
+
output = parseOutputBinding(line.slice("output ".length));
|
|
343
|
+
bindings.push(output.name);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const context = buildExpressionContext({ bindings, paramTypes, recordFields: records, callables });
|
|
347
|
+
const startsAt = line.match(/^(.+) starts at (.+)$/);
|
|
348
|
+
if (startsAt) {
|
|
349
|
+
statements.push({
|
|
350
|
+
kind: "startsAt",
|
|
351
|
+
name: startsAt[1]?.trim() ?? "",
|
|
352
|
+
value: parseLineExpression(startsAt[2] ?? "", context, source, lineNumber),
|
|
353
|
+
span: lineSpan(source, lineNumber),
|
|
354
|
+
});
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const addWhen = line.match(/^add (.+) when (.+)$/);
|
|
358
|
+
if (addWhen) {
|
|
359
|
+
statements.push({
|
|
360
|
+
kind: "addWhen",
|
|
361
|
+
amount: parseLineExpression(addWhen[1] ?? "", context, source, lineNumber),
|
|
362
|
+
condition: parseLineExpression(addWhen[2] ?? "", context, source, lineNumber),
|
|
363
|
+
span: lineSpan(source, lineNumber),
|
|
364
|
+
});
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const loop = line.match(/^for each (.+) in (.+)$/);
|
|
368
|
+
if (loop) {
|
|
369
|
+
const item = loop[1]?.trim() ?? "";
|
|
370
|
+
const iterable = parseLineExpression(loop[2] ?? "", context, source, lineNumber);
|
|
371
|
+
const loopBody: PointSemanticMutationStatement[] = [];
|
|
372
|
+
const loopParamTypes = new Map(paramTypes);
|
|
373
|
+
loopParamTypes.set(item, listItemType(loop[2] ?? "", paramTypes));
|
|
374
|
+
const loopBindings = [...bindings, item];
|
|
375
|
+
while (body.lines[lineIndex + 1] && !isLoopBoundary(body.lines[lineIndex + 1]!)) {
|
|
376
|
+
lineIndex += 1;
|
|
377
|
+
const mutation = parseMutationStatement(body.lines[lineIndex]!, buildExpressionContext({ bindings: loopBindings, paramTypes: loopParamTypes, recordFields: records, callables }), source, body.lineNumbers[lineIndex] ?? lineNumber);
|
|
378
|
+
if (mutation) loopBody.push(mutation);
|
|
379
|
+
}
|
|
380
|
+
statements.push({ kind: "forEach", item, iterable, body: loopBody, span: lineSpan(source, lineNumber) });
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
const mutation = parseMutationStatement(line, context, source, lineNumber);
|
|
384
|
+
if (mutation) {
|
|
385
|
+
statements.push(mutation);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (line.startsWith("return ")) {
|
|
389
|
+
statements.push({
|
|
390
|
+
kind: "return",
|
|
391
|
+
value: parseLineExpression(line.slice("return ".length), context, source, lineNumber),
|
|
392
|
+
span: lineSpan(source, lineNumber),
|
|
393
|
+
});
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
throw new Error(`Unknown rule statement: ${line}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
declaration: { kind: "rule", name, inputs, output, body: statements, span: lineSpan(source, start + 1) },
|
|
401
|
+
next: body.next,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function parseLabel(
|
|
406
|
+
lines: string[],
|
|
407
|
+
start: number,
|
|
408
|
+
source: string,
|
|
409
|
+
records: Map<string, Map<string, string>>,
|
|
410
|
+
callables: string[],
|
|
411
|
+
): { declaration: PointSemanticLabelDeclaration; next: number } {
|
|
412
|
+
const name = (lines[start] ?? "").trim().slice("label ".length).trim();
|
|
413
|
+
const body = collectSemanticBody(lines, start + 1);
|
|
414
|
+
const inputs: PointSemanticBinding[] = [];
|
|
415
|
+
const paramTypes = new Map<string, string>();
|
|
416
|
+
const bindings: string[] = [];
|
|
417
|
+
let output: PointSemanticOutputBinding = { name: "result", type: { kind: "typeRef", name: "Text", args: [] } };
|
|
418
|
+
const statements: PointSemanticLabelStatement[] = [];
|
|
419
|
+
|
|
420
|
+
for (let lineIndex = 0; lineIndex < body.lines.length; lineIndex += 1) {
|
|
421
|
+
const line = body.lines[lineIndex] ?? "";
|
|
422
|
+
const lineNumber = body.lineNumbers[lineIndex] ?? start + 2;
|
|
423
|
+
if (line.startsWith("input ")) {
|
|
424
|
+
const binding = parseInputBinding(line.slice("input ".length), source, lineNumber);
|
|
425
|
+
inputs.push(binding);
|
|
426
|
+
paramTypes.set(binding.label, typeLabel(binding.type));
|
|
427
|
+
bindings.push(binding.label);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (line.startsWith("output ")) {
|
|
431
|
+
output = parseOutputBinding(line.slice("output ".length));
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const context = buildExpressionContext({ bindings, paramTypes, recordFields: records, callables });
|
|
435
|
+
const whenReturn = line.match(/^when (.+) return (.+)$/);
|
|
436
|
+
if (whenReturn) {
|
|
437
|
+
statements.push({
|
|
438
|
+
kind: "whenReturn",
|
|
439
|
+
condition: parseLineExpression(whenReturn[1] ?? "", context, source, lineNumber),
|
|
440
|
+
value: parseLineExpression(whenReturn[2] ?? "", context, source, lineNumber),
|
|
441
|
+
span: lineSpan(source, lineNumber),
|
|
442
|
+
});
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (line.startsWith("otherwise return ")) {
|
|
446
|
+
statements.push({
|
|
447
|
+
kind: "otherwiseReturn",
|
|
448
|
+
value: parseLineExpression(line.slice("otherwise return ".length), context, source, lineNumber),
|
|
449
|
+
span: lineSpan(source, lineNumber),
|
|
450
|
+
});
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
throw new Error(`Unknown label statement: ${line}`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
declaration: { kind: "label", name, inputs, output, body: statements, span: lineSpan(source, start + 1) },
|
|
458
|
+
next: body.next,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function parseExternal(lines: string[], start: number, source: string): { declaration: PointSemanticExternalDeclaration; next: number } {
|
|
463
|
+
const name = (lines[start] ?? "").trim().slice("external ".length).trim();
|
|
464
|
+
const body = collectSemanticBody(lines, start + 1);
|
|
465
|
+
const functions: PointSemanticExternalFunction[] = [];
|
|
466
|
+
for (let lineIndex = 0; lineIndex < body.lines.length; lineIndex += 1) {
|
|
467
|
+
const line = body.lines[lineIndex] ?? "";
|
|
468
|
+
const lineNumber = body.lineNumbers[lineIndex] ?? start + 2;
|
|
469
|
+
const match = line.match(/^(.+)\((.*)\):\s*(.+?)\s+from\s+"([^"]+)"(?:\s+as\s+([A-Za-z_][A-Za-z0-9_]*))?$/);
|
|
470
|
+
if (!match) throw new Error(`Unknown external declaration: ${line}`);
|
|
471
|
+
functions.push({
|
|
472
|
+
label: match[1]?.trim() ?? "",
|
|
473
|
+
params: (match[2] ?? "")
|
|
474
|
+
.split(",")
|
|
475
|
+
.map((part) => part.trim())
|
|
476
|
+
.filter(Boolean)
|
|
477
|
+
.map((part) => parseInputBinding(part, source, lineNumber)),
|
|
478
|
+
returnType: parseSemanticTypeExpression(match[3] ?? "Void"),
|
|
479
|
+
from: match[4] ?? "",
|
|
480
|
+
importAs: match[5],
|
|
481
|
+
span: lineSpan(source, lineNumber),
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
declaration: { kind: "external", name, functions, span: lineSpan(source, start + 1) },
|
|
486
|
+
next: body.next,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function parseAction(
|
|
491
|
+
lines: string[],
|
|
492
|
+
start: number,
|
|
493
|
+
source: string,
|
|
494
|
+
records: Map<string, Map<string, string>>,
|
|
495
|
+
callables: string[],
|
|
496
|
+
): { declaration: PointSemanticActionDeclaration; next: number } {
|
|
497
|
+
return parseCallableBlock("action", lines, start, source, records, callables) as { declaration: PointSemanticActionDeclaration; next: number };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function parsePolicy(
|
|
501
|
+
lines: string[],
|
|
502
|
+
start: number,
|
|
503
|
+
source: string,
|
|
504
|
+
records: Map<string, Map<string, string>>,
|
|
505
|
+
callables: string[],
|
|
506
|
+
): { declaration: PointSemanticPolicyDeclaration; next: number } {
|
|
507
|
+
const name = (lines[start] ?? "").trim().slice("policy ".length).trim();
|
|
508
|
+
const body = collectSemanticBody(lines, start + 1);
|
|
509
|
+
const inputs: PointSemanticBinding[] = [];
|
|
510
|
+
const paramTypes = new Map<string, string>();
|
|
511
|
+
const bindings: string[] = [];
|
|
512
|
+
const statements: PointSemanticPolicyStatement[] = [];
|
|
513
|
+
for (let lineIndex = 0; lineIndex < body.lines.length; lineIndex += 1) {
|
|
514
|
+
const line = body.lines[lineIndex] ?? "";
|
|
515
|
+
const lineNumber = body.lineNumbers[lineIndex] ?? start + 2;
|
|
516
|
+
if (line.startsWith("input ")) {
|
|
517
|
+
const binding = parseInputBinding(line.slice("input ".length), source, lineNumber);
|
|
518
|
+
inputs.push(binding);
|
|
519
|
+
paramTypes.set(binding.label, typeLabel(binding.type));
|
|
520
|
+
bindings.push(binding.label);
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
const context = buildExpressionContext({ bindings, paramTypes, recordFields: records, callables });
|
|
524
|
+
if (line.startsWith("allow ")) {
|
|
525
|
+
statements.push({ kind: "allow", condition: parseLineExpression(line.slice("allow ".length), context, source, lineNumber), span: lineSpan(source, lineNumber) });
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (line.startsWith("deny ")) {
|
|
529
|
+
statements.push({ kind: "deny", condition: parseLineExpression(line.slice("deny ".length), context, source, lineNumber), span: lineSpan(source, lineNumber) });
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (line.startsWith("require ")) {
|
|
533
|
+
statements.push({ kind: "require", condition: parseLineExpression(line.slice("require ".length), context, source, lineNumber), span: lineSpan(source, lineNumber) });
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
throw new Error(`Unknown policy statement: ${line}`);
|
|
537
|
+
}
|
|
538
|
+
return { declaration: { kind: "policy", name, inputs, body: statements, span: lineSpan(source, start + 1) }, next: body.next };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function parseView(
|
|
542
|
+
lines: string[],
|
|
543
|
+
start: number,
|
|
544
|
+
source: string,
|
|
545
|
+
records: Map<string, Map<string, string>>,
|
|
546
|
+
callables: string[],
|
|
547
|
+
): { declaration: PointSemanticViewDeclaration; next: number } {
|
|
548
|
+
const name = (lines[start] ?? "").trim().slice("view ".length).trim();
|
|
549
|
+
const body = collectSemanticBody(lines, start + 1);
|
|
550
|
+
const inputs: PointSemanticBinding[] = [];
|
|
551
|
+
const paramTypes = new Map<string, string>();
|
|
552
|
+
const bindings: string[] = [];
|
|
553
|
+
let output: PointSemanticOutputBinding = { name: "page", type: { kind: "typeRef", name: "Page", args: [] } };
|
|
554
|
+
const statements: PointSemanticViewStatement[] = [];
|
|
555
|
+
|
|
556
|
+
for (let lineIndex = 0; lineIndex < body.lines.length; lineIndex += 1) {
|
|
557
|
+
const line = body.lines[lineIndex] ?? "";
|
|
558
|
+
const lineNumber = body.lineNumbers[lineIndex] ?? start + 2;
|
|
559
|
+
if (line.startsWith("input ")) {
|
|
560
|
+
const binding = parseInputBinding(line.slice("input ".length), source, lineNumber);
|
|
561
|
+
inputs.push(binding);
|
|
562
|
+
paramTypes.set(binding.label, typeLabel(binding.type));
|
|
563
|
+
bindings.push(binding.label);
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (line.startsWith("output ")) {
|
|
567
|
+
output = parseOutputBinding(line.slice("output ".length));
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
const context = buildExpressionContext({ bindings, paramTypes, recordFields: records, callables });
|
|
571
|
+
const whenRender = line.match(/^when (.+) render (.+)$/);
|
|
572
|
+
if (whenRender) {
|
|
573
|
+
statements.push({
|
|
574
|
+
kind: "whenRender",
|
|
575
|
+
condition: parseLineExpression(whenRender[1] ?? "", context, source, lineNumber),
|
|
576
|
+
value: parseLineExpression(whenRender[2] ?? "", context, source, lineNumber),
|
|
577
|
+
span: lineSpan(source, lineNumber),
|
|
578
|
+
});
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (line.startsWith("render ")) {
|
|
582
|
+
statements.push({
|
|
583
|
+
kind: "render",
|
|
584
|
+
value: parseLineExpression(line.slice("render ".length), context, source, lineNumber),
|
|
585
|
+
span: lineSpan(source, lineNumber),
|
|
586
|
+
});
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
throw new Error(`Unknown view statement: ${line}`);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return {
|
|
593
|
+
declaration: { kind: "view", name, inputs, output, body: statements, span: lineSpan(source, start + 1) },
|
|
594
|
+
next: body.next,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function parseRoute(
|
|
599
|
+
lines: string[],
|
|
600
|
+
start: number,
|
|
601
|
+
source: string,
|
|
602
|
+
records: Map<string, Map<string, string>>,
|
|
603
|
+
callables: string[],
|
|
604
|
+
): { declaration: PointSemanticRouteDeclaration; next: number } {
|
|
605
|
+
const name = (lines[start] ?? "").trim().slice("route ".length).trim();
|
|
606
|
+
const body = collectSemanticBody(lines, start + 1);
|
|
607
|
+
let method = "GET";
|
|
608
|
+
let path = "/";
|
|
609
|
+
const inputs: PointSemanticBinding[] = [];
|
|
610
|
+
const paramTypes = new Map<string, string>();
|
|
611
|
+
const bindings: string[] = [];
|
|
612
|
+
let output: PointSemanticOutputBinding = { name: "response", type: { kind: "typeRef", name: "Text", args: [] } };
|
|
613
|
+
const statements: PointSemanticRouteStatement[] = [];
|
|
614
|
+
|
|
615
|
+
for (let lineIndex = 0; lineIndex < body.lines.length; lineIndex += 1) {
|
|
616
|
+
const line = body.lines[lineIndex] ?? "";
|
|
617
|
+
const lineNumber = body.lineNumbers[lineIndex] ?? start + 2;
|
|
618
|
+
if (line.startsWith("method ")) {
|
|
619
|
+
method = line.slice("method ".length).trim();
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
if (line.startsWith("path ")) {
|
|
623
|
+
path = line.slice("path ".length).trim().replace(/^"|"$/g, "");
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
if (line.startsWith("input ")) {
|
|
627
|
+
const binding = parseInputBinding(line.slice("input ".length), source, lineNumber);
|
|
628
|
+
inputs.push(binding);
|
|
629
|
+
paramTypes.set(binding.label, typeLabel(binding.type));
|
|
630
|
+
bindings.push(binding.label);
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
if (line.startsWith("output ")) {
|
|
634
|
+
output = parseOutputBinding(line.slice("output ".length));
|
|
635
|
+
bindings.push(output.name);
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
const context = buildExpressionContext({ bindings, paramTypes, recordFields: records, callables });
|
|
639
|
+
if (line.startsWith("return ")) {
|
|
640
|
+
statements.push({
|
|
641
|
+
kind: "return",
|
|
642
|
+
value: parseLineExpression(line.slice("return ".length), context, source, lineNumber),
|
|
643
|
+
span: lineSpan(source, lineNumber),
|
|
644
|
+
});
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
throw new Error(`Unknown route statement: ${line}`);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return {
|
|
651
|
+
declaration: {
|
|
652
|
+
kind: "route",
|
|
653
|
+
name,
|
|
654
|
+
method,
|
|
655
|
+
path,
|
|
656
|
+
inputs,
|
|
657
|
+
output,
|
|
658
|
+
body: statements,
|
|
659
|
+
span: lineSpan(source, start + 1),
|
|
660
|
+
},
|
|
661
|
+
next: body.next,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function parseWorkflow(
|
|
666
|
+
lines: string[],
|
|
667
|
+
start: number,
|
|
668
|
+
source: string,
|
|
669
|
+
records: Map<string, Map<string, string>>,
|
|
670
|
+
callables: string[],
|
|
671
|
+
): { declaration: PointSemanticWorkflowDeclaration; next: number } {
|
|
672
|
+
const name = (lines[start] ?? "").trim().slice("workflow ".length).trim();
|
|
673
|
+
const body = collectSemanticBody(lines, start + 1);
|
|
674
|
+
const inputs: PointSemanticBinding[] = [];
|
|
675
|
+
const paramTypes = new Map<string, string>();
|
|
676
|
+
const bindings: string[] = [];
|
|
677
|
+
let output: PointSemanticOutputBinding = { name: "result", type: { kind: "typeRef", name: "Void", args: [] } };
|
|
678
|
+
const statements: PointSemanticWorkflowStatement[] = [];
|
|
679
|
+
for (let lineIndex = 0; lineIndex < body.lines.length; lineIndex += 1) {
|
|
680
|
+
const line = body.lines[lineIndex] ?? "";
|
|
681
|
+
const lineNumber = body.lineNumbers[lineIndex] ?? start + 2;
|
|
682
|
+
if (line.startsWith("input ")) {
|
|
683
|
+
const binding = parseInputBinding(line.slice("input ".length), source, lineNumber);
|
|
684
|
+
inputs.push(binding);
|
|
685
|
+
paramTypes.set(binding.label, typeLabel(binding.type));
|
|
686
|
+
bindings.push(binding.label);
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (line.startsWith("output ")) {
|
|
690
|
+
output = parseOutputBinding(line.slice("output ".length));
|
|
691
|
+
bindings.push(output.name);
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
const context = buildExpressionContext({ bindings, paramTypes, recordFields: records, callables });
|
|
695
|
+
const step = line.match(/^step (.+) is (.+)$/);
|
|
696
|
+
if (step) {
|
|
697
|
+
const stepName = step[1]?.trim() ?? "";
|
|
698
|
+
statements.push({
|
|
699
|
+
kind: "step",
|
|
700
|
+
name: stepName,
|
|
701
|
+
value: parseLineExpression(step[2] ?? "", context, source, lineNumber),
|
|
702
|
+
span: lineSpan(source, lineNumber),
|
|
703
|
+
});
|
|
704
|
+
bindings.push(stepName);
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
if (line.startsWith("return ")) {
|
|
708
|
+
statements.push({
|
|
709
|
+
kind: "return",
|
|
710
|
+
value: parseLineExpression(line.slice("return ".length), context, source, lineNumber),
|
|
711
|
+
span: lineSpan(source, lineNumber),
|
|
712
|
+
});
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
throw new Error(`Unknown workflow statement: ${line}`);
|
|
716
|
+
}
|
|
717
|
+
return { declaration: { kind: "workflow", name, inputs, output, body: statements, span: lineSpan(source, start + 1) }, next: body.next };
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function parseCommand(
|
|
721
|
+
lines: string[],
|
|
722
|
+
start: number,
|
|
723
|
+
source: string,
|
|
724
|
+
records: Map<string, Map<string, string>>,
|
|
725
|
+
callables: string[],
|
|
726
|
+
): { declaration: PointSemanticCommandDeclaration; next: number } {
|
|
727
|
+
const parsed = parseCallableBlock("command", lines, start, source, records, callables) as { declaration: PointSemanticCommandDeclaration; next: number };
|
|
728
|
+
return parsed;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function parseCallableBlock(
|
|
732
|
+
kind: "action" | "command",
|
|
733
|
+
lines: string[],
|
|
734
|
+
start: number,
|
|
735
|
+
source: string,
|
|
736
|
+
records: Map<string, Map<string, string>>,
|
|
737
|
+
callables: string[],
|
|
738
|
+
) {
|
|
739
|
+
const name = (lines[start] ?? "").trim().slice(`${kind} `.length).trim();
|
|
740
|
+
const body = collectSemanticBody(lines, start + 1);
|
|
741
|
+
const parsed = parseSimpleCallableBody(body, source, start, records, callables);
|
|
742
|
+
if (kind === "command") {
|
|
743
|
+
const commandStatements: PointSemanticCommandStatement[] = parsed.statements
|
|
744
|
+
.filter((statement): statement is PointSemanticActionStatement => statement.kind === "return")
|
|
745
|
+
.map((statement) => ({ kind: "return", value: statement.value, span: statement.span }));
|
|
746
|
+
return {
|
|
747
|
+
declaration: {
|
|
748
|
+
kind: "command",
|
|
749
|
+
name,
|
|
750
|
+
inputs: parsed.inputs,
|
|
751
|
+
output: parsed.output,
|
|
752
|
+
body: commandStatements,
|
|
753
|
+
span: lineSpan(source, start + 1),
|
|
754
|
+
},
|
|
755
|
+
next: body.next,
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
return {
|
|
759
|
+
declaration: {
|
|
760
|
+
kind: "action",
|
|
761
|
+
name,
|
|
762
|
+
inputs: parsed.inputs,
|
|
763
|
+
output: parsed.output,
|
|
764
|
+
touches: parsed.touches,
|
|
765
|
+
body: parsed.statements,
|
|
766
|
+
span: lineSpan(source, start + 1),
|
|
767
|
+
},
|
|
768
|
+
next: body.next,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function parseSimpleCallableBody(
|
|
773
|
+
body: SemanticBody,
|
|
774
|
+
source: string,
|
|
775
|
+
start: number,
|
|
776
|
+
records: Map<string, Map<string, string>>,
|
|
777
|
+
callables: string[],
|
|
778
|
+
) {
|
|
779
|
+
const inputs: PointSemanticBinding[] = [];
|
|
780
|
+
const paramTypes = new Map<string, string>();
|
|
781
|
+
const bindings: string[] = [];
|
|
782
|
+
let output: PointSemanticOutputBinding = { name: "result", type: { kind: "typeRef", name: "Void", args: [] } };
|
|
783
|
+
const touches: string[] = [];
|
|
784
|
+
const statements: PointSemanticActionStatement[] = [];
|
|
785
|
+
for (let lineIndex = 0; lineIndex < body.lines.length; lineIndex += 1) {
|
|
786
|
+
const line = body.lines[lineIndex] ?? "";
|
|
787
|
+
const lineNumber = body.lineNumbers[lineIndex] ?? start + 2;
|
|
788
|
+
if (line.startsWith("input ")) {
|
|
789
|
+
const binding = parseInputBinding(line.slice("input ".length), source, lineNumber);
|
|
790
|
+
inputs.push(binding);
|
|
791
|
+
paramTypes.set(binding.label, typeLabel(binding.type));
|
|
792
|
+
bindings.push(binding.label);
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
if (line.startsWith("output ")) {
|
|
796
|
+
output = parseOutputBinding(line.slice("output ".length));
|
|
797
|
+
bindings.push(output.name);
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (line.startsWith("touches ")) {
|
|
801
|
+
touches.push(...line.slice("touches ".length).split(",").map((effect) => effect.trim()).filter(Boolean));
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
const context = buildExpressionContext({ bindings, paramTypes, recordFields: records, callables });
|
|
805
|
+
if (line.startsWith("return ")) {
|
|
806
|
+
statements.push({
|
|
807
|
+
kind: "return",
|
|
808
|
+
value: parseLineExpression(line.slice("return ".length), context, source, lineNumber),
|
|
809
|
+
span: lineSpan(source, lineNumber),
|
|
810
|
+
});
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
throw new Error(`Unknown callable statement: ${line}`);
|
|
814
|
+
}
|
|
815
|
+
return { inputs, output, touches, statements };
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function parseMutationStatement(
|
|
819
|
+
line: string,
|
|
820
|
+
context: ReturnType<typeof buildExpressionContext>,
|
|
821
|
+
fileSource: string,
|
|
822
|
+
lineNumber: number,
|
|
823
|
+
): PointSemanticMutationStatement | null {
|
|
824
|
+
const addTo = line.match(/^add (.+) to (.+)$/);
|
|
825
|
+
if (addTo) {
|
|
826
|
+
return {
|
|
827
|
+
kind: "addTo",
|
|
828
|
+
amount: parseLineExpression(addTo[1] ?? "", context, fileSource, lineNumber),
|
|
829
|
+
target: addTo[2]?.trim() ?? "",
|
|
830
|
+
span: lineSpan(fileSource, lineNumber),
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
const subtractFrom = line.match(/^subtract (.+) from (.+)$/);
|
|
834
|
+
if (subtractFrom) {
|
|
835
|
+
return {
|
|
836
|
+
kind: "subtractFrom",
|
|
837
|
+
amount: parseLineExpression(subtractFrom[1] ?? "", context, fileSource, lineNumber),
|
|
838
|
+
target: subtractFrom[2]?.trim() ?? "",
|
|
839
|
+
span: lineSpan(fileSource, lineNumber),
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
const setTo = line.match(/^set (.+) to (.+)$/);
|
|
843
|
+
if (setTo) {
|
|
844
|
+
return {
|
|
845
|
+
kind: "setTo",
|
|
846
|
+
target: setTo[1]?.trim() ?? "",
|
|
847
|
+
value: parseLineExpression(setTo[2] ?? "", context, fileSource, lineNumber),
|
|
848
|
+
span: lineSpan(fileSource, lineNumber),
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
return null;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function parseInputBinding(source: string, _fileSource: string, lineNumber: number): PointSemanticBinding {
|
|
855
|
+
const colon = source.indexOf(":");
|
|
856
|
+
if (colon === -1) throw new Error(`Expected typed binding: ${source}`);
|
|
857
|
+
return {
|
|
858
|
+
label: source.slice(0, colon).trim(),
|
|
859
|
+
type: parseSemanticTypeExpression(source.slice(colon + 1).trim()),
|
|
860
|
+
span: lineSpanFromLine(lineNumber, source),
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function parseOutputBinding(source: string): PointSemanticOutputBinding {
|
|
865
|
+
const colon = source.indexOf(":");
|
|
866
|
+
if (colon !== -1) {
|
|
867
|
+
return {
|
|
868
|
+
name: source.slice(0, colon).trim(),
|
|
869
|
+
type: parseSemanticTypeExpression(source.slice(colon + 1).trim()),
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
return { name: "result", type: parseSemanticTypeExpression(source.trim()) };
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
interface SemanticBody {
|
|
876
|
+
lines: string[];
|
|
877
|
+
lineNumbers: number[];
|
|
878
|
+
next: number;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function collectSemanticBody(lines: string[], start: number): SemanticBody {
|
|
882
|
+
const body: string[] = [];
|
|
883
|
+
const lineNumbers: number[] = [];
|
|
884
|
+
let index = start;
|
|
885
|
+
for (; index < lines.length; index += 1) {
|
|
886
|
+
const trimmed = (lines[index] ?? "").trim();
|
|
887
|
+
if (!trimmed) continue;
|
|
888
|
+
if (isSemanticTopLevel(trimmed)) break;
|
|
889
|
+
body.push(trimmed);
|
|
890
|
+
lineNumbers.push(index + 1);
|
|
891
|
+
}
|
|
892
|
+
return { lines: body, lineNumbers, next: index };
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function isSemanticTopLevel(line: string): boolean {
|
|
896
|
+
return /^(module|use|record|calculation|rule|label|external|action|policy|view|route|workflow|command)\s+/.test(line);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function isLoopBoundary(line: string): boolean {
|
|
900
|
+
return (
|
|
901
|
+
line.startsWith("input ") ||
|
|
902
|
+
line.startsWith("output ") ||
|
|
903
|
+
line.startsWith("return ") ||
|
|
904
|
+
line.startsWith("for each ") ||
|
|
905
|
+
/^(.+) starts (at|as) (.+)$/.test(line) ||
|
|
906
|
+
isSemanticTopLevel(line)
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function typeLabel(type: { kind: "typeRef"; name: string; args: unknown[] }): string {
|
|
911
|
+
if (type.name === "List" && type.args[0]) {
|
|
912
|
+
return `List<${typeLabel(type.args[0] as { kind: "typeRef"; name: string; args: unknown[] })}>`;
|
|
913
|
+
}
|
|
914
|
+
if (type.name === "Maybe" && type.args[0]) {
|
|
915
|
+
return `Maybe<${typeLabel(type.args[0] as { kind: "typeRef"; name: string; args: unknown[] })}>`;
|
|
916
|
+
}
|
|
917
|
+
const primitives = new Set(["Text", "Int", "Float", "Bool", "Void", "Maybe", "Or", "Error", "Page"]);
|
|
918
|
+
if (primitives.has(type.name)) return type.name;
|
|
919
|
+
return toPascalCase(type.name);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function listItemType(source: string, paramTypes: Map<string, string>): string {
|
|
923
|
+
const trimmed = source.trim();
|
|
924
|
+
for (const [label, type] of paramTypes) {
|
|
925
|
+
if (trimmed === label && type.startsWith("List<")) return type.slice("List<".length, -1);
|
|
926
|
+
}
|
|
927
|
+
return "Unknown";
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function toPascalCase(label: string): string {
|
|
931
|
+
const words = label.match(/[A-Za-z0-9]+/g) ?? [];
|
|
932
|
+
return words.map((word) => `${word.slice(0, 1).toUpperCase()}${word.slice(1)}`).join("");
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function toIdentifier(label: string): string {
|
|
936
|
+
const words = label.match(/[A-Za-z0-9]+/g) ?? [];
|
|
937
|
+
return words.map((word, index) => (index === 0 ? word.toLowerCase() : toPascalCase(word))).join("");
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function lineSpanFromLine(lineNumber: number, line: string): PointSourceSpan {
|
|
941
|
+
return {
|
|
942
|
+
start: { line: lineNumber, column: 1, offset: 0 },
|
|
943
|
+
end: { line: lineNumber, column: line.length + 1, offset: line.length },
|
|
944
|
+
};
|
|
945
|
+
}
|