Skip to content

Commit

Permalink
feat(repl): support multi-line inputs and declarations (gnolang#978)
Browse files Browse the repository at this point in the history
  • Loading branch information
ajnavarro authored and Doozers committed Aug 31, 2023
1 parent fded434 commit 182606b
Show file tree
Hide file tree
Showing 4 changed files with 519 additions and 151 deletions.
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 {
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
}

return mlOut.String(), nil
}
Loading

0 comments on commit 182606b

Please sign in to comment.