First attempt at making the typing diagnositcs prettier
This commit is contained in:
parent
f1a365e29c
commit
dac8c9d946
3 changed files with 134 additions and 36 deletions
139
src/checker.ts
139
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<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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
) {
|
||||
|
||||
}
|
||||
|
|
|
@ -69,3 +69,4 @@ let is_odd x.
|
|||
|
||||
not (is_even True)
|
||||
```
|
||||
|
||||
|
|
Loading…
Reference in a new issue