diff --git a/src/ast.ts b/src/ast.ts index 5e1d0f2..07caf77 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -187,6 +187,25 @@ export type Constraint = { 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 = | Literal | Variable diff --git a/src/cg/01-stdlib.cg b/src/cg/01-stdlib.cg index 562e5b5..8852925 100644 --- a/src/cg/01-stdlib.cg +++ b/src/cg/01-stdlib.cg @@ -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 }; myPoint = point 3 Blah; -# builtins -# TODO: once we get typeclasses, make these the actual types -cat : a \ a \ a; -add : a \ a \ a; -sub : a \ a \ a; -mul : a \ a \ a; -div : a \ a \ a; -eq : a \ a \ Bool; +Num a { + add : a \ a \ a; + sub : a \ a \ a; + mul : a \ a \ a; + div : a \ a \ a; +}; + +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; Bool = True | False; diff --git a/src/compiler.ts b/src/compiler.ts index 3d116fa..2c3918f 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -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 { typecheck } from './typechecker'; @@ -221,8 +221,8 @@ function compilePattern(pattern: Pattern, expr: string): { condition: string, bi } } -export function compileAndRun(defs: Definition[], typeDefs: TypeDefinition[] = []) { - typecheck(defs, typeDefs); +export function compileAndRun(defs: Definition[], typeDefs: TypeDefinition[] = [], classDefs: ClassDefinition[] = [], instances: InstanceDeclaration[] = []) { + typecheck(defs, typeDefs, classDefs, instances); const compiledDefs: string[] = []; diff --git a/src/main.ts b/src/main.ts index 4e31cb4..d6aa963 100644 --- a/src/main.ts +++ b/src/main.ts @@ -23,13 +23,13 @@ store.viewport = { width: window.innerWidth, height: window.innerHeight }; try { const tokens = tokenize(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(); // TODO remove once we're booting from store, files are backup if (!store.paletteHistory) store.paletteHistory = []; - compileAndRun(defs, typeDefs); + compileAndRun(defs, typeDefs, classDefs, instances); saveDefinitions(); runAppCompiled(canvas, store); diff --git a/src/parser.ts b/src/parser.ts index 4891672..04fb806 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,5 +1,5 @@ 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' 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 typeDefinitions: TypeDefinition[] = []; + const classDefinitions: ClassDefinition[] = []; + const instanceDeclarations: InstanceDeclaration[] = []; while (this.current().kind !== 'eof') { if (this.current().kind === 'type-ident') { - typeDefinitions.push(this.parseTypeDefinition()); + 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()); + } continue; } definitions.push(this.parseDefinition()); } - return { definitions, typeDefinitions }; + return { definitions, typeDefinitions, classDefinitions, instanceDeclarations }; } private parseDefinition(): Definition { @@ -662,4 +674,32 @@ export class Parser { } 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) }; + } } diff --git a/src/typechecker.ts b/src/typechecker.ts index e813d4a..47d1e1e 100644 --- a/src/typechecker.ts +++ b/src/typechecker.ts @@ -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' // 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); 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; } @@ -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(); +let moduleInstances = new Map>(); + +export function typecheck(defs: Definition[], typeDefs: TypeDefinition[] = [], classDefs: ClassDefinition[] = [], instances: InstanceDeclaration[] = []) { const env: TypeEnv = new Map(); + // Register instances as a lookup: className -> Set of type names + const instanceMap = new Map>(); + 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 for (const td of typeDefs) { const resultType: TypeAST = td.params.length === 0