import * as ast from "./ast.ts"; import * as stringify from "./stringify.ts"; export class Ty { private static idCounter = 0; private static internedTys = new Map(); static create( tag: Tag, kind: Omit, ): Ty { const ty = new Ty( this.idCounter, { tag, ...kind } as TyKind & { tag: Tag }, ); const hash = ty.internHash(); if (this.internedTys.has(hash)) { return this.internedTys.get(hash)!; } this.internedTys.set(hash, ty); this.idCounter += 1; return ty; } static Error = Ty.create("Error", {}); static Void = Ty.create("Void", {}); static I8 = Ty.create("Int", { intTy: "i8" }); static I16 = Ty.create("Int", { intTy: "i16" }); static I32 = Ty.create("Int", { intTy: "i32" }); static I64 = Ty.create("Int", { intTy: "i64" }); static ISize = Ty.create("Int", { intTy: "isize" }); static U8 = Ty.create("Int", { intTy: "u8" }); static U16 = Ty.create("Int", { intTy: "u16" }); static U32 = Ty.create("Int", { intTy: "u32" }); static U64 = Ty.create("Int", { intTy: "u64" }); static USize = Ty.create("Int", { intTy: "usize" }); static Bool = Ty.create("Bool", {}); static Ptr(ty: Ty): Ty { return this.create("Ptr", { ty }); } static PtrMut(ty: Ty): Ty { return this.create("PtrMut", { ty }); } static Slice(ty: Ty): Ty { return this.create("Slice", { ty }); } static Array(ty: Ty, length: number): Ty { return this.create("Array", { ty, length }); } static Fn(params: Ty[], retTy: Ty, generics: Ty[] | null): Ty { return this.create("Fn", { params, retTy, generics }); } static Generic(idx: number): Ty { return this.create("Generic", { idx }); } static Instance(ty: Ty, args: Ty[]): Ty { return this.create("Instance", { ty, args }); } /** Only used in type checker. */ static Any = Ty.create("Any", {}); /** Only used in type checker. */ static AnyInt = Ty.create("AnyInt", {}); /** Only used in type checker. */ static AnyIndexable(ty: Ty): Ty { return this.create("AnyIndexable", { ty }); } /** Only used in type checker. */ static AnyCallable(ty: Ty): Ty { return this.create("AnyCallable", { ty }); } /** Only used in type checker. */ static AnyDerefable(ty: Ty): Ty { return this.create("AnyDerefable", { ty }); } private internHash(): string { if (this.is("FnStmt")) { return JSON.stringify({ ...this.kind, stmt: this.kind.stmt.id }); } return JSON.stringify(this.kind); } private constructor( public id: number, public kind: TyKind, ) {} is< Tag extends TyKind["tag"], >(tag: Tag): this is Ty & { kind: { tag: Tag } } { return this.kind.tag === tag; } assertIs< Tag extends TyKind["tag"], >(tag: Tag): asserts this is Ty & { kind: { tag: Tag } } { if (this.kind.tag !== tag) { throw new Error(`ty is not '${tag}'`); } } as< Tag extends TyKind["tag"], >(tag: Tag): Ty & { kind: { tag: Tag } } { this.assertIs(tag); return this; } /** * Used for checking and inference in the type checker * to say if two types are able to implicitly be converted * into each other or to a third type. Past type inference * this function is meaningless. */ resolvableWith(other: Ty): boolean { // types are interned--we can just do this if (this === other) { return true; } if (this.is("Error")) { return false; } if (this.is("Void")) { return other.is("Void"); } if (this.is("Int")) { return other.is("Int") && this.kind.intTy == other.kind.intTy || other.is("AnyInt"); } if (this.is("Bool")) { return other.is("Bool"); } if (this.is("Ptr")) { if (!other.is("Ptr")) { return false; } if (!this.kind.ty.resolvableWith(other.kind.ty)) { return false; } return true; } if (this.is("Array")) { if (!other.is("Array")) { return false; } if (!this.kind.ty.resolvableWith(other.kind.ty)) { return false; } if (this.kind.length !== other.kind.length) { return false; } return true; } if (this.is("Slice")) { if (!other.is("Slice")) { return false; } if (!this.kind.ty.resolvableWith(other.kind.ty)) { return false; } return true; } if (this.is("Range")) { return other.is("Range"); } if (this.is("Fn")) { if (!other.is("Fn")) { return false; } for (const i of this.kind.params.keys()) { if (!this.kind.params[i].resolvableWith(other.kind.params[i])) { return false; } } if (!this.kind.retTy.resolvableWith(other.kind.retTy)) { return false; } return true; } if (this.is("FnStmt")) { // Since FnStmt tys are only compatible with itself, // we can count on the ty cache for this check. return false; } if (this.is("Any")) { return true; } if (this.is("AnyInt")) { return other.is("Int"); } throw new Error(`'${this.kind.tag}' not handled`); } isSized(): boolean { if (this.is("Slice")) { return false; } return true; } isIndexable(): boolean { return this.is("Array") || this.is("Slice") || this.is("AnyIndexable"); } indexableTy(): Ty | null { if (this.is("Array") || this.is("Slice") || this.is("AnyIndexable")) { return this.kind.ty; } return null; } isCallable(): boolean { return this.is("FnStmt") || this.is("Fn"); } callableTy(): Ty & { kind: { tag: "Fn" } } { if (this.is("Fn")) { return this; } this.assertIs("FnStmt"); return this.kind.ty.as("Fn"); } isDerefable(): boolean { return this.is("Ptr") || this.is("PtrMut") || this.is("AnyDerefable"); } derefableTy(): Ty | null { if (this.is("Ptr") || this.is("PtrMut") || this.is("AnyDerefable")) { return this.kind.ty; } return null; } isCheckerInternal(): boolean { return this.is("Any") || this.is("AnyInt") || this.is("AnyIndexable") || this.is("AnyCallable") || this.is("AnyDerefable"); } pretty(colors?: stringify.PrettyColors): string { return stringify.tyPretty(this, colors); } } export type TyKind = | { tag: "Error" } | { tag: "Void" } | { tag: "Int"; intTy: ast.IntTy } | { tag: "Bool" } | { tag: "Ptr"; ty: Ty } | { tag: "PtrMut"; ty: Ty } | { tag: "Array"; ty: Ty; length: number } | { tag: "Slice"; ty: Ty } | { tag: "Range" } | { tag: "Fn"; params: Ty[]; retTy: Ty; generics: Ty[] | null } | { tag: "FnStmt"; ty: Ty; stmt: ast.NodeWithKind<"FnStmt"> } | { tag: "Generic"; idx: number } | { tag: "Instance"; ty: Ty; args: Ty[] } | { tag: "Any" | "AnyInt" } | { tag: "AnyIndexable"; ty: Ty } | { tag: "AnyCallable"; ty: Ty } | { tag: "AnyDerefable"; ty: Ty };