Created
August 8, 2022 05:20
-
-
Save ianfun/bda976df5995455dd8c001158de16c70 to your computer and use it in GitHub Desktop.
Simple calculator written in Nim
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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