@ascent-lang/dev 0.1.0 → 0.2.0
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/dist/errors/elaborate.d.ts +24 -0
- package/dist/errors/elaborate.d.ts.map +1 -0
- package/dist/errors/elaborate.js +53 -0
- package/dist/errors/elaborate.js.map +1 -0
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +356 -30
- package/dist/errors/index.js.map +1 -1
- package/dist/errors/render.d.ts +3 -0
- package/dist/errors/render.d.ts.map +1 -0
- package/dist/errors/render.js +43 -0
- package/dist/errors/render.js.map +1 -0
- package/dist/errors/types.d.ts +29 -0
- package/dist/errors/types.d.ts.map +1 -1
- package/dist/index.js +18 -11
- package/dist/index.js.map +1 -1
- package/dist/interpreter.d.ts.map +1 -1
- package/dist/interpreter.js +28 -5
- package/dist/interpreter.js.map +1 -1
- package/dist/lexer/index.d.ts.map +1 -1
- package/dist/lexer/index.js +4 -3
- package/dist/lexer/index.js.map +1 -1
- package/dist/lexer/keywords.d.ts.map +1 -1
- package/dist/lexer/keywords.js +3 -0
- package/dist/lexer/keywords.js.map +1 -1
- package/dist/lexer/token.d.ts +7 -1
- package/dist/lexer/token.d.ts.map +1 -1
- package/dist/parser/ast.d.ts +8 -4
- package/dist/parser/ast.d.ts.map +1 -1
- package/dist/parser/expr.d.ts.map +1 -1
- package/dist/parser/expr.js +34 -19
- package/dist/parser/expr.js.map +1 -1
- package/dist/parser/stmt.d.ts.map +1 -1
- package/dist/parser/stmt.js +5 -3
- package/dist/parser/stmt.js.map +1 -1
- package/dist/parser/token-stream.d.ts +4 -4
- package/dist/parser/token-stream.d.ts.map +1 -1
- package/dist/parser/token-stream.js +21 -9
- package/dist/parser/token-stream.js.map +1 -1
- package/dist/parser/type-expr.d.ts.map +1 -1
- package/dist/parser/type-expr.js +3 -2
- package/dist/parser/type-expr.js.map +1 -1
- package/dist/parser/typechecker.d.ts.map +1 -1
- package/dist/parser/typechecker.js +109 -67
- package/dist/parser/typechecker.js.map +1 -1
- package/dist/types/types.d.ts +4 -0
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/types.js +27 -15
- package/dist/types/types.js.map +1 -1
- package/package.json +1 -1
- package/src/errors/elaborate.ts +88 -0
- package/src/errors/index.ts +356 -30
- package/src/errors/lexical.yml +48 -13
- package/src/errors/name.yml +45 -9
- package/src/errors/render.ts +59 -0
- package/src/errors/syntactic.yml +128 -49
- package/src/errors/typechecker.yml +147 -61
- package/src/errors/types.ts +55 -0
- package/src/index.ts +17 -11
- package/src/interpreter.ts +24 -6
- package/src/lexer/index.ts +4 -3
- package/src/lexer/keywords.ts +3 -0
- package/src/lexer/token.ts +18 -0
- package/src/parser/ast.ts +7 -6
- package/src/parser/expr.ts +34 -19
- package/src/parser/stmt.ts +5 -3
- package/src/parser/token-stream.ts +20 -8
- package/src/parser/type-expr.ts +3 -2
- package/src/parser/typechecker.ts +140 -52
- package/src/types/types.ts +36 -16
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { Expr, Statement, Program, Block, If, TypeExpr } from './ast.js';
|
|
1
|
+
import type { Expr, Statement, Program, Block, If, TypeExpr, TypeName, ArgType } from './ast.js';
|
|
2
2
|
import type { Marker, Span } from '../lexer/token.js';
|
|
3
3
|
import type { TypedExpr, TypedBlock, TypedIf, TypedStatement, TypedProgram } from './typed-ast.js';
|
|
4
4
|
import {
|
|
5
5
|
AscentType, INT_TYPE, FLOAT_TYPE, BOOL_TYPE, STRING_TYPE, NONE_TYPE, DONE_TYPE, listOfType,
|
|
6
|
-
leastCommonType, isAssignableTo,
|
|
6
|
+
leastCommonType, isAssignableTo, typeToString,
|
|
7
7
|
} from '../types/types.js';
|
|
8
8
|
|
|
9
9
|
export interface TypeCheckResult {
|
|
@@ -11,17 +11,27 @@ export interface TypeCheckResult {
|
|
|
11
11
|
errorMarkers: Marker[];
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// origin records how the name was created — 'fix'/'mut' declarations, or a
|
|
15
|
+
// program 'arg' input — so the three reassignment mistakes get distinct errors.
|
|
16
|
+
// declSpan is where a fix/mut name was created (so errors can point back at it);
|
|
17
|
+
// it is null for names with no source location (program args).
|
|
18
|
+
interface Binding {
|
|
19
|
+
ty: AscentType;
|
|
20
|
+
origin: 'fix' | 'mut' | 'arg';
|
|
21
|
+
declSpan: Span | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
// A chain of scopes mirroring Environment in the interpreter.
|
|
15
25
|
class TypeEnv {
|
|
16
|
-
private vars = new Map<string,
|
|
26
|
+
private vars = new Map<string, Binding>();
|
|
17
27
|
public constructor(private readonly parent: TypeEnv | null = null) { }
|
|
18
28
|
|
|
19
|
-
public get(name: string):
|
|
29
|
+
public get(name: string): Binding | null {
|
|
20
30
|
return this.vars.get(name) ?? this.parent?.get(name) ?? null;
|
|
21
31
|
}
|
|
22
32
|
|
|
23
|
-
public set(name: string, ty: AscentType,
|
|
24
|
-
this.vars.set(name, { ty,
|
|
33
|
+
public set(name: string, ty: AscentType, origin: Binding['origin'], declSpan: Span | null = null): void {
|
|
34
|
+
this.vars.set(name, { ty, origin, declSpan });
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
public child(): TypeEnv {
|
|
@@ -29,34 +39,59 @@ class TypeEnv {
|
|
|
29
39
|
}
|
|
30
40
|
}
|
|
31
41
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
case '
|
|
43
|
-
|
|
42
|
+
// ---- Type formation: ⊢ T type --------------------------------------
|
|
43
|
+
//
|
|
44
|
+
// The one place a syntactic type name becomes a semantic AscentType.
|
|
45
|
+
// Total over the name union, so an unexpected name is a compile error
|
|
46
|
+
// here rather than a silent fall-through elsewhere.
|
|
47
|
+
|
|
48
|
+
const typeFromName = (name: TypeName['name'] | ArgType): AscentType => {
|
|
49
|
+
switch (name) {
|
|
50
|
+
case 'Int': return INT_TYPE;
|
|
51
|
+
case 'Float': return FLOAT_TYPE;
|
|
52
|
+
case 'Bool': return BOOL_TYPE;
|
|
53
|
+
case 'String': return STRING_TYPE;
|
|
44
54
|
}
|
|
45
55
|
};
|
|
46
56
|
|
|
57
|
+
const typeFromExpr = (te: TypeExpr): AscentType =>
|
|
58
|
+
te.kind === 'TypeName' ? typeFromName(te.name) : listOfType(typeFromExpr(te.elem));
|
|
59
|
+
|
|
47
60
|
// ---- Method type signatures ------------------------------------------
|
|
48
61
|
|
|
49
62
|
const requireArity = (expected: number, got: number, markers: Marker[], span: Span): boolean => {
|
|
50
|
-
if (got !== expected) {
|
|
63
|
+
if (got !== expected) {
|
|
64
|
+
markers.push({ code: 'T0007', span, data: { expected: String(expected), got: String(got) } });
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
51
67
|
return true;
|
|
52
68
|
};
|
|
53
69
|
|
|
70
|
+
// A value-type mismatch that carries the expected and actual type names.
|
|
71
|
+
const typeMismatch = (
|
|
72
|
+
code: string, markers: Marker[], span: Span, expected: AscentType, actual: AscentType,
|
|
73
|
+
related: { key: string; span: Span }[] = [],
|
|
74
|
+
): null => {
|
|
75
|
+
markers.push({
|
|
76
|
+
code, span, related,
|
|
77
|
+
data: { expected: typeToString(expected), actual: typeToString(actual) },
|
|
78
|
+
});
|
|
79
|
+
return null;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// An operator applied to operands it doesn't accept (T0009). `operands` is the
|
|
83
|
+
// joined list of type names — one for a unary '-', two for a binary operator.
|
|
84
|
+
const operandError = (markers: Marker[], op: string, span: Span, ...operands: AscentType[]): null => {
|
|
85
|
+
markers.push({ code: 'T0009', span, data: { op, operands: operands.map(typeToString).join(' and ') } });
|
|
86
|
+
return null;
|
|
87
|
+
};
|
|
88
|
+
|
|
54
89
|
const intMethodType = (method: string, argTypes: AscentType[], markers: Marker[], span: Span): AscentType | null => {
|
|
55
90
|
switch (method) {
|
|
56
91
|
case 'toStr': return requireArity(0, argTypes.length, markers, span) ? STRING_TYPE : null;
|
|
57
92
|
case 'toFloat': return requireArity(0, argTypes.length, markers, span) ? FLOAT_TYPE : null;
|
|
58
93
|
case 'abs': return requireArity(0, argTypes.length, markers, span) ? INT_TYPE : null;
|
|
59
|
-
default: markers.push({ code: 'T0006', span }); return null;
|
|
94
|
+
default: markers.push({ code: 'T0006', span, data: { method, type: 'Int' } }); return null;
|
|
60
95
|
}
|
|
61
96
|
};
|
|
62
97
|
|
|
@@ -65,7 +100,7 @@ const floatMethodType = (method: string, argTypes: AscentType[], markers: Marker
|
|
|
65
100
|
case 'toStr': return requireArity(0, argTypes.length, markers, span) ? STRING_TYPE : null;
|
|
66
101
|
case 'toInt': return requireArity(0, argTypes.length, markers, span) ? INT_TYPE : null;
|
|
67
102
|
case 'abs': return requireArity(0, argTypes.length, markers, span) ? FLOAT_TYPE : null;
|
|
68
|
-
default: markers.push({ code: 'T0006', span }); return null;
|
|
103
|
+
default: markers.push({ code: 'T0006', span, data: { method, type: 'Float' } }); return null;
|
|
69
104
|
}
|
|
70
105
|
};
|
|
71
106
|
|
|
@@ -80,18 +115,18 @@ const listMethodType = (
|
|
|
80
115
|
case 'prepend': {
|
|
81
116
|
if (!requireArity(1, argTypes.length, markers, span)) return null;
|
|
82
117
|
const ct = leastCommonType(elemType, argTypes[0]!);
|
|
83
|
-
if (ct === null)
|
|
118
|
+
if (ct === null) return typeMismatch('T0008', markers, span, elemType, argTypes[0]!);
|
|
84
119
|
return listOfType(ct);
|
|
85
120
|
}
|
|
86
121
|
case 'concat': {
|
|
87
122
|
if (!requireArity(1, argTypes.length, markers, span)) return null;
|
|
88
123
|
const arg = argTypes[0]!;
|
|
89
|
-
if (arg.kind !== 'List')
|
|
124
|
+
if (arg.kind !== 'List') return typeMismatch('T0008', markers, span, listOfType(elemType), arg);
|
|
90
125
|
const ct = leastCommonType(elemType, arg.elem);
|
|
91
|
-
if (ct === null)
|
|
126
|
+
if (ct === null) return typeMismatch('T0008', markers, span, listOfType(elemType), arg);
|
|
92
127
|
return listOfType(ct);
|
|
93
128
|
}
|
|
94
|
-
default: markers.push({ code: 'T0006', span }); return null;
|
|
129
|
+
default: markers.push({ code: 'T0006', span, data: { method, type: typeToString(listOfType(elemType)) } }); return null;
|
|
95
130
|
}
|
|
96
131
|
};
|
|
97
132
|
|
|
@@ -124,19 +159,25 @@ const inferExpr = (
|
|
|
124
159
|
|
|
125
160
|
case 'call': {
|
|
126
161
|
// floor is the only built-in for now.
|
|
127
|
-
if (expr.callee !== 'floor') { markers.push({ code: '
|
|
162
|
+
if (expr.callee !== 'floor') { markers.push({ code: 'T0013', span: expr.span, data: { name: expr.callee } }); return null; }
|
|
128
163
|
if (!requireArity(1, expr.args.length, markers, expr.span)) return null;
|
|
129
164
|
const typedArg = inferExpr(expr.args[0]!, env, markers);
|
|
130
165
|
if (typedArg === null) return null;
|
|
131
|
-
if (typedArg.type.kind !== 'Float')
|
|
166
|
+
if (typedArg.type.kind !== 'Float') return typeMismatch('T0008', markers, expr.span, FLOAT_TYPE, typedArg.type);
|
|
132
167
|
return { kind: 'call', callee: expr.callee, args: [typedArg], type: FLOAT_TYPE, span: expr.span };
|
|
133
168
|
}
|
|
134
169
|
|
|
135
170
|
case 'unary': {
|
|
136
171
|
const typedOperand = inferExpr(expr.operand, env, markers);
|
|
137
172
|
if (typedOperand === null) return null;
|
|
173
|
+
if (expr.op === 'not') {
|
|
174
|
+
if (typedOperand.type.kind !== 'Bool') {
|
|
175
|
+
return operandError(markers, expr.op, expr.span, typedOperand.type);
|
|
176
|
+
}
|
|
177
|
+
return { kind: 'unary', op: expr.op, operand: typedOperand, type: BOOL_TYPE, span: expr.span };
|
|
178
|
+
}
|
|
138
179
|
if (typedOperand.type.kind !== 'Int' && typedOperand.type.kind !== 'Float') {
|
|
139
|
-
markers.
|
|
180
|
+
return operandError(markers, expr.op, expr.span, typedOperand.type);
|
|
140
181
|
}
|
|
141
182
|
return { kind: 'unary', op: expr.op, operand: typedOperand, type: typedOperand.type, span: expr.span };
|
|
142
183
|
}
|
|
@@ -152,35 +193,43 @@ const inferExpr = (
|
|
|
152
193
|
switch (expr.op) {
|
|
153
194
|
case '+': case '-': case '*': {
|
|
154
195
|
if ((lt.kind !== 'Int' && lt.kind !== 'Float') || (rt.kind !== 'Int' && rt.kind !== 'Float')) {
|
|
155
|
-
markers.
|
|
196
|
+
return operandError(markers, expr.op, expr.span, lt, rt);
|
|
156
197
|
}
|
|
157
198
|
type = (lt.kind === 'Float' || rt.kind === 'Float') ? FLOAT_TYPE : INT_TYPE;
|
|
158
199
|
break;
|
|
159
200
|
}
|
|
160
201
|
case '/': {
|
|
161
202
|
if ((lt.kind !== 'Int' && lt.kind !== 'Float') || (rt.kind !== 'Int' && rt.kind !== 'Float')) {
|
|
162
|
-
markers.
|
|
203
|
+
return operandError(markers, expr.op, expr.span, lt, rt);
|
|
163
204
|
}
|
|
164
205
|
type = FLOAT_TYPE;
|
|
165
206
|
break;
|
|
166
207
|
}
|
|
167
208
|
case 'div': case 'mod': {
|
|
168
209
|
if (lt.kind !== 'Int' || rt.kind !== 'Int') {
|
|
169
|
-
markers.
|
|
210
|
+
return operandError(markers, expr.op, expr.span, lt, rt);
|
|
170
211
|
}
|
|
171
212
|
type = INT_TYPE;
|
|
172
213
|
break;
|
|
173
214
|
}
|
|
174
215
|
case '==': case '!=': {
|
|
175
216
|
if (leastCommonType(lt, rt) === null) {
|
|
176
|
-
markers.
|
|
217
|
+
return operandError(markers, expr.op, expr.span, lt, rt);
|
|
177
218
|
}
|
|
178
219
|
type = BOOL_TYPE;
|
|
179
220
|
break;
|
|
180
221
|
}
|
|
181
222
|
case '<': case '<=': case '>': case '>=': {
|
|
182
223
|
if ((lt.kind !== 'Int' && lt.kind !== 'Float') || (rt.kind !== 'Int' && rt.kind !== 'Float')) {
|
|
183
|
-
markers.
|
|
224
|
+
return operandError(markers, expr.op, expr.span, lt, rt);
|
|
225
|
+
}
|
|
226
|
+
type = BOOL_TYPE;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
case 'and':
|
|
230
|
+
case 'or': {
|
|
231
|
+
if (lt.kind !== 'Bool' || rt.kind !== 'Bool') {
|
|
232
|
+
return operandError(markers, expr.op, expr.span, lt, rt);
|
|
184
233
|
}
|
|
185
234
|
type = BOOL_TYPE;
|
|
186
235
|
break;
|
|
@@ -209,7 +258,14 @@ const inferExpr = (
|
|
|
209
258
|
let elemType: AscentType = typedElements[0]!.type;
|
|
210
259
|
for (const te of typedElements.slice(1)) {
|
|
211
260
|
const ct = leastCommonType(elemType, te.type);
|
|
212
|
-
if (ct === null) {
|
|
261
|
+
if (ct === null) {
|
|
262
|
+
markers.push({
|
|
263
|
+
code: 'T0002', span: expr.span,
|
|
264
|
+
data: { first: typeToString(elemType), other: typeToString(te.type) },
|
|
265
|
+
related: [{ key: 'element', span: te.span }],
|
|
266
|
+
});
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
213
269
|
elemType = ct;
|
|
214
270
|
}
|
|
215
271
|
// If the surrounding context expects a List with a wider element type
|
|
@@ -226,8 +282,14 @@ const inferExpr = (
|
|
|
226
282
|
const typedList = inferExpr(expr.list, env, markers);
|
|
227
283
|
const typedIndex = inferExpr(expr.index, env, markers);
|
|
228
284
|
if (typedList === null || typedIndex === null) return null;
|
|
229
|
-
if (typedList.type.kind !== 'List') {
|
|
230
|
-
|
|
285
|
+
if (typedList.type.kind !== 'List') {
|
|
286
|
+
markers.push({ code: 'T0010', span: expr.list.span, data: { actual: typeToString(typedList.type) } });
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
if (typedIndex.type.kind !== 'Int') {
|
|
290
|
+
markers.push({ code: 'T0011', span: expr.index.span, data: { actual: typeToString(typedIndex.type) } });
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
231
293
|
return { kind: 'index', list: typedList, index: typedIndex, type: typedList.type.elem, span: expr.span };
|
|
232
294
|
}
|
|
233
295
|
|
|
@@ -249,7 +311,7 @@ const inferExpr = (
|
|
|
249
311
|
case 'Int': resultType = intMethodType(expr.method, argTypes, markers, expr.span); break;
|
|
250
312
|
case 'Float': resultType = floatMethodType(expr.method, argTypes, markers, expr.span); break;
|
|
251
313
|
case 'List': resultType = listMethodType(typedReceiver.type.elem, expr.method, argTypes, markers, expr.span); break;
|
|
252
|
-
default: markers.push({ code: 'T0012', span: expr.span }); return null;
|
|
314
|
+
default: markers.push({ code: 'T0012', span: expr.span, data: { type: typeToString(typedReceiver.type) } }); return null;
|
|
253
315
|
}
|
|
254
316
|
if (resultType === null) return null;
|
|
255
317
|
return {
|
|
@@ -289,7 +351,7 @@ const inferBlock = (block: Block, env: TypeEnv, markers: Marker[]): TypedBlock |
|
|
|
289
351
|
const inferIf = (expr: If, env: TypeEnv, markers: Marker[]): TypedIf | null => {
|
|
290
352
|
const typedCond = inferExpr(expr.cond, env, markers);
|
|
291
353
|
if (typedCond !== null && typedCond.type.kind !== 'Bool') {
|
|
292
|
-
markers.push({ code: 'T0004', span: expr.cond.span });
|
|
354
|
+
markers.push({ code: 'T0004', span: expr.cond.span, data: { actual: typeToString(typedCond.type) } });
|
|
293
355
|
}
|
|
294
356
|
|
|
295
357
|
const typedThen = inferBlock(expr.then, env, markers);
|
|
@@ -306,7 +368,17 @@ const inferIf = (expr: If, env: TypeEnv, markers: Marker[]): TypedIf | null => {
|
|
|
306
368
|
if (typedCond === null || typedThen === null || typedElse === null) return null;
|
|
307
369
|
|
|
308
370
|
const ct = leastCommonType(typedThen.type, typedElse.type);
|
|
309
|
-
if (ct === null) {
|
|
371
|
+
if (ct === null) {
|
|
372
|
+
markers.push({
|
|
373
|
+
code: 'T0005', span: expr.span,
|
|
374
|
+
data: { then: typeToString(typedThen.type), else: typeToString(typedElse.type) },
|
|
375
|
+
related: [
|
|
376
|
+
{ key: 'then', span: typedThen.span },
|
|
377
|
+
{ key: 'else', span: typedElse.span },
|
|
378
|
+
],
|
|
379
|
+
});
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
310
382
|
|
|
311
383
|
return { kind: 'if', cond: typedCond, then: typedThen, else: typedElse, type: ct, span: expr.span };
|
|
312
384
|
};
|
|
@@ -315,20 +387,24 @@ const inferStmt = (stmt: Statement, env: TypeEnv, markers: Marker[]): TypedState
|
|
|
315
387
|
switch (stmt.kind) {
|
|
316
388
|
case 'fix':
|
|
317
389
|
case 'mut': {
|
|
318
|
-
const annotation = stmt.typeAnnotation !== null ?
|
|
390
|
+
const annotation = stmt.typeAnnotation !== null ? typeFromExpr(stmt.typeAnnotation) : null;
|
|
319
391
|
const typedInit = inferExpr(stmt.init, env, markers, annotation);
|
|
320
392
|
|
|
321
393
|
let slotType: AscentType | null;
|
|
322
394
|
if (annotation !== null) {
|
|
323
395
|
if (typedInit !== null && !isAssignableTo(typedInit.type, annotation)) {
|
|
324
|
-
markers.push({
|
|
396
|
+
markers.push({
|
|
397
|
+
code: 'T0001', span: stmt.init.span,
|
|
398
|
+
data: { expected: typeToString(annotation), actual: typeToString(typedInit.type) },
|
|
399
|
+
related: [{ key: 'annotation', span: stmt.typeAnnotation!.span }],
|
|
400
|
+
});
|
|
325
401
|
}
|
|
326
402
|
slotType = annotation;
|
|
327
403
|
} else {
|
|
328
404
|
slotType = typedInit?.type ?? null;
|
|
329
405
|
}
|
|
330
406
|
|
|
331
|
-
if (slotType !== null) env.set(stmt.name, slotType, stmt.kind
|
|
407
|
+
if (slotType !== null) env.set(stmt.name, slotType, stmt.kind, stmt.span);
|
|
332
408
|
if (typedInit === null) return null;
|
|
333
409
|
|
|
334
410
|
return {
|
|
@@ -344,13 +420,29 @@ const inferStmt = (stmt: Statement, env: TypeEnv, markers: Marker[]): TypedState
|
|
|
344
420
|
case 'assign': {
|
|
345
421
|
const binding = env.get(stmt.name);
|
|
346
422
|
if (binding === null) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
markers.push({ code: '
|
|
423
|
+
// Assigning to a name that was never created — a different mistake
|
|
424
|
+
// (and lesson) than using an undefined name in an expression (N0001).
|
|
425
|
+
markers.push({ code: 'N0003', span: stmt.nameSpan });
|
|
426
|
+
} else if (binding.origin === 'arg') {
|
|
427
|
+
// A program input is read-only for the whole run — its own lesson,
|
|
428
|
+
// distinct from a 'fix' slot (there is no 'mut' arg to switch to).
|
|
429
|
+
markers.push({ code: 'N0004', span: stmt.nameSpan });
|
|
430
|
+
} else if (binding.origin === 'fix') {
|
|
431
|
+
// Point back at the 'fix' declaration ("created with 'fix' here"),
|
|
432
|
+
// which always has a source location.
|
|
433
|
+
const related = binding.declSpan !== null
|
|
434
|
+
? [{ key: 'declaration', span: binding.declSpan }]
|
|
435
|
+
: [];
|
|
436
|
+
markers.push({ code: 'N0002', span: stmt.nameSpan, related });
|
|
350
437
|
}
|
|
351
438
|
const typedValue = inferExpr(stmt.value, env, markers);
|
|
352
439
|
if (binding !== null && typedValue !== null && !isAssignableTo(typedValue.type, binding.ty)) {
|
|
353
|
-
|
|
440
|
+
const related = binding.declSpan !== null ? [{ key: 'declaration', span: binding.declSpan }] : [];
|
|
441
|
+
markers.push({
|
|
442
|
+
code: 'T0001', span: stmt.value.span,
|
|
443
|
+
data: { expected: typeToString(binding.ty), actual: typeToString(typedValue.type) },
|
|
444
|
+
related,
|
|
445
|
+
});
|
|
354
446
|
}
|
|
355
447
|
if (typedValue === null) return null;
|
|
356
448
|
return {
|
|
@@ -365,7 +457,7 @@ const inferStmt = (stmt: Statement, env: TypeEnv, markers: Marker[]): TypedState
|
|
|
365
457
|
case 'while': {
|
|
366
458
|
const typedCond = inferExpr(stmt.cond, env, markers);
|
|
367
459
|
if (typedCond !== null && typedCond.type.kind !== 'Bool') {
|
|
368
|
-
markers.push({ code: 'T0004', span: stmt.cond.span });
|
|
460
|
+
markers.push({ code: 'T0004', span: stmt.cond.span, data: { actual: typeToString(typedCond.type) } });
|
|
369
461
|
}
|
|
370
462
|
const typedBody = inferBlock(stmt.body, env, markers);
|
|
371
463
|
if (typedCond === null || typedBody === null) return null;
|
|
@@ -385,11 +477,7 @@ export const typecheck = (program: Program): TypeCheckResult => {
|
|
|
385
477
|
const env = new TypeEnv();
|
|
386
478
|
|
|
387
479
|
for (const arg of program.args) {
|
|
388
|
-
|
|
389
|
-
: arg.type === 'Float' ? FLOAT_TYPE
|
|
390
|
-
: arg.type === 'Bool' ? BOOL_TYPE
|
|
391
|
-
: STRING_TYPE;
|
|
392
|
-
env.set(arg.name, ty, false);
|
|
480
|
+
env.set(arg.name, typeFromName(arg.type), 'arg');
|
|
393
481
|
}
|
|
394
482
|
|
|
395
483
|
const typedStmts: TypedStatement[] = [];
|
package/src/types/types.ts
CHANGED
|
@@ -34,20 +34,44 @@ export const typesEqual = (a: AscentType, b: AscentType): boolean => {
|
|
|
34
34
|
return true;
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
export
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
// A coercion is the runtime witness of a subtyping edge: how to turn a value
|
|
38
|
+
// of the sub-type into one of the super-type. `null` means the two types are
|
|
39
|
+
// equal — no runtime conversion needed.
|
|
40
|
+
export type Coercion = 'intToFloat' | { elem: Coercion } | null;
|
|
41
|
+
|
|
42
|
+
// S <: T — the one place widening is defined. Int widens to Float, and lists
|
|
43
|
+
// widen covariantly (sound only because Ascent lists are immutable: append /
|
|
44
|
+
// prepend / concat return new lists rather than mutating in place). Returns
|
|
45
|
+
// the coercion that witnesses the edge, or `false` when S is not a subtype of T.
|
|
46
|
+
export const subtype = (sub: AscentType, sup: AscentType): Coercion | false => {
|
|
47
|
+
if (typesEqual(sub, sup)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (sub.kind === 'Int' && sup.kind === 'Float') {
|
|
52
|
+
return 'intToFloat';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (sub.kind === 'List' && sup.kind === 'List') {
|
|
56
|
+
const c = subtype(sub.elem, sup.elem);
|
|
57
|
+
return c === false ? false : { elem: c };
|
|
43
58
|
}
|
|
44
59
|
|
|
45
|
-
|
|
46
|
-
|
|
60
|
+
return false;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// The least common supertype — derived from subtyping. When one side
|
|
64
|
+
// subtypes the other, that supertype is the join. Otherwise, for two lists
|
|
65
|
+
// whose elements aren't directly related by subtyping, recurse on the
|
|
66
|
+
// elements (structural join; doesn't add any widening knowledge of its own).
|
|
67
|
+
// Returns null when the two types have no common supertype.
|
|
68
|
+
export const leastCommonType = (a: AscentType, b: AscentType): AscentType | null => {
|
|
69
|
+
if (subtype(a, b) !== false) {
|
|
70
|
+
return b;
|
|
47
71
|
}
|
|
48
72
|
|
|
49
|
-
if (a
|
|
50
|
-
return
|
|
73
|
+
if (subtype(b, a) !== false) {
|
|
74
|
+
return a;
|
|
51
75
|
}
|
|
52
76
|
|
|
53
77
|
if (a.kind === 'List' && b.kind === 'List') {
|
|
@@ -57,9 +81,5 @@ export const leastCommonType = (a: AscentType, b: AscentType): AscentType | null
|
|
|
57
81
|
return null;
|
|
58
82
|
};
|
|
59
83
|
|
|
60
|
-
// `from` is assignable to `to` when
|
|
61
|
-
|
|
62
|
-
export const isAssignableTo = (from: AscentType, to: AscentType): boolean => {
|
|
63
|
-
const lct = leastCommonType(from, to);
|
|
64
|
-
return lct !== null && typesEqual(lct, to);
|
|
65
|
-
};
|
|
84
|
+
// `from` is assignable to `to` exactly when it's a subtype of `to`.
|
|
85
|
+
export const isAssignableTo = (from: AscentType, to: AscentType): boolean => subtype(from, to) !== false;
|