diff --git a/src/ast.ts b/src/ast.ts index 36cb678..976d39a 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -146,6 +146,7 @@ export type Definition = { line?: number column?: number start?: number + annotation?: Annotation } export type Rebind = { @@ -157,6 +158,35 @@ export type Rebind = { start?: number } +export type TypeAST = + | { kind: 'type-name', name: string } // Int, String, Bool + | { kind: 'type-var', name: string } // a, b + | { kind: 'type-function', param: TypeAST, result: TypeAST } // a \ b + | { kind: 'type-apply', constructor: TypeAST, args: TypeAST[] } // List a, Maybe Int + | { kind: 'type-record', fields: { name: string, type: TypeAST }[] } // { x: Int } + +export type TypeConstructor = { name: string, args: TypeAST[] } + +export type TypeDefinition = { + kind: 'type-definition' + name: string + params: string[] + constructors: TypeConstructor[] + line?: number + column?: number + start?: number +} + +export type Annotation = { + constraints: Constraint[] + type: TypeAST +} + +export type Constraint = { + className: string + typeVar: string +} + export type AST = | Literal | Variable @@ -292,7 +322,10 @@ export function prettyPrint(ast: AST, indent = 0): string { return `...${prettyPrint(ast.spread, indent)}`; case 'definition': - return `${ast.name} = ${prettyPrint(ast.body, indent)};`; + const ann = ast.annotation + ? `${ast.name} : ${prettyPrintType(ast.annotation.type)};\n` + : ''; + return `${ann}${ast.name} = ${prettyPrint(ast.body, indent)};`; default: return `Unknown AST kind: ${i}${(ast as any).kind}` @@ -338,6 +371,44 @@ function prettyPrintPattern(pattern: Pattern): string { } } +function prettyPrintType(type: TypeAST): string { + switch (type.kind) { + case 'type-name': + case 'type-var': + return type.name; + case 'type-function': + const param = type.param.kind === 'type-function' + ? `(${prettyPrintType(type.param)})` + : prettyPrintType(type.param); + return `${param} \\ ${prettyPrintType(type.result)}`; + case 'type-apply': + const args = type.args.map(a => + a.kind === 'type-function' || a.kind === 'type-apply' + ? `(${prettyPrintType(a)})` + : prettyPrintType(a) + ).join(' '); + return `${prettyPrintType(type.constructor)} ${args}`; + case 'type-record': + const fields = type.fields + .map(f => `${f.name} : ${prettyPrintType(f.type)}`) + .join(', '); + return `{ ${fields} }`; + } +} + +function prettyPrintTypeDefinition(td: TypeDefinition): string { + const params = td.params.length > 0 ? ' ' + td.params.join(' ') : ''; + const ctors = td.constructors.map(c => { + const args = c.args.map(a => + a.kind === 'type-function' || a.kind === 'type-apply' + ? `(${prettyPrintType(a)})` + : prettyPrintType(a) + ).join(' '); + return args ? `${c.name} ${args}` : c.name; + }).join(' | '); + return `${td.name}${params} = ${ctors};`; +} + function needsQuotes(key: string): boolean { return key === '_' || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key); } diff --git a/src/cg/01-stdlib.cg b/src/cg/01-stdlib.cg index 0686984..83a97fb 100644 --- a/src/cg/01-stdlib.cg +++ b/src/cg/01-stdlib.cg @@ -1,3 +1,6 @@ +Maybe a = None | Some a; + + # nth : Int \ List a \ Maybe a # in host at the moment, until we get typeclasses or something and this can work on strings too # nth = i list \ [i, list] @@ -5,7 +8,7 @@ # | [0, [x, ...xs]] \ (Some x) # | [n, [x, ...xs]] \ nth (n - 1) xs; -# map : (a \ b) \ List a \ List b +map : (a \ b) \ List a \ List b; map = f list \ list | [] \ [] | [x, ...xs] \ [f x, ...map f xs]; diff --git a/src/main.ts b/src/main.ts index 07edb31..a78e982 100644 --- a/src/main.ts +++ b/src/main.ts @@ -23,7 +23,7 @@ store.viewport = { width: window.innerWidth, height: window.innerHeight }; try { const tokens = tokenize(cgCode); const parser = new Parser(tokens, cgCode); - const defs = parser.parse(); + const { definitions: defs } = parser.parse(); loadDefinitions(); // TODO remove once we're booting from store, files are backup diff --git a/src/parser.ts b/src/parser.ts index 1e25974..f1ecc9c 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,5 +1,5 @@ import type { Token } from './lexer' -import type { AST, MatchCase, Pattern, Definition } from './ast' +import type { AST, MatchCase, Pattern, Definition, TypeAST, TypeDefinition, TypeConstructor } from './ast' import { ParseError } from './error' export class Parser { @@ -150,14 +150,35 @@ export class Parser { } } - parse(): Definition[] { + parse(): { definitions: Definition[], typeDefinitions: TypeDefinition[] } { const definitions: Definition[] = []; + const typeDefinitions: TypeDefinition[] = []; while (this.current().kind !== 'eof') { + if (this.current().kind === 'type-ident') { + typeDefinitions.push(this.parseTypeDefinition()); + continue; + } + + // type annotation + if (this.current().kind === 'ident' && this.peek().kind === 'colon') { + this.advance(); // eat ident + + this.advance(); + const type = this.parseType(); + this.expect('semicolon'); + + // parse definition + const def = this.parseDefinition(); + def.annotation = { constraints: [], type }; + + definitions.push(def); + continue; + } definitions.push(this.parseDefinition()); } - return definitions; + return { definitions, typeDefinitions }; } private parseDefinition(): Definition { @@ -545,4 +566,103 @@ export class Parser { throw ParseError(`Unexpected token: ${token.kind}`, token.line, token.column, this.source); } + + private parseTypeAtom(): TypeAST { + const token = this.current(); + + if (token.kind === 'type-ident') { + this.advance(); + return { kind: 'type-name', name: token.value }; + } + + if (token.kind === 'ident') { + this.advance(); + return { kind: 'type-var', name: token.value }; + } + + if (token.kind === 'open-paren') { + this.advance(); + const type = this.parseType(); + this.expect('close-paren'); + return type; + } + + if (token.kind === 'open-brace') { + this.advance(); + const fields: { name: string, type: TypeAST }[] = []; + while (this.current().kind !== 'close-brace') { + const name = this.expectIdent() as { value: string }; + this.expect('colon'); + const type = this.parseType(); + fields.push({ name: name.value, type }); + if (this.current().kind === 'comma') this.advance(); + } + this.expect('close-brace'); + return { kind: 'type-record', fields }; + } + + throw ParseError(`Expected type, got ${token.kind}`, token.line, token.column, this.source); + } + + private parseTypeApply(): TypeAST { + const base = this.parseTypeAtom(); + const args: TypeAST[] = []; + while (this.canStartTypeAtom()) { + args.push(this.parseTypeAtom()); + } + if (args.length === 0) return base; + return { kind: 'type-apply', constructor: base, args }; + } + + private canStartTypeAtom(): boolean { + const kind = this.current().kind; + return kind === 'type-ident' || kind === 'ident' || + kind === 'open-paren' || kind === 'open-brace'; + } + + private parseType(): TypeAST { + const left = this.parseTypeApply(); + + if (this.current().kind === 'backslash') { + this.advance(); + const right = this.parseType(); + return { kind: 'type-function', param: left, result: right }; + } + + return left; + } + + private parseTypeDefinition(): TypeDefinition { + const nameToken = this.advance(); + const name = (nameToken as { value: string }).value; + + // Collect type params + const params: string[] = []; + while (this.current().kind === 'ident') { + params.push((this.advance() as { value: string }).value); + } + + this.expect('equals'); + + // Parse constructors separated by | + const constructors: TypeConstructor[] = []; + constructors.push(this.parseTypeConstructor()); + while (this.current().kind === 'pipe') { + this.advance(); + constructors.push(this.parseTypeConstructor()); + } + + if (this.current().kind === 'semicolon') this.advance(); + + return { kind: 'type-definition', name, params, constructors, ...this.getPos(nameToken) }; + } + + private parseTypeConstructor(): TypeConstructor { + const name = (this.expect('type-ident') as { value: string }).value; + const args: TypeAST[] = []; + while (this.canStartTypeAtom()) { + args.push(this.parseTypeAtom()); + } + return { name, args }; + } } diff --git a/src/runtime-js.ts b/src/runtime-js.ts index db04c8a..6235707 100644 --- a/src/runtime-js.ts +++ b/src/runtime-js.ts @@ -209,7 +209,7 @@ export const _rt = { const fullCode = trimmed.endsWith(';') ? trimmed : trimmed + ';'; const tokens = tokenize(fullCode); const parser = new Parser(tokens, fullCode); - const defs = parser.parse(); + const { definitions: defs } = parser.parse(); if (defs.length > 0) { const def = defs[0]; @@ -229,7 +229,7 @@ export const _rt = { const wrapped = `_expr = ${trimmed};`; const tokens = tokenize(wrapped); const parser = new Parser(tokens, wrapped); - const defs = parser.parse(); + const { definitions: defs } = parser.parse(); const ast = defs[0].body; // validate free vars @@ -275,7 +275,7 @@ export function loadDefinitions() { for (const [_, source] of Object.entries(saved)) { const tokens = tokenize(source as string); const parser = new Parser(tokens, source as string); - const defs = parser.parse(); + const { definitions: defs } = parser.parse(); if (defs.length > 0) { recompile(defs[0].name, defs[0].body); }