diff --git a/src/ast.ts b/src/ast.ts index e608f63..4f3cfb6 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -127,7 +127,7 @@ export function prettyPrint(ast: AST, indent = 0): string { switch (ast.kind) { case 'literal': - return `${i}${ast.value.value}`; + return `${i}${ast.value}`; case 'variable': return `${i}${ast.name}`; @@ -154,17 +154,56 @@ export function prettyPrint(ast: AST, indent = 0): string { return `${i}{${fields}}`; case 'lambda': - const params = ast.params.join(', ') + const params = ast.params.join(', '); return `${i}(${params}) => ${prettyPrint(ast.body)}` case 'record-access': - return `${i}${prettyPrint(ast.record)}.${ast.field}` + return `${i}${prettyPrint(ast.record)}.${ast.field}`; case 'record-update': const updates = Object.entries(ast.updates).map(([k, v]) => `${k} = ${prettyPrint(v, 0)}`).join(', '); return `${i}${prettyPrint(ast.record)} { ${updates} }` + case 'match': + const expr = prettyPrint(ast.expr, 0); + const cases = ast.cases + .map(c => ` | ${prettyPrintPattern(c.pattern)} -> ${prettyPrint(c.result, 0)}`) + .join('\n'); + return `${i}match ${expr}\n${cases}`; + + default: return `${i}${ast.kind}` } } + +function prettyPrintPattern(pattern: Pattern): string { + switch (pattern.kind) { + case 'wildcard': + return '_'; + + case 'var': + return pattern.name; + + case 'literal': + return JSON.stringify(pattern.value); + + case 'constructor': + if (pattern.args.length === 0) { + return pattern.name; + } + const args = pattern.args.map(prettyPrintPattern).join(' '); + return `(${pattern.name} ${args})`; + + case 'list': + const elems = pattern.elements.map(prettyPrintPattern).join(', '); + return `[${elems}]`; + + case 'record': + const fields = Object.entries(pattern.fields) + .map(([k, p]) => `${k} = ${prettyPrintPattern(p)}`) + .join(', '); + + return `{${fields}}`; + } +} diff --git a/src/lexer.ts b/src/lexer.ts index f8fe039..86a5131 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -48,6 +48,7 @@ export function tokenize(source: string): Token[] { continue; } + // Comments if (char === '#') { while (i < source.length && source[i] !== '\n') { i++; @@ -76,7 +77,7 @@ export function tokenize(source: string): Token[] { continue; } - // Idents + // Idents & Wildcard if (/[A-Za-z_]/.test(char)) { let str = ''; while (i < source.length && /[A-Za-z0-9_!-]/.test(source[i])) { @@ -84,11 +85,16 @@ export function tokenize(source: string): Token[] { i++; } - const isType = /[A-Z]/.test(str[0]); + if (str === '_') { + // Wildcards + tokens.push({ kind: 'underscore' }); + } else { + const isType = /[A-Z]/.test(str[0]); - tokens.push(isType - ? { kind: 'type-ident', value: str } - : { kind: 'ident', value: str }); + tokens.push(isType + ? { kind: 'type-ident', value: str } + : { kind: 'ident', value: str }); + } continue; } diff --git a/src/main.ts b/src/main.ts index 4be01f2..6e53a64 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,11 +5,15 @@ import { Parser } from './parser' import { prettyPrint } from './ast'; function e(str: string) { + console.log(str); const tokens = tokenize(str); + console.log(tokens); const p = new Parser(tokens); const ast = p.parse(); + console.log(ast); + console.log(prettyPrint(ast)); const env: Env = new Map(); - console.log(str, tokens, prettyPrint(ast), evaluate(ast, env)); + // console.log(evaluate(ast, env)); } e('add1 = (x \\ x + 1); add1 3'); @@ -21,3 +25,6 @@ e('rec = { a = 3, b = 5 }; rec{ a = 10 }'); e('add1 = (x \\ x + 1); 3 > add1'); e('[1, 2] & [3, 4]'); e('"abc" & "def"'); +// e('n | 0 \\ 1 | _ \\ 99'); +e('m | Some x \\ 1 | None \\ 0'); +e('head = list \\ list | [x, _] \\ Some x | [] \\ None; head'); diff --git a/src/parser.ts b/src/parser.ts index a210614..6493355 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,5 +1,5 @@ import type { Token } from './lexer' -import type { AST } from './ast' +import type { AST, MatchCase, Pattern } from './ast' export class Parser { private tokens: Token[] @@ -92,15 +92,128 @@ export class Parser { } private parseExpression(): AST { + // Lambda if (this.isLambdaStart()) { return this.parseLambda(); } + // Let if (this.current().kind === 'ident' && this.peek().kind === 'equals') { return this.parseLet(); } - return this.parseInfix(); + let expr = this.parseInfix(); + + // Match + if (this.current().kind === 'pipe') { + return this.parseMatch(expr); + } + + return expr; + } + + private parseMatch(expr: AST): AST { + const cases: MatchCase[] = []; + + while(this.current().kind === 'pipe') { + this.advance(); + const pattern = this.parsePattern(); + this.expect('backslash'); + const result = this.parseInfix(); + cases.push({ pattern, result }) + } + + return { kind: 'match', expr, cases }; + } + + private parsePattern(): Pattern { + const token = this.current(); + + // Wildcard + if (token.kind === 'underscore') { + this.advance(); + return { kind: 'wildcard' }; + } + + // Literal + if (token.kind === 'int' || token.kind === 'float' || token.kind === 'string') { + this.advance(); + return { kind: 'literal', value: token.value }; + } + + // Variable + if (token.kind === 'ident') { + this.advance(); + return { kind: 'var', name: token.value }; + } + + // Constructor + if (token.kind === 'type-ident') { + this.advance(); + const name = token.value; + const args: Pattern[] = []; + + while (this.canStartPattern()) { + args.push(this.parsePattern()); + } + + return { kind: 'constructor', name, args }; + } + + // List + if (token.kind === 'open-bracket') { + this.advance(); + const elements: Pattern[] = []; + let first = true; + + while (this.current().kind !== 'close-bracket') { + if (!first) this.expect('comma'); + first = false; + elements.push(this.parsePattern()); + } + + this.expect('close-bracket'); + return { kind: 'list', elements }; + } + + // Record + if (token.kind === 'open-brace') { + this.advance(); + const fields: { [key: string]: Pattern }= []; + let first = true; + + while (this.current().kind !== 'close-brace') { + if (!first) this.expect('comma'); + first = false; + + const keyToken = this.expect('ident'); + const key = (keyToken as { value: string }).value; + this.expect('equals'); + fields[key] = this.parsePattern(); + } + + this.expect('close-brace'); + return { kind: 'list', fields }; + } + + // Parens + if (token.kind === 'open-paren') { + this.advance(); + const pattern = this.parsePattern(); + this.expect('close-paren'); + return pattern; + } + + throw new Error(`Unexpected token in pattern: ${token.kind}`) + } + + private canStartPattern(): boolean { + const kind = this.current().kind; + + return kind === 'underscore' || kind === 'ident' || + kind === 'type-ident' || kind === 'int' || + kind === 'float' || kind === 'string' || + kind === 'open-paren'; } private parseLambda(): AST {