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