@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 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
- - core parser, formatter, checker, and TypeScript emitter APIs
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 point:fmt-check:all
15
- bun run point:check:all
16
- bun run point:build:all
27
+ bun run fmt-check
28
+ bun run check
29
+ bun run build
17
30
  ```
18
31
 
19
- `point:build:all` emits TypeScript that can be imported by React, Vue, Bun, Node, and Vite projects.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hatchingpoint/point",
3
- "version": "0.0.3",
3
+ "version": "0.0.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Point language compiler and CLI.",
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
- PointCoreFunctionDeclaration,
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<string, PointCoreTypeExpression>();
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: Map<string, PointCoreTypeExpression>,
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 === "if") {
124
- this.checkExpressionAssignable(statement.condition, typeRef("Bool"), `fn.${fn.name}.if.condition`, locals);
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
- this.typeOfExpression(statement.value, locals, `fn.${fn.name}.expression`);
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 checkExpressionAssignable(
135
- expression: PointCoreExpression,
136
- expected: PointCoreTypeExpression,
137
- path: string,
138
- scope: Map<string, PointCoreTypeExpression>,
177
+ private checkAssignment(
178
+ statement: Extract<PointCoreStatement, { kind: "assignment" }>,
179
+ fn: PointCoreFunctionDeclaration,
180
+ locals: Scope,
139
181
  ) {
140
- if (expression.kind === "list") {
141
- this.checkListAssignable(expression, expected, path, scope);
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: Map<string, PointCoreTypeExpression>,
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: Map<string, PointCoreTypeExpression>,
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: Map<string, PointCoreTypeExpression>,
220
- path: string,
221
- ): PointCoreTypeExpression | null {
222
- if (expression.kind === "literal") {
223
- const valueType =
224
- typeof expression.value === "string"
225
- ? "Text"
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 type = scope.get(expression.name);
240
- if (!type) {
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
- const target = this.functions.get(expression.callee);
256
- if (!target) {
257
- this.push("unknown-function", `Unknown function ${expression.callee}`, path, expression.span, {
258
- actual: expression.callee,
259
- expected: [...this.functions.keys()],
260
- repair: `Define fn ${expression.callee}(...) or call an existing function.`,
261
- });
262
- return null;
263
- }
264
- if (target.params.length !== expression.args.length) {
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: Map<string, PointCoreTypeExpression>,
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: Map<string, PointCoreTypeExpression>,
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
- const declaration = this.typeDeclarations.get(String(targetType.name));
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: Map<string, PointCoreTypeExpression>,
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 !== "List" && type.args.length > 0 && !this.typeDeclarations.has(String(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
- return `${type.name}<${args.map(formatType).join(", ")}>`;
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 };