From 25c3ac5c0df60e4b56751296d9b9f34a931ebb7f Mon Sep 17 00:00:00 2001 From: Dustin Swan Date: Sun, 12 Apr 2026 20:26:16 -0600 Subject: [PATCH] Type Aliases! --- src/ast.ts | 1 + src/parser.ts | 7 +++++++ src/typechecker.ts | 41 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/ast.ts b/src/ast.ts index 1ed39e6..4db2ae2 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -173,6 +173,7 @@ export type TypeDefinition = { name: string params: string[] constructors: TypeConstructor[] + alias?: TypeAST line?: number column?: number start?: number diff --git a/src/parser.ts b/src/parser.ts index ba2b9be..8ede0b6 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -715,6 +715,13 @@ export class Parser { this.expect('equals'); + // If next token isn't a type-ident, it's a type alias + if (this.current().kind !== 'type-ident') { + const alias = this.parseType(); + if (this.current().kind === 'semicolon') this.advance(); + return { kind: 'type-definition', name, params, constructors: [], alias, ...this.getPos(nameToken) }; + } + // Parse constructors separated by | const constructors: TypeConstructor[] = []; constructors.push(this.parseTypeConstructor()); diff --git a/src/typechecker.ts b/src/typechecker.ts index cd84c84..c494c0f 100644 --- a/src/typechecker.ts +++ b/src/typechecker.ts @@ -26,8 +26,8 @@ function applySubst(type: TypeAST, subst: Subst): TypeAST { } function unify(t1: TypeAST, t2: TypeAST, subst: Subst): string | null { - const a = applySubst(t1, subst); - const b = applySubst(t2, subst); + const a = resolveAlias(applySubst(t1, subst)); + const b = resolveAlias(applySubst(t2, subst)); // Same type name if (a.kind === 'type-name' && b.kind === 'type-name' && a.name === b.name) return null; @@ -376,6 +376,7 @@ let typeConstructors = new Map(); export function typecheck(defs: Definition[], typeDefs: TypeDefinition[] = [], classDefs: ClassDefinition[] = [], instances: InstanceDeclaration[] = []) { const env: TypeEnv = new Map(); + const typeAliases = new Map(); // Register instances as a lookup: className -> Set of type names const instanceMap = new Map>(); @@ -395,6 +396,11 @@ export function typecheck(defs: Definition[], typeDefs: TypeDefinition[] = [], c // Register constructors for (const td of typeDefs) { + if (td.alias) { + typeAliases.set(td.name, { params: td.params, type: td.alias }); + continue; // don't register constructors for aliases + } + typeConstructors.set(td.name, td.constructors.map(c => c.name)); const resultType: TypeAST = td.params.length === 0 @@ -414,6 +420,8 @@ export function typecheck(defs: Definition[], typeDefs: TypeDefinition[] = [], c } } + moduleTypeAliases = typeAliases; + // Register all annotated defs in env first so they can ref eachother for (const def of defs) { if (def.annotation) { @@ -543,3 +551,32 @@ function checkExhaustiveness(scrutType: TypeAST, cases: { pattern: Pattern }[], warn(`Non-exhaustive match, missing: ${missing.join(', ')}`, expr); } } + +let moduleTypeAliases = new Map(); + +function resolveAlias(t: TypeAST): TypeAST { + if (t.kind === 'type-name' && moduleTypeAliases.has(t.name)) { + return moduleTypeAliases.get(t.name)!.type; + } + + if (t.kind === 'type-apply' && t.constructor.kind === 'type-name' && moduleTypeAliases.has(t.constructor.name)) { + const alias = moduleTypeAliases.get(t.constructor.name); + // Substitute params: alias.params[i] -> t.args[i] + let result = alias.type; + for (let i = 0; i < alias.params.length && i < t.args.length; i++) { + result = substituteTypeVar(alias.params[i], t.args[i], result); + } + return result; + } + return t; +} + +function substituteTypeVar(name: string, replacement: TypeAST, type: TypeAST): TypeAST { + switch (type.kind) { + case 'type-var': return type.name === name ? replacement : type; + case 'type-name': return type; + case 'type-function': return { kind: 'type-function', param: substituteTypeVar(name, replacement, type.param), result: substituteTypeVar(name, replacement, type.result) }; + case 'type-apply': return { kind: 'type-apply', constructor: substituteTypeVar(name, replacement, type.constructor), args: type.args.map(a => substituteTypeVar(name, replacement, a)) }; + case 'type-record': return { kind: 'type-record', fields: type.fields.map(f => ({ name: f.name, type: substituteTypeVar(name, replacement, f.type) })) }; + } +}