Skip to content

Commit

Permalink
feat(repl): improve support of multi-line statements
Browse files Browse the repository at this point in the history
This is a followup of gnolang#978. Instead of starting in multi-line mode prior
to submit, the line is parsed, and new inputs are appended to it as long
as the statment is not complete, as detected by the Go scanner.

This is simpler and more general than previous attempt. The secondary
prompt is "...", different from primary "gno>", similarly to
many REPL programs (node, python, bash, ...).

The "/editor" command is removed as not useful anymore. Note also
that it is now possible to exit using Ctrl-D.
  • Loading branch information
mvertes committed Sep 15, 2023
1 parent 6190894 commit 2af4f01
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 54 deletions.
87 changes: 34 additions & 53 deletions gnovm/cmd/gno/repl.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package main

import (
"bufio"
"bytes"
"context"
"errors"
"flag"
"fmt"
"go/scanner"
"os"
"strings"

Expand Down Expand Up @@ -88,91 +89,71 @@ func execRepl(cfg *replCfg, args []string) error {
// 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
// gno> /exit // alternative to <Ctrl-D>
`)
}

return runRepl(cfg)
}

func runRepl(cfg *replCfg) error {
// init repl state
r := repl.NewRepl()

if cfg.initialCommand != "" {
handleInput(r, cfg.initialCommand)
}

var multiline bool
for {
fmt.Fprint(os.Stdout, "gno> ")
fmt.Fprint(os.Stdout, "gno> ")

input, err := getInput(multiline)
if err != nil {
return err
prevline := ""
liner := bufio.NewScanner(os.Stdin)

for liner.Scan() {
line := liner.Text()
if prevline != "" {
line = prevline + "\n" + line
prevline = ""
}

if err := handleInput(r, line); err != nil {
var goScanError scanner.ErrorList
if errors.As(err, &goScanError) {
// We assune that a Go scanner error indicates an incomplete Go statement.
// Append next line and retry.
prevline = line
} else {
fmt.Fprintln(os.Stderr, err)
}
}

multiline = handleInput(r, input)
if prevline == "" {
fmt.Fprint(os.Stdout, "gno> ")
} else {
fmt.Fprint(os.Stdout, "... ")
}
}
return nil
}

// 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 {
// handleInput executes specific "/" commands, or evaluates input as Gno source code.
func handleInput(r *repl.Repl, input string) error {
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
// Avoid to increase the repl execution counter if no input.
default:
out, err := r.Process(input)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return err
}
fmt.Fprintln(os.Stdout, out)
}

return false
}

const (
inputBreaker = "^D"
nl = "\n"
)

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
}

if line == inputBreaker {
break
}

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

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

return mlOut.String(), nil
return nil
}
2 changes: 1 addition & 1 deletion gnovm/pkg/repl/repl.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ func (r *Repl) Process(input string) (out string, err error) {
return r.handleExpression(exp)
}

return "", fmt.Errorf("error parsing code:\n\t- as expression (error: %q)\n\t- as declarations (error: %q)", expErr.Error(), declErr.Error())
return "", fmt.Errorf("error parsing code:\n\t- as expression: %w\n\t- as declarations: %w", expErr, declErr)
}

func (r *Repl) handleExpression(e *ast.File) (string, error) {
Expand Down

0 comments on commit 2af4f01

Please sign in to comment.