@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
|
@@ -1,347 +1,347 @@
|
|
|
1
|
-
import type { PointSourceSpan } from "../core/ast.ts";
|
|
2
|
-
import { lexPointCore } from "../core/lexer.ts";
|
|
3
|
-
import type {
|
|
4
|
-
PointSemanticBinaryOperator,
|
|
5
|
-
PointSemanticExpression,
|
|
6
|
-
PointSemanticRecordLiteralField,
|
|
7
|
-
PointSemanticTypeExpression,
|
|
8
|
-
} from "./ast.ts";
|
|
9
|
-
|
|
10
|
-
export interface PointSemanticExpressionContext {
|
|
11
|
-
bindings: string[];
|
|
12
|
-
atoms: string[];
|
|
13
|
-
callables: string[];
|
|
14
|
-
recordFields: Map<string, string[]>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function parseSemanticTypeExpression(source: string): PointSemanticTypeExpression {
|
|
18
|
-
const trimmed = source.trim();
|
|
19
|
-
const orParts = splitTopLevel(trimmed, " or ");
|
|
20
|
-
if (orParts.length > 1) {
|
|
21
|
-
return {
|
|
22
|
-
kind: "typeRef",
|
|
23
|
-
name: "Or",
|
|
24
|
-
args: orParts.map(parseSemanticTypeExpression),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
const listMatch = trimmed.match(/^List<(.+)>$/);
|
|
28
|
-
if (listMatch) {
|
|
29
|
-
return { kind: "typeRef", name: "List", args: [parseSemanticTypeExpression(listMatch[1] ?? "")] };
|
|
30
|
-
}
|
|
31
|
-
const maybeMatch = trimmed.match(/^Maybe<(.+)>$/);
|
|
32
|
-
if (maybeMatch) {
|
|
33
|
-
return { kind: "typeRef", name: "Maybe", args: [parseSemanticTypeExpression(maybeMatch[1] ?? "")] };
|
|
34
|
-
}
|
|
35
|
-
return { kind: "typeRef", name: trimmed, args: [] };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function parseSemanticExpression(
|
|
39
|
-
source: string,
|
|
40
|
-
context: PointSemanticExpressionContext,
|
|
41
|
-
span?: PointSourceSpan,
|
|
42
|
-
): PointSemanticExpression {
|
|
43
|
-
const errorMatch = source.trim().match(/^Error\s+("(?:\\.|[^"\\])*")$/);
|
|
44
|
-
if (errorMatch) {
|
|
45
|
-
const expression: PointSemanticExpression = { kind: "error", message: JSON.parse(errorMatch[1] ?? '""') as string };
|
|
46
|
-
return span ? withExpressionSpan(expression, span) : expression;
|
|
47
|
-
}
|
|
48
|
-
const expression = parseBinaryExpression(source.trim(), 0, context).expression;
|
|
49
|
-
return span ? withExpressionSpan(expression, span) : expression;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function withExpressionSpan(expression: PointSemanticExpression, span: PointSourceSpan): PointSemanticExpression {
|
|
53
|
-
switch (expression.kind) {
|
|
54
|
-
case "literal":
|
|
55
|
-
case "name":
|
|
56
|
-
case "error":
|
|
57
|
-
return { ...expression, span: expression.span ?? span };
|
|
58
|
-
case "property":
|
|
59
|
-
return {
|
|
60
|
-
...expression,
|
|
61
|
-
span: expression.span ?? span,
|
|
62
|
-
target: withExpressionSpan(expression.target, span),
|
|
63
|
-
};
|
|
64
|
-
case "binary":
|
|
65
|
-
return {
|
|
66
|
-
...expression,
|
|
67
|
-
span: expression.span ?? span,
|
|
68
|
-
left: withExpressionSpan(expression.left, span),
|
|
69
|
-
right: withExpressionSpan(expression.right, span),
|
|
70
|
-
};
|
|
71
|
-
case "call":
|
|
72
|
-
return {
|
|
73
|
-
...expression,
|
|
74
|
-
span: expression.span ?? span,
|
|
75
|
-
args: expression.args.map((arg) => withExpressionSpan(arg, span)),
|
|
76
|
-
};
|
|
77
|
-
case "await":
|
|
78
|
-
return {
|
|
79
|
-
...expression,
|
|
80
|
-
span: expression.span ?? span,
|
|
81
|
-
value: withExpressionSpan(expression.value, span),
|
|
82
|
-
};
|
|
83
|
-
case "list":
|
|
84
|
-
return {
|
|
85
|
-
...expression,
|
|
86
|
-
span: expression.span ?? span,
|
|
87
|
-
items: expression.items.map((item) => withExpressionSpan(item, span)),
|
|
88
|
-
};
|
|
89
|
-
case "record":
|
|
90
|
-
return {
|
|
91
|
-
...expression,
|
|
92
|
-
span: expression.span ?? span,
|
|
93
|
-
fields: expression.fields.map((field) => ({
|
|
94
|
-
...field,
|
|
95
|
-
value: withExpressionSpan(field.value, span),
|
|
96
|
-
})),
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function parseBinaryExpression(
|
|
102
|
-
source: string,
|
|
103
|
-
minPrecedence: number,
|
|
104
|
-
context: PointSemanticExpressionContext,
|
|
105
|
-
): { expression: PointSemanticExpression; consumed: string } {
|
|
106
|
-
let left = parsePrimaryExpression(source, context);
|
|
107
|
-
let rest = left.consumed.trimStart();
|
|
108
|
-
while (rest) {
|
|
109
|
-
const operator = peekBinaryOperator(rest);
|
|
110
|
-
if (!operator) break;
|
|
111
|
-
const precedence = precedenceFor(operator.op);
|
|
112
|
-
if (precedence < minPrecedence) break;
|
|
113
|
-
const right = parseBinaryExpression(operator.rest.trimStart(), precedence + 1, context);
|
|
114
|
-
left = {
|
|
115
|
-
expression: {
|
|
116
|
-
kind: "binary",
|
|
117
|
-
operator: operator.op,
|
|
118
|
-
left: left.expression,
|
|
119
|
-
right: right.expression,
|
|
120
|
-
},
|
|
121
|
-
consumed: right.consumed,
|
|
122
|
-
};
|
|
123
|
-
rest = left.consumed.trimStart();
|
|
124
|
-
}
|
|
125
|
-
return left;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function parsePrimaryExpression(source: string, context: PointSemanticExpressionContext): { expression: PointSemanticExpression; consumed: string } {
|
|
129
|
-
const trimmed = source.trim();
|
|
130
|
-
if (trimmed.startsWith("await ")) {
|
|
131
|
-
const inner = parsePrimaryExpression(trimmed.slice("await ".length), context);
|
|
132
|
-
return { expression: { kind: "await", value: inner.expression }, consumed: inner.consumed };
|
|
133
|
-
}
|
|
134
|
-
if (trimmed.startsWith("[")) {
|
|
135
|
-
return parseListExpression(trimmed, context);
|
|
136
|
-
}
|
|
137
|
-
if (trimmed.startsWith("{")) {
|
|
138
|
-
return parseRecordExpression(trimmed, context);
|
|
139
|
-
}
|
|
140
|
-
if (trimmed.startsWith('"')) {
|
|
141
|
-
const end = trimmed.indexOf('"', 1);
|
|
142
|
-
const value = JSON.parse(trimmed.slice(0, end + 1));
|
|
143
|
-
return { expression: { kind: "literal", value }, consumed: trimmed.slice(end + 1) };
|
|
144
|
-
}
|
|
145
|
-
if (/^(true|false|null|none)\b/.test(trimmed)) {
|
|
146
|
-
const match = trimmed.match(/^(true|false|null|none)\b/);
|
|
147
|
-
const token = match?.[1];
|
|
148
|
-
const value = token === "true" ? true : token === "false" ? false : null;
|
|
149
|
-
return { expression: { kind: "literal", value }, consumed: trimmed.slice(match?.[0].length ?? 0) };
|
|
150
|
-
}
|
|
151
|
-
if (/^\d/.test(trimmed)) {
|
|
152
|
-
const match = trimmed.match(/^(\d+(?:\.\d+)?)/);
|
|
153
|
-
const value = Number(match?.[1] ?? 0);
|
|
154
|
-
return { expression: { kind: "literal", value }, consumed: trimmed.slice(match?.[0].length ?? 0) };
|
|
155
|
-
}
|
|
156
|
-
const callMatch = matchCall(trimmed, context);
|
|
157
|
-
if (callMatch) return callMatch;
|
|
158
|
-
const atom = matchAtom(trimmed, context);
|
|
159
|
-
if (atom) {
|
|
160
|
-
if (atom.includes(".")) {
|
|
161
|
-
const dot = atom.indexOf(".");
|
|
162
|
-
return {
|
|
163
|
-
expression: {
|
|
164
|
-
kind: "property",
|
|
165
|
-
target: { kind: "name", label: atom.slice(0, dot) },
|
|
166
|
-
label: atom.slice(dot + 1),
|
|
167
|
-
},
|
|
168
|
-
consumed: trimmed.slice(atom.length),
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
let expression: PointSemanticExpression = { kind: "name", label: atom };
|
|
172
|
-
let consumed = trimmed.slice(atom.length);
|
|
173
|
-
while (consumed.trimStart().startsWith(".")) {
|
|
174
|
-
let rest = consumed.trimStart().slice(1);
|
|
175
|
-
const fieldMatch = rest.match(/^([A-Za-z][A-Za-z0-9 ]*[A-Za-z0-9]|[A-Za-z])/);
|
|
176
|
-
if (!fieldMatch) break;
|
|
177
|
-
const label = fieldMatch[0] ?? "";
|
|
178
|
-
expression = { kind: "property", target: expression, label };
|
|
179
|
-
consumed = rest.slice(label.length);
|
|
180
|
-
}
|
|
181
|
-
return { expression, consumed };
|
|
182
|
-
}
|
|
183
|
-
throw new Error(`Unable to parse semantic expression: ${source}`);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function parseListExpression(source: string, context: PointSemanticExpressionContext): { expression: PointSemanticExpression; consumed: string } {
|
|
187
|
-
let rest = source.trim().slice(1).trimStart();
|
|
188
|
-
const items: PointSemanticExpression[] = [];
|
|
189
|
-
while (rest && !rest.startsWith("]")) {
|
|
190
|
-
const parsed = parseBinaryExpression(rest, 0, context);
|
|
191
|
-
items.push(parsed.expression);
|
|
192
|
-
rest = parsed.consumed.trimStart();
|
|
193
|
-
if (rest.startsWith(",")) rest = rest.slice(1).trimStart();
|
|
194
|
-
}
|
|
195
|
-
if (!rest.startsWith("]")) throw new Error(`Unterminated list literal: ${source}`);
|
|
196
|
-
return { expression: { kind: "list", items }, consumed: rest.slice(1) };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function parseRecordExpression(source: string, context: PointSemanticExpressionContext): { expression: PointSemanticExpression; consumed: string } {
|
|
200
|
-
let rest = source.trim().slice(1).trimStart();
|
|
201
|
-
const fields: PointSemanticRecordLiteralField[] = [];
|
|
202
|
-
while (rest && !rest.startsWith("}")) {
|
|
203
|
-
const colon = rest.indexOf(":");
|
|
204
|
-
if (colon === -1) throw new Error(`Expected record field label: ${source}`);
|
|
205
|
-
const label = rest.slice(0, colon).trim();
|
|
206
|
-
rest = rest.slice(colon + 1).trimStart();
|
|
207
|
-
const parsed = parseBinaryExpression(rest, 0, context);
|
|
208
|
-
fields.push({ label, value: parsed.expression });
|
|
209
|
-
rest = parsed.consumed.trimStart();
|
|
210
|
-
if (rest.startsWith(",")) rest = rest.slice(1).trimStart();
|
|
211
|
-
}
|
|
212
|
-
if (!rest.startsWith("}")) throw new Error(`Unterminated record literal: ${source}`);
|
|
213
|
-
return { expression: { kind: "record", fields }, consumed: rest.slice(1) };
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function matchCall(source: string, context: PointSemanticExpressionContext): { expression: PointSemanticExpression; consumed: string } | null {
|
|
217
|
-
const candidates = [...context.callables, ...context.atoms, ...context.bindings].sort((a, b) => b.length - a.length);
|
|
218
|
-
for (const callee of candidates) {
|
|
219
|
-
const parsed = parseCallExpression(source, callee, context);
|
|
220
|
-
if (parsed) return parsed;
|
|
221
|
-
}
|
|
222
|
-
const generic = source.match(/^([A-Za-z_][A-Za-z0-9_]*)\(/);
|
|
223
|
-
if (generic) return parseCallExpression(source, generic[1] ?? "", context);
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function parseCallExpression(
|
|
228
|
-
source: string,
|
|
229
|
-
callee: string,
|
|
230
|
-
context: PointSemanticExpressionContext,
|
|
231
|
-
): { expression: PointSemanticExpression; consumed: string } | null {
|
|
232
|
-
if (!source.startsWith(callee)) return null;
|
|
233
|
-
const after = source.slice(callee.length);
|
|
234
|
-
if (!after.startsWith("(")) return null;
|
|
235
|
-
let depth = 0;
|
|
236
|
-
let index = 0;
|
|
237
|
-
for (; index < after.length; index += 1) {
|
|
238
|
-
const char = after[index];
|
|
239
|
-
if (char === "(") depth += 1;
|
|
240
|
-
if (char === ")") {
|
|
241
|
-
depth -= 1;
|
|
242
|
-
if (depth === 0) break;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
const argSource = after.slice(1, index);
|
|
246
|
-
const args = argSource.trim() ? splitTopLevel(argSource, ",").map((part) => parseSemanticExpression(part, context)) : [];
|
|
247
|
-
return {
|
|
248
|
-
expression: { kind: "call", callee, args },
|
|
249
|
-
consumed: source.slice(callee.length + index + 1),
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function matchAtom(source: string, context: PointSemanticExpressionContext): string | null {
|
|
254
|
-
for (const atom of [...context.atoms, ...context.bindings].sort((a, b) => b.length - a.length)) {
|
|
255
|
-
if (!source.startsWith(atom)) continue;
|
|
256
|
-
const next = source[atom.length];
|
|
257
|
-
if (next && /[A-Za-z0-9_]/.test(next)) continue;
|
|
258
|
-
return atom;
|
|
259
|
-
}
|
|
260
|
-
return null;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function peekBinaryOperator(source: string): { op: PointSemanticBinaryOperator; rest: string } | null {
|
|
264
|
-
for (const op of ["==", "!=", "<=", ">=", "and", "or", "+", "-", "*", "/", "<", ">"] as const) {
|
|
265
|
-
if (source.startsWith(op)) {
|
|
266
|
-
const next = source[op.length];
|
|
267
|
-
if (op === "-" && /^\d/.test(source)) return null;
|
|
268
|
-
if (next && /[A-Za-z0-9_]/.test(next) && !["and", "or"].includes(op)) continue;
|
|
269
|
-
return { op, rest: source.slice(op.length) };
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
return null;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function precedenceFor(operator: PointSemanticBinaryOperator): number {
|
|
276
|
-
if (operator === "or") return 1;
|
|
277
|
-
if (operator === "and") return 2;
|
|
278
|
-
if (operator === "==" || operator === "!=") return 3;
|
|
279
|
-
if (operator === "<" || operator === "<=" || operator === ">" || operator === ">=") return 4;
|
|
280
|
-
if (operator === "+" || operator === "-") return 5;
|
|
281
|
-
return 6;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function splitTopLevel(source: string, separator: string): string[] {
|
|
285
|
-
const parts: string[] = [];
|
|
286
|
-
let depth = 0;
|
|
287
|
-
let quote: '"' | null = null;
|
|
288
|
-
let current = "";
|
|
289
|
-
for (let index = 0; index < source.length; index += 1) {
|
|
290
|
-
const char = source[index];
|
|
291
|
-
if (quote) {
|
|
292
|
-
current += char;
|
|
293
|
-
if (char === quote && source[index - 1] !== "\\") quote = null;
|
|
294
|
-
continue;
|
|
295
|
-
}
|
|
296
|
-
if (char === '"') {
|
|
297
|
-
quote = '"';
|
|
298
|
-
current += char;
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
if (char === "<" || char === "(" || char === "[") depth += 1;
|
|
302
|
-
if (char === ">" || char === ")" || char === "]") depth -= 1;
|
|
303
|
-
if (depth === 0 && source.slice(index, index + separator.length) === separator) {
|
|
304
|
-
parts.push(current.trim());
|
|
305
|
-
current = "";
|
|
306
|
-
index += separator.length - 1;
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
current += char;
|
|
310
|
-
}
|
|
311
|
-
if (current.trim()) parts.push(current.trim());
|
|
312
|
-
return parts;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
export function buildExpressionContext(options: {
|
|
316
|
-
bindings?: string[];
|
|
317
|
-
paramTypes?: Map<string, string>;
|
|
318
|
-
recordFields?: Map<string, Map<string, string>>;
|
|
319
|
-
callables?: string[];
|
|
320
|
-
}): PointSemanticExpressionContext {
|
|
321
|
-
const bindings = options.bindings ?? [];
|
|
322
|
-
const callables = options.callables ?? [];
|
|
323
|
-
const atoms: string[] = [...bindings];
|
|
324
|
-
const recordFields = new Map<string, string[]>();
|
|
325
|
-
for (const [param, type] of options.paramTypes ?? []) {
|
|
326
|
-
const fields = options.recordFields?.get(type);
|
|
327
|
-
if (!fields) continue;
|
|
328
|
-
recordFields.set(type, [...fields.keys()]);
|
|
329
|
-
for (const label of fields.keys()) atoms.push(`${param}.${label}`);
|
|
330
|
-
}
|
|
331
|
-
return { bindings, atoms, callables, recordFields };
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
export function lineSpan(source: string, lineNumber: number): PointSourceSpan {
|
|
335
|
-
const lines = source.split(/\r?\n/);
|
|
336
|
-
let offset = 0;
|
|
337
|
-
for (let index = 0; index < lineNumber - 1; index += 1) offset += (lines[index]?.length ?? 0) + 1;
|
|
338
|
-
const line = lines[lineNumber - 1] ?? "";
|
|
339
|
-
return {
|
|
340
|
-
start: { line: lineNumber, column: 1, offset },
|
|
341
|
-
end: { line: lineNumber, column: line.length + 1, offset: offset + line.length },
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
export function validateExpressionWithLexer(source: string): void {
|
|
346
|
-
lexPointCore(source.replace(/\band\b/g, "&&").replace(/\bor\b/g, "||"));
|
|
347
|
-
}
|
|
1
|
+
import type { PointSourceSpan } from "../core/ast.ts";
|
|
2
|
+
import { lexPointCore } from "../core/lexer.ts";
|
|
3
|
+
import type {
|
|
4
|
+
PointSemanticBinaryOperator,
|
|
5
|
+
PointSemanticExpression,
|
|
6
|
+
PointSemanticRecordLiteralField,
|
|
7
|
+
PointSemanticTypeExpression,
|
|
8
|
+
} from "./ast.ts";
|
|
9
|
+
|
|
10
|
+
export interface PointSemanticExpressionContext {
|
|
11
|
+
bindings: string[];
|
|
12
|
+
atoms: string[];
|
|
13
|
+
callables: string[];
|
|
14
|
+
recordFields: Map<string, string[]>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function parseSemanticTypeExpression(source: string): PointSemanticTypeExpression {
|
|
18
|
+
const trimmed = source.trim();
|
|
19
|
+
const orParts = splitTopLevel(trimmed, " or ");
|
|
20
|
+
if (orParts.length > 1) {
|
|
21
|
+
return {
|
|
22
|
+
kind: "typeRef",
|
|
23
|
+
name: "Or",
|
|
24
|
+
args: orParts.map(parseSemanticTypeExpression),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const listMatch = trimmed.match(/^List<(.+)>$/);
|
|
28
|
+
if (listMatch) {
|
|
29
|
+
return { kind: "typeRef", name: "List", args: [parseSemanticTypeExpression(listMatch[1] ?? "")] };
|
|
30
|
+
}
|
|
31
|
+
const maybeMatch = trimmed.match(/^Maybe<(.+)>$/);
|
|
32
|
+
if (maybeMatch) {
|
|
33
|
+
return { kind: "typeRef", name: "Maybe", args: [parseSemanticTypeExpression(maybeMatch[1] ?? "")] };
|
|
34
|
+
}
|
|
35
|
+
return { kind: "typeRef", name: trimmed, args: [] };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parseSemanticExpression(
|
|
39
|
+
source: string,
|
|
40
|
+
context: PointSemanticExpressionContext,
|
|
41
|
+
span?: PointSourceSpan,
|
|
42
|
+
): PointSemanticExpression {
|
|
43
|
+
const errorMatch = source.trim().match(/^Error\s+("(?:\\.|[^"\\])*")$/);
|
|
44
|
+
if (errorMatch) {
|
|
45
|
+
const expression: PointSemanticExpression = { kind: "error", message: JSON.parse(errorMatch[1] ?? '""') as string };
|
|
46
|
+
return span ? withExpressionSpan(expression, span) : expression;
|
|
47
|
+
}
|
|
48
|
+
const expression = parseBinaryExpression(source.trim(), 0, context).expression;
|
|
49
|
+
return span ? withExpressionSpan(expression, span) : expression;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function withExpressionSpan(expression: PointSemanticExpression, span: PointSourceSpan): PointSemanticExpression {
|
|
53
|
+
switch (expression.kind) {
|
|
54
|
+
case "literal":
|
|
55
|
+
case "name":
|
|
56
|
+
case "error":
|
|
57
|
+
return { ...expression, span: expression.span ?? span };
|
|
58
|
+
case "property":
|
|
59
|
+
return {
|
|
60
|
+
...expression,
|
|
61
|
+
span: expression.span ?? span,
|
|
62
|
+
target: withExpressionSpan(expression.target, span),
|
|
63
|
+
};
|
|
64
|
+
case "binary":
|
|
65
|
+
return {
|
|
66
|
+
...expression,
|
|
67
|
+
span: expression.span ?? span,
|
|
68
|
+
left: withExpressionSpan(expression.left, span),
|
|
69
|
+
right: withExpressionSpan(expression.right, span),
|
|
70
|
+
};
|
|
71
|
+
case "call":
|
|
72
|
+
return {
|
|
73
|
+
...expression,
|
|
74
|
+
span: expression.span ?? span,
|
|
75
|
+
args: expression.args.map((arg) => withExpressionSpan(arg, span)),
|
|
76
|
+
};
|
|
77
|
+
case "await":
|
|
78
|
+
return {
|
|
79
|
+
...expression,
|
|
80
|
+
span: expression.span ?? span,
|
|
81
|
+
value: withExpressionSpan(expression.value, span),
|
|
82
|
+
};
|
|
83
|
+
case "list":
|
|
84
|
+
return {
|
|
85
|
+
...expression,
|
|
86
|
+
span: expression.span ?? span,
|
|
87
|
+
items: expression.items.map((item) => withExpressionSpan(item, span)),
|
|
88
|
+
};
|
|
89
|
+
case "record":
|
|
90
|
+
return {
|
|
91
|
+
...expression,
|
|
92
|
+
span: expression.span ?? span,
|
|
93
|
+
fields: expression.fields.map((field) => ({
|
|
94
|
+
...field,
|
|
95
|
+
value: withExpressionSpan(field.value, span),
|
|
96
|
+
})),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseBinaryExpression(
|
|
102
|
+
source: string,
|
|
103
|
+
minPrecedence: number,
|
|
104
|
+
context: PointSemanticExpressionContext,
|
|
105
|
+
): { expression: PointSemanticExpression; consumed: string } {
|
|
106
|
+
let left = parsePrimaryExpression(source, context);
|
|
107
|
+
let rest = left.consumed.trimStart();
|
|
108
|
+
while (rest) {
|
|
109
|
+
const operator = peekBinaryOperator(rest);
|
|
110
|
+
if (!operator) break;
|
|
111
|
+
const precedence = precedenceFor(operator.op);
|
|
112
|
+
if (precedence < minPrecedence) break;
|
|
113
|
+
const right = parseBinaryExpression(operator.rest.trimStart(), precedence + 1, context);
|
|
114
|
+
left = {
|
|
115
|
+
expression: {
|
|
116
|
+
kind: "binary",
|
|
117
|
+
operator: operator.op,
|
|
118
|
+
left: left.expression,
|
|
119
|
+
right: right.expression,
|
|
120
|
+
},
|
|
121
|
+
consumed: right.consumed,
|
|
122
|
+
};
|
|
123
|
+
rest = left.consumed.trimStart();
|
|
124
|
+
}
|
|
125
|
+
return left;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parsePrimaryExpression(source: string, context: PointSemanticExpressionContext): { expression: PointSemanticExpression; consumed: string } {
|
|
129
|
+
const trimmed = source.trim();
|
|
130
|
+
if (trimmed.startsWith("await ")) {
|
|
131
|
+
const inner = parsePrimaryExpression(trimmed.slice("await ".length), context);
|
|
132
|
+
return { expression: { kind: "await", value: inner.expression }, consumed: inner.consumed };
|
|
133
|
+
}
|
|
134
|
+
if (trimmed.startsWith("[")) {
|
|
135
|
+
return parseListExpression(trimmed, context);
|
|
136
|
+
}
|
|
137
|
+
if (trimmed.startsWith("{")) {
|
|
138
|
+
return parseRecordExpression(trimmed, context);
|
|
139
|
+
}
|
|
140
|
+
if (trimmed.startsWith('"')) {
|
|
141
|
+
const end = trimmed.indexOf('"', 1);
|
|
142
|
+
const value = JSON.parse(trimmed.slice(0, end + 1));
|
|
143
|
+
return { expression: { kind: "literal", value }, consumed: trimmed.slice(end + 1) };
|
|
144
|
+
}
|
|
145
|
+
if (/^(true|false|null|none)\b/.test(trimmed)) {
|
|
146
|
+
const match = trimmed.match(/^(true|false|null|none)\b/);
|
|
147
|
+
const token = match?.[1];
|
|
148
|
+
const value = token === "true" ? true : token === "false" ? false : null;
|
|
149
|
+
return { expression: { kind: "literal", value }, consumed: trimmed.slice(match?.[0].length ?? 0) };
|
|
150
|
+
}
|
|
151
|
+
if (/^\d/.test(trimmed)) {
|
|
152
|
+
const match = trimmed.match(/^(\d+(?:\.\d+)?)/);
|
|
153
|
+
const value = Number(match?.[1] ?? 0);
|
|
154
|
+
return { expression: { kind: "literal", value }, consumed: trimmed.slice(match?.[0].length ?? 0) };
|
|
155
|
+
}
|
|
156
|
+
const callMatch = matchCall(trimmed, context);
|
|
157
|
+
if (callMatch) return callMatch;
|
|
158
|
+
const atom = matchAtom(trimmed, context);
|
|
159
|
+
if (atom) {
|
|
160
|
+
if (atom.includes(".")) {
|
|
161
|
+
const dot = atom.indexOf(".");
|
|
162
|
+
return {
|
|
163
|
+
expression: {
|
|
164
|
+
kind: "property",
|
|
165
|
+
target: { kind: "name", label: atom.slice(0, dot) },
|
|
166
|
+
label: atom.slice(dot + 1),
|
|
167
|
+
},
|
|
168
|
+
consumed: trimmed.slice(atom.length),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
let expression: PointSemanticExpression = { kind: "name", label: atom };
|
|
172
|
+
let consumed = trimmed.slice(atom.length);
|
|
173
|
+
while (consumed.trimStart().startsWith(".")) {
|
|
174
|
+
let rest = consumed.trimStart().slice(1);
|
|
175
|
+
const fieldMatch = rest.match(/^([A-Za-z][A-Za-z0-9 ]*[A-Za-z0-9]|[A-Za-z])/);
|
|
176
|
+
if (!fieldMatch) break;
|
|
177
|
+
const label = fieldMatch[0] ?? "";
|
|
178
|
+
expression = { kind: "property", target: expression, label };
|
|
179
|
+
consumed = rest.slice(label.length);
|
|
180
|
+
}
|
|
181
|
+
return { expression, consumed };
|
|
182
|
+
}
|
|
183
|
+
throw new Error(`Unable to parse semantic expression: ${source}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function parseListExpression(source: string, context: PointSemanticExpressionContext): { expression: PointSemanticExpression; consumed: string } {
|
|
187
|
+
let rest = source.trim().slice(1).trimStart();
|
|
188
|
+
const items: PointSemanticExpression[] = [];
|
|
189
|
+
while (rest && !rest.startsWith("]")) {
|
|
190
|
+
const parsed = parseBinaryExpression(rest, 0, context);
|
|
191
|
+
items.push(parsed.expression);
|
|
192
|
+
rest = parsed.consumed.trimStart();
|
|
193
|
+
if (rest.startsWith(",")) rest = rest.slice(1).trimStart();
|
|
194
|
+
}
|
|
195
|
+
if (!rest.startsWith("]")) throw new Error(`Unterminated list literal: ${source}`);
|
|
196
|
+
return { expression: { kind: "list", items }, consumed: rest.slice(1) };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function parseRecordExpression(source: string, context: PointSemanticExpressionContext): { expression: PointSemanticExpression; consumed: string } {
|
|
200
|
+
let rest = source.trim().slice(1).trimStart();
|
|
201
|
+
const fields: PointSemanticRecordLiteralField[] = [];
|
|
202
|
+
while (rest && !rest.startsWith("}")) {
|
|
203
|
+
const colon = rest.indexOf(":");
|
|
204
|
+
if (colon === -1) throw new Error(`Expected record field label: ${source}`);
|
|
205
|
+
const label = rest.slice(0, colon).trim();
|
|
206
|
+
rest = rest.slice(colon + 1).trimStart();
|
|
207
|
+
const parsed = parseBinaryExpression(rest, 0, context);
|
|
208
|
+
fields.push({ label, value: parsed.expression });
|
|
209
|
+
rest = parsed.consumed.trimStart();
|
|
210
|
+
if (rest.startsWith(",")) rest = rest.slice(1).trimStart();
|
|
211
|
+
}
|
|
212
|
+
if (!rest.startsWith("}")) throw new Error(`Unterminated record literal: ${source}`);
|
|
213
|
+
return { expression: { kind: "record", fields }, consumed: rest.slice(1) };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function matchCall(source: string, context: PointSemanticExpressionContext): { expression: PointSemanticExpression; consumed: string } | null {
|
|
217
|
+
const candidates = [...context.callables, ...context.atoms, ...context.bindings].sort((a, b) => b.length - a.length);
|
|
218
|
+
for (const callee of candidates) {
|
|
219
|
+
const parsed = parseCallExpression(source, callee, context);
|
|
220
|
+
if (parsed) return parsed;
|
|
221
|
+
}
|
|
222
|
+
const generic = source.match(/^([A-Za-z_][A-Za-z0-9_]*)\(/);
|
|
223
|
+
if (generic) return parseCallExpression(source, generic[1] ?? "", context);
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function parseCallExpression(
|
|
228
|
+
source: string,
|
|
229
|
+
callee: string,
|
|
230
|
+
context: PointSemanticExpressionContext,
|
|
231
|
+
): { expression: PointSemanticExpression; consumed: string } | null {
|
|
232
|
+
if (!source.startsWith(callee)) return null;
|
|
233
|
+
const after = source.slice(callee.length);
|
|
234
|
+
if (!after.startsWith("(")) return null;
|
|
235
|
+
let depth = 0;
|
|
236
|
+
let index = 0;
|
|
237
|
+
for (; index < after.length; index += 1) {
|
|
238
|
+
const char = after[index];
|
|
239
|
+
if (char === "(") depth += 1;
|
|
240
|
+
if (char === ")") {
|
|
241
|
+
depth -= 1;
|
|
242
|
+
if (depth === 0) break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const argSource = after.slice(1, index);
|
|
246
|
+
const args = argSource.trim() ? splitTopLevel(argSource, ",").map((part) => parseSemanticExpression(part, context)) : [];
|
|
247
|
+
return {
|
|
248
|
+
expression: { kind: "call", callee, args },
|
|
249
|
+
consumed: source.slice(callee.length + index + 1),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function matchAtom(source: string, context: PointSemanticExpressionContext): string | null {
|
|
254
|
+
for (const atom of [...context.atoms, ...context.bindings].sort((a, b) => b.length - a.length)) {
|
|
255
|
+
if (!source.startsWith(atom)) continue;
|
|
256
|
+
const next = source[atom.length];
|
|
257
|
+
if (next && /[A-Za-z0-9_]/.test(next)) continue;
|
|
258
|
+
return atom;
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function peekBinaryOperator(source: string): { op: PointSemanticBinaryOperator; rest: string } | null {
|
|
264
|
+
for (const op of ["==", "!=", "<=", ">=", "and", "or", "+", "-", "*", "/", "<", ">"] as const) {
|
|
265
|
+
if (source.startsWith(op)) {
|
|
266
|
+
const next = source[op.length];
|
|
267
|
+
if (op === "-" && /^\d/.test(source)) return null;
|
|
268
|
+
if (next && /[A-Za-z0-9_]/.test(next) && !["and", "or"].includes(op)) continue;
|
|
269
|
+
return { op, rest: source.slice(op.length) };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function precedenceFor(operator: PointSemanticBinaryOperator): number {
|
|
276
|
+
if (operator === "or") return 1;
|
|
277
|
+
if (operator === "and") return 2;
|
|
278
|
+
if (operator === "==" || operator === "!=") return 3;
|
|
279
|
+
if (operator === "<" || operator === "<=" || operator === ">" || operator === ">=") return 4;
|
|
280
|
+
if (operator === "+" || operator === "-") return 5;
|
|
281
|
+
return 6;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function splitTopLevel(source: string, separator: string): string[] {
|
|
285
|
+
const parts: string[] = [];
|
|
286
|
+
let depth = 0;
|
|
287
|
+
let quote: '"' | null = null;
|
|
288
|
+
let current = "";
|
|
289
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
290
|
+
const char = source[index];
|
|
291
|
+
if (quote) {
|
|
292
|
+
current += char;
|
|
293
|
+
if (char === quote && source[index - 1] !== "\\") quote = null;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (char === '"') {
|
|
297
|
+
quote = '"';
|
|
298
|
+
current += char;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (char === "<" || char === "(" || char === "[") depth += 1;
|
|
302
|
+
if (char === ">" || char === ")" || char === "]") depth -= 1;
|
|
303
|
+
if (depth === 0 && source.slice(index, index + separator.length) === separator) {
|
|
304
|
+
parts.push(current.trim());
|
|
305
|
+
current = "";
|
|
306
|
+
index += separator.length - 1;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
current += char;
|
|
310
|
+
}
|
|
311
|
+
if (current.trim()) parts.push(current.trim());
|
|
312
|
+
return parts;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function buildExpressionContext(options: {
|
|
316
|
+
bindings?: string[];
|
|
317
|
+
paramTypes?: Map<string, string>;
|
|
318
|
+
recordFields?: Map<string, Map<string, string>>;
|
|
319
|
+
callables?: string[];
|
|
320
|
+
}): PointSemanticExpressionContext {
|
|
321
|
+
const bindings = options.bindings ?? [];
|
|
322
|
+
const callables = options.callables ?? [];
|
|
323
|
+
const atoms: string[] = [...bindings];
|
|
324
|
+
const recordFields = new Map<string, string[]>();
|
|
325
|
+
for (const [param, type] of options.paramTypes ?? []) {
|
|
326
|
+
const fields = options.recordFields?.get(type);
|
|
327
|
+
if (!fields) continue;
|
|
328
|
+
recordFields.set(type, [...fields.keys()]);
|
|
329
|
+
for (const label of fields.keys()) atoms.push(`${param}.${label}`);
|
|
330
|
+
}
|
|
331
|
+
return { bindings, atoms, callables, recordFields };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function lineSpan(source: string, lineNumber: number): PointSourceSpan {
|
|
335
|
+
const lines = source.split(/\r?\n/);
|
|
336
|
+
let offset = 0;
|
|
337
|
+
for (let index = 0; index < lineNumber - 1; index += 1) offset += (lines[index]?.length ?? 0) + 1;
|
|
338
|
+
const line = lines[lineNumber - 1] ?? "";
|
|
339
|
+
return {
|
|
340
|
+
start: { line: lineNumber, column: 1, offset },
|
|
341
|
+
end: { line: lineNumber, column: line.length + 1, offset: offset + line.length },
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function validateExpressionWithLexer(source: string): void {
|
|
346
|
+
lexPointCore(source.replace(/\band\b/g, "&&").replace(/\bor\b/g, "||"));
|
|
347
|
+
}
|