@hatchingpoint/point 0.0.3 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -5
- package/package.json +1 -1
- package/src/core/ast.ts +46 -1
- package/src/core/check.ts +217 -69
- package/src/core/cli.ts +332 -39
- package/src/core/context.ts +213 -36
- package/src/core/emit-javascript.ts +124 -0
- package/src/core/emit-typescript.ts +39 -5
- package/src/core/format.ts +4 -101
- package/src/core/incremental.ts +53 -0
- package/src/core/index.ts +5 -0
- package/src/core/lexer.ts +15 -5
- package/src/core/parser.ts +11 -362
- package/src/core/semantic-source.ts +26 -0
- package/src/core/serialize.ts +18 -0
- package/src/core/test-only/core-text-parser.ts +415 -0
- package/src/core/test-only/format-core.ts +120 -0
- package/src/core/test-only/index.ts +3 -0
- package/src/core/test-only/legacy-lowering.ts +1047 -0
- package/src/semantic/ast.ts +230 -0
- package/src/semantic/callables.ts +51 -0
- package/src/semantic/context.ts +347 -0
- package/src/semantic/desugar.ts +665 -0
- package/src/semantic/expressions.ts +347 -0
- package/src/semantic/format.ts +222 -0
- package/src/semantic/index.ts +10 -0
- package/src/semantic/metadata.ts +37 -0
- package/src/semantic/naming.ts +33 -0
- package/src/semantic/parse.ts +945 -0
- package/src/semantic/serialize.ts +18 -0
package/README.md
CHANGED
|
@@ -2,20 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
Point is an AI-first general-purpose language core for coding-agent-native software engineering.
|
|
4
4
|
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Requires [Bun](https://bun.sh) on PATH.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @hatchingpoint/point
|
|
11
|
+
point check examples/math.point
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Pair with the [Point Language](https://marketplace.visualstudio.com/items?itemName=hatchingpoint.point) extension in VS Code or Cursor.
|
|
15
|
+
|
|
16
|
+
Point's public source language is semantic product logic. The compiler lowers that source into an internal typed core and emits TypeScript for existing JavaScript infrastructure.
|
|
17
|
+
|
|
5
18
|
This package is the source of truth for Point. It exposes:
|
|
6
19
|
|
|
7
20
|
- `point` CLI through `src/cli.ts`
|
|
8
21
|
- core language APIs through `@hatchingpoint/point/core`
|
|
9
|
-
-
|
|
22
|
+
- semantic parser/lowering, core checker, formatter, and TypeScript emitter APIs
|
|
10
23
|
|
|
11
24
|
Core workflow:
|
|
12
25
|
|
|
13
26
|
```bash
|
|
14
|
-
bun run
|
|
15
|
-
bun run
|
|
16
|
-
bun run
|
|
27
|
+
bun run fmt-check
|
|
28
|
+
bun run check
|
|
29
|
+
bun run build
|
|
17
30
|
```
|
|
18
31
|
|
|
19
|
-
`
|
|
32
|
+
`bun run build` emits TypeScript into `generated/` for React, Vue, Bun, Node, and Vite projects.
|
|
20
33
|
|
|
21
34
|
When Point is extracted, this package can move into a standalone repo with the same package name and public entrypoints.
|
package/package.json
CHANGED
package/src/core/ast.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { PointSemanticProgram } from "../semantic/ast.ts";
|
|
2
|
+
|
|
1
3
|
export type PointCorePrimitiveType = "Text" | "Int" | "Float" | "Bool" | "Void";
|
|
2
4
|
|
|
3
5
|
export interface PointSourcePosition {
|
|
@@ -16,10 +18,13 @@ export interface PointCoreProgram {
|
|
|
16
18
|
module?: string;
|
|
17
19
|
declarations: PointCoreDeclaration[];
|
|
18
20
|
span?: PointSourceSpan;
|
|
21
|
+
semantic?: PointSemanticProgramMetadata;
|
|
22
|
+
semanticSource?: PointSemanticProgram;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
export type PointCoreDeclaration =
|
|
22
26
|
| PointCoreImportDeclaration
|
|
27
|
+
| PointCoreExternalDeclaration
|
|
23
28
|
| PointCoreValueDeclaration
|
|
24
29
|
| PointCoreFunctionDeclaration
|
|
25
30
|
| PointCoreTypeDeclaration;
|
|
@@ -47,6 +52,18 @@ export interface PointCoreFunctionDeclaration {
|
|
|
47
52
|
returnType: PointCoreTypeExpression;
|
|
48
53
|
body: PointCoreStatement[];
|
|
49
54
|
span?: PointSourceSpan;
|
|
55
|
+
semantic?: PointSemanticDeclarationMetadata;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface PointCoreExternalDeclaration {
|
|
59
|
+
kind: "external";
|
|
60
|
+
name: string;
|
|
61
|
+
params: PointCoreParameter[];
|
|
62
|
+
returnType: PointCoreTypeExpression;
|
|
63
|
+
from: string;
|
|
64
|
+
importName?: string;
|
|
65
|
+
span?: PointSourceSpan;
|
|
66
|
+
semantic?: PointSemanticDeclarationMetadata;
|
|
50
67
|
}
|
|
51
68
|
|
|
52
69
|
export interface PointCoreTypeDeclaration {
|
|
@@ -54,12 +71,25 @@ export interface PointCoreTypeDeclaration {
|
|
|
54
71
|
name: string;
|
|
55
72
|
fields: PointCoreParameter[];
|
|
56
73
|
span?: PointSourceSpan;
|
|
74
|
+
semantic?: PointSemanticDeclarationMetadata;
|
|
57
75
|
}
|
|
58
76
|
|
|
59
77
|
export interface PointCoreParameter {
|
|
60
78
|
name: string;
|
|
61
79
|
type: PointCoreTypeExpression;
|
|
62
80
|
span?: PointSourceSpan;
|
|
81
|
+
semanticName?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface PointSemanticProgramMetadata {
|
|
85
|
+
source: "semantic";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface PointSemanticDeclarationMetadata {
|
|
89
|
+
kind: "record" | "calculation" | "rule" | "label" | "external" | "action" | "policy" | "view" | "route" | "workflow" | "command";
|
|
90
|
+
name: string;
|
|
91
|
+
outputName?: string;
|
|
92
|
+
effects?: string[];
|
|
63
93
|
}
|
|
64
94
|
|
|
65
95
|
export interface PointCoreTypeExpression {
|
|
@@ -78,6 +108,13 @@ export interface PointCoreRecordField {
|
|
|
78
108
|
export type PointCoreStatement =
|
|
79
109
|
| { kind: "return"; value?: PointCoreExpression; span?: PointSourceSpan }
|
|
80
110
|
| PointCoreValueDeclaration
|
|
111
|
+
| {
|
|
112
|
+
kind: "assignment";
|
|
113
|
+
name: string;
|
|
114
|
+
operator: "=" | "+=" | "-=";
|
|
115
|
+
value: PointCoreExpression;
|
|
116
|
+
span?: PointSourceSpan;
|
|
117
|
+
}
|
|
81
118
|
| {
|
|
82
119
|
kind: "if";
|
|
83
120
|
condition: PointCoreExpression;
|
|
@@ -85,14 +122,22 @@ export type PointCoreStatement =
|
|
|
85
122
|
elseBody: PointCoreStatement[];
|
|
86
123
|
span?: PointSourceSpan;
|
|
87
124
|
}
|
|
125
|
+
| {
|
|
126
|
+
kind: "for";
|
|
127
|
+
itemName: string;
|
|
128
|
+
iterable: PointCoreExpression;
|
|
129
|
+
body: PointCoreStatement[];
|
|
130
|
+
span?: PointSourceSpan;
|
|
131
|
+
}
|
|
88
132
|
| { kind: "expression"; value: PointCoreExpression; span?: PointSourceSpan };
|
|
89
133
|
|
|
90
134
|
export type PointCoreExpression =
|
|
91
|
-
| { kind: "literal"; value: string | number | boolean; span?: PointSourceSpan }
|
|
135
|
+
| { kind: "literal"; value: string | number | boolean | null; span?: PointSourceSpan }
|
|
92
136
|
| { kind: "identifier"; name: string; span?: PointSourceSpan }
|
|
93
137
|
| { kind: "list"; items: PointCoreExpression[]; span?: PointSourceSpan }
|
|
94
138
|
| { kind: "record"; fields: PointCoreRecordField[]; span?: PointSourceSpan }
|
|
95
139
|
| { kind: "property"; target: PointCoreExpression; name: string; span?: PointSourceSpan }
|
|
140
|
+
| { kind: "await"; value: PointCoreExpression; span?: PointSourceSpan }
|
|
96
141
|
| {
|
|
97
142
|
kind: "binary";
|
|
98
143
|
operator: PointCoreBinaryOperator;
|
package/src/core/check.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
PointCoreDeclaration,
|
|
3
|
-
PointCoreExpression,
|
|
4
|
-
|
|
3
|
+
PointCoreExpression,
|
|
4
|
+
PointCoreExternalDeclaration,
|
|
5
|
+
PointCoreFunctionDeclaration,
|
|
5
6
|
PointCoreProgram,
|
|
6
7
|
PointCoreStatement,
|
|
7
8
|
PointCoreTypeDeclaration,
|
|
@@ -24,8 +25,10 @@ export interface PointCoreDiagnostic {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
type DiagnosticMetadata = Partial<Pick<PointCoreDiagnostic, "expected" | "actual" | "repair" | "relatedRefs">>;
|
|
28
|
+
type ScopeEntry = { type: PointCoreTypeExpression; mutable: boolean };
|
|
29
|
+
type Scope = Map<string, ScopeEntry>;
|
|
27
30
|
|
|
28
|
-
const PRIMITIVE_TYPES = new Set(["Text", "Int", "Float", "Bool", "Void", "List"]);
|
|
31
|
+
const PRIMITIVE_TYPES = new Set(["Text", "Int", "Float", "Bool", "Void", "List", "Maybe", "Error", "Or"]);
|
|
29
32
|
|
|
30
33
|
export function checkPointCore(program: PointCoreProgram): PointCoreDiagnostic[] {
|
|
31
34
|
const checker = new CoreChecker(program);
|
|
@@ -36,8 +39,8 @@ class CoreChecker {
|
|
|
36
39
|
private readonly diagnostics: PointCoreDiagnostic[] = [];
|
|
37
40
|
private readonly types = new Set(PRIMITIVE_TYPES);
|
|
38
41
|
private readonly typeDeclarations = new Map<string, PointCoreTypeDeclaration>();
|
|
39
|
-
private readonly globals = new Map
|
|
40
|
-
private readonly functions = new Map<string, PointCoreFunctionDeclaration>();
|
|
42
|
+
private readonly globals: Scope = new Map();
|
|
43
|
+
private readonly functions = new Map<string, PointCoreFunctionDeclaration | PointCoreExternalDeclaration>();
|
|
41
44
|
|
|
42
45
|
constructor(private readonly program: PointCoreProgram) {}
|
|
43
46
|
|
|
@@ -57,12 +60,18 @@ class CoreChecker {
|
|
|
57
60
|
this.typeDeclarations.set(declaration.name, declaration);
|
|
58
61
|
}
|
|
59
62
|
if (declaration.kind === "value") this.addGlobal(declaration);
|
|
60
|
-
if (declaration.kind === "function") {
|
|
63
|
+
if (declaration.kind === "function") {
|
|
61
64
|
if (this.functions.has(declaration.name)) {
|
|
62
65
|
this.push("duplicate-function", `Duplicate function ${declaration.name}`, `fn.${declaration.name}`, declaration.span);
|
|
63
66
|
}
|
|
64
|
-
this.functions.set(declaration.name, declaration);
|
|
65
|
-
}
|
|
67
|
+
this.functions.set(declaration.name, declaration);
|
|
68
|
+
}
|
|
69
|
+
if (declaration.kind === "external") {
|
|
70
|
+
if (this.functions.has(declaration.name)) {
|
|
71
|
+
this.push("duplicate-function", `Duplicate function ${declaration.name}`, `external.${declaration.name}`, declaration.span);
|
|
72
|
+
}
|
|
73
|
+
this.functions.set(declaration.name, declaration);
|
|
74
|
+
}
|
|
66
75
|
}
|
|
67
76
|
}
|
|
68
77
|
|
|
@@ -70,11 +79,16 @@ class CoreChecker {
|
|
|
70
79
|
if (this.globals.has(declaration.name)) {
|
|
71
80
|
this.push("duplicate-value", `Duplicate value ${declaration.name}`, `value.${declaration.name}`, declaration.span);
|
|
72
81
|
}
|
|
73
|
-
this.globals.set(declaration.name, declaration.type);
|
|
82
|
+
this.globals.set(declaration.name, { type: declaration.type, mutable: declaration.mutable });
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
private checkDeclaration(declaration: PointCoreDeclaration) {
|
|
77
|
-
if (declaration.kind === "import") return;
|
|
86
|
+
if (declaration.kind === "import") return;
|
|
87
|
+
if (declaration.kind === "external") {
|
|
88
|
+
for (const param of declaration.params) this.checkType(param.type, `external.${declaration.name}.${param.name}.type`);
|
|
89
|
+
this.checkType(declaration.returnType, `external.${declaration.name}.return`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
78
92
|
if (declaration.kind === "type") {
|
|
79
93
|
for (const field of declaration.fields) this.checkType(field.type, `type.${declaration.name}.${field.name}`);
|
|
80
94
|
return;
|
|
@@ -92,7 +106,7 @@ class CoreChecker {
|
|
|
92
106
|
const locals = new Map(this.globals);
|
|
93
107
|
for (const param of declaration.params) {
|
|
94
108
|
this.checkType(param.type, `fn.${declaration.name}.${param.name}.type`);
|
|
95
|
-
locals.set(param.name, param.type);
|
|
109
|
+
locals.set(param.name, { type: param.type, mutable: false });
|
|
96
110
|
}
|
|
97
111
|
for (const statement of declaration.body) {
|
|
98
112
|
this.checkStatement(statement, declaration, locals);
|
|
@@ -102,7 +116,7 @@ class CoreChecker {
|
|
|
102
116
|
private checkStatement(
|
|
103
117
|
statement: PointCoreStatement,
|
|
104
118
|
fn: PointCoreFunctionDeclaration,
|
|
105
|
-
locals:
|
|
119
|
+
locals: Scope,
|
|
106
120
|
) {
|
|
107
121
|
if (statement.kind === "return") {
|
|
108
122
|
if (!statement.value) {
|
|
@@ -117,30 +131,113 @@ class CoreChecker {
|
|
|
117
131
|
if (statement.kind === "value") {
|
|
118
132
|
this.checkType(statement.type, `fn.${fn.name}.${statement.name}.type`);
|
|
119
133
|
this.checkExpressionAssignable(statement.value, statement.type, `fn.${fn.name}.${statement.name}.value`, locals);
|
|
120
|
-
locals.set(statement.name, statement.type);
|
|
134
|
+
locals.set(statement.name, { type: statement.type, mutable: statement.mutable });
|
|
121
135
|
return;
|
|
122
136
|
}
|
|
123
|
-
if (statement.kind === "
|
|
124
|
-
this.
|
|
125
|
-
const thenLocals = new Map(locals);
|
|
126
|
-
for (const child of statement.thenBody) this.checkStatement(child, fn, thenLocals);
|
|
127
|
-
const elseLocals = new Map(locals);
|
|
128
|
-
for (const child of statement.elseBody) this.checkStatement(child, fn, elseLocals);
|
|
137
|
+
if (statement.kind === "assignment") {
|
|
138
|
+
this.checkAssignment(statement, fn, locals);
|
|
129
139
|
return;
|
|
130
140
|
}
|
|
131
|
-
|
|
132
|
-
|
|
141
|
+
if (statement.kind === "if") {
|
|
142
|
+
this.checkExpressionAssignable(statement.condition, typeRef("Bool"), `fn.${fn.name}.if.condition`, locals);
|
|
143
|
+
const thenLocals = new Map(locals);
|
|
144
|
+
for (const child of statement.thenBody) this.checkStatement(child, fn, thenLocals);
|
|
145
|
+
const elseLocals = new Map(locals);
|
|
146
|
+
for (const child of statement.elseBody) this.checkStatement(child, fn, elseLocals);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (statement.kind === "for") {
|
|
150
|
+
this.checkForStatement(statement, fn, locals);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
this.typeOfExpression(statement.value, locals, `fn.${fn.name}.expression`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private checkForStatement(
|
|
157
|
+
statement: Extract<PointCoreStatement, { kind: "for" }>,
|
|
158
|
+
fn: PointCoreFunctionDeclaration,
|
|
159
|
+
locals: Scope,
|
|
160
|
+
) {
|
|
161
|
+
const path = `fn.${fn.name}.for.${statement.itemName}`;
|
|
162
|
+
const iterableType = this.typeOfExpression(statement.iterable, locals, `${path}.iterable`);
|
|
163
|
+
if (!iterableType) return;
|
|
164
|
+
if (iterableType.name !== "List" || iterableType.args.length !== 1) {
|
|
165
|
+
this.push("iteration-type-mismatch", "for requires a List<T> iterable", path, statement.span, {
|
|
166
|
+
expected: "List<T>",
|
|
167
|
+
actual: formatType(iterableType),
|
|
168
|
+
repair: "Iterate over a List<T> value or change this expression to a list.",
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const loopLocals = new Map(locals);
|
|
173
|
+
loopLocals.set(statement.itemName, { type: iterableType.args[0]!, mutable: false });
|
|
174
|
+
for (const child of statement.body) this.checkStatement(child, fn, loopLocals);
|
|
175
|
+
}
|
|
133
176
|
|
|
134
|
-
private
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
scope: Map<string, PointCoreTypeExpression>,
|
|
177
|
+
private checkAssignment(
|
|
178
|
+
statement: Extract<PointCoreStatement, { kind: "assignment" }>,
|
|
179
|
+
fn: PointCoreFunctionDeclaration,
|
|
180
|
+
locals: Scope,
|
|
139
181
|
) {
|
|
140
|
-
|
|
141
|
-
|
|
182
|
+
const target = locals.get(statement.name);
|
|
183
|
+
const path = `fn.${fn.name}.${statement.name}.assignment`;
|
|
184
|
+
if (!target) {
|
|
185
|
+
this.push("unknown-identifier", `Unknown identifier ${statement.name}`, path, statement.span, {
|
|
186
|
+
actual: statement.name,
|
|
187
|
+
repair: `Declare var ${statement.name}: <Type> before assigning to it.`,
|
|
188
|
+
});
|
|
189
|
+
this.typeOfExpression(statement.value, locals, `${path}.value`);
|
|
142
190
|
return;
|
|
143
191
|
}
|
|
192
|
+
if (!target.mutable) {
|
|
193
|
+
this.push("immutable-assignment", `Cannot assign to immutable value ${statement.name}`, path, statement.span, {
|
|
194
|
+
actual: statement.name,
|
|
195
|
+
repair: `Declare ${statement.name} with var if it needs to change.`,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if ((statement.operator === "+=" || statement.operator === "-=") && !isNumeric(String(target.type.name))) {
|
|
199
|
+
this.push("operator-type-mismatch", `${statement.operator} requires a numeric target`, path, statement.span, {
|
|
200
|
+
expected: "Int or Float target",
|
|
201
|
+
actual: formatType(target.type),
|
|
202
|
+
repair: `Use ${statement.operator} only with Int or Float values.`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
this.checkExpressionAssignable(statement.value, target.type, `${path}.value`, locals);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private checkExpressionAssignable(
|
|
209
|
+
expression: PointCoreExpression,
|
|
210
|
+
expected: PointCoreTypeExpression,
|
|
211
|
+
path: string,
|
|
212
|
+
scope: Scope,
|
|
213
|
+
) {
|
|
214
|
+
if (expected.name === "Maybe" && expected.args.length === 1) {
|
|
215
|
+
if (expression.kind === "literal" && expression.value === null) return;
|
|
216
|
+
if (expression.kind !== "record" && expression.kind !== "list") {
|
|
217
|
+
const actual = this.typeOfExpression(expression, scope, path);
|
|
218
|
+
if (actual && sameType(actual, expected)) return;
|
|
219
|
+
}
|
|
220
|
+
this.checkExpressionAssignable(expression, expected.args[0]!, path, scope);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (expected.name === "Or" && expected.args.length > 0) {
|
|
224
|
+
const diagnosticsBefore = this.diagnostics.length;
|
|
225
|
+
const actual = this.typeOfExpression(expression, scope, path);
|
|
226
|
+
if (!actual) return;
|
|
227
|
+
if (sameType(actual, expected)) return;
|
|
228
|
+
if (expected.args.some((candidate) => sameType(candidate, actual))) return;
|
|
229
|
+
this.push("type-mismatch", `Expected ${formatType(expected)}, got ${formatType(actual)}`, path, expression.span, {
|
|
230
|
+
expected: formatType(expected),
|
|
231
|
+
actual: formatType(actual),
|
|
232
|
+
repair: `Return or assign one of: ${expected.args.map(formatType).join(", ")}.`,
|
|
233
|
+
});
|
|
234
|
+
if (this.diagnostics.length > diagnosticsBefore + 1) return;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (expression.kind === "list") {
|
|
238
|
+
this.checkListAssignable(expression, expected, path, scope);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
144
241
|
if (expression.kind === "record") {
|
|
145
242
|
this.checkRecordAssignable(expression, expected, path, scope);
|
|
146
243
|
return;
|
|
@@ -159,7 +256,7 @@ class CoreChecker {
|
|
|
159
256
|
expression: Extract<PointCoreExpression, { kind: "list" }>,
|
|
160
257
|
expected: PointCoreTypeExpression,
|
|
161
258
|
path: string,
|
|
162
|
-
scope:
|
|
259
|
+
scope: Scope,
|
|
163
260
|
) {
|
|
164
261
|
if (expected.name !== "List" || expected.args.length !== 1) {
|
|
165
262
|
this.push("type-mismatch", `Expected ${formatType(expected)}, got List`, path, expression.span, {
|
|
@@ -178,7 +275,7 @@ class CoreChecker {
|
|
|
178
275
|
expression: Extract<PointCoreExpression, { kind: "record" }>,
|
|
179
276
|
expected: PointCoreTypeExpression,
|
|
180
277
|
path: string,
|
|
181
|
-
scope:
|
|
278
|
+
scope: Scope,
|
|
182
279
|
) {
|
|
183
280
|
const declaration = this.typeDeclarations.get(String(expected.name));
|
|
184
281
|
if (!declaration) {
|
|
@@ -214,15 +311,17 @@ class CoreChecker {
|
|
|
214
311
|
}
|
|
215
312
|
}
|
|
216
313
|
|
|
217
|
-
private typeOfExpression(
|
|
218
|
-
expression: PointCoreExpression,
|
|
219
|
-
scope:
|
|
220
|
-
path: string,
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
314
|
+
private typeOfExpression(
|
|
315
|
+
expression: PointCoreExpression,
|
|
316
|
+
scope: Scope,
|
|
317
|
+
path: string,
|
|
318
|
+
awaitedCall = false,
|
|
319
|
+
): PointCoreTypeExpression | null {
|
|
320
|
+
if (expression.kind === "literal") {
|
|
321
|
+
if (expression.value === null) return { kind: "typeRef", name: "Void", args: [], span: expression.span };
|
|
322
|
+
const valueType =
|
|
323
|
+
typeof expression.value === "string"
|
|
324
|
+
? "Text"
|
|
226
325
|
: typeof expression.value === "boolean"
|
|
227
326
|
? "Bool"
|
|
228
327
|
: Number.isInteger(expression.value)
|
|
@@ -236,32 +335,58 @@ class CoreChecker {
|
|
|
236
335
|
return null;
|
|
237
336
|
}
|
|
238
337
|
if (expression.kind === "identifier") {
|
|
239
|
-
const
|
|
240
|
-
if (!
|
|
338
|
+
const entry = scope.get(expression.name);
|
|
339
|
+
if (!entry) {
|
|
241
340
|
this.push("unknown-identifier", `Unknown identifier ${expression.name}`, path, expression.span, {
|
|
242
341
|
actual: expression.name,
|
|
243
342
|
repair: `Declare ${expression.name}, pass it as a parameter, or replace it with an in-scope symbol.`,
|
|
244
343
|
});
|
|
245
344
|
return null;
|
|
246
345
|
}
|
|
247
|
-
return type;
|
|
346
|
+
return entry.type;
|
|
248
347
|
}
|
|
249
348
|
if (expression.kind === "binary") {
|
|
250
349
|
return this.typeOfBinaryExpression(expression, scope, path);
|
|
251
350
|
}
|
|
252
|
-
if (expression.kind === "property") {
|
|
253
|
-
return this.typeOfPropertyExpression(expression, scope, path);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
351
|
+
if (expression.kind === "property") {
|
|
352
|
+
return this.typeOfPropertyExpression(expression, scope, path);
|
|
353
|
+
}
|
|
354
|
+
if (expression.kind === "await") {
|
|
355
|
+
return this.typeOfExpression(expression.value, scope, path, true);
|
|
356
|
+
}
|
|
357
|
+
if (expression.callee === "Error") {
|
|
358
|
+
if (expression.args.length !== 1) {
|
|
359
|
+
this.push("arity-mismatch", "Error expects 1 message argument", path, expression.span, {
|
|
360
|
+
expected: "1 arg",
|
|
361
|
+
actual: `${expression.args.length} args`,
|
|
362
|
+
repair: "Construct errors as Error(\"message\").",
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
const message = expression.args[0];
|
|
366
|
+
if (message) this.checkExpressionAssignable(message, typeRef("Text"), `${path}.message`, scope);
|
|
367
|
+
return typeRef("Error", [], expression.span);
|
|
368
|
+
}
|
|
369
|
+
if (expression.callee === "Ok") {
|
|
370
|
+
return expression.args[0] ? this.typeOfExpression(expression.args[0], scope, `${path}.value`) : typeRef("Void", [], expression.span);
|
|
371
|
+
}
|
|
372
|
+
const target = this.functions.get(expression.callee);
|
|
373
|
+
if (!target) {
|
|
374
|
+
this.push("unknown-function", `Unknown function ${expression.callee}`, path, expression.span, {
|
|
375
|
+
actual: expression.callee,
|
|
376
|
+
expected: [...this.functions.keys()],
|
|
377
|
+
repair: `Define fn ${expression.callee}(...) or call an existing function.`,
|
|
378
|
+
});
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
if ((target.semantic?.kind === "action" || target.semantic?.kind === "workflow") && !awaitedCall) {
|
|
382
|
+
this.push("missing-await", `Action ${expression.callee} must be awaited`, path, expression.span, {
|
|
383
|
+
expected: `await ${expression.callee}(...)`,
|
|
384
|
+
actual: `${expression.callee}(...)`,
|
|
385
|
+
repair: "Prefix this action call with await.",
|
|
386
|
+
relatedRefs: [this.refFor(`fn.${target.name}`)],
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
if (target.params.length !== expression.args.length) {
|
|
265
390
|
this.push("arity-mismatch", `Function ${expression.callee} expects ${target.params.length} args`, path, expression.span, {
|
|
266
391
|
expected: `${target.params.length} args`,
|
|
267
392
|
actual: `${expression.args.length} args`,
|
|
@@ -278,7 +403,7 @@ class CoreChecker {
|
|
|
278
403
|
|
|
279
404
|
private typeOfListExpression(
|
|
280
405
|
expression: Extract<PointCoreExpression, { kind: "list" }>,
|
|
281
|
-
scope:
|
|
406
|
+
scope: Scope,
|
|
282
407
|
path: string,
|
|
283
408
|
): PointCoreTypeExpression | null {
|
|
284
409
|
if (expression.items.length === 0) {
|
|
@@ -298,12 +423,20 @@ class CoreChecker {
|
|
|
298
423
|
|
|
299
424
|
private typeOfPropertyExpression(
|
|
300
425
|
expression: Extract<PointCoreExpression, { kind: "property" }>,
|
|
301
|
-
scope:
|
|
426
|
+
scope: Scope,
|
|
302
427
|
path: string,
|
|
303
428
|
): PointCoreTypeExpression | null {
|
|
304
|
-
const targetType = this.typeOfExpression(expression.target, scope, `${path}.target`);
|
|
305
|
-
if (!targetType) return null;
|
|
306
|
-
|
|
429
|
+
const targetType = this.typeOfExpression(expression.target, scope, `${path}.target`);
|
|
430
|
+
if (!targetType) return null;
|
|
431
|
+
if (targetType.name === "Maybe" && targetType.args.length === 1) {
|
|
432
|
+
this.push("nullable-field-access", `Cannot access field ${expression.name} on nullable ${formatType(targetType)}`, path, expression.span, {
|
|
433
|
+
expected: formatType(targetType.args[0]!),
|
|
434
|
+
actual: formatType(targetType),
|
|
435
|
+
repair: "Check that this Maybe value is present before accessing its fields.",
|
|
436
|
+
});
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
const declaration = this.typeDeclarations.get(String(targetType.name));
|
|
307
440
|
if (!declaration) {
|
|
308
441
|
this.push("not-a-record", `${formatType(targetType)} has no fields`, path, expression.span, {
|
|
309
442
|
actual: formatType(targetType),
|
|
@@ -326,7 +459,7 @@ class CoreChecker {
|
|
|
326
459
|
|
|
327
460
|
private typeOfBinaryExpression(
|
|
328
461
|
expression: Extract<PointCoreExpression, { kind: "binary" }>,
|
|
329
|
-
scope:
|
|
462
|
+
scope: Scope,
|
|
330
463
|
path: string,
|
|
331
464
|
): PointCoreTypeExpression | null {
|
|
332
465
|
const left = this.typeOfExpression(expression.left, scope, `${path}.left`);
|
|
@@ -377,14 +510,28 @@ class CoreChecker {
|
|
|
377
510
|
repair: `Declare type ${type.name} or use an existing type.`,
|
|
378
511
|
});
|
|
379
512
|
}
|
|
380
|
-
if (type.name === "List" && type.args.length !== 1) {
|
|
513
|
+
if (type.name === "List" && type.args.length !== 1) {
|
|
381
514
|
this.push("invalid-type-arity", "List requires one type argument", path, type.span, {
|
|
382
515
|
expected: "List<T>",
|
|
383
516
|
actual: formatType(type),
|
|
384
517
|
repair: "Use List<Text>, List<Int>, or another concrete item type.",
|
|
385
518
|
});
|
|
386
|
-
}
|
|
387
|
-
if (type.name
|
|
519
|
+
}
|
|
520
|
+
if (type.name === "Maybe" && type.args.length !== 1) {
|
|
521
|
+
this.push("invalid-type-arity", "Maybe requires one type argument", path, type.span, {
|
|
522
|
+
expected: "Maybe<T>",
|
|
523
|
+
actual: formatType(type),
|
|
524
|
+
repair: "Use Maybe<Text>, Maybe<User>, or another concrete optional type.",
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
if (type.name === "Or" && type.args.length < 2) {
|
|
528
|
+
this.push("invalid-type-arity", "Or requires at least two type arguments", path, type.span, {
|
|
529
|
+
expected: "A or B",
|
|
530
|
+
actual: formatType(type),
|
|
531
|
+
repair: "Use syntax such as User or Error.",
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
if (type.name !== "List" && type.name !== "Maybe" && type.name !== "Or" && type.args.length > 0 && !this.typeDeclarations.has(String(type.name))) {
|
|
388
535
|
this.push("invalid-type-arity", `${type.name} does not accept type arguments`, path, type.span, {
|
|
389
536
|
expected: String(type.name),
|
|
390
537
|
actual: formatType(type),
|
|
@@ -431,11 +578,12 @@ function sameType(left: PointCoreTypeExpression, right: PointCoreTypeExpression)
|
|
|
431
578
|
return left.name === right.name && leftArgs.length === rightArgs.length && leftArgs.every((arg, index) => sameType(arg, rightArgs[index]!));
|
|
432
579
|
}
|
|
433
580
|
|
|
434
|
-
function formatType(type: PointCoreTypeExpression): string {
|
|
435
|
-
const args = type.args ?? [];
|
|
436
|
-
if (args.length === 0) return String(type.name);
|
|
437
|
-
|
|
438
|
-
}
|
|
581
|
+
function formatType(type: PointCoreTypeExpression): string {
|
|
582
|
+
const args = type.args ?? [];
|
|
583
|
+
if (args.length === 0) return String(type.name);
|
|
584
|
+
if (type.name === "Or") return args.map(formatType).join(" or ");
|
|
585
|
+
return `${type.name}<${args.map(formatType).join(", ")}>`;
|
|
586
|
+
}
|
|
439
587
|
|
|
440
588
|
function typeRef(name: string, args: PointCoreTypeExpression[] = [], span?: PointSourceSpan): PointCoreTypeExpression {
|
|
441
589
|
return { kind: "typeRef", name, args, span };
|