1
0
treerack/docs/examples/acalc/main.go
2026-01-18 22:52:27 +01:00

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)
}