Parse simple expressions

This commit is contained in:
Simon Binder 2019-06-16 20:50:07 +02:00
parent 2a5ede1c04
commit b442d32a87
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
11 changed files with 356 additions and 24 deletions

View File

@ -0,0 +1,14 @@
import 'package:sqlparser/src/ast/expressions/literals.dart';
import 'package:sqlparser/src/ast/expressions/simple.dart';
abstract class AstNode {
Iterable<AstNode> get childNodes;
T accept<T>(AstVisitor<T> visitor);
}
abstract class AstVisitor<T> {
T visitBinaryExpression(BinaryExpression e);
T visitUnaryExpression(UnaryExpression e);
T visitIsExpression(IsExpression e);
T visitLiteral(Literal e);
}

View File

@ -1,3 +1,5 @@
abstract class Expression {
import 'package:sqlparser/src/ast/ast.dart';
abstract class Expression implements AstNode {
const Expression();
}

View File

@ -1,16 +1,43 @@
import 'package:sqlparser/src/ast/ast.dart';
import 'package:sqlparser/src/reader/tokenizer/token.dart';
import 'expressions.dart';
// https://www.sqlite.org/syntax/literal-value.html
class NullLiteral extends Expression {
const NullLiteral();
abstract class Literal extends Expression {
final Token token;
Literal(this.token);
@override
String toString() => 'NULL';
T accept<T>(AstVisitor<T> visitor) => visitor.visitLiteral(this);
@override
int get hashCode => runtimeType.hashCode;
@override
bool operator ==(other) => identical(this, other) || other is NullLiteral;
final Iterable<AstNode> childNodes = const <AstNode>[];
}
class NullLiteral extends Literal {
NullLiteral(Token token) : super(token);
}
class NumericLiteral extends Literal {
final num number;
NumericLiteral(this.number, Token token) : super(token);
}
class BooleanLiteral extends NumericLiteral {
BooleanLiteral.withFalse(Token token) : super(0, token);
BooleanLiteral.withTrue(Token token) : super(1, token);
}
class StringLiteral extends Literal {
final String data;
final bool isBinary;
StringLiteral(StringLiteralToken token)
: data = token.value,
isBinary = token.binary,
super(token);
}

View File

@ -0,0 +1,63 @@
import 'package:sqlparser/src/ast/ast.dart';
import 'package:sqlparser/src/reader/tokenizer/token.dart';
import 'expressions.dart';
class UnaryExpression extends Expression {
final Token operator;
final Expression inner;
UnaryExpression(this.operator, this.inner);
@override
T accept<T>(AstVisitor<T> visitor) => visitor.visitUnaryExpression(this);
@override
Iterable<AstNode> get childNodes => [inner];
}
class BinaryExpression extends Expression {
final Token operator;
final Expression left;
final Expression right;
BinaryExpression(this.left, this.operator, this.right);
@override
T accept<T>(AstVisitor<T> visitor) => visitor.visitBinaryExpression(this);
@override
Iterable<AstNode> get childNodes => [left, right];
}
class IsExpression extends Expression {
final bool negated;
final Expression left;
final Expression right;
IsExpression(this.negated, this.left, this.right);
@override
T accept<T>(AstVisitor<T> visitor) {
return visitor.visitIsExpression(this);
}
@override
Iterable<AstNode> get childNodes => [left, right];
}
class Parentheses extends Expression {
final Token openingLeft;
final Expression expression;
final Token closingRight;
Parentheses(this.openingLeft, this.expression, this.closingRight);
@override
T accept<T>(AstVisitor<T> visitor) {
return expression.accept(visitor);
}
@override
Iterable<AstNode> get childNodes => [expression];
}

View File

@ -1,10 +0,0 @@
import 'package:sqlparser/src/reader/tokenizer/token.dart';
import 'expressions.dart';
class UnaryExpression extends Expression {
final Token operator;
final Expression inner;
UnaryExpression(this.operator, this.inner);
}

View File

@ -1,9 +1,35 @@
import 'package:meta/meta.dart';
import 'package:sqlparser/src/ast/expressions/expressions.dart';
import 'package:sqlparser/src/ast/expressions/unary.dart';
import 'package:sqlparser/src/ast/expressions/literals.dart';
import 'package:sqlparser/src/ast/expressions/simple.dart';
import 'package:sqlparser/src/reader/tokenizer/token.dart';
const _comparisonOperators = [
TokenType.less,
TokenType.lessEqual,
TokenType.more,
TokenType.moreEqual,
];
const _binaryOperators = const [
TokenType.shiftLeft,
TokenType.shiftRight,
TokenType.ampersand,
TokenType.pipe,
];
class ParsingError implements Exception {
final Token token;
final String message;
ParsingError(this.token, this.message);
}
// todo better error handling and synchronisation, like it's done here:
// https://craftinginterpreters.com/parsing-expressions.html#synchronizing-a-recursive-descent-parser
class Parser {
final List<Token> tokens;
final List<ParsingError> errors = [];
int _current = 0;
Parser(this.tokens);
@ -34,6 +60,18 @@ class Parser {
return _previous;
}
@alwaysThrows
void _error(String message) {
final error = ParsingError(_peek, message);
errors.add(error);
throw error;
}
Token _consume(TokenType type, String message) {
if (_check(type)) return _advance();
_error(message);
}
/* We parse expressions here.
* Operators have the following precedence:
* - + ~ NOT (unary)
@ -48,10 +86,84 @@ class Parser {
* We also treat expressions in parentheses and literals with the highest
* priority. Parsing methods are written in ascending precedence, and each
* parsing method calls the next higher precedence if unsuccessful.
* https://www.sqlite.org/lang_expr.html
* */
Expression expression() {
return _unary();
return _or();
}
/// Parses an expression of the form a <T> b, where <T> is in [types] and
/// both a and b are expressions with a higher precedence parsed from
/// [higherPrecedence].
Expression _parseSimpleBinary(
List<TokenType> types, Expression Function() higherPrecedence) {
var expression = higherPrecedence();
while (_match(types)) {
final operator = _previous;
final right = higherPrecedence();
expression = BinaryExpression(expression, operator, right);
}
return expression;
}
Expression _or() => _parseSimpleBinary(const [TokenType.or], _and);
Expression _and() => _parseSimpleBinary(const [TokenType.and], _equals);
Expression _equals() {
var expression = _comparison();
final ops = const [
TokenType.equal,
TokenType.doubleEqual,
TokenType.exclamationEqual,
TokenType.lessMore,
TokenType.$is,
TokenType.$in,
TokenType.like,
TokenType.glob,
TokenType.match,
TokenType.regexp,
];
while (_match(ops)) {
final operator = _previous;
if (operator.type == TokenType.$is) {
final not = _match(const [TokenType.not]);
// special case: is not expression
expression = IsExpression(not, expression, _comparison());
} else {
expression = BinaryExpression(expression, operator, _comparison());
}
}
return expression;
}
Expression _comparison() {
return _parseSimpleBinary(_comparisonOperators, _binaryOperation);
}
Expression _binaryOperation() {
return _parseSimpleBinary(_binaryOperators, _addition);
}
Expression _addition() {
return _parseSimpleBinary(const [
TokenType.plus,
TokenType.minus,
], _multiplication);
}
Expression _multiplication() {
return _parseSimpleBinary(const [
TokenType.star,
TokenType.slash,
TokenType.percent,
], _concatenation);
}
Expression _concatenation() {
return _parseSimpleBinary(const [TokenType.doublePipe], _unary);
}
Expression _unary() {
@ -70,6 +182,32 @@ class Parser {
}
Expression _primary() {
return null;
final token = _advance();
final type = token.type;
switch (type) {
case TokenType.numberLiteral:
// todo get the proper value out of this one
return NumericLiteral(42, _peek);
case TokenType.stringLiteral:
final token = _peek as StringLiteralToken;
return StringLiteral(token);
case TokenType.$null:
return NullLiteral(_peek);
case TokenType.$true:
return BooleanLiteral.withTrue(_peek);
case TokenType.$false:
return BooleanLiteral.withFalse(_peek);
// todo CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP
case TokenType.leftParen:
final left = _previous;
final expr = expression();
_consume(TokenType.rightParen, 'Expected a closing bracket');
return Parentheses(left, expr, _previous);
default:
break;
}
// nothing found -> issue error
_error('Could not parse this expression');
}
}

View File

@ -175,7 +175,7 @@ class Scanner {
_nextChar();
final value = source.substring(_startOffset + 1, _currentOffset - 1);
tokens.add(StringLiteral(value, _currentSpan, binary: binary));
tokens.add(StringLiteralToken(value, _currentSpan, binary: binary));
}
void _numeric(String firstChar) {

View File

@ -36,6 +36,12 @@ enum TokenType {
stringLiteral,
numberLiteral,
$true,
$false,
$null,
currentTime,
currentDate,
currentTimestamp,
identifier,
select,
@ -56,6 +62,12 @@ const Map<String, TokenType> keywords = {
'MATCH': TokenType.match,
'REGEXP': TokenType.regexp,
'NOT': TokenType.not,
'TRUE': TokenType.$true,
'FALSE': TokenType.$false,
'NULL': TokenType.$null,
'CURRENT_TIME': TokenType.currentTime,
'CURRENT_DATE': TokenType.currentDate,
'CURRENT_TIMESTAMP': TokenType.currentTimestamp,
};
class Token {
@ -66,13 +78,13 @@ class Token {
const Token(this.type, this.span);
}
class StringLiteral extends Token {
class StringLiteralToken extends Token {
final String value;
/// sqlite allows binary strings (x'literal') which are interpreted as blobs.
final bool binary;
const StringLiteral(this.value, SourceSpan span, {this.binary = false})
const StringLiteralToken(this.value, SourceSpan span, {this.binary = false})
: super(TokenType.stringLiteral, span);
}

View File

@ -0,0 +1,46 @@
import 'package:sqlparser/src/ast/ast.dart';
import 'package:sqlparser/src/ast/expressions/simple.dart';
/// Checks whether [a] and [b] are equal. If they aren't, throws an exception.
void enforceEqual(AstNode a, AstNode b) {
if (a.runtimeType != b.runtimeType) {
throw ArgumentError('Not equal: First was $a, second $b');
}
_checkAdditional(a, b);
final childrenA = a.childNodes.iterator;
final childrenB = b.childNodes.iterator;
// always move both iterators
while (childrenA.moveNext() & childrenB.moveNext()) {
enforceEqual(childrenA.current, childrenB.current);
}
if (childrenA.moveNext() || childrenB.moveNext()) {
throw ArgumentError("$a and $b don't have an equal amount of children");
}
}
void _checkAdditional<T extends AstNode>(T a, T b) {
if (a is IsExpression) {
return _checkIsExpr(a, b as IsExpression);
} else if (a is UnaryExpression) {
_checkUnaryExpression(a, b as UnaryExpression);
} else if (a is BinaryExpression) {
_checkBinaryExpression(a, b as BinaryExpression);
}
}
void _checkIsExpr(IsExpression a, IsExpression b) {
if (a.negated != b.negated) {
throw ArgumentError('Negation status not the same');
}
}
void _checkUnaryExpression(UnaryExpression a, UnaryExpression b) {
if (a.operator.type != b.operator.type) throw ArgumentError('Different type');
}
void _checkBinaryExpression(BinaryExpression a, BinaryExpression b) {
if (a.operator.type != b.operator.type) throw ArgumentError('Different type');
}

View File

@ -0,0 +1,35 @@
import 'package:sqlparser/src/ast/expressions/literals.dart';
import 'package:sqlparser/src/ast/expressions/simple.dart';
import 'package:sqlparser/src/reader/parser/parser.dart';
import 'package:sqlparser/src/reader/tokenizer/scanner.dart';
import 'package:sqlparser/src/reader/tokenizer/token.dart';
import 'package:sqlparser/src/utils/ast_equality.dart';
import 'package:test/test.dart';
import 'utils.dart';
void main() {
test('parses simple expressions', () {
final scanner = Scanner('3 * 4 + 5 == 17');
final tokens = scanner.scanTokens();
final parser = Parser(tokens);
final expression = parser.expression();
enforceEqual(
expression,
BinaryExpression(
BinaryExpression(
BinaryExpression(
NumericLiteral(3, token(TokenType.numberLiteral)),
token(TokenType.star),
NumericLiteral(4, token(TokenType.numberLiteral)),
),
token(TokenType.plus),
NumericLiteral(5, token(TokenType.numberLiteral)),
),
token(TokenType.doubleEqual),
NumericLiteral(17, token(TokenType.numberLiteral)),
),
);
});
}

View File

@ -0,0 +1,5 @@
import 'package:sqlparser/src/reader/tokenizer/token.dart';
Token token(TokenType type) {
return Token(type, null);
}