@hatchingpoint/point 0.0.3 → 0.0.5
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/package.json +1 -1
- package/src/core/ast.ts +7 -0
- package/src/core/check.ts +52 -15
- package/src/core/cli.ts +15 -3
- package/src/core/emit-typescript.ts +1 -0
- package/src/core/format.ts +1 -0
- package/src/core/lexer.ts +5 -0
- package/src/core/parser.ts +253 -3
package/package.json
CHANGED
package/src/core/ast.ts
CHANGED
|
@@ -78,6 +78,13 @@ export interface PointCoreRecordField {
|
|
|
78
78
|
export type PointCoreStatement =
|
|
79
79
|
| { kind: "return"; value?: PointCoreExpression; span?: PointSourceSpan }
|
|
80
80
|
| PointCoreValueDeclaration
|
|
81
|
+
| {
|
|
82
|
+
kind: "assignment";
|
|
83
|
+
name: string;
|
|
84
|
+
operator: "=" | "+=";
|
|
85
|
+
value: PointCoreExpression;
|
|
86
|
+
span?: PointSourceSpan;
|
|
87
|
+
}
|
|
81
88
|
| {
|
|
82
89
|
kind: "if";
|
|
83
90
|
condition: PointCoreExpression;
|
package/src/core/check.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface PointCoreDiagnostic {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
type DiagnosticMetadata = Partial<Pick<PointCoreDiagnostic, "expected" | "actual" | "repair" | "relatedRefs">>;
|
|
27
|
+
type ScopeEntry = { type: PointCoreTypeExpression; mutable: boolean };
|
|
28
|
+
type Scope = Map<string, ScopeEntry>;
|
|
27
29
|
|
|
28
30
|
const PRIMITIVE_TYPES = new Set(["Text", "Int", "Float", "Bool", "Void", "List"]);
|
|
29
31
|
|
|
@@ -36,7 +38,7 @@ class CoreChecker {
|
|
|
36
38
|
private readonly diagnostics: PointCoreDiagnostic[] = [];
|
|
37
39
|
private readonly types = new Set(PRIMITIVE_TYPES);
|
|
38
40
|
private readonly typeDeclarations = new Map<string, PointCoreTypeDeclaration>();
|
|
39
|
-
private readonly globals = new Map
|
|
41
|
+
private readonly globals: Scope = new Map();
|
|
40
42
|
private readonly functions = new Map<string, PointCoreFunctionDeclaration>();
|
|
41
43
|
|
|
42
44
|
constructor(private readonly program: PointCoreProgram) {}
|
|
@@ -70,7 +72,7 @@ class CoreChecker {
|
|
|
70
72
|
if (this.globals.has(declaration.name)) {
|
|
71
73
|
this.push("duplicate-value", `Duplicate value ${declaration.name}`, `value.${declaration.name}`, declaration.span);
|
|
72
74
|
}
|
|
73
|
-
this.globals.set(declaration.name, declaration.type);
|
|
75
|
+
this.globals.set(declaration.name, { type: declaration.type, mutable: declaration.mutable });
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
private checkDeclaration(declaration: PointCoreDeclaration) {
|
|
@@ -92,7 +94,7 @@ class CoreChecker {
|
|
|
92
94
|
const locals = new Map(this.globals);
|
|
93
95
|
for (const param of declaration.params) {
|
|
94
96
|
this.checkType(param.type, `fn.${declaration.name}.${param.name}.type`);
|
|
95
|
-
locals.set(param.name, param.type);
|
|
97
|
+
locals.set(param.name, { type: param.type, mutable: false });
|
|
96
98
|
}
|
|
97
99
|
for (const statement of declaration.body) {
|
|
98
100
|
this.checkStatement(statement, declaration, locals);
|
|
@@ -102,7 +104,7 @@ class CoreChecker {
|
|
|
102
104
|
private checkStatement(
|
|
103
105
|
statement: PointCoreStatement,
|
|
104
106
|
fn: PointCoreFunctionDeclaration,
|
|
105
|
-
locals:
|
|
107
|
+
locals: Scope,
|
|
106
108
|
) {
|
|
107
109
|
if (statement.kind === "return") {
|
|
108
110
|
if (!statement.value) {
|
|
@@ -117,7 +119,11 @@ class CoreChecker {
|
|
|
117
119
|
if (statement.kind === "value") {
|
|
118
120
|
this.checkType(statement.type, `fn.${fn.name}.${statement.name}.type`);
|
|
119
121
|
this.checkExpressionAssignable(statement.value, statement.type, `fn.${fn.name}.${statement.name}.value`, locals);
|
|
120
|
-
locals.set(statement.name, statement.type);
|
|
122
|
+
locals.set(statement.name, { type: statement.type, mutable: statement.mutable });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (statement.kind === "assignment") {
|
|
126
|
+
this.checkAssignment(statement, fn, locals);
|
|
121
127
|
return;
|
|
122
128
|
}
|
|
123
129
|
if (statement.kind === "if") {
|
|
@@ -131,11 +137,42 @@ class CoreChecker {
|
|
|
131
137
|
this.typeOfExpression(statement.value, locals, `fn.${fn.name}.expression`);
|
|
132
138
|
}
|
|
133
139
|
|
|
140
|
+
private checkAssignment(
|
|
141
|
+
statement: Extract<PointCoreStatement, { kind: "assignment" }>,
|
|
142
|
+
fn: PointCoreFunctionDeclaration,
|
|
143
|
+
locals: Scope,
|
|
144
|
+
) {
|
|
145
|
+
const target = locals.get(statement.name);
|
|
146
|
+
const path = `fn.${fn.name}.${statement.name}.assignment`;
|
|
147
|
+
if (!target) {
|
|
148
|
+
this.push("unknown-identifier", `Unknown identifier ${statement.name}`, path, statement.span, {
|
|
149
|
+
actual: statement.name,
|
|
150
|
+
repair: `Declare var ${statement.name}: <Type> before assigning to it.`,
|
|
151
|
+
});
|
|
152
|
+
this.typeOfExpression(statement.value, locals, `${path}.value`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (!target.mutable) {
|
|
156
|
+
this.push("immutable-assignment", `Cannot assign to immutable value ${statement.name}`, path, statement.span, {
|
|
157
|
+
actual: statement.name,
|
|
158
|
+
repair: `Declare ${statement.name} with var if it needs to change.`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (statement.operator === "+=" && !isNumeric(String(target.type.name))) {
|
|
162
|
+
this.push("operator-type-mismatch", "+= requires a numeric target", path, statement.span, {
|
|
163
|
+
expected: "Int or Float target",
|
|
164
|
+
actual: formatType(target.type),
|
|
165
|
+
repair: "Use += only with Int or Float values.",
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
this.checkExpressionAssignable(statement.value, target.type, `${path}.value`, locals);
|
|
169
|
+
}
|
|
170
|
+
|
|
134
171
|
private checkExpressionAssignable(
|
|
135
172
|
expression: PointCoreExpression,
|
|
136
173
|
expected: PointCoreTypeExpression,
|
|
137
174
|
path: string,
|
|
138
|
-
scope:
|
|
175
|
+
scope: Scope,
|
|
139
176
|
) {
|
|
140
177
|
if (expression.kind === "list") {
|
|
141
178
|
this.checkListAssignable(expression, expected, path, scope);
|
|
@@ -159,7 +196,7 @@ class CoreChecker {
|
|
|
159
196
|
expression: Extract<PointCoreExpression, { kind: "list" }>,
|
|
160
197
|
expected: PointCoreTypeExpression,
|
|
161
198
|
path: string,
|
|
162
|
-
scope:
|
|
199
|
+
scope: Scope,
|
|
163
200
|
) {
|
|
164
201
|
if (expected.name !== "List" || expected.args.length !== 1) {
|
|
165
202
|
this.push("type-mismatch", `Expected ${formatType(expected)}, got List`, path, expression.span, {
|
|
@@ -178,7 +215,7 @@ class CoreChecker {
|
|
|
178
215
|
expression: Extract<PointCoreExpression, { kind: "record" }>,
|
|
179
216
|
expected: PointCoreTypeExpression,
|
|
180
217
|
path: string,
|
|
181
|
-
scope:
|
|
218
|
+
scope: Scope,
|
|
182
219
|
) {
|
|
183
220
|
const declaration = this.typeDeclarations.get(String(expected.name));
|
|
184
221
|
if (!declaration) {
|
|
@@ -216,7 +253,7 @@ class CoreChecker {
|
|
|
216
253
|
|
|
217
254
|
private typeOfExpression(
|
|
218
255
|
expression: PointCoreExpression,
|
|
219
|
-
scope:
|
|
256
|
+
scope: Scope,
|
|
220
257
|
path: string,
|
|
221
258
|
): PointCoreTypeExpression | null {
|
|
222
259
|
if (expression.kind === "literal") {
|
|
@@ -236,15 +273,15 @@ class CoreChecker {
|
|
|
236
273
|
return null;
|
|
237
274
|
}
|
|
238
275
|
if (expression.kind === "identifier") {
|
|
239
|
-
const
|
|
240
|
-
if (!
|
|
276
|
+
const entry = scope.get(expression.name);
|
|
277
|
+
if (!entry) {
|
|
241
278
|
this.push("unknown-identifier", `Unknown identifier ${expression.name}`, path, expression.span, {
|
|
242
279
|
actual: expression.name,
|
|
243
280
|
repair: `Declare ${expression.name}, pass it as a parameter, or replace it with an in-scope symbol.`,
|
|
244
281
|
});
|
|
245
282
|
return null;
|
|
246
283
|
}
|
|
247
|
-
return type;
|
|
284
|
+
return entry.type;
|
|
248
285
|
}
|
|
249
286
|
if (expression.kind === "binary") {
|
|
250
287
|
return this.typeOfBinaryExpression(expression, scope, path);
|
|
@@ -278,7 +315,7 @@ class CoreChecker {
|
|
|
278
315
|
|
|
279
316
|
private typeOfListExpression(
|
|
280
317
|
expression: Extract<PointCoreExpression, { kind: "list" }>,
|
|
281
|
-
scope:
|
|
318
|
+
scope: Scope,
|
|
282
319
|
path: string,
|
|
283
320
|
): PointCoreTypeExpression | null {
|
|
284
321
|
if (expression.items.length === 0) {
|
|
@@ -298,7 +335,7 @@ class CoreChecker {
|
|
|
298
335
|
|
|
299
336
|
private typeOfPropertyExpression(
|
|
300
337
|
expression: Extract<PointCoreExpression, { kind: "property" }>,
|
|
301
|
-
scope:
|
|
338
|
+
scope: Scope,
|
|
302
339
|
path: string,
|
|
303
340
|
): PointCoreTypeExpression | null {
|
|
304
341
|
const targetType = this.typeOfExpression(expression.target, scope, `${path}.target`);
|
|
@@ -326,7 +363,7 @@ class CoreChecker {
|
|
|
326
363
|
|
|
327
364
|
private typeOfBinaryExpression(
|
|
328
365
|
expression: Extract<PointCoreExpression, { kind: "binary" }>,
|
|
329
|
-
scope:
|
|
366
|
+
scope: Scope,
|
|
330
367
|
path: string,
|
|
331
368
|
): PointCoreTypeExpression | null {
|
|
332
369
|
const left = this.typeOfExpression(expression.left, scope, `${path}.left`);
|
package/src/core/cli.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { checkPointCore } from "./check.ts";
|
|
|
3
3
|
import { createPointCoreIndex, createPointCoreRepairPlan, explainPointCoreRef } from "./context.ts";
|
|
4
4
|
import { emitPointCoreTypeScript } from "./emit-typescript.ts";
|
|
5
5
|
import { formatPointCore } from "./format.ts";
|
|
6
|
-
import { parsePointCore } from "./parser.ts";
|
|
6
|
+
import { isSemanticPointSyntax, parsePointCore } from "./parser.ts";
|
|
7
7
|
|
|
8
8
|
const DEFAULT_INPUT = "examples/math.point";
|
|
9
9
|
const DEFAULT_OUTPUT = "generated/math.ast.json";
|
|
@@ -24,12 +24,20 @@ export async function main() {
|
|
|
24
24
|
const diagnostics = checkPointCore(program);
|
|
25
25
|
|
|
26
26
|
if (command === "fmt") {
|
|
27
|
+
if (isSemanticPointSyntax(source)) {
|
|
28
|
+
console.log(`Point core fmt preserved semantic source: ${input}`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
27
31
|
await Bun.write(inputPath, formatPointCore(program));
|
|
28
32
|
console.log(`Point core fmt wrote ${input}`);
|
|
29
33
|
return;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
if (command === "fmt-check") {
|
|
37
|
+
if (isSemanticPointSyntax(source)) {
|
|
38
|
+
console.log(`Point core fmt check passed: ${input}`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
33
41
|
const formatted = formatPointCore(program);
|
|
34
42
|
if (source !== formatted) {
|
|
35
43
|
console.error(`Point core fmt check failed: ${input}`);
|
|
@@ -111,7 +119,9 @@ async function runProjectCommand(command: string) {
|
|
|
111
119
|
if (command === "fmt-all") {
|
|
112
120
|
await Promise.all(
|
|
113
121
|
results.map((result) =>
|
|
114
|
-
|
|
122
|
+
isSemanticPointSyntax(result.source)
|
|
123
|
+
? Promise.resolve()
|
|
124
|
+
: Bun.write(resolve(process.cwd(), result.input), formatPointCore(result.program)),
|
|
115
125
|
),
|
|
116
126
|
);
|
|
117
127
|
console.log(`Point core fmt wrote ${results.length} files`);
|
|
@@ -119,7 +129,9 @@ async function runProjectCommand(command: string) {
|
|
|
119
129
|
}
|
|
120
130
|
|
|
121
131
|
if (command === "fmt-check-all") {
|
|
122
|
-
const unformatted = results.filter(
|
|
132
|
+
const unformatted = results.filter(
|
|
133
|
+
(result) => !isSemanticPointSyntax(result.source) && result.source !== formatPointCore(result.program),
|
|
134
|
+
);
|
|
123
135
|
if (unformatted.length > 0) {
|
|
124
136
|
console.error(JSON.stringify({ ok: false, unformatted: unformatted.map((result) => result.input) }, null, 2));
|
|
125
137
|
process.exit(1);
|
|
@@ -57,6 +57,7 @@ function emitStatement(statement: PointCoreStatement): string[] {
|
|
|
57
57
|
return [statement.value ? `return ${emitExpression(statement.value)};` : "return;"];
|
|
58
58
|
}
|
|
59
59
|
if (statement.kind === "value") return [emitValue(statement, false)];
|
|
60
|
+
if (statement.kind === "assignment") return [`${statement.name} ${statement.operator} ${emitExpression(statement.value)};`];
|
|
60
61
|
if (statement.kind === "if") {
|
|
61
62
|
const lines = [
|
|
62
63
|
`if (${emitCondition(statement.condition)}) {`,
|
package/src/core/format.ts
CHANGED
|
@@ -51,6 +51,7 @@ function formatStatementLines(statement: PointCoreStatement): string[] {
|
|
|
51
51
|
return [statement.value ? `return ${formatExpression(statement.value)}` : "return"];
|
|
52
52
|
}
|
|
53
53
|
if (statement.kind === "value") return [formatValue(statement)];
|
|
54
|
+
if (statement.kind === "assignment") return [`${statement.name} ${statement.operator} ${formatExpression(statement.value)}`];
|
|
54
55
|
if (statement.kind === "if") return formatIf(statement);
|
|
55
56
|
return [formatExpression(statement.value)];
|
|
56
57
|
}
|
package/src/core/lexer.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type PointCoreTokenType =
|
|
|
14
14
|
| "colon"
|
|
15
15
|
| "dot"
|
|
16
16
|
| "equals"
|
|
17
|
+
| "plusEquals"
|
|
17
18
|
| "equalsEquals"
|
|
18
19
|
| "bangEquals"
|
|
19
20
|
| "less"
|
|
@@ -128,6 +129,10 @@ class CoreLexer {
|
|
|
128
129
|
continue;
|
|
129
130
|
}
|
|
130
131
|
if (char === "+") {
|
|
132
|
+
if (this.peek(1) === "=") {
|
|
133
|
+
this.push("plusEquals", `${this.advance()}${this.advance()}`);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
131
136
|
this.push("plus", this.advance());
|
|
132
137
|
continue;
|
|
133
138
|
}
|
package/src/core/parser.ts
CHANGED
|
@@ -21,10 +21,241 @@ export class PointCoreParserError extends Error {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export function parsePointCore(source: string): PointCoreProgram {
|
|
24
|
-
const parser = new CoreParser(lexPointCore(source));
|
|
24
|
+
const parser = new CoreParser(lexPointCore(lowerSemanticPointSyntax(source)));
|
|
25
25
|
return parser.parseProgram();
|
|
26
26
|
}
|
|
27
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
|
+
|
|
28
259
|
class CoreParser {
|
|
29
260
|
private current = 0;
|
|
30
261
|
|
|
@@ -123,10 +354,25 @@ class CoreParser {
|
|
|
123
354
|
}
|
|
124
355
|
if (this.matchKeyword("if")) return this.parseIfStatement();
|
|
125
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();
|
|
126
358
|
const value = this.parseExpression();
|
|
127
359
|
return { kind: "expression", value, span: value.span };
|
|
128
360
|
}
|
|
129
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
|
+
|
|
130
376
|
private parseIfStatement(): PointCoreStatement {
|
|
131
377
|
const start = this.previous().span.start;
|
|
132
378
|
const condition = this.parseExpression();
|
|
@@ -334,13 +580,17 @@ class CoreParser {
|
|
|
334
580
|
return this.peek().type === type;
|
|
335
581
|
}
|
|
336
582
|
|
|
583
|
+
private checkNext(type: PointCoreTokenType) {
|
|
584
|
+
return this.peek(1).type === type;
|
|
585
|
+
}
|
|
586
|
+
|
|
337
587
|
private advance() {
|
|
338
588
|
if (!this.check("eof")) this.current += 1;
|
|
339
589
|
return this.previous();
|
|
340
590
|
}
|
|
341
591
|
|
|
342
|
-
private peek() {
|
|
343
|
-
return this.tokens[this.current] ?? this.tokens[this.tokens.length - 1]!;
|
|
592
|
+
private peek(offset = 0) {
|
|
593
|
+
return this.tokens[this.current + offset] ?? this.tokens[this.tokens.length - 1]!;
|
|
344
594
|
}
|
|
345
595
|
|
|
346
596
|
private previous() {
|