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)
- }
-}