First attempt at making the typing diagnositcs prettier

This commit is contained in:
Sam Vervaeck 2022-09-09 20:02:35 +02:00
parent f1a365e29c
commit dac8c9d946
3 changed files with 134 additions and 36 deletions

View file

@ -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<TVar>;
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<TVar> {
@ -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<TVar> {
}
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<TVar> {
@ -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<string, Type>,
public node: Syntax | null = null,
) {
super();
super(node);
}
public *getTypeVars(): Iterable<TVar> {
@ -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;
}

View file

@ -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,
) {
}

View file

@ -69,3 +69,4 @@ let is_odd x.
not (is_even True)
```