repl: factor the REPL into a separate package (#50)
...so that users can build REPLs for their dialects.
Also, add support for interrupting the REPL with Control-C.
diff --git a/cmd/skylark/skylark.go b/cmd/skylark/skylark.go
index 04903f0..a3be90d 100644
--- a/cmd/skylark/skylark.go
+++ b/cmd/skylark/skylark.go
@@ -3,35 +3,10 @@
// license that can be found in the LICENSE file.
// The skylark command interprets a Skylark file.
-//
// With no arguments, it starts a read-eval-print loop (REPL).
-// If an input line can be parsed as an expression,
-// the REPL parses and evaluates it and prints its result.
-// Otherwise the REPL reads lines until a blank line,
-// then tries again to parse the multi-line input as an
-// expression. If the input still cannot be parsed as an expression,
-// the REPL parses and executes it as a file (a list of statements),
-// for side effects.
package main
-// TODO(adonovan):
-//
-// - Distinguish expressions from statements more precisely.
-// Otherwise e.g. 1 is parsed as an expression but
-// 1000000000000000000000000000 is parsed as a file
-// because the scanner fails to convert it to an int64.
-// The spec should clarify limits on numeric literals.
-//
-// - Unparenthesized tuples are not parsed as a single expression:
-// >>> (1, 2)
-// (1, 2)
-// >>> 1, 2
-// ...
-// >>>
-// This is not necessarily a bug.
-
import (
- "bytes"
"flag"
"fmt"
"log"
@@ -40,10 +15,9 @@
"sort"
"strings"
- "github.com/chzyer/readline"
"github.com/google/skylark"
+ "github.com/google/skylark/repl"
"github.com/google/skylark/resolve"
- "github.com/google/skylark/syntax"
)
// flags
@@ -76,23 +50,23 @@
defer pprof.StopCPUProfile()
}
+ thread := &skylark.Thread{Load: repl.MakeLoad()}
+ globals := make(skylark.StringDict)
+
switch len(flag.Args()) {
case 0:
- repl()
+ fmt.Println("Welcome to Skylark (github.com/google/skylark)")
+ repl.REPL(thread, globals)
case 1:
- execfile(flag.Args()[0])
+ // Execute specified file.
+ filename := flag.Args()[0]
+ if err := skylark.ExecFile(thread, filename, nil, globals); err != nil {
+ repl.PrintError(err)
+ os.Exit(1)
+ }
default:
log.Fatal("want at most one Skylark file name")
}
-}
-
-func execfile(filename string) {
- thread := &skylark.Thread{Load: load}
- globals := make(skylark.StringDict)
- if err := skylark.ExecFile(thread, filename, nil, globals); err != nil {
- printError(err)
- os.Exit(1)
- }
// Print the global environment.
if *showenv {
@@ -108,148 +82,3 @@
}
}
}
-
-func repl() {
- fmt.Println("Welcome to Skylark (github.com/google/skylark)")
- thread := &skylark.Thread{Load: load}
- globals := make(skylark.StringDict)
-
- rl, err := readline.New(">>> ")
- if err != nil {
- printError(err)
- return
- }
- defer rl.Close()
-outer:
- for {
- rl.SetPrompt(">>> ")
- line, err := rl.Readline()
- if err != nil {
- break
- }
-
- if l := strings.TrimSpace(line); l == "" || l[0] == '#' {
- continue // blank or comment
- }
-
- // If the line contains a well-formed expression, evaluate it.
- if _, err := syntax.ParseExpr("<stdin>", line); err == nil {
- if v, err := skylark.Eval(thread, "<stdin>", line, globals); err != nil {
- printError(err)
- } else if v != skylark.None {
- fmt.Println(v)
- }
- continue
- }
-
- // If the input so far is a single load or assignment statement,
- // execute it without waiting for a blank line.
- if f, err := syntax.Parse("<stdin>", line); err == nil && len(f.Stmts) == 1 {
- switch f.Stmts[0].(type) {
- case *syntax.AssignStmt, *syntax.LoadStmt:
- // Execute it as a file.
- if err := execFileNoFreeze(thread, line, globals); err != nil {
- printError(err)
- }
- continue
- }
- }
-
- // Otherwise assume it is the first of several
- // comprising a file, followed by a blank line.
- var buf bytes.Buffer
- fmt.Fprintln(&buf, line)
- for {
- rl.SetPrompt("... ")
- line, err := rl.Readline()
- if err != nil {
- break outer
- }
- if l := strings.TrimSpace(line); l == "" {
- break // blank
- }
- fmt.Fprintln(&buf, line)
- }
- text := buf.Bytes()
-
- // Try parsing it once more as an expression,
- // such as a call spread over several lines:
- // f(
- // 1,
- // 2
- // )
- if _, err := syntax.ParseExpr("<stdin>", text); err == nil {
- if v, err := skylark.Eval(thread, "<stdin>", text, globals); err != nil {
- printError(err)
- } else if v != skylark.None {
- fmt.Println(v)
- }
- continue
- }
-
- // Execute it as a file.
- if err := execFileNoFreeze(thread, text, globals); err != nil {
- printError(err)
- }
- }
- fmt.Println()
-}
-
-// execFileNoFreeze is skylark.ExecFile without globals.Freeze().
-func execFileNoFreeze(thread *skylark.Thread, src interface{}, globals skylark.StringDict) error {
- // parse
- f, err := syntax.Parse("<stdin>", src)
- if err != nil {
- return err
- }
-
- // resolve
- if err := resolve.File(f, globals.Has, skylark.Universe.Has); err != nil {
- return err
-
- }
-
- // execute
- fr := thread.Push(globals, len(f.Locals))
- defer thread.Pop()
- return fr.ExecStmts(f.Stmts)
-}
-
-type entry struct {
- globals skylark.StringDict
- err error
-}
-
-var cache = make(map[string]*entry)
-
-// load is a simple sequential implementation of module loading.
-func load(thread *skylark.Thread, module string) (skylark.StringDict, error) {
- e, ok := cache[module]
- if e == nil {
- if ok {
- // request for package whose loading is in progress
- return nil, fmt.Errorf("cycle in load graph")
- }
-
- // Add a placeholder to indicate "load in progress".
- cache[module] = nil
-
- // Load it.
- thread := &skylark.Thread{Load: load}
- globals := make(skylark.StringDict)
- err := skylark.ExecFile(thread, module, nil, globals)
- e = &entry{globals, err}
-
- // Update the cache.
- cache[module] = e
- }
- return e.globals, e.err
-}
-
-func printError(err error) {
- if evalErr, ok := err.(*skylark.EvalError); ok {
- fmt.Fprintln(os.Stderr, evalErr.Backtrace())
- } else {
- fmt.Fprintln(os.Stderr, err)
- }
-}
diff --git a/repl/repl.go b/repl/repl.go
new file mode 100644
index 0000000..4334540
--- /dev/null
+++ b/repl/repl.go
@@ -0,0 +1,234 @@
+// The repl package provides a read/eval/print loop for Skylark.
+//
+// It supports readline-style command editing,
+// and interrupts through Control-C.
+//
+// If an input line can be parsed as an expression,
+// the REPL parses and evaluates it and prints its result.
+// Otherwise the REPL reads lines until a blank line,
+// then tries again to parse the multi-line input as an
+// expression. If the input still cannot be parsed as an expression,
+// the REPL parses and executes it as a file (a list of statements),
+// for side effects.
+package repl
+
+// TODO(adonovan):
+//
+// - Distinguish expressions from statements more precisely.
+// Otherwise e.g. 1 is parsed as an expression but
+// 1000000000000000000000000000 is parsed as a file
+// because the scanner fails to convert it to an int64.
+// The spec should clarify limits on numeric literals.
+//
+// - Unparenthesized tuples are not parsed as a single expression:
+// >>> (1, 2)
+// (1, 2)
+// >>> 1, 2
+// ...
+// >>>
+// This is not necessarily a bug.
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "os"
+ "os/signal"
+ "strings"
+
+ "github.com/chzyer/readline"
+ "github.com/google/skylark"
+ "github.com/google/skylark/resolve"
+ "github.com/google/skylark/syntax"
+)
+
+var interrupted = make(chan os.Signal, 1)
+
+// REPL executes a read, eval, print loop.
+//
+// Before evaluating each expression, it sets the Skylark thread local
+// variable named "context" to a context.Context that is cancelled by a
+// SIGINT (Control-C). Client-supplied global functions may use this
+// context to make long-running operations interruptable.
+//
+func REPL(thread *skylark.Thread, globals skylark.StringDict) {
+ signal.Notify(interrupted, os.Interrupt)
+ defer signal.Stop(interrupted)
+
+ rl, err := readline.New(">>> ")
+ if err != nil {
+ PrintError(err)
+ return
+ }
+ defer rl.Close()
+ for {
+ if err := rep(rl, thread, globals); err != nil {
+ if err == readline.ErrInterrupt {
+ fmt.Println(err)
+ continue
+ }
+ break
+ }
+ }
+ fmt.Println()
+}
+
+// rep reads, evaluates, and prints one item.
+//
+// It returns an error (possibly readline.ErrInterrupt)
+// only if readline failed. Skylark errors are printed.
+func rep(rl *readline.Instance, thread *skylark.Thread, globals skylark.StringDict) error {
+ // Each item gets its own context,
+ // which is cancelled by a SIGINT.
+ //
+ // Note: during Readline calls, Control-C causes Readline to return
+ // ErrInterrupt but does not generate a SIGINT.
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ go func() {
+ select {
+ case <-interrupted:
+ cancel()
+ case <-ctx.Done():
+ }
+ }()
+
+ thread.SetLocal("context", ctx)
+
+ rl.SetPrompt(">>> ")
+ line, err := rl.Readline()
+ if err != nil {
+ return err // may be ErrInterrupt
+ }
+
+ if l := strings.TrimSpace(line); l == "" || l[0] == '#' {
+ return nil // blank or comment
+ }
+
+ // If the line contains a well-formed expression, evaluate it.
+ if _, err := syntax.ParseExpr("<stdin>", line); err == nil {
+ if v, err := skylark.Eval(thread, "<stdin>", line, globals); err != nil {
+ PrintError(err)
+ } else if v != skylark.None {
+ fmt.Println(v)
+ }
+ return nil
+ }
+
+ // If the input so far is a single load or assignment statement,
+ // execute it without waiting for a blank line.
+ if f, err := syntax.Parse("<stdin>", line); err == nil && len(f.Stmts) == 1 {
+ switch f.Stmts[0].(type) {
+ case *syntax.AssignStmt, *syntax.LoadStmt:
+ // Execute it as a file.
+ if err := execFileNoFreeze(thread, line, globals); err != nil {
+ PrintError(err)
+ }
+ return nil
+ }
+ }
+
+ // Otherwise assume it is the first of several
+ // comprising a file, followed by a blank line.
+ var buf bytes.Buffer
+ fmt.Fprintln(&buf, line)
+ for {
+ rl.SetPrompt("... ")
+ line, err := rl.Readline()
+ if err != nil {
+ return err // may be ErrInterrupt
+ }
+ if l := strings.TrimSpace(line); l == "" {
+ break // blank
+ }
+ fmt.Fprintln(&buf, line)
+ }
+ text := buf.Bytes()
+
+ // Try parsing it once more as an expression,
+ // such as a call spread over several lines:
+ // f(
+ // 1,
+ // 2
+ // )
+ if _, err := syntax.ParseExpr("<stdin>", text); err == nil {
+ if v, err := skylark.Eval(thread, "<stdin>", text, globals); err != nil {
+ PrintError(err)
+ } else if v != skylark.None {
+ fmt.Println(v)
+ }
+ return nil
+ }
+
+ // Execute it as a file.
+ if err := execFileNoFreeze(thread, text, globals); err != nil {
+ PrintError(err)
+ }
+
+ return nil
+}
+
+// execFileNoFreeze is skylark.ExecFile without globals.Freeze().
+func execFileNoFreeze(thread *skylark.Thread, src interface{}, globals skylark.StringDict) error {
+ // parse
+ f, err := syntax.Parse("<stdin>", src)
+ if err != nil {
+ return err
+ }
+
+ // resolve
+ if err := resolve.File(f, globals.Has, skylark.Universe.Has); err != nil {
+ return err
+
+ }
+
+ // execute
+ fr := thread.Push(globals, len(f.Locals))
+ defer thread.Pop()
+ return fr.ExecStmts(f.Stmts)
+}
+
+// PrintError prints the error to stderr,
+// or its backtrace if it is a Skylark evaluation error.
+func PrintError(err error) {
+ if evalErr, ok := err.(*skylark.EvalError); ok {
+ fmt.Fprintln(os.Stderr, evalErr.Backtrace())
+ } else {
+ fmt.Fprintln(os.Stderr, err)
+ }
+}
+
+// MakeLoad returns a simple sequential implementation of module loading
+// suitable for use in the REPL.
+// Each function returned by MakeLoad accesses a distinct private cache.
+func MakeLoad() func(thread *skylark.Thread, module string) (skylark.StringDict, error) {
+ type entry struct {
+ globals skylark.StringDict
+ err error
+ }
+
+ var cache = make(map[string]*entry)
+
+ return func(thread *skylark.Thread, module string) (skylark.StringDict, error) {
+ e, ok := cache[module]
+ if e == nil {
+ if ok {
+ // request for package whose loading is in progress
+ return nil, fmt.Errorf("cycle in load graph")
+ }
+
+ // Add a placeholder to indicate "load in progress".
+ cache[module] = nil
+
+ // Load it.
+ thread := &skylark.Thread{Load: thread.Load}
+ globals := make(skylark.StringDict)
+ err := skylark.ExecFile(thread, module, nil, globals)
+ e = &entry{globals, err}
+
+ // Update the cache.
+ cache[module] = e
+ }
+ return e.globals, e.err
+ }
+}