Type constraints

This commit is contained in:
Dustin Swan 2026-03-28 21:39:53 -06:00
parent f1bba4f58d
commit 784e095345
No known key found for this signature in database
GPG key ID: 30D46587E2100467
4 changed files with 46 additions and 4 deletions

View file

@ -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;

View file

@ -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 {

View file

@ -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 :: <type>
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;

View file

@ -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 });
}
}
}