Skip to content

Calculator example

yassir RAMDANI edited this page Nov 27, 2020 · 1 revision

Import SwiLex to define terminal tokens needed by the SwiLex lexer. :

import SwiLex

Import SwiParse to define a parser and its grammar:

import SwiParse

In this example we will create a parser for the following grammar to build a simple calculator :

<expr> →   <expr> + <term> 
         | <expr> - <term> 
         | <term>

<term> →   <term> * <factor> 
         | <term> / <factor> 
         | <factor>

<fact> →   ( <expr> ) 
         | number

Define a SwiLaxable enum to list the terminal tokens of the grammar with their corresponding regular expressions:

enum Terminal: String, SwiLexable {
    static var separators: Set<Character> = [" "]

    case number = "[0-9]+"
    case plus = #"\+"#
    case minus = #"-"#
    case times = #"\*"#
    case divide = #"/"#

    case lParenthesis = #"\("#
    case rParenthesis = #"\)"#

    case eof
    case none   // empty token
}

Define a SwiParsable enum to list the non-terminal tokens used in the grammar:

enum NonTerminal: SwiParsable {
    case expr
    case fact
    case term

    case start  // starting token
}

Define the reduction actions for each rule of the grammar :

func sum(a: Int, op _: Substring, b: Int) -> Int { a + b }
func minus(a: Int, op _: Substring, b: Int) -> Int { a - b }

func times(a: Int, op _: Substring, b: Int) -> Int { a * b }
func divide(a: Int, op _: Substring, b: Int) -> Int { a / b }

func parenthesis(_: Substring, a: Int, _: Substring) -> Int { a }
func toInt(s: Substring) -> Int { Int(s) ?? 0 }

func keepValue(a: Int) -> Int { a }
Important:

In a grammar, each rule leads to the reduction of one or more terminal / non-terminal tokens (the right part of the rule) into a non-terminal token (the left part of the rule) by applying a reduction action.

The reduction action of a rule in SwiParse takes the value of each token being reduced by the rule as argument (the right part of the rule) and returns the new value of the reduced non-terminal token (the left part of the rule).

Terminal tokens' values are passed as Substrings (directly read by the SwiLex lexer) while non-terminal ones are the output of the rule that produced them (a non-terminal token is necessarily the result of a reduction rule thus its value is the output of the rule's reduction action).

Next, define a parser with the grammar by providing its rules and their reduction actions:

let parser = try SwiParse<Terminal, NonTerminal>(rules: [
    .start => [Word(.expr)], // accepting rule

    .expr => [Word(.expr), Word(.plus), Word(.term)] >>>> sum,
    .expr => [Word(.expr), Word(.minus), Word(.term)] >>>> minus,
    .expr => [Word(.term)] >>>> keepValue,

    .term => [Word(.term), Word(.times), Word(.fact)] >>>> times,
    .term => [Word(.term), Word(.divide), Word(.fact)] >>>> divide,
    .term => [Word(.fact)] >>>> keepValue,

    .fact => [Word(.lParenthesis), Word(.expr), Word(.rParenthesis)] >>>> parenthesis,
    .fact => [Word(.number)] >>>> toInt,
])

Note that each grammar should have a single starting rule that will be used as

Finally, parse an input string using the defined parser:

let result = try parser.parse(input: "(1 + 2) * 3")
// 9