-
Notifications
You must be signed in to change notification settings - Fork 1
Calculator Interpreter
The purpose of this document is to provide users with an example of how Kanonic can be leveraged in the own codebase. All of the code described in this document can be found within examples/calculator
.
The goal of this mini-example is to write an interpreter from scratch that handles the syntax of a calculator. In short, we hope to allow users to execute 1 + 1
and receive 2
as the result.
You can find the full grammar in examples/calculator/src/main/kanonic/calculator.knc
.
Here's what the tokens, at the time of writing, look like:
/** TOKENS **/
INTEGER: "[0-9]+";
PLUS: "\+";
MINUS: "-";
ASTERISK: "\*";
SLASH_FORWARD: "/";
CARROT: "\^";
PAREN_LEFT: "\(";
PAREN_RIGHT: "\)";
WS: "[\s]" --> hidden;
/** RULES **/
root
: expr
;
expr
: @lhs=expr @op=(PLUS|MINUS) @rhs=math_op --> expr_plus
| math_op --> expr_atomic
;
math_op
: @lhs=math_op @op=(SLASH_FORWARD|ASTERISK) @rhs=math_op_second --> expr_div
| math_op_second --> math_op_fallback
;
math_op_second
: @lhs=math_op_second CARROT @rhs=atomic --> math_op_second
| atomic --> expr_atom
;
atomic
: INTEGER --> int
| PAREN_LEFT @exp=expr PAREN_RIGHT --> paren
;
With the above syntax, we can:
- syntactically support ADDs, MINUSs, DIVIDEs, MULTIPLYs, and EXPONENTs
- simplify rules using aliases (see
lhs
,op
, andrhs
) - express precedence using the order of rules
- For those unfamiliar with expression precedence in grammars, it's important to note that the precedence can be seen as "bottom to top". So, in the above grammar,
atomic
holds the highest precedence. Then,math_op_second
holds the second highest, and so on. - The result of this is that the parser can understand how to parse:
1 + 2^3 / 4 - 2 * 7
. As humans, while it might take us a few seconds, we intuitively could re-write the equation to be:(1) + (((2)^(3)) / 4) - (2 * 7)
.
All of the generated code can be found under examples/calculator/src/main/kotlin/io/johnedquinn/calculator/generated
.
Now that we have all of the generated code, all we need to do to convert equations into ASTs is to instantiate and use the KanonicParser
:
val parser = KanonicParser.Builder.standard().withSpecification(CalculatorSpecification).build()
val ast = parser.parse("1 + 2^3 / 4 - 2 * 7")
Now that we have all of the generated code, all we need to do to convert equations into ASTs is to instantiate and use the KanonicParser
:
val parser = KanonicParser.Builder.standard().withSpecification(CalculatorSpecification).build()
val ast = parser.parse("1 + 2^3 / 4 - 2 * 7")
Now, what do we do with this AST? Well, since we are creating an interpreter, we just need to visit the tree and accumulate our result!
Using the generated CalculatorBaseVisitor
, we can override some visits:
/**
* When we parse a terminal, or token, we attempt to grab the integer
* from the string.
*/
override fun visitTerminal(node: TerminalNode, ctx: Unit): Int {
return when (val int = node.token.content.toIntOrNull()) {
null -> 0
else -> int
}
}
override fun visitExprDiv(node: CalculatorNode.MathOpNode.ExprDivNode, ctx: Unit): Int {
val op = when (node.op().type) {
5 -> { a: Int, b: Int -> a * b }
6 -> { a: Int, b: Int -> a / b }
else -> error("Didn't understand op type: ${node.op().type}")
}
val lhs = visitMathOp(node.lhs(), ctx)
val rhs = visitMathOpSecond(node.rhs(), ctx)
return op(lhs, rhs)
}
Now, we can traverse the AST and calculate the result of equations!
Thanks for checking out Kanonic's documentation!
If you'd like to contribute to the codebase or documentation, please feel free to open issues and PRs.