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 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 getTypeVars(): Iterable<TVar>;
|
||||||
|
|
||||||
|
public abstract shallowClone(): Type;
|
||||||
|
|
||||||
public abstract substitute(sub: TVSub): Type;
|
public abstract substitute(sub: TVSub): Type;
|
||||||
|
|
||||||
public hasTypeVar(tv: TVar): boolean {
|
public hasTypeVar(tv: TVar): boolean {
|
||||||
|
@ -56,6 +72,7 @@ class TVar extends TypeBase {
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
public id: number,
|
public id: number,
|
||||||
|
public node: Syntax | null = null,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
@ -64,6 +81,10 @@ class TVar extends TypeBase {
|
||||||
yield this;
|
yield this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public shallowClone(): TVar {
|
||||||
|
return new TVar(this.id, this.node);
|
||||||
|
}
|
||||||
|
|
||||||
public substitute(sub: TVSub): Type {
|
public substitute(sub: TVSub): Type {
|
||||||
const other = sub.get(this);
|
const other = sub.get(this);
|
||||||
return other === undefined
|
return other === undefined
|
||||||
|
@ -79,6 +100,7 @@ export class TArrow extends TypeBase {
|
||||||
public constructor(
|
public constructor(
|
||||||
public paramTypes: Type[],
|
public paramTypes: Type[],
|
||||||
public returnType: Type,
|
public returnType: Type,
|
||||||
|
public node: Syntax | null = null,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
@ -90,6 +112,14 @@ export class TArrow extends TypeBase {
|
||||||
yield* this.returnType.getTypeVars();
|
yield* this.returnType.getTypeVars();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public shallowClone(): TArrow {
|
||||||
|
return new TArrow(
|
||||||
|
this.paramTypes,
|
||||||
|
this.returnType,
|
||||||
|
this.node,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
public substitute(sub: TVSub): Type {
|
public substitute(sub: TVSub): Type {
|
||||||
let changed = false;
|
let changed = false;
|
||||||
const newParamTypes = [];
|
const newParamTypes = [];
|
||||||
|
@ -104,12 +134,12 @@ export class TArrow extends TypeBase {
|
||||||
if (newReturnType !== this.returnType) {
|
if (newReturnType !== this.returnType) {
|
||||||
changed = true;
|
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;
|
public readonly kind = TypeKind.Con;
|
||||||
|
|
||||||
|
@ -117,8 +147,9 @@ class TCon extends TypeBase {
|
||||||
public id: number,
|
public id: number,
|
||||||
public argTypes: Type[],
|
public argTypes: Type[],
|
||||||
public displayName: string,
|
public displayName: string,
|
||||||
|
public node: Syntax | null = null,
|
||||||
) {
|
) {
|
||||||
super();
|
super(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
public *getTypeVars(): Iterable<TVar> {
|
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 {
|
public substitute(sub: TVSub): Type {
|
||||||
let changed = false;
|
let changed = false;
|
||||||
const newArgTypes = [];
|
const newArgTypes = [];
|
||||||
|
@ -137,21 +177,7 @@ class TCon extends TypeBase {
|
||||||
}
|
}
|
||||||
newArgTypes.push(newArgType);
|
newArgTypes.push(newArgType);
|
||||||
}
|
}
|
||||||
return changed ? new TCon(this.id, newArgTypes, this.displayName) : this;
|
return changed ? new TCon(this.id, newArgTypes, this.displayName, this.node) : this;
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class TAny extends TypeBase {
|
|
||||||
|
|
||||||
public readonly kind = TypeKind.Any;
|
|
||||||
|
|
||||||
public *getTypeVars(): Iterable<TVar> {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public substitute(sub: TVSub): Type {
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -162,8 +188,9 @@ class TTuple extends TypeBase {
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
public elementTypes: Type[],
|
public elementTypes: Type[],
|
||||||
|
public node: Syntax | null = null,
|
||||||
) {
|
) {
|
||||||
super();
|
super(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
public *getTypeVars(): Iterable<TVar> {
|
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 {
|
public substitute(sub: TVSub): Type {
|
||||||
let changed = false;
|
let changed = false;
|
||||||
const newElementTypes = [];
|
const newElementTypes = [];
|
||||||
|
@ -182,12 +216,12 @@ class TTuple extends TypeBase {
|
||||||
}
|
}
|
||||||
newElementTypes.push(newElementType);
|
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;
|
public readonly kind = TypeKind.Labeled;
|
||||||
|
|
||||||
|
@ -197,8 +231,9 @@ class TLabeled extends TypeBase {
|
||||||
public constructor(
|
public constructor(
|
||||||
public name: string,
|
public name: string,
|
||||||
public type: Type,
|
public type: Type,
|
||||||
|
public node: Syntax | null = null,
|
||||||
) {
|
) {
|
||||||
super();
|
super(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
public find(): TLabeled {
|
public find(): TLabeled {
|
||||||
|
@ -214,14 +249,22 @@ class TLabeled extends TypeBase {
|
||||||
return this.type.getTypeVars();
|
return this.type.getTypeVars();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public shallowClone(): TLabeled {
|
||||||
|
return new TLabeled(
|
||||||
|
this.name,
|
||||||
|
this.type,
|
||||||
|
this.node,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public substitute(sub: TVSub): Type {
|
public substitute(sub: TVSub): Type {
|
||||||
const newType = this.type.substitute(sub);
|
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;
|
public readonly kind = TypeKind.Record;
|
||||||
|
|
||||||
|
@ -230,8 +273,9 @@ class TRecord extends TypeBase {
|
||||||
public constructor(
|
public constructor(
|
||||||
public decl: StructDeclaration,
|
public decl: StructDeclaration,
|
||||||
public fields: Map<string, Type>,
|
public fields: Map<string, Type>,
|
||||||
|
public node: Syntax | null = null,
|
||||||
) {
|
) {
|
||||||
super();
|
super(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
public *getTypeVars(): Iterable<TVar> {
|
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 {
|
public substitute(sub: TVSub): Type {
|
||||||
let changed = false;
|
let changed = false;
|
||||||
const newFields = new Map();
|
const newFields = new Map();
|
||||||
|
@ -250,7 +302,7 @@ class TRecord extends TypeBase {
|
||||||
}
|
}
|
||||||
newFields.set(key, newType);
|
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));
|
this.diagnostics.add(new BindingNotFoudDiagnostic(node.name.name.text, node.name.name));
|
||||||
return new TAny();
|
return new TAny();
|
||||||
}
|
}
|
||||||
return this.instantiate(scheme, node);
|
const type = this.instantiate(scheme, node);
|
||||||
|
type.node = node;
|
||||||
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
case SyntaxKind.MemberExpression:
|
case SyntaxKind.MemberExpression:
|
||||||
|
@ -733,6 +787,8 @@ export class Checker {
|
||||||
ty = this.getIntType();
|
ty = this.getIntType();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
ty = ty.shallowClone();
|
||||||
|
ty.node = node;
|
||||||
return ty;
|
return ty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -741,7 +797,7 @@ export class Checker {
|
||||||
const scheme = this.lookup(node.name.text);
|
const scheme = this.lookup(node.name.text);
|
||||||
if (scheme === null) {
|
if (scheme === null) {
|
||||||
this.diagnostics.add(new BindingNotFoudDiagnostic(node.name.text, node.name));
|
this.diagnostics.add(new BindingNotFoudDiagnostic(node.name.text, node.name));
|
||||||
return new TAny();
|
return this.createTypeVar();
|
||||||
}
|
}
|
||||||
const type = this.instantiate(scheme, node.name);
|
const type = this.instantiate(scheme, node.name);
|
||||||
assert(type.kind === TypeKind.Con);
|
assert(type.kind === TypeKind.Con);
|
||||||
|
@ -749,7 +805,7 @@ export class Checker {
|
||||||
for (const element of node.elements) {
|
for (const element of node.elements) {
|
||||||
argTypes.push(this.inferExpression(element));
|
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:
|
case SyntaxKind.StructExpression:
|
||||||
|
@ -786,7 +842,7 @@ export class Checker {
|
||||||
throw new Error(`Unexpected ${member}`);
|
throw new Error(`Unexpected ${member}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const type = new TRecord(recordType.decl, fields);
|
const type = new TRecord(recordType.decl, fields, node);
|
||||||
this.addConstraint(
|
this.addConstraint(
|
||||||
new CEqual(
|
new CEqual(
|
||||||
recordType,
|
recordType,
|
||||||
|
@ -836,7 +892,9 @@ export class Checker {
|
||||||
this.diagnostics.add(new BindingNotFoudDiagnostic(node.name.text, node.name));
|
this.diagnostics.add(new BindingNotFoudDiagnostic(node.name.text, node.name));
|
||||||
return new TAny();
|
return new TAny();
|
||||||
}
|
}
|
||||||
return this.instantiate(scheme, node.name);
|
const type = this.instantiate(scheme, node.name);
|
||||||
|
type.node = node;
|
||||||
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -1338,7 +1396,7 @@ export class Checker {
|
||||||
|
|
||||||
while (queue.length > 0) {
|
while (queue.length > 0) {
|
||||||
|
|
||||||
const constraint = queue.pop()!;
|
const constraint = queue.shift()!;
|
||||||
|
|
||||||
switch (constraint.kind) {
|
switch (constraint.kind) {
|
||||||
|
|
||||||
|
@ -1382,6 +1440,7 @@ export class Checker {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
solution.set(left, right);
|
solution.set(left, right);
|
||||||
|
TypeBase.join(left, right);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1390,6 +1449,7 @@ export class Checker {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (left.kind === TypeKind.Any || right.kind === TypeKind.Any) {
|
if (left.kind === TypeKind.Any || right.kind === TypeKind.Any) {
|
||||||
|
TypeBase.join(left, right);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1408,6 +1468,9 @@ export class Checker {
|
||||||
if (!this.unify(left.returnType, right.returnType, solution, constraint)) {
|
if (!this.unify(left.returnType, right.returnType, solution, constraint)) {
|
||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
|
if (success) {
|
||||||
|
TypeBase.join(left, right);
|
||||||
|
}
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1429,6 +1492,9 @@ export class Checker {
|
||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (success) {
|
||||||
|
TypeBase.join(left, right);
|
||||||
|
}
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1456,6 +1522,9 @@ export class Checker {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delete right.fields;
|
delete right.fields;
|
||||||
|
if (success) {
|
||||||
|
TypeBase.join(left, right);
|
||||||
|
}
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1480,6 +1549,9 @@ export class Checker {
|
||||||
for (const fieldName of remaining) {
|
for (const fieldName of remaining) {
|
||||||
this.diagnostics.add(new FieldDoesNotExistDiagnostic(left, fieldName));
|
this.diagnostics.add(new FieldDoesNotExistDiagnostic(left, fieldName));
|
||||||
}
|
}
|
||||||
|
if (success) {
|
||||||
|
TypeBase.join(left, right);
|
||||||
|
}
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1497,6 +1569,9 @@ export class Checker {
|
||||||
this.diagnostics.add(new FieldMissingDiagnostic(left, fieldName));
|
this.diagnostics.add(new FieldMissingDiagnostic(left, fieldName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (success) {
|
||||||
|
TypeBase.join(left, right);
|
||||||
|
}
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
|
||||||
import { describe } from "yargs";
|
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 { Syntax, SyntaxKind, TextFile, TextPosition, TextRange, Token } from "./cst";
|
||||||
import { countDigits } from "./util";
|
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 {
|
export class UnificationFailedDiagnostic {
|
||||||
|
|
||||||
public readonly level = Level.Error;
|
public readonly level = Level.Error;
|
||||||
|
@ -228,6 +236,8 @@ export class UnificationFailedDiagnostic {
|
||||||
}
|
}
|
||||||
|
|
||||||
public format(): string {
|
public format(): string {
|
||||||
|
const leftNode = getFirstNodeInTypeChain(this.left);
|
||||||
|
const rightNode = getFirstNodeInTypeChain(this.right);
|
||||||
const node = this.nodes[0];
|
const node = this.nodes[0];
|
||||||
let out = ANSI_FG_RED + ANSI_BOLD + `error: ` + ANSI_RESET
|
let out = ANSI_FG_RED + ANSI_BOLD + `error: ` + ANSI_RESET
|
||||||
+ `unification of ` + ANSI_FG_GREEN + describeType(this.left) + 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 += ' ... in an instantiation of the following expression\n\n'
|
||||||
out += printNode(node, { indentation: i === 0 ? ' ' : ' ' }) + '\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;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,9 +297,13 @@ export class FieldMissingDiagnostic {
|
||||||
}
|
}
|
||||||
|
|
||||||
public format(): string {
|
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 `
|
+ `field '${this.fieldName}' is missing from `
|
||||||
+ describeType(this.recordType) + '\n\n';
|
+ 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 readonly level = Level.Error;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
public recordType: TRecord,
|
|
||||||
public fieldName: string,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,3 +69,4 @@ let is_odd x.
|
||||||
|
|
||||||
not (is_even True)
|
not (is_even True)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue