elvish/pkg/eval/compile_lvalue.go
Qi Xiao 8c71635ca3 pkg/eval: Move all assignments to the start of the pipeline they belong in.
This avoids race conditions of accessing the local scope. The test case
"x = 1", "put $x | y = (all)" used to contain a race condition but no longer
does.

This addresses #73.
2020-03-28 23:49:30 +00:00

176 lines
4.6 KiB
Go

package eval
import (
"errors"
"fmt"
"github.com/elves/elvish/pkg/diag"
"github.com/elves/elvish/pkg/eval/vars"
"github.com/elves/elvish/pkg/parse"
)
// lvaluesOp compiles lvalues, returning the fixed part and, optionally a rest
// part.
//
// In the AST an lvalue is either an Indexing node where the head is a string
// literal, or a braced list of such Indexing nodes. The last Indexing node may
// be prefixed by @, in which case they become the rest part. For instance, in
// {a[x],b,@c[z]}, "a[x],b" is the fixed part and "c[z]" is the rest part.
func (cp *compiler) lvaluesOp(n *parse.Indexing) (lvaluesOp, lvaluesOp) {
if n.Head.Type == parse.Braced {
// Braced list of variable specs, possibly with indicies.
if len(n.Indicies) > 0 {
cp.errorpf(n, "may not have indicies")
}
return cp.lvaluesMulti(n.Head.Braced)
}
rest, opFunc := cp.lvalueBase(n, "must be an lvalue or a braced list of those")
op := lvaluesOp{opFunc, n.Range()}
if rest {
return lvaluesOp{}, op
}
return op, lvaluesOp{}
}
func (cp *compiler) lvaluesMulti(nodes []*parse.Compound) (lvaluesOp, lvaluesOp) {
opFuncs := make([]lvaluesOpBody, len(nodes))
var restNode *parse.Indexing
var restOpFunc lvaluesOpBody
// Compile each spec inside the brace.
fixedEnd := 0
for i, cn := range nodes {
if len(cn.Indexings) != 1 {
cp.errorpf(cn, "must be an lvalue")
}
var rest bool
rest, opFuncs[i] = cp.lvalueBase(cn.Indexings[0], "must be an lvalue ")
// Only the last one may a rest part.
if rest {
if i == len(nodes)-1 {
restNode = cn.Indexings[0]
restOpFunc = opFuncs[i]
} else {
cp.errorpf(cn, "only the last lvalue may have @")
}
} else {
fixedEnd = cn.Range().To
}
}
var restOp lvaluesOp
// If there is a rest part, make LValuesOp for it and remove it from opFuncs.
if restOpFunc != nil {
restOp = lvaluesOp{restOpFunc, restNode.Range()}
opFuncs = opFuncs[:len(opFuncs)-1]
}
var op lvaluesOp
// If there is still anything left in opFuncs, make LValuesOp for the fixed part.
if len(opFuncs) > 0 {
op = lvaluesOp{seqLValuesOpBody{opFuncs}, diag.Ranging{From: nodes[0].Range().From, To: fixedEnd}}
}
return op, restOp
}
func (cp *compiler) lvalueBase(n *parse.Indexing, msg string) (bool, lvaluesOpBody) {
ref := cp.literal(n.Head, msg)
sigil, qname := SplitVariableRef(ref)
// TODO: Deal with other sigils too
explode := sigil != ""
if len(n.Indicies) == 0 {
cp.registerVariableSet(qname)
return explode, varOp{qname}
}
return explode, cp.lvalueElement(qname, n)
}
func (cp *compiler) lvalueElement(qname string, n *parse.Indexing) lvaluesOpBody {
if !cp.registerVariableGet(qname) {
cp.errorpf(n, "variable $%s not found", qname)
}
begin, end := n.Range().From, n.Range().To
ends := make([]int, len(n.Indicies)+1)
ends[0] = n.Head.Range().To
for i, idx := range n.Indicies {
ends[i+1] = idx.Range().To
}
indexOps := cp.arrayOps(n.Indicies)
return &elemOp{qname, indexOps, begin, end, ends}
}
type seqLValuesOpBody struct {
ops []lvaluesOpBody
}
func (op seqLValuesOpBody) invoke(fm *Frame) ([]vars.Var, error) {
var variables []vars.Var
for _, op := range op.ops {
moreVariables, err := op.invoke(fm)
if err != nil {
return nil, err
}
variables = append(variables, moreVariables...)
}
return variables, nil
}
type varOp struct {
qname string
}
func (op varOp) invoke(fm *Frame) ([]vars.Var, error) {
variable := fm.ResolveVar(op.qname)
if variable == nil {
ns, _ := SplitQNameNs(op.qname)
if ns == "" || ns == ":" || ns == "local:" {
// This should have been created as part of pipelineOp; a compiler bug.
return nil, errors.New("compiler bug: new local variable not created in pipeline")
} else {
return nil, fmt.Errorf("new variables can only be created in local scope")
}
}
return []vars.Var{variable}, nil
}
type elemOp struct {
qname string
indexOps []valuesOp
begin int
end int
ends []int
}
func (op *elemOp) invoke(fm *Frame) ([]vars.Var, error) {
variable := fm.ResolveVar(op.qname)
if variable == nil {
return nil, fmt.Errorf("variable $%s does not exist, compiler bug", op.qname)
}
indicies := make([]interface{}, len(op.indexOps))
for i, op := range op.indexOps {
values, err := op.exec(fm)
if err != nil {
return nil, err
}
// TODO: Implement multi-indexing.
if len(values) != 1 {
return nil, errors.New("multi indexing not implemented")
}
indicies[i] = values[0]
}
elemVar, err := vars.MakeElement(variable, indicies)
if err != nil {
level := vars.ElementErrorLevel(err)
if level < 0 {
return nil, fm.errorpf(op.begin, op.end, "%s", err)
}
return nil, fm.errorpf(op.begin, op.ends[level], "%s", err)
}
return []vars.Var{elemVar}, nil
}