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