From 784e095345c3b8877d287e0605205dbf43fc3d18 Mon Sep 17 00:00:00 2001 From: Dustin Swan Date: Sat, 28 Mar 2026 21:39:53 -0600 Subject: [PATCH] Type constraints --- src/cg/01-stdlib.cg | 3 +-- src/lexer.ts | 6 +++++- src/parser.ts | 35 ++++++++++++++++++++++++++++++++++- src/typechecker.ts | 6 ++++++ 4 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/cg/01-stdlib.cg b/src/cg/01-stdlib.cg index 680b526..2997bd7 100644 --- a/src/cg/01-stdlib.cg +++ b/src/cg/01-stdlib.cg @@ -195,8 +195,7 @@ range : Int \ Int \ List Int = start end \ start >= end | True \ [] | False \ [start, ...range (start + 1) end]; -# TODO Number -sum : List a \ a = list \ fold add 0 list; +sum : Num a :: List a \ a = list \ fold add 0 list; any : (a \ Bool) \ List a \ Bool = f list \ fold (acc x \ or acc (f x)) False list; diff --git a/src/lexer.ts b/src/lexer.ts index 0789c58..44054ab 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -25,6 +25,7 @@ export type Token = ( // Symbols | { kind: 'colon' } + | { kind: 'double-colon' } | { kind: 'colon-equals' } | { kind: 'semicolon' } | { kind: 'backslash' } @@ -216,7 +217,10 @@ export function tokenize(source: string): Token[] { break; } case ':': { - if (source[i + 1] === '=') { + if (source[i + 1] === ':') { + tokens.push({ kind: 'double-colon', line: startLine, column: startColumn, start }); + advance(); + } else if (source[i + 1] === '=') { tokens.push({ kind: 'colon-equals', line: startLine, column: startColumn, start }); advance(); } else { diff --git a/src/parser.ts b/src/parser.ts index 04fb806..ea2d162 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -185,7 +185,7 @@ export class Parser { let annotation: Annotation | undefined; if (this.current().kind === 'colon') { this.advance(); - annotation = { constraints: [], type: this.parseType() }; + annotation = { constraints: this.parsedConstraints, type: this.parseType() }; // Declaration only if (this.current().kind === 'semicolon') { @@ -629,7 +629,12 @@ export class Parser { kind === 'open-paren' || kind === 'open-brace'; } + private parsedConstraints: { className: string, typeVar: string }[] = []; + private parseType(): TypeAST { + // Check for constraints: Num a, Eq b :: + this.parsedConstraints = this.tryParseConstraints(); + const left = this.parseTypeApply(); if (this.current().kind === 'backslash') { @@ -641,6 +646,34 @@ export class Parser { return left; } + private tryParseConstraints(): { className: string, typeVar: string }[] { + // Look ahead for :: to decide if we have constraints + let offset = 0; + let foundDoubleColon = false; + while (true) { + const t = this.peek(offset); + if (t.kind === 'double-colon') { foundDoubleColon = true; break } + if (t.kind === 'backslash' || t.kind === 'semicolon' || t.kind === 'equals' || t.kind === 'eof') break; + offset++; + } + if (!foundDoubleColon) return []; + + // Parse constraints: ClassName varName (, ClassName varName)* + const constraints: { className: string, typeVar: string }[] = []; + while (true) { + const className = (this.expect('type-ident') as { value: string }).value; + const typeVar = (this.expect('ident') as { value: string }).value; + constraints.push({ className, typeVar }); + if (this.current().kind === 'comma') { + this.advance(); + } else { + break; + } + } + this.expect('double-colon'); + return constraints; + } + private parseTypeDefinition(): TypeDefinition { const nameToken = this.advance(); const name = (nameToken as { value: string }).value; diff --git a/src/typechecker.ts b/src/typechecker.ts index 1f9a618..cd84c84 100644 --- a/src/typechecker.ts +++ b/src/typechecker.ts @@ -418,6 +418,12 @@ export function typecheck(defs: Definition[], typeDefs: TypeDefinition[] = [], c for (const def of defs) { if (def.annotation) { env.set(def.name, def.annotation.type); + + // Register explicit constraints + if (def.annotation.constraints.length > 0) { + const c = def.annotation.constraints[0]; + moduleConstraints.set(def.name, { param: c.typeVar, className: c.className }); + } } }