Files
rpnc/calc.go

455 lines
9.7 KiB
Go
Raw Normal View History

/*
Copyright © 2023 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
2023-10-30 14:22:43 +01:00
package main
import (
"errors"
2023-10-30 14:22:43 +01:00
"fmt"
"math"
"os"
2023-10-30 14:22:43 +01:00
"regexp"
"strconv"
"strings"
"github.com/chzyer/readline"
)
type Calc struct {
debug bool
batch bool
stdin bool
stack *Stack
history []string
completer readline.AutoCompleter
interpreter *Interpreter
Space *regexp.Regexp
Constants []string
LuaFunctions []string
Funcalls Funcalls
BatchFuncalls Funcalls
2023-10-30 14:22:43 +01:00
}
// help for lua functions will be added dynamically
2023-10-30 14:22:43 +01:00
const Help string = `Available commands:
batch toggle batch mode
debug toggle debug output
dump display the stack contents
clear clear the whole stack
shift remove the last element of the stack
history display calculation history
help|? show this message
quit|exit|c-d|c-c exit program
2023-10-30 14:22:43 +01:00
Available operators:
basic operators: + - x /
Available math functions:
sqrt square root
mod remainder of division (alias: remainder)
max batch mode only: max of all values
min batch mode only: min of all values
mean batch mode only: mean of all values (alias: avg)
median batch mode only: median of all values
% percent
%- substract percent
%+ add percent
Math operators:
^ power`
2023-10-30 14:22:43 +01:00
// commands, constants and operators, defined here to feed completion
// and our mode switch in Eval() dynamically
const (
Commands string = `dump reverse debug undebug clear batch shift undo help history manual exit quit`
Constants string = `Pi Phi Sqrt2 SqrtE SqrtPi SqrtPhi Ln2 Log2E Ln10 Log10E`
)
// That way we can add custom functions to completion
func GetCompleteCustomFunctions() func(string) []string {
return func(line string) []string {
completions := []string{}
for luafunc := range LuaFuncs {
completions = append(completions, luafunc)
}
completions = append(completions, strings.Split(Commands, " ")...)
completions = append(completions, strings.Split(Constants, " ")...)
return completions
}
}
func (c *Calc) GetCompleteCustomFuncalls() func(string) []string {
return func(line string) []string {
completions := []string{}
for function := range c.Funcalls {
completions = append(completions, function)
}
for function := range c.BatchFuncalls {
completions = append(completions, function)
}
return completions
}
}
2023-10-30 14:22:43 +01:00
func NewCalc() *Calc {
c := Calc{stack: NewStack(), debug: false}
c.Funcalls = DefineFunctions()
c.BatchFuncalls = DefineBatchFunctions()
2023-10-30 14:22:43 +01:00
c.completer = readline.NewPrefixCompleter(
// custom lua functions
readline.PcItemDynamic(GetCompleteCustomFunctions()),
readline.PcItemDynamic(c.GetCompleteCustomFuncalls()),
2023-10-30 14:22:43 +01:00
)
c.Space = regexp.MustCompile(`\s+`)
// pre-calculate mode switching arrays
c.Constants = strings.Split(Constants, " ")
for name := range LuaFuncs {
c.LuaFunctions = append(c.LuaFunctions, name)
}
2023-10-30 14:22:43 +01:00
return &c
}
// setup the interpreter, called from main()
func (c *Calc) SetInt(I *Interpreter) {
c.interpreter = I
}
2023-10-30 14:22:43 +01:00
func (c *Calc) ToggleDebug() {
c.debug = !c.debug
c.stack.ToggleDebug()
fmt.Printf("debugging set to %t\n", c.debug)
2023-10-30 14:22:43 +01:00
}
func (c *Calc) ToggleBatch() {
c.batch = !c.batch
fmt.Printf("batchmode set to %t\n", c.batch)
2023-10-30 14:22:43 +01:00
}
func (c *Calc) ToggleStdin() {
c.stdin = !c.stdin
}
func (c *Calc) Prompt() string {
p := "\033[31m»\033[0m "
b := ""
if c.batch {
b = "->batch"
}
d := ""
v := ""
if c.debug {
d = "->debug"
v = fmt.Sprintf("/rev%d", c.stack.rev)
}
return fmt.Sprintf("rpn%s%s [%d%s]%s", b, d, c.stack.Len(), v, p)
}
// the actual work horse, evaluate a line of calc command[s]
2023-10-30 14:22:43 +01:00
func (c *Calc) Eval(line string) {
line = strings.TrimSpace(line)
if line == "" {
return
}
for _, item := range c.Space.Split(line, -1) {
2023-10-30 14:22:43 +01:00
num, err := strconv.ParseFloat(item, 64)
if err == nil {
c.stack.Backup()
c.stack.Push(num)
} else {
/*
if contains(c.MathFunctions, item) {
// go builtin math function, if implemented
c.mathfunc(item)
continue
}
if contains(c.BatchFunctions, item) {
// math functions only supported in batch mode like max or mean
c.batchfunc(item)
continue
}
*/
2023-10-30 14:22:43 +01:00
if contains(c.Constants, item) {
// put the constant onto the stack
c.stack.Backup()
c.stack.Push(const2num(item))
continue
}
if _, ok := c.Funcalls[item]; ok {
if err := c.DoFuncall(item); err != nil {
fmt.Println(err)
} else {
c.Result()
}
continue
}
if c.batch {
if _, ok := c.BatchFuncalls[item]; ok {
if err := c.DoFuncall(item); err != nil {
fmt.Println(err)
} else {
c.Result()
}
continue
}
} else {
if _, ok := c.BatchFuncalls[item]; ok {
fmt.Println("only supported in batch mode")
continue
}
}
if contains(c.LuaFunctions, item) {
// user provided custom lua functions
c.luafunc(item)
continue
}
// management commands
2023-10-30 14:22:43 +01:00
switch item {
case "?":
fallthrough
2023-10-30 14:22:43 +01:00
case "help":
fmt.Println(Help)
fmt.Println("Lua functions:")
for name, function := range LuaFuncs {
fmt.Printf("%-20s %s\n", name, function.help)
}
2023-10-30 14:22:43 +01:00
case "dump":
c.stack.Dump()
case "debug":
c.ToggleDebug()
case "undebug":
c.debug = false
2023-10-30 14:22:43 +01:00
case "batch":
c.ToggleBatch()
case "clear":
c.stack.Backup()
c.stack.Clear()
case "shift":
c.stack.Backup()
c.stack.Shift()
case "reverse":
c.stack.Backup()
c.stack.Reverse()
2023-10-30 14:22:43 +01:00
case "undo":
c.stack.Restore()
case "history":
for _, entry := range c.history {
fmt.Println(entry)
}
case "exit":
fallthrough
case "quit":
os.Exit(0)
case "manual":
man()
2023-10-30 14:22:43 +01:00
default:
fmt.Println("unknown command or operator!")
}
}
}
}
// Execute a math function, check if it is defined just in case
func (c *Calc) DoFuncall(funcname string) error {
var function *Funcall
if c.batch {
function = c.BatchFuncalls[funcname]
} else {
function = c.Funcalls[funcname]
}
var args Numbers
batch := false
if function.Expectargs == -1 {
// batch mode, but always < stack len, so check first
args = c.stack.All()
batch = true
} else {
// this is way better behavior than just using 0 in place of
// non-existing stack items
if c.stack.Len() < function.Expectargs {
return errors.New("stack doesn't provide enough arguments")
}
args = c.stack.Last(function.Expectargs)
}
2023-11-05 13:00:26 +01:00
c.Debug(fmt.Sprintf("calling %s with args: %v", funcname, args))
// the actual lambda call, so to say. We provide a slice of
// the requested size, fetched from the stack (but not popped
// yet!)
R := function.Func(args)
if R.Err != nil {
// leave the stack untouched in case of any error
return R.Err
}
if batch {
// get rid of stack
c.stack.Clear()
} else {
// remove operands
c.stack.Shift(function.Expectargs)
}
// save result
c.stack.Push(R.Res)
// thanks a lot
c.SetHistory(funcname, args, R.Res)
return nil
}
// we need to add a history entry for each operation
func (c *Calc) SetHistory(op string, args Numbers, res float64) {
c.History("%s %s -> %f", list2str(args), op, res)
}
// just a textual representation of math operations, viewable with the
// history command
func (c *Calc) History(format string, args ...any) {
c.history = append(c.history, fmt.Sprintf(format, args...))
}
// print the result
func (c *Calc) Result() float64 {
2023-10-30 14:22:43 +01:00
if !c.stdin {
fmt.Print("= ")
}
fmt.Println(c.stack.Last()[0])
return c.stack.Last()[0]
2023-10-30 14:22:43 +01:00
}
func (c *Calc) Debug(msg string) {
if c.debug {
fmt.Printf("DEBUG(calc): %s\n", msg)
}
}
// do simple calculations
2023-10-30 14:22:43 +01:00
func (c *Calc) simple(op byte) {
c.stack.Backup()
for c.stack.Len() > 1 {
b := c.stack.Pop()
a := c.stack.Pop()
var x float64
c.Debug(fmt.Sprintf("evaluating: %.2f %c %.2f", a, op, b))
2023-10-30 14:22:43 +01:00
switch op {
case '+':
x = a + b
case '-':
x = a - b
case 'x':
fallthrough // alias for *
2023-10-30 14:22:43 +01:00
case '*':
x = a * b
case '/':
if b == 0 {
fmt.Println("error: division by null!")
return
}
x = a / b
case '^':
x = math.Pow(a, b)
2023-10-30 14:22:43 +01:00
default:
panic("invalid operator!")
}
c.stack.Push(x)
c.History("%f %c %f = %f", a, op, b, x)
2023-10-30 14:22:43 +01:00
if !c.batch {
break
}
}
c.Result()
}
func (c *Calc) luafunc(funcname string) {
// called from calc loop
var x float64
var err error
switch c.interpreter.FuncNumArgs(funcname) {
case 1:
x, err = c.interpreter.CallLuaFunc(funcname, c.stack.Last())
case 2:
x, err = c.interpreter.CallLuaFunc(funcname, c.stack.Last(2))
case -1:
x, err = c.interpreter.CallLuaFunc(funcname, c.stack.All())
default:
x, err = 0, errors.New("invalid number of argument requested")
}
if err != nil {
fmt.Println(err)
return
}
c.stack.Backup()
switch c.interpreter.FuncNumArgs(funcname) {
case 1:
a := c.stack.Pop()
c.History("%s(%f) = %f", funcname, a, x)
case 2:
a := c.stack.Pop()
b := c.stack.Pop()
c.History("%s(%f,%f) = %f", funcname, a, b, x)
case -1:
c.stack.Clear()
c.History("%s(*) = %f", funcname, x)
}
c.stack.Push(x)
c.Result()
2023-10-30 14:22:43 +01:00
}