Skip to main content

Parsing Expression

Grammar rules

phần trước, chúng ta biết rằng expression trong Lox có thể được generate theo những quy tắc sau

expression     → literal
| unary
| binary
| grouping ;

literal → NUMBER | STRING | "true" | "false" | "nil" ;
grouping → "(" expression ")" ;
unary → ( "-" | "!" ) expression ;
binary → expression operator expression ;
operator → "==" | "!=" | "<" | "<=" | ">" | ">="
| "+" | "-" | "*" | "/" ;

Xét biểu thức:

Sử dụng quy tắc trên, có hai cách để generate ra biểu thức này, và cho ra hai kết quả khác nhau!

Như vậy quy tắc grammar trên có sự mơ hồ (ambiguity). Trong toán học, vấn đề này được giải quyết bằng 2 luật

  • Precedence: quy định phép toán được tính trước trong một biểu thức gồm nhiều loại phép toán. E.g: phép nhân (x) được tính trước phép cộng (+).

    Precedence, lowest to highest
    | =
    | or
    | and
    | ==, !=
    | <, >, <=, >=
    | +, -
    | *, /
    | not, - (negate)
    v ()
  • Associativity: quy định toán tử được tính trước trong một biểu thức gồm nhiều toán tử giống nhau.

    • Phép cộng là left-associate. E.g: 1 + 2 + 3 có thể viết dưới dạng (1 + 2) + 3
    • Phép gán (=) là right-associate. E.g: x = y = z có thể viết dưới dạng x = (y = z)
| Name        | Operator     | Associativity |
| -------- | -------- | -------- |
| Equality | ==, != | Left |
| Comparison | >, >=, <, <= | Left |
| Term | +, - | Left |
| Factor | * / | Left |
| Unary | !, - | Right |

Dựa vào luật precedence và associativity, ta có thể viết lại ngữ pháp cho expression nhằm loại bỏ sự mơ hồ của ngữ pháp ở phần trước:

expression     → equality ;
equality → comparison ( ( "!=" | "==" ) comparison )* ;
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )* ;
term → factor ( ( "-" | "+" ) factor )* ;
factor → unary ( ( "/" | "*" ) unary )* ;
unary → ( "!" | "-" ) unary
| primary ;
primary → NUMBER | STRING | "true" | "false" | "nil"
| "(" expression ")" ;

Dưới đây, parser sẽ được cài đặt theo ngữ pháp này.

Expression parser

Trong phần này, ta sử dụng kĩ thuật parsing có tên Recursive Descent Parsing. Kĩ thuật này có cách tiếp cận top-down, tức là bắt đầu từ rule trên cùng (expression) và dần dần đi xuống từng tầng cho đến rule cuối cùng (primary).

Parse function

  // Parse a single expression and return remaining tokens
def parse(ts: List[Token]): ParseResult = expression(ts)

def expression(tokens: List[Token]): ParseResult = equality(tokens)

Parse function trả ra một expression hoàn chỉnh và tokens còn lại sau quá trình parse.

Mỗi rule (e.g: expression, equality, etc) sẽ tương ứng với một hàm và có kiểu input/output tương tự hàm parse.

Binary expression

Hãy bắt đầu với ví dụ:

Parse biểu thức a * b + c - d

Biểu thức trên được viết dưới dạng tokens như sau

| a | * | b | + | c | - | d | ; |

Rule được sử dụng để parse là

term           → factor ( ( "-" | "+" ) factor )* ;

Các bước trong quá trình parse sẽ diễn ra như sau

StepDescriptionExprRemaining tokens
0Khởi đầu(?)a * b + c - d ;
1Chạy rule factor(a * b + c - d ;)a * b+ c - d ;
2Thêm dấu +a * b + (?)c - d ;
3Chạy rule factor(c - d ;)a * b + c- d ;
4Thêm dấu -a * b + c - (?)d ;
5Chạy rule factor(d ;)a * b + c - d;
6Kết thúca * b + c - d;

Mặc dù ví dụ ở trên giải thích các bước parse phép cộng (term), nhưng ta hoàn toàn có thể áp dụng thuật toán này cho các phép toán nhị phân (binary expression) khác.

Trong Scala, thuật toán trên được cài đặt như sau:

  def binary(
op: BinaryOp,
descendant: List[Token] => ParseResult,
)(
tokens: List[Token]
): ParseResult =
def matchOp(ts: List[Token], l: Expr): ParseResult =
ts match
case token :: rest =>
op(token) match
case Some(fn) => descendant(rest).flatMap((r, rmn) => matchOp(rmn, fn(l, r)))
case None => Right(l, ts)
case _ => Right(l, ts)

descendant(tokens).flatMap((expr, rest) => matchOp(rest, expr))

sau đó có thể sử dụng hàm này này cho các phép toán nhị phân

  def equality = binary(equalityOp, comparison)
def comparison = binary(comparisonOp, term)
def term = binary(termOp, factor)
def factor = binary(factorOp, unary)

Unary expression

Lưu ý, unary là right-associate operator, vì thế ta gọi đệ quy hàm unary ngay khi gặp dấu ! hoặc -. Nếu không gặp một trong hai dấu trên, ta gọi xuống hàm primary.

  def unary(tokens: List[Token]): ParseResult =
tokens match
case token :: rest =>
unaryOp(token) match
case Some(fn) => unary(rest).flatMap((expr, rmn) => Right(fn(expr), rmn))
case None => primary(tokens)
case _ => primary(tokens)

Primary expression

Cài đặt primary expression tương đối rõ ràng

  def primary(tokens: List[Token]): ParseResult =
tokens match
case Literal.Number(l) :: rest => Right(Expr.Literal(l.toDouble), rest)
case Literal.Str(l) :: rest => Right(Expr.Literal(l), rest)
case Keyword.True :: rest => Right(Expr.Literal(true), rest)
case Keyword.False :: rest => Right(Expr.Literal(false), rest)
case Keyword.Nil :: rest => Right(Expr.Literal(null), rest)
case Operator.LeftParen :: rest => parenBody(rest)
case _ => Left(Error.ExpectExpression(tokens))

Khi gặp dấu mở ngoặc (left paren), ta tiếp tục chạy rule expression cho những tokens tiếp theo, sau đó kiểm tra token kế tiếp có phải dấu đóng ngoặc hay không.

  def parenBody(
tokens: List[Token]
): ParseResult = expression(tokens).flatMap((expr, rest) =>
rest match
case Operator.RightParen :: rmn => Right(Expr.Grouping(expr), rmn)
case _ => Left(Error.ExpectClosing(rest))
)