Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(repl): support multi-line inputs and declarations #978

Merged
merged 13 commits into from
Aug 10, 2023
220 changes: 70 additions & 150 deletions gnovm/cmd/gno/repl.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,15 @@ package main

import (
"bufio"
"bytes"
"context"
"errors"
"flag"
"fmt"
"io"
"os"
"strconv"
"strings"

gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
"github.com/gnolang/gno/gnovm/tests"
"github.com/gnolang/gno/gnovm/pkg/repl"
"github.com/gnolang/gno/tm2/pkg/commands"
"golang.org/x/term"
)

type replCfg struct {
Expand Down Expand Up @@ -89,170 +85,94 @@ func execRepl(cfg *replCfg, args []string) error {

if !cfg.skipUsage {
fmt.Fprint(os.Stderr, `// Usage:
// gno:1> /import "gno.land/p/demo/avl" // import the p/demo/avl package
// gno:2> /func a() string { return "a" } // declare a new function named a
// gno:3> /src // print current generated source
// gno:4> println(a()) // print the result of calling a()
// gno:5> /exit
// gno> import "gno.land/p/demo/avl" // import the p/demo/avl package
// gno> func a() string { return "a" } // declare a new function named a
// gno> /src // print current generated source
// gno> /editor // enter in editor mode to add several lines
// gno> /reset // remove all previously inserted code
// gno> println(a()) // print the result of calling a()
// gno> /exit
`)
}

return runRepl(cfg)
}

type repl struct {
// repl state
imports []string
funcs []string
lastInput string
i int
// TODO: support setting global vars
// TODO: switch to state machine, and support rollback of anything

stderr io.Writer
stdout io.Writer
machine *gno.Machine
}
func runRepl(cfg *replCfg) error {
// init repl state
r := repl.NewRepl()

func (r *repl) handleInput(input string) error {
if strings.TrimSpace(input) == "" {
return nil
if cfg.initialCommand != "" {
handleInput(r, cfg.initialCommand)
}

r.i++
funcName := fmt.Sprintf("repl_%d", r.i)
// FIXME: support ";" as line separator?
// FIXME: support multiline when unclosed parenthesis, etc

imports := strings.Join(r.imports, "\n")
funcs := strings.Join(r.funcs, "\n")
srcBefore := "// generated by 'gno repl'\npackage test\n" + imports + "\n" + funcs + "\nfunc " + funcName + "() {\n"
srcAfter := "\n}"
var multiline bool
for {
fmt.Fprint(os.Stdout, "gno> ")

fields := strings.Fields(input)
command := fields[0]
switch {
case command == "/import":
imp := fields[1]
if strings.HasPrefix(imp, `"`) {
imp, _ = strconv.Unquote(imp) // support with or without quotes
}
imp = strings.TrimSpace(imp)
if imp == "" {
fmt.Fprintf(r.stdout, "invalid import: %q\n", imp)
return nil
input, err := getInput(multiline)
if err != nil {
return err
}
r.imports = append(r.imports, `import "`+imp+`"`)
// TODO: check if valid, else rollback
return nil
case command == "/func":
r.funcs = append(r.funcs, input[1:])
// TODO: check if valid, else rollback
return nil
case command == "/src":
// TODO: use go/format for pretty print
src := srcBefore + r.lastInput + srcAfter
fmt.Fprintln(r.stdout, src)
return nil
case command == "/exit":
os.Exit(0) // return special err?
case strings.HasPrefix(command, "/"):
fmt.Fprintln(r.stdout, "unsupported command")
return nil
default:
// not a command, probably code to run
}

r.lastInput = input
src := srcBefore + r.lastInput + srcAfter
n := gno.MustParseFile(funcName+".gno", src)
// TODO: run fmt check + linter
r.machine.RunFiles(n)
// TODO: smart recover system
r.machine.RunStatement(gno.S(gno.Call(gno.X(funcName))))
// TODO: if output is empty, consider that it's a persisted variable?
return nil
multiline = handleInput(r, input)
}
}

func runRepl(cfg *replCfg) error {
stdin := os.Stdin
stdout := os.Stdout
stderr := os.Stderr

// init repl state
r := repl{
i: 1,
stdout: stdout,
stderr: stderr,
imports: make([]string, 0),
funcs: make([]string, 0),
lastInput: "// your code will be here", // initial value, to make it easier to identify with '/src'
}
for _, imp := range strings.Split(cfg.initialImports, ",") {
if strings.TrimSpace(imp) == "" {
continue
// handleInput reads the input string and parses it depending if it
// is a specific command, or source code. It returns true if the following
// input is expected to be on more than one line.
func handleInput(r *repl.Repl, input string) bool {
ajnavarro marked this conversation as resolved.
Show resolved Hide resolved
switch strings.TrimSpace(input) {
case "/reset":
r.Reset()
case "/src":
fmt.Fprintln(os.Stdout, r.Src())
case "/exit":
os.Exit(0)
case "/editor":
fmt.Fprintln(os.Stdout, "// Entering editor mode (^D to finish)")
return true
case "":
// avoid to increase the repl execution counter if sending empty content
fmt.Fprintln(os.Stdout, "")
return false
default:
out, err := r.Process(input)
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
r.imports = append(r.imports, `import "`+imp+`"`)
}
testStore := tests.TestStore(cfg.rootDir, "", stdin, stdout, stderr, tests.ImportModeStdlibsOnly)
if cfg.verbose {
testStore.SetLogStoreOps(true)
}
r.machine = gno.NewMachineWithOptions(gno.MachineOptions{
PkgPath: "test",
Output: stdout,
Store: testStore,
})
defer r.machine.Release()

if cfg.initialCommand != "" {
r.handleInput(cfg.initialCommand)
fmt.Fprintln(os.Stdout, out)
}

// main loop
isTerm := term.IsTerminal(int(stdin.Fd()))
return false
}

if isTerm {
rw := struct {
io.Reader
io.Writer
}{os.Stdin, os.Stderr}
t := term.NewTerminal(rw, "")
for {
// prompt and parse
t.SetPrompt(fmt.Sprintf("gno:%d> ", r.i))
oldState, err := term.MakeRaw(0)
if err != nil {
return fmt.Errorf("make term raw: %w", err)
}
input, err := t.ReadLine()
if err != nil {
term.Restore(0, oldState)
if errors.Is(err, io.EOF) {
return nil
}
return fmt.Errorf("term error: %w", err)
}
term.Restore(0, oldState)
const (
inputBreaker = "^D"
nl = "\n"
)

err = r.handleInput(input)
if err != nil {
return fmt.Errorf("handle repl input: %w", err)
}
func getInput(ml bool) (string, error) {
s := bufio.NewScanner(os.Stdin)
var mlOut bytes.Buffer
for s.Scan() {
line := s.Text()
if !ml {
return line, nil
}
} else { // !isTerm
scanner := bufio.NewScanner(stdin)
for scanner.Scan() {
input := scanner.Text()
err := r.handleInput(input)
if err != nil {
return fmt.Errorf("handle repl input: %w", err)
}
}
err := scanner.Err()
if err != nil {
return fmt.Errorf("read stdin: %w", err)

if line == inputBreaker {
break
}

mlOut.WriteString(line)
mlOut.WriteString(nl)
}
return nil

if err := s.Err(); err != nil {
return "", err
}

ajnavarro marked this conversation as resolved.
Show resolved Hide resolved
return mlOut.String(), nil
}
Loading
Loading