diff --git a/lib/src/parser.dart b/lib/src/parser.dart new file mode 100644 index 0000000..7e81e01 --- /dev/null +++ b/lib/src/parser.dart @@ -0,0 +1,187 @@ +import 'address.dart'; +import 'ast.dart'; +import 'lexer.dart'; +import 'tokens.dart'; + +class FormulaException implements Exception { + final String message; + const FormulaException(this.message); + + @override + String toString() => 'FormulaException: $message'; +} + +Expr parseFormula(String body) => Parser.forFormula(body).parse(); + +class Parser { + final List tokens; + int _pos = 0; + + Parser(this.tokens); + + factory Parser.forFormula(String body) { + try { + return Parser(Lexer(body).tokenize()); + } on LexException catch (error) { + throw FormulaException(error.message); + } + } + + Expr parse() { + final expr = _comparison(); + if (_peek().type != TokenType.eof) { + throw FormulaException('Unexpected token "${_peek().lexeme}"'); + } + return expr; + } + + Expr _comparison() { + var left = _concat(); + while (_matchAny(const [ + TokenType.eq, + TokenType.neq, + TokenType.lt, + TokenType.lte, + TokenType.gt, + TokenType.gte, + ])) { + final op = _previous().lexeme; + final right = _concat(); + left = BinaryOp(op, left, right); + } + return left; + } + + Expr _concat() { + var left = _additive(); + while (_matchAny(const [TokenType.amp])) { + final right = _additive(); + left = BinaryOp('&', left, right); + } + return left; + } + + Expr _additive() { + var left = _multiplicative(); + while (_matchAny(const [TokenType.plus, TokenType.minus])) { + final op = _previous().lexeme; + final right = _multiplicative(); + left = BinaryOp(op, left, right); + } + return left; + } + + Expr _multiplicative() { + var left = _unary(); + while (_matchAny(const [TokenType.star, TokenType.slash, TokenType.percent])) { + final op = _previous().lexeme; + final right = _unary(); + left = BinaryOp(op, left, right); + } + return left; + } + + Expr _unary() { + if (_matchAny(const [TokenType.minus, TokenType.plus])) { + final op = _previous().lexeme; + return UnaryOp(op, _unary()); + } + return _power(); + } + + Expr _power() { + final base = _primary(); + if (_matchAny(const [TokenType.caret])) { + final exponent = _unary(); + return BinaryOp('^', base, exponent); + } + return base; + } + + Expr _primary() { + final token = _peek(); + switch (token.type) { + case TokenType.number: + _advance(); + return NumberLit(token.numberValue); + case TokenType.text: + _advance(); + return TextLit(token.textValue); + case TokenType.errorLit: + _advance(); + return ErrorLit(token.errorKind); + case TokenType.lparen: + _advance(); + final expr = _comparison(); + _expect(TokenType.rparen, ')'); + return expr; + case TokenType.cellRef: + _advance(); + return _reference(token.lexeme, null); + case TokenType.quotedName: + _advance(); + _expect(TokenType.bang, '!'); + final ref = _expect(TokenType.cellRef, 'cell reference'); + return _reference(ref.lexeme, token.textValue); + case TokenType.identifier: + _advance(); + if (_peek().type == TokenType.lparen) { + return FuncCall(token.lexeme, _argumentList()); + } + if (_peek().type == TokenType.bang) { + _advance(); + final ref = _expect(TokenType.cellRef, 'cell reference'); + return _reference(ref.lexeme, token.lexeme); + } + final upper = token.lexeme.toUpperCase(); + if (upper == 'TRUE') return const BoolLit(true); + if (upper == 'FALSE') return const BoolLit(false); + return NameNode(token.lexeme); + default: + throw FormulaException('Unexpected token "${token.lexeme}"'); + } + } + + Expr _reference(String startLexeme, String? sheet) { + final start = CellAddress.parse(startLexeme); + if (_matchAny(const [TokenType.colon])) { + final endToken = _expect(TokenType.cellRef, 'cell reference'); + return RangeNode(start, CellAddress.parse(endToken.lexeme), sheet); + } + return RefNode(start, sheet); + } + + List _argumentList() { + _expect(TokenType.lparen, '('); + final args = []; + if (_peek().type != TokenType.rparen) { + args.add(_comparison()); + while (_matchAny(const [TokenType.comma])) { + args.add(_comparison()); + } + } + _expect(TokenType.rparen, ')'); + return args; + } + + bool _matchAny(List types) { + if (types.contains(_peek().type)) { + _advance(); + return true; + } + return false; + } + + Token _expect(TokenType type, String label) { + if (_peek().type != type) { + throw FormulaException('Expected "$label" but found "${_peek().lexeme}"'); + } + return _advance(); + } + + Token _peek() => tokens[_pos]; + + Token _previous() => tokens[_pos - 1]; + + Token _advance() => tokens[_pos++]; +}