nimkalc/README.md

4.7 KiB

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)

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:

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