@ascent-lang/dev 0.1.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 (111) hide show
  1. package/README.md +68 -0
  2. package/dist/errors/index.d.ts +4 -0
  3. package/dist/errors/index.d.ts.map +1 -0
  4. package/dist/errors/index.js +35 -0
  5. package/dist/errors/index.js.map +1 -0
  6. package/dist/errors/types.d.ts +9 -0
  7. package/dist/errors/types.d.ts.map +1 -0
  8. package/dist/errors/types.js +5 -0
  9. package/dist/errors/types.js.map +1 -0
  10. package/dist/index.d.ts +3 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +200 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/interpreter.d.ts +35 -0
  15. package/dist/interpreter.d.ts.map +1 -0
  16. package/dist/interpreter.js +305 -0
  17. package/dist/interpreter.js.map +1 -0
  18. package/dist/lexer/chars.d.ts +6 -0
  19. package/dist/lexer/chars.d.ts.map +1 -0
  20. package/dist/lexer/chars.js +6 -0
  21. package/dist/lexer/chars.js.map +1 -0
  22. package/dist/lexer/cursor.d.ts +16 -0
  23. package/dist/lexer/cursor.d.ts.map +1 -0
  24. package/dist/lexer/cursor.js +43 -0
  25. package/dist/lexer/cursor.js.map +1 -0
  26. package/dist/lexer/index.d.ts +21 -0
  27. package/dist/lexer/index.d.ts.map +1 -0
  28. package/dist/lexer/index.js +163 -0
  29. package/dist/lexer/index.js.map +1 -0
  30. package/dist/lexer/keywords.d.ts +5 -0
  31. package/dist/lexer/keywords.d.ts.map +1 -0
  32. package/dist/lexer/keywords.js +39 -0
  33. package/dist/lexer/keywords.js.map +1 -0
  34. package/dist/lexer/token.d.ts +20 -0
  35. package/dist/lexer/token.d.ts.map +1 -0
  36. package/dist/lexer/token.js +2 -0
  37. package/dist/lexer/token.js.map +1 -0
  38. package/dist/lib.d.ts +14 -0
  39. package/dist/lib.d.ts.map +1 -0
  40. package/dist/lib.js +17 -0
  41. package/dist/lib.js.map +1 -0
  42. package/dist/parser/ast.d.ts +128 -0
  43. package/dist/parser/ast.d.ts.map +1 -0
  44. package/dist/parser/ast.js +2 -0
  45. package/dist/parser/ast.js.map +1 -0
  46. package/dist/parser/expr.d.ts +4 -0
  47. package/dist/parser/expr.d.ts.map +1 -0
  48. package/dist/parser/expr.js +277 -0
  49. package/dist/parser/expr.js.map +1 -0
  50. package/dist/parser/index.d.ts +12 -0
  51. package/dist/parser/index.d.ts.map +1 -0
  52. package/dist/parser/index.js +40 -0
  53. package/dist/parser/index.js.map +1 -0
  54. package/dist/parser/printer.d.ts +7 -0
  55. package/dist/parser/printer.d.ts.map +1 -0
  56. package/dist/parser/printer.js +130 -0
  57. package/dist/parser/printer.js.map +1 -0
  58. package/dist/parser/stmt.d.ts +7 -0
  59. package/dist/parser/stmt.d.ts.map +1 -0
  60. package/dist/parser/stmt.js +153 -0
  61. package/dist/parser/stmt.js.map +1 -0
  62. package/dist/parser/token-stream.d.ts +19 -0
  63. package/dist/parser/token-stream.d.ts.map +1 -0
  64. package/dist/parser/token-stream.js +111 -0
  65. package/dist/parser/token-stream.js.map +1 -0
  66. package/dist/parser/type-expr.d.ts +5 -0
  67. package/dist/parser/type-expr.d.ts.map +1 -0
  68. package/dist/parser/type-expr.js +54 -0
  69. package/dist/parser/type-expr.js.map +1 -0
  70. package/dist/parser/typechecker.d.ts +9 -0
  71. package/dist/parser/typechecker.d.ts.map +1 -0
  72. package/dist/parser/typechecker.js +446 -0
  73. package/dist/parser/typechecker.js.map +1 -0
  74. package/dist/parser/typed-ast.d.ts +130 -0
  75. package/dist/parser/typed-ast.d.ts.map +1 -0
  76. package/dist/parser/typed-ast.js +2 -0
  77. package/dist/parser/typed-ast.js.map +1 -0
  78. package/dist/parser/typed-printer.d.ts +3 -0
  79. package/dist/parser/typed-printer.d.ts.map +1 -0
  80. package/dist/parser/typed-printer.js +91 -0
  81. package/dist/parser/typed-printer.js.map +1 -0
  82. package/dist/types/types.d.ts +28 -0
  83. package/dist/types/types.d.ts.map +1 -0
  84. package/dist/types/types.js +48 -0
  85. package/dist/types/types.js.map +1 -0
  86. package/package.json +70 -0
  87. package/src/errors/index.ts +38 -0
  88. package/src/errors/lexical.yml +16 -0
  89. package/src/errors/name.yml +11 -0
  90. package/src/errors/syntactic.yml +66 -0
  91. package/src/errors/typechecker.yml +61 -0
  92. package/src/errors/types.ts +13 -0
  93. package/src/index.ts +213 -0
  94. package/src/interpreter.ts +332 -0
  95. package/src/lexer/chars.ts +12 -0
  96. package/src/lexer/cursor.ts +51 -0
  97. package/src/lexer/index.ts +174 -0
  98. package/src/lexer/keywords.ts +43 -0
  99. package/src/lexer/token.ts +62 -0
  100. package/src/lib.ts +33 -0
  101. package/src/parser/ast.ts +64 -0
  102. package/src/parser/expr.ts +313 -0
  103. package/src/parser/index.ts +50 -0
  104. package/src/parser/printer.ts +146 -0
  105. package/src/parser/stmt.ts +176 -0
  106. package/src/parser/token-stream.ts +121 -0
  107. package/src/parser/type-expr.ts +63 -0
  108. package/src/parser/typechecker.ts +406 -0
  109. package/src/parser/typed-ast.ts +63 -0
  110. package/src/parser/typed-printer.ts +105 -0
  111. package/src/types/types.ts +65 -0
@@ -0,0 +1,406 @@
1
+ import type { Expr, Statement, Program, Block, If, TypeExpr } from './ast.js';
2
+ import type { Marker, Span } from '../lexer/token.js';
3
+ import type { TypedExpr, TypedBlock, TypedIf, TypedStatement, TypedProgram } from './typed-ast.js';
4
+ import {
5
+ AscentType, INT_TYPE, FLOAT_TYPE, BOOL_TYPE, STRING_TYPE, NONE_TYPE, DONE_TYPE, listOfType,
6
+ leastCommonType, isAssignableTo,
7
+ } from '../types/types.js';
8
+
9
+ export interface TypeCheckResult {
10
+ typedProgram: TypedProgram | null;
11
+ errorMarkers: Marker[];
12
+ }
13
+
14
+ // A chain of scopes mirroring Environment in the interpreter.
15
+ class TypeEnv {
16
+ private vars = new Map<string, { ty: AscentType; mutable: boolean }>();
17
+ public constructor(private readonly parent: TypeEnv | null = null) { }
18
+
19
+ public get(name: string): { ty: AscentType; mutable: boolean } | null {
20
+ return this.vars.get(name) ?? this.parent?.get(name) ?? null;
21
+ }
22
+
23
+ public set(name: string, ty: AscentType, mutable: boolean): void {
24
+ this.vars.set(name, { ty, mutable });
25
+ }
26
+
27
+ public child(): TypeEnv {
28
+ return new TypeEnv(this);
29
+ }
30
+ }
31
+
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));
44
+ }
45
+ };
46
+
47
+ // ---- Method type signatures ------------------------------------------
48
+
49
+ const requireArity = (expected: number, got: number, markers: Marker[], span: Span): boolean => {
50
+ if (got !== expected) { markers.push({ code: 'T0007', span }); return false; }
51
+ return true;
52
+ };
53
+
54
+ const intMethodType = (method: string, argTypes: AscentType[], markers: Marker[], span: Span): AscentType | null => {
55
+ switch (method) {
56
+ case 'toStr': return requireArity(0, argTypes.length, markers, span) ? STRING_TYPE : null;
57
+ case 'toFloat': return requireArity(0, argTypes.length, markers, span) ? FLOAT_TYPE : null;
58
+ case 'abs': return requireArity(0, argTypes.length, markers, span) ? INT_TYPE : null;
59
+ default: markers.push({ code: 'T0006', span }); return null;
60
+ }
61
+ };
62
+
63
+ const floatMethodType = (method: string, argTypes: AscentType[], markers: Marker[], span: Span): AscentType | null => {
64
+ switch (method) {
65
+ case 'toStr': return requireArity(0, argTypes.length, markers, span) ? STRING_TYPE : null;
66
+ case 'toInt': return requireArity(0, argTypes.length, markers, span) ? INT_TYPE : null;
67
+ case 'abs': return requireArity(0, argTypes.length, markers, span) ? FLOAT_TYPE : null;
68
+ default: markers.push({ code: 'T0006', span }); return null;
69
+ }
70
+ };
71
+
72
+ const listMethodType = (
73
+ elemType: AscentType, method: string, argTypes: AscentType[], markers: Marker[], span: Span,
74
+ ): AscentType | null => {
75
+ switch (method) {
76
+ case 'length': return requireArity(0, argTypes.length, markers, span) ? INT_TYPE : null;
77
+ case 'isEmpty': return requireArity(0, argTypes.length, markers, span) ? BOOL_TYPE : null;
78
+ case 'reverse': return requireArity(0, argTypes.length, markers, span) ? listOfType(elemType) : null;
79
+ case 'append':
80
+ case 'prepend': {
81
+ if (!requireArity(1, argTypes.length, markers, span)) return null;
82
+ const ct = leastCommonType(elemType, argTypes[0]!);
83
+ if (ct === null) { markers.push({ code: 'T0008', span }); return null; }
84
+ return listOfType(ct);
85
+ }
86
+ case 'concat': {
87
+ if (!requireArity(1, argTypes.length, markers, span)) return null;
88
+ const arg = argTypes[0]!;
89
+ if (arg.kind !== 'List') { markers.push({ code: 'T0008', span }); return null; }
90
+ const ct = leastCommonType(elemType, arg.elem);
91
+ if (ct === null) { markers.push({ code: 'T0008', span }); return null; }
92
+ return listOfType(ct);
93
+ }
94
+ default: markers.push({ code: 'T0006', span }); return null;
95
+ }
96
+ };
97
+
98
+ // ---- Expression inference -------------------------------------------
99
+ //
100
+ // Returns a TypedExpr with the inferred type embedded, or null when
101
+ // inference fails (error already recorded in markers). Callers that
102
+ // get null should still continue checking siblings to surface more errors.
103
+
104
+ const inferExpr = (
105
+ expr: Expr, env: TypeEnv, markers: Marker[], contextType: AscentType | null = null,
106
+ ): TypedExpr | null => {
107
+ switch (expr.kind) {
108
+ case 'literal': {
109
+ switch (expr.valueType) {
110
+ case 'Int': return { ...expr, type: INT_TYPE };
111
+ case 'Float': return { ...expr, type: FLOAT_TYPE };
112
+ case 'Bool': return { ...expr, type: BOOL_TYPE };
113
+ case 'String': return { ...expr, type: STRING_TYPE };
114
+ case 'None': return { ...expr, type: NONE_TYPE };
115
+ case 'Done': return { ...expr, type: DONE_TYPE };
116
+ }
117
+ }
118
+
119
+ case 'slot': {
120
+ const binding = env.get(expr.name);
121
+ if (binding === null) { markers.push({ code: 'N0001', span: expr.span }); return null; }
122
+ return { ...expr, type: binding.ty };
123
+ }
124
+
125
+ case 'call': {
126
+ // floor is the only built-in for now.
127
+ if (expr.callee !== 'floor') { markers.push({ code: 'T0006', span: expr.span }); return null; }
128
+ if (!requireArity(1, expr.args.length, markers, expr.span)) return null;
129
+ const typedArg = inferExpr(expr.args[0]!, env, markers);
130
+ if (typedArg === null) return null;
131
+ if (typedArg.type.kind !== 'Float') { markers.push({ code: 'T0008', span: expr.span }); return null; }
132
+ return { kind: 'call', callee: expr.callee, args: [typedArg], type: FLOAT_TYPE, span: expr.span };
133
+ }
134
+
135
+ case 'unary': {
136
+ const typedOperand = inferExpr(expr.operand, env, markers);
137
+ if (typedOperand === null) return null;
138
+ if (typedOperand.type.kind !== 'Int' && typedOperand.type.kind !== 'Float') {
139
+ markers.push({ code: 'T0009', span: expr.span }); return null;
140
+ }
141
+ return { kind: 'unary', op: expr.op, operand: typedOperand, type: typedOperand.type, span: expr.span };
142
+ }
143
+
144
+ case 'binary': {
145
+ const typedLeft = inferExpr(expr.left, env, markers);
146
+ const typedRight = inferExpr(expr.right, env, markers);
147
+ if (typedLeft === null || typedRight === null) return null;
148
+ const lt = typedLeft.type;
149
+ const rt = typedRight.type;
150
+
151
+ let type: AscentType;
152
+ switch (expr.op) {
153
+ case '+': case '-': case '*': {
154
+ if ((lt.kind !== 'Int' && lt.kind !== 'Float') || (rt.kind !== 'Int' && rt.kind !== 'Float')) {
155
+ markers.push({ code: 'T0009', span: expr.span }); return null;
156
+ }
157
+ type = (lt.kind === 'Float' || rt.kind === 'Float') ? FLOAT_TYPE : INT_TYPE;
158
+ break;
159
+ }
160
+ case '/': {
161
+ if ((lt.kind !== 'Int' && lt.kind !== 'Float') || (rt.kind !== 'Int' && rt.kind !== 'Float')) {
162
+ markers.push({ code: 'T0009', span: expr.span }); return null;
163
+ }
164
+ type = FLOAT_TYPE;
165
+ break;
166
+ }
167
+ case 'div': case 'mod': {
168
+ if (lt.kind !== 'Int' || rt.kind !== 'Int') {
169
+ markers.push({ code: 'T0009', span: expr.span }); return null;
170
+ }
171
+ type = INT_TYPE;
172
+ break;
173
+ }
174
+ case '==': case '!=': {
175
+ if (leastCommonType(lt, rt) === null) {
176
+ markers.push({ code: 'T0009', span: expr.span }); return null;
177
+ }
178
+ type = BOOL_TYPE;
179
+ break;
180
+ }
181
+ case '<': case '<=': case '>': case '>=': {
182
+ if ((lt.kind !== 'Int' && lt.kind !== 'Float') || (rt.kind !== 'Int' && rt.kind !== 'Float')) {
183
+ markers.push({ code: 'T0009', span: expr.span }); return null;
184
+ }
185
+ type = BOOL_TYPE;
186
+ break;
187
+ }
188
+ }
189
+ return { kind: 'binary', op: expr.op, left: typedLeft, right: typedRight, type, span: expr.span };
190
+ }
191
+
192
+ case 'list': {
193
+ if (expr.elements.length === 0) {
194
+ if (contextType !== null && contextType.kind === 'List') {
195
+ return { kind: 'list', elements: [], type: contextType, span: expr.span };
196
+ }
197
+ markers.push({ code: 'T0003', span: expr.span });
198
+ return null;
199
+ }
200
+
201
+ const typedElements: TypedExpr[] = [];
202
+ let failed = false;
203
+ for (const el of expr.elements) {
204
+ const te = inferExpr(el, env, markers);
205
+ if (te === null) { failed = true; } else { typedElements.push(te); }
206
+ }
207
+ if (failed) return null;
208
+
209
+ let elemType: AscentType = typedElements[0]!.type;
210
+ for (const te of typedElements.slice(1)) {
211
+ const ct = leastCommonType(elemType, te.type);
212
+ if (ct === null) { markers.push({ code: 'T0002', span: expr.span }); return null; }
213
+ elemType = ct;
214
+ }
215
+ // If the surrounding context expects a List with a wider element type
216
+ // (e.g. List<Float> when elements are all Int), widen to match so that
217
+ // the interpreter can coerce elements using expr.type.elem.
218
+ if (contextType !== null && contextType.kind === 'List') {
219
+ const ct = leastCommonType(elemType, contextType.elem);
220
+ if (ct !== null) elemType = ct;
221
+ }
222
+ return { kind: 'list', elements: typedElements, type: listOfType(elemType), span: expr.span };
223
+ }
224
+
225
+ case 'index': {
226
+ const typedList = inferExpr(expr.list, env, markers);
227
+ const typedIndex = inferExpr(expr.index, env, markers);
228
+ 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; }
231
+ return { kind: 'index', list: typedList, index: typedIndex, type: typedList.type.elem, span: expr.span };
232
+ }
233
+
234
+ case 'methodCall': {
235
+ const typedReceiver = inferExpr(expr.receiver, env, markers);
236
+ if (typedReceiver === null) return null;
237
+
238
+ const typedArgs: TypedExpr[] = [];
239
+ let failed = false;
240
+ for (const arg of expr.args) {
241
+ const ta = inferExpr(arg, env, markers);
242
+ if (ta === null) { failed = true; } else { typedArgs.push(ta); }
243
+ }
244
+ if (failed) return null;
245
+
246
+ const argTypes = typedArgs.map(a => a.type);
247
+ let resultType: AscentType | null;
248
+ switch (typedReceiver.type.kind) {
249
+ case 'Int': resultType = intMethodType(expr.method, argTypes, markers, expr.span); break;
250
+ case 'Float': resultType = floatMethodType(expr.method, argTypes, markers, expr.span); break;
251
+ 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;
253
+ }
254
+ if (resultType === null) return null;
255
+ return {
256
+ kind: 'methodCall', receiver: typedReceiver, method: expr.method,
257
+ args: typedArgs, type: resultType, span: expr.span,
258
+ };
259
+ }
260
+
261
+ case 'block':
262
+ return inferBlock(expr, env, markers);
263
+
264
+ case 'if':
265
+ return inferIf(expr, env, markers);
266
+ }
267
+ };
268
+
269
+ const inferBlock = (block: Block, env: TypeEnv, markers: Marker[]): TypedBlock | null => {
270
+ const inner = env.child();
271
+ const typedStmts: TypedStatement[] = [];
272
+ let failed = false;
273
+ let blockType: AscentType = DONE_TYPE;
274
+
275
+ for (const stmt of block.stmts) {
276
+ const typedStmt = inferStmt(stmt, inner, markers);
277
+ if (typedStmt === null) {
278
+ failed = true;
279
+ } else {
280
+ typedStmts.push(typedStmt);
281
+ blockType = typedStmt.kind === 'expr' ? typedStmt.expr.type : DONE_TYPE;
282
+ }
283
+ }
284
+
285
+ if (failed) return null;
286
+ return { kind: 'block', stmts: typedStmts, type: blockType, span: block.span };
287
+ };
288
+
289
+ const inferIf = (expr: If, env: TypeEnv, markers: Marker[]): TypedIf | null => {
290
+ const typedCond = inferExpr(expr.cond, env, markers);
291
+ if (typedCond !== null && typedCond.type.kind !== 'Bool') {
292
+ markers.push({ code: 'T0004', span: expr.cond.span });
293
+ }
294
+
295
+ const typedThen = inferBlock(expr.then, env, markers);
296
+
297
+ if (expr.else === null) {
298
+ if (typedCond === null || typedThen === null) return null;
299
+ return { kind: 'if', cond: typedCond, then: typedThen, else: null, type: DONE_TYPE, span: expr.span };
300
+ }
301
+
302
+ const typedElse: TypedBlock | TypedIf | null = expr.else.kind === 'if'
303
+ ? inferIf(expr.else, env, markers)
304
+ : inferBlock(expr.else, env, markers);
305
+
306
+ if (typedCond === null || typedThen === null || typedElse === null) return null;
307
+
308
+ const ct = leastCommonType(typedThen.type, typedElse.type);
309
+ if (ct === null) { markers.push({ code: 'T0005', span: expr.span }); return null; }
310
+
311
+ return { kind: 'if', cond: typedCond, then: typedThen, else: typedElse, type: ct, span: expr.span };
312
+ };
313
+
314
+ const inferStmt = (stmt: Statement, env: TypeEnv, markers: Marker[]): TypedStatement | null => {
315
+ switch (stmt.kind) {
316
+ case 'fix':
317
+ case 'mut': {
318
+ const annotation = stmt.typeAnnotation !== null ? resolveTypeExpr(stmt.typeAnnotation) : null;
319
+ const typedInit = inferExpr(stmt.init, env, markers, annotation);
320
+
321
+ let slotType: AscentType | null;
322
+ if (annotation !== null) {
323
+ if (typedInit !== null && !isAssignableTo(typedInit.type, annotation)) {
324
+ markers.push({ code: 'T0001', span: stmt.span });
325
+ }
326
+ slotType = annotation;
327
+ } else {
328
+ slotType = typedInit?.type ?? null;
329
+ }
330
+
331
+ if (slotType !== null) env.set(stmt.name, slotType, stmt.kind === 'mut');
332
+ if (typedInit === null) return null;
333
+
334
+ return {
335
+ kind: stmt.kind,
336
+ name: stmt.name,
337
+ typeAnnotation: stmt.typeAnnotation,
338
+ slotType: slotType ?? DONE_TYPE,
339
+ init: typedInit,
340
+ span: stmt.span,
341
+ };
342
+ }
343
+
344
+ case 'assign': {
345
+ const binding = env.get(stmt.name);
346
+ if (binding === null) {
347
+ markers.push({ code: 'N0001', span: stmt.span });
348
+ } else if (!binding.mutable) {
349
+ markers.push({ code: 'N0002', span: stmt.span });
350
+ }
351
+ const typedValue = inferExpr(stmt.value, env, markers);
352
+ if (binding !== null && typedValue !== null && !isAssignableTo(typedValue.type, binding.ty)) {
353
+ markers.push({ code: 'T0001', span: stmt.span });
354
+ }
355
+ if (typedValue === null) return null;
356
+ return {
357
+ kind: 'assign',
358
+ name: stmt.name,
359
+ slotType: binding?.ty ?? DONE_TYPE,
360
+ value: typedValue,
361
+ span: stmt.span,
362
+ };
363
+ }
364
+
365
+ case 'while': {
366
+ const typedCond = inferExpr(stmt.cond, env, markers);
367
+ if (typedCond !== null && typedCond.type.kind !== 'Bool') {
368
+ markers.push({ code: 'T0004', span: stmt.cond.span });
369
+ }
370
+ const typedBody = inferBlock(stmt.body, env, markers);
371
+ if (typedCond === null || typedBody === null) return null;
372
+ return { kind: 'while', cond: typedCond, body: typedBody, span: stmt.span };
373
+ }
374
+
375
+ case 'expr': {
376
+ const typedExpr = inferExpr(stmt.expr, env, markers);
377
+ if (typedExpr === null) return null;
378
+ return { kind: 'expr', expr: typedExpr, span: stmt.span };
379
+ }
380
+ }
381
+ };
382
+
383
+ export const typecheck = (program: Program): TypeCheckResult => {
384
+ const markers: Marker[] = [];
385
+ const env = new TypeEnv();
386
+
387
+ 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);
393
+ }
394
+
395
+ const typedStmts: TypedStatement[] = [];
396
+ let failed = false;
397
+ for (const stmt of program.stmts) {
398
+ const typedStmt = inferStmt(stmt, env, markers);
399
+ if (typedStmt === null) { failed = true; } else { typedStmts.push(typedStmt); }
400
+ }
401
+
402
+ if (failed || markers.length > 0) {
403
+ return { typedProgram: null, errorMarkers: markers };
404
+ }
405
+ return { typedProgram: { args: program.args, stmts: typedStmts }, errorMarkers: [] };
406
+ };
@@ -0,0 +1,63 @@
1
+ import type { Span } from '../lexer/token.js';
2
+ import type { UnaryOp, BinaryOp, ArgDef, TypeExpr } from './ast.js';
3
+ import type { AscentType } from '../types/types.js';
4
+
5
+ // Every typed expression carries `type`: the Type inferred by the type
6
+ // checker. On literal nodes, `valueType` remains the literal kind
7
+ // discriminator (Int/Float/Bool/String/None/Done), exactly as in the
8
+ // untyped AST, to avoid a field-name collision with `type`.
9
+ export type TypedLiteral = (
10
+ | { kind: 'literal'; valueType: 'Int'; value: bigint; type: AscentType; span: Span }
11
+ | { kind: 'literal'; valueType: 'Float'; value: number; type: AscentType; span: Span }
12
+ | { kind: 'literal'; valueType: 'Bool'; value: boolean; type: AscentType; span: Span }
13
+ | { kind: 'literal'; valueType: 'String'; value: string; type: AscentType; span: Span }
14
+ | { kind: 'literal'; valueType: 'None'; type: AscentType; span: Span }
15
+ | { kind: 'literal'; valueType: 'Done'; type: AscentType; span: Span }
16
+ );
17
+
18
+ export type TypedExpr = (
19
+ | TypedLiteral
20
+ | { kind: 'slot'; name: string; type: AscentType; span: Span }
21
+ | { kind: 'call'; callee: string; args: TypedExpr[]; type: AscentType; span: Span }
22
+ | { kind: 'methodCall'; receiver: TypedExpr; method: string; args: TypedExpr[]; type: AscentType; span: Span }
23
+ | { kind: 'list'; elements: TypedExpr[]; type: AscentType; span: Span }
24
+ | { kind: 'index'; list: TypedExpr; index: TypedExpr; type: AscentType; span: Span }
25
+ | { kind: 'unary'; op: UnaryOp; operand: TypedExpr; type: AscentType; span: Span }
26
+ | { kind: 'binary'; op: BinaryOp; left: TypedExpr; right: TypedExpr; type: AscentType; span: Span }
27
+ | TypedBlock
28
+ | TypedIf
29
+ );
30
+
31
+ // type is the type the block yields: the type of the last expr-statement,
32
+ // or Done when the block is empty or ends with a non-expr statement.
33
+ export type TypedBlock = {
34
+ kind: 'block';
35
+ stmts: TypedStatement[];
36
+ type: AscentType;
37
+ span: Span;
38
+ };
39
+
40
+ export type TypedIf = {
41
+ kind: 'if';
42
+ cond: TypedExpr;
43
+ then: TypedBlock;
44
+ else: TypedBlock | TypedIf | null;
45
+ type: AscentType;
46
+ span: Span;
47
+ };
48
+
49
+ // slotType is the definitive declared type of the slot — the annotation type
50
+ // when provided, otherwise the inferred init type. The interpreter uses it to
51
+ // coerce the init value (e.g. Int → Float when the annotation says Float).
52
+ export type TypedStatement = (
53
+ | { kind: 'fix'; name: string; typeAnnotation: TypeExpr | null; slotType: AscentType; init: TypedExpr; span: Span }
54
+ | { kind: 'mut'; name: string; typeAnnotation: TypeExpr | null; slotType: AscentType; init: TypedExpr; span: Span }
55
+ | { kind: 'assign'; name: string; slotType: AscentType; value: TypedExpr; span: Span }
56
+ | { kind: 'expr'; expr: TypedExpr; span: Span }
57
+ | { kind: 'while'; cond: TypedExpr; body: TypedBlock; span: Span }
58
+ );
59
+
60
+ export type TypedProgram = {
61
+ args: ArgDef[];
62
+ stmts: TypedStatement[];
63
+ };
@@ -0,0 +1,105 @@
1
+ import chalk from 'chalk';
2
+ import type { TypedExpr, TypedStatement } from './typed-ast.js';
3
+ import { typeToString } from '../types/types.js';
4
+ import { branch } from './printer.js';
5
+
6
+ // Appends the inferred type as a dim annotation on a node label.
7
+ const ty = (t: ReturnType<typeof typeToString>): string => chalk.dim(`: ${t}`);
8
+
9
+ const typedExprLines = (expr: TypedExpr): string[] => {
10
+ const t = ty(typeToString(expr.type));
11
+
12
+ switch (expr.kind) {
13
+ case 'literal':
14
+ switch (expr.valueType) {
15
+ case 'Int': return [`${chalk.cyan('Lit')} ${chalk.yellow(String(expr.value))}${t}`];
16
+ case 'Float': return [`${chalk.cyan('Lit')} ${chalk.yellow(String(expr.value))}${t}`];
17
+ case 'Bool': return [`${chalk.cyan('Lit')} ${chalk.yellow(expr.value ? 'True' : 'False')}${t}`];
18
+ case 'String': return [`${chalk.cyan('Lit')} ${chalk.green(JSON.stringify(expr.value))}${t}`];
19
+ case 'None': return [`${chalk.cyan('Lit')} ${chalk.yellow('None')}${t}`];
20
+ case 'Done': return [`${chalk.cyan('Lit')} ${chalk.yellow('Done')}${t}`];
21
+ }
22
+ case 'slot':
23
+ return [`${chalk.cyan('Slot')} ${chalk.green(expr.name)}${t}`];
24
+ case 'call': {
25
+ const argLines = expr.args.flatMap((arg, i) =>
26
+ branch(typedExprLines(arg), i === expr.args.length - 1)
27
+ );
28
+ return [`${chalk.cyan('Call')} ${chalk.green(expr.callee)}${t}`, ...argLines];
29
+ }
30
+ case 'methodCall': {
31
+ const children = [expr.receiver, ...expr.args];
32
+ const childLines = children.flatMap((child, i) =>
33
+ branch(typedExprLines(child), i === children.length - 1)
34
+ );
35
+ return [`${chalk.cyan('MethodCall')} ${chalk.green('.' + expr.method)}${t}`, ...childLines];
36
+ }
37
+ case 'list': {
38
+ if (expr.elements.length === 0) {
39
+ return [`${chalk.cyan('List')} ${chalk.dim('[]')}${t}`];
40
+ }
41
+ const elementLines = expr.elements.flatMap((el, i) =>
42
+ branch(typedExprLines(el), i === expr.elements.length - 1)
43
+ );
44
+ return [`${chalk.cyan('List')}${t}`, ...elementLines];
45
+ }
46
+ case 'index': {
47
+ const listLines = branch(typedExprLines(expr.list), false);
48
+ const indexLines = branch(typedExprLines(expr.index), true);
49
+ return [`${chalk.cyan('Index')}${t}`, ...listLines, ...indexLines];
50
+ }
51
+ case 'unary': {
52
+ const operandLines = branch(typedExprLines(expr.operand), true);
53
+ return [`${chalk.cyan('Unary')} ${chalk.magenta(expr.op)}${t}`, ...operandLines];
54
+ }
55
+ case 'binary': {
56
+ const leftLines = branch(typedExprLines(expr.left), false);
57
+ const rightLines = branch(typedExprLines(expr.right), true);
58
+ return [`${chalk.cyan('Binary')} ${chalk.magenta(expr.op)}${t}`, ...leftLines, ...rightLines];
59
+ }
60
+ case 'block': {
61
+ if (expr.stmts.length === 0) {
62
+ return [`${chalk.cyan('Block')} ${chalk.dim('(empty)')}${t}`];
63
+ }
64
+ const stmtLines = expr.stmts.flatMap((stmt, i) =>
65
+ branch(typedStmtLines(stmt), i === expr.stmts.length - 1)
66
+ );
67
+ return [`${chalk.cyan('Block')}${t}`, ...stmtLines];
68
+ }
69
+ case 'if': {
70
+ const condLines = branch(typedExprLines(expr.cond), false);
71
+ const thenLines = branch(typedExprLines(expr.then), expr.else === null);
72
+ const elseLines = expr.else !== null
73
+ ? branch(typedExprLines(expr.else), true)
74
+ : [];
75
+ return [`${chalk.cyan('If')}${t}`, ...condLines, ...thenLines, ...elseLines];
76
+ }
77
+ }
78
+ };
79
+
80
+ const typedStmtLines = (stmt: TypedStatement): string[] => {
81
+ switch (stmt.kind) {
82
+ case 'fix':
83
+ case 'mut': {
84
+ const label = stmt.kind === 'fix' ? 'Fix' : 'Mut';
85
+ const slotTy = ty(typeToString(stmt.slotType));
86
+ const initLines = branch(typedExprLines(stmt.init), true);
87
+ return [`${chalk.cyan(label)} ${chalk.green(stmt.name)}${slotTy}`, ...initLines];
88
+ }
89
+ case 'assign': {
90
+ const slotTy = ty(typeToString(stmt.slotType));
91
+ const valueLines = branch(typedExprLines(stmt.value), true);
92
+ return [`${chalk.cyan('Assign')} ${chalk.green(stmt.name)}${slotTy}`, ...valueLines];
93
+ }
94
+ case 'expr':
95
+ return typedExprLines(stmt.expr);
96
+ case 'while': {
97
+ const condLines = branch(typedExprLines(stmt.cond), false);
98
+ const bodyLines = branch(typedExprLines(stmt.body), true);
99
+ return [`${chalk.cyan('While')}`, ...condLines, ...bodyLines];
100
+ }
101
+ }
102
+ };
103
+
104
+ export const formatTypedStmt = (stmt: TypedStatement): string =>
105
+ typedStmtLines(stmt).join('\n');
@@ -0,0 +1,65 @@
1
+ export type AscentType =
2
+ | { kind: 'Int' }
3
+ | { kind: 'Float' }
4
+ | { kind: 'Bool' }
5
+ | { kind: 'String' }
6
+ | { kind: 'None' }
7
+ | { kind: 'Done' }
8
+ | { kind: 'List'; elem: AscentType };
9
+
10
+ export const INT_TYPE: AscentType = { kind: 'Int' };
11
+ export const FLOAT_TYPE: AscentType = { kind: 'Float' };
12
+ export const BOOL_TYPE: AscentType = { kind: 'Bool' };
13
+ export const STRING_TYPE: AscentType = { kind: 'String' };
14
+ export const NONE_TYPE: AscentType = { kind: 'None' };
15
+ export const DONE_TYPE: AscentType = { kind: 'Done' };
16
+ export const listOfType = (elem: AscentType): AscentType => ({ kind: 'List', elem });
17
+
18
+ export const typeToString = (t: AscentType): string => {
19
+ if (t.kind === 'List') {
20
+ return `List<${typeToString(t.elem)}>`;
21
+ }
22
+ return t.kind;
23
+ };
24
+
25
+ export const typesEqual = (a: AscentType, b: AscentType): boolean => {
26
+ if (a.kind !== b.kind) {
27
+ return false;
28
+ }
29
+
30
+ if (a.kind === 'List' && b.kind === 'List') {
31
+ return typesEqual(a.elem, b.elem);
32
+ }
33
+
34
+ return true;
35
+ };
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;
43
+ }
44
+
45
+ if (a.kind === 'Int' && b.kind === 'Float') {
46
+ return FLOAT_TYPE;
47
+ }
48
+
49
+ if (a.kind === 'Float' && b.kind === 'Int') {
50
+ return FLOAT_TYPE;
51
+ }
52
+
53
+ if (a.kind === 'List' && b.kind === 'List') {
54
+ const elem = leastCommonType(a.elem, b.elem);
55
+ return elem !== null ? listOfType(elem) : null;
56
+ }
57
+ return null;
58
+ };
59
+
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
+ };