133 lines
2.9 KiB
Go
133 lines
2.9 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
var errExit = errors.New("exit")
|
|
|
|
// repl runs the Read-Eval-Print Loop.
|
|
func repl(input io.Reader, output io.Writer) {
|
|
|
|
// use buffered io, to read the input line-by-line:
|
|
buf := bufio.NewReader(os.Stdin)
|
|
|
|
// our REPL:
|
|
for {
|
|
// print a input prompt marker:
|
|
if _, err := output.Write([]byte("> ")); err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
|
|
// read the input and handle the errors:
|
|
expr, err := read(buf)
|
|
|
|
// handle EOF (Ctrl+D):
|
|
if errors.Is(err, io.EOF) {
|
|
output.Write([]byte{'\n'})
|
|
os.Exit(0)
|
|
}
|
|
|
|
// handle the explicit exit command:
|
|
if errors.Is(err, errExit) {
|
|
os.Exit(0)
|
|
}
|
|
|
|
// handle parser errors (allow the user to retry):
|
|
var perr *parseError
|
|
if errors.As(err, &perr) {
|
|
log.Println(err)
|
|
continue
|
|
}
|
|
|
|
// handle possible I/O errors:
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
|
|
// evaluate and print:
|
|
result := eval(expr)
|
|
if err := print(output, result); err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func read(input *bufio.Reader) (*node, error) {
|
|
line, err := input.ReadString('\n')
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// parse the line using the generated parser:
|
|
expr, err := parse(bytes.NewBufferString(line))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if strings.TrimSpace(expr.Text()) == "exit" {
|
|
return nil, errExit
|
|
}
|
|
|
|
// based on our syntax, the root node always has exactly one child: either a number or a binary operation.
|
|
return expr.Nodes[0], nil
|
|
}
|
|
|
|
// eval always returns the calculated result as a float64:
|
|
func eval(expr *node) float64 {
|
|
var value float64
|
|
switch expr.Name {
|
|
case "num":
|
|
|
|
// the number format in our syntax is based on the JSON spec, so we can piggy-back on it for the number
|
|
// parsing. In a real application, we would need to handle the errors here anyway, even if our parser
|
|
// already validated the input:
|
|
json.Unmarshal([]byte(expr.Text()), &value)
|
|
return value
|
|
default:
|
|
|
|
// evaluate binary expressions. Format: Operand [Operator Operand]...
|
|
value, expr.Nodes = eval(expr.Nodes[0]), expr.Nodes[1:]
|
|
for len(expr.Nodes) > 0 {
|
|
var (
|
|
operator string
|
|
operand float64
|
|
)
|
|
|
|
operator, operand, expr.Nodes = expr.Nodes[0].Name, eval(expr.Nodes[1]), expr.Nodes[2:]
|
|
switch operator {
|
|
case "add":
|
|
value += operand
|
|
case "sub":
|
|
value -= operand
|
|
case "mul":
|
|
value *= operand
|
|
case "div":
|
|
value /= operand // Go returns on division by zero +/-Inf
|
|
}
|
|
}
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
func print(output io.Writer, result float64) error {
|
|
// we can use the stdlib fmt package to print float64:
|
|
_, err := fmt.Fprintln(output, result)
|
|
return err
|
|
}
|
|
|
|
func main() {
|
|
// for testability, we define the REPL in a separate function so that the test code can call it with
|
|
// in-memory buffers as input and output. Our main function calls it with the stdio handles:
|
|
repl(os.Stdin, os.Stdout)
|
|
}
|