We're checking types!!!!
This commit is contained in:
parent
f3c3a76671
commit
f272ffaca2
6 changed files with 304 additions and 24 deletions
259
src/typechecker.ts
Normal file
259
src/typechecker.ts
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
import type { AST, TypeAST, Pattern, Definition } from './ast'
|
||||
import { prettyPrintType } from './ast'
|
||||
|
||||
// Map type var names to their types
|
||||
type Subst = Map<string, TypeAST>
|
||||
|
||||
// Map var names to their types
|
||||
type TypeEnv = Map<string, TypeAST>
|
||||
|
||||
// Replace type vars with resolved types
|
||||
function applySubst(type: TypeAST, subst: Subst): TypeAST {
|
||||
switch (type.kind) {
|
||||
case 'type-var':
|
||||
const resolved = subst.get(type.name);
|
||||
return resolved ? applySubst(resolved, subst) : type;
|
||||
case 'type-function':
|
||||
return { kind: 'type-function', param: applySubst(type.param, subst), result: applySubst(type.result, subst) };
|
||||
case 'type-apply':
|
||||
return { kind: 'type-apply', constructor: applySubst(type.constructor, subst), args: type.args.map(a => applySubst(a, subst)) };
|
||||
case 'type-record':
|
||||
return { kind: 'type-record', fields: type.fields.map(f => ({ name: f.name, type: applySubst(f.type, subst) } )) };
|
||||
case 'type-name':
|
||||
return type;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function unify(t1: TypeAST, t2: TypeAST, subst: Subst): string | null {
|
||||
const a = applySubst(t1, subst);
|
||||
const b = applySubst(t2, subst);
|
||||
|
||||
// Same type name
|
||||
if (a.kind === 'type-name' && b.kind === 'type-name' && a.name === b.name) return null;
|
||||
|
||||
// Type var binds to anything
|
||||
if (a.kind === 'type-var') { subst.set(a.name, b); return null; }
|
||||
if (b.kind === 'type-var') { subst.set(b.name, a); return null; }
|
||||
|
||||
// Functions: unify param & result
|
||||
if (a.kind === 'type-function' && b.kind === 'type-function') {
|
||||
const err = unify(a.param, b.param, subst);
|
||||
if (err) return err;
|
||||
return unify(a.result, b.result, subst);
|
||||
}
|
||||
|
||||
// Type application: unify constructor and args
|
||||
if (a.kind === 'type-apply' && b.kind === 'type-apply') {
|
||||
const err = unify(a.constructor, b.constructor, subst);
|
||||
if (err) return err;
|
||||
if (a.args.length !== b.args.length) return `Type argument mismatch`;
|
||||
for (let i = 0; i < a.args.length; i++) {
|
||||
const err = unify(a.args[i], b.args[i], subst);
|
||||
if (err) return err;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Records: unify matching fields
|
||||
if (a.kind === 'type-record' && b.kind === 'type-record') {
|
||||
for (const af of a.fields) {
|
||||
const bf = b.fields.find(f => f.name == af.name);
|
||||
if (bf) {
|
||||
const err = unify(af.type, bf.type, subst);
|
||||
if (err) return err;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return `Cannot unify ${prettyPrintType(a)} with ${prettyPrintType(b)}`;
|
||||
}
|
||||
|
||||
function infer(expr: AST, env: TypeEnv, subst: Subst): TypeAST | null {
|
||||
switch (expr.kind) {
|
||||
case 'literal':
|
||||
if (expr.value.kind === 'int') return { kind: 'type-name', name: 'Int' };
|
||||
if (expr.value.kind === 'float') return { kind: 'type-name', name: 'Float' };
|
||||
if (expr.value.kind === 'string') return { kind: 'type-name', name: 'String' };
|
||||
return null;
|
||||
|
||||
case 'variable': {
|
||||
const t = env.get(expr.name);
|
||||
return t ? applySubst(t, subst) : null;
|
||||
}
|
||||
|
||||
case 'constructor': {
|
||||
const t = env.get(expr.name);
|
||||
return t ? applySubst(t, subst) : null;
|
||||
}
|
||||
|
||||
case 'record': {
|
||||
const fields: { name: string, type: TypeAST }[] = [];
|
||||
for (const entry of expr.entries) {
|
||||
if (entry.kind === 'spread') continue;
|
||||
const t = infer(entry.value, env, subst);
|
||||
if (!t) return null;
|
||||
fields.push({ name: entry.key, type: t });
|
||||
}
|
||||
return { kind: 'type-record', fields };
|
||||
}
|
||||
|
||||
case 'record-access': {
|
||||
const recType = infer(expr.record, env, subst);
|
||||
if (!recType) return null;
|
||||
const resolved = applySubst(recType, subst);
|
||||
if (resolved.kind === 'type-record') {
|
||||
const field = resolved.fields.find(f => f.name === expr.field);
|
||||
return field ? field.type : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
case 'apply': {
|
||||
const funcType = infer(expr.func, env, subst);
|
||||
if (!funcType) return null;
|
||||
|
||||
let current = applySubst(funcType, subst);
|
||||
for (const arg of expr.args) {
|
||||
if (current.kind !== 'type-function') return null;
|
||||
const err = check(arg, current.param, env, subst);
|
||||
if (err) warn(err, arg);
|
||||
current = applySubst(current.result, subst);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
case 'let': {
|
||||
const valType = infer(expr.value, env, subst);
|
||||
const newEnv = new Map(env);
|
||||
if (valType) newEnv.set(expr.name, valType);
|
||||
return infer(expr.body, newEnv, subst);
|
||||
}
|
||||
|
||||
case 'lambda':
|
||||
return null;
|
||||
|
||||
case 'match': {
|
||||
const scrutType = infer(expr.expr, env, subst);
|
||||
if (expr.cases.length === 0) return null;
|
||||
const firstEnv = new Map(env);
|
||||
if (scrutType) bindPattern(expr.cases[0].pattern, scrutType, firstEnv, subst);
|
||||
return infer(expr.cases[0].result, firstEnv, subst);
|
||||
}
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function check(expr: AST, expected: TypeAST, env: TypeEnv, subst: Subst): string | null {
|
||||
const exp = applySubst(expected, subst);
|
||||
|
||||
// Lambda against function type
|
||||
if (expr.kind === 'lambda' && exp.kind === 'type-function') {
|
||||
const newEnv = new Map(env);
|
||||
newEnv.set(expr.params[0], exp.param);
|
||||
|
||||
if (expr.params.length > 1) {
|
||||
const innerLambda: AST = {
|
||||
kind: 'lambda',
|
||||
params: expr.params.slice(1),
|
||||
body: expr.body,
|
||||
};
|
||||
|
||||
return check(innerLambda, exp.result, newEnv, subst);
|
||||
}
|
||||
|
||||
return check(expr.body, exp.result, newEnv, subst);
|
||||
}
|
||||
|
||||
// Match: check each case result against expected
|
||||
if (expr.kind === 'match') {
|
||||
const scrutType = infer(expr.expr, env, subst);
|
||||
for (const c of expr.cases) {
|
||||
const caseEnv = new Map(env);
|
||||
if (scrutType) bindPattern(c.pattern, scrutType, caseEnv, subst);
|
||||
const err = check(c.result, expected, caseEnv, subst);
|
||||
if (err) warn(err, c.result);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Let
|
||||
if (expr.kind === 'let') {
|
||||
const valType = infer(expr.value, env, subst);
|
||||
const newEnv = new Map(env);
|
||||
if (valType) newEnv.set(expr.name, valType);
|
||||
return check(expr.body, expected, newEnv, subst);
|
||||
}
|
||||
|
||||
// Fallback: infer and unify
|
||||
const inferred = infer(expr, env, subst);
|
||||
if (!inferred) return null; // Can't infer, skip silently
|
||||
return unify(inferred, expected, subst);
|
||||
}
|
||||
|
||||
function warn(msg: string, expr: AST) {
|
||||
const loc = expr.line ? ` (line ${expr.line})` : '';
|
||||
console.warn(`TypeError${loc}: ${msg}`);
|
||||
}
|
||||
|
||||
function bindPattern(pattern: Pattern, type: TypeAST, env: TypeEnv, subst: Subst): void {
|
||||
const t = applySubst(type, subst);
|
||||
switch (pattern.kind) {
|
||||
case 'var':
|
||||
env.set(pattern.name, t);
|
||||
break;
|
||||
case 'constructor':
|
||||
// TODO: look up ctor arg types
|
||||
break;
|
||||
case 'list':
|
||||
case 'list-spread':
|
||||
// TODO: bind element types
|
||||
break;
|
||||
case 'record':
|
||||
if (t.kind === 'type-record') {
|
||||
for (const [key, pat] of Object.entries(pattern.fields)) {
|
||||
const field = t.fields.find(f => f.name === key);
|
||||
if (field) bindPattern(pat, field.type, env, subst);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// const int: TypeAST = { kind: 'type-name', name: 'Int' };
|
||||
// const float: TypeAST = { kind: 'type-name', name: 'Float' };
|
||||
// const str: TypeAST = { kind: 'type-name', name: 'String' };
|
||||
// const bool: TypeAST = { kind: 'type-name', name: 'Bool' };
|
||||
// const tvar = (name: string): TypeAST => ({ kind: 'type-var', name });
|
||||
// const tfn = (param: TypeAST, result: TypeAST): TypeAST => ({ kind: 'type-function', param, result });
|
||||
|
||||
export function typecheck(defs: Definition[]) {
|
||||
const env: TypeEnv = new Map();
|
||||
|
||||
// seed env with builtin types
|
||||
// env.set('cat', tfn(str, tfn(str, str)));
|
||||
// env.set('add', tfn(int, tfn(int, int)));
|
||||
// env.set('sub', tfn(int, tfn(int, int)));
|
||||
// env.set('mul', tfn(int, tfn(int, int)));
|
||||
// env.set('div', tfn(int, tfn(int, int)));
|
||||
// env.set('eq', tfn(tvar('a'), tfn(tvar('a'), bool)));
|
||||
|
||||
// Register all annotated defs in env first so they can ref eachother
|
||||
for (const def of defs) {
|
||||
if (def.annotation) {
|
||||
env.set(def.name, def.annotation.type);
|
||||
}
|
||||
}
|
||||
|
||||
// Check each annotated def
|
||||
for (const def of defs) {
|
||||
if (def.annotation && def.body) {
|
||||
const subst: Subst = new Map();
|
||||
const err = check(def.body, def.annotation.type, env, subst);
|
||||
if (err) warn(err, def.body);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue