elvish/pkg/eval/eval.go

346 lines
9.9 KiB
Go
Raw Normal View History

2018-01-01 23:12:34 +08:00
// Package eval handles evaluation of parsed Elvish code and provides runtime
// facilities.
package eval
import (
2014-01-27 18:55:45 +08:00
"fmt"
"io"
"os"
"strconv"
2014-02-10 11:33:53 +08:00
2019-12-24 04:00:59 +08:00
"github.com/elves/elvish/pkg/daemon"
"github.com/elves/elvish/pkg/diag"
"github.com/elves/elvish/pkg/eval/mods/bundled"
2019-12-24 04:00:59 +08:00
"github.com/elves/elvish/pkg/eval/vals"
"github.com/elves/elvish/pkg/eval/vars"
"github.com/elves/elvish/pkg/logutil"
2019-12-24 04:00:59 +08:00
"github.com/elves/elvish/pkg/parse"
"github.com/xiaq/persistent/vector"
)
var logger = logutil.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.
2015-02-25 07:37:18 +08:00
type Evaler struct {
evalerScopes
state state
2018-09-29 03:09:02 +08:00
// Chdir hooks.
beforeChdir []func(string)
afterChdir []func(string)
// State of the module system.
libDir string
2018-09-29 03:09:02 +08:00
bundled map[string]string
// Internal modules are indexed by use specs. External modules are indexed by
// absolute paths.
modules map[string]*Ns
2018-09-29 03:09:02 +08:00
deprecations deprecationRegistry
2018-09-29 03:09:02 +08:00
// Dependencies.
//
// TODO: Remove these dependency by providing more general extension points.
2019-12-27 01:23:18 +08:00
DaemonClient daemon.Client
2018-09-29 03:09:02 +08:00
Editor Editor
}
// EvalCfg keeps configuration for the (*Evaler).Eval method.
type EvalCfg struct {
// Ports to use in evaluation. If nil, equivalent to specifying
// DevNullPorts[:]. If not nil, must contain at least 3 elements.
Ports []*Port
// Callback to get a channel of interrupt signals and a function to call
// when the channel is no longer needed.
Interrupt func() (<-chan struct{}, func())
// Whether the Eval method should try to put the Elvish in the foreground
// after the code is executed.
PutInFg bool
}
type evalerScopes struct {
Global *Ns
Builtin *Ns
}
//elvdoc:var after-chdir
//
// A list of functions to run after changing directory. These functions are always
// called with directory to change it, which might be a relative path. The
// following example also shows `$before-chdir`:
//
// ```elvish-transcript
// ~> before-chdir = [[dir]{ echo "Going to change to "$dir", pwd is "$pwd }]
// ~> after-chdir = [[dir]{ echo "Changed to "$dir", pwd is "$pwd }]
// ~> cd /usr
// Going to change to /usr, pwd is /Users/xiaq
// Changed to /usr, pwd is /usr
// /usr> cd local
// Going to change to local, pwd is /usr
// Changed to local, pwd is /usr/local
// /usr/local>
// ```
//
// @cf before-chdir
//elvdoc:var before-chdir
//
// A list of functions to run before changing directory. These functions are always
// called with the new working directory.
//
// @cf after-chdir
//elvdoc:var num-bg-jobs
//
// Number of background jobs.
//elvdoc:var notify-bg-job-success
//
// Whether to notify success of background jobs, defaulting to `$true`.
//
// Failures of background jobs are always notified.
//elvdoc:var value-out-indicator
//
// A string put before value outputs (such as those of of `put`). Defaults to
// `'▶ '`. Example:
//
// ```elvish-transcript
// ~> put lorem ipsum
// ▶ lorem
// ▶ ipsum
// ~> value-out-indicator = 'val> '
// ~> put lorem ipsum
// val> lorem
// val> ipsum
// ```
//
// Note that you almost always want some trailing whitespace for readability.
// NewEvaler creates a new Evaler.
func NewEvaler() *Evaler {
builtin := builtinNs.Ns()
2017-06-19 18:58:58 +08:00
ev := &Evaler{
state: state{
valuePrefix: defaultValuePrefix,
notifyBgJobSuccess: defaultNotifyBgJobSuccess,
numBgJobs: 0,
},
evalerScopes: evalerScopes{
Global: new(Ns),
Builtin: builtin,
},
modules: map[string]*Ns{
"builtin": builtin,
},
bundled: bundled.Get(),
deprecations: newDeprecationRegistry(),
}
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))
moreBuiltinsBuilder := make(NsBuilder)
moreBuiltinsBuilder["before-chdir"] = vars.FromPtr(&beforeChdirElvish)
moreBuiltinsBuilder["after-chdir"] = vars.FromPtr(&afterChdirElvish)
moreBuiltinsBuilder["value-out-indicator"] = vars.FromPtrWithMutex(
&ev.state.valuePrefix, &ev.state.mutex)
moreBuiltinsBuilder["notify-bg-job-success"] = vars.FromPtrWithMutex(
&ev.state.notifyBgJobSuccess, &ev.state.mutex)
moreBuiltinsBuilder["num-bg-jobs"] = vars.FromGet(func() interface{} {
return strconv.Itoa(ev.state.getNumBgJobs())
})
moreBuiltinsBuilder["pwd"] = NewPwdVar(ev)
moreBuiltins := moreBuiltinsBuilder.Ns()
builtin.slots = append(builtin.slots, moreBuiltins.slots...)
builtin.names = append(builtin.names, moreBuiltins.names...)
return ev
}
func adaptChdirHook(name string, ev *Evaler, pfns *vector.Vector) func(string) {
return func(path string) {
ports, cleanup := portsFromFiles(
[3]*os.File{os.Stdin, os.Stdout, os.Stderr}, ev.state.getValuePrefix())
defer cleanup()
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, parse.Source{Name: "[hook " + name + "]"}, ports[:])
2020-04-10 04:01:14 +08:00
err := fn.Call(fm, []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)
}
2018-01-03 08:56:19 +08:00
// InstallDaemonClient installs a daemon client to the Evaler.
2019-12-27 01:23:18 +08:00
func (ev *Evaler) InstallDaemonClient(client daemon.Client) {
2018-01-03 08:56:19 +08:00
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) {
2018-01-04 05:49:44 +08:00
ev.modules[name] = mod
}
2018-09-29 03:09:02 +08:00
// 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)
}
// TODO: Avoid creating the variable dynamically; instead, always create the
// variable, and set its value here.
ev.Builtin.slots = append(ev.Builtin.slots, vars.NewReadOnly(v))
ev.Builtin.names = append(ev.Builtin.names, "args")
}
2018-01-01 23:12:34 +08:00
// SetLibDir sets the library directory, in which external modules are to be
// found.
func (ev *Evaler) SetLibDir(libDir string) {
ev.libDir = libDir
}
// 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 {
2014-05-24 03:29:40 +08:00
return
}
ports := fm.ports
fm.ports = make([]*Port, n)
copy(fm.ports, ports)
2014-05-24 03:29:40 +08:00
}
// Eval evaluates a piece of source code with the given configuration. The
// returned error may be a parse error, compilation error or exception.
func (ev *Evaler) Eval(src parse.Source, cfg EvalCfg) error {
cfg.Ports = fillPorts(cfg.Ports)
op, err := ev.parseAndCompile(src, cfg.Ports[2].File)
if err != nil {
return err
}
return ev.execOp(op, cfg)
}
func fillPorts(ports []*Port) []*Port {
if len(ports) < 3 {
ports = append(ports, make([]*Port, 3-len(ports))...)
}
if ports[0] == nil {
ports[0] = DevNullClosedChan
}
if ports[1] == nil {
ports[1] = DevNullBlackholeChan
}
if ports[2] == nil {
ports[2] = DevNullBlackholeChan
}
return ports
}
// Executes an Op with the specified configuration.
func (ev *Evaler) execOp(op Op, cfg EvalCfg) error {
fm := NewTopFrame(ev, op.Src, cfg.Ports)
if cfg.Interrupt != nil {
intCh, cleanup := cfg.Interrupt()
defer cleanup()
fm.intCh = intCh
}
if cfg.PutInFg {
defer func() {
err := putSelfInFg()
if err != nil {
fmt.Fprintln(cfg.Ports[2].File,
"failed to put myself in foreground:", err)
}
}()
}
2020-04-10 04:01:14 +08:00
return op.Inner.exec(fm)
2018-03-01 10:50:27 +08:00
}
// Parses and compiles a Source. Aborts if parse fails.
func (ev *Evaler) parseAndCompile(src parse.Source, w io.Writer) (Op, error) {
tree, err := parse.ParseWithDeprecation(src, w)
if err != nil {
return Op{}, err
}
return ev.Compile(tree, w)
}
// Check checks the given source code and returns any parse error and
// compilation error in it. It always tries to compile the code even if there is
// a parse error; both return values may be non-nil. If the io.Writer argument
// is not nil, deprecation messages are written to it.
func (ev *Evaler) Check(src parse.Source, w io.Writer) (*parse.Error, *diag.Error) {
tree, parseErr := parse.ParseWithDeprecation(src, w)
_, compileErr := ev.Compile(tree, w)
return parse.GetError(parseErr), GetCompilationError(compileErr)
}
// 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(tree parse.Tree, w io.Writer) (Op, error) {
return ev.CompileWithGlobal(tree, ev.Global, w)
}
// 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(tree parse.Tree, g *Ns, w io.Writer) (Op, error) {
return compile(ev.Builtin.static(), g.static(), tree, w)
}