@hatchingpoint/point 0.0.6 → 0.0.8
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/LICENSE +21 -21
- package/README.md +34 -34
- package/package.json +34 -30
- package/src/cli.ts +7 -7
- package/src/core/ast.ts +162 -162
- package/src/core/check.ts +412 -412
- package/src/core/cli.ts +497 -497
- package/src/core/context.ts +181 -181
- package/src/core/emit-javascript.ts +124 -124
- package/src/core/emit-typescript.ts +166 -166
- package/src/core/format.ts +6 -6
- package/src/core/incremental.ts +53 -53
- package/src/core/index.ts +12 -12
- package/src/core/lexer.ts +234 -234
- package/src/core/semantic-source.ts +26 -26
- package/src/core/serialize.ts +18 -18
- package/src/core/test-only/core-text-parser.ts +400 -400
- package/src/core/test-only/format-core.ts +120 -120
- package/src/core/test-only/index.ts +3 -3
- package/src/core/test-only/legacy-lowering.ts +1030 -1030
- package/src/index.ts +1 -1
- package/src/semantic/ast.ts +230 -230
- package/src/semantic/callables.ts +51 -51
- package/src/semantic/context.ts +347 -347
- package/src/semantic/desugar.ts +665 -665
- package/src/semantic/expressions.ts +347 -347
- package/src/semantic/format.ts +222 -222
- package/src/semantic/index.ts +10 -10
- package/src/semantic/metadata.ts +37 -37
- package/src/semantic/naming.ts +33 -33
- package/src/semantic/parse.ts +945 -945
- package/src/semantic/serialize.ts +18 -18
|
@@ -15,1033 +15,1033 @@ function parsePointSourceViaLowering(source: string): PointCoreProgram {
|
|
|
15
15
|
return program;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
function lowerSemanticPointSyntax(source: string): string {
|
|
19
|
-
if (!isSemanticPointSyntax(source)) return source;
|
|
20
|
-
const lines = source.split(/\r?\n/);
|
|
21
|
-
const output: string[] = [];
|
|
22
|
-
const records = new Map<string, Map<string, string>>();
|
|
23
|
-
const externalBindings = new Map([...collectExternalBindings(source), ...collectCallableBindings(source)]);
|
|
24
|
-
let index = 0;
|
|
25
|
-
|
|
26
|
-
while (index < lines.length) {
|
|
27
|
-
const line = lines[index] ?? "";
|
|
28
|
-
const trimmed = line.trim();
|
|
29
|
-
if (!trimmed) {
|
|
30
|
-
index += 1;
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
if (trimmed.startsWith("module ")) {
|
|
34
|
-
output.push(trimmed);
|
|
35
|
-
index += 1;
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
if (trimmed.startsWith("use ")) {
|
|
39
|
-
index += 1;
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
if (trimmed.startsWith("record ")) {
|
|
43
|
-
const lowered = lowerRecord(lines, index, records);
|
|
44
|
-
output.push(...lowered.lines);
|
|
45
|
-
index = lowered.next;
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
if (trimmed.startsWith("calculation ")) {
|
|
49
|
-
const lowered = lowerCalculation(lines, index, records, externalBindings);
|
|
50
|
-
output.push(...lowered.lines);
|
|
51
|
-
index = lowered.next;
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
if (trimmed.startsWith("rule ")) {
|
|
55
|
-
const lowered = lowerRule(lines, index, records, externalBindings);
|
|
56
|
-
output.push(...lowered.lines);
|
|
57
|
-
index = lowered.next;
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
if (trimmed.startsWith("label ")) {
|
|
61
|
-
const lowered = lowerLabel(lines, index, records, externalBindings);
|
|
62
|
-
output.push(...lowered.lines);
|
|
63
|
-
index = lowered.next;
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
if (trimmed.startsWith("external ")) {
|
|
67
|
-
const lowered = lowerExternal(lines, index);
|
|
68
|
-
output.push(...lowered.lines);
|
|
69
|
-
index = lowered.next;
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if (trimmed.startsWith("action ")) {
|
|
73
|
-
const lowered = lowerAction(lines, index, records, externalBindings);
|
|
74
|
-
output.push(...lowered.lines);
|
|
75
|
-
index = lowered.next;
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
if (trimmed.startsWith("policy ")) {
|
|
79
|
-
const lowered = lowerPolicy(lines, index, records, externalBindings);
|
|
80
|
-
output.push(...lowered.lines);
|
|
81
|
-
index = lowered.next;
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
if (trimmed.startsWith("view ")) {
|
|
85
|
-
const lowered = lowerView(lines, index, records, externalBindings);
|
|
86
|
-
output.push(...lowered.lines);
|
|
87
|
-
index = lowered.next;
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
if (trimmed.startsWith("route ")) {
|
|
91
|
-
const lowered = lowerRoute(lines, index, records, externalBindings);
|
|
92
|
-
output.push(...lowered.lines);
|
|
93
|
-
index = lowered.next;
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
if (trimmed.startsWith("workflow ")) {
|
|
97
|
-
const lowered = lowerWorkflow(lines, index, records, externalBindings);
|
|
98
|
-
output.push(...lowered.lines);
|
|
99
|
-
index = lowered.next;
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
if (trimmed.startsWith("command ")) {
|
|
103
|
-
const lowered = lowerCommand(lines, index, records, externalBindings);
|
|
104
|
-
output.push(...lowered.lines);
|
|
105
|
-
index = lowered.next;
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
output.push(line);
|
|
109
|
-
index += 1;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return `${output.join("\n")}\n`;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function assertSemanticPointSource(source: string) {
|
|
116
|
-
const oldStyleTopLevel = /^(import|type|let|var|fn)\s+/;
|
|
117
|
-
const lines = source.split(/\r?\n/);
|
|
118
|
-
let hasSemanticDeclaration = false;
|
|
119
|
-
|
|
120
|
-
for (const [index, line] of lines.entries()) {
|
|
121
|
-
const trimmed = line.trim();
|
|
122
|
-
if (!trimmed || trimmed.startsWith("//")) continue;
|
|
123
|
-
if (/^(record|calculation|rule|label|external|action|policy|view|route|workflow|command)\s+/.test(trimmed)) hasSemanticDeclaration = true;
|
|
124
|
-
if (oldStyleTopLevel.test(trimmed)) {
|
|
125
|
-
throw new Error(
|
|
126
|
-
`Point source uses internal core syntax at ${index + 1}:1. Use record, calculation, rule, or label instead.`,
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (!hasSemanticDeclaration) {
|
|
132
|
-
throw new Error("Point source must contain at least one semantic declaration: record, calculation, rule, or label.");
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function lowerRecord(
|
|
137
|
-
lines: string[],
|
|
138
|
-
start: number,
|
|
139
|
-
records: Map<string, Map<string, string>>,
|
|
140
|
-
): { lines: string[]; next: number } {
|
|
141
|
-
const name = (lines[start] ?? "").trim().slice("record ".length).trim();
|
|
142
|
-
const typeName = toPascalCase(name);
|
|
143
|
-
const fields = new Map<string, string>();
|
|
144
|
-
const output = [`type ${typeName} {`];
|
|
145
|
-
let index = start + 1;
|
|
146
|
-
for (; index < lines.length; index += 1) {
|
|
147
|
-
const trimmed = (lines[index] ?? "").trim();
|
|
148
|
-
if (!trimmed) continue;
|
|
149
|
-
if (isSemanticTopLevel(trimmed)) break;
|
|
150
|
-
const colon = trimmed.indexOf(":");
|
|
151
|
-
if (colon === -1) throw new Error(`Expected field type in record ${name}: ${trimmed}`);
|
|
152
|
-
const label = trimmed.slice(0, colon).trim();
|
|
153
|
-
const fieldName = toIdentifier(label);
|
|
154
|
-
fields.set(label, fieldName);
|
|
155
|
-
output.push(` ${fieldName}: ${trimmed.slice(colon + 1).trim()}`);
|
|
156
|
-
}
|
|
157
|
-
output.push("}", "");
|
|
158
|
-
records.set(typeName, fields);
|
|
159
|
-
return { lines: output, next: index };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function lowerRule(
|
|
163
|
-
lines: string[],
|
|
164
|
-
start: number,
|
|
165
|
-
records: Map<string, Map<string, string>>,
|
|
166
|
-
externalBindings: Map<string, string> = new Map(),
|
|
167
|
-
): { lines: string[]; next: number } {
|
|
168
|
-
const label = (lines[start] ?? "").trim().slice("rule ".length).trim();
|
|
169
|
-
const body = collectSemanticBody(lines, start + 1);
|
|
170
|
-
const params: string[] = [];
|
|
171
|
-
const paramTypes = new Map<string, string>();
|
|
172
|
-
const bindings = new Map<string, string>(externalBindings);
|
|
173
|
-
let outputName = "result";
|
|
174
|
-
let outputType = "Void";
|
|
175
|
-
const statements: string[] = [];
|
|
176
|
-
|
|
177
|
-
for (let lineIndex = 0; lineIndex < body.lines.length; lineIndex += 1) {
|
|
178
|
-
const line = body.lines[lineIndex] ?? "";
|
|
179
|
-
if (line.startsWith("input ")) {
|
|
180
|
-
const param = parseTypedBinding(line.slice("input ".length));
|
|
181
|
-
const paramName = toIdentifier(param.name);
|
|
182
|
-
params.push(`${paramName}: ${param.type}`);
|
|
183
|
-
paramTypes.set(paramName, param.type);
|
|
184
|
-
bindings.set(param.name, paramName);
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
if (line.startsWith("output ")) {
|
|
188
|
-
const output = parseOutputBinding(line.slice("output ".length));
|
|
189
|
-
outputName = toIdentifier(output.name);
|
|
190
|
-
outputType = output.type;
|
|
191
|
-
bindings.set(output.name, outputName);
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
const loop = line.match(/^for each (.+) in (.+)$/);
|
|
195
|
-
if (loop) {
|
|
196
|
-
const itemLabel = loop[1]?.trim() ?? "";
|
|
197
|
-
const itemName = toIdentifier(itemLabel);
|
|
198
|
-
const iterableSource = loop[2] ?? "";
|
|
199
|
-
const iterable = lowerExpression(iterableSource, paramTypes, records, bindings);
|
|
200
|
-
const loopParamTypes = new Map(paramTypes);
|
|
201
|
-
const itemType = listItemTypeFor(iterableSource, paramTypes, bindings);
|
|
202
|
-
if (itemType) loopParamTypes.set(itemName, itemType);
|
|
203
|
-
const loopBindings = new Map(bindings);
|
|
204
|
-
loopBindings.set(itemLabel, itemName);
|
|
205
|
-
const loopBody: string[] = [];
|
|
206
|
-
while (body.lines[lineIndex + 1] && !isSemanticLoopBoundary(body.lines[lineIndex + 1]!)) {
|
|
207
|
-
lineIndex += 1;
|
|
208
|
-
loopBody.push(...lowerSemanticMutationStatement(body.lines[lineIndex]!, loopParamTypes, records, loopBindings));
|
|
209
|
-
}
|
|
210
|
-
statements.push(`for ${itemName} in ${iterable} {`);
|
|
211
|
-
statements.push(...indentRaw(loopBody));
|
|
212
|
-
statements.push("}");
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
const startsAt = line.match(/^(.+) starts at (.+)$/);
|
|
216
|
-
if (startsAt) {
|
|
217
|
-
const name = toIdentifier(startsAt[1] ?? "");
|
|
218
|
-
bindings.set(startsAt[1]?.trim() ?? name, name);
|
|
219
|
-
statements.push(`var ${name}: ${outputType} = ${lowerExpression(startsAt[2] ?? "", paramTypes, records, bindings)}`);
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
const addWhen = line.match(/^add (.+) when (.+)$/);
|
|
223
|
-
if (addWhen) {
|
|
224
|
-
statements.push(`if ${lowerExpression(addWhen[2] ?? "", paramTypes, records, bindings)} {`);
|
|
225
|
-
statements.push(` ${outputName} += ${lowerExpression(addWhen[1] ?? "", paramTypes, records, bindings)}`);
|
|
226
|
-
statements.push("}");
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
const mutation = lowerSemanticMutationStatement(line, paramTypes, records, bindings);
|
|
230
|
-
if (mutation.length > 0) {
|
|
231
|
-
statements.push(...mutation);
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
if (line.startsWith("return ")) {
|
|
235
|
-
statements.push(`return ${lowerExpression(line.slice("return ".length), paramTypes, records, bindings)}`);
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
throw new Error(`Unknown rule statement: ${line}`);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const functionName = semanticFunctionName(label, outputName, "rule");
|
|
242
|
-
return {
|
|
243
|
-
lines: [`fn ${functionName}(${params.join(", ")}): ${outputType} {`, ...indentRaw(statements), "}", ""],
|
|
244
|
-
next: body.next,
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function lowerCalculation(
|
|
249
|
-
lines: string[],
|
|
250
|
-
start: number,
|
|
251
|
-
records: Map<string, Map<string, string>>,
|
|
252
|
-
externalBindings: Map<string, string> = new Map(),
|
|
253
|
-
): { lines: string[]; next: number } {
|
|
254
|
-
const label = (lines[start] ?? "").trim().slice("calculation ".length).trim();
|
|
255
|
-
const body = collectSemanticBody(lines, start + 1);
|
|
256
|
-
const params: string[] = [];
|
|
257
|
-
const paramTypes = new Map<string, string>();
|
|
258
|
-
const bindings = new Map<string, string>(externalBindings);
|
|
259
|
-
let outputName = "result";
|
|
260
|
-
let outputType = "Void";
|
|
261
|
-
const statements: string[] = [];
|
|
262
|
-
|
|
263
|
-
for (let lineIndex = 0; lineIndex < body.lines.length; lineIndex += 1) {
|
|
264
|
-
const line = body.lines[lineIndex] ?? "";
|
|
265
|
-
if (line.startsWith("input ")) {
|
|
266
|
-
const param = parseTypedBinding(line.slice("input ".length));
|
|
267
|
-
const paramName = toIdentifier(param.name);
|
|
268
|
-
params.push(`${paramName}: ${param.type}`);
|
|
269
|
-
paramTypes.set(paramName, param.type);
|
|
270
|
-
bindings.set(param.name, paramName);
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
if (line.startsWith("output ")) {
|
|
274
|
-
const output = parseOutputBinding(line.slice("output ".length));
|
|
275
|
-
outputName = toIdentifier(output.name);
|
|
276
|
-
outputType = output.type;
|
|
277
|
-
bindings.set(output.name, outputName);
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
const loop = line.match(/^for each (.+) in (.+)$/);
|
|
281
|
-
if (loop) {
|
|
282
|
-
const itemLabel = loop[1]?.trim() ?? "";
|
|
283
|
-
const itemName = toIdentifier(itemLabel);
|
|
284
|
-
const iterableSource = loop[2] ?? "";
|
|
285
|
-
const iterable = lowerExpression(iterableSource, paramTypes, records, bindings);
|
|
286
|
-
const loopParamTypes = new Map(paramTypes);
|
|
287
|
-
const itemType = listItemTypeFor(iterableSource, paramTypes, bindings);
|
|
288
|
-
if (itemType) loopParamTypes.set(itemName, itemType);
|
|
289
|
-
const loopBindings = new Map(bindings);
|
|
290
|
-
loopBindings.set(itemLabel, itemName);
|
|
291
|
-
const loopBody: string[] = [];
|
|
292
|
-
while (body.lines[lineIndex + 1] && !isSemanticLoopBoundary(body.lines[lineIndex + 1]!)) {
|
|
293
|
-
lineIndex += 1;
|
|
294
|
-
loopBody.push(...lowerSemanticMutationStatement(body.lines[lineIndex]!, loopParamTypes, records, loopBindings));
|
|
295
|
-
}
|
|
296
|
-
statements.push(`for ${itemName} in ${iterable} {`);
|
|
297
|
-
statements.push(...indentRaw(loopBody));
|
|
298
|
-
statements.push("}");
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
const isExpression = line.match(/^(.+) is (.+)$/);
|
|
302
|
-
if (isExpression) {
|
|
303
|
-
const name = toIdentifier(isExpression[1] ?? "");
|
|
304
|
-
if (name !== outputName) throw new Error(`Calculation ${label} can only assign its output ${outputName}`);
|
|
305
|
-
statements.push(`return ${lowerExpression(isExpression[2] ?? "", paramTypes, records, bindings)}`);
|
|
306
|
-
continue;
|
|
307
|
-
}
|
|
308
|
-
const startsAs = line.match(/^(.+) starts as (.+)$/);
|
|
309
|
-
if (startsAs) {
|
|
310
|
-
const name = toIdentifier(startsAs[1] ?? "");
|
|
311
|
-
bindings.set(startsAs[1]?.trim() ?? name, name);
|
|
312
|
-
statements.push(`var ${name}: ${outputType} = ${lowerExpression(startsAs[2] ?? "", paramTypes, records, bindings)}`);
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
const startsAt = line.match(/^(.+) starts at (.+)$/);
|
|
316
|
-
if (startsAt) {
|
|
317
|
-
const name = toIdentifier(startsAt[1] ?? "");
|
|
318
|
-
bindings.set(startsAt[1]?.trim() ?? name, name);
|
|
319
|
-
statements.push(`var ${name}: ${outputType} = ${lowerExpression(startsAt[2] ?? "", paramTypes, records, bindings)}`);
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
const mutation = lowerSemanticMutationStatement(line, paramTypes, records, bindings);
|
|
323
|
-
if (mutation.length > 0) {
|
|
324
|
-
statements.push(...mutation);
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
if (line.startsWith("return ")) {
|
|
328
|
-
statements.push(`return ${lowerExpression(line.slice("return ".length), paramTypes, records, bindings)}`);
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
|
-
throw new Error(`Unknown calculation statement: ${line}`);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
return {
|
|
335
|
-
lines: [`fn ${semanticFunctionName(label, outputName, "calculation")}(${params.join(", ")}): ${outputType} {`, ...indentRaw(statements), "}", ""],
|
|
336
|
-
next: body.next,
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function lowerLabel(
|
|
341
|
-
lines: string[],
|
|
342
|
-
start: number,
|
|
343
|
-
records: Map<string, Map<string, string>>,
|
|
344
|
-
externalBindings: Map<string, string> = new Map(),
|
|
345
|
-
): { lines: string[]; next: number } {
|
|
346
|
-
const label = (lines[start] ?? "").trim().slice("label ".length).trim();
|
|
347
|
-
const body = collectSemanticBody(lines, start + 1);
|
|
348
|
-
const params: string[] = [];
|
|
349
|
-
const paramTypes = new Map<string, string>();
|
|
350
|
-
const bindings = new Map<string, string>(externalBindings);
|
|
351
|
-
let outputType = "Text";
|
|
352
|
-
const statements: string[] = [];
|
|
353
|
-
|
|
354
|
-
for (const line of body.lines) {
|
|
355
|
-
if (line.startsWith("input ")) {
|
|
356
|
-
const param = parseTypedBinding(line.slice("input ".length));
|
|
357
|
-
const paramName = toIdentifier(param.name);
|
|
358
|
-
params.push(`${paramName}: ${param.type}`);
|
|
359
|
-
paramTypes.set(paramName, param.type);
|
|
360
|
-
bindings.set(param.name, paramName);
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
if (line.startsWith("output ")) {
|
|
364
|
-
outputType = parseOutputBinding(line.slice("output ".length)).type;
|
|
365
|
-
continue;
|
|
366
|
-
}
|
|
367
|
-
const whenReturn = line.match(/^when (.+) return (.+)$/);
|
|
368
|
-
if (whenReturn) {
|
|
369
|
-
statements.push(`if ${lowerExpression(whenReturn[1] ?? "", paramTypes, records, bindings)} {`);
|
|
370
|
-
statements.push(` return ${lowerExpression(whenReturn[2] ?? "", paramTypes, records, bindings)}`);
|
|
371
|
-
statements.push("}");
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
if (line.startsWith("otherwise return ")) {
|
|
375
|
-
statements.push(`return ${lowerExpression(line.slice("otherwise return ".length), paramTypes, records, bindings)}`);
|
|
376
|
-
continue;
|
|
377
|
-
}
|
|
378
|
-
throw new Error(`Unknown label statement: ${line}`);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
return {
|
|
382
|
-
lines: [`fn ${semanticFunctionName(label, "label", "label")}(${params.join(", ")}): ${outputType} {`, ...indentRaw(statements), "}", ""],
|
|
383
|
-
next: body.next,
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function lowerExternal(lines: string[], start: number): { lines: string[]; next: number } {
|
|
388
|
-
const body = collectSemanticBody(lines, start + 1);
|
|
389
|
-
const output: string[] = [];
|
|
390
|
-
for (const line of body.lines) {
|
|
391
|
-
const match = line.match(/^(.+)\((.*)\):\s*(.+?)\s+from\s+"([^"]+)"(?:\s+as\s+([A-Za-z_][A-Za-z0-9_]*))?$/);
|
|
392
|
-
if (!match) throw new Error(`Unknown external declaration: ${line}`);
|
|
393
|
-
const name = toIdentifier(match[1] ?? "");
|
|
394
|
-
const params = (match[2] ?? "")
|
|
395
|
-
.split(",")
|
|
396
|
-
.map((param) => param.trim())
|
|
397
|
-
.filter(Boolean)
|
|
398
|
-
.map((param) => {
|
|
399
|
-
const binding = parseTypedBinding(param);
|
|
400
|
-
return `${toIdentifier(binding.name)}: ${binding.type}`;
|
|
401
|
-
});
|
|
402
|
-
const returnType = normalizeTypeExpressionSource(match[3] ?? "Void");
|
|
403
|
-
const importName = match[5] ? ` as ${match[5]}` : "";
|
|
404
|
-
output.push(`external fn ${name}(${params.join(", ")}): ${returnType} from ${JSON.stringify(match[4])}${importName}`);
|
|
405
|
-
}
|
|
406
|
-
output.push("");
|
|
407
|
-
return { lines: output, next: body.next };
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function lowerAction(
|
|
411
|
-
lines: string[],
|
|
412
|
-
start: number,
|
|
413
|
-
records: Map<string, Map<string, string>>,
|
|
414
|
-
externalBindings: Map<string, string> = new Map(),
|
|
415
|
-
): { lines: string[]; next: number } {
|
|
416
|
-
const label = (lines[start] ?? "").trim().slice("action ".length).trim();
|
|
417
|
-
const body = collectSemanticBody(lines, start + 1);
|
|
418
|
-
const params: string[] = [];
|
|
419
|
-
const paramTypes = new Map<string, string>();
|
|
420
|
-
const bindings = new Map<string, string>(externalBindings);
|
|
421
|
-
let outputName = "result";
|
|
422
|
-
let outputType = "Void";
|
|
423
|
-
const statements: string[] = [];
|
|
424
|
-
|
|
425
|
-
for (const line of body.lines) {
|
|
426
|
-
if (line.startsWith("input ")) {
|
|
427
|
-
const param = parseTypedBinding(line.slice("input ".length));
|
|
428
|
-
const paramName = toIdentifier(param.name);
|
|
429
|
-
params.push(`${paramName}: ${param.type}`);
|
|
430
|
-
paramTypes.set(paramName, param.type);
|
|
431
|
-
bindings.set(param.name, paramName);
|
|
432
|
-
continue;
|
|
433
|
-
}
|
|
434
|
-
if (line.startsWith("output ")) {
|
|
435
|
-
const output = parseOutputBinding(line.slice("output ".length));
|
|
436
|
-
outputName = toIdentifier(output.name);
|
|
437
|
-
outputType = output.type;
|
|
438
|
-
bindings.set(output.name, outputName);
|
|
439
|
-
continue;
|
|
440
|
-
}
|
|
441
|
-
if (line.startsWith("touches ")) continue;
|
|
442
|
-
if (line.startsWith("return ")) {
|
|
443
|
-
statements.push(`return ${lowerExpression(line.slice("return ".length), paramTypes, records, bindings)}`);
|
|
444
|
-
continue;
|
|
445
|
-
}
|
|
446
|
-
throw new Error(`Unknown action statement: ${line}`);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
return {
|
|
450
|
-
lines: [`fn ${semanticFunctionName(label, outputName, "action")}(${params.join(", ")}): ${outputType} {`, ...indentRaw(statements), "}", ""],
|
|
451
|
-
next: body.next,
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function lowerPolicy(
|
|
456
|
-
lines: string[],
|
|
457
|
-
start: number,
|
|
458
|
-
records: Map<string, Map<string, string>>,
|
|
459
|
-
externalBindings: Map<string, string> = new Map(),
|
|
460
|
-
): { lines: string[]; next: number } {
|
|
461
|
-
const label = (lines[start] ?? "").trim().slice("policy ".length).trim();
|
|
462
|
-
const body = collectSemanticBody(lines, start + 1);
|
|
463
|
-
const params: string[] = [];
|
|
464
|
-
const paramTypes = new Map<string, string>();
|
|
465
|
-
const bindings = new Map<string, string>(externalBindings);
|
|
466
|
-
const statements: string[] = [];
|
|
467
|
-
|
|
468
|
-
for (const line of body.lines) {
|
|
469
|
-
if (line.startsWith("input ")) {
|
|
470
|
-
const param = parseTypedBinding(line.slice("input ".length));
|
|
471
|
-
const paramName = toIdentifier(param.name);
|
|
472
|
-
params.push(`${paramName}: ${param.type}`);
|
|
473
|
-
paramTypes.set(paramName, param.type);
|
|
474
|
-
bindings.set(param.name, paramName);
|
|
475
|
-
continue;
|
|
476
|
-
}
|
|
477
|
-
if (line.startsWith("allow ") || line.startsWith("require ")) {
|
|
478
|
-
const expression = line.replace(/^(allow|require)\s+/, "");
|
|
479
|
-
statements.push(`return ${lowerExpression(expression, paramTypes, records, bindings)}`);
|
|
480
|
-
continue;
|
|
481
|
-
}
|
|
482
|
-
if (line.startsWith("deny ")) {
|
|
483
|
-
statements.push(`return ${lowerExpression(line.slice("deny ".length), paramTypes, records, bindings)} == false`);
|
|
484
|
-
continue;
|
|
485
|
-
}
|
|
486
|
-
throw new Error(`Unknown policy statement: ${line}`);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
return {
|
|
490
|
-
lines: [`fn ${semanticFunctionName(label, "policy", "policy")}(${params.join(", ")}): Bool {`, ...indentRaw(statements), "}", ""],
|
|
491
|
-
next: body.next,
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function lowerView(
|
|
496
|
-
lines: string[],
|
|
497
|
-
start: number,
|
|
498
|
-
records: Map<string, Map<string, string>>,
|
|
499
|
-
externalBindings: Map<string, string> = new Map(),
|
|
500
|
-
): { lines: string[]; next: number } {
|
|
501
|
-
const label = (lines[start] ?? "").trim().slice("view ".length).trim();
|
|
502
|
-
const body = collectSemanticBody(lines, start + 1);
|
|
503
|
-
const params: string[] = [];
|
|
504
|
-
const paramTypes = new Map<string, string>();
|
|
505
|
-
const bindings = new Map<string, string>(externalBindings);
|
|
506
|
-
const statements: string[] = [];
|
|
507
|
-
|
|
508
|
-
for (const line of body.lines) {
|
|
509
|
-
if (line.startsWith("input ")) {
|
|
510
|
-
const param = parseTypedBinding(line.slice("input ".length));
|
|
511
|
-
const paramName = toIdentifier(param.name);
|
|
512
|
-
params.push(`${paramName}: ${param.type}`);
|
|
513
|
-
paramTypes.set(paramName, param.type);
|
|
514
|
-
bindings.set(param.name, paramName);
|
|
515
|
-
continue;
|
|
516
|
-
}
|
|
517
|
-
if (line.startsWith("render ")) {
|
|
518
|
-
statements.push(`return ${lowerExpression(line.slice("render ".length), paramTypes, records, bindings)}`);
|
|
519
|
-
continue;
|
|
520
|
-
}
|
|
521
|
-
const whenRender = line.match(/^when (.+) render (.+)$/);
|
|
522
|
-
if (whenRender) {
|
|
523
|
-
statements.push(`if ${lowerExpression(whenRender[1] ?? "", paramTypes, records, bindings)} {`);
|
|
524
|
-
statements.push(` return ${lowerExpression(whenRender[2] ?? "", paramTypes, records, bindings)}`);
|
|
525
|
-
statements.push("}");
|
|
526
|
-
continue;
|
|
527
|
-
}
|
|
528
|
-
throw new Error(`Unknown view statement: ${line}`);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
return {
|
|
532
|
-
lines: [`fn ${semanticFunctionName(label, "view", "view")}(${params.join(", ")}): Text {`, ...indentRaw(statements), "}", ""],
|
|
533
|
-
next: body.next,
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function lowerRoute(
|
|
538
|
-
lines: string[],
|
|
539
|
-
start: number,
|
|
540
|
-
records: Map<string, Map<string, string>>,
|
|
541
|
-
externalBindings: Map<string, string> = new Map(),
|
|
542
|
-
): { lines: string[]; next: number } {
|
|
543
|
-
const label = (lines[start] ?? "").trim().slice("route ".length).trim();
|
|
544
|
-
const body = collectSemanticBody(lines, start + 1);
|
|
545
|
-
const params: string[] = [];
|
|
546
|
-
const paramTypes = new Map<string, string>();
|
|
547
|
-
const bindings = new Map<string, string>(externalBindings);
|
|
548
|
-
let outputName = "response";
|
|
549
|
-
let outputType = "Text";
|
|
550
|
-
const statements: string[] = [];
|
|
551
|
-
|
|
552
|
-
for (const line of body.lines) {
|
|
553
|
-
if (line.startsWith("method ") || line.startsWith("path ")) continue;
|
|
554
|
-
if (line.startsWith("input ")) {
|
|
555
|
-
const param = parseTypedBinding(line.slice("input ".length));
|
|
556
|
-
const paramName = toIdentifier(param.name);
|
|
557
|
-
params.push(`${paramName}: ${param.type}`);
|
|
558
|
-
paramTypes.set(paramName, param.type);
|
|
559
|
-
bindings.set(param.name, paramName);
|
|
560
|
-
continue;
|
|
561
|
-
}
|
|
562
|
-
if (line.startsWith("output ")) {
|
|
563
|
-
const output = parseOutputBinding(line.slice("output ".length));
|
|
564
|
-
outputName = toIdentifier(output.name);
|
|
565
|
-
outputType = output.type;
|
|
566
|
-
bindings.set(output.name, outputName);
|
|
567
|
-
continue;
|
|
568
|
-
}
|
|
569
|
-
if (line.startsWith("return ")) {
|
|
570
|
-
statements.push(`return ${lowerExpression(line.slice("return ".length), paramTypes, records, bindings)}`);
|
|
571
|
-
continue;
|
|
572
|
-
}
|
|
573
|
-
throw new Error(`Unknown route statement: ${line}`);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
return {
|
|
577
|
-
lines: [`fn ${semanticFunctionName(label, "route", "route")}(${params.join(", ")}): ${outputType} {`, ...indentRaw(statements), "}", ""],
|
|
578
|
-
next: body.next,
|
|
579
|
-
};
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
function lowerWorkflow(
|
|
583
|
-
lines: string[],
|
|
584
|
-
start: number,
|
|
585
|
-
records: Map<string, Map<string, string>>,
|
|
586
|
-
externalBindings: Map<string, string> = new Map(),
|
|
587
|
-
): { lines: string[]; next: number } {
|
|
588
|
-
const label = (lines[start] ?? "").trim().slice("workflow ".length).trim();
|
|
589
|
-
const body = collectSemanticBody(lines, start + 1);
|
|
590
|
-
const params: string[] = [];
|
|
591
|
-
const paramTypes = new Map<string, string>();
|
|
592
|
-
const bindings = new Map<string, string>(externalBindings);
|
|
593
|
-
let outputName = "result";
|
|
594
|
-
let outputType = "Void";
|
|
595
|
-
const statements: string[] = [];
|
|
596
|
-
|
|
597
|
-
for (const line of body.lines) {
|
|
598
|
-
if (line.startsWith("input ")) {
|
|
599
|
-
const param = parseTypedBinding(line.slice("input ".length));
|
|
600
|
-
const paramName = toIdentifier(param.name);
|
|
601
|
-
params.push(`${paramName}: ${param.type}`);
|
|
602
|
-
paramTypes.set(paramName, param.type);
|
|
603
|
-
bindings.set(param.name, paramName);
|
|
604
|
-
continue;
|
|
605
|
-
}
|
|
606
|
-
if (line.startsWith("output ")) {
|
|
607
|
-
const output = parseOutputBinding(line.slice("output ".length));
|
|
608
|
-
outputName = toIdentifier(output.name);
|
|
609
|
-
outputType = output.type;
|
|
610
|
-
bindings.set(output.name, outputName);
|
|
611
|
-
continue;
|
|
612
|
-
}
|
|
613
|
-
const step = line.match(/^step (.+) is (.+)$/);
|
|
614
|
-
if (step) {
|
|
615
|
-
const name = toIdentifier(step[1] ?? "");
|
|
616
|
-
bindings.set(step[1]?.trim() ?? name, name);
|
|
617
|
-
statements.push(`let ${name}: ${outputType} = ${lowerExpression(step[2] ?? "", paramTypes, records, bindings)}`);
|
|
618
|
-
continue;
|
|
619
|
-
}
|
|
620
|
-
if (line.startsWith("return ")) {
|
|
621
|
-
statements.push(`return ${lowerExpression(line.slice("return ".length), paramTypes, records, bindings)}`);
|
|
622
|
-
continue;
|
|
623
|
-
}
|
|
624
|
-
throw new Error(`Unknown workflow statement: ${line}`);
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
return {
|
|
628
|
-
lines: [`fn ${semanticFunctionName(label, "workflow", "workflow")}(${params.join(", ")}): ${outputType} {`, ...indentRaw(statements), "}", ""],
|
|
629
|
-
next: body.next,
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
function lowerCommand(
|
|
634
|
-
lines: string[],
|
|
635
|
-
start: number,
|
|
636
|
-
records: Map<string, Map<string, string>>,
|
|
637
|
-
externalBindings: Map<string, string> = new Map(),
|
|
638
|
-
): { lines: string[]; next: number } {
|
|
639
|
-
const label = (lines[start] ?? "").trim().slice("command ".length).trim();
|
|
640
|
-
const body = collectSemanticBody(lines, start + 1);
|
|
641
|
-
const params: string[] = [];
|
|
642
|
-
const paramTypes = new Map<string, string>();
|
|
643
|
-
const bindings = new Map<string, string>(externalBindings);
|
|
644
|
-
let outputName = "result";
|
|
645
|
-
let outputType = "Void";
|
|
646
|
-
const statements: string[] = [];
|
|
647
|
-
|
|
648
|
-
for (const line of body.lines) {
|
|
649
|
-
if (line.startsWith("input ")) {
|
|
650
|
-
const param = parseTypedBinding(line.slice("input ".length));
|
|
651
|
-
const paramName = toIdentifier(param.name);
|
|
652
|
-
params.push(`${paramName}: ${param.type}`);
|
|
653
|
-
paramTypes.set(paramName, param.type);
|
|
654
|
-
bindings.set(param.name, paramName);
|
|
655
|
-
continue;
|
|
656
|
-
}
|
|
657
|
-
if (line.startsWith("output ")) {
|
|
658
|
-
const output = parseOutputBinding(line.slice("output ".length));
|
|
659
|
-
outputName = toIdentifier(output.name);
|
|
660
|
-
outputType = output.type;
|
|
661
|
-
bindings.set(output.name, outputName);
|
|
662
|
-
continue;
|
|
663
|
-
}
|
|
664
|
-
if (line.startsWith("return ")) {
|
|
665
|
-
statements.push(`return ${lowerExpression(line.slice("return ".length), paramTypes, records, bindings)}`);
|
|
666
|
-
continue;
|
|
667
|
-
}
|
|
668
|
-
throw new Error(`Unknown command statement: ${line}`);
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
return {
|
|
672
|
-
lines: [`fn ${semanticFunctionName(label, "command", "command")}(${params.join(", ")}): ${outputType} {`, ...indentRaw(statements), "}", ""],
|
|
673
|
-
next: body.next,
|
|
674
|
-
};
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
interface SemanticDeclarationInfo {
|
|
678
|
-
kind: "record" | "calculation" | "rule" | "label";
|
|
679
|
-
name: string;
|
|
680
|
-
loweredName: string;
|
|
681
|
-
outputName?: string;
|
|
682
|
-
fields?: Map<string, string>;
|
|
683
|
-
params?: Map<string, string>;
|
|
684
|
-
effects?: string[];
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
function attachSemanticMetadata(program: PointCoreProgram, source: string) {
|
|
688
|
-
program.semantic = { source: "semantic" };
|
|
689
|
-
const declarations = collectSemanticDeclarationInfo(source);
|
|
690
|
-
const byLoweredName = new Map(declarations.map((declaration) => [declaration.loweredName, declaration]));
|
|
691
|
-
for (const declaration of program.declarations) {
|
|
692
|
-
if (declaration.kind !== "type" && declaration.kind !== "function" && declaration.kind !== "external") continue;
|
|
693
|
-
const semantic = byLoweredName.get(declaration.name);
|
|
694
|
-
if (!semantic) continue;
|
|
695
|
-
declaration.semantic = {
|
|
696
|
-
kind: semantic.kind,
|
|
697
|
-
name: semantic.name,
|
|
698
|
-
outputName: semantic.outputName,
|
|
699
|
-
effects: semantic.effects,
|
|
700
|
-
};
|
|
701
|
-
if (declaration.kind === "type") {
|
|
702
|
-
for (const field of declaration.fields) {
|
|
703
|
-
field.semanticName = semantic.fields ? findSemanticName(semantic.fields, field.name) : field.name;
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
if (declaration.kind === "function" || declaration.kind === "external") {
|
|
707
|
-
for (const param of declaration.params) {
|
|
708
|
-
param.semanticName = semantic.params ? findSemanticName(semantic.params, param.name) : param.name;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function collectSemanticDeclarationInfo(source: string): SemanticDeclarationInfo[] {
|
|
715
|
-
const lines = source.split(/\r?\n/);
|
|
716
|
-
const records = new Map<string, Map<string, string>>();
|
|
717
|
-
const declarations: SemanticDeclarationInfo[] = [];
|
|
718
|
-
let index = 0;
|
|
719
|
-
while (index < lines.length) {
|
|
720
|
-
const trimmed = (lines[index] ?? "").trim();
|
|
721
|
-
if (!trimmed || trimmed.startsWith("module ")) {
|
|
722
|
-
index += 1;
|
|
723
|
-
continue;
|
|
724
|
-
}
|
|
725
|
-
if (trimmed.startsWith("record ")) {
|
|
726
|
-
const name = trimmed.slice("record ".length).trim();
|
|
727
|
-
const fields = new Map<string, string>();
|
|
728
|
-
index += 1;
|
|
729
|
-
for (; index < lines.length; index += 1) {
|
|
730
|
-
const line = (lines[index] ?? "").trim();
|
|
731
|
-
if (!line) continue;
|
|
732
|
-
if (isSemanticTopLevel(line)) break;
|
|
733
|
-
const colon = line.indexOf(":");
|
|
734
|
-
if (colon !== -1) fields.set(line.slice(0, colon).trim(), toIdentifier(line.slice(0, colon).trim()));
|
|
735
|
-
}
|
|
736
|
-
const loweredName = toPascalCase(name);
|
|
737
|
-
records.set(loweredName, fields);
|
|
738
|
-
declarations.push({ kind: "record", name, loweredName, fields });
|
|
739
|
-
continue;
|
|
740
|
-
}
|
|
741
|
-
if (
|
|
742
|
-
trimmed.startsWith("calculation ") ||
|
|
743
|
-
trimmed.startsWith("rule ") ||
|
|
744
|
-
trimmed.startsWith("label ") ||
|
|
745
|
-
trimmed.startsWith("action ") ||
|
|
746
|
-
trimmed.startsWith("policy ") ||
|
|
747
|
-
trimmed.startsWith("view ") ||
|
|
748
|
-
trimmed.startsWith("route ") ||
|
|
749
|
-
trimmed.startsWith("workflow ") ||
|
|
750
|
-
trimmed.startsWith("command ")
|
|
751
|
-
) {
|
|
752
|
-
const kind = trimmed.startsWith("calculation ")
|
|
753
|
-
? "calculation"
|
|
754
|
-
: trimmed.startsWith("rule ")
|
|
755
|
-
? "rule"
|
|
756
|
-
: trimmed.startsWith("label ")
|
|
757
|
-
? "label"
|
|
758
|
-
: trimmed.startsWith("action ")
|
|
759
|
-
? "action"
|
|
760
|
-
: trimmed.startsWith("policy ")
|
|
761
|
-
? "policy"
|
|
762
|
-
: trimmed.startsWith("view ")
|
|
763
|
-
? "view"
|
|
764
|
-
: trimmed.startsWith("route ")
|
|
765
|
-
? "route"
|
|
766
|
-
: trimmed.startsWith("workflow ")
|
|
767
|
-
? "workflow"
|
|
768
|
-
: "command";
|
|
769
|
-
const prefix = `${kind} `;
|
|
770
|
-
const name = trimmed.slice(prefix.length).trim();
|
|
771
|
-
const body = collectSemanticBody(lines, index + 1);
|
|
772
|
-
const params = new Map<string, string>();
|
|
773
|
-
let outputName =
|
|
774
|
-
kind === "label" ? "label" : kind === "policy" ? "policy" : kind === "view" ? "view" : kind === "route" ? "route" : "result";
|
|
775
|
-
const effects: string[] = [];
|
|
776
|
-
for (const line of body.lines) {
|
|
777
|
-
if (line.startsWith("input ")) {
|
|
778
|
-
const param = parseTypedBinding(line.slice("input ".length));
|
|
779
|
-
params.set(param.name, toIdentifier(param.name));
|
|
780
|
-
}
|
|
781
|
-
if (line.startsWith("output ")) {
|
|
782
|
-
const output = parseOutputBinding(line.slice("output ".length));
|
|
783
|
-
outputName = output.name;
|
|
784
|
-
}
|
|
785
|
-
if (line.startsWith("touches ")) effects.push(...line.slice("touches ".length).split(",").map((effect) => effect.trim()).filter(Boolean));
|
|
786
|
-
}
|
|
787
|
-
declarations.push({
|
|
788
|
-
kind,
|
|
789
|
-
name,
|
|
790
|
-
loweredName: semanticFunctionName(name, outputName, kind),
|
|
791
|
-
outputName,
|
|
792
|
-
params,
|
|
793
|
-
effects,
|
|
794
|
-
});
|
|
795
|
-
index = body.next;
|
|
796
|
-
continue;
|
|
797
|
-
}
|
|
798
|
-
if (trimmed.startsWith("external ")) {
|
|
799
|
-
const name = trimmed.slice("external ".length).trim();
|
|
800
|
-
const body = collectSemanticBody(lines, index + 1);
|
|
801
|
-
for (const line of body.lines) {
|
|
802
|
-
const match = line.match(/^(.+)\(/);
|
|
803
|
-
if (match) declarations.push({ kind: "external", name: match[1]?.trim() ?? name, loweredName: toIdentifier(match[1] ?? name) });
|
|
804
|
-
}
|
|
805
|
-
index = body.next;
|
|
806
|
-
continue;
|
|
807
|
-
}
|
|
808
|
-
index += 1;
|
|
809
|
-
}
|
|
810
|
-
return declarations;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
function collectExternalBindings(source: string): Map<string, string> {
|
|
814
|
-
const bindings = new Map<string, string>();
|
|
815
|
-
const lines = source.split(/\r?\n/);
|
|
816
|
-
let index = 0;
|
|
817
|
-
while (index < lines.length) {
|
|
818
|
-
const trimmed = (lines[index] ?? "").trim();
|
|
819
|
-
if (!trimmed.startsWith("external ")) {
|
|
820
|
-
index += 1;
|
|
821
|
-
continue;
|
|
822
|
-
}
|
|
823
|
-
const body = collectSemanticBody(lines, index + 1);
|
|
824
|
-
for (const line of body.lines) {
|
|
825
|
-
const match = line.match(/^(.+)\(/);
|
|
826
|
-
if (match) bindings.set(match[1]?.trim() ?? "", toIdentifier(match[1] ?? ""));
|
|
827
|
-
}
|
|
828
|
-
index = body.next;
|
|
829
|
-
}
|
|
830
|
-
return bindings;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
function collectCallableBindings(source: string): Map<string, string> {
|
|
834
|
-
const bindings = new Map<string, string>();
|
|
835
|
-
for (const declaration of collectSemanticDeclarationInfo(source)) {
|
|
836
|
-
if (
|
|
837
|
-
declaration.kind === "calculation" ||
|
|
838
|
-
declaration.kind === "rule" ||
|
|
839
|
-
declaration.kind === "label" ||
|
|
840
|
-
declaration.kind === "action" ||
|
|
841
|
-
declaration.kind === "policy" ||
|
|
842
|
-
declaration.kind === "view" ||
|
|
843
|
-
declaration.kind === "route" ||
|
|
844
|
-
declaration.kind === "workflow" ||
|
|
845
|
-
declaration.kind === "command"
|
|
846
|
-
) {
|
|
847
|
-
bindings.set(declaration.name, declaration.loweredName);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
return bindings;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
function findSemanticName(names: Map<string, string>, loweredName: string): string {
|
|
854
|
-
for (const [semanticName, candidate] of names) {
|
|
855
|
-
if (candidate === loweredName) return semanticName;
|
|
856
|
-
}
|
|
857
|
-
return loweredName;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
function collectSemanticBody(lines: string[], start: number): { lines: string[]; next: number } {
|
|
861
|
-
const body: string[] = [];
|
|
862
|
-
let index = start;
|
|
863
|
-
for (; index < lines.length; index += 1) {
|
|
864
|
-
const trimmed = (lines[index] ?? "").trim();
|
|
865
|
-
if (!trimmed) continue;
|
|
866
|
-
if (isSemanticTopLevel(trimmed)) break;
|
|
867
|
-
body.push(trimmed);
|
|
868
|
-
}
|
|
869
|
-
return { lines: body, next: index };
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
function isSemanticTopLevel(line: string): boolean {
|
|
873
|
-
return /^(module|use|record|calculation|rule|label|external|action|policy|view|route|workflow|command|type|fn|let|var|import)\s+/.test(line);
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
function isSemanticLoopBoundary(line: string): boolean {
|
|
877
|
-
return (
|
|
878
|
-
line.startsWith("input ") ||
|
|
879
|
-
line.startsWith("output ") ||
|
|
880
|
-
line.startsWith("return ") ||
|
|
881
|
-
line.startsWith("for each ") ||
|
|
882
|
-
/^(.+) starts (at|as) (.+)$/.test(line)
|
|
883
|
-
);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
function lowerSemanticMutationStatement(
|
|
887
|
-
line: string,
|
|
888
|
-
paramTypes: Map<string, string>,
|
|
889
|
-
records: Map<string, Map<string, string>>,
|
|
890
|
-
bindings: Map<string, string>,
|
|
891
|
-
): string[] {
|
|
892
|
-
const addTo = line.match(/^add (.+) to (.+)$/);
|
|
893
|
-
if (addTo) {
|
|
894
|
-
return [`${lowerExpression(addTo[2] ?? "", paramTypes, records, bindings)} += ${lowerExpression(addTo[1] ?? "", paramTypes, records, bindings)}`];
|
|
895
|
-
}
|
|
896
|
-
const subtractFrom = line.match(/^subtract (.+) from (.+)$/);
|
|
897
|
-
if (subtractFrom) {
|
|
898
|
-
return [`${lowerExpression(subtractFrom[2] ?? "", paramTypes, records, bindings)} -= ${lowerExpression(subtractFrom[1] ?? "", paramTypes, records, bindings)}`];
|
|
899
|
-
}
|
|
900
|
-
const setTo = line.match(/^set (.+) to (.+)$/);
|
|
901
|
-
if (setTo) {
|
|
902
|
-
return [`${lowerExpression(setTo[1] ?? "", paramTypes, records, bindings)} = ${lowerExpression(setTo[2] ?? "", paramTypes, records, bindings)}`];
|
|
903
|
-
}
|
|
904
|
-
return [];
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
function listItemTypeFor(source: string, paramTypes: Map<string, string>, bindings: Map<string, string>): string | null {
|
|
908
|
-
const trimmed = source.trim();
|
|
909
|
-
const identifier = bindings.get(trimmed) ?? toIdentifier(trimmed);
|
|
910
|
-
const type = paramTypes.get(identifier);
|
|
911
|
-
const match = type?.match(/^List<(.+)>$/);
|
|
912
|
-
return match?.[1] ?? null;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
function parseTypedBinding(source: string): { name: string; type: string } {
|
|
916
|
-
const colon = source.indexOf(":");
|
|
917
|
-
if (colon === -1) throw new Error(`Expected typed binding: ${source}`);
|
|
918
|
-
return { name: source.slice(0, colon).trim(), type: normalizeTypeExpressionSource(source.slice(colon + 1).trim()) };
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
function parseOutputBinding(source: string): { name: string; type: string } {
|
|
922
|
-
const colon = source.indexOf(":");
|
|
923
|
-
if (colon !== -1) return parseTypedBinding(source);
|
|
924
|
-
return { name: "result", type: normalizeTypeExpressionSource(source.trim()) };
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
function lowerExpression(
|
|
928
|
-
source: string,
|
|
929
|
-
paramTypes: Map<string, string>,
|
|
930
|
-
records: Map<string, Map<string, string>>,
|
|
931
|
-
bindings: Map<string, string> = new Map(),
|
|
932
|
-
): string {
|
|
933
|
-
let expression = source.trim();
|
|
934
|
-
expression = expression.replace(/^Error\s+"([^"]*)"$/, (_match, message: string) => `Error(${JSON.stringify(message)})`);
|
|
935
|
-
for (const [label, identifier] of [...bindings].sort((a, b) => b[0].length - a[0].length)) {
|
|
936
|
-
expression = replaceSemanticName(expression, label, identifier);
|
|
937
|
-
}
|
|
938
|
-
for (const [param, type] of paramTypes) {
|
|
939
|
-
const fields = records.get(type);
|
|
940
|
-
if (!fields) continue;
|
|
941
|
-
const labels = [...fields.keys()].sort((a, b) => b.length - a.length);
|
|
942
|
-
for (const label of labels) {
|
|
943
|
-
const field = fields.get(label);
|
|
944
|
-
if (!field) continue;
|
|
945
|
-
expression = expression.replaceAll(`${param}.${label}`, `${param}.${field}`);
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
for (const fields of records.values()) {
|
|
949
|
-
for (const [label, field] of [...fields].sort((a, b) => b[0].length - a[0].length)) {
|
|
950
|
-
expression = replaceRecordFieldLabel(expression, label, field);
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
return expression;
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
function replaceSemanticName(source: string, label: string, identifier: string): string {
|
|
957
|
-
if (label === identifier) return source;
|
|
958
|
-
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
959
|
-
return source.replace(new RegExp(`(?<![A-Za-z0-9_])${escaped}(?![A-Za-z0-9_])`, "g"), identifier);
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
function replaceRecordFieldLabel(source: string, label: string, identifier: string): string {
|
|
963
|
-
if (label === identifier) return source;
|
|
964
|
-
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
965
|
-
return source.replace(new RegExp(`([{,]\\s*)${escaped}(?=\\s*:)`, "g"), `$1${identifier}`);
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
function toIdentifier(label: string): string {
|
|
969
|
-
const words = label.match(/[A-Za-z0-9]+/g) ?? [];
|
|
970
|
-
return words.map((word, index) => (index === 0 ? word.toLowerCase() : toPascalCase(word))).join("");
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
function semanticFunctionName(
|
|
974
|
-
label: string,
|
|
975
|
-
outputName: string,
|
|
976
|
-
kind: "calculation" | "rule" | "label" | "action" | "policy" | "view" | "route" | "workflow" | "command",
|
|
977
|
-
): string {
|
|
978
|
-
const base = toIdentifier(label);
|
|
979
|
-
const suffix =
|
|
980
|
-
kind === "label"
|
|
981
|
-
? "Label"
|
|
982
|
-
: kind === "policy"
|
|
983
|
-
? "Policy"
|
|
984
|
-
: kind === "view"
|
|
985
|
-
? "View"
|
|
986
|
-
: kind === "route"
|
|
987
|
-
? "Route"
|
|
988
|
-
: kind === "workflow"
|
|
989
|
-
? "Workflow"
|
|
990
|
-
: kind === "command"
|
|
991
|
-
? "Command"
|
|
992
|
-
: toPascalCase(outputName);
|
|
993
|
-
if (!suffix) return base;
|
|
994
|
-
return base.toLowerCase().endsWith(suffix.toLowerCase()) ? base : `${base}${suffix}`;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
function toPascalCase(label: string): string {
|
|
998
|
-
const words = label.match(/[A-Za-z0-9]+/g) ?? [];
|
|
999
|
-
return words.map((word) => `${word.slice(0, 1).toUpperCase()}${word.slice(1)}`).join("");
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
function normalizeTypeExpressionSource(source: string): string {
|
|
1003
|
-
const trimmed = source.trim();
|
|
1004
|
-
const listMatch = trimmed.match(/^List<(.+)>$/);
|
|
1005
|
-
if (listMatch) return `List<${normalizeTypeExpressionSource(listMatch[1] ?? "")}>`;
|
|
1006
|
-
const maybeMatch = trimmed.match(/^Maybe<(.+)>$/);
|
|
1007
|
-
if (maybeMatch) return `Maybe<${normalizeTypeExpressionSource(maybeMatch[1] ?? "")}>`;
|
|
1008
|
-
const orParts = splitTopLevelOr(trimmed);
|
|
1009
|
-
if (orParts.length > 1) return `Or<${orParts.map(normalizeTypeExpressionSource).join(", ")}>`;
|
|
1010
|
-
if (
|
|
1011
|
-
trimmed === "Text" ||
|
|
1012
|
-
trimmed === "Int" ||
|
|
1013
|
-
trimmed === "Float" ||
|
|
1014
|
-
trimmed === "Bool" ||
|
|
1015
|
-
trimmed === "Void" ||
|
|
1016
|
-
trimmed === "Maybe" ||
|
|
1017
|
-
trimmed === "Error" ||
|
|
1018
|
-
trimmed === "Or"
|
|
1019
|
-
) {
|
|
1020
|
-
return trimmed;
|
|
1021
|
-
}
|
|
1022
|
-
return toPascalCase(trimmed);
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
function splitTopLevelOr(source: string): string[] {
|
|
1026
|
-
const parts: string[] = [];
|
|
1027
|
-
let depth = 0;
|
|
1028
|
-
let current = "";
|
|
1029
|
-
for (const token of source.split(/(\s+or\s+|[<>])/)) {
|
|
1030
|
-
if (!token) continue;
|
|
1031
|
-
if (token === "<") depth += 1;
|
|
1032
|
-
if (token === ">") depth -= 1;
|
|
1033
|
-
if (depth === 0 && /^\s+or\s+$/.test(token)) {
|
|
1034
|
-
parts.push(current.trim());
|
|
1035
|
-
current = "";
|
|
1036
|
-
continue;
|
|
1037
|
-
}
|
|
1038
|
-
current += token;
|
|
1039
|
-
}
|
|
1040
|
-
if (parts.length === 0) return [source];
|
|
1041
|
-
parts.push(current.trim());
|
|
1042
|
-
return parts;
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
function indentRaw(lines: string[]): string[] {
|
|
1046
|
-
return lines.map((line) => ` ${line}`);
|
|
1047
|
-
}
|
|
18
|
+
function lowerSemanticPointSyntax(source: string): string {
|
|
19
|
+
if (!isSemanticPointSyntax(source)) return source;
|
|
20
|
+
const lines = source.split(/\r?\n/);
|
|
21
|
+
const output: string[] = [];
|
|
22
|
+
const records = new Map<string, Map<string, string>>();
|
|
23
|
+
const externalBindings = new Map([...collectExternalBindings(source), ...collectCallableBindings(source)]);
|
|
24
|
+
let index = 0;
|
|
25
|
+
|
|
26
|
+
while (index < lines.length) {
|
|
27
|
+
const line = lines[index] ?? "";
|
|
28
|
+
const trimmed = line.trim();
|
|
29
|
+
if (!trimmed) {
|
|
30
|
+
index += 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (trimmed.startsWith("module ")) {
|
|
34
|
+
output.push(trimmed);
|
|
35
|
+
index += 1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (trimmed.startsWith("use ")) {
|
|
39
|
+
index += 1;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (trimmed.startsWith("record ")) {
|
|
43
|
+
const lowered = lowerRecord(lines, index, records);
|
|
44
|
+
output.push(...lowered.lines);
|
|
45
|
+
index = lowered.next;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (trimmed.startsWith("calculation ")) {
|
|
49
|
+
const lowered = lowerCalculation(lines, index, records, externalBindings);
|
|
50
|
+
output.push(...lowered.lines);
|
|
51
|
+
index = lowered.next;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (trimmed.startsWith("rule ")) {
|
|
55
|
+
const lowered = lowerRule(lines, index, records, externalBindings);
|
|
56
|
+
output.push(...lowered.lines);
|
|
57
|
+
index = lowered.next;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (trimmed.startsWith("label ")) {
|
|
61
|
+
const lowered = lowerLabel(lines, index, records, externalBindings);
|
|
62
|
+
output.push(...lowered.lines);
|
|
63
|
+
index = lowered.next;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (trimmed.startsWith("external ")) {
|
|
67
|
+
const lowered = lowerExternal(lines, index);
|
|
68
|
+
output.push(...lowered.lines);
|
|
69
|
+
index = lowered.next;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (trimmed.startsWith("action ")) {
|
|
73
|
+
const lowered = lowerAction(lines, index, records, externalBindings);
|
|
74
|
+
output.push(...lowered.lines);
|
|
75
|
+
index = lowered.next;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (trimmed.startsWith("policy ")) {
|
|
79
|
+
const lowered = lowerPolicy(lines, index, records, externalBindings);
|
|
80
|
+
output.push(...lowered.lines);
|
|
81
|
+
index = lowered.next;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (trimmed.startsWith("view ")) {
|
|
85
|
+
const lowered = lowerView(lines, index, records, externalBindings);
|
|
86
|
+
output.push(...lowered.lines);
|
|
87
|
+
index = lowered.next;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (trimmed.startsWith("route ")) {
|
|
91
|
+
const lowered = lowerRoute(lines, index, records, externalBindings);
|
|
92
|
+
output.push(...lowered.lines);
|
|
93
|
+
index = lowered.next;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (trimmed.startsWith("workflow ")) {
|
|
97
|
+
const lowered = lowerWorkflow(lines, index, records, externalBindings);
|
|
98
|
+
output.push(...lowered.lines);
|
|
99
|
+
index = lowered.next;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (trimmed.startsWith("command ")) {
|
|
103
|
+
const lowered = lowerCommand(lines, index, records, externalBindings);
|
|
104
|
+
output.push(...lowered.lines);
|
|
105
|
+
index = lowered.next;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
output.push(line);
|
|
109
|
+
index += 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return `${output.join("\n")}\n`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function assertSemanticPointSource(source: string) {
|
|
116
|
+
const oldStyleTopLevel = /^(import|type|let|var|fn)\s+/;
|
|
117
|
+
const lines = source.split(/\r?\n/);
|
|
118
|
+
let hasSemanticDeclaration = false;
|
|
119
|
+
|
|
120
|
+
for (const [index, line] of lines.entries()) {
|
|
121
|
+
const trimmed = line.trim();
|
|
122
|
+
if (!trimmed || trimmed.startsWith("//")) continue;
|
|
123
|
+
if (/^(record|calculation|rule|label|external|action|policy|view|route|workflow|command)\s+/.test(trimmed)) hasSemanticDeclaration = true;
|
|
124
|
+
if (oldStyleTopLevel.test(trimmed)) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Point source uses internal core syntax at ${index + 1}:1. Use record, calculation, rule, or label instead.`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!hasSemanticDeclaration) {
|
|
132
|
+
throw new Error("Point source must contain at least one semantic declaration: record, calculation, rule, or label.");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function lowerRecord(
|
|
137
|
+
lines: string[],
|
|
138
|
+
start: number,
|
|
139
|
+
records: Map<string, Map<string, string>>,
|
|
140
|
+
): { lines: string[]; next: number } {
|
|
141
|
+
const name = (lines[start] ?? "").trim().slice("record ".length).trim();
|
|
142
|
+
const typeName = toPascalCase(name);
|
|
143
|
+
const fields = new Map<string, string>();
|
|
144
|
+
const output = [`type ${typeName} {`];
|
|
145
|
+
let index = start + 1;
|
|
146
|
+
for (; index < lines.length; index += 1) {
|
|
147
|
+
const trimmed = (lines[index] ?? "").trim();
|
|
148
|
+
if (!trimmed) continue;
|
|
149
|
+
if (isSemanticTopLevel(trimmed)) break;
|
|
150
|
+
const colon = trimmed.indexOf(":");
|
|
151
|
+
if (colon === -1) throw new Error(`Expected field type in record ${name}: ${trimmed}`);
|
|
152
|
+
const label = trimmed.slice(0, colon).trim();
|
|
153
|
+
const fieldName = toIdentifier(label);
|
|
154
|
+
fields.set(label, fieldName);
|
|
155
|
+
output.push(` ${fieldName}: ${trimmed.slice(colon + 1).trim()}`);
|
|
156
|
+
}
|
|
157
|
+
output.push("}", "");
|
|
158
|
+
records.set(typeName, fields);
|
|
159
|
+
return { lines: output, next: index };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function lowerRule(
|
|
163
|
+
lines: string[],
|
|
164
|
+
start: number,
|
|
165
|
+
records: Map<string, Map<string, string>>,
|
|
166
|
+
externalBindings: Map<string, string> = new Map(),
|
|
167
|
+
): { lines: string[]; next: number } {
|
|
168
|
+
const label = (lines[start] ?? "").trim().slice("rule ".length).trim();
|
|
169
|
+
const body = collectSemanticBody(lines, start + 1);
|
|
170
|
+
const params: string[] = [];
|
|
171
|
+
const paramTypes = new Map<string, string>();
|
|
172
|
+
const bindings = new Map<string, string>(externalBindings);
|
|
173
|
+
let outputName = "result";
|
|
174
|
+
let outputType = "Void";
|
|
175
|
+
const statements: string[] = [];
|
|
176
|
+
|
|
177
|
+
for (let lineIndex = 0; lineIndex < body.lines.length; lineIndex += 1) {
|
|
178
|
+
const line = body.lines[lineIndex] ?? "";
|
|
179
|
+
if (line.startsWith("input ")) {
|
|
180
|
+
const param = parseTypedBinding(line.slice("input ".length));
|
|
181
|
+
const paramName = toIdentifier(param.name);
|
|
182
|
+
params.push(`${paramName}: ${param.type}`);
|
|
183
|
+
paramTypes.set(paramName, param.type);
|
|
184
|
+
bindings.set(param.name, paramName);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (line.startsWith("output ")) {
|
|
188
|
+
const output = parseOutputBinding(line.slice("output ".length));
|
|
189
|
+
outputName = toIdentifier(output.name);
|
|
190
|
+
outputType = output.type;
|
|
191
|
+
bindings.set(output.name, outputName);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const loop = line.match(/^for each (.+) in (.+)$/);
|
|
195
|
+
if (loop) {
|
|
196
|
+
const itemLabel = loop[1]?.trim() ?? "";
|
|
197
|
+
const itemName = toIdentifier(itemLabel);
|
|
198
|
+
const iterableSource = loop[2] ?? "";
|
|
199
|
+
const iterable = lowerExpression(iterableSource, paramTypes, records, bindings);
|
|
200
|
+
const loopParamTypes = new Map(paramTypes);
|
|
201
|
+
const itemType = listItemTypeFor(iterableSource, paramTypes, bindings);
|
|
202
|
+
if (itemType) loopParamTypes.set(itemName, itemType);
|
|
203
|
+
const loopBindings = new Map(bindings);
|
|
204
|
+
loopBindings.set(itemLabel, itemName);
|
|
205
|
+
const loopBody: string[] = [];
|
|
206
|
+
while (body.lines[lineIndex + 1] && !isSemanticLoopBoundary(body.lines[lineIndex + 1]!)) {
|
|
207
|
+
lineIndex += 1;
|
|
208
|
+
loopBody.push(...lowerSemanticMutationStatement(body.lines[lineIndex]!, loopParamTypes, records, loopBindings));
|
|
209
|
+
}
|
|
210
|
+
statements.push(`for ${itemName} in ${iterable} {`);
|
|
211
|
+
statements.push(...indentRaw(loopBody));
|
|
212
|
+
statements.push("}");
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const startsAt = line.match(/^(.+) starts at (.+)$/);
|
|
216
|
+
if (startsAt) {
|
|
217
|
+
const name = toIdentifier(startsAt[1] ?? "");
|
|
218
|
+
bindings.set(startsAt[1]?.trim() ?? name, name);
|
|
219
|
+
statements.push(`var ${name}: ${outputType} = ${lowerExpression(startsAt[2] ?? "", paramTypes, records, bindings)}`);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const addWhen = line.match(/^add (.+) when (.+)$/);
|
|
223
|
+
if (addWhen) {
|
|
224
|
+
statements.push(`if ${lowerExpression(addWhen[2] ?? "", paramTypes, records, bindings)} {`);
|
|
225
|
+
statements.push(` ${outputName} += ${lowerExpression(addWhen[1] ?? "", paramTypes, records, bindings)}`);
|
|
226
|
+
statements.push("}");
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const mutation = lowerSemanticMutationStatement(line, paramTypes, records, bindings);
|
|
230
|
+
if (mutation.length > 0) {
|
|
231
|
+
statements.push(...mutation);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (line.startsWith("return ")) {
|
|
235
|
+
statements.push(`return ${lowerExpression(line.slice("return ".length), paramTypes, records, bindings)}`);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
throw new Error(`Unknown rule statement: ${line}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const functionName = semanticFunctionName(label, outputName, "rule");
|
|
242
|
+
return {
|
|
243
|
+
lines: [`fn ${functionName}(${params.join(", ")}): ${outputType} {`, ...indentRaw(statements), "}", ""],
|
|
244
|
+
next: body.next,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function lowerCalculation(
|
|
249
|
+
lines: string[],
|
|
250
|
+
start: number,
|
|
251
|
+
records: Map<string, Map<string, string>>,
|
|
252
|
+
externalBindings: Map<string, string> = new Map(),
|
|
253
|
+
): { lines: string[]; next: number } {
|
|
254
|
+
const label = (lines[start] ?? "").trim().slice("calculation ".length).trim();
|
|
255
|
+
const body = collectSemanticBody(lines, start + 1);
|
|
256
|
+
const params: string[] = [];
|
|
257
|
+
const paramTypes = new Map<string, string>();
|
|
258
|
+
const bindings = new Map<string, string>(externalBindings);
|
|
259
|
+
let outputName = "result";
|
|
260
|
+
let outputType = "Void";
|
|
261
|
+
const statements: string[] = [];
|
|
262
|
+
|
|
263
|
+
for (let lineIndex = 0; lineIndex < body.lines.length; lineIndex += 1) {
|
|
264
|
+
const line = body.lines[lineIndex] ?? "";
|
|
265
|
+
if (line.startsWith("input ")) {
|
|
266
|
+
const param = parseTypedBinding(line.slice("input ".length));
|
|
267
|
+
const paramName = toIdentifier(param.name);
|
|
268
|
+
params.push(`${paramName}: ${param.type}`);
|
|
269
|
+
paramTypes.set(paramName, param.type);
|
|
270
|
+
bindings.set(param.name, paramName);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (line.startsWith("output ")) {
|
|
274
|
+
const output = parseOutputBinding(line.slice("output ".length));
|
|
275
|
+
outputName = toIdentifier(output.name);
|
|
276
|
+
outputType = output.type;
|
|
277
|
+
bindings.set(output.name, outputName);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const loop = line.match(/^for each (.+) in (.+)$/);
|
|
281
|
+
if (loop) {
|
|
282
|
+
const itemLabel = loop[1]?.trim() ?? "";
|
|
283
|
+
const itemName = toIdentifier(itemLabel);
|
|
284
|
+
const iterableSource = loop[2] ?? "";
|
|
285
|
+
const iterable = lowerExpression(iterableSource, paramTypes, records, bindings);
|
|
286
|
+
const loopParamTypes = new Map(paramTypes);
|
|
287
|
+
const itemType = listItemTypeFor(iterableSource, paramTypes, bindings);
|
|
288
|
+
if (itemType) loopParamTypes.set(itemName, itemType);
|
|
289
|
+
const loopBindings = new Map(bindings);
|
|
290
|
+
loopBindings.set(itemLabel, itemName);
|
|
291
|
+
const loopBody: string[] = [];
|
|
292
|
+
while (body.lines[lineIndex + 1] && !isSemanticLoopBoundary(body.lines[lineIndex + 1]!)) {
|
|
293
|
+
lineIndex += 1;
|
|
294
|
+
loopBody.push(...lowerSemanticMutationStatement(body.lines[lineIndex]!, loopParamTypes, records, loopBindings));
|
|
295
|
+
}
|
|
296
|
+
statements.push(`for ${itemName} in ${iterable} {`);
|
|
297
|
+
statements.push(...indentRaw(loopBody));
|
|
298
|
+
statements.push("}");
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
const isExpression = line.match(/^(.+) is (.+)$/);
|
|
302
|
+
if (isExpression) {
|
|
303
|
+
const name = toIdentifier(isExpression[1] ?? "");
|
|
304
|
+
if (name !== outputName) throw new Error(`Calculation ${label} can only assign its output ${outputName}`);
|
|
305
|
+
statements.push(`return ${lowerExpression(isExpression[2] ?? "", paramTypes, records, bindings)}`);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const startsAs = line.match(/^(.+) starts as (.+)$/);
|
|
309
|
+
if (startsAs) {
|
|
310
|
+
const name = toIdentifier(startsAs[1] ?? "");
|
|
311
|
+
bindings.set(startsAs[1]?.trim() ?? name, name);
|
|
312
|
+
statements.push(`var ${name}: ${outputType} = ${lowerExpression(startsAs[2] ?? "", paramTypes, records, bindings)}`);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const startsAt = line.match(/^(.+) starts at (.+)$/);
|
|
316
|
+
if (startsAt) {
|
|
317
|
+
const name = toIdentifier(startsAt[1] ?? "");
|
|
318
|
+
bindings.set(startsAt[1]?.trim() ?? name, name);
|
|
319
|
+
statements.push(`var ${name}: ${outputType} = ${lowerExpression(startsAt[2] ?? "", paramTypes, records, bindings)}`);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const mutation = lowerSemanticMutationStatement(line, paramTypes, records, bindings);
|
|
323
|
+
if (mutation.length > 0) {
|
|
324
|
+
statements.push(...mutation);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (line.startsWith("return ")) {
|
|
328
|
+
statements.push(`return ${lowerExpression(line.slice("return ".length), paramTypes, records, bindings)}`);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
throw new Error(`Unknown calculation statement: ${line}`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
lines: [`fn ${semanticFunctionName(label, outputName, "calculation")}(${params.join(", ")}): ${outputType} {`, ...indentRaw(statements), "}", ""],
|
|
336
|
+
next: body.next,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function lowerLabel(
|
|
341
|
+
lines: string[],
|
|
342
|
+
start: number,
|
|
343
|
+
records: Map<string, Map<string, string>>,
|
|
344
|
+
externalBindings: Map<string, string> = new Map(),
|
|
345
|
+
): { lines: string[]; next: number } {
|
|
346
|
+
const label = (lines[start] ?? "").trim().slice("label ".length).trim();
|
|
347
|
+
const body = collectSemanticBody(lines, start + 1);
|
|
348
|
+
const params: string[] = [];
|
|
349
|
+
const paramTypes = new Map<string, string>();
|
|
350
|
+
const bindings = new Map<string, string>(externalBindings);
|
|
351
|
+
let outputType = "Text";
|
|
352
|
+
const statements: string[] = [];
|
|
353
|
+
|
|
354
|
+
for (const line of body.lines) {
|
|
355
|
+
if (line.startsWith("input ")) {
|
|
356
|
+
const param = parseTypedBinding(line.slice("input ".length));
|
|
357
|
+
const paramName = toIdentifier(param.name);
|
|
358
|
+
params.push(`${paramName}: ${param.type}`);
|
|
359
|
+
paramTypes.set(paramName, param.type);
|
|
360
|
+
bindings.set(param.name, paramName);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (line.startsWith("output ")) {
|
|
364
|
+
outputType = parseOutputBinding(line.slice("output ".length)).type;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const whenReturn = line.match(/^when (.+) return (.+)$/);
|
|
368
|
+
if (whenReturn) {
|
|
369
|
+
statements.push(`if ${lowerExpression(whenReturn[1] ?? "", paramTypes, records, bindings)} {`);
|
|
370
|
+
statements.push(` return ${lowerExpression(whenReturn[2] ?? "", paramTypes, records, bindings)}`);
|
|
371
|
+
statements.push("}");
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (line.startsWith("otherwise return ")) {
|
|
375
|
+
statements.push(`return ${lowerExpression(line.slice("otherwise return ".length), paramTypes, records, bindings)}`);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
throw new Error(`Unknown label statement: ${line}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
lines: [`fn ${semanticFunctionName(label, "label", "label")}(${params.join(", ")}): ${outputType} {`, ...indentRaw(statements), "}", ""],
|
|
383
|
+
next: body.next,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function lowerExternal(lines: string[], start: number): { lines: string[]; next: number } {
|
|
388
|
+
const body = collectSemanticBody(lines, start + 1);
|
|
389
|
+
const output: string[] = [];
|
|
390
|
+
for (const line of body.lines) {
|
|
391
|
+
const match = line.match(/^(.+)\((.*)\):\s*(.+?)\s+from\s+"([^"]+)"(?:\s+as\s+([A-Za-z_][A-Za-z0-9_]*))?$/);
|
|
392
|
+
if (!match) throw new Error(`Unknown external declaration: ${line}`);
|
|
393
|
+
const name = toIdentifier(match[1] ?? "");
|
|
394
|
+
const params = (match[2] ?? "")
|
|
395
|
+
.split(",")
|
|
396
|
+
.map((param) => param.trim())
|
|
397
|
+
.filter(Boolean)
|
|
398
|
+
.map((param) => {
|
|
399
|
+
const binding = parseTypedBinding(param);
|
|
400
|
+
return `${toIdentifier(binding.name)}: ${binding.type}`;
|
|
401
|
+
});
|
|
402
|
+
const returnType = normalizeTypeExpressionSource(match[3] ?? "Void");
|
|
403
|
+
const importName = match[5] ? ` as ${match[5]}` : "";
|
|
404
|
+
output.push(`external fn ${name}(${params.join(", ")}): ${returnType} from ${JSON.stringify(match[4])}${importName}`);
|
|
405
|
+
}
|
|
406
|
+
output.push("");
|
|
407
|
+
return { lines: output, next: body.next };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function lowerAction(
|
|
411
|
+
lines: string[],
|
|
412
|
+
start: number,
|
|
413
|
+
records: Map<string, Map<string, string>>,
|
|
414
|
+
externalBindings: Map<string, string> = new Map(),
|
|
415
|
+
): { lines: string[]; next: number } {
|
|
416
|
+
const label = (lines[start] ?? "").trim().slice("action ".length).trim();
|
|
417
|
+
const body = collectSemanticBody(lines, start + 1);
|
|
418
|
+
const params: string[] = [];
|
|
419
|
+
const paramTypes = new Map<string, string>();
|
|
420
|
+
const bindings = new Map<string, string>(externalBindings);
|
|
421
|
+
let outputName = "result";
|
|
422
|
+
let outputType = "Void";
|
|
423
|
+
const statements: string[] = [];
|
|
424
|
+
|
|
425
|
+
for (const line of body.lines) {
|
|
426
|
+
if (line.startsWith("input ")) {
|
|
427
|
+
const param = parseTypedBinding(line.slice("input ".length));
|
|
428
|
+
const paramName = toIdentifier(param.name);
|
|
429
|
+
params.push(`${paramName}: ${param.type}`);
|
|
430
|
+
paramTypes.set(paramName, param.type);
|
|
431
|
+
bindings.set(param.name, paramName);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (line.startsWith("output ")) {
|
|
435
|
+
const output = parseOutputBinding(line.slice("output ".length));
|
|
436
|
+
outputName = toIdentifier(output.name);
|
|
437
|
+
outputType = output.type;
|
|
438
|
+
bindings.set(output.name, outputName);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (line.startsWith("touches ")) continue;
|
|
442
|
+
if (line.startsWith("return ")) {
|
|
443
|
+
statements.push(`return ${lowerExpression(line.slice("return ".length), paramTypes, records, bindings)}`);
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
throw new Error(`Unknown action statement: ${line}`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
lines: [`fn ${semanticFunctionName(label, outputName, "action")}(${params.join(", ")}): ${outputType} {`, ...indentRaw(statements), "}", ""],
|
|
451
|
+
next: body.next,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function lowerPolicy(
|
|
456
|
+
lines: string[],
|
|
457
|
+
start: number,
|
|
458
|
+
records: Map<string, Map<string, string>>,
|
|
459
|
+
externalBindings: Map<string, string> = new Map(),
|
|
460
|
+
): { lines: string[]; next: number } {
|
|
461
|
+
const label = (lines[start] ?? "").trim().slice("policy ".length).trim();
|
|
462
|
+
const body = collectSemanticBody(lines, start + 1);
|
|
463
|
+
const params: string[] = [];
|
|
464
|
+
const paramTypes = new Map<string, string>();
|
|
465
|
+
const bindings = new Map<string, string>(externalBindings);
|
|
466
|
+
const statements: string[] = [];
|
|
467
|
+
|
|
468
|
+
for (const line of body.lines) {
|
|
469
|
+
if (line.startsWith("input ")) {
|
|
470
|
+
const param = parseTypedBinding(line.slice("input ".length));
|
|
471
|
+
const paramName = toIdentifier(param.name);
|
|
472
|
+
params.push(`${paramName}: ${param.type}`);
|
|
473
|
+
paramTypes.set(paramName, param.type);
|
|
474
|
+
bindings.set(param.name, paramName);
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (line.startsWith("allow ") || line.startsWith("require ")) {
|
|
478
|
+
const expression = line.replace(/^(allow|require)\s+/, "");
|
|
479
|
+
statements.push(`return ${lowerExpression(expression, paramTypes, records, bindings)}`);
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
if (line.startsWith("deny ")) {
|
|
483
|
+
statements.push(`return ${lowerExpression(line.slice("deny ".length), paramTypes, records, bindings)} == false`);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
throw new Error(`Unknown policy statement: ${line}`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
lines: [`fn ${semanticFunctionName(label, "policy", "policy")}(${params.join(", ")}): Bool {`, ...indentRaw(statements), "}", ""],
|
|
491
|
+
next: body.next,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function lowerView(
|
|
496
|
+
lines: string[],
|
|
497
|
+
start: number,
|
|
498
|
+
records: Map<string, Map<string, string>>,
|
|
499
|
+
externalBindings: Map<string, string> = new Map(),
|
|
500
|
+
): { lines: string[]; next: number } {
|
|
501
|
+
const label = (lines[start] ?? "").trim().slice("view ".length).trim();
|
|
502
|
+
const body = collectSemanticBody(lines, start + 1);
|
|
503
|
+
const params: string[] = [];
|
|
504
|
+
const paramTypes = new Map<string, string>();
|
|
505
|
+
const bindings = new Map<string, string>(externalBindings);
|
|
506
|
+
const statements: string[] = [];
|
|
507
|
+
|
|
508
|
+
for (const line of body.lines) {
|
|
509
|
+
if (line.startsWith("input ")) {
|
|
510
|
+
const param = parseTypedBinding(line.slice("input ".length));
|
|
511
|
+
const paramName = toIdentifier(param.name);
|
|
512
|
+
params.push(`${paramName}: ${param.type}`);
|
|
513
|
+
paramTypes.set(paramName, param.type);
|
|
514
|
+
bindings.set(param.name, paramName);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (line.startsWith("render ")) {
|
|
518
|
+
statements.push(`return ${lowerExpression(line.slice("render ".length), paramTypes, records, bindings)}`);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
const whenRender = line.match(/^when (.+) render (.+)$/);
|
|
522
|
+
if (whenRender) {
|
|
523
|
+
statements.push(`if ${lowerExpression(whenRender[1] ?? "", paramTypes, records, bindings)} {`);
|
|
524
|
+
statements.push(` return ${lowerExpression(whenRender[2] ?? "", paramTypes, records, bindings)}`);
|
|
525
|
+
statements.push("}");
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
throw new Error(`Unknown view statement: ${line}`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
lines: [`fn ${semanticFunctionName(label, "view", "view")}(${params.join(", ")}): Text {`, ...indentRaw(statements), "}", ""],
|
|
533
|
+
next: body.next,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function lowerRoute(
|
|
538
|
+
lines: string[],
|
|
539
|
+
start: number,
|
|
540
|
+
records: Map<string, Map<string, string>>,
|
|
541
|
+
externalBindings: Map<string, string> = new Map(),
|
|
542
|
+
): { lines: string[]; next: number } {
|
|
543
|
+
const label = (lines[start] ?? "").trim().slice("route ".length).trim();
|
|
544
|
+
const body = collectSemanticBody(lines, start + 1);
|
|
545
|
+
const params: string[] = [];
|
|
546
|
+
const paramTypes = new Map<string, string>();
|
|
547
|
+
const bindings = new Map<string, string>(externalBindings);
|
|
548
|
+
let outputName = "response";
|
|
549
|
+
let outputType = "Text";
|
|
550
|
+
const statements: string[] = [];
|
|
551
|
+
|
|
552
|
+
for (const line of body.lines) {
|
|
553
|
+
if (line.startsWith("method ") || line.startsWith("path ")) continue;
|
|
554
|
+
if (line.startsWith("input ")) {
|
|
555
|
+
const param = parseTypedBinding(line.slice("input ".length));
|
|
556
|
+
const paramName = toIdentifier(param.name);
|
|
557
|
+
params.push(`${paramName}: ${param.type}`);
|
|
558
|
+
paramTypes.set(paramName, param.type);
|
|
559
|
+
bindings.set(param.name, paramName);
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
if (line.startsWith("output ")) {
|
|
563
|
+
const output = parseOutputBinding(line.slice("output ".length));
|
|
564
|
+
outputName = toIdentifier(output.name);
|
|
565
|
+
outputType = output.type;
|
|
566
|
+
bindings.set(output.name, outputName);
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (line.startsWith("return ")) {
|
|
570
|
+
statements.push(`return ${lowerExpression(line.slice("return ".length), paramTypes, records, bindings)}`);
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
throw new Error(`Unknown route statement: ${line}`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
lines: [`fn ${semanticFunctionName(label, "route", "route")}(${params.join(", ")}): ${outputType} {`, ...indentRaw(statements), "}", ""],
|
|
578
|
+
next: body.next,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function lowerWorkflow(
|
|
583
|
+
lines: string[],
|
|
584
|
+
start: number,
|
|
585
|
+
records: Map<string, Map<string, string>>,
|
|
586
|
+
externalBindings: Map<string, string> = new Map(),
|
|
587
|
+
): { lines: string[]; next: number } {
|
|
588
|
+
const label = (lines[start] ?? "").trim().slice("workflow ".length).trim();
|
|
589
|
+
const body = collectSemanticBody(lines, start + 1);
|
|
590
|
+
const params: string[] = [];
|
|
591
|
+
const paramTypes = new Map<string, string>();
|
|
592
|
+
const bindings = new Map<string, string>(externalBindings);
|
|
593
|
+
let outputName = "result";
|
|
594
|
+
let outputType = "Void";
|
|
595
|
+
const statements: string[] = [];
|
|
596
|
+
|
|
597
|
+
for (const line of body.lines) {
|
|
598
|
+
if (line.startsWith("input ")) {
|
|
599
|
+
const param = parseTypedBinding(line.slice("input ".length));
|
|
600
|
+
const paramName = toIdentifier(param.name);
|
|
601
|
+
params.push(`${paramName}: ${param.type}`);
|
|
602
|
+
paramTypes.set(paramName, param.type);
|
|
603
|
+
bindings.set(param.name, paramName);
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
if (line.startsWith("output ")) {
|
|
607
|
+
const output = parseOutputBinding(line.slice("output ".length));
|
|
608
|
+
outputName = toIdentifier(output.name);
|
|
609
|
+
outputType = output.type;
|
|
610
|
+
bindings.set(output.name, outputName);
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
const step = line.match(/^step (.+) is (.+)$/);
|
|
614
|
+
if (step) {
|
|
615
|
+
const name = toIdentifier(step[1] ?? "");
|
|
616
|
+
bindings.set(step[1]?.trim() ?? name, name);
|
|
617
|
+
statements.push(`let ${name}: ${outputType} = ${lowerExpression(step[2] ?? "", paramTypes, records, bindings)}`);
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (line.startsWith("return ")) {
|
|
621
|
+
statements.push(`return ${lowerExpression(line.slice("return ".length), paramTypes, records, bindings)}`);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
throw new Error(`Unknown workflow statement: ${line}`);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return {
|
|
628
|
+
lines: [`fn ${semanticFunctionName(label, "workflow", "workflow")}(${params.join(", ")}): ${outputType} {`, ...indentRaw(statements), "}", ""],
|
|
629
|
+
next: body.next,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function lowerCommand(
|
|
634
|
+
lines: string[],
|
|
635
|
+
start: number,
|
|
636
|
+
records: Map<string, Map<string, string>>,
|
|
637
|
+
externalBindings: Map<string, string> = new Map(),
|
|
638
|
+
): { lines: string[]; next: number } {
|
|
639
|
+
const label = (lines[start] ?? "").trim().slice("command ".length).trim();
|
|
640
|
+
const body = collectSemanticBody(lines, start + 1);
|
|
641
|
+
const params: string[] = [];
|
|
642
|
+
const paramTypes = new Map<string, string>();
|
|
643
|
+
const bindings = new Map<string, string>(externalBindings);
|
|
644
|
+
let outputName = "result";
|
|
645
|
+
let outputType = "Void";
|
|
646
|
+
const statements: string[] = [];
|
|
647
|
+
|
|
648
|
+
for (const line of body.lines) {
|
|
649
|
+
if (line.startsWith("input ")) {
|
|
650
|
+
const param = parseTypedBinding(line.slice("input ".length));
|
|
651
|
+
const paramName = toIdentifier(param.name);
|
|
652
|
+
params.push(`${paramName}: ${param.type}`);
|
|
653
|
+
paramTypes.set(paramName, param.type);
|
|
654
|
+
bindings.set(param.name, paramName);
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
if (line.startsWith("output ")) {
|
|
658
|
+
const output = parseOutputBinding(line.slice("output ".length));
|
|
659
|
+
outputName = toIdentifier(output.name);
|
|
660
|
+
outputType = output.type;
|
|
661
|
+
bindings.set(output.name, outputName);
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (line.startsWith("return ")) {
|
|
665
|
+
statements.push(`return ${lowerExpression(line.slice("return ".length), paramTypes, records, bindings)}`);
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
throw new Error(`Unknown command statement: ${line}`);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
lines: [`fn ${semanticFunctionName(label, "command", "command")}(${params.join(", ")}): ${outputType} {`, ...indentRaw(statements), "}", ""],
|
|
673
|
+
next: body.next,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
interface SemanticDeclarationInfo {
|
|
678
|
+
kind: "record" | "calculation" | "rule" | "label";
|
|
679
|
+
name: string;
|
|
680
|
+
loweredName: string;
|
|
681
|
+
outputName?: string;
|
|
682
|
+
fields?: Map<string, string>;
|
|
683
|
+
params?: Map<string, string>;
|
|
684
|
+
effects?: string[];
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function attachSemanticMetadata(program: PointCoreProgram, source: string) {
|
|
688
|
+
program.semantic = { source: "semantic" };
|
|
689
|
+
const declarations = collectSemanticDeclarationInfo(source);
|
|
690
|
+
const byLoweredName = new Map(declarations.map((declaration) => [declaration.loweredName, declaration]));
|
|
691
|
+
for (const declaration of program.declarations) {
|
|
692
|
+
if (declaration.kind !== "type" && declaration.kind !== "function" && declaration.kind !== "external") continue;
|
|
693
|
+
const semantic = byLoweredName.get(declaration.name);
|
|
694
|
+
if (!semantic) continue;
|
|
695
|
+
declaration.semantic = {
|
|
696
|
+
kind: semantic.kind,
|
|
697
|
+
name: semantic.name,
|
|
698
|
+
outputName: semantic.outputName,
|
|
699
|
+
effects: semantic.effects,
|
|
700
|
+
};
|
|
701
|
+
if (declaration.kind === "type") {
|
|
702
|
+
for (const field of declaration.fields) {
|
|
703
|
+
field.semanticName = semantic.fields ? findSemanticName(semantic.fields, field.name) : field.name;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (declaration.kind === "function" || declaration.kind === "external") {
|
|
707
|
+
for (const param of declaration.params) {
|
|
708
|
+
param.semanticName = semantic.params ? findSemanticName(semantic.params, param.name) : param.name;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function collectSemanticDeclarationInfo(source: string): SemanticDeclarationInfo[] {
|
|
715
|
+
const lines = source.split(/\r?\n/);
|
|
716
|
+
const records = new Map<string, Map<string, string>>();
|
|
717
|
+
const declarations: SemanticDeclarationInfo[] = [];
|
|
718
|
+
let index = 0;
|
|
719
|
+
while (index < lines.length) {
|
|
720
|
+
const trimmed = (lines[index] ?? "").trim();
|
|
721
|
+
if (!trimmed || trimmed.startsWith("module ")) {
|
|
722
|
+
index += 1;
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
if (trimmed.startsWith("record ")) {
|
|
726
|
+
const name = trimmed.slice("record ".length).trim();
|
|
727
|
+
const fields = new Map<string, string>();
|
|
728
|
+
index += 1;
|
|
729
|
+
for (; index < lines.length; index += 1) {
|
|
730
|
+
const line = (lines[index] ?? "").trim();
|
|
731
|
+
if (!line) continue;
|
|
732
|
+
if (isSemanticTopLevel(line)) break;
|
|
733
|
+
const colon = line.indexOf(":");
|
|
734
|
+
if (colon !== -1) fields.set(line.slice(0, colon).trim(), toIdentifier(line.slice(0, colon).trim()));
|
|
735
|
+
}
|
|
736
|
+
const loweredName = toPascalCase(name);
|
|
737
|
+
records.set(loweredName, fields);
|
|
738
|
+
declarations.push({ kind: "record", name, loweredName, fields });
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (
|
|
742
|
+
trimmed.startsWith("calculation ") ||
|
|
743
|
+
trimmed.startsWith("rule ") ||
|
|
744
|
+
trimmed.startsWith("label ") ||
|
|
745
|
+
trimmed.startsWith("action ") ||
|
|
746
|
+
trimmed.startsWith("policy ") ||
|
|
747
|
+
trimmed.startsWith("view ") ||
|
|
748
|
+
trimmed.startsWith("route ") ||
|
|
749
|
+
trimmed.startsWith("workflow ") ||
|
|
750
|
+
trimmed.startsWith("command ")
|
|
751
|
+
) {
|
|
752
|
+
const kind = trimmed.startsWith("calculation ")
|
|
753
|
+
? "calculation"
|
|
754
|
+
: trimmed.startsWith("rule ")
|
|
755
|
+
? "rule"
|
|
756
|
+
: trimmed.startsWith("label ")
|
|
757
|
+
? "label"
|
|
758
|
+
: trimmed.startsWith("action ")
|
|
759
|
+
? "action"
|
|
760
|
+
: trimmed.startsWith("policy ")
|
|
761
|
+
? "policy"
|
|
762
|
+
: trimmed.startsWith("view ")
|
|
763
|
+
? "view"
|
|
764
|
+
: trimmed.startsWith("route ")
|
|
765
|
+
? "route"
|
|
766
|
+
: trimmed.startsWith("workflow ")
|
|
767
|
+
? "workflow"
|
|
768
|
+
: "command";
|
|
769
|
+
const prefix = `${kind} `;
|
|
770
|
+
const name = trimmed.slice(prefix.length).trim();
|
|
771
|
+
const body = collectSemanticBody(lines, index + 1);
|
|
772
|
+
const params = new Map<string, string>();
|
|
773
|
+
let outputName =
|
|
774
|
+
kind === "label" ? "label" : kind === "policy" ? "policy" : kind === "view" ? "view" : kind === "route" ? "route" : "result";
|
|
775
|
+
const effects: string[] = [];
|
|
776
|
+
for (const line of body.lines) {
|
|
777
|
+
if (line.startsWith("input ")) {
|
|
778
|
+
const param = parseTypedBinding(line.slice("input ".length));
|
|
779
|
+
params.set(param.name, toIdentifier(param.name));
|
|
780
|
+
}
|
|
781
|
+
if (line.startsWith("output ")) {
|
|
782
|
+
const output = parseOutputBinding(line.slice("output ".length));
|
|
783
|
+
outputName = output.name;
|
|
784
|
+
}
|
|
785
|
+
if (line.startsWith("touches ")) effects.push(...line.slice("touches ".length).split(",").map((effect) => effect.trim()).filter(Boolean));
|
|
786
|
+
}
|
|
787
|
+
declarations.push({
|
|
788
|
+
kind,
|
|
789
|
+
name,
|
|
790
|
+
loweredName: semanticFunctionName(name, outputName, kind),
|
|
791
|
+
outputName,
|
|
792
|
+
params,
|
|
793
|
+
effects,
|
|
794
|
+
});
|
|
795
|
+
index = body.next;
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
if (trimmed.startsWith("external ")) {
|
|
799
|
+
const name = trimmed.slice("external ".length).trim();
|
|
800
|
+
const body = collectSemanticBody(lines, index + 1);
|
|
801
|
+
for (const line of body.lines) {
|
|
802
|
+
const match = line.match(/^(.+)\(/);
|
|
803
|
+
if (match) declarations.push({ kind: "external", name: match[1]?.trim() ?? name, loweredName: toIdentifier(match[1] ?? name) });
|
|
804
|
+
}
|
|
805
|
+
index = body.next;
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
index += 1;
|
|
809
|
+
}
|
|
810
|
+
return declarations;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function collectExternalBindings(source: string): Map<string, string> {
|
|
814
|
+
const bindings = new Map<string, string>();
|
|
815
|
+
const lines = source.split(/\r?\n/);
|
|
816
|
+
let index = 0;
|
|
817
|
+
while (index < lines.length) {
|
|
818
|
+
const trimmed = (lines[index] ?? "").trim();
|
|
819
|
+
if (!trimmed.startsWith("external ")) {
|
|
820
|
+
index += 1;
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
const body = collectSemanticBody(lines, index + 1);
|
|
824
|
+
for (const line of body.lines) {
|
|
825
|
+
const match = line.match(/^(.+)\(/);
|
|
826
|
+
if (match) bindings.set(match[1]?.trim() ?? "", toIdentifier(match[1] ?? ""));
|
|
827
|
+
}
|
|
828
|
+
index = body.next;
|
|
829
|
+
}
|
|
830
|
+
return bindings;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function collectCallableBindings(source: string): Map<string, string> {
|
|
834
|
+
const bindings = new Map<string, string>();
|
|
835
|
+
for (const declaration of collectSemanticDeclarationInfo(source)) {
|
|
836
|
+
if (
|
|
837
|
+
declaration.kind === "calculation" ||
|
|
838
|
+
declaration.kind === "rule" ||
|
|
839
|
+
declaration.kind === "label" ||
|
|
840
|
+
declaration.kind === "action" ||
|
|
841
|
+
declaration.kind === "policy" ||
|
|
842
|
+
declaration.kind === "view" ||
|
|
843
|
+
declaration.kind === "route" ||
|
|
844
|
+
declaration.kind === "workflow" ||
|
|
845
|
+
declaration.kind === "command"
|
|
846
|
+
) {
|
|
847
|
+
bindings.set(declaration.name, declaration.loweredName);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return bindings;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function findSemanticName(names: Map<string, string>, loweredName: string): string {
|
|
854
|
+
for (const [semanticName, candidate] of names) {
|
|
855
|
+
if (candidate === loweredName) return semanticName;
|
|
856
|
+
}
|
|
857
|
+
return loweredName;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function collectSemanticBody(lines: string[], start: number): { lines: string[]; next: number } {
|
|
861
|
+
const body: string[] = [];
|
|
862
|
+
let index = start;
|
|
863
|
+
for (; index < lines.length; index += 1) {
|
|
864
|
+
const trimmed = (lines[index] ?? "").trim();
|
|
865
|
+
if (!trimmed) continue;
|
|
866
|
+
if (isSemanticTopLevel(trimmed)) break;
|
|
867
|
+
body.push(trimmed);
|
|
868
|
+
}
|
|
869
|
+
return { lines: body, next: index };
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function isSemanticTopLevel(line: string): boolean {
|
|
873
|
+
return /^(module|use|record|calculation|rule|label|external|action|policy|view|route|workflow|command|type|fn|let|var|import)\s+/.test(line);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function isSemanticLoopBoundary(line: string): boolean {
|
|
877
|
+
return (
|
|
878
|
+
line.startsWith("input ") ||
|
|
879
|
+
line.startsWith("output ") ||
|
|
880
|
+
line.startsWith("return ") ||
|
|
881
|
+
line.startsWith("for each ") ||
|
|
882
|
+
/^(.+) starts (at|as) (.+)$/.test(line)
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function lowerSemanticMutationStatement(
|
|
887
|
+
line: string,
|
|
888
|
+
paramTypes: Map<string, string>,
|
|
889
|
+
records: Map<string, Map<string, string>>,
|
|
890
|
+
bindings: Map<string, string>,
|
|
891
|
+
): string[] {
|
|
892
|
+
const addTo = line.match(/^add (.+) to (.+)$/);
|
|
893
|
+
if (addTo) {
|
|
894
|
+
return [`${lowerExpression(addTo[2] ?? "", paramTypes, records, bindings)} += ${lowerExpression(addTo[1] ?? "", paramTypes, records, bindings)}`];
|
|
895
|
+
}
|
|
896
|
+
const subtractFrom = line.match(/^subtract (.+) from (.+)$/);
|
|
897
|
+
if (subtractFrom) {
|
|
898
|
+
return [`${lowerExpression(subtractFrom[2] ?? "", paramTypes, records, bindings)} -= ${lowerExpression(subtractFrom[1] ?? "", paramTypes, records, bindings)}`];
|
|
899
|
+
}
|
|
900
|
+
const setTo = line.match(/^set (.+) to (.+)$/);
|
|
901
|
+
if (setTo) {
|
|
902
|
+
return [`${lowerExpression(setTo[1] ?? "", paramTypes, records, bindings)} = ${lowerExpression(setTo[2] ?? "", paramTypes, records, bindings)}`];
|
|
903
|
+
}
|
|
904
|
+
return [];
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function listItemTypeFor(source: string, paramTypes: Map<string, string>, bindings: Map<string, string>): string | null {
|
|
908
|
+
const trimmed = source.trim();
|
|
909
|
+
const identifier = bindings.get(trimmed) ?? toIdentifier(trimmed);
|
|
910
|
+
const type = paramTypes.get(identifier);
|
|
911
|
+
const match = type?.match(/^List<(.+)>$/);
|
|
912
|
+
return match?.[1] ?? null;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function parseTypedBinding(source: string): { name: string; type: string } {
|
|
916
|
+
const colon = source.indexOf(":");
|
|
917
|
+
if (colon === -1) throw new Error(`Expected typed binding: ${source}`);
|
|
918
|
+
return { name: source.slice(0, colon).trim(), type: normalizeTypeExpressionSource(source.slice(colon + 1).trim()) };
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function parseOutputBinding(source: string): { name: string; type: string } {
|
|
922
|
+
const colon = source.indexOf(":");
|
|
923
|
+
if (colon !== -1) return parseTypedBinding(source);
|
|
924
|
+
return { name: "result", type: normalizeTypeExpressionSource(source.trim()) };
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function lowerExpression(
|
|
928
|
+
source: string,
|
|
929
|
+
paramTypes: Map<string, string>,
|
|
930
|
+
records: Map<string, Map<string, string>>,
|
|
931
|
+
bindings: Map<string, string> = new Map(),
|
|
932
|
+
): string {
|
|
933
|
+
let expression = source.trim();
|
|
934
|
+
expression = expression.replace(/^Error\s+"([^"]*)"$/, (_match, message: string) => `Error(${JSON.stringify(message)})`);
|
|
935
|
+
for (const [label, identifier] of [...bindings].sort((a, b) => b[0].length - a[0].length)) {
|
|
936
|
+
expression = replaceSemanticName(expression, label, identifier);
|
|
937
|
+
}
|
|
938
|
+
for (const [param, type] of paramTypes) {
|
|
939
|
+
const fields = records.get(type);
|
|
940
|
+
if (!fields) continue;
|
|
941
|
+
const labels = [...fields.keys()].sort((a, b) => b.length - a.length);
|
|
942
|
+
for (const label of labels) {
|
|
943
|
+
const field = fields.get(label);
|
|
944
|
+
if (!field) continue;
|
|
945
|
+
expression = expression.replaceAll(`${param}.${label}`, `${param}.${field}`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
for (const fields of records.values()) {
|
|
949
|
+
for (const [label, field] of [...fields].sort((a, b) => b[0].length - a[0].length)) {
|
|
950
|
+
expression = replaceRecordFieldLabel(expression, label, field);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return expression;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function replaceSemanticName(source: string, label: string, identifier: string): string {
|
|
957
|
+
if (label === identifier) return source;
|
|
958
|
+
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
959
|
+
return source.replace(new RegExp(`(?<![A-Za-z0-9_])${escaped}(?![A-Za-z0-9_])`, "g"), identifier);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function replaceRecordFieldLabel(source: string, label: string, identifier: string): string {
|
|
963
|
+
if (label === identifier) return source;
|
|
964
|
+
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
965
|
+
return source.replace(new RegExp(`([{,]\\s*)${escaped}(?=\\s*:)`, "g"), `$1${identifier}`);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function toIdentifier(label: string): string {
|
|
969
|
+
const words = label.match(/[A-Za-z0-9]+/g) ?? [];
|
|
970
|
+
return words.map((word, index) => (index === 0 ? word.toLowerCase() : toPascalCase(word))).join("");
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function semanticFunctionName(
|
|
974
|
+
label: string,
|
|
975
|
+
outputName: string,
|
|
976
|
+
kind: "calculation" | "rule" | "label" | "action" | "policy" | "view" | "route" | "workflow" | "command",
|
|
977
|
+
): string {
|
|
978
|
+
const base = toIdentifier(label);
|
|
979
|
+
const suffix =
|
|
980
|
+
kind === "label"
|
|
981
|
+
? "Label"
|
|
982
|
+
: kind === "policy"
|
|
983
|
+
? "Policy"
|
|
984
|
+
: kind === "view"
|
|
985
|
+
? "View"
|
|
986
|
+
: kind === "route"
|
|
987
|
+
? "Route"
|
|
988
|
+
: kind === "workflow"
|
|
989
|
+
? "Workflow"
|
|
990
|
+
: kind === "command"
|
|
991
|
+
? "Command"
|
|
992
|
+
: toPascalCase(outputName);
|
|
993
|
+
if (!suffix) return base;
|
|
994
|
+
return base.toLowerCase().endsWith(suffix.toLowerCase()) ? base : `${base}${suffix}`;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function toPascalCase(label: string): string {
|
|
998
|
+
const words = label.match(/[A-Za-z0-9]+/g) ?? [];
|
|
999
|
+
return words.map((word) => `${word.slice(0, 1).toUpperCase()}${word.slice(1)}`).join("");
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function normalizeTypeExpressionSource(source: string): string {
|
|
1003
|
+
const trimmed = source.trim();
|
|
1004
|
+
const listMatch = trimmed.match(/^List<(.+)>$/);
|
|
1005
|
+
if (listMatch) return `List<${normalizeTypeExpressionSource(listMatch[1] ?? "")}>`;
|
|
1006
|
+
const maybeMatch = trimmed.match(/^Maybe<(.+)>$/);
|
|
1007
|
+
if (maybeMatch) return `Maybe<${normalizeTypeExpressionSource(maybeMatch[1] ?? "")}>`;
|
|
1008
|
+
const orParts = splitTopLevelOr(trimmed);
|
|
1009
|
+
if (orParts.length > 1) return `Or<${orParts.map(normalizeTypeExpressionSource).join(", ")}>`;
|
|
1010
|
+
if (
|
|
1011
|
+
trimmed === "Text" ||
|
|
1012
|
+
trimmed === "Int" ||
|
|
1013
|
+
trimmed === "Float" ||
|
|
1014
|
+
trimmed === "Bool" ||
|
|
1015
|
+
trimmed === "Void" ||
|
|
1016
|
+
trimmed === "Maybe" ||
|
|
1017
|
+
trimmed === "Error" ||
|
|
1018
|
+
trimmed === "Or"
|
|
1019
|
+
) {
|
|
1020
|
+
return trimmed;
|
|
1021
|
+
}
|
|
1022
|
+
return toPascalCase(trimmed);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function splitTopLevelOr(source: string): string[] {
|
|
1026
|
+
const parts: string[] = [];
|
|
1027
|
+
let depth = 0;
|
|
1028
|
+
let current = "";
|
|
1029
|
+
for (const token of source.split(/(\s+or\s+|[<>])/)) {
|
|
1030
|
+
if (!token) continue;
|
|
1031
|
+
if (token === "<") depth += 1;
|
|
1032
|
+
if (token === ">") depth -= 1;
|
|
1033
|
+
if (depth === 0 && /^\s+or\s+$/.test(token)) {
|
|
1034
|
+
parts.push(current.trim());
|
|
1035
|
+
current = "";
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
current += token;
|
|
1039
|
+
}
|
|
1040
|
+
if (parts.length === 0) return [source];
|
|
1041
|
+
parts.push(current.trim());
|
|
1042
|
+
return parts;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function indentRaw(lines: string[]): string[] {
|
|
1046
|
+
return lines.map((line) => ` ${line}`);
|
|
1047
|
+
}
|