Typeclasses
This commit is contained in:
parent
a4daf88085
commit
8020f9d1a0
6 changed files with 123 additions and 19 deletions
19
src/ast.ts
19
src/ast.ts
|
|
@ -187,6 +187,25 @@ export type Constraint = {
|
||||||
typeVar: string
|
typeVar: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ClassDefinition = {
|
||||||
|
kind: 'class-definition'
|
||||||
|
name: string
|
||||||
|
param: string
|
||||||
|
methods: { name: string, type: TypeAST }[]
|
||||||
|
line?: number
|
||||||
|
column?: number
|
||||||
|
start?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InstanceDeclaration = {
|
||||||
|
kind: 'instance-declaration'
|
||||||
|
typeName: string
|
||||||
|
className: string
|
||||||
|
line?: number
|
||||||
|
column?: number
|
||||||
|
start?: number
|
||||||
|
}
|
||||||
|
|
||||||
export type AST =
|
export type AST =
|
||||||
| Literal
|
| Literal
|
||||||
| Variable
|
| Variable
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,22 @@ getX : { x : Int, y : Int } \ Int = p \ p.x;
|
||||||
setX : Int \ { x : Int, y : Int } \ { x : Int, y : Int } = newX p \ p.{ x = newX };
|
setX : Int \ { x : Int, y : Int } \ { x : Int, y : Int } = newX p \ p.{ x = newX };
|
||||||
myPoint = point 3 Blah;
|
myPoint = point 3 Blah;
|
||||||
|
|
||||||
# builtins
|
Num a {
|
||||||
# TODO: once we get typeclasses, make these the actual types
|
|
||||||
cat : a \ a \ a;
|
|
||||||
add : a \ a \ a;
|
add : a \ a \ a;
|
||||||
sub : a \ a \ a;
|
sub : a \ a \ a;
|
||||||
mul : a \ a \ a;
|
mul : a \ a \ a;
|
||||||
div : a \ a \ a;
|
div : a \ a \ a;
|
||||||
eq : a \ a \ Bool;
|
};
|
||||||
|
|
||||||
|
Int : Num;
|
||||||
|
Float : Num;
|
||||||
|
|
||||||
|
Eq a { eq : a \ a \ Bool }
|
||||||
|
Int : Eq;
|
||||||
|
String : Eq;
|
||||||
|
|
||||||
|
Semigroup a { cat : a \ a \ a };
|
||||||
|
String : Semigroup;
|
||||||
|
|
||||||
Maybe a = None | Some a;
|
Maybe a = None | Some a;
|
||||||
Bool = True | False;
|
Bool = True | False;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { AST, Pattern, Definition } from './ast';
|
import type { AST, Pattern, Definition, TypeDefinition, ClassDefinition, InstanceDeclaration } from './ast';
|
||||||
import { store } from './runtime-js';
|
import { store } from './runtime-js';
|
||||||
import { typecheck } from './typechecker';
|
import { typecheck } from './typechecker';
|
||||||
|
|
||||||
|
|
@ -221,8 +221,8 @@ function compilePattern(pattern: Pattern, expr: string): { condition: string, bi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function compileAndRun(defs: Definition[], typeDefs: TypeDefinition[] = []) {
|
export function compileAndRun(defs: Definition[], typeDefs: TypeDefinition[] = [], classDefs: ClassDefinition[] = [], instances: InstanceDeclaration[] = []) {
|
||||||
typecheck(defs, typeDefs);
|
typecheck(defs, typeDefs, classDefs, instances);
|
||||||
|
|
||||||
const compiledDefs: string[] = [];
|
const compiledDefs: string[] = [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,13 @@ store.viewport = { width: window.innerWidth, height: window.innerHeight };
|
||||||
try {
|
try {
|
||||||
const tokens = tokenize(cgCode);
|
const tokens = tokenize(cgCode);
|
||||||
const parser = new Parser(tokens, cgCode);
|
const parser = new Parser(tokens, cgCode);
|
||||||
const { definitions: defs, typeDefinitions: typeDefs } = parser.parse();
|
const { definitions: defs, typeDefinitions: typeDefs, classDefinitions: classDefs, instanceDeclarations: instances } = parser.parse();
|
||||||
loadDefinitions();
|
loadDefinitions();
|
||||||
|
|
||||||
// TODO remove once we're booting from store, files are backup
|
// TODO remove once we're booting from store, files are backup
|
||||||
if (!store.paletteHistory) store.paletteHistory = [];
|
if (!store.paletteHistory) store.paletteHistory = [];
|
||||||
|
|
||||||
compileAndRun(defs, typeDefs);
|
compileAndRun(defs, typeDefs, classDefs, instances);
|
||||||
saveDefinitions();
|
saveDefinitions();
|
||||||
|
|
||||||
runAppCompiled(canvas, store);
|
runAppCompiled(canvas, store);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Token } from './lexer'
|
import type { Token } from './lexer'
|
||||||
import type { AST, MatchCase, Pattern, Definition, TypeAST, TypeDefinition, TypeConstructor, Annotation } from './ast'
|
import type { AST, MatchCase, Pattern, Definition, TypeAST, TypeDefinition, TypeConstructor, Annotation, ClassDefinition, InstanceDeclaration } from './ast'
|
||||||
import { ParseError } from './error'
|
import { ParseError } from './error'
|
||||||
|
|
||||||
export class Parser {
|
export class Parser {
|
||||||
|
|
@ -150,20 +150,32 @@ export class Parser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parse(): { definitions: Definition[], typeDefinitions: TypeDefinition[] } {
|
parse(): { definitions: Definition[], typeDefinitions: TypeDefinition[], classDefinitions: ClassDefinition[], instanceDeclarations: InstanceDeclaration[] } {
|
||||||
const definitions: Definition[] = [];
|
const definitions: Definition[] = [];
|
||||||
const typeDefinitions: TypeDefinition[] = [];
|
const typeDefinitions: TypeDefinition[] = [];
|
||||||
|
const classDefinitions: ClassDefinition[] = [];
|
||||||
|
const instanceDeclarations: InstanceDeclaration[] = [];
|
||||||
|
|
||||||
while (this.current().kind !== 'eof') {
|
while (this.current().kind !== 'eof') {
|
||||||
if (this.current().kind === 'type-ident') {
|
if (this.current().kind === 'type-ident') {
|
||||||
|
let offset = 1;
|
||||||
|
while (this.peek(offset).kind === 'ident') offset++;
|
||||||
|
const after = this.peek(offset);
|
||||||
|
|
||||||
|
if (after.kind === 'open-brace') {
|
||||||
|
classDefinitions.push(this.parseClassDefinition());
|
||||||
|
} else if (after.kind === 'colon') {
|
||||||
|
instanceDeclarations.push(this.parseInstanceDeclaration());
|
||||||
|
} else {
|
||||||
typeDefinitions.push(this.parseTypeDefinition());
|
typeDefinitions.push(this.parseTypeDefinition());
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
definitions.push(this.parseDefinition());
|
definitions.push(this.parseDefinition());
|
||||||
}
|
}
|
||||||
|
|
||||||
return { definitions, typeDefinitions };
|
return { definitions, typeDefinitions, classDefinitions, instanceDeclarations };
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseDefinition(): Definition {
|
private parseDefinition(): Definition {
|
||||||
|
|
@ -662,4 +674,32 @@ export class Parser {
|
||||||
}
|
}
|
||||||
return { name, args };
|
return { name, args };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private parseClassDefinition(): ClassDefinition {
|
||||||
|
const nameToken = this.advance();
|
||||||
|
const param = (this.expect('ident') as { value: string }).value;
|
||||||
|
|
||||||
|
this.expect('open-brace');
|
||||||
|
const methods: { name: string, type: TypeAST }[] = [];
|
||||||
|
while (this.current().kind !== 'close-brace') {
|
||||||
|
const methodName = (this.expect('ident') as { value: string }).value;
|
||||||
|
this.expect('colon');
|
||||||
|
const type = this.parseType();
|
||||||
|
methods.push({ name: methodName, type });
|
||||||
|
if (this.current().kind === 'semicolon') this.advance();
|
||||||
|
}
|
||||||
|
this.expect('close-brace');
|
||||||
|
if (this.current().kind === 'semicolon') this.advance();
|
||||||
|
|
||||||
|
return { kind: 'class-definition', name: (nameToken as { value: string }).value, param, methods, ...this.getPos(nameToken) };
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseInstanceDeclaration(): InstanceDeclaration {
|
||||||
|
const typeToken = this.advance();
|
||||||
|
this.expect('colon');
|
||||||
|
const classToken = this.expect('type-ident');
|
||||||
|
if (this.current().kind === 'semicolon') this.advance();
|
||||||
|
|
||||||
|
return { kind: 'instance-declaration', typeName: (typeToken as { value: string }).value, className: (classToken as { value: string }).value, ...this.getPos(typeToken) };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { AST, TypeAST, Pattern, Definition, TypeDefinition } from './ast'
|
import type { AST, TypeAST, Pattern, Definition, TypeDefinition, ClassDefinition, InstanceDeclaration } from './ast'
|
||||||
import { prettyPrintType } from './ast'
|
import { prettyPrintType } from './ast'
|
||||||
|
|
||||||
// Map type var names to their types
|
// Map type var names to their types
|
||||||
|
|
@ -155,6 +155,24 @@ function infer(expr: AST, env: TypeEnv, subst: Subst): TypeAST | null {
|
||||||
if (err) warn(err, arg);
|
if (err) warn(err, arg);
|
||||||
current = applySubst(current.result, subst);
|
current = applySubst(current.result, subst);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check typeclass constraints
|
||||||
|
if (expr.func.kind === 'variable') {
|
||||||
|
const constraint = moduleConstraints.get(expr.func.name);
|
||||||
|
if (constraint) {
|
||||||
|
const argType = infer(expr.args[0], env, subst);
|
||||||
|
if (argType) {
|
||||||
|
const resolved = applySubst(argType, subst);
|
||||||
|
if (resolved.kind === 'type-name') {
|
||||||
|
const instances = moduleInstances.get(constraint.className);
|
||||||
|
if (!instances || !instances.has(resolved.name)) {
|
||||||
|
warn(`No instance ${constraint.className} ${resolved.name}`, expr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -309,9 +327,28 @@ function bindPattern(pattern: Pattern, type: TypeAST, env: TypeEnv, subst: Subst
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function typecheck(defs: Definition[], typeDefs: TypeDefinition[] = []) {
|
let moduleConstraints = new Map<string, { param: string, className: string }>();
|
||||||
|
let moduleInstances = new Map<string, Set<string>>();
|
||||||
|
|
||||||
|
export function typecheck(defs: Definition[], typeDefs: TypeDefinition[] = [], classDefs: ClassDefinition[] = [], instances: InstanceDeclaration[] = []) {
|
||||||
const env: TypeEnv = new Map();
|
const env: TypeEnv = new Map();
|
||||||
|
|
||||||
|
// Register instances as a lookup: className -> Set of type names
|
||||||
|
const instanceMap = new Map<string, Set<string>>();
|
||||||
|
for (const inst of instances) {
|
||||||
|
if (!instanceMap.has(inst.className)) instanceMap.set(inst.className, new Set());
|
||||||
|
instanceMap.get(inst.className)!.add(inst.typeName);
|
||||||
|
}
|
||||||
|
moduleInstances = instanceMap;
|
||||||
|
|
||||||
|
// Register class methods with constraints in env
|
||||||
|
for (const cls of classDefs) {
|
||||||
|
for (const method of cls.methods) {
|
||||||
|
env.set(method.name, method.type);
|
||||||
|
moduleConstraints.set(method.name, { param: cls.param, className: cls.name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Register constructors
|
// Register constructors
|
||||||
for (const td of typeDefs) {
|
for (const td of typeDefs) {
|
||||||
const resultType: TypeAST = td.params.length === 0
|
const resultType: TypeAST = td.params.length === 0
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue