package eval // Builtin special forms. Special forms behave mostly like ordinary commands - // they are valid commands syntactically, and can take part in pipelines - but // they have special rules for the evaluation of their arguments and can affect // the compilation phase (whereas ordinary commands can only affect the // evaluation phase). // // For example, the "and" special form evaluates its arguments from left to // right, and stops as soon as one booleanly false value is obtained: the // command "and $false (fail haha)" does not produce an exception. // // As another example, the "del" special form removes a variable, affecting the // compiler. // // Flow control structures are also implemented as special forms in elvish, with // closures functioning as code blocks. import ( "fmt" "os" "path/filepath" "strings" "unicode/utf8" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/parse/cmpd" ) type compileBuiltin func(*compiler, *parse.Form) effectOp var builtinSpecials map[string]compileBuiltin // IsBuiltinSpecial is the set of all names of builtin special forms. It is // intended for external consumption, e.g. the syntax highlighter. var IsBuiltinSpecial = map[string]bool{} // NoSuchModule encodes an error where a module spec cannot be resolved. type NoSuchModule struct{ spec string } // Error implements the error interface. func (err NoSuchModule) Error() string { return "no such module: " + err.spec } func init() { // Needed to avoid initialization loop builtinSpecials = map[string]compileBuiltin{ "var": compileVar, "set": compileSet, "tmp": compileTmp, "del": compileDel, "fn": compileFn, "use": compileUse, "and": compileAnd, "or": compileOr, "coalesce": compileCoalesce, "if": compileIf, "while": compileWhile, "for": compileFor, "try": compileTry, "pragma": compilePragma, } for name := range builtinSpecials { IsBuiltinSpecial[name] = true } } // VarForm = 'var' { VariablePrimary } [ '=' { Compound } ] func compileVar(cp *compiler, fn *parse.Form) effectOp { lhsArgs, rhs := compileLHSRHS(cp, fn) lhs := cp.parseCompoundLValues(lhsArgs, newLValue) if rhs == nil { // Just create new variables, nothing extra to do at runtime. return nopOp{} } return &assignOp{fn.Range(), lhs, rhs, false} } // SetForm = 'set' { LHS } '=' { Compound } func compileSet(cp *compiler, fn *parse.Form) effectOp { lhs, rhs := compileSetArgs(cp, fn) return &assignOp{fn.Range(), lhs, rhs, false} } // TmpForm = 'tmp' { LHS } '=' { Compound } func compileTmp(cp *compiler, fn *parse.Form) effectOp { if len(cp.scopes) <= 1 { cp.errorpf(fn, "tmp may only be used inside a function") } lhs, rhs := compileSetArgs(cp, fn) return &assignOp{fn.Range(), lhs, rhs, true} } func compileSetArgs(cp *compiler, fn *parse.Form) (lvaluesGroup, valuesOp) { lhsArgs, rhs := compileLHSRHS(cp, fn) if rhs == nil { cp.errorpf(diag.PointRanging(fn.Range().To), "need = and right-hand-side") } lhs := cp.parseCompoundLValues(lhsArgs, setLValue) return lhs, rhs } func compileLHSRHS(cp *compiler, fn *parse.Form) ([]*parse.Compound, valuesOp) { for i, cn := range fn.Args { if parse.SourceText(cn) == "=" { lhs := fn.Args[:i] if i == len(fn.Args)-1 { return lhs, nopValuesOp{diag.PointRanging(fn.Range().To)} } return lhs, seqValuesOp{ diag.MixedRanging(fn.Args[i+1], fn.Args[len(fn.Args)-1]), cp.compoundOps(fn.Args[i+1:])} } } return fn.Args, nil } const delArgMsg = "arguments to del must be variable or variable elements" // DelForm = 'del' { LHS } func compileDel(cp *compiler, fn *parse.Form) effectOp { var ops []effectOp for _, cn := range fn.Args { if len(cn.Indexings) != 1 { cp.errorpf(cn, delArgMsg) continue } head, indices := cn.Indexings[0].Head, cn.Indexings[0].Indices if head.Type == parse.Variable { cp.errorpf(cn, "arguments to del must omit the dollar sign") continue } else if !parse.ValidLHSVariable(head, false) { cp.errorpf(cn, delArgMsg) continue } qname := head.Value var f effectOp ref := resolveVarRef(cp, qname, nil) if ref == nil { cp.errorpf(cn, "no variable $%s", head.Value) continue } if len(indices) == 0 { if ref.scope == envScope { f = delEnvVarOp{fn.Range(), ref.subNames[0]} } else if ref.scope == localScope && len(ref.subNames) == 0 { f = delLocalVarOp{ref.index} cp.thisScope().infos[ref.index].deleted = true } else { cp.errorpf(cn, "only variables in the local scope or E: can be deleted") continue } } else { f = newDelElementOp(ref, head.Range().From, head.Range().To, cp.arrayOps(indices)) } ops = append(ops, f) } return seqOp{ops} } type delLocalVarOp struct{ index int } func (op delLocalVarOp) exec(fm *Frame) Exception { fm.local.slots[op.index] = nil return nil } type delEnvVarOp struct { diag.Ranging name string } func (op delEnvVarOp) exec(fm *Frame) Exception { return fm.errorp(op, os.Unsetenv(op.name)) } func newDelElementOp(ref *varRef, begin, headEnd int, indexOps []valuesOp) effectOp { ends := make([]int, len(indexOps)+1) ends[0] = headEnd for i, op := range indexOps { ends[i+1] = op.Range().To } return &delElemOp{ref, indexOps, begin, ends} } type delElemOp struct { ref *varRef indexOps []valuesOp begin int ends []int } func (op *delElemOp) Range() diag.Ranging { return diag.Ranging{From: op.begin, To: op.ends[0]} } func (op *delElemOp) exec(fm *Frame) Exception { var indices []any for _, indexOp := range op.indexOps { indexValues, exc := indexOp.exec(fm) if exc != nil { return exc } if len(indexValues) != 1 { return fm.errorpf(indexOp, "index must evaluate to a single value in argument to del") } indices = append(indices, indexValues[0]) } err := vars.DelElement(deref(fm, op.ref), indices) if err != nil { if level := vars.ElementErrorLevel(err); level >= 0 { return fm.errorp(diag.Ranging{From: op.begin, To: op.ends[level]}, err) } return fm.errorp(op, err) } return nil } // FnForm = 'fn' StringPrimary LambdaPrimary // // fn f { foobar } is a shorthand for set '&'f = { foobar }. func compileFn(cp *compiler, fn *parse.Form) effectOp { args := getArgs(cp, fn) name := args.get(0, "name").stringLiteral() bodyNode := args.get(1, "function body").lambda() if !args.finish() { return nil } // Define the variable before compiling the body, so that the body may refer // to the function itself. index := cp.thisScope().add(name + FnSuffix) op := cp.lambda(bodyNode) return fnOp{fn.Args[0].Range(), index, op} } type fnOp struct { nameRange diag.Ranging varIndex int lambdaOp valuesOp } func (op fnOp) exec(fm *Frame) Exception { // Initialize the function variable with the builtin nop function. This step // allows the definition of recursive functions; the actual function will // never be called. fm.local.slots[op.varIndex].Set(NewGoFn("", nop)) values, exc := op.lambdaOp.exec(fm) if exc != nil { return exc } c := values[0].(*Closure) c.op = fnWrap{c.op} return fm.errorp(op.nameRange, fm.local.slots[op.varIndex].Set(c)) } type fnWrap struct{ effectOp } func (op fnWrap) Range() diag.Ranging { return op.effectOp.(diag.Ranger).Range() } func (op fnWrap) exec(fm *Frame) Exception { exc := op.effectOp.exec(fm) if exc != nil && exc.Reason() != Return { // rethrow return exc } return nil } // UseForm = 'use' StringPrimary func compileUse(cp *compiler, fn *parse.Form) effectOp { args := getArgs(cp, fn) spec := args.get(0, "module spec").stringLiteral() name := "" if args.has(1) { name = args.get(1, "module name").stringLiteral() } else { name = spec[strings.LastIndexByte(spec, '/')+1:] } if !args.finish() { return nil } return useOp{fn.Range(), cp.thisScope().add(name + NsSuffix), spec} } type useOp struct { diag.Ranging varIndex int spec string } func (op useOp) exec(fm *Frame) Exception { ns, err := use(fm, op.spec, op) if err != nil { return fm.errorp(op, err) } fm.local.slots[op.varIndex].Set(ns) return nil } // TODO: Add support for module specs relative to a package/workspace. // See https://github.com/elves/elvish/issues/1421. func use(fm *Frame, spec string, r diag.Ranger) (*Ns, error) { // Handle relative imports. Note that this deliberately does not support Windows backslash as a // path separator because module specs are meant to be platform independent. If necessary, we // translate a module spec to an appropriate path for the platform. if strings.HasPrefix(spec, "./") || strings.HasPrefix(spec, "../") { var dir string if fm.srcMeta.IsFile { dir = filepath.Dir(fm.srcMeta.Name) } else { var err error dir, err = os.Getwd() if err != nil { return nil, err } } path := filepath.Clean(dir + "/" + spec) return useFromFile(fm, spec, path, r) } // Handle imports of pre-defined modules like `builtin` and `str`. if ns, ok := fm.Evaler.modules[spec]; ok { return ns, nil } if code, ok := fm.Evaler.BundledModules[spec]; ok { return evalModule(fm, spec, parse.Source{Name: "[bundled " + spec + "]", Code: code}, r) } // Handle imports relative to the Elvish module search directories. // // TODO: For non-relative imports, use the spec (instead of the full path) // as the module key instead to avoid searching every time. for _, dir := range fm.Evaler.LibDirs { ns, err := useFromFile(fm, spec, filepath.Join(dir, spec), r) if _, isNoSuchModule := err.(NoSuchModule); isNoSuchModule { continue } return ns, err } // Sadly, we couldn't resolve the module spec. return nil, NoSuchModule{spec} } // TODO: Make access to fm.Evaler.modules concurrency-safe. func useFromFile(fm *Frame, spec, path string, r diag.Ranger) (*Ns, error) { if ns, ok := fm.Evaler.modules[path]; ok { return ns, nil } _, err := os.Stat(path + ".so") if err != nil { code, err := readFileUTF8(path + ".elv") if err != nil { if os.IsNotExist(err) { return nil, NoSuchModule{spec} } return nil, err } src := parse.Source{Name: path + ".elv", Code: code, IsFile: true} return evalModule(fm, path, src, r) } plug, err := pluginOpen(path + ".so") if err != nil { return nil, NoSuchModule{spec} } sym, err := plug.Lookup("Ns") if err != nil { return nil, err } ns, ok := sym.(**Ns) if !ok { return nil, NoSuchModule{spec} } fm.Evaler.modules[path] = *ns return *ns, nil } func readFileUTF8(fname string) (string, error) { bytes, err := os.ReadFile(fname) if err != nil { return "", err } if !utf8.Valid(bytes) { return "", fmt.Errorf("%s: source is not valid UTF-8", fname) } return string(bytes), nil } // TODO: Make access to fm.Evaler.modules concurrency-safe. func evalModule(fm *Frame, key string, src parse.Source, r diag.Ranger) (*Ns, error) { ns, exec, err := fm.PrepareEval(src, r, new(Ns)) if err != nil { return nil, err } // Installs the namespace before executing. This prevent circular use'es // from resulting in an infinite recursion. fm.Evaler.modules[key] = ns err = exec() if err != nil { // Unload the namespace. delete(fm.Evaler.modules, key) return nil, err } return ns, nil } // compileAnd compiles the "and" special form. // // The and special form evaluates arguments until a false-ish values is found // and outputs it; the remaining arguments are not evaluated. If there are no // false-ish values, the last value is output. If there are no arguments, it // outputs $true, as if there is a hidden $true before actual arguments. func compileAnd(cp *compiler, fn *parse.Form) effectOp { return &andOrOp{fn.Range(), cp.compoundOps(fn.Args), true, false} } // compileOr compiles the "or" special form. // // The or special form evaluates arguments until a true-ish values is found and // outputs it; the remaining arguments are not evaluated. If there are no // true-ish values, the last value is output. If there are no arguments, it // outputs $false, as if there is a hidden $false before actual arguments. func compileOr(cp *compiler, fn *parse.Form) effectOp { return &andOrOp{fn.Range(), cp.compoundOps(fn.Args), false, true} } type andOrOp struct { diag.Ranging argOps []valuesOp init bool stopAt bool } func (op *andOrOp) exec(fm *Frame) Exception { var lastValue any = vals.Bool(op.init) out := fm.ValueOutput() for _, argOp := range op.argOps { values, exc := argOp.exec(fm) if exc != nil { return exc } for _, value := range values { if vals.Bool(value) == op.stopAt { return fm.errorp(op, out.Put(value)) } lastValue = value } } return fm.errorp(op, out.Put(lastValue)) } // Compiles the "coalesce" special form, which is like "or", but evaluates until // a non-nil value is found. func compileCoalesce(cp *compiler, fn *parse.Form) effectOp { return &coalesceOp{fn.Range(), cp.compoundOps(fn.Args)} } type coalesceOp struct { diag.Ranging argOps []valuesOp } func (op *coalesceOp) exec(fm *Frame) Exception { out := fm.ValueOutput() for _, argOp := range op.argOps { values, exc := argOp.exec(fm) if exc != nil { return exc } for _, value := range values { if value != nil { return fm.errorp(op, out.Put(value)) } } } return fm.errorp(op, out.Put(nil)) } func compileIf(cp *compiler, fn *parse.Form) effectOp { args := getArgs(cp, fn) var condNodes []*parse.Compound var bodyNodes []*parse.Primary i := 0 bodyName := "if body" for { condNodes = append(condNodes, args.get(i, "condition").any()) bodyNodes = append(bodyNodes, args.get(i+1, bodyName).thunk()) i += 2 if !args.hasKeyword(i, "elif") { break } i++ bodyName = "elif body" } elseBody := args.optionalKeywordBody(i, "else") if !args.finish() { return nil } condOps := cp.compoundOps(condNodes) bodyOps := cp.primaryOps(bodyNodes) var elseOp valuesOp if elseBody != nil { elseOp = cp.primaryOp(elseBody) } return &ifOp{fn.Range(), condOps, bodyOps, elseOp} } type ifOp struct { diag.Ranging condOps []valuesOp bodyOps []valuesOp elseOp valuesOp } func (op *ifOp) exec(fm *Frame) Exception { bodies := make([]Callable, len(op.bodyOps)) for i, bodyOp := range op.bodyOps { bodies[i] = execLambdaOp(fm, bodyOp) } elseFn := execLambdaOp(fm, op.elseOp) for i, condOp := range op.condOps { condValues, exc := condOp.exec(fm.Fork("if cond")) if exc != nil { return exc } if allTrue(condValues) { return fm.errorp(op, bodies[i].Call(fm.Fork("if body"), NoArgs, NoOpts)) } } if op.elseOp != nil { return fm.errorp(op, elseFn.Call(fm.Fork("if else"), NoArgs, NoOpts)) } return nil } func compileWhile(cp *compiler, fn *parse.Form) effectOp { args := getArgs(cp, fn) condNode := args.get(0, "condition").any() bodyNode := args.get(1, "while body").thunk() elseNode := args.optionalKeywordBody(2, "else") if !args.finish() { return nil } condOp := cp.compoundOp(condNode) bodyOp := cp.primaryOp(bodyNode) var elseOp valuesOp if elseNode != nil { elseOp = cp.primaryOp(elseNode) } return &whileOp{fn.Range(), condOp, bodyOp, elseOp} } type whileOp struct { diag.Ranging condOp, bodyOp, elseOp valuesOp } func (op *whileOp) exec(fm *Frame) Exception { body := execLambdaOp(fm, op.bodyOp) elseBody := execLambdaOp(fm, op.elseOp) iterated := false for { condValues, exc := op.condOp.exec(fm.Fork("while cond")) if exc != nil { return exc } if !allTrue(condValues) { break } iterated = true err := body.Call(fm.Fork("while"), NoArgs, NoOpts) if err != nil { exc := err.(Exception) if exc.Reason() == Continue { // Do nothing } else if exc.Reason() == Break { break } else { return exc } } } if op.elseOp != nil && !iterated { return fm.errorp(op, elseBody.Call(fm.Fork("while else"), NoArgs, NoOpts)) } return nil } func compileFor(cp *compiler, fn *parse.Form) effectOp { args := getArgs(cp, fn) varNode := args.get(0, "variable").any() iterNode := args.get(1, "iterable").any() bodyNode := args.get(2, "for body").thunk() elseNode := args.optionalKeywordBody(3, "else") if !args.finish() { return nil } lvalue := cp.compileOneLValue(varNode, setLValue|newLValue) iterOp := cp.compoundOp(iterNode) bodyOp := cp.primaryOp(bodyNode) var elseOp valuesOp if elseNode != nil { elseOp = cp.primaryOp(elseNode) } return &forOp{fn.Range(), lvalue, iterOp, bodyOp, elseOp} } type forOp struct { diag.Ranging lvalue lvalue iterOp valuesOp bodyOp valuesOp elseOp valuesOp } func (op *forOp) exec(fm *Frame) Exception { variable, err := derefLValue(fm, op.lvalue) if err != nil { return fm.errorp(op.lvalue, err) } iterable, err := evalForValue(fm, op.iterOp, "value being iterated") if err != nil { return fm.errorp(op, err) } body := execLambdaOp(fm, op.bodyOp) elseBody := execLambdaOp(fm, op.elseOp) iterated := false var errElement error errIterate := vals.Iterate(iterable, func(v any) bool { iterated = true err := variable.Set(v) if err != nil { errElement = err return false } err = body.Call(fm.Fork("for"), NoArgs, NoOpts) if err != nil { exc := err.(Exception) if exc.Reason() == Continue { // do nothing } else if exc.Reason() == Break { return false } else { errElement = err return false } } return true }) if errIterate != nil { return fm.errorp(op, errIterate) } if errElement != nil { return fm.errorp(op, errElement) } if !iterated && elseBody != nil { return fm.errorp(op, elseBody.Call(fm.Fork("for else"), NoArgs, NoOpts)) } return nil } func compileTry(cp *compiler, fn *parse.Form) effectOp { args := getArgs(cp, fn) bodyNode := args.get(0, "try body").thunk() i := 1 var catchVarNode *parse.Compound var catchNode *parse.Primary if args.hasKeyword(i, "catch") { i++ // Parse an optional lvalue into exceptVarNode. n := args.get(i, "variable or body").any() if _, ok := cmpd.StringLiteral(n); ok { catchVarNode = n i++ } catchNode = args.get(i, "catch body").thunk() i++ } elseNode := args.optionalKeywordBody(i, "else") if elseNode != nil { i += 2 } finallyNode := args.optionalKeywordBody(i, "finally") if !args.finish() { return nil } if catchNode == nil && finallyNode == nil { cp.errorpf(fn, "try must be followed by a catch block or a finally block") } var catchVar lvalue var bodyOp, catchOp, elseOp, finallyOp valuesOp bodyOp = cp.primaryOp(bodyNode) if catchVarNode != nil { catchVar = cp.compileOneLValue(catchVarNode, setLValue|newLValue) } if catchNode != nil { catchOp = cp.primaryOp(catchNode) } if elseNode != nil { elseOp = cp.primaryOp(elseNode) } if finallyNode != nil { finallyOp = cp.primaryOp(finallyNode) } return &tryOp{fn.Range(), bodyOp, catchVar, catchOp, elseOp, finallyOp} } type tryOp struct { diag.Ranging bodyOp valuesOp catchVar lvalue catchOp valuesOp elseOp valuesOp finallyOp valuesOp } func (op *tryOp) exec(fm *Frame) Exception { body := execLambdaOp(fm, op.bodyOp) var exceptVar vars.Var if op.catchVar.ref != nil { var err error exceptVar, err = derefLValue(fm, op.catchVar) if err != nil { return fm.errorp(op, err) } } catch := execLambdaOp(fm, op.catchOp) elseFn := execLambdaOp(fm, op.elseOp) finally := execLambdaOp(fm, op.finallyOp) err := body.Call(fm.Fork("try body"), NoArgs, NoOpts) if err != nil { if catch != nil { if exceptVar != nil { err := exceptVar.Set(err.(Exception)) if err != nil { return fm.errorp(op.catchVar, err) } } err = catch.Call(fm.Fork("try catch"), NoArgs, NoOpts) } } else { if elseFn != nil { err = elseFn.Call(fm.Fork("try else"), NoArgs, NoOpts) } } if finally != nil { errFinally := finally.Call(fm.Fork("try finally"), NoArgs, NoOpts) if errFinally != nil { // TODO: If err is not nil, this discards err. Use something similar // to pipeline exception to expose both. return fm.errorp(op, errFinally) } } return fm.errorp(op, err) } // PragmaForm = 'pragma' 'fallback-resolver' '=' { Compound } func compilePragma(cp *compiler, fn *parse.Form) effectOp { args := getArgs(cp, fn) name := args.get(0, "pragma name").stringLiteral() eq := args.get(1, "literal =").stringLiteral() if args.has(1) && eq != "=" { args.errorpf(fn.Args[1], "must be literal =") } valueNode := args.get(2, "pragma value").any() if !args.finish() { return nil } switch name { case "unknown-command": value := stringLiteralOrError(cp, valueNode, "value for unknown-command") switch value { case "disallow": cp.currentPragma().unknownCommandIsExternal = false case "external": cp.currentPragma().unknownCommandIsExternal = true default: cp.errorpf(valueNode, "invalid value for unknown-command: %s", parse.Quote(value)) } default: cp.errorpf(fn.Args[0], "unknown pragma %s", parse.Quote(name)) } return nopOp{} } func (cp *compiler) compileOneLValue(n *parse.Compound, f lvalueFlag) lvalue { if len(n.Indexings) != 1 { cp.errorpf(n, "must be valid lvalue") } lvalues := cp.parseIndexingLValue(n.Indexings[0], f) if lvalues.rest != -1 { cp.errorpf(lvalues.lvalues[lvalues.rest], "rest variable not allowed") } if len(lvalues.lvalues) != 1 { cp.errorpf(n, "must be exactly one lvalue") } return lvalues.lvalues[0] } // Executes a valuesOp that is known to yield a lambda and returns the lambda. // Returns nil if op is nil. func execLambdaOp(fm *Frame, op valuesOp) Callable { if op == nil { return nil } values, exc := op.exec(fm) if exc != nil { panic("must not be erroneous") } return values[0].(Callable) }