144 lines
3.5 KiB
Go
144 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
var errExit = errors.New("exit")
|
|
|
|
func repl(input io.Reader, output io.Writer) {
|
|
// use buffered io, to be able to read the input line-by-line:
|
|
buf := bufio.NewReader(os.Stdin)
|
|
|
|
// our REPL loop:
|
|
for {
|
|
// print a basic prompt:
|
|
if _, err := output.Write([]byte("> ")); err != nil {
|
|
|
|
// we cannot fix it if there is an error here:
|
|
log.Fatalln(err)
|
|
}
|
|
|
|
// read the input and handle the errors:
|
|
expr, err := read(buf)
|
|
|
|
// when EOF, that means the user pressed Ctrl+D. Let's terminate the output with a conventional newline
|
|
// and exit:
|
|
if errors.Is(err, io.EOF) {
|
|
output.Write([]byte{'\n'})
|
|
os.Exit(0)
|
|
}
|
|
|
|
// when errExit, that means the user entered exit:
|
|
if errors.Is(err, errExit) {
|
|
os.Exit(0)
|
|
}
|
|
|
|
// if it's a parser error, we print and continue from reading again, to allow the user to fix the
|
|
// problem:
|
|
var perr *parseError
|
|
if errors.As(err, &perr) {
|
|
log.Println(err)
|
|
continue
|
|
}
|
|
|
|
// in case of any other error, we don't know what's going on, so we get out of here right away:
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
|
|
// if we received an expression, then we can evaluate it. We are not expecting errors here:
|
|
result := eval(expr)
|
|
|
|
// we have the result, we need to print it:
|
|
if err := print(output, result); err != nil {
|
|
|
|
// if printing fails, we don't know how to fix it, so we get out of here:
|
|
log.Fatalln(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func read(input *bufio.Reader) (*node, error) {
|
|
line, err := input.ReadString('\n')
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// expr will be of type *node, which type is defined in the generated code
|
|
expr, err := parse(bytes.NewBufferString(line))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if strings.TrimSpace(expr.Text()) == "exit" {
|
|
return nil, errExit
|
|
}
|
|
|
|
// we know based on the syntax, that the top level node will always have a single child, either a number
|
|
// literal or a binary operation:
|
|
return expr.Nodes[0], nil
|
|
}
|
|
|
|
// eval always returns the calculated result as a float64:
|
|
func eval(expr *node) float64 {
|
|
|
|
// we know that it's either a number or a binary operation:
|
|
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:
|
|
|
|
// we know that the first node is either a number of a child expression:
|
|
value, expr.Nodes = eval(expr.Nodes[0]), expr.Nodes[1:]
|
|
|
|
// we don't need to track back, so we can drop the processed nodes while consuming them:
|
|
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":
|
|
// Go returns -Inf or +Inf on division by zero:
|
|
value /= operand
|
|
}
|
|
}
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
func print(output io.Writer, result float64) error {
|
|
_, err := fmt.Fprintln(output, result)
|
|
return err
|
|
}
|
|
|
|
func main() {
|
|
// for testability, we define the REPL loop 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)
|
|
}
|