You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

390 lines
11 KiB
TypeScript

import type { Token } from './lexer'
import type { AST, MatchCase, Pattern } from './ast'
export class Parser {
private tokens: Token[]
private pos: number = 0
constructor(tokens: Token[]) {
this.tokens = tokens;
}
private current(): Token {
if (this.pos >= this.tokens.length) {
return { kind: 'eof' } as Token;
}
return this.tokens[this.pos];
}
private advance(): Token {
return this.tokens[this.pos++];
}
private peek(offset = 1): Token {
const pos = this.pos + offset;
if (pos >= this.tokens.length) {
return { kind: 'eof' } as Token;
}
return this.tokens[pos];
}
private expect(kind: Token['kind']): Token {
const token = this.current();
if (token.kind !== kind) {
throw new Error(`Expected ${kind}, got ${token.kind}`);
}
return this.advance();
}
private isInfixOp(): boolean {
const kind = this.current().kind;
return kind === 'plus' || kind === 'minus' ||
kind === 'star' || kind === 'slash' ||
kind === 'greater-than' || kind === 'ampersand';
}
private tokenToOpName(token: Token): string {
switch (token.kind) {
case 'plus': return '+';
case 'minus': return '-';
case 'star': return '*';
case 'slash': return '/';
case 'greater-than': return '>';
case 'ampersand': return '&';
default: throw new Error(`Not an operator: ${token.kind}`);
}
}
private canStartPrimary(): boolean {
const kind = this.current().kind;
return kind === 'ident' || kind === 'type-ident' ||
kind === 'int' || kind === 'float' || kind === 'string' ||
kind === 'open-paren' || kind === 'open-bracket' ||
kind === 'open-brace';
}
private isLambdaStart(): boolean {
const kind = this.current().kind;
if (kind === 'backslash') {
return true;
}
if (kind !== 'ident') {
return false;
}
let offset = 1;
while (true) {
const token = this.peek(offset);
if (token.kind === 'backslash') return true;
if (token.kind !== 'ident') return false;
offset++;
}
}
parse(): AST {
return this.parseExpression();
}
private parseExpression(): AST {
// Lambda
if (this.isLambdaStart()) {
return this.parseLambda();
}
// Let
if (this.current().kind === 'ident' && this.peek().kind === 'equals') {
return this.parseLet();
}
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 {
const params: string[] = [];
while (this.current().kind === 'ident') {
const param = this.advance();
params.push((param as { value: string }).value);
}
this.expect('backslash');
const body = this.parseExpression();
return { kind: 'lambda', params, body };
}
private parseLet(): AST {
const nameToken = this.expect('ident');
const name = (nameToken as { value: string }).value;
this.expect('equals');
const value = this.parseExpression();
this.expect('semicolon');
const body = this.parseExpression();
return { kind: 'let', name, value, body };
}
private parseInfix(): AST {
let left = this.parseApplication();
while (this.isInfixOp()) {
const opToken = this.advance();
const opName = this.tokenToOpName(opToken);
const right = this.parseApplication();
left = {
kind: 'apply',
func: { kind: 'variable', name: opName },
args: [left, right]
}
}
return left;
}
private parseApplication(): AST {
let func = this.parsePostfix();
while (this.canStartPrimary()) {
const arg = this.parsePostfix();
func = { kind: 'apply', func, args: [arg] };
}
return func;
}
private parsePostfix(): AST {
let expr = this.parsePrimary();
while (true) {
if (this.current().kind === 'dot') {
// Record access
this.advance();
const fieldToken = this.expect('ident');
const field = (fieldToken as { value: string }).value;
expr = { kind: 'record-access', record: expr, field };
} else if (this.current().kind === 'open-brace') {
// Record update
this.advance();
const updates: { [key: string]: AST } = {};
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');
updates[key] = this.parseExpression();
}
this.expect('close-brace');
expr = { kind: 'record-update', record: expr, updates }
} else {
break;
}
}
return expr;
}
private parsePrimary(): AST {
const token = this.current();
if (token.kind === 'open-paren') {
this.advance();
const expr = this.parseExpression();
this.expect('close-paren');
return expr;
}
if (token.kind === 'open-bracket') {
this.advance();
const items: AST[] = [];
let first = true;
while (this.current().kind !== 'close-bracket') {
if (!first) {
this.expect('comma');
}
first = false;
items.push(this.parseExpression());
}
this.expect('close-bracket');
return { kind: 'list', elements: items };
}
if (token.kind === 'open-brace') {
this.advance();
const fields: { [key: string]: AST } = {};
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.parseExpression();
}
this.expect('close-brace');
return { kind: 'record', fields };
}
if (token.kind === 'int') {
this.advance();
return { kind: 'literal', value: { kind: 'int', value: token.value } };
}
if (token.kind === 'float') {
this.advance();
return { kind: 'literal', value: { kind: 'float', value: token.value } };
}
if (token.kind === 'string') {
this.advance();
return { kind: 'literal', value: { kind: 'string', value: token.value } };
}
if (token.kind === 'ident') {
this.advance();
return { kind: 'variable', name: token.value };
}
if (token.kind === 'type-ident') {
this.advance();
return { kind: 'constructor', name: token.value };
}
throw new Error(`Unexpected token: ${token.kind}`);
}
}