Better errors!

This commit is contained in:
Dustin Swan 2026-02-04 21:21:52 -07:00
parent c44f06268f
commit 9d1b079361
No known key found for this signature in database
GPG key ID: 30D46587E2100467
8 changed files with 305 additions and 132 deletions

View file

@ -1,12 +1,15 @@
import type { Token } from './lexer'
import type { AST, MatchCase, Pattern } from './ast'
import { ParseError } from './error'
export class Parser {
private tokens: Token[]
private pos: number = 0
private source: string
constructor(tokens: Token[]) {
constructor(tokens: Token[], source: string) {
this.tokens = tokens;
this.source = source;
}
private current(): Token {
@ -33,12 +36,20 @@ export class Parser {
const token = this.current();
if (token.kind !== kind) {
throw new Error(`Expected ${kind}, got ${token.kind}`);
throw ParseError(`Expected ${kind}, got ${token.kind}`, token.line, token.column, this.source);
}
return this.advance();
}
private getPos(token: Token) {
return {
line: token.line,
column: token.column,
start: token.start
};
}
private isInfixOp(): boolean {
const kind = this.current().kind;
return kind === 'plus' || kind === 'minus' ||
@ -71,7 +82,7 @@ export class Parser {
case 'less-than': return 'lt';
case 'less-equals': return 'lte';
default: throw new Error(`Not an operator: ${token.kind}`);
default: throw ParseError(`Not an operator: ${token.kind}`, token.line, token.column, this.source);
}
}
@ -145,6 +156,7 @@ export class Parser {
private parseMatch(expr: AST): AST {
const token = this.current();
const cases: MatchCase[] = [];
while(this.current().kind === 'pipe') {
@ -152,10 +164,10 @@ export class Parser {
const pattern = this.parsePattern();
this.expect('backslash');
const result = this.parseExpressionNoMatch();
cases.push({ pattern, result })
cases.push({ pattern, result, ...this.getPos(token) })
}
return { kind: 'match', expr, cases };
return { kind: 'match', expr, cases, ...this.getPos(token) };
}
private parsePattern(): Pattern {
@ -217,7 +229,7 @@ export class Parser {
this.expect('close-bracket');
if (spreadName !== null) {
return { kind: 'list-spread', head: elements, spread: spreadName };
return { kind: 'list-spread', head: elements, spread: spreadName, ...this.getPos(token) };
}
return { kind: 'list', elements };
@ -251,7 +263,7 @@ export class Parser {
return pattern;
}
throw new Error(`Unexpected token in pattern: ${token.kind}`)
throw ParseError(`Unexpected token in pattern: ${token.kind}`, token.line, token.column, this.source);
}
private canStartPattern(): boolean {
@ -264,6 +276,7 @@ export class Parser {
}
private parseLambda(): AST {
const token = this.current();
const params: string[] = [];
while (this.current().kind === 'ident') {
@ -274,7 +287,7 @@ export class Parser {
this.expect('backslash');
const body = this.parseExpression();
return { kind: 'lambda', params, body };
return { kind: 'lambda', params, body, ...this.getPos(token) };
}
private parseLet(): AST {
@ -288,7 +301,7 @@ export class Parser {
name = (nameToken as { value: string }).value;
this.advance();
} else {
throw new Error(`Expected ident or underscore, got ${nameToken.kind}`);
throw ParseError(`Expected ident or underscore, got ${nameToken.kind}`, nameToken.line, nameToken.column, this.source);
}
@ -297,10 +310,11 @@ export class Parser {
this.expect('semicolon');
const body = this.parseExpressionNoMatch();
return { kind: 'let', name, value, body };
return { kind: 'let', name, value, body, ...this.getPos(nameToken) };
}
private parseInfix(): AST {
const token = this.current();
let left = this.parseApplication();
while (this.isInfixOp()) {
@ -313,7 +327,8 @@ export class Parser {
left = {
kind: 'apply',
func: right,
args: [left]
args: [left],
...this.getPos(token)
};
} else {
// operators desugar to function calls
@ -322,8 +337,9 @@ export class Parser {
left = {
kind: 'apply',
func: { kind: 'variable', name: opName },
args: [left, right]
func: { kind: 'variable', name: opName, ...this.getPos(token) },
args: [left, right],
...this.getPos(token)
}
}
}
@ -332,17 +348,19 @@ export class Parser {
}
private parseApplication(): AST {
const token = this.current();
let func = this.parsePostfix();
while (this.canStartPrimary()) {
const arg = this.parsePostfix();
func = { kind: 'apply', func, args: [arg] };
func = { kind: 'apply', func, args: [arg], ...this.getPos(token) };
}
return func;
}
private parsePostfix(): AST {
const token = this.current();
let expr = this.parsePrimary();
while (true) {
@ -368,13 +386,13 @@ export class Parser {
}
this.expect('close-brace');
expr = { kind: 'record-update', record: expr, updates }
expr = { kind: 'record-update', record: expr, updates, ...this.getPos(token) }
} else {
// Record access
const fieldToken = this.expect('ident');
const field = (fieldToken as { value: string }).value;
expr = { kind: 'record-access', record: expr, field };
expr = { kind: 'record-access', record: expr, field, ...this.getPos(token) };
}
} else {
break;
@ -397,7 +415,7 @@ export class Parser {
if (token.kind === 'open-bracket') {
this.advance();
const items: (AST | { spread: AST })[] = [];
const items: AST[] = [];
let first = true;
while (this.current().kind !== 'close-bracket') {
@ -408,16 +426,17 @@ export class Parser {
// Spread
if (this.current().kind === 'dot-dot-dot') {
const spreadToken = this.current();
this.advance();
const expr = this.parseExpression();
items.push({ spread: expr })
items.push({ kind: 'list-spread', spread: expr, ...this.getPos(spreadToken) })
} else {
items.push(this.parseExpression());
}
}
this.expect('close-bracket');
return { kind: 'list', elements: items };
return { kind: 'list', elements: items, ...this.getPos(token) };
}
if (token.kind === 'open-brace') {
@ -439,34 +458,34 @@ export class Parser {
}
this.expect('close-brace');
return { kind: 'record', fields };
return { kind: 'record', fields, ...this.getPos(token) };
}
if (token.kind === 'int') {
this.advance();
return { kind: 'literal', value: { kind: 'int', value: token.value } };
return { kind: 'literal', value: { kind: 'int', value: token.value }, ...this.getPos(token) };
}
if (token.kind === 'float') {
this.advance();
return { kind: 'literal', value: { kind: 'float', value: token.value } };
return { kind: 'literal', value: { kind: 'float', value: token.value }, ...this.getPos(token) };
}
if (token.kind === 'string') {
this.advance();
return { kind: 'literal', value: { kind: 'string', value: token.value } };
return { kind: 'literal', value: { kind: 'string', value: token.value }, ...this.getPos(token) };
}
if (token.kind === 'ident') {
this.advance();
return { kind: 'variable', name: token.value };
return { kind: 'variable', name: token.value, ...this.getPos(token) };
}
if (token.kind === 'type-ident') {
this.advance();
return { kind: 'constructor', name: token.value };
return { kind: 'constructor', name: token.value, ...this.getPos(token) };
}
throw new Error(`Unexpected token: ${token.kind}`);
throw ParseError(`Unexpected token: ${token.kind}`, token.line, token.column, this.source);
}
}