Add some type-checking logic and improve diagnostics

This commit is contained in:
Sam Vervaeck 2022-08-31 13:29:56 +02:00
parent cda44e4c25
commit 48f1b0f45c
7 changed files with 1248 additions and 76 deletions

View file

@ -7,9 +7,11 @@ import path from "path"
import fs from "fs" import fs from "fs"
import yargs from "yargs" import yargs from "yargs"
import { Diagnostics } from "../diagnostics" import { Diagnostics, UnexpectedCharDiagnostic, UnexpectedTokenDiagnostic } from "../diagnostics"
import { Punctuator, Scanner } from "../scanner" import { Punctuator, Scanner } from "../scanner"
import { Parser } from "../parser" import { ParseError, Parser } from "../parser"
import { Checker } from "../checker"
import { TextFile } from "../cst"
function debug(value: any) { function debug(value: any) {
console.error(util.inspect(value, { colors: true, depth: Infinity })); console.error(util.inspect(value, { colors: true, depth: Infinity }));
@ -31,14 +33,27 @@ yargs
const cwd = args.C; const cwd = args.C;
const filename = path.resolve(cwd, args.file); const filename = path.resolve(cwd, args.file);
const diagnostics = new Diagnostics(); const diagnostics = new Diagnostics();
const text = fs.readFileSync(filename, 'utf8') const text = fs.readFileSync(filename, 'utf8')
const scanner = new Scanner(text, 0, diagnostics); const file = new TextFile(filename, text);
const scanner = new Scanner(text, 0, diagnostics, file);
const punctuated = new Punctuator(scanner); const punctuated = new Punctuator(scanner);
const parser = new Parser(punctuated); const parser = new Parser(file, punctuated);
const sourceFile = parser.parseSourceFile(); let sourceFile;
try {
debug(sourceFile.toJSON()); sourceFile = parser.parseSourceFile();
} catch (error) {
if (!(error instanceof ParseError)) {
throw error;
}
diagnostics.add(new UnexpectedTokenDiagnostic(error.file, error.actual, error.expected));
return;
}
sourceFile.setParents();
//debug(sourceFile.toJSON());
const checker = new Checker(diagnostics);
checker.check(sourceFile);
} }
) )

View file

@ -0,0 +1,802 @@
import {
Expression,
Pattern,
Syntax,
SyntaxKind,
TypeExpression
} from "./cst";
import { BindingNotFoudDiagnostic, Diagnostics, UnificationFailedDiagnostic } from "./diagnostics";
import { assert } from "./util";
export enum TypeKind {
Arrow,
Var,
Con,
Any,
Tuple,
}
abstract class TypeBase {
public abstract readonly kind: TypeKind;
public abstract getTypeVars(): Iterable<TVar>;
public abstract substitute(sub: TVSub): Type;
public hasTypeVar(tv: TVar): boolean {
for (const other of this.getTypeVars()) {
if (tv.id === other.id) {
return true;
}
}
return false;
}
}
class TVar extends TypeBase {
public readonly kind = TypeKind.Var;
public constructor(
public id: number,
) {
super();
}
public *getTypeVars(): Iterable<TVar> {
yield this;
}
public substitute(sub: TVSub): Type {
return sub.get(this) ?? this;
}
}
class TArrow extends TypeBase {
public readonly kind = TypeKind.Arrow;
public constructor(
public paramTypes: Type[],
public returnType: Type,
) {
super();
}
public *getTypeVars(): Iterable<TVar> {
for (const paramType of this.paramTypes) {
yield* paramType.getTypeVars();
}
yield* this.returnType.getTypeVars();
}
public substitute(sub: TVSub): Type {
let changed = false;
const newParamTypes = [];
for (const paramType of this.paramTypes) {
const newParamType = paramType.substitute(sub);
if (newParamType !== paramType) {
changed = true;
}
newParamTypes.push(newParamType);
}
const newReturnType = this.returnType.substitute(sub);
if (newReturnType !== this.returnType) {
changed = true;
}
return changed ? new TArrow(newParamTypes, newReturnType) : this;
}
}
class TCon extends TypeBase {
public readonly kind = TypeKind.Con;
public constructor(
public id: number,
public argTypes: Type[],
public displayName: string,
) {
super();
}
public *getTypeVars(): Iterable<TVar> {
for (const argType of this.argTypes) {
yield* argType.getTypeVars();
}
}
public substitute(sub: TVSub): Type {
let changed = false;
const newArgTypes = [];
for (const argType of this.argTypes) {
const newArgType = argType.substitute(sub);
if (newArgType !== argType) {
changed = true;
}
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;
}
}
class TTuple extends TypeBase {
public readonly kind = TypeKind.Tuple;
public constructor(
public elementTypes: Type[],
) {
super();
}
public *getTypeVars(): Iterable<TVar> {
for (const elementType of this.elementTypes) {
yield* elementType.getTypeVars();
}
}
public substitute(sub: TVSub): Type {
let changed = false;
const newElementTypes = [];
for (const elementType of this.elementTypes) {
const newElementType = elementType.substitute(sub);
if (newElementType !== elementType) {
changed = true;
}
newElementTypes.push(newElementType);
}
return changed ? new TTuple(newElementTypes) : this;
}
}
export type Type
= TCon
| TArrow
| TVar
| TAny
| TTuple
class TVSet {
private mapping = new Map<number, TVar>();
public add(tv: TVar): void {
this.mapping.set(tv.id, tv);
}
public delete(tv: TVar): void {
this.mapping.delete(tv.id);
}
public [Symbol.iterator](): Iterator<TVar> {
return this.mapping.values();
}
}
class TVSub {
private mapping = new Map<number, Type>();
public set(tv: TVar, type: Type): void {
this.mapping.set(tv.id, type);
}
public get(tv: TVar): Type | undefined {
return this.mapping.get(tv.id);
}
public has(tv: TVar): boolean {
return this.mapping.has(tv.id);
}
public delete(tv: TVar): void {
this.mapping.delete(tv.id);
}
public values(): Iterable<Type> {
return this.mapping.values();
}
}
const enum ConstraintKind {
Equal,
Many,
}
abstract class ConstraintBase {
public abstract substitute(sub: TVSub): Constraint;
}
class CEqual extends ConstraintBase {
public readonly kind = ConstraintKind.Equal;
public constructor(
public left: Type,
public right: Type,
public node: Syntax,
) {
super();
}
public substitute(sub: TVSub): Constraint {
return new CEqual(
this.left.substitute(sub),
this.right.substitute(sub),
this.node,
);
}
}
class CMany extends ConstraintBase {
public readonly kind = ConstraintKind.Many;
public constructor(
public elements: Constraint[]
) {
super();
}
public substitute(sub: TVSub): Constraint {
const newElements = [];
for (const element of this.elements) {
newElements.push(element.substitute(sub));
}
return new CMany(newElements);
}
}
type Constraint
= CEqual
| CMany
class ConstraintSet extends Array<Constraint> {
}
abstract class SchemeBase {
}
class Forall extends SchemeBase {
public constructor(
public tvs: TVar[],
public constraints: Constraint[],
public type: Type,
) {
super();
}
}
type Scheme
= Forall
class TypeEnv extends Map<string, Scheme> {
}
export interface InferContext {
typeVars: TVSet;
env: TypeEnv;
constraints: ConstraintSet;
}
export class Checker {
private nextTypeVarId = 0;
private nextConTypeId = 0;
private stringType = new TCon(this.nextConTypeId++, [], 'String');
private intType = new TCon(this.nextConTypeId++, [], 'Int');
private boolType = new TCon(this.nextConTypeId++, [], 'Bool');
private typeEnvs: TypeEnv[] = [];
private typeVars: TVSet[] = [];
private constraints: ConstraintSet[] = [];
private returnTypes: Type[] = [];
public constructor(
private diagnostics: Diagnostics
) {
}
public getIntType(): Type {
return this.intType;
}
public getStringType(): Type {
return this.stringType;
}
public getBoolType(): Type {
return this.boolType;
}
private createTypeVar(): TVar {
const typeVar = new TVar(this.nextTypeVarId++);
this.typeVars[this.typeVars.length-1].add(typeVar);
return typeVar;
}
private addConstraint(constraint: Constraint): void {
this.constraints[this.constraints.length-1].push(constraint);
}
private pushContext(context: InferContext) {
if (context.typeVars !== null) {
this.typeVars.push(context.typeVars);
}
if (context.env !== null) {
this.typeEnvs.push(context.env);
}
if (context.constraints !== null) {
this.constraints.push(context.constraints);
}
}
private popContext(context: InferContext) {
if (context.typeVars !== null) {
this.typeVars.pop();
}
if (context.env !== null) {
this.typeEnvs.pop();
}
if (context.constraints !== null) {
this.constraints.pop();
}
}
private lookup(name: string): Scheme | null {
for (let i = this.typeEnvs.length-1; i >= 0; i--) {
const scheme = this.typeEnvs[i].get(name);
if (scheme !== undefined) {
return scheme;
}
}
return null;
}
private getReturnType(): Type {
assert(this.returnTypes.length > 0);
return this.returnTypes[this.returnTypes.length-1];
}
private instantiate(scheme: Scheme): Type {
const sub = new TVSub();
for (const tv of scheme.tvs) {
sub.set(tv, this.createTypeVar());
}
for (const constraint of scheme.constraints) {
this.addConstraint(constraint.substitute(sub));
}
return scheme.type.substitute(sub);
}
private addBinding(name: string, scheme: Scheme): void {
const env = this.typeEnvs[this.typeEnvs.length-1];
env.set(name, scheme);
}
private forwardDeclare(node: Syntax): void {
switch (node.kind) {
case SyntaxKind.SourceFile:
{
for (const element of node.elements) {
this.forwardDeclare(element);
}
break;
}
case SyntaxKind.ExpressionStatement:
case SyntaxKind.ReturnStatement:
{
// TODO This should be updated if block-scoped expressions are allowed.
break;
}
case SyntaxKind.LetDeclaration:
{
const typeVars = new TVSet();
const env = new TypeEnv();
const constraints = new ConstraintSet();
const context = { typeVars, env, constraints };
node.context = context;
this.pushContext(context);
let type;
if (node.typeAssert !== null) {
type = this.inferTypeExpression(node.typeAssert.typeExpression);
} else {
type = this.createTypeVar();
}
node.type = type;
if (node.body !== null && node.body.kind === SyntaxKind.BlockBody) {
for (const element of node.body.elements) {
this.forwardDeclare(element);
}
}
this.popContext(context);
break;
}
}
}
public infer(node: Syntax): void {
switch (node.kind) {
case SyntaxKind.SourceFile:
{
for (const element of node.elements) {
this.infer(element);
}
break;
}
case SyntaxKind.ExpressionStatement:
{
this.inferExpression(node.expression);
break;
}
case SyntaxKind.ReturnStatement:
{
let type;
if (node.expression === null) {
type = new TTuple([]);
} else {
type = this.inferExpression(node.expression);
}
this.addConstraint(
new CEqual(
this.getReturnType(),
type,
node
)
);
break;
}
case SyntaxKind.LetDeclaration:
{
// Get the type that was stored on the node by forwardDeclare()
const type = node.type!;
const context = node.context!;
this.pushContext(context);
const paramTypes = [];
const returnType = this.createTypeVar();
for (const param of node.params) {
const paramType = this.createTypeVar()
this.inferBindings(param.pattern, paramType, [], []);
paramTypes.push(paramType);
}
if (node.body !== null) {
switch (node.body.kind) {
case SyntaxKind.ExprBody:
{
this.addConstraint(
new CEqual(
this.inferExpression(node.body.expression),
returnType,
node.body.expression
)
);
break;
}
case SyntaxKind.BlockBody:
{
for (const element of node.body.elements) {
this.infer(element);
}
break;
}
}
}
this.addConstraint(new CEqual(type, new TArrow(paramTypes, returnType), node));
this.popContext(context);
this.inferBindings(node.pattern, type, context.typeVars, context.constraints);
// FIXME these two may need to go below inferBindings
//this.typeVars.pop();
//this.constraints.pop();
break;
}
default:
throw new Error(`Unexpected ${node}`);
}
}
public inferExpression(node: Expression): Type {
switch (node.kind) {
case SyntaxKind.ReferenceExpression:
{
assert(node.name.modulePath.length === 0);
const scheme = this.lookup(node.name.name.text);
if (scheme === null) {
this.diagnostics.add(new BindingNotFoudDiagnostic(node.name.name.text, node.name.name));
return new TAny();
}
return this.instantiate(scheme);
}
case SyntaxKind.CallExpression:
{
const opType = this.inferExpression(node.func);
const retType = this.createTypeVar();
const paramTypes = [];
for (const arg of node.args) {
paramTypes.push(this.inferExpression(arg));
}
this.addConstraint(
new CEqual(
opType,
new TArrow(paramTypes, retType),
node
)
);
return retType;
}
case SyntaxKind.ConstantExpression:
{
let ty;
switch (node.token.kind) {
case SyntaxKind.StringLiteral:
ty = this.getStringType();
break;
case SyntaxKind.Integer:
ty = this.getIntType();
break;
}
return ty;
}
case SyntaxKind.NamedTupleExpression:
{
const scheme = this.lookup(node.name.text);
if (scheme === null) {
this.diagnostics.add(new BindingNotFoudDiagnostic(node.name.text, node.name));
return new TAny();
}
const type = this.instantiate(scheme);
assert(type.kind === TypeKind.Con);
const argTypes = [];
for (const element of node.elements) {
argTypes.push(this.inferExpression(element));
}
return new TCon(type.id, argTypes, type.displayName);
}
case SyntaxKind.InfixExpression:
{
const scheme = this.lookup(node.operator.text);
if (scheme === null) {
this.diagnostics.add(new BindingNotFoudDiagnostic(node.operator.text, node.operator));
return new TAny();
}
const opType = this.instantiate(scheme);
const retType = this.createTypeVar();
const leftType = this.inferExpression(node.left);
const rightType = this.inferExpression(node.right);
this.addConstraint(
new CEqual(
new TArrow([ leftType, rightType ], retType),
opType,
node,
),
);
return retType;
}
default:
throw new Error(`Unexpected ${node}`);
}
}
public inferTypeExpression(node: TypeExpression): Type {
switch (node.kind) {
case SyntaxKind.ReferenceTypeExpression:
{
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.instantiate(scheme);
}
default:
throw new Error(`Unrecognised ${node}`);
}
}
public inferBindings(pattern: Pattern, type: Type, tvs: TVar[], constraints: Constraint[]): void {
switch (pattern.kind) {
case SyntaxKind.BindPattern:
{
this.addBinding(pattern.name.text, new Forall(tvs, constraints, type));
break;
}
}
}
public check(node: Syntax): void {
const constraints = new ConstraintSet();
const env = new TypeEnv();
env.set('String', new Forall([], [], this.stringType));
env.set('Int', new Forall([], [], this.intType));
env.set('True', new Forall([], [], this.boolType));
env.set('False', new Forall([], [], this.boolType));
env.set('+', new Forall([], [], new TArrow([ this.intType, this.intType ], this.intType)));
env.set('-', new Forall([], [], new TArrow([ this.intType, this.intType ], this.intType)));
env.set('*', new Forall([], [], new TArrow([ this.intType, this.intType ], this.intType)));
env.set('/', new Forall([], [], new TArrow([ this.intType, this.intType ], this.intType)));
this.typeVars.push(new TVSet);
this.constraints.push(constraints);
this.typeEnvs.push(env);
this.forwardDeclare(node);
this.infer(node);
this.solve(new CMany(constraints));
this.typeVars.pop();
this.constraints.pop();
this.typeEnvs.pop();
}
private solve(constraint: Constraint): TVSub {
const queue = [ constraint ];
const solution = new TVSub();
while (queue.length > 0) {
const constraint = queue.pop()!;
switch (constraint.kind) {
case ConstraintKind.Many:
{
for (const element of constraint.elements) {
queue.push(element);
}
break;
}
case ConstraintKind.Equal:
{
if (!this.unify(constraint.left, constraint.right, solution)) {
this.diagnostics.add(
new UnificationFailedDiagnostic(
constraint.left.substitute(solution),
constraint.right.substitute(solution),
constraint.node
)
);
}
break;
}
}
}
return solution;
}
private unify(left: Type, right: Type, solution: TVSub): boolean {
if (left.kind === TypeKind.Var && solution.has(left)) {
left = solution.get(left)!;
}
if (right.kind === TypeKind.Var && solution.has(right)) {
right = solution.get(right)!;
}
if (left.kind === TypeKind.Var) {
if (right.hasTypeVar(left)) {
// TODO occurs check diagnostic
}
solution.set(left, right);
return true;
}
if (right.kind === TypeKind.Var) {
return this.unify(right, left, solution);
}
if (left.kind === TypeKind.Arrow && right.kind === TypeKind.Arrow) {
if (left.paramTypes.length !== right.paramTypes.length) {
this.diagnostics.add(new ArityMismatchDiagnostic(left, right));
return false;
}
let success = true;
const count = left.paramTypes.length;
for (let i = 0; i < count; i++) {
if (!this.unify(left.paramTypes[i], right.paramTypes[i], solution)) {
success = false;
}
}
if (!this.unify(left.returnType, right.returnType, solution)) {
success = false;
}
return success;
}
if (left.kind === TypeKind.Con && right.kind === TypeKind.Con) {
if (left.id !== right.id) {
return false;
}
assert(left.argTypes.length === right.argTypes.length);
const count = left.argTypes.length;
for (let i = 0; i < count; i++) {
if (!this.unify(left.argTypes[i], right.argTypes[i], solution)) {
return false;
}
}
return true;
}
return false;
}
}

View file

@ -1,4 +1,5 @@
import { JSONObject, JSONValue } from "./util"; import { JSONObject, JSONValue } from "./util";
import type { InferContext, Type } from "./checker"
export type TextSpan = [number, number]; export type TextSpan = [number, number];
@ -116,7 +117,10 @@ export const enum SyntaxKind {
VariadicStructPatternElement, VariadicStructPatternElement,
// Expressions // Expressions
CallExpression,
ReferenceExpression, ReferenceExpression,
NamedTupleExpression,
StructExpression,
TupleExpression, TupleExpression,
NestedExpression, NestedExpression,
ConstantExpression, ConstantExpression,
@ -156,7 +160,10 @@ export const enum SyntaxKind {
export type Syntax export type Syntax
= SourceFile = SourceFile
| Module
| Token
| Param | Param
| Body
| StructDeclarationField | StructDeclarationField
| Declaration | Declaration
| Statement | Statement
@ -164,14 +171,64 @@ export type Syntax
| TypeExpression | TypeExpression
| Pattern | Pattern
function isIgnoredProperty(key: string): boolean {
return key === 'kind' || key === 'parent';
}
abstract class SyntaxBase { abstract class SyntaxBase {
public parent: Syntax | null = null;
public abstract readonly kind: SyntaxKind; public abstract readonly kind: SyntaxKind;
public abstract getFirstToken(): Token; public abstract getFirstToken(): Token;
public abstract getLastToken(): Token; public abstract getLastToken(): Token;
public getRange(): TextRange {
return new TextRange(
this.getFirstToken().getStartPosition(),
this.getLastToken().getEndPosition(),
);
}
public getSourceFile(): SourceFile {
let curr = this as any;
do {
if (curr.kind === SyntaxKind.SourceFile) {
return curr;
}
curr = curr.parent;
} while (curr != null);
throw new Error(`Could not find a SourceFile in any of the parent nodes of ${this}`);
}
public setParents(): void {
const visit = (value: any) => {
if (value === null) {
return;
}
if (Array.isArray(value)) {
value.forEach(visit);
return;
}
if (value instanceof SyntaxBase) {
value.parent = this as any;
value.setParents();
return;
}
}
for (const key of Object.getOwnPropertyNames(this)) {
if (isIgnoredProperty(key)) {
continue;
}
visit((this as any)[key]);
}
}
public toJSON(): JSONObject { public toJSON(): JSONObject {
const obj: JSONObject = {}; const obj: JSONObject = {};
@ -179,7 +236,7 @@ abstract class SyntaxBase {
obj['type'] = this.constructor.name; obj['type'] = this.constructor.name;
for (const key of Object.getOwnPropertyNames(this)) { for (const key of Object.getOwnPropertyNames(this)) {
if (key === 'kind') { if (isIgnoredProperty(key)) {
continue; continue;
} }
obj[key] = encode((this as any)[key]); obj[key] = encode((this as any)[key]);
@ -207,7 +264,7 @@ abstract class TokenBase extends SyntaxBase {
private endPos: TextPosition | null = null; private endPos: TextPosition | null = null;
constructor( public constructor(
private startPos: TextPosition, private startPos: TextPosition,
) { ) {
super(); super();
@ -221,6 +278,13 @@ abstract class TokenBase extends SyntaxBase {
throw new Error(`Trying to get the last token of an object that is a token itself.`); throw new Error(`Trying to get the last token of an object that is a token itself.`);
} }
public getRange(): TextRange {
return new TextRange(
this.getStartPosition(),
this.getEndPosition(),
);
}
public getStartPosition(): TextPosition { public getStartPosition(): TextPosition {
return this.startPos; return this.startPos;
} }
@ -915,6 +979,54 @@ export class QualifiedName extends SyntaxBase {
} }
export class CallExpression extends SyntaxBase {
public readonly kind = SyntaxKind.CallExpression;
public constructor(
public func: Expression,
public args: Expression[],
) {
super();
}
public getFirstToken(): Token {
return this.func.getFirstToken();
}
public getLastToken(): Token {
if (this.args.length > 0) {
return this.args[this.args.length-1].getLastToken();
}
return this.func.getLastToken();
}
}
export class NamedTupleExpression extends SyntaxBase {
public readonly kind = SyntaxKind.NamedTupleExpression;
public constructor(
public name: Constructor,
public elements: Expression[],
) {
super();
}
public getFirstToken(): Token {
return this.name;
}
public getLastToken(): Token {
if (this.elements.length > 0) {
return this.elements[this.elements.length-1].getLastToken();
}
return this.name;
}
}
export class ReferenceExpression extends SyntaxBase { export class ReferenceExpression extends SyntaxBase {
public readonly kind = SyntaxKind.ReferenceExpression; public readonly kind = SyntaxKind.ReferenceExpression;
@ -1000,7 +1112,9 @@ export class InfixExpression extends SyntaxBase {
} }
export type Expression export type Expression
= ReferenceExpression = CallExpression
| NamedTupleExpression
| ReferenceExpression
| ConstantExpression | ConstantExpression
| TupleExpression | TupleExpression
| NestedExpression | NestedExpression
@ -1200,6 +1314,9 @@ export class LetDeclaration extends SyntaxBase {
public readonly kind = SyntaxKind.LetDeclaration; public readonly kind = SyntaxKind.LetDeclaration;
public type?: Type;
public context?: InferContext;
public constructor( public constructor(
public pubKeyword: PubKeyword | null, public pubKeyword: PubKeyword | null,
public letKeyword: LetKeyword, public letKeyword: LetKeyword,
@ -1317,6 +1434,7 @@ export class SourceFile extends SyntaxBase {
public readonly kind = SyntaxKind.SourceFile; public readonly kind = SyntaxKind.SourceFile;
public constructor( public constructor(
private file: TextFile,
public elements: SourceFileElement[], public elements: SourceFileElement[],
public eof: EndOfFile, public eof: EndOfFile,
) { ) {
@ -1337,4 +1455,8 @@ export class SourceFile extends SyntaxBase {
return this.eof; return this.eof;
} }
public getFile() {
return this.file;
}
} }

View file

@ -1,23 +1,199 @@
import { TypeKind, Type } from "./checker";
import { Syntax, SyntaxKind, TextFile, TextPosition, TextRange, Token } from "./cst";
import { countDigits } from "./util";
const ANSI_RESET = "\u001b[0m"
const ANSI_BOLD = "\u001b[1m"
const ANSI_UNDERLINE = "\u001b[4m"
const ANSI_REVERSED = "\u001b[7m"
const ANSI_FG_BLACK = "\u001b[30m"
const ANSI_FG_RED = "\u001b[31m"
const ANSI_FG_GREEN = "\u001b[32m"
const ANSI_FG_YELLOW = "\u001b[33m"
const ANSI_FG_BLUE = "\u001b[34m"
const ANSI_FG_CYAN = "\u001b[35m"
const ANSI_FG_MAGENTA = "\u001b[36m"
const ANSI_FG_WHITE = "\u001b[37m"
const ANSI_BG_BLACK = "\u001b[40m"
const ANSI_BG_RED = "\u001b[41m"
const ANSI_BG_GREEN = "\u001b[42m"
const ANSI_BG_YELLOW = "\u001b[43m"
const ANSI_BG_BLUE = "\u001b[44m"
const ANSI_BG_CYAN = "\u001b[45m"
const ANSI_BG_MAGENTA = "\u001b[46m"
const ANSI_BG_WHITE = "\u001b[47m"
export class UnexpectedCharDiagnostic { export class UnexpectedCharDiagnostic {
public constructor( public constructor(
public text: string, public file: TextFile,
public offset: number, public position: TextPosition,
public actual: string, public actual: string,
) { ) {
} }
public format(): string { public format(): string {
let out = `error: unexpeced character '${this.actual}'.`; const endPos = this.position.clone();
endPos.advance(this.actual);
return ANSI_FG_RED + ANSI_BOLD + 'error: ' + ANSI_RESET
+ `unexpeced character '${this.actual}'.\n\n`
+ printExcerpt(this.file, new TextRange(this.position, endPos)) + '\n';
}
}
const DESCRIPTIONS: Record<SyntaxKind, string> = {
[SyntaxKind.StringLiteral]: 'a string literal',
[SyntaxKind.Identifier]: "an identifier",
[SyntaxKind.Comma]: "','",
[SyntaxKind.Colon]: "':'",
[SyntaxKind.Integer]: "an integer",
[SyntaxKind.LParen]: "'('",
[SyntaxKind.RParen]: "')'",
[SyntaxKind.LBrace]: "'{'",
[SyntaxKind.RBrace]: "'}'",
[SyntaxKind.LBracket]: "'['",
[SyntaxKind.RBracket]: "']'",
[SyntaxKind.ConstantExpression]: 'a constant expression',
[SyntaxKind.ReferenceExpression]: 'a reference expression',
[SyntaxKind.LineFoldEnd]: 'the end of the current line-fold',
[SyntaxKind.TupleExpression]: 'a tuple expression such as (1, 2)',
[SyntaxKind.ReferenceExpression]: 'a reference to some variable',
[SyntaxKind.NestedExpression]: 'an expression nested with parentheses',
[SyntaxKind.ConstantExpression]: 'a constant expression such as 1 or "foo"',
[SyntaxKind.NamedTupleExpression]: 'a named tuple expression',
[SyntaxKind.StructExpression]: 'a struct expression',
}
function describeSyntaxKind(kind: SyntaxKind): string {
const desc = DESCRIPTIONS[kind];
if (desc === undefined) {
throw new Error(`Could not describe SyntaxKind '${kind}'`);
}
return desc
}
function describeExpected(expected: SyntaxKind[]) {
if (expected.length === 0) {
return 'nothing';
}
let out = describeSyntaxKind(expected[0]);
if (expected.length === 1) {
return out; return out;
} }
for (let i = 1; i < expected.length-1; i++) {
const kind = expected[i];
out += ', ' + describeSyntaxKind(kind);
}
out += ' or ' + describeSyntaxKind(expected[expected.length-1])
return out;
}
export class UnexpectedTokenDiagnostic {
public constructor(
public file: TextFile,
public actual: Token,
public expected: SyntaxKind[],
) {
}
public format(): string {
return ANSI_FG_RED + ANSI_BOLD + 'fatal: ' + ANSI_RESET
+ `expected ${describeExpected(this.expected)} but got '${this.actual.text}'\n\n`
+ printExcerpt(this.file, this.actual.getRange()) + '\n';
}
}
export class BindingNotFoudDiagnostic {
public constructor(
public name: string,
public node: Syntax,
) {
}
public format(): string {
const file = this.node.getSourceFile().getFile();
return ANSI_FG_RED + ANSI_BOLD + 'error: ' + ANSI_RESET
+ `binding '${this.name}' was not found.\n\n`
+ printExcerpt(file, this.node.getRange()) + '\n';
}
}
function describeType(type: Type): string {
switch (type.kind) {
case TypeKind.Any:
return 'Any';
case TypeKind.Con:
{
let out = type.displayName;
for (const argType of type.argTypes) {
out += ' ' + describeType(argType);
}
return out;
}
case TypeKind.Var:
return 'a' + type.id;
case TypeKind.Arrow:
{
let out = '(';
let first = true;
for (const paramType of type.paramTypes) {
if (first) first = false;
else out += ', ';
out += describeType(paramType);
}
out += ') -> ' + describeType(type.returnType);
return out;
}
case TypeKind.Tuple:
{
let out = '(';
let first = true;
for (const elementType of type.elementTypes) {
if (first) first = false;
else out += ', ';
out += describeType(elementType);
}
return out;
}
}
}
export class UnificationFailedDiagnostic {
public constructor(
public left: Type,
public right: Type,
public node: Syntax,
) {
}
public format(): string {
const file = this.node.getSourceFile().getFile();
return ANSI_FG_RED + ANSI_BOLD + `error: ` + ANSI_RESET
+ `unification of ` + ANSI_FG_GREEN + describeType(this.left) + ANSI_RESET
+ ' and ' + ANSI_FG_GREEN + describeType(this.right) + ANSI_RESET + ' failed.\n\n'
+ printExcerpt(file, this.node.getRange()) + '\n';
}
} }
export type Diagnostic export type Diagnostic
= UnexpectedCharDiagnostic; = UnexpectedCharDiagnostic
| BindingNotFoudDiagnostic
| UnificationFailedDiagnostic
| UnexpectedTokenDiagnostic
export class Diagnostics { export class Diagnostics {
@ -30,3 +206,51 @@ export class Diagnostics {
} }
function printExcerpt(file: TextFile, span: TextRange, { indentation = ' ', extraLineCount = 2 } = {}): string {
let out = '';
const content = file.text;
const startLine = Math.max(0, span.start.line-1-extraLineCount)
const lines = content.split('\n')
const endLine = Math.min(lines.length, (span.end !== undefined ? span.end.line : startLine) + extraLineCount)
const gutterWidth = Math.max(2, countDigits(endLine+1))
for (let i = startLine; i < endLine; i++) {
const line = lines[i];
let j = firstIndexOfNonEmpty(line);
out += indentation + ' ' + ANSI_FG_BLACK + ANSI_BG_WHITE + ' '.repeat(gutterWidth-countDigits(i+1))+(i+1).toString() + ANSI_RESET + ' ' + line + '\n'
const gutter = indentation + ' ' + ANSI_FG_BLACK + ANSI_BG_WHITE + ' '.repeat(gutterWidth) + ANSI_RESET + ' '
let mark: number;
let skip: number;
if (i === span.start.line-1 && i === span.end.line-1) {
skip = span.start.column-1;
mark = span.end.column-span.start.column;
} else if (i === span.start.line-1) {
skip = span.start.column-1;
mark = line.length-span.start.column+1;
} else if (i === span.end.line-1) {
skip = 0;
mark = span.end.column-1;
} else if (i > span.start.line-1 && i < span.end.line-1) {
skip = 0;
mark = line.length;
} else {
continue;
}
if (j <= skip) {
j = 0;
}
out += gutter + ' '.repeat(j+skip) + ANSI_FG_RED + '~'.repeat(mark-j) + ANSI_RESET + '\n'
}
return out;
}
function firstIndexOfNonEmpty(str: string) {
let j = 0;
for (; j < str.length; j++) {
const ch = str[j];
if (ch !== ' ' && ch !== '\t') {
break;
}
}
return j
}

View file

@ -1,5 +1,4 @@
import { privateDecrypt } from "crypto";
import { import {
ReferenceTypeExpression, ReferenceTypeExpression,
SourceFile, SourceFile,
@ -35,70 +34,24 @@ import {
FieldStructPatternElement, FieldStructPatternElement,
TuplePattern, TuplePattern,
InfixExpression, InfixExpression,
TextFile,
CallExpression,
NamedTupleExpression,
} from "./cst" } from "./cst"
import { Stream, MultiDict } from "./util"; import { Stream } from "./util";
const DESCRIPTIONS: Record<SyntaxKind, string> = { export class ParseError extends Error {
[SyntaxKind.StringLiteral]: 'a string literal',
[SyntaxKind.Identifier]: "an identifier",
[SyntaxKind.Comma]: "','",
[SyntaxKind.Colon]: "':'",
[SyntaxKind.Integer]: "an integer",
[SyntaxKind.LParen]: "'('",
[SyntaxKind.RParen]: "')'",
[SyntaxKind.LBrace]: "'{'",
[SyntaxKind.RBrace]: "'}'",
[SyntaxKind.LBracket]: "'['",
[SyntaxKind.RBracket]: "']'",
[SyntaxKind.ConstantExpression]: 'a constant expression',
[SyntaxKind.ReferenceExpression]: 'a reference expression',
[SyntaxKind.LineFoldEnd]: 'the end of the current line-fold',
[SyntaxKind.TupleExpression]: 'a tuple expression such as (1, 2)',
[SyntaxKind.ReferenceExpression]: 'a reference to some variable',
[SyntaxKind.NestedExpression]: 'an expression nested with parentheses',
[SyntaxKind.ConstantExpression]: 'a constant expression such as 1 or "foo"',
}
function describeSyntaxKind(kind: SyntaxKind): string {
const desc = DESCRIPTIONS[kind];
if (desc === undefined) {
throw new Error(`Could not describe SyntaxKind '${kind}'`);
}
return desc
}
function describeExpected(expected: SyntaxKind[]) {
if (expected.length === 0) {
return 'nothing';
}
let out = describeSyntaxKind(expected[0]);
if (expected.length === 1) {
return out;
}
for (let i = 1; i < expected.length-1; i++) {
const kind = expected[i];
out += ', ' + describeSyntaxKind(kind);
}
out += ' or ' + describeSyntaxKind(expected[expected.length-1])
return out;
}
class ParseError extends Error {
public constructor( public constructor(
public file: TextFile,
public actual: Token, public actual: Token,
public expected: SyntaxKind[], public expected: SyntaxKind[],
) { ) {
super(`got '${actual.text}' but expected ${describeExpected(expected)}`); super(`Uncaught parse error`);
} }
} }
function isConstructor(token: Token): boolean {
return token.kind === SyntaxKind.Identifier
&& token.text[0].toUpperCase() === token.text[0];
}
function isBinaryOperatorLike(token: Token): boolean { function isBinaryOperatorLike(token: Token): boolean {
return token.kind === SyntaxKind.CustomOperator; return token.kind === SyntaxKind.CustomOperator;
} }
@ -148,6 +101,7 @@ export class Parser {
private suffixExprOperators = new Set<string>(); private suffixExprOperators = new Set<string>();
public constructor( public constructor(
public file: TextFile,
public tokens: Stream<Token>, public tokens: Stream<Token>,
) { ) {
for (const [name, mode, precedence] of EXPR_OPERATOR_TABLE) { for (const [name, mode, precedence] of EXPR_OPERATOR_TABLE) {
@ -178,7 +132,7 @@ export class Parser {
} }
private raiseParseError(actual: Token, expected: SyntaxKind[]): never { private raiseParseError(actual: Token, expected: SyntaxKind[]): never {
throw new ParseError(actual, expected); throw new ParseError(this.file, actual, expected);
} }
private peekTokenAfterModifiers(): Token { private peekTokenAfterModifiers(): Token {
@ -203,7 +157,7 @@ export class Parser {
case SyntaxKind.Identifier: case SyntaxKind.Identifier:
return this.parseReferenceTypeExpression(); return this.parseReferenceTypeExpression();
default: default:
throw new ParseError(t0, [ SyntaxKind.Identifier ]); this.raiseParseError(t0, [ SyntaxKind.Identifier ]);
} }
} }
@ -237,10 +191,10 @@ export class Parser {
private parseExpressionWithParens(): Expression { private parseExpressionWithParens(): Expression {
const lparen = this.expectToken(SyntaxKind.LParen) const lparen = this.expectToken(SyntaxKind.LParen)
const t1 = this.peekToken(); const t1 = this.peekToken();
// FIXME should be able to parse tuples
if (t1.kind === SyntaxKind.RParen) { if (t1.kind === SyntaxKind.RParen) {
this.getToken(); this.getToken();
return new TupleExpression(lparen, [], t1); return new TupleExpression(lparen, [], t1);
} else if (t1.kind === SyntaxKind.Constructor) {
} else { } else {
const expression = this.parseExpression(); const expression = this.parseExpression();
const t2 = this.expectToken(SyntaxKind.RParen); const t2 = this.expectToken(SyntaxKind.RParen);
@ -248,18 +202,45 @@ export class Parser {
} }
} }
private parseExpressionNoOperators(): Expression { private parsePrimitiveExpression(): Expression {
const t0 = this.peekToken(); const t0 = this.peekToken();
switch (t0.kind) { switch (t0.kind) {
case SyntaxKind.LParen: case SyntaxKind.LParen:
return this.parseExpressionWithParens(); return this.parseExpressionWithParens();
case SyntaxKind.Identifier: case SyntaxKind.Identifier:
return this.parseReferenceExpression(); return this.parseReferenceExpression();
case SyntaxKind.Constructor:
{
this.getToken();
const t1 = this.peekToken();
if (t1.kind === SyntaxKind.LBrace) {
this.getToken();
const fields = [];
let rparen;
for (;;) {
}
return new StructExpression(t0, t1, fields, rparen);
}
const elements = [];
for (;;) {
const t2 = this.peekToken();
if (t2.kind === SyntaxKind.LineFoldEnd
|| t2.kind === SyntaxKind.RParen
|| isBinaryOperatorLike(t2)
|| isPrefixOperatorLike(t2)) {
break;
}
elements.push(this.parseExpression());
}
return new NamedTupleExpression(t0, elements);
}
case SyntaxKind.Integer: case SyntaxKind.Integer:
case SyntaxKind.StringLiteral: case SyntaxKind.StringLiteral:
return this.parseConstantExpression(); return this.parseConstantExpression();
default: default:
this.raiseParseError(t0, [ this.raiseParseError(t0, [
SyntaxKind.NamedTupleExpression,
SyntaxKind.TupleExpression, SyntaxKind.TupleExpression,
SyntaxKind.NestedExpression, SyntaxKind.NestedExpression,
SyntaxKind.ConstantExpression, SyntaxKind.ConstantExpression,
@ -268,6 +249,25 @@ export class Parser {
} }
} }
private parseExpressionNoOperators(): Expression {
const func = this.parsePrimitiveExpression();
const args = [];
for (;;) {
const t1 = this.peekToken();
if (t1.kind === SyntaxKind.LineFoldEnd
|| t1.kind === SyntaxKind.RParen
|| isBinaryOperatorLike(t1)
|| isPrefixOperatorLike(t1)) {
break;
}
args.push(this.parsePrimitiveExpression());
}
if (args.length === 0) {
return func
}
return new CallExpression(func, args);
}
private parseUnaryExpression(): Expression { private parseUnaryExpression(): Expression {
let result = this.parseExpressionNoOperators() let result = this.parseExpressionNoOperators()
const prefixes = []; const prefixes = [];
@ -562,7 +562,7 @@ export class Parser {
const element = this.parseSourceFileElement(); const element = this.parseSourceFileElement();
elements.push(element); elements.push(element);
} }
return new SourceFile(elements, eof); return new SourceFile(this.file, elements, eof);
} }
} }

View file

@ -27,6 +27,7 @@ import {
CustomOperator, CustomOperator,
Constructor, Constructor,
Integer, Integer,
TextFile,
} from "./cst" } from "./cst"
import { Diagnostics, UnexpectedCharDiagnostic } from "./diagnostics" import { Diagnostics, UnexpectedCharDiagnostic } from "./diagnostics"
import { Stream, BufferedStream, assert } from "./util"; import { Stream, BufferedStream, assert } from "./util";
@ -74,6 +75,7 @@ export class Scanner extends BufferedStream<Token> {
public text: string, public text: string,
public textOffset: number = 0, public textOffset: number = 0,
public diagnostics: Diagnostics, public diagnostics: Diagnostics,
private file: TextFile,
) { ) {
super(); super();
} }
@ -157,8 +159,9 @@ export class Scanner extends BufferedStream<Token> {
let contents = ''; let contents = '';
let escaping = false; let escaping = false;
for (;;) { for (;;) {
const c1 = this.getChar();
if (escaping) { if (escaping) {
const startPos = this.getCurrentPosition();
const c1 = this.getChar();
switch (c1) { switch (c1) {
case 'a': contents += '\a'; break; case 'a': contents += '\a'; break;
case 'b': contents += '\b'; break; case 'b': contents += '\b'; break;
@ -171,11 +174,12 @@ export class Scanner extends BufferedStream<Token> {
case '\'': contents += '\''; break; case '\'': contents += '\''; break;
case '\"': contents += '\"'; break; case '\"': contents += '\"'; break;
default: default:
this.diagnostics.add(new UnexpectedCharDiagnostic(this.text, this.textOffset, c1)); this.diagnostics.add(new UnexpectedCharDiagnostic(this.file, startPos, c1));
throw new ScanError(); throw new ScanError();
} }
escaping = false; escaping = false;
} else { } else {
const c1 = this.getChar();
if (c1 === '"') { if (c1 === '"') {
break; break;
} else { } else {
@ -199,6 +203,7 @@ export class Scanner extends BufferedStream<Token> {
case '}': return new RBrace(startPos); case '}': return new RBrace(startPos);
case ',': return new Comma(startPos); case ',': return new Comma(startPos);
case ':': return new Colon(startPos); case ':': return new Colon(startPos);
case '.': return new Dot(startPos);
case '+': case '+':
case '-': case '-':
@ -331,7 +336,7 @@ export class Scanner extends BufferedStream<Token> {
default: default:
// Nothing matched, so the current character is unrecognisable // Nothing matched, so the current character is unrecognisable
this.diagnostics.add(new UnexpectedCharDiagnostic(this.text, this.textOffset, c0)); this.diagnostics.add(new UnexpectedCharDiagnostic(this.file, startPos, c0));
throw new ScanError(); throw new ScanError();
} }

View file

@ -5,6 +5,10 @@ export function assert(test: boolean): asserts test {
} }
} }
export function countDigits(x: number, base: number = 10) {
return x === 0 ? 1 : Math.ceil(Math.log(x+1) / Math.log(base))
}
export type JSONValue = null | boolean | number | string | JSONArray | JSONObject export type JSONValue = null | boolean | number | string | JSONArray | JSONObject
export type JSONArray = Array<JSONValue>; export type JSONArray = Array<JSONValue>;
export type JSONObject = { [key: string]: JSONValue }; export type JSONObject = { [key: string]: JSONValue };