elvish/pkg/eval/eval.go
2019-12-26 17:23:18 +00:00

259 lines
7.1 KiB
Go

// Package eval handles evaluation of parsed Elvish code and provides runtime
// facilities.
package eval
import (
"fmt"
"os"
"strconv"
"strings"
"github.com/elves/elvish/pkg/daemon"
"github.com/elves/elvish/pkg/eval/bundled"
"github.com/elves/elvish/pkg/eval/vals"
"github.com/elves/elvish/pkg/eval/vars"
"github.com/elves/elvish/pkg/parse"
"github.com/elves/elvish/pkg/util"
"github.com/xiaq/persistent/vector"
)
var logger = util.GetLogger("[eval] ")
const (
// FnSuffix is the suffix for the variable names of functions. Defining a
// function "foo" is equivalent to setting a variable named "foo~", and vice
// versa.
FnSuffix = "~"
// NsSuffix is the suffix for the variable names of namespaces. Defining a
// namespace foo is equivalent to setting a variable named "foo:", and vice
// versa.
NsSuffix = ":"
)
const (
defaultValuePrefix = "▶ "
defaultNotifyBgJobSuccess = true
initIndent = vals.NoPretty
)
// Evaler is used to evaluate elvish sources. It maintains runtime context
// shared among all evalCtx instances.
type Evaler struct {
evalerScopes
state state
// Chdir hooks.
beforeChdir []func(string)
afterChdir []func(string)
// Used to receive SIGINT.
intCh chan struct{}
// State of the module system.
libDir string
bundled map[string]string
modules map[string]Ns
// Dependencies.
//
// TODO: Remove these dependency by providing more general extension points.
DaemonClient daemon.Client
Editor Editor
}
type evalerScopes struct {
Global Ns
Builtin Ns
}
// NewEvaler creates a new Evaler.
func NewEvaler() *Evaler {
builtin := builtinNs.Clone()
ev := &Evaler{
state: state{
valuePrefix: defaultValuePrefix,
notifyBgJobSuccess: defaultNotifyBgJobSuccess,
numBgJobs: 0,
},
evalerScopes: evalerScopes{
Global: make(Ns),
Builtin: builtin,
},
modules: map[string]Ns{
"builtin": builtin,
},
bundled: bundled.Get(),
Editor: nil,
intCh: nil,
}
beforeChdirElvish, afterChdirElvish := vector.Empty, vector.Empty
ev.beforeChdir = append(ev.beforeChdir,
adaptChdirHook("before-chdir", ev, &beforeChdirElvish))
ev.afterChdir = append(ev.afterChdir,
adaptChdirHook("after-chdir", ev, &afterChdirElvish))
builtin["before-chdir"] = vars.FromPtr(&beforeChdirElvish)
builtin["after-chdir"] = vars.FromPtr(&afterChdirElvish)
builtin["value-out-indicator"] = vars.FromPtrWithMutex(
&ev.state.valuePrefix, &ev.state.mutex)
builtin["notify-bg-job-success"] = vars.FromPtrWithMutex(
&ev.state.notifyBgJobSuccess, &ev.state.mutex)
builtin["num-bg-jobs"] = vars.FromGet(func() interface{} {
return strconv.Itoa(ev.state.getNumBgJobs())
})
builtin["pwd"] = PwdVariable{ev}
return ev
}
func adaptChdirHook(name string, ev *Evaler, pfns *vector.Vector) func(string) {
return func(path string) {
stdPorts := newStdPorts(
os.Stdin, os.Stdout, os.Stderr, ev.state.getValuePrefix())
defer stdPorts.close()
for it := (*pfns).Iterator(); it.HasElem(); it.Next() {
fn, ok := it.Elem().(Callable)
if !ok {
fmt.Fprintln(os.Stderr, name, "hook must be callable")
continue
}
fm := NewTopFrame(ev,
NewInternalSource("["+name+" hook]"), stdPorts.ports[:])
err := fm.Call(fn, []interface{}{path}, NoOpts)
if err != nil {
// TODO: Stack trace
fmt.Fprintln(os.Stderr, err)
}
}
}
}
// Close releases resources allocated when creating this Evaler. Currently this
// does nothing and always returns a nil error.
func (ev *Evaler) Close() error {
return nil
}
// AddBeforeChdir adds a function to run before changing directory.
func (ev *Evaler) AddBeforeChdir(f func(string)) {
ev.beforeChdir = append(ev.beforeChdir, f)
}
// AddAfterChdir adds a function to run after changing directory.
func (ev *Evaler) AddAfterChdir(f func(string)) {
ev.afterChdir = append(ev.afterChdir, f)
}
// InstallDaemonClient installs a daemon client to the Evaler.
func (ev *Evaler) InstallDaemonClient(client daemon.Client) {
ev.DaemonClient = client
}
// InstallModule installs a module to the Evaler so that it can be used with
// "use $name" from script.
func (ev *Evaler) InstallModule(name string, mod Ns) {
ev.modules[name] = mod
}
// InstallBundled installs a bundled module to the Evaler.
func (ev *Evaler) InstallBundled(name, src string) {
ev.bundled[name] = src
}
// SetArgs replaces the $args builtin variable with a vector built from the
// argument.
func (ev *Evaler) SetArgs(args []string) {
v := vector.Empty
for _, arg := range args {
v = v.Cons(arg)
}
ev.Builtin["args"] = vars.NewReadOnly(v)
}
// SetLibDir sets the library directory, in which external modules are to be
// found.
func (ev *Evaler) SetLibDir(libDir string) {
ev.libDir = libDir
}
func searchPaths() []string {
return strings.Split(os.Getenv("PATH"), ":")
}
// growPorts makes the size of ec.ports at least n, adding nil's if necessary.
func (fm *Frame) growPorts(n int) {
if len(fm.ports) >= n {
return
}
ports := fm.ports
fm.ports = make([]*Port, n)
copy(fm.ports, ports)
}
// Eval evaluates an Op using the specified ports.
func (ev *Evaler) Eval(op Op, ports []*Port) error {
ec := NewTopFrame(ev, op.Src, ports)
return ec.eval(op.Inner)
}
// EvalSourceInTTY evaluates Elvish source code in the current terminal.
func (ev *Evaler) EvalSourceInTTY(src *Source) error {
n, err := parse.AsChunk(src.name, src.code)
if err != nil {
return err
}
op, err := ev.Compile(n, src)
if err != nil {
return err
}
return ev.EvalInTTY(op)
}
// EvalInTTY evaluates an Op in the current terminal. It uses the stdin, stdout
// and stderr to build the ports, relays SIGINT from the terminal to ev.intCh,
// and puts Elvish in the foreground after evaluation finishes.
//
// TODO(xiaq): This function can only be used to evaluate an Op, and cannot be
// used to call functions with stdPorts. Make the Evaler initialize a stdPorts
// on construction, instead of in this function, so that NewTopFrame does not
// require the caller to supply the ports.
func (ev *Evaler) EvalInTTY(op Op) error {
stdPorts := newStdPorts(
os.Stdin, os.Stdout, os.Stderr, ev.state.getValuePrefix())
defer stdPorts.close()
intCh, cleanupInt := listenInterrupts()
ev.intCh = intCh
defer func() {
cleanupInt()
ev.intCh = nil
// Put myself in foreground, in case some command has put me in background.
err := putSelfInFg()
if err != nil {
fmt.Println("failed to put myself in foreground:", err)
}
}()
return ev.Eval(op, stdPorts.ports[:])
}
// Compile compiles Elvish code in the global scope. If the error is not nil, it
// can be passed to GetCompilationError to retrieve more details.
func (ev *Evaler) Compile(n *parse.Chunk, src *Source) (Op, error) {
return ev.CompileWithGlobal(n, src, ev.Global)
}
// CompileWithGlobal compiles Elvish code in an alternative global scope. If the
// error is not nil, it can be passed to GetCompilationError to retrieve more
// details.
//
// TODO(xiaq): To use the Op created, the caller must create a Frame and mutate
// its local scope manually. Consider restructuring the API to make that
// unnecessary.
func (ev *Evaler) CompileWithGlobal(n *parse.Chunk, src *Source, g Ns) (Op, error) {
return compile(ev.Builtin.static(), g.static(), n, src)
}