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