Skip to content

Commit

Permalink
checked that a type exists
Browse files Browse the repository at this point in the history
  • Loading branch information
ascandone committed Jan 19, 2024
1 parent 807b438 commit a3abe73
Show file tree
Hide file tree
Showing 9 changed files with 108 additions and 40 deletions.
4 changes: 4 additions & 0 deletions src/__snapshots__/parser.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1278,6 +1278,10 @@ exports[`parses a concrete type with no args as a type hint 1`] = `
"typeHint": {
"args": [],
"name": "Int",
"span": [
8,
11,
],
"type": "named",
},
"value": {
Expand Down
39 changes: 18 additions & 21 deletions src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type TypeHint = {
args: TypeHint[];
};

export type Expr<Meta = {}> = Meta &
export type Expr<TypeMeta = {}> = (TypeMeta & SpanMeta) &
(
| {
type: "constant";
Expand All @@ -21,33 +21,33 @@ export type Expr<Meta = {}> = Meta &
}
| {
type: "fn";
params: Array<{ name: string } & Meta>;
body: Expr<Meta>;
params: Array<{ name: string } & TypeMeta & SpanMeta>;
body: Expr<TypeMeta>;
}
| {
type: "application";
caller: Expr<Meta>;
args: Expr<Meta>[];
caller: Expr<TypeMeta>;
args: Expr<TypeMeta>[];
}
| {
type: "let";
binding: { name: string } & Meta;
value: Expr<Meta>;
body: Expr<Meta>;
binding: { name: string } & TypeMeta & SpanMeta;
value: Expr<TypeMeta>;
body: Expr<TypeMeta>;
}
| {
type: "if";
condition: Expr<Meta>;
then: Expr<Meta>;
else: Expr<Meta>;
condition: Expr<TypeMeta>;
then: Expr<TypeMeta>;
else: Expr<TypeMeta>;
}
);

export type Statement<Meta = {}> = Meta & {
export type Statement<TypeMeta = {}> = TypeMeta & {
type: "let";
typeHint?: TypeHint;
binding: { name: string } & Meta;
value: Expr<Meta>;
typeHint?: TypeHint & SpanMeta;
binding: { name: string } & SpanMeta;
value: Expr<TypeMeta>;
};

export type Program<Meta = {}> = {
Expand All @@ -61,10 +61,7 @@ function spanContains([start, end]: Span, offset: number) {
return start <= offset && end >= offset;
}

function exprByOffset<T extends SpanMeta>(
ast: Expr<T>,
offset: number,
): T | undefined {
function exprByOffset<T>(ast: Expr<T>, offset: number): T | undefined {
if (!spanContains(ast.span, offset)) {
return;
}
Expand Down Expand Up @@ -108,13 +105,13 @@ function exprByOffset<T extends SpanMeta>(
}
}

export function declByOffset<T extends SpanMeta>(
export function declByOffset<T>(
program: Program<T>,
offset: number,
): T | undefined {
for (const st of program.statements) {
if (spanContains(st.binding.span, offset)) {
return st.binding;
return st.value;
}
const e = exprByOffset(st.value, offset);
if (e !== undefined) {
Expand Down
3 changes: 1 addition & 2 deletions src/cli/commands/lsp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { TextDocument } from "vscode-languageserver-textdocument";
import { parse } from "../../parser";
import { typeErrorPPrint, typePPrint } from "../../typecheck/pretty-printer";
import { TypeMeta, typecheck } from "../../typecheck/typecheck";
import { prelude } from "../../typecheck/prelude";
import { Program, SpanMeta, declByOffset } from "../../ast";

const documents = new TextDocuments(TextDocument);
Expand Down Expand Up @@ -52,7 +51,7 @@ export function lspCmd() {
return;
}

const [typed, errors] = typecheck(parsed.value, prelude);
const [typed, errors] = typecheck(parsed.value);
docs.set(change.document.uri, [change.document, typed]);
connection.sendDiagnostics({
uri: change.document.uri,
Expand Down
3 changes: 1 addition & 2 deletions src/cli/commands/typecheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { readFileSync } from "fs";
import { parse } from "../../parser";
import { typecheck } from "../../typecheck/typecheck";
import { typeErrorPPrint } from "../../typecheck/pretty-printer";
import { prelude } from "../../typecheck/prelude";
import { Span } from "../../ast";

const FgRed = "\x1b[31m";
Expand All @@ -22,7 +21,7 @@ export function typecheckCmd(path: string) {
return;
}

const [, errors] = typecheck(parseResult.value, prelude);
const [, errors] = typecheck(parseResult.value);

for (const error of errors) {
const msg = typeErrorPPrint(error);
Expand Down
7 changes: 6 additions & 1 deletion src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,12 @@ semantics.addOperation<Statement<SpanMeta>>("statement()", {
const th =
typeHint.numChildren === 0
? {}
: { typeHint: typeHint.child(0).typeHint() };
: {
typeHint: {
...typeHint.child(0).typeHint(),
span: getSpan(typeHint.child(0)),
},
};

return {
type: "let",
Expand Down
12 changes: 12 additions & 0 deletions src/typecheck/prelude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,15 @@ function gen(f: (args: Generator<Type>) => Type): Type {
const t = f(freshVars());
return generalize(t);
}

export type TypesPool = Record<string, number>;

export const defaultTypesPool: TypesPool = {
Int: 0,
Float: 0,
String: 0,
Nil: 0,
Bool: 0,
Maybe: 1,
List: 1,
};
9 changes: 5 additions & 4 deletions src/typecheck/pretty-printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@ export function typePPrint(t: Type): string {
return pprintHelper(generalize(t));
}

export function typeErrorPPrint(e: TypeError<unknown>) {
export function typeErrorPPrint(e: TypeError<unknown>): string {
switch (e.type) {
case "unbound-variable":
return `Unbound variable: "${e.ident}"`;

return `Unbound variable: "${e.ident}"\n`;
case "occurs-check":
return "Cannot construct the infinite type";
return "Cannot construct the infinite type\n";
case "unbound-type":
return `Unbound type: ${e.name}/${e.arity}\n`;
case "type-mismatch":
if (
e.left.type === "fn" &&
Expand Down
22 changes: 18 additions & 4 deletions src/typecheck/typecheck.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { unsafeParse } from "../parser";
import { typecheck, TypeError } from "./typecheck";
import { typePPrint } from "./pretty-printer";
import { Context } from "./unify";
import { Int, Bool } from "./prelude";
import { Int, Bool, TypesPool } from "./prelude";

test("infer int", () => {
const [types, errors] = tc(`
Expand Down Expand Up @@ -273,16 +273,30 @@ test("recursive let declarations", () => {
});

test("type hints are used by typechecker", () => {
const [types, errs] = tc("let x: Int = 1.1");
const [types, errs] = tc(
"let x: Int = 1.1",
{},
{
Int: 0,
},
);
expect(errs).not.toEqual([]);
expect(types).toEqual({
x: "Int",
});
});

test("unknown types are rejected", () => {
const [types, errs] = tc("let x: NotFound = 1", {}, {});
expect(errs).not.toEqual([]);
expect(types).toEqual({
x: "Int",
});
});

function tc(src: string, context: Context = {}) {
function tc(src: string, context: Context = {}, typesContext: TypesPool = {}) {
const parsedProgram = unsafeParse(src);
const [typed, errors] = typecheck(parsedProgram, context);
const [typed, errors] = typecheck(parsedProgram, context, typesContext);

const kvs = typed.statements.map((decl) => [
decl.binding.name,
Expand Down
49 changes: 43 additions & 6 deletions src/typecheck/typecheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
SpanMeta,
TypeHint,
} from "../ast";
import { TypesPool, defaultTypesPool, prelude } from "./prelude";
import { TVar, Type, unify, Context, generalize, instantiate } from "./unify";

export type UnifyErrorType = "type-mismatch" | "occurs-check";
Expand All @@ -15,6 +16,12 @@ export type TypeError<Meta> =
ident: string;
node: Expr<Meta>;
}
| {
type: "unbound-type";
name: string;
arity: number;
node: SpanMeta;
}
| {
type: UnifyErrorType;
node: Expr<Meta>;
Expand All @@ -24,22 +31,52 @@ export type TypeError<Meta> =

export type TypeMeta = { $: TVar };

function unboundTypeError<T extends SpanMeta>(
node: T,
t: Type,
tCtx: TypesPool,
): TypeError<T> | undefined {
if (t.type !== "named") {
return undefined;
}

const arity = tCtx[t.name];
const expectedArity = t.args.length;
if (arity !== undefined && arity === expectedArity) {
return undefined;
}

return {
type: "unbound-type",
name: t.name,
arity: expectedArity,
node,
};
}

export function typecheck<T extends SpanMeta>(
ast: Program<T>,
initialContext: Context = {},
): [Program<T & TypeMeta>, TypeError<T & TypeMeta>[]] {
initialContext: Context = prelude,
typesContext: TypesPool = defaultTypesPool,
): [Program<T & TypeMeta>, TypeError<SpanMeta>[]] {
TVar.resetId();
const errors: TypeError<T & TypeMeta & SpanMeta>[] = [];
const errors: TypeError<SpanMeta>[] = [];
let context: Context = { ...initialContext };

const typedStatements = ast.statements.map<Statement<T & TypeMeta>>(
(decl) => {
const annotated = annotateExpr(decl.value);
if (decl.typeHint !== undefined) {
const t = inferTypeHint(decl.typeHint);
// TODO collect error
// - but is it even possible to fail to unify a fresh var?
unify(t, annotated.$.asType());

const err = unboundTypeError<SpanMeta>(decl.typeHint, t, typesContext);
if (err) {
errors.push(err);
} else {
// TODO collect error
// - but is it even possible to fail to unify a fresh var?
unify(t, annotated.$.asType());
}
}

errors.push(
Expand Down

0 comments on commit a3abe73

Please sign in to comment.