From dac8c9d9469d850e99e6939ed80ebf5083c0a50d Mon Sep 17 00:00:00 2001 From: Sam Vervaeck Date: Fri, 9 Sep 2022 20:02:35 +0200 Subject: [PATCH] First attempt at making the typing diagnositcs prettier --- src/checker.ts | 139 ++++++++++++++++++++++++++++--------- src/diagnostics.ts | 30 ++++++-- src/test/type-inference.md | 1 + 3 files changed, 134 insertions(+), 36 deletions(-) diff --git a/src/checker.ts b/src/checker.ts index 59745cf10..eba77cf79 100644 --- a/src/checker.ts +++ b/src/checker.ts @@ -35,8 +35,24 @@ abstract class TypeBase { public abstract readonly kind: TypeKind; + public next: Type = this as any; + + public constructor( + public node: Syntax | null = null + ) { + + } + + public static join(a: Type, b: Type): void { + const keep = a.next; + a.next = b; + b.next = keep; + } + public abstract getTypeVars(): Iterable; + public abstract shallowClone(): Type; + public abstract substitute(sub: TVSub): Type; public hasTypeVar(tv: TVar): boolean { @@ -56,6 +72,7 @@ class TVar extends TypeBase { public constructor( public id: number, + public node: Syntax | null = null, ) { super(); } @@ -64,6 +81,10 @@ class TVar extends TypeBase { yield this; } + public shallowClone(): TVar { + return new TVar(this.id, this.node); + } + public substitute(sub: TVSub): Type { const other = sub.get(this); return other === undefined @@ -79,6 +100,7 @@ export class TArrow extends TypeBase { public constructor( public paramTypes: Type[], public returnType: Type, + public node: Syntax | null = null, ) { super(); } @@ -90,6 +112,14 @@ export class TArrow extends TypeBase { yield* this.returnType.getTypeVars(); } + public shallowClone(): TArrow { + return new TArrow( + this.paramTypes, + this.returnType, + this.node, + ) + } + public substitute(sub: TVSub): Type { let changed = false; const newParamTypes = []; @@ -104,12 +134,12 @@ export class TArrow extends TypeBase { if (newReturnType !== this.returnType) { changed = true; } - return changed ? new TArrow(newParamTypes, newReturnType) : this; + return changed ? new TArrow(newParamTypes, newReturnType, this.node) : this; } } -class TCon extends TypeBase { +export class TCon extends TypeBase { public readonly kind = TypeKind.Con; @@ -117,8 +147,9 @@ class TCon extends TypeBase { public id: number, public argTypes: Type[], public displayName: string, + public node: Syntax | null = null, ) { - super(); + super(node); } public *getTypeVars(): Iterable { @@ -127,6 +158,15 @@ class TCon extends TypeBase { } } + public shallowClone(): TCon { + return new TCon( + this.id, + this.argTypes, + this.displayName, + this.node, + ); + } + public substitute(sub: TVSub): Type { let changed = false; const newArgTypes = []; @@ -137,21 +177,7 @@ class TCon extends TypeBase { } newArgTypes.push(newArgType); } - return changed ? new TCon(this.id, newArgTypes, this.displayName) : this; - } - -} - -class TAny extends TypeBase { - - public readonly kind = TypeKind.Any; - - public *getTypeVars(): Iterable { - - } - - public substitute(sub: TVSub): Type { - return this; + return changed ? new TCon(this.id, newArgTypes, this.displayName, this.node) : this; } } @@ -162,8 +188,9 @@ class TTuple extends TypeBase { public constructor( public elementTypes: Type[], + public node: Syntax | null = null, ) { - super(); + super(node); } public *getTypeVars(): Iterable { @@ -172,6 +199,13 @@ class TTuple extends TypeBase { } } + public shallowClone(): TTuple { + return new TTuple( + this.elementTypes, + this.node, + ); + } + public substitute(sub: TVSub): Type { let changed = false; const newElementTypes = []; @@ -182,12 +216,12 @@ class TTuple extends TypeBase { } newElementTypes.push(newElementType); } - return changed ? new TTuple(newElementTypes) : this; + return changed ? new TTuple(newElementTypes, this.node) : this; } } -class TLabeled extends TypeBase { +export class TLabeled extends TypeBase { public readonly kind = TypeKind.Labeled; @@ -197,8 +231,9 @@ class TLabeled extends TypeBase { public constructor( public name: string, public type: Type, + public node: Syntax | null = null, ) { - super(); + super(node); } public find(): TLabeled { @@ -214,14 +249,22 @@ class TLabeled extends TypeBase { return this.type.getTypeVars(); } + public shallowClone(): TLabeled { + return new TLabeled( + this.name, + this.type, + this.node, + ); + } + public substitute(sub: TVSub): Type { const newType = this.type.substitute(sub); - return newType !== this.type ? new TLabeled(this.name, newType) : this; + return newType !== this.type ? new TLabeled(this.name, newType, this.node) : this; } } -class TRecord extends TypeBase { +export class TRecord extends TypeBase { public readonly kind = TypeKind.Record; @@ -230,8 +273,9 @@ class TRecord extends TypeBase { public constructor( public decl: StructDeclaration, public fields: Map, + public node: Syntax | null = null, ) { - super(); + super(node); } public *getTypeVars(): Iterable { @@ -240,6 +284,14 @@ class TRecord extends TypeBase { } } + public shallowClone(): TRecord { + return new TRecord( + this.decl, + this.fields, + this.node + ); + } + public substitute(sub: TVSub): Type { let changed = false; const newFields = new Map(); @@ -250,7 +302,7 @@ class TRecord extends TypeBase { } newFields.set(key, newType); } - return changed ? new TRecord(this.decl, newFields) : this; + return changed ? new TRecord(this.decl, newFields, this.node) : this; } } @@ -684,7 +736,9 @@ export class Checker { this.diagnostics.add(new BindingNotFoudDiagnostic(node.name.name.text, node.name.name)); return new TAny(); } - return this.instantiate(scheme, node); + const type = this.instantiate(scheme, node); + type.node = node; + return type; } case SyntaxKind.MemberExpression: @@ -733,6 +787,8 @@ export class Checker { ty = this.getIntType(); break; } + ty = ty.shallowClone(); + ty.node = node; return ty; } @@ -741,7 +797,7 @@ export class Checker { const scheme = this.lookup(node.name.text); if (scheme === null) { this.diagnostics.add(new BindingNotFoudDiagnostic(node.name.text, node.name)); - return new TAny(); + return this.createTypeVar(); } const type = this.instantiate(scheme, node.name); assert(type.kind === TypeKind.Con); @@ -749,7 +805,7 @@ export class Checker { for (const element of node.elements) { argTypes.push(this.inferExpression(element)); } - return new TCon(type.id, argTypes, type.displayName); + return new TCon(type.id, argTypes, type.displayName, node); } case SyntaxKind.StructExpression: @@ -786,7 +842,7 @@ export class Checker { throw new Error(`Unexpected ${member}`); } } - const type = new TRecord(recordType.decl, fields); + const type = new TRecord(recordType.decl, fields, node); this.addConstraint( new CEqual( recordType, @@ -836,7 +892,9 @@ export class Checker { this.diagnostics.add(new BindingNotFoudDiagnostic(node.name.text, node.name)); return new TAny(); } - return this.instantiate(scheme, node.name); + const type = this.instantiate(scheme, node.name); + type.node = node; + return type; } default: @@ -1338,7 +1396,7 @@ export class Checker { while (queue.length > 0) { - const constraint = queue.pop()!; + const constraint = queue.shift()!; switch (constraint.kind) { @@ -1382,6 +1440,7 @@ export class Checker { return false; } solution.set(left, right); + TypeBase.join(left, right); return true; } @@ -1390,6 +1449,7 @@ export class Checker { } if (left.kind === TypeKind.Any || right.kind === TypeKind.Any) { + TypeBase.join(left, right); return true; } @@ -1408,6 +1468,9 @@ export class Checker { if (!this.unify(left.returnType, right.returnType, solution, constraint)) { success = false; } + if (success) { + TypeBase.join(left, right); + } return success; } @@ -1429,6 +1492,9 @@ export class Checker { success = false; } } + if (success) { + TypeBase.join(left, right); + } return success; } } @@ -1456,6 +1522,9 @@ export class Checker { } } delete right.fields; + if (success) { + TypeBase.join(left, right); + } return success; } @@ -1480,6 +1549,9 @@ export class Checker { for (const fieldName of remaining) { this.diagnostics.add(new FieldDoesNotExistDiagnostic(left, fieldName)); } + if (success) { + TypeBase.join(left, right); + } return success; } @@ -1497,6 +1569,9 @@ export class Checker { this.diagnostics.add(new FieldMissingDiagnostic(left, fieldName)); } } + if (success) { + TypeBase.join(left, right); + } return success; } diff --git a/src/diagnostics.ts b/src/diagnostics.ts index 1a4107d91..7a7875b7d 100644 --- a/src/diagnostics.ts +++ b/src/diagnostics.ts @@ -1,6 +1,6 @@ import { describe } from "yargs"; -import { TypeKind, type Type, type TArrow } from "./checker"; +import { TypeKind, type Type, type TArrow, TRecord } from "./checker"; import { Syntax, SyntaxKind, TextFile, TextPosition, TextRange, Token } from "./cst"; import { countDigits } from "./util"; @@ -215,6 +215,14 @@ export function describeType(type: Type): string { } } +function getFirstNodeInTypeChain(type: Type): Syntax | null { + let curr = type.next; + while (curr !== type && (curr.kind === TypeKind.Var || curr.node === null)) { + curr = curr.next; + } + return curr.node; +} + export class UnificationFailedDiagnostic { public readonly level = Level.Error; @@ -228,6 +236,8 @@ export class UnificationFailedDiagnostic { } public format(): string { + const leftNode = getFirstNodeInTypeChain(this.left); + const rightNode = getFirstNodeInTypeChain(this.right); const node = this.nodes[0]; let out = ANSI_FG_RED + ANSI_BOLD + `error: ` + ANSI_RESET + `unification of ` + ANSI_FG_GREEN + describeType(this.left) + ANSI_RESET @@ -238,6 +248,16 @@ export class UnificationFailedDiagnostic { out += ' ... in an instantiation of the following expression\n\n' out += printNode(node, { indentation: i === 0 ? ' ' : ' ' }) + '\n'; } + if (leftNode !== null) { + out += ANSI_FG_YELLOW + ANSI_BOLD + `info: ` + ANSI_RESET + + `type ` + ANSI_FG_GREEN + describeType(this.left) + ANSI_RESET + ` was inferred from this expression:\n\n` + + printNode(leftNode) + '\n'; + } + if (rightNode !== null) { + out += ANSI_FG_YELLOW + ANSI_BOLD + `info: ` + ANSI_RESET + + `type ` + ANSI_FG_GREEN + describeType(this.right) + ANSI_RESET + ` was inferred from this expression:\n\n` + + printNode(rightNode) + '\n'; + } return out; } @@ -277,9 +297,13 @@ export class FieldMissingDiagnostic { } public format(): string { - return ANSI_FG_RED + ANSI_BOLD + 'error: ' + ANSI_RESET + let out = ANSI_FG_RED + ANSI_BOLD + 'error: ' + ANSI_RESET + `field '${this.fieldName}' is missing from ` + describeType(this.recordType) + '\n\n'; + if (this.recordType.node !== null) { + out += printNode(this.recordType.node) + '\n'; + } + return out } } @@ -289,8 +313,6 @@ export class FieldDoesNotExistDiagnostic { public readonly level = Level.Error; public constructor( - public recordType: TRecord, - public fieldName: string, ) { } diff --git a/src/test/type-inference.md b/src/test/type-inference.md index 606335b0e..37b2e0217 100644 --- a/src/test/type-inference.md +++ b/src/test/type-inference.md @@ -69,3 +69,4 @@ let is_odd x. not (is_even True) ``` +