Files
moxa/interp/interp.go
Marc Vertes a988459dcd interp: fix a memory management issue causing wrong closure context
The first change forces a variable definition to reallocate a
new memory slot to avoid corrupting a previously defined one in
a loop block.

The second change ensures that the frame clone operations obtains
a copy of the original data slice, to preserve the original context
set in a loop.

Fixes #1035.
2021-03-09 09:30:04 +01:00

853 lines
26 KiB
Go

package interp
import (
"bufio"
"context"
"errors"
"flag"
"fmt"
"go/build"
"go/scanner"
"go/token"
"io"
"io/ioutil"
"log"
"os"
"os/signal"
"reflect"
"runtime"
"runtime/debug"
"strconv"
"strings"
"sync"
"sync/atomic"
)
// Interpreter node structure for AST and CFG.
type node struct {
child []*node // child subtrees (AST)
anc *node // ancestor (AST)
start *node // entry point in subtree (CFG)
tnext *node // true branch successor (CFG)
fnext *node // false branch successor (CFG)
interp *Interpreter // interpreter context
frame *frame // frame pointer used for closures only (TODO: suppress this)
index int64 // node index (dot display)
findex int // index of value in frame or frame size (func def, type def)
level int // number of frame indirections to access value
nleft int // number of children in left part (assign) or indicates preceding type (compositeLit)
nright int // number of children in right part (assign)
kind nkind // kind of node
pos token.Pos // position in source code, relative to fset
sym *symbol // associated symbol
typ *itype // type of value in frame, or nil
recv *receiver // method receiver node for call, or nil
types []reflect.Type // frame types, used by function literals only
action action // action
exec bltn // generated function to execute
gen bltnGenerator // generator function to produce above bltn
val interface{} // static generic value (CFG execution)
rval reflect.Value // reflection value to let runtime access interpreter (CFG)
ident string // set if node is a var or func
}
// receiver stores method receiver object access path.
type receiver struct {
node *node // receiver value for alias and struct types
val reflect.Value // receiver value for interface type and value type
index []int // path in receiver value for interface or value type
}
// frame contains values for the current execution level (a function context).
type frame struct {
// id is an atomic counter used for cancellation, only accessed
// via newFrame/runid/setrunid/clone.
// Located at start of struct to ensure proper aligment.
id uint64
root *frame // global space
anc *frame // ancestor frame (caller space)
data []reflect.Value // values
mutex sync.RWMutex
deferred [][]reflect.Value // defer stack
recovered interface{} // to handle panic recover
done reflect.SelectCase // for cancellation of channel operations
}
func newFrame(anc *frame, length int, id uint64) *frame {
f := &frame{
anc: anc,
data: make([]reflect.Value, length),
id: id,
}
if anc == nil {
f.root = f
} else {
f.done = anc.done
f.root = anc.root
}
return f
}
func (f *frame) runid() uint64 { return atomic.LoadUint64(&f.id) }
func (f *frame) setrunid(id uint64) { atomic.StoreUint64(&f.id, id) }
func (f *frame) clone(fork bool) *frame {
f.mutex.RLock()
defer f.mutex.RUnlock()
nf := &frame{
anc: f.anc,
root: f.root,
deferred: f.deferred,
recovered: f.recovered,
id: f.runid(),
done: f.done,
}
if fork {
nf.data = make([]reflect.Value, len(f.data))
copy(nf.data, f.data)
} else {
nf.data = f.data
}
return nf
}
// Exports stores the map of binary packages per package path.
type Exports map[string]map[string]reflect.Value
// imports stores the map of source packages per package path.
type imports map[string]map[string]*symbol
// opt stores interpreter options.
type opt struct {
astDot bool // display AST graph (debug)
cfgDot bool // display CFG graph (debug)
// dotCmd is the command to process the dot graph produced when astDot and/or
// cfgDot is enabled. It defaults to 'dot -Tdot -o <filename>.dot'.
dotCmd string
noRun bool // compile, but do not run
fastChan bool // disable cancellable chan operations
context build.Context // build context: GOPATH, build constraints
stdin io.Reader // standard input
stdout io.Writer // standard output
stderr io.Writer // standard error
}
// Interpreter contains global resources and state.
type Interpreter struct {
// id is an atomic counter counter used for run cancellation,
// only accessed via runid/stop
// Located at start of struct to ensure proper alignment on 32 bit
// architectures.
id uint64
// nindex is a node number incremented for each new node.
// It is used for debug (AST and CFG graphs). As it is atomically
// incremented, keep it aligned on 64 bits boundary.
nindex int64
name string // name of the input source file (or main)
opt // user settable options
cancelChan bool // enables cancellable chan operations
fset *token.FileSet // fileset to locate node in source code
binPkg Exports // binary packages used in interpreter, indexed by path
rdir map[string]bool // for src import cycle detection
mutex sync.RWMutex
frame *frame // program data storage during execution
universe *scope // interpreter global level scope
scopes map[string]*scope // package level scopes, indexed by import path
srcPkg imports // source packages used in interpreter, indexed by path
pkgNames map[string]string // package names, indexed by import path
done chan struct{} // for cancellation of channel operations
hooks *hooks // symbol hooks
}
const (
mainID = "main"
selfPrefix = "github.com/traefik/yaegi"
selfPath = selfPrefix + "/interp"
// DefaultSourceName is the name used by default when the name of the input
// source file has not been specified for an Eval.
// TODO(mpl): something even more special as a name?
DefaultSourceName = "_.go"
// Test is the value to pass to EvalPath to activate evaluation of test functions.
Test = false
// NoTest is the value to pass to EvalPath to skip evaluation of test functions.
NoTest = true
)
// Symbols exposes interpreter values.
var Symbols = Exports{
selfPath: map[string]reflect.Value{
"New": reflect.ValueOf(New),
"Interpreter": reflect.ValueOf((*Interpreter)(nil)),
"Options": reflect.ValueOf((*Options)(nil)),
"Panic": reflect.ValueOf((*Panic)(nil)),
},
}
func init() { Symbols[selfPath]["Symbols"] = reflect.ValueOf(Symbols) }
// _error is a wrapper of error interface type.
type _error struct {
WError func() string
}
func (w _error) Error() string { return w.WError() }
// Panic is an error recovered from a panic call in interpreted code.
type Panic struct {
// Value is the recovered value of a call to panic.
Value interface{}
// Callers is the call stack obtained from the recover call.
// It may be used as the parameter to runtime.CallersFrames.
Callers []uintptr
// Stack is the call stack buffer for debug.
Stack []byte
}
// TODO: Capture interpreter stack frames also and remove
// fmt.Fprintln(n.interp.stderr, oNode.cfgErrorf("panic")) in runCfg.
func (e Panic) Error() string { return fmt.Sprint(e.Value) }
// Walk traverses AST n in depth first order, call cbin function
// at node entry and cbout function at node exit.
func (n *node) Walk(in func(n *node) bool, out func(n *node)) {
if in != nil && !in(n) {
return
}
for _, child := range n.child {
child.Walk(in, out)
}
if out != nil {
out(n)
}
}
// Options are the interpreter options.
type Options struct {
// GoPath sets GOPATH for the interpreter.
GoPath string
// BuildTags sets build constraints for the interpreter.
BuildTags []string
// Standard input, output and error streams.
// They default to os.Stding, os.Stdout and os.Stderr respectively.
Stdin io.Reader
Stdout, Stderr io.Writer
}
// New returns a new interpreter.
func New(options Options) *Interpreter {
i := Interpreter{
opt: opt{context: build.Default},
frame: newFrame(nil, 0, 0),
fset: token.NewFileSet(),
universe: initUniverse(),
scopes: map[string]*scope{},
binPkg: Exports{"": map[string]reflect.Value{"_error": reflect.ValueOf((*_error)(nil))}},
srcPkg: imports{},
pkgNames: map[string]string{},
rdir: map[string]bool{},
hooks: &hooks{},
}
if i.opt.stdin = options.Stdin; i.opt.stdin == nil {
i.opt.stdin = os.Stdin
}
if i.opt.stdout = options.Stdout; i.opt.stdout == nil {
i.opt.stdout = os.Stdout
}
if i.opt.stderr = options.Stderr; i.opt.stderr == nil {
i.opt.stderr = os.Stderr
}
i.opt.context.GOPATH = options.GoPath
if len(options.BuildTags) > 0 {
i.opt.context.BuildTags = options.BuildTags
}
// astDot activates AST graph display for the interpreter
i.opt.astDot, _ = strconv.ParseBool(os.Getenv("YAEGI_AST_DOT"))
// cfgDot activates CFG graph display for the interpreter
i.opt.cfgDot, _ = strconv.ParseBool(os.Getenv("YAEGI_CFG_DOT"))
// dotCmd defines how to process the dot code generated whenever astDot and/or
// cfgDot is enabled. It defaults to 'dot -Tdot -o<filename>.dot' where filename
// is context dependent.
i.opt.dotCmd = os.Getenv("YAEGI_DOT_CMD")
// noRun disables the execution (but not the compilation) in the interpreter
i.opt.noRun, _ = strconv.ParseBool(os.Getenv("YAEGI_NO_RUN"))
// fastChan disables the cancellable version of channel operations in evalWithContext
i.opt.fastChan, _ = strconv.ParseBool(os.Getenv("YAEGI_FAST_CHAN"))
return &i
}
const (
bltnAppend = "append"
bltnCap = "cap"
bltnClose = "close"
bltnComplex = "complex"
bltnImag = "imag"
bltnCopy = "copy"
bltnDelete = "delete"
bltnLen = "len"
bltnMake = "make"
bltnNew = "new"
bltnPanic = "panic"
bltnPrint = "print"
bltnPrintln = "println"
bltnReal = "real"
bltnRecover = "recover"
)
func initUniverse() *scope {
sc := &scope{global: true, sym: map[string]*symbol{
// predefined Go types
"bool": {kind: typeSym, typ: &itype{cat: boolT, name: "bool"}},
"byte": {kind: typeSym, typ: &itype{cat: uint8T, name: "uint8"}},
"complex64": {kind: typeSym, typ: &itype{cat: complex64T, name: "complex64"}},
"complex128": {kind: typeSym, typ: &itype{cat: complex128T, name: "complex128"}},
"error": {kind: typeSym, typ: &itype{cat: errorT, name: "error"}},
"float32": {kind: typeSym, typ: &itype{cat: float32T, name: "float32"}},
"float64": {kind: typeSym, typ: &itype{cat: float64T, name: "float64"}},
"int": {kind: typeSym, typ: &itype{cat: intT, name: "int"}},
"int8": {kind: typeSym, typ: &itype{cat: int8T, name: "int8"}},
"int16": {kind: typeSym, typ: &itype{cat: int16T, name: "int16"}},
"int32": {kind: typeSym, typ: &itype{cat: int32T, name: "int32"}},
"int64": {kind: typeSym, typ: &itype{cat: int64T, name: "int64"}},
"interface{}": {kind: typeSym, typ: &itype{cat: interfaceT}},
"rune": {kind: typeSym, typ: &itype{cat: int32T, name: "int32"}},
"string": {kind: typeSym, typ: &itype{cat: stringT, name: "string"}},
"uint": {kind: typeSym, typ: &itype{cat: uintT, name: "uint"}},
"uint8": {kind: typeSym, typ: &itype{cat: uint8T, name: "uint8"}},
"uint16": {kind: typeSym, typ: &itype{cat: uint16T, name: "uint16"}},
"uint32": {kind: typeSym, typ: &itype{cat: uint32T, name: "uint32"}},
"uint64": {kind: typeSym, typ: &itype{cat: uint64T, name: "uint64"}},
"uintptr": {kind: typeSym, typ: &itype{cat: uintptrT, name: "uintptr"}},
// predefined Go constants
"false": {kind: constSym, typ: untypedBool(), rval: reflect.ValueOf(false)},
"true": {kind: constSym, typ: untypedBool(), rval: reflect.ValueOf(true)},
"iota": {kind: constSym, typ: untypedInt()},
// predefined Go zero value
"nil": {typ: &itype{cat: nilT, untyped: true}},
// predefined Go builtins
bltnAppend: {kind: bltnSym, builtin: _append},
bltnCap: {kind: bltnSym, builtin: _cap},
bltnClose: {kind: bltnSym, builtin: _close},
bltnComplex: {kind: bltnSym, builtin: _complex},
bltnImag: {kind: bltnSym, builtin: _imag},
bltnCopy: {kind: bltnSym, builtin: _copy},
bltnDelete: {kind: bltnSym, builtin: _delete},
bltnLen: {kind: bltnSym, builtin: _len},
bltnMake: {kind: bltnSym, builtin: _make},
bltnNew: {kind: bltnSym, builtin: _new},
bltnPanic: {kind: bltnSym, builtin: _panic},
bltnPrint: {kind: bltnSym, builtin: _print},
bltnPrintln: {kind: bltnSym, builtin: _println},
bltnReal: {kind: bltnSym, builtin: _real},
bltnRecover: {kind: bltnSym, builtin: _recover},
}}
return sc
}
// resizeFrame resizes the global frame of interpreter.
func (interp *Interpreter) resizeFrame() {
l := len(interp.universe.types)
b := len(interp.frame.data)
if l-b <= 0 {
return
}
data := make([]reflect.Value, l)
copy(data, interp.frame.data)
for j, t := range interp.universe.types[b:] {
data[b+j] = reflect.New(t).Elem()
}
interp.frame.data = data
}
// Eval evaluates Go code represented as a string. Eval returns the last result
// computed by the interpreter, and a non nil error in case of failure.
func (interp *Interpreter) Eval(src string) (res reflect.Value, err error) {
return interp.eval(src, "", true)
}
// EvalPath evaluates Go code located at path and returns the last result computed
// by the interpreter, and a non nil error in case of failure.
// The main function of the main package is executed if present.
func (interp *Interpreter) EvalPath(path string) (res reflect.Value, err error) {
if !isFile(path) {
_, err := interp.importSrc(mainID, path, NoTest)
return res, err
}
b, err := ioutil.ReadFile(path)
if err != nil {
return res, err
}
return interp.eval(string(b), path, false)
}
// EvalTest evaluates Go code located at path, including test files with "_test.go" suffix.
// A non nil error is returned in case of failure.
// The main function, test functions and benchmark functions are internally compiled but not
// executed. Test functions can be retrieved using the Symbol() method.
func (interp *Interpreter) EvalTest(path string) error {
_, err := interp.importSrc(mainID, path, Test)
return err
}
// Symbols returns a map of interpreter exported symbol values for the given
// import path. If the argument is the empty string, all known symbols are
// returned.
func (interp *Interpreter) Symbols(importPath string) Exports {
m := map[string]map[string]reflect.Value{}
interp.mutex.RLock()
defer interp.mutex.RUnlock()
for k, v := range interp.srcPkg {
if importPath != "" && k != importPath {
continue
}
syms := map[string]reflect.Value{}
for n, s := range v {
if !canExport(n) {
// Skip private non-exported symbols.
continue
}
switch s.kind {
case constSym:
syms[n] = s.rval
case funcSym:
syms[n] = genFunctionWrapper(s.node)(interp.frame)
case varSym:
syms[n] = interp.frame.data[s.index]
case typeSym:
syms[n] = reflect.New(s.typ.TypeOf())
}
}
if len(syms) > 0 {
m[k] = syms
}
if importPath != "" {
return m
}
}
if importPath != "" && len(m) > 0 {
return m
}
for k, v := range interp.binPkg {
if importPath != "" && k != importPath {
continue
}
m[k] = v
if importPath != "" {
return m
}
}
return m
}
func isFile(path string) bool {
fi, err := os.Stat(path)
return err == nil && fi.Mode().IsRegular()
}
func (interp *Interpreter) eval(src, name string, inc bool) (res reflect.Value, err error) {
if name != "" {
interp.name = name
}
if interp.name == "" {
interp.name = DefaultSourceName
}
defer func() {
r := recover()
if r != nil {
var pc [64]uintptr // 64 frames should be enough.
n := runtime.Callers(1, pc[:])
err = Panic{Value: r, Callers: pc[:n], Stack: debug.Stack()}
}
}()
// Parse source to AST.
pkgName, root, err := interp.ast(src, interp.name, inc)
if err != nil || root == nil {
return res, err
}
if interp.astDot {
dotCmd := interp.dotCmd
if dotCmd == "" {
dotCmd = defaultDotCmd(interp.name, "yaegi-ast-")
}
root.astDot(dotWriter(dotCmd), interp.name)
if interp.noRun {
return res, err
}
}
// Perform global types analysis.
if err = interp.gtaRetry([]*node{root}, pkgName); err != nil {
return res, err
}
// Annotate AST with CFG informations.
initNodes, err := interp.cfg(root, pkgName)
if err != nil {
if interp.cfgDot {
dotCmd := interp.dotCmd
if dotCmd == "" {
dotCmd = defaultDotCmd(interp.name, "yaegi-cfg-")
}
root.cfgDot(dotWriter(dotCmd))
}
return res, err
}
if root.kind != fileStmt {
// REPL may skip package statement.
setExec(root.start)
}
interp.mutex.Lock()
gs := interp.scopes[pkgName]
if interp.universe.sym[pkgName] == nil {
// Make the package visible under a path identical to its name.
interp.srcPkg[pkgName] = gs.sym
interp.universe.sym[pkgName] = &symbol{kind: pkgSym, typ: &itype{cat: srcPkgT, path: pkgName}}
interp.pkgNames[pkgName] = pkgName
}
interp.mutex.Unlock()
// Add main to list of functions to run, after all inits.
if m := gs.sym[mainID]; pkgName == mainID && m != nil {
initNodes = append(initNodes, m.node)
}
if interp.cfgDot {
dotCmd := interp.dotCmd
if dotCmd == "" {
dotCmd = defaultDotCmd(interp.name, "yaegi-cfg-")
}
root.cfgDot(dotWriter(dotCmd))
}
if interp.noRun {
return res, err
}
// Generate node exec closures.
if err = genRun(root); err != nil {
return res, err
}
// Init interpreter execution memory frame.
interp.frame.setrunid(interp.runid())
interp.frame.mutex.Lock()
interp.resizeFrame()
interp.frame.mutex.Unlock()
// Execute node closures.
interp.run(root, nil)
// Wire and execute global vars.
n, err := genGlobalVars([]*node{root}, interp.scopes[pkgName])
if err != nil {
return res, err
}
interp.run(n, nil)
for _, n := range initNodes {
interp.run(n, interp.frame)
}
v := genValue(root)
res = v(interp.frame)
// If result is an interpreter node, wrap it in a runtime callable function.
if res.IsValid() {
if n, ok := res.Interface().(*node); ok {
res = genFunctionWrapper(n)(interp.frame)
}
}
return res, err
}
// EvalWithContext evaluates Go code represented as a string. It returns
// a map on current interpreted package exported symbols.
func (interp *Interpreter) EvalWithContext(ctx context.Context, src string) (reflect.Value, error) {
var v reflect.Value
var err error
interp.mutex.Lock()
interp.done = make(chan struct{})
interp.cancelChan = !interp.opt.fastChan
interp.mutex.Unlock()
done := make(chan struct{})
go func() {
defer close(done)
v, err = interp.Eval(src)
}()
select {
case <-ctx.Done():
interp.stop()
return reflect.Value{}, ctx.Err()
case <-done:
}
return v, err
}
// stop sends a semaphore to all running frames and closes the chan
// operation short circuit channel. stop may only be called once per
// invocation of EvalWithContext.
func (interp *Interpreter) stop() {
atomic.AddUint64(&interp.id, 1)
close(interp.done)
}
func (interp *Interpreter) runid() uint64 { return atomic.LoadUint64(&interp.id) }
// getWrapper returns the wrapper type of the corresponding interface, or nil if not found.
func (interp *Interpreter) getWrapper(t reflect.Type) reflect.Type {
if p, ok := interp.binPkg[t.PkgPath()]; ok {
return p["_"+t.Name()].Type().Elem()
}
return nil
}
// Use loads binary runtime symbols in the interpreter context so
// they can be used in interpreted code.
func (interp *Interpreter) Use(values Exports) {
for k, v := range values {
if k == selfPrefix {
interp.hooks.Parse(v)
continue
}
if interp.binPkg[k] == nil {
interp.binPkg[k] = make(map[string]reflect.Value)
}
for s, sym := range v {
interp.binPkg[k][s] = sym
}
}
// Checks if input values correspond to stdlib packages by looking for one
// well known stdlib package path.
if _, ok := values["fmt"]; ok {
fixStdio(interp)
}
}
// fixStdio redefines interpreter stdlib symbols to use the standard input,
// output and errror assigned to the interpreter. The changes are limited to
// the interpreter only. Global values os.Stdin, os.Stdout and os.Stderr are
// not changed. Note that it is possible to escape the virtualized stdio by
// read/write directly to file descriptors 0, 1, 2.
func fixStdio(interp *Interpreter) {
p := interp.binPkg["fmt"]
if p == nil {
return
}
stdin, stdout, stderr := interp.stdin, interp.stdout, interp.stderr
p["Print"] = reflect.ValueOf(func(a ...interface{}) (n int, err error) { return fmt.Fprint(stdout, a...) })
p["Printf"] = reflect.ValueOf(func(f string, a ...interface{}) (n int, err error) { return fmt.Fprintf(stdout, f, a...) })
p["Println"] = reflect.ValueOf(func(a ...interface{}) (n int, err error) { return fmt.Fprintln(stdout, a...) })
p["Scan"] = reflect.ValueOf(func(a ...interface{}) (n int, err error) { return fmt.Fscan(stdin, a...) })
p["Scanf"] = reflect.ValueOf(func(f string, a ...interface{}) (n int, err error) { return fmt.Fscanf(stdin, f, a...) })
p["Scanln"] = reflect.ValueOf(func(a ...interface{}) (n int, err error) { return fmt.Fscanln(stdin, a...) })
if p = interp.binPkg["flag"]; p != nil {
c := flag.NewFlagSet(os.Args[0], flag.PanicOnError)
c.SetOutput(stderr)
p["CommandLine"] = reflect.ValueOf(&c).Elem()
}
if p = interp.binPkg["log"]; p != nil {
l := log.New(stderr, "", log.LstdFlags)
// Restrict Fatal symbols to panic instead of exit.
p["Fatal"] = reflect.ValueOf(l.Panic)
p["Fatalf"] = reflect.ValueOf(l.Panicf)
p["Fatalln"] = reflect.ValueOf(l.Panicln)
p["Flags"] = reflect.ValueOf(l.Flags)
p["Output"] = reflect.ValueOf(l.Output)
p["Panic"] = reflect.ValueOf(l.Panic)
p["Panicf"] = reflect.ValueOf(l.Panicf)
p["Panicln"] = reflect.ValueOf(l.Panicln)
p["Prefix"] = reflect.ValueOf(l.Prefix)
p["Print"] = reflect.ValueOf(l.Print)
p["Printf"] = reflect.ValueOf(l.Printf)
p["Println"] = reflect.ValueOf(l.Println)
p["SetFlags"] = reflect.ValueOf(l.SetFlags)
p["SetOutput"] = reflect.ValueOf(l.SetOutput)
p["SetPrefix"] = reflect.ValueOf(l.SetPrefix)
p["Writer"] = reflect.ValueOf(l.Writer)
}
if p = interp.binPkg["os"]; p != nil {
p["Stdin"] = reflect.ValueOf(&stdin).Elem()
p["Stdout"] = reflect.ValueOf(&stdout).Elem()
p["Stderr"] = reflect.ValueOf(&stderr).Elem()
}
}
// ignoreScannerError returns true if the error from Go scanner can be safely ignored
// to let the caller grab one more line before retrying to parse its input.
func ignoreScannerError(e *scanner.Error, s string) bool {
msg := e.Msg
if strings.HasSuffix(msg, "found 'EOF'") {
return true
}
if msg == "raw string literal not terminated" {
return true
}
if strings.HasPrefix(msg, "expected operand, found '}'") && !strings.HasSuffix(s, "}") {
return true
}
return false
}
// REPL performs a Read-Eval-Print-Loop on input reader.
// Results are printed to the output writer of the Interpreter, provided as option
// at creation time. Errors are printed to the similarly defined errors writer.
// The last interpreter result value and error are returned.
func (interp *Interpreter) REPL() (reflect.Value, error) {
// Preimport used bin packages, to avoid having to import these packages manually
// in REPL mode. These packages are already loaded anyway.
sc := interp.universe
for k := range interp.binPkg {
name := identifier.FindString(k)
if name == "" || name == "rand" || name == "scanner" || name == "template" || name == "pprof" {
// Skip any package with an ambiguous name (i.e crypto/rand vs math/rand).
// Those will have to be imported explicitly.
continue
}
sc.sym[name] = &symbol{kind: pkgSym, typ: &itype{cat: binPkgT, path: k, scope: sc}}
}
in, out, errs := interp.stdin, interp.stdout, interp.stderr
ctx, cancel := context.WithCancel(context.Background())
end := make(chan struct{}) // channel to terminate the REPL
sig := make(chan os.Signal, 1) // channel to trap interrupt signal (Ctrl-C)
lines := make(chan string) // channel to read REPL input lines
prompt := getPrompt(in, out) // prompt activated on tty like IO stream
s := bufio.NewScanner(in) // read input stream line by line
var v reflect.Value // result value from eval
var err error // error from eval
src := "" // source string to evaluate
signal.Notify(sig, os.Interrupt)
defer signal.Stop(sig)
prompt(v)
go func() {
defer close(end)
for s.Scan() {
lines <- s.Text()
}
if e := s.Err(); e != nil {
fmt.Fprintln(errs, e)
}
}()
go func() {
for {
select {
case <-sig:
cancel()
lines <- ""
case <-end:
return
}
}
}()
for {
var line string
select {
case <-end:
cancel()
return v, err
case line = <-lines:
src += line + "\n"
}
v, err = interp.EvalWithContext(ctx, src)
if err != nil {
switch e := err.(type) {
case scanner.ErrorList:
if len(e) > 0 && ignoreScannerError(e[0], line) {
continue
}
fmt.Fprintln(errs, strings.TrimPrefix(e[0].Error(), DefaultSourceName+":"))
case Panic:
fmt.Fprintln(errs, e.Value)
fmt.Fprintln(errs, string(e.Stack))
default:
fmt.Fprintln(errs, err)
}
}
if errors.Is(err, context.Canceled) {
ctx, cancel = context.WithCancel(context.Background())
}
src = ""
prompt(v)
}
}
func doPrompt(out io.Writer) func(v reflect.Value) {
return func(v reflect.Value) {
if v.IsValid() {
fmt.Fprintln(out, ":", v)
}
fmt.Fprint(out, "> ")
}
}
// getPrompt returns a function which prints a prompt only if input is a terminal.
func getPrompt(in io.Reader, out io.Writer) func(reflect.Value) {
forcePrompt, _ := strconv.ParseBool(os.Getenv("YAEGI_PROMPT"))
if forcePrompt {
return doPrompt(out)
}
s, ok := in.(interface{ Stat() (os.FileInfo, error) })
if !ok {
return func(reflect.Value) {}
}
stat, err := s.Stat()
if err == nil && stat.Mode()&os.ModeCharDevice != 0 {
return doPrompt(out)
}
return func(reflect.Value) {}
}