Skip to content

Instantly share code, notes, and snippets.

@ianfun
Created August 8, 2022 05:20
Show Gist options
  • Save ianfun/bda976df5995455dd8c001158de16c70 to your computer and use it in GitHub Desktop.
Save ianfun/bda976df5995455dd8c001158de16c70 to your computer and use it in GitHub Desktop.
Simple calculator written in Nim
import std/[strutils, streams, math]
type
Token = enum
TInvalid, TEOF, TNumber, TIdentifier, TOperator, TOpen, TClose, TNewLine
Parser = object
fd: Stream
c: char # lookahead
line: int
col: int
val: Value
Value = object
case tok: Token
of TEOF:
discard
of TNumber:
fval: float
of TIdentifier:
sval: string
of TOperator:
op: Operator
of TOpen:
discard
of TClose:
discard
of TNewLine:
discard
of TInvalid:
discard
Operator = enum Mul='*', Add='+', Sub='-' , Div='/'
ExprKind = enum BinaryExpr, UnaryExpr, LiteralExpr, CallExpr, Symbol
Expr = ref object
case kind: ExprKind
of BinaryExpr: # val `op` val
op: Operator
lhs, rhs: Expr
of UnaryExpr: # +/- val
sign: bool
val: Expr
of LiteralExpr: # val
fval: float
of CallExpr: # f(x)
fn: string
a: Expr
of Symbol: # pi, e
sym: string
CalcError = object of CatchableError
LexerError = object of CalcError
EvalError = object of CalcError
proc `$`(self: Operator): string =
return $ char(self)
proc `$`(self: Expr): string =
assert self != nil
case self.kind:
of LiteralExpr:
return $self.fval
of UnaryExpr:
if self.sign:
return '(' & $self.val & ')'
else:
return "(-" & $self.val & ')'
of BinaryExpr:
return '(' & $self.lhs & ' ' & $self.op & ' ' & $self.rhs & ')'
of CallExpr:
return self.fn & '(' & $self.a & ')'
of Symbol:
return self.sym
proc eval(e: Expr): float =
assert e != nil
case e.kind:
of LiteralExpr:
e.fval
of UnaryExpr:
if e.sign:
eval(e.val)
else:
-eval(e.val)
of BinaryExpr:
let l = eval(e.lhs)
let r = eval(e.rhs)
case e.op:
of Add:
l + r
of Sub:
l - r
of Mul:
l * r
of Div:
l / r
of Symbol:
case e.sym:
of "e":
math.E
of "pi":
math.PI
else:
raise newException(EvalError, "symbol not found: " & e.sym)
of CallExpr:
let a = eval(e.a)
case e.fn:
of "sin":
sin(a)
of "cos":
cos(a)
of "sqrt":
sqrt(a)
else:
raise newException(EvalError, "function not found: " & e.fn)
proc getTok(p: var Parser) =
while p.c in {' ', '\t', '\r'}:
p.c = p.fd.readChar()
if isDigit(p.c) or p.c == '.':
var s: string
while true:
s.add(p.c)
p.c = p.fd.readChar()
inc p.col
if not (isDigit(p.c) or p.c=='.'):
break
p.val = Value(tok: TNumber, fval: parseFloat(s))
return
if isAlphaAscii(p.c):
var s: string
while true:
s.add(p.c)
p.c = p.fd.readChar()
inc p.col
if not isAlphaAscii(p.c):
break
p.val = Value(tok: TIdentifier, sval: s)
return
let c = p.c
if c == '\0': # EOF
p.val = Value(tok: TEOF)
return
p.c = p.fd.readChar()
inc p.col
case c:
of '\n':
p.val = Value(tok: TNewLine)
p.col = 0
inc p.line
of '+':
p.val = Value(tok: TOperator, op: Add)
of '-':
p.val = Value(tok: TOperator, op: Sub)
of '*':
p.val = Value(tok: TOperator, op: Mul)
of '/':
p.val = Value(tok: TOperator, op: Div)
of '(':
p.val = Value(tok: TOpen)
of ')':
p.val = Value(tok: TClose)
else:
raise newException(LexerError, "invalid token: " & repr(c))
proc error(p: var Parser, msg: string) =
stderr.writeLine("\e[31mparse error: line " & $p.line & ", column " & $p.col & ":\n " & msg & "\e[0m")
proc expr(p: var Parser): Expr
proc term(p: var Parser): Expr
proc factor(p: var Parser): Expr
proc atom(p: var Parser): Expr
proc expr(p: var Parser): Expr =
result = term(p)
if result != nil:
while p.val.tok == TOperator and p.val.op in {Add, Sub}:
var op = p.val.op
getTok(p)
let rhs = term(p)
if rhs == nil:
p.error("expect number, `+`, `-` , `(` before " & repr(p.c))
return nil
result = Expr(kind: BinaryExpr, op: op, lhs: result, rhs: rhs)
proc term(p: var Parser): Expr =
result = factor(p)
if result != nil:
while p.val.tok == TOperator and p.val.op in {Mul, Div}:
var op = p.val.op
getTok(p)
let rhs = factor(p)
if rhs == nil:
p.error("expect number, `+`, `-` , `(` before " & repr(p.c))
return nil
result = Expr(kind: BinaryExpr, op: op, lhs: result, rhs: rhs)
proc factor(p: var Parser): Expr =
if p.val.tok == TOperator:
let op = p.val.op
getTok(p)
let t = atom(p)
if t == nil:
p.error("expect number, `+`, `-` , `(`, got " & repr(p.c))
return nil
case op:
of Add:
return Expr(kind: UnaryExpr, sign: true, val: t)
of Sub:
return Expr(kind: UnaryExpr, sign: false, val: t)
else:
p.error("invalid unary operator: " & repr(p.c))
else:
return atom(p)
proc atom(p: var Parser): Expr =
case p.val.tok:
of TNumber:
result = Expr(kind: LiteralExpr, fval: p.val.fval)
getTok(p)
of TOpen:
getTok(p)
let v = expr(p)
if p.val.tok != TClose:
p.error("expect `)` to close `(`, got " & repr(p.c))
return nil
getTok(p)
return v
of TIdentifier:
let s = p.val.sval
getTok(p)
if p.val.tok == TOpen:
getTok(p)
let e = expr(p)
if p.val.tok != TClose:
p.error("expect `)` to close `(`, got " & repr(p.c))
return nil
getTok(p)
return Expr(kind: CallExpr, fn: s, a: e)
else:
return Expr(kind: Symbol, sym: s)
else:
p.error("invalid start of expression: " & repr(p.c))
proc loop() =
var p = Parser(fd: nil, line: 1, col: 1, c: ' ', val: Value(tok: TInvalid))
while true:
stdout.write("\e[33mcalc> \e[0m")
let s = stdin.readLine()
if s.len > 0 and s[0] == ':':
let command = s[1..^1]
case command:
of "quit", "q":
break
of "help":
stdout.writeLine("type math expression\nExample: 2 + 3 * e\ncos(0) + sin(pi)")
else:
stderr.writeLine("Command not found.Try :help for help")
continue
p.fd = newStringStream(s)
try:
getTok(p)
let e = expr(p)
if p.val.tok != TEOF:
stderr.writeLine("\e[31mwarning: extra expression " & $p.val & " is not parsed\e[0m")
if e != nil:
stdout.writeLine($e & " => \e[32m" & $eval(e) & "\e[0m")
except CalcError as e:
stderr.writeLine("\e[31m" & e.msg & "\e[0m")
except ValueError as e: # parseFloat
stderr.writeLine("\e[31m" & e.msg & "\e[0m")
p.fd.close()
p.c = ' '
p.col = 0
loop()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment