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.
465 lines
13 KiB
TypeScript
465 lines
13 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 === 'percent' || kind === 'caret' ||
|
|
kind === 'ampersand' ||
|
|
kind === 'equals-equals' || kind === 'not-equals' ||
|
|
kind === 'less-than' || kind === 'less-equals' ||
|
|
kind === 'greater-than' || kind === 'greater-equals' ||
|
|
kind === 'tilde';
|
|
}
|
|
|
|
private tokenToOpName(token: Token): string {
|
|
switch (token.kind) {
|
|
case 'ampersand': return 'cat';
|
|
|
|
// Arithmetic
|
|
case 'plus': return 'add';
|
|
case 'minus': return 'sub';
|
|
case 'star': return 'mul';
|
|
case 'slash': return 'div';
|
|
case 'percent': return 'mod';
|
|
case 'caret': return 'pow';
|
|
|
|
// Comparison
|
|
case 'equals-equals': return 'eq';
|
|
case 'not-equals': return 'neq';
|
|
case 'greater-than': return 'gt';
|
|
case 'greater-equals': return 'gte';
|
|
case 'less-than': return 'lt';
|
|
case 'less-equals': return 'lte';
|
|
|
|
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;
|
|
}
|
|
|
|
// Used in match, doesn't match another match
|
|
private parseExpressionNoMatch(): AST {
|
|
// Lambda
|
|
if (this.isLambdaStart()) {
|
|
return this.parseLambda();
|
|
}
|
|
|
|
// Let
|
|
if (this.current().kind === 'ident' && this.peek().kind === 'equals') {
|
|
return this.parseLet();
|
|
}
|
|
|
|
return this.parseInfix();
|
|
}
|
|
|
|
|
|
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.parseExpressionNoMatch();
|
|
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;
|
|
let spreadName: string | null = null;
|
|
|
|
while (this.current().kind !== 'close-bracket') {
|
|
if (!first) this.expect('comma');
|
|
first = false;
|
|
|
|
// Spread
|
|
if (this.current().kind === 'dot-dot-dot') {
|
|
this.advance();
|
|
const nameToken = this.expect('ident');
|
|
spreadName = (nameToken as { value: string }).value;
|
|
break;
|
|
}
|
|
|
|
elements.push(this.parsePattern());
|
|
}
|
|
|
|
this.expect('close-bracket');
|
|
|
|
if (spreadName !== null) {
|
|
return { kind: 'list-spread', head: elements, spread: spreadName };
|
|
}
|
|
|
|
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: 'record', 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.parseExpressionNoMatch();
|
|
this.expect('semicolon');
|
|
const body = this.parseExpressionNoMatch();
|
|
|
|
return { kind: 'let', name, value, body };
|
|
}
|
|
|
|
private parseInfix(): AST {
|
|
let left = this.parseApplication();
|
|
|
|
while (this.isInfixOp()) {
|
|
const opToken = this.advance();
|
|
|
|
if (opToken.kind === 'tilde') {
|
|
// function application operator
|
|
const right = this.parseApplication();
|
|
|
|
left = {
|
|
kind: 'apply',
|
|
func: right,
|
|
args: [left]
|
|
};
|
|
} else {
|
|
// operators desugar to function calls
|
|
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') {
|
|
this.advance();
|
|
|
|
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 {
|
|
// Record access
|
|
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') {
|
|
// Function / constructor application
|
|
const record = this.parsePrimary();
|
|
expr = { kind: 'apply', func: expr, args: [record] };
|
|
} 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 | { spread: AST })[] = [];
|
|
let first = true;
|
|
|
|
while (this.current().kind !== 'close-bracket') {
|
|
if (!first) {
|
|
this.expect('comma');
|
|
}
|
|
first = false;
|
|
|
|
// Spread
|
|
if (this.current().kind === 'dot-dot-dot') {
|
|
this.advance();
|
|
const expr = this.parseExpression();
|
|
items.push({ spread: expr })
|
|
} else {
|
|
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}`);
|
|
}
|
|
}
|