palle-k / Covfefe

A parser for nondeterministic context free languages
https://palle-k.github.io/Covfefe/
MIT License
62 stars 8 forks source link

Example usage #5

Closed mlajtos closed 3 years ago

mlajtos commented 3 years ago

If I have a simple arithmetic grammar, how should I go on and traverse the syntax tree and implement the eval?

import Covfefe

let grammarString = """
expression       = binary-operation | brackets | unary-operation | number;
brackets         = '(', expression, ')';
binary-operation = expression, binary-operator, expression;
binary-operator  = '+' | '-' | '*' | '/';
unary-operation  = unary-operator, expression;
unary-operator   = '+' | '-';
number           = {digit};
digit            = '0' ... '9';
"""

let grammar = try Grammar(ebnf: grammarString, start: "expression")
let parser = EarleyParser(grammar: grammar)

let syntaxTree = try parser.syntaxTree(for: "(23+47)*(-3)")

// syntaxTree.map(transform: { node in ???})

I am coming from Ohm/JS world and I feel lost on how to proceed from here. From the source I gathered that I have to guard for terminal vs. non-terminal and desctructure accordingly, but I can't really put that into Swift code. Can you please provide some snippet to get me going? I think README would benefit from it too.

palle-k commented 3 years ago

Hi!

The Earley parser returns a syntax tree with the non-terminals as nodes and the ranges, in which terminals were identified, as leafs:

let tree: SyntaxTree<NonTerminal, Range<String.Index>>

Each value of this type SyntaxTree<NonTerminal, Range<String.Index>> is therefore either a case node(...) or a case leaf(...) and you can process them recursively through pattern matching:

switch tree {
case .node(key: let nonTerminal, children: let subtrees) where nonTerminal.name == "expression":
    // parse whole expression

case .node(key: let nonTerminal, children: let subtrees) where nonTerminal.name == "binary-operation":
    // parse binary operation
    let operatorSymbol = getOperator(subtrees[1]) // the 0th subtree is the left hand side expression, the 1st subtree is the binary-operator, the 2nd subtree is the right hand side expression.

// more cases for the remaining non-terminals
...

case .leaf(let range):
    // range is the position in which this leaf was identified in the parsed string.
    // you can get the text at this position using
    let leafText = String(parsedExpression[range])
}

This forms the base on which you can process your tree, for example mapping it into an expression type or evaluating it.

I have implemented an example project, which evaluates math expressions here: https://github.com/palle-k/ExpressionSolver

Let me know if this helps or if you have any further questions :)

mlajtos commented 3 years ago

Yup. This is it, now I can hack together something. Thank you. ☺️

I added #6 – I think it should be mentioned somewhere because it is a great starting point for anybody that is dumb like me. :)