@hatchingpoint/point 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -5
- package/package.json +1 -1
- package/src/core/ast.ts +40 -2
- package/src/core/check.ts +178 -67
- package/src/core/cli.ts +332 -51
- package/src/core/context.ts +213 -36
- package/src/core/emit-javascript.ts +124 -0
- package/src/core/emit-typescript.ts +38 -5
- package/src/core/format.ts +4 -102
- package/src/core/incremental.ts +53 -0
- package/src/core/index.ts +5 -0
- package/src/core/lexer.ts +11 -6
- package/src/core/parser.ts +11 -612
- package/src/core/semantic-source.ts +26 -0
- package/src/core/serialize.ts +18 -0
- package/src/core/test-only/core-text-parser.ts +415 -0
- package/src/core/test-only/format-core.ts +120 -0
- package/src/core/test-only/index.ts +3 -0
- package/src/core/test-only/legacy-lowering.ts +1047 -0
- package/src/semantic/ast.ts +230 -0
- package/src/semantic/callables.ts +51 -0
- package/src/semantic/context.ts +347 -0
- package/src/semantic/desugar.ts +665 -0
- package/src/semantic/expressions.ts +347 -0
- package/src/semantic/format.ts +222 -0
- package/src/semantic/index.ts +10 -0
- package/src/semantic/metadata.ts +37 -0
- package/src/semantic/naming.ts +33 -0
- package/src/semantic/parse.ts +945 -0
- package/src/semantic/serialize.ts +18 -0
package/src/core/parser.ts
CHANGED
|
@@ -1,612 +1,11 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
PointSourceSpan,
|
|
13
|
-
} from "./ast.ts";
|
|
14
|
-
import { lexPointCore, type PointCoreToken, type PointCoreTokenType } from "./lexer.ts";
|
|
15
|
-
|
|
16
|
-
export class PointCoreParserError extends Error {
|
|
17
|
-
constructor(message: string, public readonly token: PointCoreToken) {
|
|
18
|
-
super(`${message} at ${token.span.start.line}:${token.span.start.column}`);
|
|
19
|
-
this.name = "PointCoreParserError";
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function parsePointCore(source: string): PointCoreProgram {
|
|
24
|
-
const parser = new CoreParser(lexPointCore(lowerSemanticPointSyntax(source)));
|
|
25
|
-
return parser.parseProgram();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function isSemanticPointSyntax(source: string): boolean {
|
|
29
|
-
return source
|
|
30
|
-
.split(/\r?\n/)
|
|
31
|
-
.some((line) => /^(record|rule|label)\s+/.test(line.trim()));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function lowerSemanticPointSyntax(source: string): string {
|
|
35
|
-
if (!isSemanticPointSyntax(source)) return source;
|
|
36
|
-
const lines = source.split(/\r?\n/);
|
|
37
|
-
const output: string[] = [];
|
|
38
|
-
const records = new Map<string, Map<string, string>>();
|
|
39
|
-
let index = 0;
|
|
40
|
-
|
|
41
|
-
while (index < lines.length) {
|
|
42
|
-
const line = lines[index] ?? "";
|
|
43
|
-
const trimmed = line.trim();
|
|
44
|
-
if (!trimmed) {
|
|
45
|
-
index += 1;
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
if (trimmed.startsWith("module ")) {
|
|
49
|
-
output.push(trimmed);
|
|
50
|
-
index += 1;
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
if (trimmed.startsWith("record ")) {
|
|
54
|
-
const lowered = lowerRecord(lines, index, records);
|
|
55
|
-
output.push(...lowered.lines);
|
|
56
|
-
index = lowered.next;
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
if (trimmed.startsWith("rule ")) {
|
|
60
|
-
const lowered = lowerRule(lines, index, records);
|
|
61
|
-
output.push(...lowered.lines);
|
|
62
|
-
index = lowered.next;
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
if (trimmed.startsWith("label ")) {
|
|
66
|
-
const lowered = lowerLabel(lines, index, records);
|
|
67
|
-
output.push(...lowered.lines);
|
|
68
|
-
index = lowered.next;
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
output.push(line);
|
|
72
|
-
index += 1;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return `${output.join("\n")}\n`;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function lowerRecord(
|
|
79
|
-
lines: string[],
|
|
80
|
-
start: number,
|
|
81
|
-
records: Map<string, Map<string, string>>,
|
|
82
|
-
): { lines: string[]; next: number } {
|
|
83
|
-
const name = (lines[start] ?? "").trim().slice("record ".length).trim();
|
|
84
|
-
const fields = new Map<string, string>();
|
|
85
|
-
const output = [`type ${name} {`];
|
|
86
|
-
let index = start + 1;
|
|
87
|
-
for (; index < lines.length; index += 1) {
|
|
88
|
-
const trimmed = (lines[index] ?? "").trim();
|
|
89
|
-
if (!trimmed) continue;
|
|
90
|
-
if (isSemanticTopLevel(trimmed)) break;
|
|
91
|
-
const colon = trimmed.indexOf(":");
|
|
92
|
-
if (colon === -1) throw new Error(`Expected field type in record ${name}: ${trimmed}`);
|
|
93
|
-
const label = trimmed.slice(0, colon).trim();
|
|
94
|
-
const fieldName = toIdentifier(label);
|
|
95
|
-
fields.set(label, fieldName);
|
|
96
|
-
output.push(` ${fieldName}: ${trimmed.slice(colon + 1).trim()}`);
|
|
97
|
-
}
|
|
98
|
-
output.push("}", "");
|
|
99
|
-
records.set(name, fields);
|
|
100
|
-
return { lines: output, next: index };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function lowerRule(
|
|
104
|
-
lines: string[],
|
|
105
|
-
start: number,
|
|
106
|
-
records: Map<string, Map<string, string>>,
|
|
107
|
-
): { lines: string[]; next: number } {
|
|
108
|
-
const label = (lines[start] ?? "").trim().slice("rule ".length).trim();
|
|
109
|
-
const body = collectSemanticBody(lines, start + 1);
|
|
110
|
-
const params: string[] = [];
|
|
111
|
-
const paramTypes = new Map<string, string>();
|
|
112
|
-
let outputName = "result";
|
|
113
|
-
let outputType = "Void";
|
|
114
|
-
const statements: string[] = [];
|
|
115
|
-
|
|
116
|
-
for (const line of body.lines) {
|
|
117
|
-
if (line.startsWith("input ")) {
|
|
118
|
-
const param = parseTypedBinding(line.slice("input ".length));
|
|
119
|
-
params.push(`${param.name}: ${param.type}`);
|
|
120
|
-
paramTypes.set(param.name, param.type);
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
if (line.startsWith("output ")) {
|
|
124
|
-
const output = parseOutputBinding(line.slice("output ".length));
|
|
125
|
-
outputName = output.name;
|
|
126
|
-
outputType = output.type;
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
const startsAt = line.match(/^([A-Za-z_][A-Za-z0-9_]*) starts at (.+)$/);
|
|
130
|
-
if (startsAt) {
|
|
131
|
-
statements.push(`var ${startsAt[1]}: ${outputType} = ${lowerExpression(startsAt[2] ?? "", paramTypes, records)}`);
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
const addWhen = line.match(/^add (.+) when (.+)$/);
|
|
135
|
-
if (addWhen) {
|
|
136
|
-
statements.push(`if ${lowerExpression(addWhen[2] ?? "", paramTypes, records)} {`);
|
|
137
|
-
statements.push(` ${outputName} += ${lowerExpression(addWhen[1] ?? "", paramTypes, records)}`);
|
|
138
|
-
statements.push("}");
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
if (line.startsWith("return ")) {
|
|
142
|
-
statements.push(`return ${lowerExpression(line.slice("return ".length), paramTypes, records)}`);
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
throw new Error(`Unknown rule statement: ${line}`);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const functionName = `${toIdentifier(label)}${toPascalCase(outputName)}`;
|
|
149
|
-
return {
|
|
150
|
-
lines: [`fn ${functionName}(${params.join(", ")}): ${outputType} {`, ...indentRaw(statements), "}", ""],
|
|
151
|
-
next: body.next,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function lowerLabel(
|
|
156
|
-
lines: string[],
|
|
157
|
-
start: number,
|
|
158
|
-
records: Map<string, Map<string, string>>,
|
|
159
|
-
): { lines: string[]; next: number } {
|
|
160
|
-
const label = (lines[start] ?? "").trim().slice("label ".length).trim();
|
|
161
|
-
const body = collectSemanticBody(lines, start + 1);
|
|
162
|
-
const params: string[] = [];
|
|
163
|
-
const paramTypes = new Map<string, string>();
|
|
164
|
-
let outputType = "Text";
|
|
165
|
-
const statements: string[] = [];
|
|
166
|
-
|
|
167
|
-
for (const line of body.lines) {
|
|
168
|
-
if (line.startsWith("input ")) {
|
|
169
|
-
const param = parseTypedBinding(line.slice("input ".length));
|
|
170
|
-
params.push(`${param.name}: ${param.type}`);
|
|
171
|
-
paramTypes.set(param.name, param.type);
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
if (line.startsWith("output ")) {
|
|
175
|
-
outputType = parseOutputBinding(line.slice("output ".length)).type;
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
const whenReturn = line.match(/^when (.+) return (.+)$/);
|
|
179
|
-
if (whenReturn) {
|
|
180
|
-
statements.push(`if ${lowerExpression(whenReturn[1] ?? "", paramTypes, records)} {`);
|
|
181
|
-
statements.push(` return ${lowerExpression(whenReturn[2] ?? "", paramTypes, records)}`);
|
|
182
|
-
statements.push("}");
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
if (line.startsWith("otherwise return ")) {
|
|
186
|
-
statements.push(`return ${lowerExpression(line.slice("otherwise return ".length), paramTypes, records)}`);
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
throw new Error(`Unknown label statement: ${line}`);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return {
|
|
193
|
-
lines: [`fn ${toIdentifier(label)}Label(${params.join(", ")}): ${outputType} {`, ...indentRaw(statements), "}", ""],
|
|
194
|
-
next: body.next,
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function collectSemanticBody(lines: string[], start: number): { lines: string[]; next: number } {
|
|
199
|
-
const body: string[] = [];
|
|
200
|
-
let index = start;
|
|
201
|
-
for (; index < lines.length; index += 1) {
|
|
202
|
-
const trimmed = (lines[index] ?? "").trim();
|
|
203
|
-
if (!trimmed) continue;
|
|
204
|
-
if (isSemanticTopLevel(trimmed)) break;
|
|
205
|
-
body.push(trimmed);
|
|
206
|
-
}
|
|
207
|
-
return { lines: body, next: index };
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function isSemanticTopLevel(line: string): boolean {
|
|
211
|
-
return /^(module|record|rule|label|type|fn|let|var|import)\s+/.test(line);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function parseTypedBinding(source: string): { name: string; type: string } {
|
|
215
|
-
const colon = source.indexOf(":");
|
|
216
|
-
if (colon === -1) throw new Error(`Expected typed binding: ${source}`);
|
|
217
|
-
return { name: source.slice(0, colon).trim(), type: source.slice(colon + 1).trim() };
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function parseOutputBinding(source: string): { name: string; type: string } {
|
|
221
|
-
const colon = source.indexOf(":");
|
|
222
|
-
if (colon !== -1) return parseTypedBinding(source);
|
|
223
|
-
return { name: "result", type: source.trim() };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function lowerExpression(
|
|
227
|
-
source: string,
|
|
228
|
-
paramTypes: Map<string, string>,
|
|
229
|
-
records: Map<string, Map<string, string>>,
|
|
230
|
-
): string {
|
|
231
|
-
let expression = source.trim();
|
|
232
|
-
for (const [param, type] of paramTypes) {
|
|
233
|
-
const fields = records.get(type);
|
|
234
|
-
if (!fields) continue;
|
|
235
|
-
const labels = [...fields.keys()].sort((a, b) => b.length - a.length);
|
|
236
|
-
for (const label of labels) {
|
|
237
|
-
const field = fields.get(label);
|
|
238
|
-
if (!field) continue;
|
|
239
|
-
expression = expression.replaceAll(`${param}.${label}`, `${param}.${field}`);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return expression;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function toIdentifier(label: string): string {
|
|
246
|
-
const words = label.match(/[A-Za-z0-9]+/g) ?? [];
|
|
247
|
-
return words.map((word, index) => (index === 0 ? word.toLowerCase() : toPascalCase(word))).join("");
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function toPascalCase(label: string): string {
|
|
251
|
-
const words = label.match(/[A-Za-z0-9]+/g) ?? [];
|
|
252
|
-
return words.map((word) => `${word.slice(0, 1).toUpperCase()}${word.slice(1)}`).join("");
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function indentRaw(lines: string[]): string[] {
|
|
256
|
-
return lines.map((line) => ` ${line}`);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
class CoreParser {
|
|
260
|
-
private current = 0;
|
|
261
|
-
|
|
262
|
-
constructor(private readonly tokens: PointCoreToken[]) {}
|
|
263
|
-
|
|
264
|
-
parseProgram(): PointCoreProgram {
|
|
265
|
-
let moduleName: string | undefined;
|
|
266
|
-
const declarations: PointCoreDeclaration[] = [];
|
|
267
|
-
const start = this.peek().span.start;
|
|
268
|
-
while (!this.check("eof")) {
|
|
269
|
-
if (this.matchKeyword("module")) {
|
|
270
|
-
moduleName = this.consume("identifier", "Expected module name").value;
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
declarations.push(this.parseDeclaration());
|
|
274
|
-
}
|
|
275
|
-
return {
|
|
276
|
-
kind: "coreProgram",
|
|
277
|
-
module: moduleName,
|
|
278
|
-
declarations,
|
|
279
|
-
span: { start, end: this.peek().span.end },
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
private parseDeclaration(): PointCoreDeclaration {
|
|
284
|
-
if (this.matchKeyword("import")) return this.parseImport();
|
|
285
|
-
if (this.checkKeyword("let") || this.checkKeyword("var")) return this.parseValueDeclaration();
|
|
286
|
-
if (this.matchKeyword("fn")) return this.parseFunction();
|
|
287
|
-
if (this.matchKeyword("type")) return this.parseTypeDeclaration();
|
|
288
|
-
throw new PointCoreParserError("Expected import, let, var, fn, or type declaration", this.peek());
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
private parseImport(): PointCoreDeclaration {
|
|
292
|
-
const start = this.previous().span.start;
|
|
293
|
-
this.consume("leftBrace", "Expected import list");
|
|
294
|
-
const names: string[] = [];
|
|
295
|
-
do {
|
|
296
|
-
names.push(this.consume("identifier", "Expected imported name").value);
|
|
297
|
-
} while (this.match("comma"));
|
|
298
|
-
this.consume("rightBrace", "Expected end of import list");
|
|
299
|
-
this.consumeKeyword("from");
|
|
300
|
-
const from = this.consume("string", "Expected import source").value;
|
|
301
|
-
return { kind: "import", names, from, span: { start, end: this.previous().span.end } };
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
private parseValueDeclaration(): PointCoreValueDeclaration {
|
|
305
|
-
const keyword = this.advance();
|
|
306
|
-
const name = this.consume("identifier", "Expected value name");
|
|
307
|
-
this.consume("colon", `Expected type annotation for ${name.value}`);
|
|
308
|
-
const type = this.parseTypeExpression();
|
|
309
|
-
this.consume("equals", `Expected initializer for ${name.value}`);
|
|
310
|
-
const value = this.parseExpression();
|
|
311
|
-
return {
|
|
312
|
-
kind: "value",
|
|
313
|
-
mutable: keyword.value === "var",
|
|
314
|
-
name: name.value,
|
|
315
|
-
type,
|
|
316
|
-
value,
|
|
317
|
-
span: { start: keyword.span.start, end: value.span?.end ?? this.previous().span.end },
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
private parseFunction(): PointCoreFunctionDeclaration {
|
|
322
|
-
const start = this.previous().span.start;
|
|
323
|
-
const name = this.consume("identifier", "Expected function name").value;
|
|
324
|
-
this.consume("leftParen", "Expected function parameters");
|
|
325
|
-
const params = this.parseParameters();
|
|
326
|
-
this.consume("rightParen", "Expected end of function parameters");
|
|
327
|
-
this.consume("colon", "Expected function return type");
|
|
328
|
-
const returnType = this.parseTypeExpression();
|
|
329
|
-
this.consume("leftBrace", "Expected function body");
|
|
330
|
-
const body: PointCoreStatement[] = [];
|
|
331
|
-
while (!this.check("rightBrace")) body.push(this.parseStatement());
|
|
332
|
-
this.consume("rightBrace", "Expected end of function body");
|
|
333
|
-
return { kind: "function", name, params, returnType, body, span: { start, end: this.previous().span.end } };
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
private parseTypeDeclaration(): PointCoreDeclaration {
|
|
337
|
-
const start = this.previous().span.start;
|
|
338
|
-
const name = this.consume("identifier", "Expected type name").value;
|
|
339
|
-
this.consume("leftBrace", "Expected type body");
|
|
340
|
-
const fields: PointCoreParameter[] = [];
|
|
341
|
-
while (!this.check("rightBrace")) {
|
|
342
|
-
fields.push(this.parseParameter());
|
|
343
|
-
}
|
|
344
|
-
this.consume("rightBrace", "Expected end of type body");
|
|
345
|
-
return { kind: "type", name, fields, span: { start, end: this.previous().span.end } };
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
private parseStatement(): PointCoreStatement {
|
|
349
|
-
if (this.matchKeyword("return")) {
|
|
350
|
-
const start = this.previous().span.start;
|
|
351
|
-
if (this.check("rightBrace")) return { kind: "return", span: { start, end: this.previous().span.end } };
|
|
352
|
-
const value = this.parseExpression();
|
|
353
|
-
return { kind: "return", value, span: { start, end: value.span?.end ?? this.previous().span.end } };
|
|
354
|
-
}
|
|
355
|
-
if (this.matchKeyword("if")) return this.parseIfStatement();
|
|
356
|
-
if (this.checkKeyword("let") || this.checkKeyword("var")) return this.parseValueDeclaration();
|
|
357
|
-
if (this.check("identifier") && (this.checkNext("equals") || this.checkNext("plusEquals"))) return this.parseAssignment();
|
|
358
|
-
const value = this.parseExpression();
|
|
359
|
-
return { kind: "expression", value, span: value.span };
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
private parseAssignment(): PointCoreStatement {
|
|
363
|
-
const name = this.consume("identifier", "Expected assignment target");
|
|
364
|
-
const operator = this.match("plusEquals") ? "+=" : "=";
|
|
365
|
-
if (operator === "=") this.consume("equals", "Expected assignment operator");
|
|
366
|
-
const value = this.parseExpression();
|
|
367
|
-
return {
|
|
368
|
-
kind: "assignment",
|
|
369
|
-
name: name.value,
|
|
370
|
-
operator,
|
|
371
|
-
value,
|
|
372
|
-
span: { start: name.span.start, end: value.span?.end ?? this.previous().span.end },
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
private parseIfStatement(): PointCoreStatement {
|
|
377
|
-
const start = this.previous().span.start;
|
|
378
|
-
const condition = this.parseExpression();
|
|
379
|
-
const thenBody = this.parseBlockStatements("Expected if body");
|
|
380
|
-
let elseBody: PointCoreStatement[] = [];
|
|
381
|
-
if (this.matchKeyword("else")) {
|
|
382
|
-
elseBody = this.parseBlockStatements("Expected else body");
|
|
383
|
-
}
|
|
384
|
-
return {
|
|
385
|
-
kind: "if",
|
|
386
|
-
condition,
|
|
387
|
-
thenBody,
|
|
388
|
-
elseBody,
|
|
389
|
-
span: { start, end: this.previous().span.end },
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
private parseBlockStatements(message: string): PointCoreStatement[] {
|
|
394
|
-
this.consume("leftBrace", message);
|
|
395
|
-
const statements: PointCoreStatement[] = [];
|
|
396
|
-
while (!this.check("rightBrace")) statements.push(this.parseStatement());
|
|
397
|
-
this.consume("rightBrace", "Expected end of block");
|
|
398
|
-
return statements;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
private parseParameters(): PointCoreParameter[] {
|
|
402
|
-
const params: PointCoreParameter[] = [];
|
|
403
|
-
if (this.check("rightParen")) return params;
|
|
404
|
-
do {
|
|
405
|
-
params.push(this.parseParameter());
|
|
406
|
-
} while (this.match("comma"));
|
|
407
|
-
return params;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
private parseParameter(): PointCoreParameter {
|
|
411
|
-
const name = this.consume("identifier", "Expected parameter name");
|
|
412
|
-
this.consume("colon", `Expected type annotation for ${name.value}`);
|
|
413
|
-
const type = this.parseTypeExpression();
|
|
414
|
-
return { name: name.value, type, span: { start: name.span.start, end: type.span?.end ?? name.span.end } };
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
private parseTypeExpression(): PointCoreTypeExpression {
|
|
418
|
-
const token = this.consume("identifier", "Expected type name");
|
|
419
|
-
const args: PointCoreTypeExpression[] = [];
|
|
420
|
-
if (this.match("less")) {
|
|
421
|
-
do {
|
|
422
|
-
args.push(this.parseTypeExpression());
|
|
423
|
-
} while (this.match("comma"));
|
|
424
|
-
this.consume("greater", "Expected end of type arguments");
|
|
425
|
-
}
|
|
426
|
-
return { kind: "typeRef", name: token.value, args, span: { start: token.span.start, end: this.previous().span.end } };
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
private parseExpression(): PointCoreExpression {
|
|
430
|
-
return this.parseBinaryExpression(0);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
private parseBinaryExpression(minPrecedence: number): PointCoreExpression {
|
|
434
|
-
let left = this.parsePrimaryExpression();
|
|
435
|
-
while (true) {
|
|
436
|
-
const operator = this.peekBinaryOperator();
|
|
437
|
-
if (!operator) break;
|
|
438
|
-
const precedence = precedenceFor(operator);
|
|
439
|
-
if (precedence < minPrecedence) break;
|
|
440
|
-
this.advance();
|
|
441
|
-
const right = this.parseBinaryExpression(precedence + 1);
|
|
442
|
-
left = {
|
|
443
|
-
kind: "binary",
|
|
444
|
-
operator,
|
|
445
|
-
left,
|
|
446
|
-
right,
|
|
447
|
-
span: { start: left.span?.start ?? this.previous().span.start, end: right.span?.end ?? this.previous().span.end },
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
return left;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
private parsePrimaryExpression(): PointCoreExpression {
|
|
454
|
-
let expression = this.parseAtomExpression();
|
|
455
|
-
while (this.match("dot")) {
|
|
456
|
-
const name = this.consume("identifier", "Expected property name");
|
|
457
|
-
expression = {
|
|
458
|
-
kind: "property",
|
|
459
|
-
target: expression,
|
|
460
|
-
name: name.value,
|
|
461
|
-
span: { start: expression.span?.start ?? name.span.start, end: name.span.end },
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
return expression;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
private parseAtomExpression(): PointCoreExpression {
|
|
468
|
-
if (this.check("string")) {
|
|
469
|
-
const token = this.advance();
|
|
470
|
-
return { kind: "literal", value: token.value, span: token.span };
|
|
471
|
-
}
|
|
472
|
-
if (this.check("number")) {
|
|
473
|
-
const token = this.advance();
|
|
474
|
-
return { kind: "literal", value: Number(token.value), span: token.span };
|
|
475
|
-
}
|
|
476
|
-
if (this.match("leftParen")) {
|
|
477
|
-
const start = this.previous().span.start;
|
|
478
|
-
const expression = this.parseExpression();
|
|
479
|
-
this.consume("rightParen", "Expected end of grouped expression");
|
|
480
|
-
return { ...expression, span: { start, end: this.previous().span.end } };
|
|
481
|
-
}
|
|
482
|
-
if (this.match("leftBracket")) return this.parseListExpression();
|
|
483
|
-
if (this.match("leftBrace")) return this.parseRecordExpression();
|
|
484
|
-
const identifier = this.consume("identifier", "Expected expression");
|
|
485
|
-
if (identifier.value === "true") return { kind: "literal", value: true, span: identifier.span };
|
|
486
|
-
if (identifier.value === "false") return { kind: "literal", value: false, span: identifier.span };
|
|
487
|
-
if (this.match("leftParen")) {
|
|
488
|
-
const args: PointCoreExpression[] = [];
|
|
489
|
-
if (!this.check("rightParen")) {
|
|
490
|
-
do {
|
|
491
|
-
args.push(this.parseExpression());
|
|
492
|
-
} while (this.match("comma"));
|
|
493
|
-
}
|
|
494
|
-
this.consume("rightParen", "Expected end of call arguments");
|
|
495
|
-
return {
|
|
496
|
-
kind: "call",
|
|
497
|
-
callee: identifier.value,
|
|
498
|
-
args,
|
|
499
|
-
span: { start: identifier.span.start, end: this.previous().span.end },
|
|
500
|
-
};
|
|
501
|
-
}
|
|
502
|
-
return { kind: "identifier", name: identifier.value, span: identifier.span };
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
private parseListExpression(): PointCoreExpression {
|
|
506
|
-
const start = this.previous().span.start;
|
|
507
|
-
const items: PointCoreExpression[] = [];
|
|
508
|
-
if (!this.check("rightBracket")) {
|
|
509
|
-
do {
|
|
510
|
-
items.push(this.parseExpression());
|
|
511
|
-
} while (this.match("comma"));
|
|
512
|
-
}
|
|
513
|
-
this.consume("rightBracket", "Expected end of list");
|
|
514
|
-
return { kind: "list", items, span: { start, end: this.previous().span.end } };
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
private parseRecordExpression(): PointCoreExpression {
|
|
518
|
-
const start = this.previous().span.start;
|
|
519
|
-
const fields: PointCoreRecordField[] = [];
|
|
520
|
-
if (!this.check("rightBrace")) {
|
|
521
|
-
do {
|
|
522
|
-
const name = this.consume("identifier", "Expected record field name");
|
|
523
|
-
this.consume("colon", `Expected value for record field ${name.value}`);
|
|
524
|
-
const value = this.parseExpression();
|
|
525
|
-
fields.push({
|
|
526
|
-
name: name.value,
|
|
527
|
-
value,
|
|
528
|
-
span: { start: name.span.start, end: value.span?.end ?? name.span.end },
|
|
529
|
-
});
|
|
530
|
-
} while (this.match("comma"));
|
|
531
|
-
}
|
|
532
|
-
this.consume("rightBrace", "Expected end of record");
|
|
533
|
-
return { kind: "record", fields, span: { start, end: this.previous().span.end } };
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
private peekBinaryOperator(): PointCoreBinaryOperator | null {
|
|
537
|
-
const token = this.peek();
|
|
538
|
-
if (token.type === "plus") return "+";
|
|
539
|
-
if (token.type === "minus") return "-";
|
|
540
|
-
if (token.type === "star") return "*";
|
|
541
|
-
if (token.type === "slash") return "/";
|
|
542
|
-
if (token.type === "equalsEquals") return "==";
|
|
543
|
-
if (token.type === "bangEquals") return "!=";
|
|
544
|
-
if (token.type === "less") return "<";
|
|
545
|
-
if (token.type === "lessEquals") return "<=";
|
|
546
|
-
if (token.type === "greater") return ">";
|
|
547
|
-
if (token.type === "greaterEquals") return ">=";
|
|
548
|
-
if (token.type === "identifier" && (token.value === "and" || token.value === "or")) return token.value;
|
|
549
|
-
return null;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
private consumeKeyword(keyword: string) {
|
|
553
|
-
const token = this.consume("identifier", `Expected ${keyword}`);
|
|
554
|
-
if (token.value !== keyword) throw new PointCoreParserError(`Expected ${keyword}`, token);
|
|
555
|
-
return token;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
private matchKeyword(keyword: string) {
|
|
559
|
-
if (!this.checkKeyword(keyword)) return false;
|
|
560
|
-
this.advance();
|
|
561
|
-
return true;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
private checkKeyword(keyword: string) {
|
|
565
|
-
return this.check("identifier") && this.peek().value === keyword;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
private consume(type: PointCoreTokenType, message: string) {
|
|
569
|
-
if (this.check(type)) return this.advance();
|
|
570
|
-
throw new PointCoreParserError(message, this.peek());
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
private match(type: PointCoreTokenType) {
|
|
574
|
-
if (!this.check(type)) return false;
|
|
575
|
-
this.advance();
|
|
576
|
-
return true;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
private check(type: PointCoreTokenType) {
|
|
580
|
-
return this.peek().type === type;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
private checkNext(type: PointCoreTokenType) {
|
|
584
|
-
return this.peek(1).type === type;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
private advance() {
|
|
588
|
-
if (!this.check("eof")) this.current += 1;
|
|
589
|
-
return this.previous();
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
private peek(offset = 0) {
|
|
593
|
-
return this.tokens[this.current + offset] ?? this.tokens[this.tokens.length - 1]!;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
private previous() {
|
|
597
|
-
return this.tokens[this.current - 1] ?? this.tokens[0]!;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function precedenceFor(operator: PointCoreBinaryOperator): number {
|
|
602
|
-
if (operator === "or") return 1;
|
|
603
|
-
if (operator === "and") return 2;
|
|
604
|
-
if (operator === "==" || operator === "!=") return 3;
|
|
605
|
-
if (operator === "<" || operator === "<=" || operator === ">" || operator === ">=") return 4;
|
|
606
|
-
if (operator === "+" || operator === "-") return 5;
|
|
607
|
-
return 6;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
export function mergeSpans(start: PointSourceSpan, end: PointSourceSpan): PointSourceSpan {
|
|
611
|
-
return { start: start.start, end: end.end };
|
|
612
|
-
}
|
|
1
|
+
import type { PointCoreProgram } from "./ast.ts";
|
|
2
|
+
import { desugarSemanticProgram } from "../semantic/desugar.ts";
|
|
3
|
+
import { parseSemanticSource } from "../semantic/parse.ts";
|
|
4
|
+
import { assertSemanticPointSource } from "./semantic-source.ts";
|
|
5
|
+
|
|
6
|
+
export { isSemanticPointSyntax } from "./semantic-source.ts";
|
|
7
|
+
|
|
8
|
+
export function parsePointSource(source: string): PointCoreProgram {
|
|
9
|
+
assertSemanticPointSource(source);
|
|
10
|
+
return desugarSemanticProgram(parseSemanticSource(source));
|
|
11
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function isSemanticPointSyntax(source: string): boolean {
|
|
2
|
+
return source
|
|
3
|
+
.split(/\r?\n/)
|
|
4
|
+
.some((line) => /^(use|record|calculation|rule|label|external|action|policy|view|route|workflow|command)\s+/.test(line.trim()));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function assertSemanticPointSource(source: string) {
|
|
8
|
+
const oldStyleTopLevel = /^(import|type|let|var|fn)\s+/;
|
|
9
|
+
const lines = source.split(/\r?\n/);
|
|
10
|
+
let hasSemanticDeclaration = false;
|
|
11
|
+
|
|
12
|
+
for (const [index, line] of lines.entries()) {
|
|
13
|
+
const trimmed = line.trim();
|
|
14
|
+
if (!trimmed || trimmed.startsWith("//")) continue;
|
|
15
|
+
if (/^(record|calculation|rule|label|external|action|policy|view|route|workflow|command)\s+/.test(trimmed)) hasSemanticDeclaration = true;
|
|
16
|
+
if (oldStyleTopLevel.test(trimmed)) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Point source uses internal core syntax at ${index + 1}:1. Use record, calculation, rule, or label instead.`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!hasSemanticDeclaration) {
|
|
24
|
+
throw new Error("Point source must contain at least one semantic declaration: record, calculation, rule, or label.");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PointCoreProgram } from "./ast.ts";
|
|
2
|
+
|
|
3
|
+
export function serializeCoreProgram(program: PointCoreProgram): string {
|
|
4
|
+
return `${JSON.stringify(stripSpans(program), null, 2)}\n`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function stripSpans(value: unknown): unknown {
|
|
8
|
+
if (Array.isArray(value)) return value.map(stripSpans);
|
|
9
|
+
if (value && typeof value === "object") {
|
|
10
|
+
const output: Record<string, unknown> = {};
|
|
11
|
+
for (const [key, child] of Object.entries(value)) {
|
|
12
|
+
if (key === "span" || key === "semanticSource") continue;
|
|
13
|
+
output[key] = stripSpans(child);
|
|
14
|
+
}
|
|
15
|
+
return output;
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|