package monkey import ( "bufio" "fmt" "github.com/tebeka/atexit" "io" "log" "os" "runtime/debug" "strings" "golang.org/x/term" "monkey/internal/compiler" "monkey/internal/object" "monkey/internal/parser" "monkey/internal/vm" ) // Package repl implements the Read-Eval-Print-Loop or interactive console // by lexing, parsing and evaluating the input in the interpreter // REPL provides a read-eval-print loop for the monkey virtual machine. func REPL(opts *Options) error { if opts == nil { opts = &Options{} } initState, err := term.MakeRaw(0) if err != nil { fmt.Println(err) return fmt.Errorf("error opening terminal: %w", err) } defer func() { term.Restore(0, initState) if err := recover(); err != nil { log.Printf("recovered from panic: %s\n", err) log.Printf("stack trace:\n%s", debug.Stack()) } }() atexit.Register(func() { term.Restore(0, initState) }) t := term.NewTerminal(os.Stdin, ">>> ") t.AutoCompleteCallback = autoComplete opts.Exit = atexit.Exit state := vm.NewState() PrintVersionInfo(t) for { input, err := t.ReadLine() check(t, initState, err) input = strings.TrimRight(input, " ") if len(input) > 0 && input[len(input)-1] == '{' { input, err = acceptUntil(t, input, "\n\n") check(t, initState, err) } res, errs := parser.Parse("", input) if len(errs) != 0 { for _, e := range errs { fmt.Fprintln(t, e) } continue } if opts.Debug { log.Printf("AST:\n%s\n", res) } c := compiler.NewWithState(state.Symbols, &state.Constants) c.Debug = opts.Debug c.SetFileInfo("", input) if err := c.Compile(res); err != nil { fmt.Fprintln(t, err) continue } if opts.Debug { log.Printf("Bytecode:\n%s\n", c.Bytecode()) } mvm := vm.New("", c.Bytecode(), append(opts.ToVMOptions(), vm.WithState(state))...) if err := mvm.Run(); err != nil { fmt.Fprintf(t, "runtime error: %v\n", err) continue } if val := mvm.LastPoppedStackElem(); val != nil && val.Type() != object.NullType { fmt.Fprintln(t, val.Inspect()) } } } func autoComplete(line string, pos int, key rune) (newLine string, newPos int, ok bool) { if key == '\t' { return line + " ", pos + 4, true } return } func check(t *term.Terminal, initState *term.State, err error) { if err != nil { // Quit without error on Ctrl^D. if err != io.EOF { fmt.Fprintln(t, err) } term.Restore(0, initState) fmt.Println() os.Exit(0) } } func acceptUntil(t *term.Terminal, start, end string) (string, error) { var buf strings.Builder buf.WriteString(start) buf.WriteRune('\n') t.SetPrompt("... ") defer t.SetPrompt(">>> ") for { line, err := t.ReadLine() if err != nil { return "", err } line = strings.TrimRight(line, " ") buf.WriteString(line) buf.WriteRune('\n') if s := buf.String(); len(s) > len(end) && s[len(s)-len(end):] == end { break } } return buf.String(), nil } // SimpleREPL provides a simple read-eval-print loop for the monkey virtual machine. func SimpleREPL(opts *Options) { if opts == nil { opts = &Options{} } reader := bufio.NewReader(os.Stdin) defer func() { if err := recover(); err != nil { log.Printf("recovered from panic: %s\n", err) log.Printf("stack trace: %s\n", debug.Stack()) } }() t := term.NewTerminal(os.Stdin, ">>> ") t.AutoCompleteCallback = autoComplete state := vm.NewState() PrintVersionInfo(os.Stdout) for { fmt.Print(">>> ") input, err := reader.ReadString('\n') simpleCheck(err) input = strings.TrimRight(input, " \n") if len(input) > 0 && input[len(input)-1] == '{' { input, err = simpleAcceptUntil(reader, input, "\n\n") simpleCheck(err) } res, errs := parser.Parse("", input) if len(errs) != 0 { for _, e := range errs { fmt.Println(e) } continue } if opts.Debug { log.Printf("AST:\n%s\n", res) } c := compiler.NewWithState(state.Symbols, &state.Constants) c.Debug = opts.Debug c.SetFileInfo("", input) if err := c.Compile(res); err != nil { fmt.Println(err) continue } if opts.Debug { log.Printf("Bytecode:\n%s\n", c.Bytecode()) } mvm := vm.New("", c.Bytecode(), append(opts.ToVMOptions(), vm.WithState(state))...) if err := mvm.Run(); err != nil { fmt.Printf("runtime error: %v\n", err) continue } if val := mvm.LastPoppedStackElem(); val != nil && val.Type() != object.NullType { fmt.Println(val.Inspect()) } } } func simpleCheck(err error) { if err != nil { fmt.Println(err) os.Exit(0) } } func simpleAcceptUntil(r *bufio.Reader, start, end string) (string, error) { var buf strings.Builder buf.WriteString(start) buf.WriteRune('\n') for { fmt.Print("... ") line, err := r.ReadString('\n') if err != nil { return "", err } line = strings.TrimRight(line, " \n") buf.WriteString(line) buf.WriteRune('\n') if s := buf.String(); len(s) > len(end) && s[len(s)-len(end):] == end { break } } return buf.String(), nil }