mirror of https://github.com/nocturn9x/nimkalc.git
136 lines
4.7 KiB
Markdown
136 lines
4.7 KiB
Markdown
|
# NimKalc - A math parsing library
|
||
|
|
||
|
NimKalc is a simple implementation of a recursive-descent top-down parser that can evaluate
|
||
|
mathematical expressions. Notable mentions are support for common mathematical constants (pi, tau, euler's number, etc),
|
||
|
functions (`sin`, `cos`, `tan`...), equation-solving algos using newton's method and scientific notation numbers (such as `2e5`)
|
||
|
|
||
|
|
||
|
## Current limitations
|
||
|
- No functions (coming soon)
|
||
|
- No equation-solving (coming soon)
|
||
|
- The parsing is a bit weird because `2 2` will parse the first 2 and just stop instead of erroring out (FIXME)
|
||
|
|
||
|
|
||
|
## How to use it
|
||
|
|
||
|
NimKalc parses mathematical expressions following this process:
|
||
|
- Tokenize the input
|
||
|
- Generate an AST
|
||
|
- Visit the nodes
|
||
|
|
||
|
Each of these steps can be run separately, but for convenience a wrapper
|
||
|
`eval` procedure has been defined which takes in a string and returns a
|
||
|
single AST node containing the result of the given expression.
|
||
|
|
||
|
## Supported operators
|
||
|
|
||
|
Beyond the classical 4 operators (`+`, `-`, `/` and `*`), NimKalc supports:
|
||
|
- `%` for modulo division
|
||
|
- `^` for exponentiation
|
||
|
- unary `-` for negation
|
||
|
- Arbitrarily nested parentheses (__not__ empty ones!) to enforce precedence
|
||
|
|
||
|
|
||
|
## Exceptions
|
||
|
|
||
|
NimKalc defines 2 exceptions:
|
||
|
- `ParseError` is used when the expression is invalid
|
||
|
- `MathError` is used when there is an arithmetical error such as division by 0 or domain errors (e.g. `log(0)`)
|
||
|
|
||
|
## Design
|
||
|
|
||
|
NimKalc treats all numerical values as `float` to simplify the implementation of the underlying operators. To tell integers
|
||
|
from floating point numbers the `AstNode` object has a `kind` discriminant which will be equal to `NodeKind.Integer` for ints
|
||
|
and `NodeKind.Float` for decimals. It is advised that you take this into account when using the library
|
||
|
|
||
|
|
||
|
__Note__: The string representation of integer nodes won't show the decimal part for clarity
|
||
|
|
||
|
## String representations
|
||
|
|
||
|
All of NimKalc's objects implement the `$` operator and are therefore printable. Integer nodes will look like `Integer(x)`, while
|
||
|
floats are represented with `Float(x.x)`. Unary operators print as `Unary(operator, right)`, while binary operators print as `Binary(left, operator, right)`.
|
||
|
Parenthesized expressions print as `Grouping(expr)`, where `expr` is the expression enclosed in parentheses (as an AST node, obviously).
|
||
|
Token objects will print as `Token(kind, lexeme)`: an example for the number 2 would be `Token(Integer, '2')`
|
||
|
|
||
|
|
||
|
## Example
|
||
|
|
||
|
Here is an example of a REPL using all of NimKalc's functionality to evaluate expressions from stdin (can be found at `examples/repl.nim`)
|
||
|
|
||
|
```nim
|
||
|
import nimkalc/objects/ast
|
||
|
import nimkalc/objects/token
|
||
|
import nimkalc/parsing/parser
|
||
|
import nimkalc/parsing/lexer
|
||
|
import nimkalc/objects/error
|
||
|
|
||
|
|
||
|
import strformat
|
||
|
import strutils
|
||
|
|
||
|
|
||
|
proc repl() =
|
||
|
## A simple REPL to demonstrate NimKalc's functionality
|
||
|
var line: string
|
||
|
var result: AstNode
|
||
|
var tokens: seq[Token]
|
||
|
let lexerObj = initLexer()
|
||
|
let parserObj = initParser()
|
||
|
let visitor = initNodeVisitor()
|
||
|
echo "Welcome to the NimKalc REPL, type a math expression and press enter"
|
||
|
while true:
|
||
|
try:
|
||
|
stdout.write("=> ")
|
||
|
line = stdin.readLine()
|
||
|
echo &"Parsing and evaluation of {line} below:"
|
||
|
tokens = lexerObj.lex(line)
|
||
|
# No-one cares about the EOF token after all
|
||
|
echo &"Tokenization of {line}: {tokens[0..^2].join(\", \")}"
|
||
|
result = parserObj.parse(tokens)
|
||
|
echo &"AST for {line}: {result}"
|
||
|
result = visitor.eval(result)
|
||
|
case result.kind:
|
||
|
# The result is an AstNode object, specifically
|
||
|
# either a node of type NodeKind.Float or a NodeKind.Integer
|
||
|
of NodeKind.Float:
|
||
|
echo &"Value of {line}: {result.value}"
|
||
|
of NodeKind.Integer:
|
||
|
echo &"Value of {line}: {int(result.value)}"
|
||
|
else:
|
||
|
discard # Unreachable
|
||
|
except IOError:
|
||
|
echo "\nGoodbye."
|
||
|
break
|
||
|
except ParseError:
|
||
|
echo &"A parsing error occurred: {getCurrentExceptionMsg()}"
|
||
|
except MathError:
|
||
|
echo &"An arithmetic error occurred: {getCurrentExceptionMsg()}"
|
||
|
except OverflowDefect:
|
||
|
echo &"Value overflow/underflow detected: {getCurrentExceptionMsg()}"
|
||
|
|
||
|
|
||
|
when isMainModule:
|
||
|
repl()
|
||
|
|
||
|
```
|
||
|
|
||
|
__Note__: If you don't need the intermediate representations shown here (tokens, AST) you can just `import nimkalc` and use
|
||
|
the `eval` procedure, which takes in a string and returns the evaluated result as a primary AST node like so:
|
||
|
|
||
|
```nim
|
||
|
import nimkalc
|
||
|
|
||
|
echo eval("2+2") # Prints Integer(4)
|
||
|
|
||
|
```
|
||
|
|
||
|
## Installing
|
||
|
|
||
|
You can clone this repository and then install the package via nimble:
|
||
|
- `git clone https://github.com/nocturn9x/nimkalc`
|
||
|
- `cd nimkalc`
|
||
|
- `nimble install`
|
||
|
|
||
|
|
||
|
__Note__: Nim 1.2.0 or higher is required to build NimKalc! Other versions are likely work if they're not too old, but they have not been tested
|