diff --git a/compiler/package.json b/compiler/package.json index ee4ac01b3..f62c2c1cd 100644 --- a/compiler/package.json +++ b/compiler/package.json @@ -1,18 +1,14 @@ { - "name": "@samvv/bolt", + "name": "@boltlang/bolt", "version": "0.0.1", "description": "A new programming language for the web", "main": "lib/index.js", "bin": { - "bolt": "lib/bin/bolt.js", - "bolt-self": "lib/bin/bolt-self.js" - }, - "scripts": { - "test": "node ./lib/bin/bolt-self.js check" + "bolt": "lib/bin/bolt.js" }, "repository": { "type": "git", - "url": "https://github.com/BoltJS" + "url": "https://github.com/samvv/BoltJS" }, "keywords": [ "programming-language", diff --git a/compiler/src/bin/bolt.ts b/compiler/src/bin/bolt.ts index 97a03bbaf..24117892e 100644 --- a/compiler/src/bin/bolt.ts +++ b/compiler/src/bin/bolt.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node import "source-map-support/register" +import "reflect-metadata" import fs from "fs" import util from "util" diff --git a/compiler/src/checker.ts b/compiler/src/checker.ts index 5d34b52e3..cd4e7c292 100644 --- a/compiler/src/checker.ts +++ b/compiler/src/checker.ts @@ -4,7 +4,6 @@ import { ClassDeclaration, - EnumDeclaration, Expression, ExprOperator, Identifier, @@ -16,7 +15,6 @@ import { ReferenceExpression, ReferenceTypeExpression, SourceFile, - StructDeclaration, StructPattern, Syntax, SyntaxKind, @@ -27,544 +25,60 @@ import { describeType, BindingNotFoundDiagnostic, Diagnostics, - FieldNotFoundDiagnostic, - TypeMismatchDiagnostic, KindMismatchDiagnostic, ModuleNotFoundDiagnostic, TypeclassNotFoundDiagnostic, TypeclassDeclaredTwiceDiagnostic, } from "./diagnostics"; -import { assert, assertNever, first, isEmpty, last, MultiMap, toStringTag, InspectFn, JSONValue, ignore, deserializable } from "./util"; +import { assert, assertNever, isEmpty, MultiMap, toStringTag, InspectFn, implementationLimitation } from "./util"; import { Analyser } from "./analysis"; import { InspectOptions } from "util"; -import { deserialize } from "v8"; +import { ConstraintSolver } from "./solver"; +import { TypeKind, TApp, TArrow, TCon, TField, TNil, TNominal, TPresent, TTuple, TVar, TVSet, TVSub, Type } from "./types"; +import { CClass, CEmpty, CEqual, CMany, Constraint, ConstraintKind, ConstraintSet } from "./constraints"; -const MAX_TYPE_ERROR_COUNT = 5; +// export class Qual { -export enum TypeKind { - Arrow, - Var, - Con, - Tuple, - App, - Nominal, - Field, - Nil, - Absent, - Present, -} +// public constructor( +// public preds: Pred[], +// public type: Type, +// ) { -abstract class TypeBase { +// } - @ignore - public abstract readonly kind: TypeKind; +// public substitute(sub: TVSub): Qual { +// return new Qual( +// this.preds.map(pred => pred.substitute(sub)), +// this.type.substitute(sub), +// ); +// } - @ignore - public next: Type = this as any; +// public *getTypeVars() { +// for (const pred of this.preds) { +// yield* pred.type.getTypeVars(); +// } +// yield* this.type.getTypeVars(); +// } - public abstract node: Syntax | null; +// } - public static join(a: Type, b: Type): void { - const keep = a.next; - a.next = b; - b.next = keep; - } +// class IsInPred { - public abstract getTypeVars(): Iterable; +// public constructor( +// public id: string, +// public type: Type, +// ) { - public abstract shallowClone(): Type; +// } - public abstract substitute(sub: TVSub): Type; +// public substitute(sub: TVSub): Pred { +// return new IsInPred(this.id, this.type.substitute(sub)); - public hasTypeVar(tv: TVar): boolean { - for (const other of this.getTypeVars()) { - if (tv.id === other.id) { - return true; - } - } - return false; - } +// } - public abstract [toStringTag](depth: number, options: InspectOptions, inspect: InspectFn): string; +// } -} - -export function isType(value: any): value is Type { - return value !== undefined - && value !== null - && value instanceof TypeBase; -} - -@deserializable() -class TVar extends TypeBase { - - public readonly kind = TypeKind.Var; - - @ignore - public context = new Set(); - - public constructor( - public id: number, - public node: Syntax | null = null, - ) { - super(); - } - - public *getTypeVars(): Iterable { - 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 - ? this : other.substitute(sub); - } - - public [toStringTag]() { - return 'a' + this.id; - } - -} - -export class TNil extends TypeBase { - - public readonly kind = TypeKind.Nil; - - public constructor( - public node: Syntax | null = null - ) { - super(); - } - - public substitute(_sub: TVSub): Type { - return this; - } - - public shallowClone(): Type { - return new TNil(this.node); - } - - public *getTypeVars(): Iterable { - - } - - public [toStringTag]() { - return '∂Abs'; - } - -} - -@deserializable() -export class TAbsent extends TypeBase { - - public readonly kind = TypeKind.Absent; - - public constructor( - public node: Syntax | null = null, - ) { - super(); - } - - public substitute(_sub: TVSub): Type { - return this; - } - - public shallowClone(): Type { - return new TAbsent(this.node); - } - - public *getTypeVars(): Iterable { - - } - - public [toStringTag]() { - return 'Abs'; - } - -} - -@deserializable() -export class TPresent extends TypeBase { - - public readonly kind = TypeKind.Present; - - public constructor( - public type: Type, - public node: Syntax | null = null, - ) { - super(); - } - - public substitute(sub: TVSub): Type { - return new TPresent(this.type.substitute(sub), this.node); - } - - public getTypeVars(): Iterable { - return this.type.getTypeVars(); - } - - public shallowClone(): Type { - return new TPresent(this.type, this.node); - } - - public [toStringTag](_depth: number, options: InspectOptions, inspect: InspectFn) { - return 'Pre ' + inspect(this.type, options); - } - -} - -@deserializable() -export class TArrow extends TypeBase { - - public readonly kind = TypeKind.Arrow; - - public constructor( - public paramType: Type, - public returnType: Type, - public node: Syntax | null = null, - ) { - super(); - } - - public static build(paramTypes: Type[], returnType: Type, node: Syntax | null = null): Type { - let result = returnType; - for (let i = paramTypes.length-1; i >= 0; i--) { - result = new TArrow(paramTypes[i], result, node); - } - return result; - } - - public *getTypeVars(): Iterable { - yield* this.paramType.getTypeVars(); - yield* this.returnType.getTypeVars(); - } - - public shallowClone(): TArrow { - return new TArrow( - this.paramType, - this.returnType, - this.node, - ) - } - - public substitute(sub: TVSub): Type { - let changed = false; - const newParamType = this.paramType.substitute(sub); - if (newParamType !== this.paramType) { - changed = true; - } - const newReturnType = this.returnType.substitute(sub); - if (newReturnType !== this.returnType) { - changed = true; - } - return changed ? new TArrow(newParamType, newReturnType, this.node) : this; - } - - public [toStringTag](_depth: number, options: InspectOptions, inspect: InspectFn) { - return inspect(this.paramType, options) + ' -> ' + inspect(this.returnType, options); - } - -} - -@deserializable() -export class TCon extends TypeBase { - - public readonly kind = TypeKind.Con; - - public constructor( - public id: number, - public argTypes: Type[], - public displayName: string, - public node: Syntax | null = null, - ) { - super(); - } - - public *getTypeVars(): Iterable { - for (const argType of this.argTypes) { - yield* argType.getTypeVars(); - } - } - - public shallowClone(): TCon { - return new TCon( - this.id, - this.argTypes, - this.displayName, - this.node, - ); - } - - 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.node) : this; - } - - public [toStringTag](_depth: number, options: InspectOptions, inspect: InspectFn) { - return this.displayName + ' ' + this.argTypes.map(t => inspect(t, options)).join(' '); - } - -} - -@deserializable() -class TTuple extends TypeBase { - - public readonly kind = TypeKind.Tuple; - - public constructor( - public elementTypes: Type[], - public node: Syntax | null = null, - ) { - super(); - } - - public *getTypeVars(): Iterable { - for (const elementType of this.elementTypes) { - yield* elementType.getTypeVars(); - } - } - - public shallowClone(): TTuple { - return new TTuple( - this.elementTypes, - this.node, - ); - } - - 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.node) : this; - } - - public [toStringTag](_depth: number, options: InspectOptions, inspect: InspectFn) { - return this.elementTypes.map(t => inspect(t, options)).join(' × '); - } - -} - -@deserializable() -export class TField extends TypeBase { - - public readonly kind = TypeKind.Field; - - public constructor( - public name: string, - public type: Type, - public restType: Type, - public node: Syntax | null = null, - ) { - super(); - } - - public getTypeVars(): Iterable { - return this.type.getTypeVars(); - } - - public shallowClone(): TField { - return new TField( - this.name, - this.type, - this.restType, - this.node, - ); - } - - public static sort(type: Type): Type { - const fields = new Map(); - while (type.kind === TypeKind.Field) { - fields.set(type.name, type); - type = type.restType; - } - const keys = [...fields.keys()].sort().reverse(); - let out: Type = type; - for (const key of keys) { - const field = fields.get(key)!; - out = new TField(key, field.type, out, field.node); - } - return out - } - - public substitute(sub: TVSub): Type { - const newType = this.type.substitute(sub); - const newRestType = this.restType.substitute(sub); - return newType !== this.type || newRestType !== this.restType - ? new TField(this.name, newType, newRestType, this.node) : this; - } - - public [toStringTag](_depth: number, options: InspectOptions, inspect: InspectFn) { - let out = '{ ' + this.name + ': ' + inspect(this.type, options); - let type = this.restType; - while (type.kind === TypeKind.Field) { - out += '; ' + type.name + ': ' + inspect(type.type, options); - type = type.restType; - } - if (type.kind !== TypeKind.Nil) { - out += '; ' + inspect(type, options); - } - return out + ' }' - } - -} - -@deserializable() -export class TApp extends TypeBase { - - public readonly kind = TypeKind.App; - - public constructor( - public left: Type, - public right: Type, - public node: Syntax | null = null - ) { - super(); - } - - public static build(resultType: Type, types: Type[], node: Syntax | null = null): Type { - for (let i = 0; i < types.length; i++) { - resultType = new TApp(types[i], resultType, node); - } - return resultType; - } - - public *getTypeVars(): Iterable { - yield* this.left.getTypeVars(); - yield* this.right.getTypeVars(); - } - - public shallowClone() { - return new TApp( - this.left, - this.right, - this.node - ); - } - - public substitute(sub: TVSub): Type { - let changed = false; - const newOperatorType = this.left.substitute(sub); - if (newOperatorType !== this.left) { - changed = true; - } - const newArgType = this.right.substitute(sub); - if (newArgType !== this.right) { - changed = true; - } - return changed ? new TApp(newOperatorType, newArgType, this.node) : this; - } - - public [toStringTag](_depth: number, options: InspectOptions, inspect: InspectFn) { - return inspect(this.left, options) + ' ' + inspect(this.right, options); - } - -} - -@deserializable() -export class TNominal extends TypeBase { - - public readonly kind = TypeKind.Nominal; - - public constructor( - public decl: StructDeclaration | EnumDeclaration, - public node: Syntax | null = null, - ) { - super(); - } - - public *getTypeVars(): Iterable { - - } - - public shallowClone(): Type { - return new TNominal( - this.decl, - this.node, - ); - } - - public substitute(_sub: TVSub): Type { - return this; - } - - public [toStringTag]() { - return this.decl.name.text; - } - -} - -export type Type - = TCon - | TArrow - | TVar - | TTuple - | TApp - | TNominal - | TField - | TNil - | TPresent - | TAbsent - -export class Qual { - - public constructor( - public preds: Pred[], - public type: Type, - ) { - - } - - public substitute(sub: TVSub): Qual { - return new Qual( - this.preds.map(pred => pred.substitute(sub)), - this.type.substitute(sub), - ); - } - - public *getTypeVars() { - for (const pred of this.preds) { - yield* pred.type.getTypeVars(); - } - yield* this.type.getTypeVars(); - } - -} - -class IsInPred { - - public constructor( - public id: string, - public type: Type, - ) { - - } - - public substitute(sub: TVSub): Pred { - return new IsInPred(this.id, this.type.substitute(sub)); - - } - -} - -type Pred = IsInPred; +// type Pred = IsInPred; export const enum KindType { Star, @@ -674,219 +188,6 @@ export type Kind | KVar | KRow -class TVSet { - - private mapping = new Map(); - - public constructor(iterable?: Iterable) { - if (iterable !== undefined) { - for (const tv of iterable) { - this.add(tv); - } - } - } - - public add(tv: TVar): void { - this.mapping.set(tv.id, tv); - } - - public has(tv: TVar): boolean { - return this.mapping.has(tv.id); - } - - public intersectsType(type: Type): boolean { - for (const tv of type.getTypeVars()) { - if (this.has(tv)) { - return true; - } - } - return false; - } - - public delete(tv: TVar): void { - this.mapping.delete(tv.id); - } - - public get size(): number { - return this.mapping.size; - } - - public [Symbol.iterator](): Iterator { - return this.mapping.values(); - } - - public [toStringTag](_depth: number, options: InspectOptions, inspect: InspectFn) { - let out = '{ '; - let first = true; - for (const tv of this) { - if (first) first = false; - else out += ', '; - out += inspect(tv, options); - } - return out + ' }'; - } - -} - -class TVSub { - - private mapping = new Map(); - - 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 { - return this.mapping.values(); - } - -} - -const enum ConstraintKind { - Equal, - Many, - Empty, -} - -abstract class ConstraintBase { - - public constructor( - public node: Syntax | null = null - ) { - - } - - public prevInstantiation: Constraint | null = null; - - public *getNodes(): Iterable { - let curr: Constraint | null = this as any; - while (curr !== null) { - if (curr.node !== null) { - yield curr.node; - } - curr = curr.prevInstantiation; - } - } - - public get lastNode(): Syntax | null { - return last(this.getNodes()[Symbol.iterator]()) ?? null; - } - - public get firstNode(): Syntax | null { - return first(this.getNodes()[Symbol.iterator]()) ?? null; - } - - public abstract freeTypeVars(): Iterable; - - 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 | null, - ) { - super(); - } - - public substitute(sub: TVSub): CEqual { - return new CEqual( - this.left.substitute(sub), - this.right.substitute(sub), - this.node, - ); - } - - public *freeTypeVars(): Iterable { - yield* this.left.getTypeVars(); - yield* this.right.getTypeVars(); - } - - public [toStringTag](_currentDepth: number, options: InspectOptions, inspect: InspectFn): string { - return inspect(this.left, options) + ' ~ ' + inspect(this.right, options); - } - -} - -class CMany extends ConstraintBase { - - public readonly kind = ConstraintKind.Many; - - public constructor( - public elements: Constraint[] - ) { - super(); - } - - public substitute(sub: TVSub): CMany { - const newElements = []; - for (const element of this.elements) { - newElements.push(element.substitute(sub)); - } - return new CMany(newElements); - } - - public *freeTypeVars(): Iterable { - for (const element of this.elements) { - yield* element.freeTypeVars(); - } - } - - public [toStringTag](currentDepth: number, { depth = 2, ...options }: InspectOptions, inspect: InspectFn): string { - if (this.elements.length === 0) { - return '[]'; - } - let out = '[\n'; - const newOptions = { ...options, depth: depth === null ? null : depth - 1 }; - out += this.elements.map(constraint => ' ' + inspect(constraint, newOptions)).join('\n'); - out += '\n]'; - return out; - } - -} - -class CEmpty extends ConstraintBase { - - public readonly kind = ConstraintKind.Empty; - - public substitute(_sub: TVSub): Constraint { - return this; - } - - public *freeTypeVars(): Iterable { - - } - - public [toStringTag]() { - return 'ε'; - } - -} - -type Constraint - = CEqual - | CMany - | CEmpty - -class ConstraintSet extends Array { -} abstract class SchemeBase { } @@ -1308,11 +609,14 @@ export class Checker { return new CMany(newConstraints); case ConstraintKind.Empty: return constraint; + case ConstraintKind.Class: case ConstraintKind.Equal: const newConstraint = constraint.substitute(sub); newConstraint.node = node; newConstraint.prevInstantiation = constraint; return newConstraint; + default: + assertNever(constraint); } } this.addConstraint(transform(scheme.constraint)); @@ -1361,6 +665,20 @@ export class Checker { break; } + case SyntaxKind.ForallTypeExpression: + { + // TODO we currently automatically introduce type variables but maybe we should use the Forall? + kind = this.inferKindFromTypeExpression(node.typeExpr, env); + break; + } + + case SyntaxKind.TypeExpressionWithConstraints: + { + // TODO check if we need to kind node.constraints + kind = this.inferKindFromTypeExpression(node.typeExpr, env); + break; + } + case SyntaxKind.VarTypeExpression: { const matchedKind = this.lookupKind(env, node.name, false); @@ -1732,7 +1050,7 @@ export class Checker { break; } const ctx = this.getContext(); - const constraints: ConstraintSet = []; + const constraints = new ConstraintSet; const innerCtx: InferContext = { ...ctx, constraints, @@ -2017,6 +1335,7 @@ export class Checker { this.diagnostics.add(new BindingNotFoundDiagnostic([], node.name.text, node.name)); } type = this.createTypeVar(); + // TODO if !introduceTypeVars: re-emit a 'var not found' whenever the same var is encountered this.addBinding(node.name.text, Forall.mono(type), Symkind.Type); } else { assert(isEmpty(scheme.typeVars)); @@ -2035,6 +1354,27 @@ export class Checker { break; } + case SyntaxKind.TypeExpressionWithConstraints: + { + for (const constraint of node.constraints) { + implementationLimitation(constraint.types.length === 1); + this.addConstraint(new CClass(constraint.name.text, this.inferTypeExpression(constraint.types[0]), constraint.name)); + } + return this.inferTypeExpression(node.typeExpr, introduceTypeVars); + } + + case SyntaxKind.ForallTypeExpression: + { + const ctx = this.getContext(); + // FIXME this is an ugly hack that doesn't even work. Either disallow Forall in this method or create a new TForall + for (const varExpr of node.varTypeExps) { + const tv = this.createTypeVar(); + ctx.env.add(varExpr.name.text, Forall.mono(tv), Symkind.Type); + ctx.typeVars.add(tv); + } + return this.inferTypeExpression(node.typeExpr, introduceTypeVars); + } + case SyntaxKind.ArrowTypeExpression: { const paramTypes = []; @@ -2200,7 +1540,7 @@ export class Checker { if (node.constraintClause !== null) { for (const constraint of node.constraintClause.constraints) { if (!this.classDecls.has(constraint.name.text)) { - this.diagnostics.add(new TypeclassNotFoundDiagnostic(constraint.name)); + this.diagnostics.add(new TypeclassNotFoundDiagnostic(constraint.name.text, constraint.name)); } } } @@ -2220,7 +1560,7 @@ export class Checker { case SyntaxKind.InstanceDeclaration: { if (!this.classDecls.has(node.name.text)) { - this.diagnostics.add(new TypeclassNotFoundDiagnostic(node.name)); + this.diagnostics.add(new TypeclassNotFoundDiagnostic(node.name.text, node.name)); } const env = node.typeEnv = new TypeEnv(parentEnv); for (const element of node.elements) { @@ -2548,7 +1888,11 @@ export class Checker { this.contexts.pop(); this.popContext(context); - this.solve(new CMany(constraints), this.solution); + const solver = new ConstraintSolver(this.diagnostics, this.nextTypeVarId); + + solver.solve(new CMany(constraints)); + + this.solution = solver.solution; } @@ -2556,294 +1900,34 @@ export class Checker { return this.classDecls.get(name) ?? null; } - private *findInstanceContext(type: TCon, clazz: ClassDeclaration): Iterable { - for (const instance of clazz.getInstances()) { - assert(instance.types.length === 1); - const instTy0 = instance.types[0]; - if ((instTy0.kind === SyntaxKind.AppTypeExpression - && instTy0.operator.kind === SyntaxKind.ReferenceTypeExpression - && instTy0.operator.name.text === type.displayName) - || (instTy0.kind === SyntaxKind.ReferenceTypeExpression - && instTy0.name.text === type.displayName)) { - if (instance.constraintClause === null) { - return; - } - for (const argType of type.argTypes) { - const classes = []; - for (const constraint of instance.constraintClause.constraints) { - assert(constraint.types.length === 1); - const classDecl = this.lookupClass(constraint.name.text); - if (classDecl === null) { - this.diagnostics.add(new TypeclassNotFoundDiagnostic(constraint.name)); - } else { - classes.push(classDecl); - } - } - yield classes; - } - } - } - } - - private solve(constraint: Constraint, solution: TVSub): void { - - const queue = [ constraint ]; - - let errorCount = 0; - - const find = (type: Type): Type => { - while (type.kind === TypeKind.Var && solution.has(type)) { - type = solution.get(type)!; - } - return type; - } - - while (queue.length > 0) { - - const constraint = queue.shift()!; - - switch (constraint.kind) { - - case ConstraintKind.Many: - { - for (const element of constraint.elements) { - queue.push(element); - } - break; - } - - case ConstraintKind.Equal: - { - let path: string[] = []; - - const unifyField = (left: Type, right: Type): boolean => { - - const swap = () => { [right, left] = [left, right]; } - - if (left.kind === TypeKind.Absent && right.kind === TypeKind.Absent) { - return true; - } - - if (right.kind === TypeKind.Absent) { - swap(); - } - - if (left.kind === TypeKind.Absent) { - assert(right.kind === TypeKind.Present); - const fieldName = path[path.length-1]; - this.diagnostics.add( - new FieldNotFoundDiagnostic(fieldName, left.node, right.type.node, constraint.firstNode) - ); - return false; - } - - assert(left.kind === TypeKind.Present && right.kind === TypeKind.Present); - return unify(left.type, right.type); - } - - const unifyPred = (left: Pred, right: Pred) => { - if (left.id === right.id) { - return unify(left.type, right.type); - } - throw new Error(`Classes do not match and no diagnostic defined`); - } - - const unify = (left: Type, right: Type): boolean => { - - left = find(left); - right = find(right); - - // console.log(`unify ${describeType(left)} @ ${left.node && left.node.constructor && left.node.constructor.name} ~ ${describeType(right)} @ ${right.node && right.node.constructor && right.node.constructor.name}`); - - const swap = () => { [right, left] = [left, right]; } - - if (left.kind !== TypeKind.Var && right.kind === TypeKind.Var) { - swap(); - } - - if (left.kind === TypeKind.Var) { - - // Perform an occurs check, verifying whether left occurs - // somewhere inside the structure of right. If so, unification - // makes no sense. - if (right.hasTypeVar(left)) { - // TODO print a diagnostic - return false; - } - - // We are ready to join the types, so the first thing we do is - // propagating the type classes that 'left' requires to 'right'. - // If 'right' is another type variable, we're lucky. We just copy - // the missing type classes from 'left' to 'right'. Otherwise, - const propagateClasses = (classes: Iterable, type: Type) => { - if (type.kind === TypeKind.Var) { - for (const constraint of classes) { - type.context.add(constraint); - } - } else if (type.kind === TypeKind.Con) { - for (const constraint of classes) { - propagateClassTCon(constraint, type); - } - } else { - //assert(false); - //this.diagnostics.add(new ); - } - } - - const propagateClassTCon = (clazz: ClassDeclaration, type: TCon) => { - const s = this.findInstanceContext(type, clazz); - let i = 0; - for (const classes of s) { - propagateClasses(classes, type.argTypes[i++]); - } - } - - propagateClasses(left.context, right); - - // We are all clear; set the actual type of left to right. - solution.set(left, right); - - // These types will be join, and we'd like to track that - // into a special chain. - TypeBase.join(left, right); - - // if (left.node !== null) { - // right.node = left.node; - // } - - return true; - } - - if (left.kind === TypeKind.Arrow && right.kind === TypeKind.Arrow) { - let success = true; - if (!unify(left.paramType, right.paramType)) { - success = false; - } - if (!unify(left.returnType, right.returnType)) { - success = false; - } - if (success) { - TypeBase.join(left, right); - } - return success; - } - - if (left.kind === TypeKind.Tuple && right.kind === TypeKind.Tuple) { - if (left.elementTypes.length === right.elementTypes.length) { - let success = false; - const count = left.elementTypes.length; - for (let i = 0; i < count; i++) { - if (!unify(left.elementTypes[i], right.elementTypes[i])) { - success = false; - } - } - if (success) { - TypeBase.join(left, right); - } - return success; - } - } - - if (left.kind === TypeKind.Con && right.kind === TypeKind.Con) { - if (left.id === right.id) { - assert(left.argTypes.length === right.argTypes.length); - const count = left.argTypes.length; - let success = true; - for (let i = 0; i < count; i++) { - if (!unify(left.argTypes[i], right.argTypes[i])) { - success = false; - } - } - if (success) { - TypeBase.join(left, right); - } - return success; - } - } - - if (left.kind === TypeKind.Nil && right.kind === TypeKind.Nil) { - return true; - } - - if (left.kind === TypeKind.Field && right.kind === TypeKind.Field) { - if (left.name === right.name) { - let success = true; - path.push(left.name); - if (!unifyField(left.type, right.type)) { - success = false; - } - path.pop(); - if (!unify(left.restType, right.restType)) { - success = false; - } - return success; - } - let success = true; - const newRestType = new TVar(this.nextTypeVarId++); - if (!unify(left.restType, new TField(right.name, right.type, newRestType))) { - success = false; - } - if (!unify(right.restType, new TField(left.name, left.type, newRestType))) { - success = false; - } - return success; - } - - if (left.kind === TypeKind.Nil && right.kind === TypeKind.Field) { - swap(); - } - - if (left.kind === TypeKind.Field && right.kind === TypeKind.Nil) { - let success = true; - path.push(left.name); - if (!unifyField(left.type, new TAbsent(right.node))) { - success = false; - } - path.pop(); - if (!unify(left.restType, right)) { - success = false; - } - return success - } - - if (left.kind === TypeKind.Nominal && right.kind === TypeKind.Nominal) { - if (left.decl === right.decl) { - return true; - } - // fall through to error reporting - } - - if (left.kind === TypeKind.App && right.kind === TypeKind.App) { - return unify(left.left, right.left) - && unify(left.right, right.right); - } - - this.diagnostics.add( - new TypeMismatchDiagnostic( - left.substitute(solution), - right.substitute(solution), - [...constraint.getNodes()], - path, - ) - ); - return false; - } - - if (!unify(constraint.left, constraint.right)) { - errorCount++; - if (errorCount === MAX_TYPE_ERROR_COUNT) { - return; - } - } - - break; - } - - } - - } - - } + // private *findInstanceContext(type: TCon, clazz: ClassDeclaration): Iterable { + // for (const instance of clazz.getInstances()) { + // assert(instance.types.length === 1); + // const instTy0 = instance.types[0]; + // if ((instTy0.kind === SyntaxKind.AppTypeExpression + // && instTy0.operator.kind === SyntaxKind.ReferenceTypeExpression + // && instTy0.operator.name.text === type.displayName) + // || (instTy0.kind === SyntaxKind.ReferenceTypeExpression + // && instTy0.name.text === type.displayName)) { + // if (instance.constraintClause === null) { + // return; + // } + // for (const argType of type.argTypes) { + // const classes = []; + // for (const constraint of instance.constraintClause.constraints) { + // assert(constraint.types.length === 1); + // const classDecl = this.lookupClass(constraint.name.text); + // if (classDecl === null) { + // this.diagnostics.add(new TypeclassNotFoundDiagnostic(constraint.name)); + // } else { + // classes.push(classDecl); + // } + // } + // yield classes; + // } + // } + // } + // } } diff --git a/compiler/src/constraints.ts b/compiler/src/constraints.ts new file mode 100644 index 000000000..4426d0407 --- /dev/null +++ b/compiler/src/constraints.ts @@ -0,0 +1,168 @@ + +import { InspectOptions } from "util"; +import { Syntax } from "./cst" +import { TVar, TVSub, Type } from "./types"; +import { first, InspectFn, last, toStringTag } from "./util"; + +export const enum ConstraintKind { + Equal, + Many, + Empty, + Class, +} + +abstract class ConstraintBase { + + public constructor( + public node: Syntax | null = null + ) { + + } + + public prevInstantiation: Constraint | null = null; + + public *getNodes(): Iterable { + let curr: Constraint | null = this as any; + while (curr !== null) { + if (curr.node !== null) { + yield curr.node; + } + curr = curr.prevInstantiation; + } + } + + public get lastNode(): Syntax | null { + return last(this.getNodes()[Symbol.iterator]()) ?? null; + } + + public get firstNode(): Syntax | null { + return first(this.getNodes()[Symbol.iterator]()) ?? null; + } + + public abstract freeTypeVars(): Iterable; + + public abstract substitute(sub: TVSub): Constraint; + +} + +export class CEqual extends ConstraintBase { + + public readonly kind = ConstraintKind.Equal; + + public constructor( + public left: Type, + public right: Type, + public node: Syntax | null, + ) { + super(); + } + + public substitute(sub: TVSub): CEqual { + return new CEqual( + this.left.substitute(sub), + this.right.substitute(sub), + this.node, + ); + } + + public *freeTypeVars(): Iterable { + yield* this.left.getTypeVars(); + yield* this.right.getTypeVars(); + } + + public [toStringTag](_currentDepth: number, options: InspectOptions, inspect: InspectFn): string { + return inspect(this.left, options) + ' ~ ' + inspect(this.right, options); + } + +} + +export class CMany extends ConstraintBase { + + public readonly kind = ConstraintKind.Many; + + public constructor( + public elements: Constraint[] + ) { + super(); + } + + public substitute(sub: TVSub): CMany { + const newElements = []; + for (const element of this.elements) { + newElements.push(element.substitute(sub)); + } + return new CMany(newElements); + } + + public *freeTypeVars(): Iterable { + for (const element of this.elements) { + yield* element.freeTypeVars(); + } + } + + public [toStringTag](currentDepth: number, { depth = 2, ...options }: InspectOptions, inspect: InspectFn): string { + if (this.elements.length === 0) { + return '[]'; + } + let out = '[\n'; + const newOptions = { ...options, depth: depth === null ? null : depth - 1 }; + out += this.elements.map(constraint => ' ' + inspect(constraint, newOptions)).join('\n'); + out += '\n]'; + return out; + } + +} + +export class CClass extends ConstraintBase { + + public readonly kind = ConstraintKind.Class; + + public constructor( + public className: string, + public type: Type, + public node: Syntax | null = null, + ) { + super(); + } + + public substitute(sub: TVSub): CClass { + return new CClass(this.className, this.type.substitute(sub)); + } + + public freeTypeVars(): Iterable { + return this.type.getTypeVars(); + } + + public [toStringTag](_depth: number, options: InspectOptions, inspect: InspectFn) { + return this.className + ' ' + inspect(this.type, options); + } + +} + +export class CEmpty extends ConstraintBase { + + public readonly kind = ConstraintKind.Empty; + + public substitute(_sub: TVSub): CEmpty { + return this; + } + + public *freeTypeVars(): Iterable { + + } + + public [toStringTag]() { + return 'ε'; + } + +} + +export type Constraint + = CEqual + | CMany + | CEmpty + | CClass + +export class ConstraintSet extends Array { + +} diff --git a/compiler/src/cst.ts b/compiler/src/cst.ts index dddbfbbe0..7d9dd659c 100644 --- a/compiler/src/cst.ts +++ b/compiler/src/cst.ts @@ -4,7 +4,8 @@ import path from "path" import { assert, deserializable, implementationLimitation, IndentWriter, JSONObject, JSONValue, nonenumerable, unreachable } from "./util"; import { isNodeWithScope, Scope } from "./scope" -import { InferContext, Kind, KindEnv, Scheme, Type, TypeEnv } from "./checker" +import type { InferContext, Kind, KindEnv, Scheme, TypeEnv } from "./checker" +import type { Type } from "./types"; import { Emitter } from "./emitter"; export type TextSpan = [number, number]; @@ -121,6 +122,7 @@ export const enum SyntaxKind { IfKeyword, ElifKeyword, ElseKeyword, + ForallKeyword, LineFoldEnd, BlockEnd, BlockStart, @@ -133,6 +135,8 @@ export const enum SyntaxKind { AppTypeExpression, NestedTypeExpression, TupleTypeExpression, + ForallTypeExpression, + TypeExpressionWithConstraints, // Patterns NamedPattern, @@ -1201,6 +1205,21 @@ export class VBar extends TokenBase { } +@deserializable() +export class ForallKeyword extends TokenBase { + + public readonly kind = SyntaxKind.ForallKeyword; + + public get text(): string { + return 'forall'; + } + + public clone(): ForallKeyword { + return new ForallKeyword(this.startPos); + } + +} + export type Token = RArrow | RArrowAlt @@ -1243,10 +1262,78 @@ export type Token | ElifKeyword | EnumKeyword | ForeignKeyword + | ForallKeyword export type TokenKind = Token['kind'] +@deserializable() +export class ForallTypeExpression extends SyntaxBase { + + public readonly kind = SyntaxKind.ForallTypeExpression; + + public constructor( + public forallKeyword: ForallKeyword, + public varTypeExps: VarTypeExpression[], + public dot: Dot, + public typeExpr: TypeExpression, + ) { + super(); + } + + public clone(): ForallTypeExpression { + return new ForallTypeExpression( + this.forallKeyword.clone(), + this.varTypeExps.map(e => e.clone()), + this.dot.clone(), + this.typeExpr.clone(), + ); + } + + public getFirstToken(): Token { + return this.forallKeyword; + } + + public getLastToken(): Token { + return this.typeExpr.getLastToken(); + } + +} + +@deserializable() +export class TypeExpressionWithConstraints extends SyntaxBase { + + public readonly kind = SyntaxKind.TypeExpressionWithConstraints; + + public constructor( + public constraints: ClassConstraint[], + public rarrowAlt: RArrowAlt, + public typeExpr: TypeExpression, + ) { + super(); + } + + public clone(): TypeExpressionWithConstraints { + return new TypeExpressionWithConstraints( + this.constraints.map(c => c.clone()), + this.rarrowAlt.clone(), + this.typeExpr.clone(), + ); + } + + public getFirstToken(): Token { + if (this.constraints.length > 0) { + return this.constraints[0].getFirstToken(); + } + return this.rarrowAlt; + } + + public getLastToken(): Token { + return this.typeExpr.getLastToken(); + } + +} + @deserializable() export class ArrowTypeExpression extends SyntaxBase { @@ -1437,6 +1524,8 @@ export type TypeExpression | AppTypeExpression | NestedTypeExpression | TupleTypeExpression + | ForallTypeExpression + | TypeExpressionWithConstraints @deserializable() export class NamedPattern extends SyntaxBase { diff --git a/compiler/src/diagnostics.ts b/compiler/src/diagnostics.ts index 744720d1b..c0ce1567a 100644 --- a/compiler/src/diagnostics.ts +++ b/compiler/src/diagnostics.ts @@ -1,5 +1,6 @@ -import { TypeKind, type Type, Kind, KindType } from "./checker"; +import { Kind, KindType } from "./checker"; +import { type Type, TypeKind } from "./types" import { ClassConstraint, ClassDeclaration, IdentifierAlt, InstanceDeclaration, Syntax, SyntaxKind, TextFile, TextPosition, TextRange, Token } from "./cst"; import { assertNever, countDigits, deserializable, IndentWriter } from "./util"; @@ -42,6 +43,7 @@ const enum DiagnosticKind { TypeMismatch, TypeclassNotFound, TypeclassDecaredTwice, + TypeclassNotImplemented, BindingNotFound, ModuleNotFound, FieldNotFound, @@ -114,7 +116,8 @@ export class TypeclassNotFoundDiagnostic extends DiagnosticBase { public level = Level.Error; public constructor( - public name: IdentifierAlt, + public name: string, + public node: Syntax | null = null, public origin: InstanceDeclaration | ClassConstraint | null = null, ) { super(); @@ -122,6 +125,23 @@ export class TypeclassNotFoundDiagnostic extends DiagnosticBase { } +@deserializable() +export class TypeclassNotImplementedDiagnostic extends DiagnosticBase { + + public readonly kind = DiagnosticKind.TypeclassNotImplemented; + + public level = Level.Error; + + public constructor( + public name: string, + public type: Type, + public node: Syntax | null = null, + ) { + super(); + } + +} + @deserializable() export class BindingNotFoundDiagnostic extends DiagnosticBase { @@ -212,6 +232,7 @@ export type Diagnostic = UnexpectedCharDiagnostic | TypeclassNotFoundDiagnostic | TypeclassDeclaredTwiceDiagnostic + | TypeclassNotImplementedDiagnostic | BindingNotFoundDiagnostic | TypeMismatchDiagnostic | UnexpectedTokenDiagnostic @@ -304,15 +325,17 @@ export class ConsoleDiagnostics implements Diagnostics { break; case DiagnosticKind.TypeclassNotFound: - this.writer.write(`the type class ${ANSI_FG_MAGENTA + diagnostic.name.text + ANSI_RESET} was not found.\n\n`); - this.writer.write(printNode(diagnostic.name) + '\n'); - if (diagnostic.origin !== null) { - this.writer.indent(); - this.writer.write(ANSI_FG_YELLOW + ANSI_BOLD + 'info: ' + ANSI_RESET); - this.writer.write(`${ANSI_FG_MAGENTA + diagnostic.name.text + ANSI_RESET} is required by ${ANSI_FG_MAGENTA + diagnostic.origin.name.text + ANSI_RESET}\n\n`); - this.writer.write(printNode(diagnostic.origin.name) + '\n'); - this.writer.dedent(); + this.writer.write(`the type class ${ANSI_FG_MAGENTA + diagnostic.name + ANSI_RESET} was not found.\n\n`); + if (diagnostic.node !== null) { + this.writer.write(printNode(diagnostic.node) + '\n'); } + // if (diagnostic.origin !== null) { + // this.writer.indent(); + // this.writer.write(ANSI_FG_YELLOW + ANSI_BOLD + 'info: ' + ANSI_RESET); + // this.writer.write(`${ANSI_FG_MAGENTA + diagnostic.name + ANSI_RESET} is required by ${ANSI_FG_MAGENTA + diagnostic.origin.name.text + ANSI_RESET}\n\n`); + // this.writer.write(printNode(diagnostic.origin.name) + '\n'); + // this.writer.dedent(); + // } break; case DiagnosticKind.BindingNotFound: diff --git a/compiler/src/parser.ts b/compiler/src/parser.ts index 11b4cf749..1ebaced65 100644 --- a/compiler/src/parser.ts +++ b/compiler/src/parser.ts @@ -44,7 +44,6 @@ import { IfStatement, MemberExpression, IdentifierAlt, - WrappedOperator, ArrowTypeExpression, EnumDeclarationStructElement, EnumDeclaration, @@ -67,6 +66,8 @@ import { InstanceDeclaration, ClassConstraintClause, AssignStatement, + ForallTypeExpression, + TypeExpressionWithConstraints, } from "./cst" import { Stream } from "./util"; @@ -253,7 +254,57 @@ export class Parser { return new AppTypeExpression(operator, args); } + public parseTypeExpressionWithConstraints(): TypeExpression { + if (!this.lookaheadHasClassConstraints()) { + return this.parseArrowTypeExpression(); + } + const constraints = []; + let rarrowAlt; + for (;;) { + const constraint = this.parseClassConstraint(); + constraints.push(constraint); + const t1 = this.getToken(); + if (t1.kind === SyntaxKind.RArrowAlt) { + rarrowAlt = t1; + break; + } else if (t1.kind !== SyntaxKind.Comma) { + this.raiseParseError(t1, [ SyntaxKind.RArrowAlt, SyntaxKind.Comma ]); + } + } + const type = this.parseArrowTypeExpression(); + return new TypeExpressionWithConstraints(constraints, rarrowAlt, type); + } + public parseTypeExpression(): TypeExpression { + const t0 = this.peekToken(); + switch (t0.kind) { + case SyntaxKind.ForallKeyword: + { + this.getToken(); + let dot; + const typeVarExps = []; + for (;;) { + const t1 = this.peekToken(); + if (t1.kind === SyntaxKind.Dot) { + dot = t1; + this.getToken(); + break; + } + typeVarExps.push(this.parseVarTypeExpression()); + } + return new ForallTypeExpression( + t0, + typeVarExps, + dot, + this.parseTypeExpression(), + ); + } + default: + return this.parseTypeExpressionWithConstraints(); + } + } + + public parseArrowTypeExpression(): TypeExpression { let returnType = this.parseAppTypeExpressionOrBelow(); const paramTypes = []; for (;;) { @@ -822,6 +873,20 @@ export class Parser { } } + private lookaheadHasClassConstraints(): boolean { + for (let i = 1;; i++) { + const token = this.peekToken(i); + switch (token.kind) { + case SyntaxKind.RArrowAlt: + return true; + case SyntaxKind.BlockStart: + case SyntaxKind.LineFoldEnd: + case SyntaxKind.Equals: + return false; + } + } + } + public parseLetDeclaration(): LetDeclaration { let t0 = this.getToken(); let pubKeyword = null; diff --git a/compiler/src/passes/BoltToC.ts b/compiler/src/passes/BoltToC.ts index a3239f607..bbe410e00 100644 --- a/compiler/src/passes/BoltToC.ts +++ b/compiler/src/passes/BoltToC.ts @@ -1,7 +1,7 @@ import { CBuiltinType, CBuiltinTypeKind, CCallExpr, CConstExpr, CDecl, CDir, CExpr, CExprStmt, CFuncDecl, CIncDir, CNode, CProgram, CRefExpr, CStmt } from "../c"; import { Expression, Syntax, SyntaxKind } from "../cst"; -import { Pass } from "../types"; +import type { Pass } from "../program"; import { assert } from "../util"; interface Context { diff --git a/compiler/src/passes/BoltToJS.ts b/compiler/src/passes/BoltToJS.ts index 8b6d7be47..f322d6184 100644 --- a/compiler/src/passes/BoltToJS.ts +++ b/compiler/src/passes/BoltToJS.ts @@ -1,6 +1,6 @@ import { Syntax } from "../cst"; import { JSNode, JSProgram } from "../js"; -import { Pass } from "../types"; +import type { Pass } from "../program"; export class BoltToJS implements Pass { diff --git a/compiler/src/passes/TypeclassDictPass.ts b/compiler/src/passes/TypeclassDictPass.ts index c0f3cf157..b704456be 100644 --- a/compiler/src/passes/TypeclassDictPass.ts +++ b/compiler/src/passes/TypeclassDictPass.ts @@ -19,7 +19,7 @@ import { canHaveInstanceDeclaration, vistEachChild } from "../cst"; -import { Pass } from "../types"; +import { Pass } from "../program"; import { assert } from "../util"; function encode(typeExpr: TypeExpression): string { @@ -58,7 +58,7 @@ export class TypeclassDictPassing implements Pass { new LetKeyword(), null, null, - new NamedPattern(new Identifier(this.mangleInstance(node))), + new NamedPattern(new Identifier(null, this.mangleInstance(node))), [], null, // TODO new ExprBody( @@ -69,7 +69,7 @@ export class TypeclassDictPassing implements Pass { assert(element.kind === SyntaxKind.LetDeclaration); assert(element.pattern.kind === SyntaxKind.NamedPattern); return new StructExpressionField( - new Identifier(element.pattern.name.text), + new Identifier(null, element.pattern.name.text), new Equals(), new FunctionExpression(new Backslash(), element.params, element.body!) ); diff --git a/compiler/src/program.ts b/compiler/src/program.ts index 165f375e2..645fff343 100644 --- a/compiler/src/program.ts +++ b/compiler/src/program.ts @@ -5,7 +5,14 @@ import { SourceFile, TextFile } from "./cst"; import { ConsoleDiagnostics, Diagnostics } from "./diagnostics"; import { Checker } from "./checker"; import { Analyser } from "./analysis"; -import { Newable, Pass } from "./types"; + +export interface Pass { + apply(input: In): Out; +} + +export interface Newable { + new (...args: any[]): T; +} type AnyPass = Pass; diff --git a/compiler/src/scanner.ts b/compiler/src/scanner.ts index 58ce79dc4..0d2e172bc 100644 --- a/compiler/src/scanner.ts +++ b/compiler/src/scanner.ts @@ -45,6 +45,7 @@ import { ClassKeyword, InstanceKeyword, Backslash, + ForallKeyword, } from "./cst" import { Diagnostics } from "./diagnostics" import { Stream, BufferedStream, assert } from "./util"; @@ -383,6 +384,7 @@ export class Scanner extends BufferedStream { case 'match': return new MatchKeyword(startPos); case 'foreign': return new ForeignKeyword(startPos); case 'mod': return new ModKeyword(startPos); + case 'forall': return new ForallKeyword(startPos); default: if (isUpper(text[0])) { return new IdentifierAlt(startPos, text); diff --git a/compiler/src/solver.ts b/compiler/src/solver.ts new file mode 100644 index 000000000..94f4197a5 --- /dev/null +++ b/compiler/src/solver.ts @@ -0,0 +1,312 @@ +import { Constraint, ConstraintKind } from "./constraints"; +import { Diagnostics, FieldNotFoundDiagnostic, TypeclassNotFoundDiagnostic, TypeclassNotImplementedDiagnostic, TypeMismatchDiagnostic } from "./diagnostics"; +import { TAbsent, TField, TVar, TVSub, Type, TypeBase, TypeKind } from "./types"; +import { assert } from "./util"; + +export class ConstraintSolver { + + private path: string[] = []; + private constraint: Constraint | null = null; + private maxTypeErrorCount = 5; + + public solution = new TVSub; + + public constructor( + public diagnostics: Diagnostics, + private nextTypeVarId: number, + ) { + + } + + private find(type: Type): Type { + while (type.kind === TypeKind.Var && this.solution.has(type)) { + type = this.solution.get(type)!; + } + return type; + } + + private unifyField(left: Type, right: Type, enableDiagnostics: boolean): boolean { + + const swap = () => { [right, left] = [left, right]; } + + if (left.kind === TypeKind.Absent && right.kind === TypeKind.Absent) { + return true; + } + + if (right.kind === TypeKind.Absent) { + swap(); + } + + if (left.kind === TypeKind.Absent) { + assert(right.kind === TypeKind.Present); + const fieldName = this.path[this.path.length-1]; + if (enableDiagnostics) { + this.diagnostics.add( + new FieldNotFoundDiagnostic(fieldName, left.node, right.type.node, this.constraint!.firstNode) + ); + } + return false; + } + + assert(left.kind === TypeKind.Present && right.kind === TypeKind.Present); + return this.unify(left.type, right.type, enableDiagnostics); + } + + + private unify(left: Type, right: Type, enableDiagnostics: boolean): boolean { + + left = this.find(left); + right = this.find(right); + + // console.log(`unify ${describeType(left)} @ ${left.node && left.node.constructor && left.node.constructor.name} ~ ${describeType(right)} @ ${right.node && right.node.constructor && right.node.constructor.name}`); + + const swap = () => { [right, left] = [left, right]; } + + if (left.kind !== TypeKind.Var && right.kind === TypeKind.Var) { + swap(); + } + + if (left.kind === TypeKind.Var) { + + // Perform an occurs check, verifying whether left occurs + // somewhere inside the structure of right. If so, unification + // makes no sense. + if (right.hasTypeVar(left)) { + // TODO print a diagnostic + return false; + } + + // We are ready to join the types, so the first thing we do is + // propagating the type classes that 'left' requires to 'right'. + // If 'right' is another type variable, we're lucky. We just copy + // the missing type classes from 'left' to 'right'. Otherwise, + //const propagateClasses = (classes: Iterable, type: Type) => { + // if (type.kind === TypeKind.Var) { + // for (const constraint of classes) { + // type.context.add(constraint); + // } + // } else if (type.kind === TypeKind.Con) { + // for (const constraint of classes) { + // propagateClassTCon(constraint, type); + // } + // } else { + // //assert(false); + // //this.diagnostics.add(new ); + // } + //} + + //const propagateClassTCon = (clazz: ClassDeclaration, type: TCon) => { + // const s = this.findInstanceContext(type, clazz); + // let i = 0; + // for (const classes of s) { + // propagateClasses(classes, type.argTypes[i++]); + // } + //} + + //propagateClasses(left.context, right); + + // We are all clear; set the actual type of left to right. + this.solution.set(left, right); + + // These types will be join, and we'd like to track that + // into a special chain. + TypeBase.join(left, right); + + // if (left.node !== null) { + // right.node = left.node; + // } + + return true; + } + + if (left.kind === TypeKind.Arrow && right.kind === TypeKind.Arrow) { + let success = true; + if (!this.unify(left.paramType, right.paramType, enableDiagnostics)) { + success = false; + } + if (!this.unify(left.returnType, right.returnType, enableDiagnostics)) { + success = false; + } + if (success) { + TypeBase.join(left, right); + } + return success; + } + + if (left.kind === TypeKind.Tuple && right.kind === TypeKind.Tuple) { + if (left.elementTypes.length === right.elementTypes.length) { + let success = false; + const count = left.elementTypes.length; + for (let i = 0; i < count; i++) { + if (!this.unify(left.elementTypes[i], right.elementTypes[i], enableDiagnostics)) { + success = false; + } + } + if (success) { + TypeBase.join(left, right); + } + return success; + } + } + + if (left.kind === TypeKind.Con && right.kind === TypeKind.Con) { + if (left.id === right.id) { + assert(left.argTypes.length === right.argTypes.length); + const count = left.argTypes.length; + let success = true; + for (let i = 0; i < count; i++) { + if (!this.unify(left.argTypes[i], right.argTypes[i], enableDiagnostics)) { + success = false; + } + } + if (success) { + TypeBase.join(left, right); + } + return success; + } + } + + if (left.kind === TypeKind.Nil && right.kind === TypeKind.Nil) { + return true; + } + + if (left.kind === TypeKind.Field && right.kind === TypeKind.Field) { + if (left.name === right.name) { + let success = true; + this.path.push(left.name); + if (!this.unifyField(left.type, right.type, enableDiagnostics)) { + success = false; + } + this.path.pop(); + if (!this.unify(left.restType, right.restType, enableDiagnostics)) { + success = false; + } + return success; + } + let success = true; + const newRestType = new TVar(this.nextTypeVarId++); + if (!this.unify(left.restType, new TField(right.name, right.type, newRestType), enableDiagnostics)) { + success = false; + } + if (!this.unify(right.restType, new TField(left.name, left.type, newRestType), enableDiagnostics)) { + success = false; + } + return success; + } + + if (left.kind === TypeKind.Nil && right.kind === TypeKind.Field) { + swap(); + } + + if (left.kind === TypeKind.Field && right.kind === TypeKind.Nil) { + let success = true; + this.path.push(left.name); + if (!this.unifyField(left.type, new TAbsent(right.node), enableDiagnostics)) { + success = false; + } + this.path.pop(); + if (!this.unify(left.restType, right, enableDiagnostics)) { + success = false; + } + return success + } + + if (left.kind === TypeKind.Nominal && right.kind === TypeKind.Nominal) { + if (left.decl === right.decl) { + return true; + } + // fall through to error reporting + } + + if (left.kind === TypeKind.App && right.kind === TypeKind.App) { + return this.unify(left.left, right.left, enableDiagnostics) + && this.unify(left.right, right.right, enableDiagnostics); + } + + if (enableDiagnostics) { + this.diagnostics.add( + new TypeMismatchDiagnostic( + left.substitute(this.solution), + right.substitute(this.solution), + [...this.constraint!.getNodes()], + this.path, + ) + ); + } + return false; + } + + public solve(constraint: Constraint): void { + + let queue = [ constraint ]; + let next = []; + let isNext = false; + + let errorCount = 0; + + for (;;) { + + if (queue.length === 0) { + if (next.length === 0) { + break; + } + isNext = true; + queue = next; + next = []; + } + + const constraint = queue.shift()!; + + sw: switch (constraint.kind) { + + case ConstraintKind.Many: + { + for (const element of constraint.elements) { + queue.push(element); + } + break; + } + +// case ConstraintKind.Class: +// { +// if (constraint.type.kind === TypeKind.Var) { +// if (isNext) { +// // TODO +// } else { +// next.push(constraint); +// } +// } else { +// const classDecl = this.lookupClass(constraint.className); +// if (classDecl === null) { +// this.diagnostics.add(new TypeclassNotFoundDiagnostic(constraint.className, constraint.node)); +// break; +// } +// for (const instance of classDecl.getInstances()) { +// if (this.unify(instance.inferredType, constraint.type, false)) { +// break sw; +// } +// } +// this.diagnostics.add(new TypeclassNotImplementedDiagnostic(constraint.className, constraint.type, constraint.node)); +// } +// break; +// } + + case ConstraintKind.Equal: + { + this.constraint = constraint; + if (!this.unify(constraint.left, constraint.right, true)) { + errorCount++; + if (errorCount === this.maxTypeErrorCount) { + return; + } + } + break; + } + + } + + } + + } + +} diff --git a/compiler/src/test/type-inference.md b/compiler/src/test/type-inference.md deleted file mode 100644 index 8988125e2..000000000 --- a/compiler/src/test/type-inference.md +++ /dev/null @@ -1,226 +0,0 @@ - -## Record types can be unified without causing an error - -``` -struct Person. - email: String - age: Int - -let bert - = Person { - email = "bar@boo.com", - age = 32 - } -let bob - = Person { - email = "boo", - age = 43 - } - -bert == bob -``` - -## Return types are polymorphic - -``` -let id x = x - -id 1 -id "foo" -id True -``` - -## Nested definitions work - -``` -let foo x. - let bar y z = y + z - x - bar - -foo True -``` - -## Everything that can be type-checked will be type-checked - -``` -let foo n. - let f : String = 1 - return n -``` - -## Recursive definitions do not cause infinite loops in the type-checker - -``` -let fac n = fac_2 n - -let fac_2 n = fac_3 n + fac n - -let fac_3 n = fac_2 (n-1) - -not (fac 1) -``` - -## Example with mutual recursion works - -``` -let is_even x. - if x == 0. - return True - else. - return is_odd (x-1) - -let is_odd x. - if x == 1. - return False - else. - return is_even (x-1) - -not (is_even True) -``` - -## Polymorphic records can be partially typed - -``` -struct Timestamped a b. - first: a - second: b - timestamp: Int - -type Foo = Timestamped Int - -type Bar = Foo Int - -let t : Bar = Timestamped { first = "bar", second = 1, timestamp = 12345 } -``` - -## Extensible records work - -``` -struct Timestamped a. - data: a - timestamp: Int - -let t = Timestamped { data = "foo", timestamp = 12345 } - -t.data == 1 -t.data == "foo" - -let u = Timestamped { data = True, timestamp = 12345 } - -u.data == "foo" -u.data == False -``` - -## A recursive function is automatically instantiated - -``` -let fac n. - if n == 0. - return 1 - else. - return n * fac (n-"foo") -``` - -## Enum-declarations are correctly typed - -``` -enum Maybe a. - Just a - Nothing - -let right_1 : Maybe Int = Just 1 -let right_2 : Maybe String = Just "foo" -let wrong : Maybe Int = Just "foo" -``` - -## Kind inference works - -``` -enum Maybe a. - Just a - Nothing - -let foo_1 : Maybe -let foo_2 : Maybe Int -let foo_3 : Maybe Int Int -let foo_4 : Maybe Int Int Int -``` - -## Can indirectly apply a polymorphic datatype to some type - -``` -enum Maybe a. - Just a - Nothing - -enum App a b. - MkApp (a b) - -enum Foo. - MkFoo (App Maybe Int) - -let f : Foo = MkFoo (MkApp (Just 1)) -``` - -## Record-declarations inside enum-declarations work - -``` -enum Shape. - Circle. - radius: Int - Rect. - width: Int - height: Int - -let z = Circle { radius = 12 } -let a = Rect { width = 12, height = 12 } - -a == z -``` - -## Tuple types are correctly inferred and unified - -``` -let foo_1 : (Int, Int, Int) = (1, 2, 3) -let foo_2 : (Int, Int, Int) = (1, 2, "foo") -``` - -## Module references work - -``` -mod CD. - mod A. - struct Foo - mod B. - let alpha: A.Foo -``` - -<<<<<<< HEAD -## Rest-expressions on extensible records work - -``` -struct Point. - x: Int - y: Int - z: Int - -let foo { x, y, .. } : Point -> Int = x + y - -foo { x = 1, y = 2 } -``` -======= - -## A polymorphic function is properly generalized when assigned to a new variable - -``` -let id x = x -let id2 = id -let id3 = id - -id3 1 -id3 "bla" - -id2 1 -id2 "bla" -```` ->>>>>>> 11bae0fed0d58eafebd863b4e3bf117176176cb1 diff --git a/compiler/src/types.ts b/compiler/src/types.ts index 5fd35e6cb..4323bce05 100644 --- a/compiler/src/types.ts +++ b/compiler/src/types.ts @@ -1,8 +1,566 @@ +import { InspectOptions } from "util"; +import { ClassDeclaration, EnumDeclaration, StructDeclaration, Syntax } from "./cst"; +import { deserializable, ignore, InspectFn, toStringTag } from "./util"; -export interface Pass { - apply(input: In): Out; +export enum TypeKind { + Arrow, + Var, + Con, + Tuple, + App, + Nominal, + Field, + Nil, + Absent, + Present, } -export interface Newable { - new (...args: any[]): T; +export abstract class TypeBase { + + @ignore + public abstract readonly kind: TypeKind; + + @ignore + public next: Type = this as any; + + public abstract node: Syntax | 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 { + for (const other of this.getTypeVars()) { + if (tv.id === other.id) { + return true; + } + } + return false; + } + + public abstract [toStringTag](depth: number, options: InspectOptions, inspect: InspectFn): string; + +} + +export function isType(value: any): value is Type { + return value !== undefined + && value !== null + && value instanceof TypeBase; +} + +@deserializable() +export class TVar extends TypeBase { + + public readonly kind = TypeKind.Var; + + @ignore + public context = new Set(); + + public constructor( + public id: number, + public node: Syntax | null = null, + ) { + super(); + } + + public *getTypeVars(): Iterable { + 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 + ? this : other.substitute(sub); + } + + public [toStringTag]() { + return 'a' + this.id; + } + +} + +export class TNil extends TypeBase { + + public readonly kind = TypeKind.Nil; + + public constructor( + public node: Syntax | null = null + ) { + super(); + } + + public substitute(_sub: TVSub): Type { + return this; + } + + public shallowClone(): Type { + return new TNil(this.node); + } + + public *getTypeVars(): Iterable { + + } + + public [toStringTag]() { + return '∂Abs'; + } + +} + +@deserializable() +export class TAbsent extends TypeBase { + + public readonly kind = TypeKind.Absent; + + public constructor( + public node: Syntax | null = null, + ) { + super(); + } + + public substitute(_sub: TVSub): Type { + return this; + } + + public shallowClone(): Type { + return new TAbsent(this.node); + } + + public *getTypeVars(): Iterable { + + } + + public [toStringTag]() { + return 'Abs'; + } + +} + +@deserializable() +export class TPresent extends TypeBase { + + public readonly kind = TypeKind.Present; + + public constructor( + public type: Type, + public node: Syntax | null = null, + ) { + super(); + } + + public substitute(sub: TVSub): Type { + return new TPresent(this.type.substitute(sub), this.node); + } + + public getTypeVars(): Iterable { + return this.type.getTypeVars(); + } + + public shallowClone(): Type { + return new TPresent(this.type, this.node); + } + + public [toStringTag](_depth: number, options: InspectOptions, inspect: InspectFn) { + return 'Pre ' + inspect(this.type, options); + } + +} + +@deserializable() +export class TArrow extends TypeBase { + + public readonly kind = TypeKind.Arrow; + + public constructor( + public paramType: Type, + public returnType: Type, + public node: Syntax | null = null, + ) { + super(); + } + + public static build(paramTypes: Type[], returnType: Type, node: Syntax | null = null): Type { + let result = returnType; + for (let i = paramTypes.length-1; i >= 0; i--) { + result = new TArrow(paramTypes[i], result, node); + } + return result; + } + + public *getTypeVars(): Iterable { + yield* this.paramType.getTypeVars(); + yield* this.returnType.getTypeVars(); + } + + public shallowClone(): TArrow { + return new TArrow( + this.paramType, + this.returnType, + this.node, + ) + } + + public substitute(sub: TVSub): Type { + let changed = false; + const newParamType = this.paramType.substitute(sub); + if (newParamType !== this.paramType) { + changed = true; + } + const newReturnType = this.returnType.substitute(sub); + if (newReturnType !== this.returnType) { + changed = true; + } + return changed ? new TArrow(newParamType, newReturnType, this.node) : this; + } + + public [toStringTag](_depth: number, options: InspectOptions, inspect: InspectFn) { + return inspect(this.paramType, options) + ' -> ' + inspect(this.returnType, options); + } + +} + +@deserializable() +export class TCon extends TypeBase { + + public readonly kind = TypeKind.Con; + + public constructor( + public id: number, + public argTypes: Type[], + public displayName: string, + public node: Syntax | null = null, + ) { + super(); + } + + public *getTypeVars(): Iterable { + for (const argType of this.argTypes) { + yield* argType.getTypeVars(); + } + } + + public shallowClone(): TCon { + return new TCon( + this.id, + this.argTypes, + this.displayName, + this.node, + ); + } + + 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.node) : this; + } + + public [toStringTag](_depth: number, options: InspectOptions, inspect: InspectFn) { + return this.displayName + ' ' + this.argTypes.map(t => inspect(t, options)).join(' '); + } + +} + +@deserializable() +export class TTuple extends TypeBase { + + public readonly kind = TypeKind.Tuple; + + public constructor( + public elementTypes: Type[], + public node: Syntax | null = null, + ) { + super(); + } + + public *getTypeVars(): Iterable { + for (const elementType of this.elementTypes) { + yield* elementType.getTypeVars(); + } + } + + public shallowClone(): TTuple { + return new TTuple( + this.elementTypes, + this.node, + ); + } + + 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.node) : this; + } + + public [toStringTag](_depth: number, options: InspectOptions, inspect: InspectFn) { + return this.elementTypes.map(t => inspect(t, options)).join(' × '); + } + +} + +@deserializable() +export class TField extends TypeBase { + + public readonly kind = TypeKind.Field; + + public constructor( + public name: string, + public type: Type, + public restType: Type, + public node: Syntax | null = null, + ) { + super(); + } + + public getTypeVars(): Iterable { + return this.type.getTypeVars(); + } + + public shallowClone(): TField { + return new TField( + this.name, + this.type, + this.restType, + this.node, + ); + } + + public static sort(type: Type): Type { + const fields = new Map(); + while (type.kind === TypeKind.Field) { + fields.set(type.name, type); + type = type.restType; + } + const keys = [...fields.keys()].sort().reverse(); + let out: Type = type; + for (const key of keys) { + const field = fields.get(key)!; + out = new TField(key, field.type, out, field.node); + } + return out + } + + public substitute(sub: TVSub): Type { + const newType = this.type.substitute(sub); + const newRestType = this.restType.substitute(sub); + return newType !== this.type || newRestType !== this.restType + ? new TField(this.name, newType, newRestType, this.node) : this; + } + + public [toStringTag](_depth: number, options: InspectOptions, inspect: InspectFn) { + let out = '{ ' + this.name + ': ' + inspect(this.type, options); + let type = this.restType; + while (type.kind === TypeKind.Field) { + out += '; ' + type.name + ': ' + inspect(type.type, options); + type = type.restType; + } + if (type.kind !== TypeKind.Nil) { + out += '; ' + inspect(type, options); + } + return out + ' }' + } + +} + +@deserializable() +export class TApp extends TypeBase { + + public readonly kind = TypeKind.App; + + public constructor( + public left: Type, + public right: Type, + public node: Syntax | null = null + ) { + super(); + } + + public static build(resultType: Type, types: Type[], node: Syntax | null = null): Type { + for (let i = 0; i < types.length; i++) { + resultType = new TApp(types[i], resultType, node); + } + return resultType; + } + + public *getTypeVars(): Iterable { + yield* this.left.getTypeVars(); + yield* this.right.getTypeVars(); + } + + public shallowClone() { + return new TApp( + this.left, + this.right, + this.node + ); + } + + public substitute(sub: TVSub): Type { + let changed = false; + const newOperatorType = this.left.substitute(sub); + if (newOperatorType !== this.left) { + changed = true; + } + const newArgType = this.right.substitute(sub); + if (newArgType !== this.right) { + changed = true; + } + return changed ? new TApp(newOperatorType, newArgType, this.node) : this; + } + + public [toStringTag](_depth: number, options: InspectOptions, inspect: InspectFn) { + return inspect(this.left, options) + ' ' + inspect(this.right, options); + } + +} + +@deserializable() +export class TNominal extends TypeBase { + + public readonly kind = TypeKind.Nominal; + + public constructor( + public decl: StructDeclaration | EnumDeclaration, + public node: Syntax | null = null, + ) { + super(); + } + + public *getTypeVars(): Iterable { + + } + + public shallowClone(): Type { + return new TNominal( + this.decl, + this.node, + ); + } + + public substitute(_sub: TVSub): Type { + return this; + } + + public [toStringTag]() { + return this.decl.name.text; + } + +} + +export type Type + = TCon + | TArrow + | TVar + | TTuple + | TApp + | TNominal + | TField + | TNil + | TPresent + | TAbsent + + +export class TVSet { + + private mapping = new Map(); + + public constructor(iterable?: Iterable) { + if (iterable !== undefined) { + for (const tv of iterable) { + this.add(tv); + } + } + } + + public add(tv: TVar): void { + this.mapping.set(tv.id, tv); + } + + public has(tv: TVar): boolean { + return this.mapping.has(tv.id); + } + + public intersectsType(type: Type): boolean { + for (const tv of type.getTypeVars()) { + if (this.has(tv)) { + return true; + } + } + return false; + } + + public delete(tv: TVar): void { + this.mapping.delete(tv.id); + } + + public get size(): number { + return this.mapping.size; + } + + public [Symbol.iterator](): Iterator { + return this.mapping.values(); + } + + public [toStringTag](_depth: number, options: InspectOptions, inspect: InspectFn) { + let out = '{ '; + let first = true; + for (const tv of this) { + if (first) first = false; + else out += ', '; + out += inspect(tv, options); + } + return out + ' }'; + } + +} + +export class TVSub { + + private mapping = new Map(); + + 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 { + return this.mapping.values(); + } + } diff --git a/compiler/src/util.ts b/compiler/src/util.ts index 80bd19b8b..e89115763 100644 --- a/compiler/src/util.ts +++ b/compiler/src/util.ts @@ -1,5 +1,4 @@ -import "reflect-metadata" import path from "path" import stream from "stream" import { InspectOptions } from "util"; diff --git a/package.json b/package.json new file mode 100644 index 000000000..a8b38c444 --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "bolt-workspace", + "private": true, + "workspaces": [ + "compiler" + ], + "dependencies": { + "typescript": "^5.0.4" + } +}