@ascent-lang/dev 0.1.0 → 0.3.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.
Files changed (86) hide show
  1. package/dist/errors/elaborate.d.ts +24 -0
  2. package/dist/errors/elaborate.d.ts.map +1 -0
  3. package/dist/errors/elaborate.js +53 -0
  4. package/dist/errors/elaborate.js.map +1 -0
  5. package/dist/errors/index.d.ts.map +1 -1
  6. package/dist/errors/index.js +356 -30
  7. package/dist/errors/index.js.map +1 -1
  8. package/dist/errors/render.d.ts +3 -0
  9. package/dist/errors/render.d.ts.map +1 -0
  10. package/dist/errors/render.js +43 -0
  11. package/dist/errors/render.js.map +1 -0
  12. package/dist/errors/types.d.ts +29 -0
  13. package/dist/errors/types.d.ts.map +1 -1
  14. package/dist/index.js +21 -28
  15. package/dist/index.js.map +1 -1
  16. package/dist/interpreter.d.ts.map +1 -1
  17. package/dist/interpreter.js +28 -5
  18. package/dist/interpreter.js.map +1 -1
  19. package/dist/lexer/index.d.ts.map +1 -1
  20. package/dist/lexer/index.js +4 -3
  21. package/dist/lexer/index.js.map +1 -1
  22. package/dist/lexer/keywords.d.ts.map +1 -1
  23. package/dist/lexer/keywords.js +3 -0
  24. package/dist/lexer/keywords.js.map +1 -1
  25. package/dist/lexer/token.d.ts +7 -1
  26. package/dist/lexer/token.d.ts.map +1 -1
  27. package/dist/lib.d.ts +3 -3
  28. package/dist/lib.d.ts.map +1 -1
  29. package/dist/lib.js +11 -6
  30. package/dist/lib.js.map +1 -1
  31. package/dist/parser/ast.d.ts +8 -4
  32. package/dist/parser/ast.d.ts.map +1 -1
  33. package/dist/parser/expr.d.ts.map +1 -1
  34. package/dist/parser/expr.js +34 -19
  35. package/dist/parser/expr.js.map +1 -1
  36. package/dist/parser/index.d.ts +3 -5
  37. package/dist/parser/index.d.ts.map +1 -1
  38. package/dist/parser/index.js +26 -33
  39. package/dist/parser/index.js.map +1 -1
  40. package/dist/parser/printer.d.ts +1 -0
  41. package/dist/parser/printer.d.ts.map +1 -1
  42. package/dist/parser/printer.js +21 -0
  43. package/dist/parser/printer.js.map +1 -1
  44. package/dist/parser/stmt.d.ts.map +1 -1
  45. package/dist/parser/stmt.js +5 -3
  46. package/dist/parser/stmt.js.map +1 -1
  47. package/dist/parser/token-stream.d.ts +4 -4
  48. package/dist/parser/token-stream.d.ts.map +1 -1
  49. package/dist/parser/token-stream.js +21 -9
  50. package/dist/parser/token-stream.js.map +1 -1
  51. package/dist/parser/type-expr.d.ts +1 -1
  52. package/dist/parser/type-expr.d.ts.map +1 -1
  53. package/dist/parser/type-expr.js +11 -4
  54. package/dist/parser/type-expr.js.map +1 -1
  55. package/dist/parser/typechecker.d.ts +2 -2
  56. package/dist/parser/typechecker.d.ts.map +1 -1
  57. package/dist/parser/typechecker.js +109 -67
  58. package/dist/parser/typechecker.js.map +1 -1
  59. package/dist/types/types.d.ts +4 -0
  60. package/dist/types/types.d.ts.map +1 -1
  61. package/dist/types/types.js +27 -15
  62. package/dist/types/types.js.map +1 -1
  63. package/package.json +1 -1
  64. package/src/errors/elaborate.ts +88 -0
  65. package/src/errors/index.ts +356 -30
  66. package/src/errors/lexical.yml +48 -13
  67. package/src/errors/name.yml +45 -9
  68. package/src/errors/render.ts +59 -0
  69. package/src/errors/syntactic.yml +128 -49
  70. package/src/errors/typechecker.yml +147 -61
  71. package/src/errors/types.ts +55 -0
  72. package/src/index.ts +20 -30
  73. package/src/interpreter.ts +24 -6
  74. package/src/lexer/index.ts +4 -3
  75. package/src/lexer/keywords.ts +3 -0
  76. package/src/lexer/token.ts +18 -0
  77. package/src/lib.ts +12 -7
  78. package/src/parser/ast.ts +7 -6
  79. package/src/parser/expr.ts +34 -19
  80. package/src/parser/index.ts +32 -32
  81. package/src/parser/printer.ts +22 -0
  82. package/src/parser/stmt.ts +5 -3
  83. package/src/parser/token-stream.ts +20 -8
  84. package/src/parser/type-expr.ts +10 -4
  85. package/src/parser/typechecker.ts +142 -54
  86. package/src/types/types.ts +36 -16
@@ -1,27 +1,37 @@
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
- export interface TypeCheckResult {
9
+ export interface TypedResult {
10
10
  typedProgram: TypedProgram | null;
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, { ty: AscentType; mutable: boolean }>();
26
+ private vars = new Map<string, Binding>();
17
27
  public constructor(private readonly parent: TypeEnv | null = null) { }
18
28
 
19
- public get(name: string): { ty: AscentType; mutable: boolean } | null {
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, mutable: boolean): void {
24
- this.vars.set(name, { ty, mutable });
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
- const resolveTypeExpr = (te: TypeExpr): AscentType => {
33
- switch (te.kind) {
34
- case 'TypeName': {
35
- switch (te.name) {
36
- case 'Int': return INT_TYPE;
37
- case 'Float': return FLOAT_TYPE;
38
- case 'Bool': return BOOL_TYPE;
39
- case 'String': return STRING_TYPE;
40
- }
41
- }
42
- case 'ListType':
43
- return listOfType(resolveTypeExpr(te.elem));
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) { markers.push({ code: 'T0007', span }); return false; }
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) { markers.push({ code: 'T0008', span }); return 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') { markers.push({ code: 'T0008', span }); return null; }
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) { markers.push({ code: 'T0008', span }); return 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: 'T0006', span: expr.span }); return null; }
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') { markers.push({ code: 'T0008', span: expr.span }); return null; }
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.push({ code: 'T0009', span: expr.span }); return null;
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.push({ code: 'T0009', span: expr.span }); return null;
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.push({ code: 'T0009', span: expr.span }); return null;
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.push({ code: 'T0009', span: expr.span }); return null;
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.push({ code: 'T0009', span: expr.span }); return null;
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.push({ code: 'T0009', span: expr.span }); return null;
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) { markers.push({ code: 'T0002', span: expr.span }); return 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') { markers.push({ code: 'T0010', span: expr.span }); return null; }
230
- if (typedIndex.type.kind !== 'Int') { markers.push({ code: 'T0011', span: expr.span }); return null; }
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) { markers.push({ code: 'T0005', span: expr.span }); return 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 ? resolveTypeExpr(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({ code: 'T0001', span: stmt.span });
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 === 'mut');
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
- markers.push({ code: 'N0001', span: stmt.span });
348
- } else if (!binding.mutable) {
349
- markers.push({ code: 'N0002', span: stmt.span });
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
- markers.push({ code: 'T0001', span: stmt.span });
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;
@@ -380,16 +472,12 @@ const inferStmt = (stmt: Statement, env: TypeEnv, markers: Marker[]): TypedState
380
472
  }
381
473
  };
382
474
 
383
- export const typecheck = (program: Program): TypeCheckResult => {
475
+ export const typecheck = (program: Program): TypedResult => {
384
476
  const markers: Marker[] = [];
385
477
  const env = new TypeEnv();
386
478
 
387
479
  for (const arg of program.args) {
388
- const ty: AscentType = arg.type === 'Int' ? INT_TYPE
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[] = [];
@@ -34,20 +34,44 @@ export const typesEqual = (a: AscentType, b: AscentType): boolean => {
34
34
  return true;
35
35
  };
36
36
 
37
- // Int widens to Float everywhere the only implicit numeric promotion.
38
- // List<Int> widens to List<Float> via the same rule applied recursively.
39
- // Returns null when the two types have no common supertype.
40
- export const leastCommonType = (a: AscentType, b: AscentType): AscentType | null => {
41
- if (typesEqual(a, b)) {
42
- return a;
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
- if (a.kind === 'Int' && b.kind === 'Float') {
46
- return FLOAT_TYPE;
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.kind === 'Float' && b.kind === 'Int') {
50
- return FLOAT_TYPE;
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 their LCT is exactly `to`
61
- // (i.e., `from` fits inside `to`, possibly by widening).
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;