Improve handling of namespaces.

* The training colons of namespaces are now considered part of the namespace,
  simplifying the internal API.

* Added tests for more complex patterns of nested namespaces that used to fail.
This commit is contained in:
Qi Xiao 2019-10-22 22:49:32 +01:00
parent 5b93b08206
commit 93c17dc7c2
19 changed files with 375 additions and 318 deletions

View File

@ -3,7 +3,6 @@ package cliedit
import (
"os"
"os/exec"
"strings"
"github.com/elves/elvish/eval"
"github.com/elves/elvish/parse"
@ -32,23 +31,24 @@ func hasCommand(ev *eval.Evaler, cmd string) bool {
return isDirOrExecutable(cmd) || hasExternalCommand(cmd)
}
explode, ns, name := eval.ParseVariableRef(cmd)
if explode {
sigil, qname := eval.SplitVariableRef(cmd)
if sigil != "" {
// The @ sign is only valid when referring to external commands.
return hasExternalCommand(cmd)
}
switch ns {
case "e":
return hasExternalCommand(name)
firstNs, rest := eval.SplitIncompleteQNameFirstNs(qname)
switch firstNs {
case "e:":
return hasExternalCommand(rest)
case "":
// Unqualified name; try builtin and global.
if hasFn(ev.Builtin, name) || hasFn(ev.Global, name) {
if hasFn(ev.Builtin, rest) || hasFn(ev.Global, rest) {
return true
}
default:
// Qualified name. Find the top-level module first.
if hasQualifiedFn(ev, strings.Split(ns, ":"), name) {
if hasQualifiedFn(ev, firstNs, rest) {
return true
}
}
@ -57,10 +57,10 @@ func hasCommand(ev *eval.Evaler, cmd string) bool {
return hasExternalCommand(cmd)
}
func hasQualifiedFn(ev *eval.Evaler, nsParts []string, name string) bool {
modVar := ev.Global[nsParts[0]+eval.NsSuffix]
func hasQualifiedFn(ev *eval.Evaler, firstNs string, rest string) bool {
modVar := ev.Global[firstNs]
if modVar == nil {
modVar = ev.Builtin[nsParts[0]+eval.NsSuffix]
modVar = ev.Builtin[firstNs]
if modVar == nil {
return false
}
@ -69,8 +69,9 @@ func hasQualifiedFn(ev *eval.Evaler, nsParts []string, name string) bool {
if !ok {
return false
}
for _, nsPart := range nsParts[1:] {
modVar = mod[nsPart+eval.NsSuffix]
segs := eval.SplitQNameNsSegs(rest)
for _, seg := range segs[:len(segs)-1] {
modVar = mod[seg]
if modVar == nil {
return false
}
@ -79,7 +80,7 @@ func hasQualifiedFn(ev *eval.Evaler, nsParts []string, name string) bool {
return false
}
}
return hasFn(mod, name)
return hasFn(mod, segs[len(segs)-1])
}
func hasFn(ns eval.Ns, name string) bool {

View File

@ -72,17 +72,18 @@ func complFormHeadInner(head string, ev *eval.Evaler, rawCands chan<- rawCandida
for special := range eval.IsBuiltinSpecial {
got(special)
}
explode, ns, _ := eval.ParseIncompleteVariableRef(head)
if !explode {
sigil, qname := eval.SplitVariableRef(head)
ns, _ := eval.SplitQNameNsIncomplete(qname)
if sigil == "" {
logger.Printf("completing commands in ns %q", ns)
ev.EachVariableInTop(ns, func(varname string) {
switch {
case strings.HasSuffix(varname, eval.FnSuffix):
got(eval.MakeVariableRef(false, ns, varname[:len(varname)-len(eval.FnSuffix)]))
got(ns + varname[:len(varname)-len(eval.FnSuffix)])
case strings.HasSuffix(varname, eval.NsSuffix):
got(eval.MakeVariableRef(false, ns, varname))
got(ns + varname)
default:
name := eval.MakeVariableRef(false, ns, varname)
name := ns + varname
rawCands <- &complexCandidate{name, " = ", " = ", ui.Styles{}}
}
})

View File

@ -9,7 +9,7 @@ import (
type variableComplContext struct {
complContextCommon
ns, nsPart string
ns string
}
func (*variableComplContext) name() string { return "variable" }
@ -17,16 +17,14 @@ func (*variableComplContext) name() string { return "variable" }
func findVariableComplContext(n parse.Node, _ pureEvaler) complContext {
primary, ok := n.(*parse.Primary)
if ok && primary.Type == parse.Variable {
explode, nsPart, nameSeed := eval.SplitIncompleteVariableRef(primary.Value)
sigil, qname := eval.SplitVariableRef(primary.Value)
ns, nameSeed := eval.SplitQNameNsIncomplete(qname)
// Move past "$", "@" and "<ns>:".
begin := primary.Range().From + 1 + len(explode) + len(nsPart)
ns := nsPart
if len(ns) > 0 {
ns = ns[:len(ns)-1]
}
begin := primary.Range().From + 1 + len(sigil) + len(ns)
return &variableComplContext{
complContextCommon{nameSeed, parse.Bareword, begin, primary.Range().To},
ns, nsPart,
ns,
}
}
return nil
@ -38,21 +36,20 @@ type evalerScopes interface {
}
func (ctx *variableComplContext) generate(env *complEnv, ch chan<- rawCandidate) error {
complVariable(ctx.ns, ctx.nsPart, env.evaler, ch)
complVariable(ctx.ns, env.evaler, ch)
return nil
}
func complVariable(ctxNs, ctxNsPart string, ev evalerScopes, ch chan<- rawCandidate) {
func complVariable(ctxNs string, ev evalerScopes, ch chan<- rawCandidate) {
ev.EachVariableInTop(ctxNs, func(varname string) {
ch <- noQuoteCandidate(varname)
})
ev.EachNsInTop(func(ns string) {
nsPart := ns + ":"
// This is to match namespaces that are "nested" under the current
// namespace.
if hasProperPrefix(nsPart, ctxNsPart) {
ch <- noQuoteCandidate(nsPart[len(ctxNsPart):])
if hasProperPrefix(ns, ctxNs) {
ch <- noQuoteCandidate(ns[len(ctxNs):])
}
})
}

View File

@ -11,13 +11,13 @@ import (
func TestFindVariableComplContext(t *testing.T) {
testComplContextFinder(t, "findVariableComplContext", findVariableComplContext, []complContextFinderTest{
{"$", &variableComplContext{
complContextCommon{"", parse.Bareword, 1, 1}, "", ""}},
complContextCommon{"", parse.Bareword, 1, 1}, ""}},
{"$a", &variableComplContext{
complContextCommon{"a", parse.Bareword, 1, 2}, "", ""}},
complContextCommon{"a", parse.Bareword, 1, 2}, ""}},
{"$a:", &variableComplContext{
complContextCommon{"", parse.Bareword, 3, 3}, "a", "a:"}},
complContextCommon{"", parse.Bareword, 3, 3}, "a:"}},
{"$a:b", &variableComplContext{
complContextCommon{"b", parse.Bareword, 3, 4}, "a", "a:"}},
complContextCommon{"b", parse.Bareword, 3, 4}, "a:"}},
// Wrong contexts
{"", nil},
{"echo", nil},
@ -27,9 +27,9 @@ func TestFindVariableComplContext(t *testing.T) {
type testEvalerScopes struct{}
var testScopes = map[string]map[string]int{
"": {"veni": 0, "vidi": 0, "vici": 0},
"foo": {"lorem": 0, "ipsum": 0},
"foo:bar": {"lorem": 0, "dolor": 0},
"": {"veni": 0, "vidi": 0, "vici": 0},
"foo:": {"lorem": 0, "ipsum": 0},
"foo:bar:": {"lorem": 0, "dolor": 0},
}
func (testEvalerScopes) EachNsInTop(f func(string)) {
@ -47,38 +47,37 @@ func (testEvalerScopes) EachVariableInTop(ns string, f func(string)) {
}
var complVariableTests = []struct {
ns string
nsPart string
want []rawCandidate
ns string
want []rawCandidate
}{
// No namespace: complete variables and namespaces
{"", "", []rawCandidate{
{"", []rawCandidate{
noQuoteCandidate("foo:"), noQuoteCandidate("foo:bar:"),
noQuoteCandidate("veni"), noQuoteCandidate("vici"), noQuoteCandidate("vidi"),
}},
// Nonempty namespace: complete variables in namespace and subnamespaces
// (but not variables in subnamespaces)
{"foo", "foo:", []rawCandidate{
{"foo:", []rawCandidate{
noQuoteCandidate("bar:"),
noQuoteCandidate("ipsum"), noQuoteCandidate("lorem"),
}},
// Bad namespace
{"bad", "bad:", nil},
{"bad:", nil},
}
func TestComplVariable(t *testing.T) {
for _, test := range complVariableTests {
got := collectComplVariable(test.ns, test.nsPart, testEvalerScopes{})
got := collectComplVariable(test.ns, testEvalerScopes{})
if !reflect.DeepEqual(got, test.want) {
t.Errorf("complVariable(%q, %q, ...) => %v, want %v", test.ns, test.nsPart, got, test.want)
t.Errorf("complVariable(%q, ...) => %v, want %v", test.ns, got, test.want)
}
}
}
func collectComplVariable(ns, nsPart string, ev evalerScopes) []rawCandidate {
func collectComplVariable(ns string, ev evalerScopes) []rawCandidate {
ch := make(chan rawCandidate)
go func() {
complVariable(ns, nsPart, ev, ch)
complVariable(ns, ev, ch)
close(ch)
}()
var results []rawCandidate

View File

@ -25,10 +25,11 @@ func goodFormHead(head string, ed *editor) bool {
return util.IsExecutable(head) || isDir(head)
} else {
ev := ed.evaler
explode, ns, name := eval.ParseVariableRef(head)
if !explode {
sigil, qname := eval.SplitVariableRef(head)
ns, name := eval.SplitIncompleteQNameFirstNs(qname)
if sigil == "" {
switch ns {
case "":
case "", ":":
if ev.Builtin[name+eval.FnSuffix] != nil || ev.Global[name+eval.FnSuffix] != nil {
return true
}

View File

@ -70,9 +70,9 @@ func resolve(fm *Frame, head string) string {
if special {
return "special"
}
explode, ns, name := ParseVariableRef(head)
if !explode && fm.ResolveVar(ns, name+FnSuffix) != nil {
return "$" + head + FnSuffix
sigil, qname := SplitVariableRef(head)
if sigil == "" && fm.ResolveVar(qname+FnSuffix) != nil {
return "$" + qname + FnSuffix
}
return "(external " + parse.Quote(head) + ")"
}

View File

@ -84,33 +84,34 @@ func compileDel(cp *compiler, fn *parse.Form) effectOpBody {
continue
}
explode, ns, name := ParseVariableRef(head.Value)
if explode {
cp.errorf("arguments to del may be have a leading @")
sigil, qname := SplitVariableRef(head.Value)
if sigil != "" {
cp.errorf("arguments to del may not have a sigils, got %q", sigil)
continue
}
var f effectOpBody
if len(indicies) == 0 {
ns, name := SplitQNameNsFirst(qname)
switch ns {
case "", "local":
case "", ":", "local:":
if !cp.thisScope().has(name) {
cp.errorf("no variable $%s in local scope", name)
continue
}
cp.thisScope().del(name)
f = delLocalVarOp{name}
case "E":
case "E:":
f = delEnvVarOp{name}
default:
cp.errorf("only variables in local: or E: can be deleted")
continue
}
} else {
if !cp.registerVariableGet(ns, name) {
if !cp.registerVariableGet(qname) {
cp.errorf("no variable $%s", head.Value)
continue
}
f = newDelElementOp(ns, name, head.Range().From, head.Range().To, cp.arrayOps(indicies))
f = newDelElementOp(qname, head.Range().From, head.Range().To, cp.arrayOps(indicies))
}
ops = append(ops, effectOp{f, cn.Range().From, cn.Range().To})
}
@ -130,18 +131,17 @@ func (op delEnvVarOp) invoke(*Frame) error {
return os.Unsetenv(op.name)
}
func newDelElementOp(ns, name string, begin, headEnd int, indexOps []valuesOp) effectOpBody {
func newDelElementOp(qname string, begin, headEnd int, indexOps []valuesOp) effectOpBody {
ends := make([]int, len(indexOps)+1)
ends[0] = headEnd
for i, op := range indexOps {
ends[i+1] = op.end
}
return &delElemOp{ns, name, indexOps, begin, ends}
return &delElemOp{qname, indexOps, begin, ends}
}
type delElemOp struct {
ns string
name string
qname string
indexOps []valuesOp
begin int
ends []int
@ -159,7 +159,7 @@ func (op *delElemOp) invoke(fm *Frame) error {
}
indicies = append(indicies, indexValues[0])
}
err := vars.DelElement(fm.ResolveVar(op.ns, op.name), indicies)
err := vars.DelElement(fm.ResolveVar(op.qname), indicies)
if err != nil {
if level := vars.ElementErrorLevel(err); level >= 0 {
return fm.errorpf(op.begin, op.ends[level], "%s", err.Error())
@ -179,7 +179,7 @@ func compileFn(cp *compiler, fn *parse.Form) effectOpBody {
bodyNode := args.nextMustLambda()
args.mustEnd()
cp.registerVariableSetQname(":" + varName)
cp.registerVariableSet(":" + varName)
op := cp.lambda(bodyNode)
return fnOp{varName, op}

View File

@ -87,6 +87,8 @@ func TestUse(t *testing.T) {
// That(`{ use lorem }; put $lorem:name`).ErrorsAny(),
// use of imported variable is captured in upvalue
That(`use lorem; { put $lorem:name }`).Puts("lorem"),
That(`{ use lorem; { put $lorem:name } }`).Puts("lorem"),
That(`({ use lorem; put { { put $lorem:name } } })`).Puts("lorem"),
// use of imported function is also captured in upvalue
That(`{ use lorem; { lorem:put-name } }`).Puts("lorem"),

View File

@ -175,10 +175,10 @@ func (cp *compiler) form(n *parse.Form) effectOpBody {
specialOpFunc = compileForm(cp, n)
} else {
var headOpFunc valuesOpBody
explode, ns, name := ParseVariableRef(headStr)
if !explode && cp.registerVariableGet(ns, name+FnSuffix) {
sigil, qname := SplitVariableRef(headStr)
if sigil == "" && cp.registerVariableGet(qname+FnSuffix) {
// $head~ resolves.
headOpFunc = variableOp{false, ns, name + FnSuffix}
headOpFunc = variableOp{false, qname + FnSuffix}
} else {
// Fall back to $e:head~.
headOpFunc = literalValues(ExternalCmd{headStr})

View File

@ -75,17 +75,19 @@ func (cp *compiler) lvaluesMulti(nodes []*parse.Compound) (lvaluesOp, lvaluesOp)
}
func (cp *compiler) lvalueBase(n *parse.Indexing, msg string) (bool, lvaluesOpBody) {
qname := cp.literal(n.Head, msg)
explode, ns, name := ParseVariableRef(qname)
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(ns, name)
return explode, varOp{ns, name}
cp.registerVariableSet(qname)
return explode, varOp{qname}
}
return explode, cp.lvalueElement(ns, name, n)
return explode, cp.lvalueElement(qname, n)
}
func (cp *compiler) lvalueElement(ns, name string, n *parse.Indexing) lvaluesOpBody {
cp.registerVariableGet(ns, name)
func (cp *compiler) lvalueElement(qname string, n *parse.Indexing) lvaluesOpBody {
cp.registerVariableGet(qname)
begin, end := n.Range().From, n.Range().To
ends := make([]int, len(n.Indicies)+1)
@ -96,7 +98,7 @@ func (cp *compiler) lvalueElement(ns, name string, n *parse.Indexing) lvaluesOpB
indexOps := cp.arrayOps(n.Indicies)
return &elemOp{ns, name, indexOps, begin, end, ends}
return &elemOp{qname, indexOps, begin, end, ends}
}
type seqLValuesOpBody struct {
@ -116,26 +118,27 @@ func (op seqLValuesOpBody) invoke(fm *Frame) ([]vars.Var, error) {
}
type varOp struct {
ns, name string
qname string
}
func (op varOp) invoke(fm *Frame) ([]vars.Var, error) {
variable := fm.ResolveVar(op.ns, op.name)
variable := fm.ResolveVar(op.qname)
if variable == nil {
if op.ns == "" || op.ns == "local" {
ns, name := SplitQNameNs(op.qname)
if ns == "" || ns == ":" || ns == "local:" {
// New variable.
// XXX We depend on the fact that this variable will
// immeidately be set.
if strings.HasSuffix(op.name, FnSuffix) {
if strings.HasSuffix(name, FnSuffix) {
val := Callable(nil)
variable = vars.FromPtr(&val)
} else if strings.HasSuffix(op.name, NsSuffix) {
} else if strings.HasSuffix(name, NsSuffix) {
val := Ns(nil)
variable = vars.FromPtr(&val)
} else {
variable = vars.FromInit(nil)
}
fm.local[op.name] = variable
fm.local[name] = variable
} else {
return nil, fmt.Errorf("new variables can only be created in local scope")
}
@ -144,8 +147,7 @@ func (op varOp) invoke(fm *Frame) ([]vars.Var, error) {
}
type elemOp struct {
ns string
name string
qname string
indexOps []valuesOp
begin int
end int
@ -153,9 +155,9 @@ type elemOp struct {
}
func (op *elemOp) invoke(fm *Frame) ([]vars.Var, error) {
variable := fm.ResolveVar(op.ns, op.name)
variable := fm.ResolveVar(op.qname)
if variable == nil {
return nil, fmt.Errorf("variable $%s:%s does not exist, compiler bug", op.ns, op.name)
return nil, fmt.Errorf("variable $%s does not exist, compiler bug", op.qname)
}
indicies := make([]interface{}, len(op.indexOps))

View File

@ -221,11 +221,11 @@ func (cp *compiler) primary(n *parse.Primary) valuesOpBody {
case parse.Bareword, parse.SingleQuoted, parse.DoubleQuoted:
return literalStr(n.Value)
case parse.Variable:
explode, ns, name := ParseVariableRef(n.Value)
if !cp.registerVariableGet(ns, name) {
cp.errorf("variable $%s not found", n.Value)
sigil, qname := SplitVariableRef(n.Value)
if !cp.registerVariableGet(qname) {
cp.errorf("variable $%s not found", qname)
}
return &variableOp{explode, ns, name}
return &variableOp{sigil != "", qname}
case parse.Wildcard:
seg, err := wildcardToSegment(n.SourceText())
if err != nil {
@ -257,14 +257,13 @@ func (cp *compiler) primary(n *parse.Primary) valuesOpBody {
type variableOp struct {
explode bool
ns string
name string
qname string
}
func (op variableOp) invoke(fm *Frame) ([]interface{}, error) {
variable := fm.ResolveVar(op.ns, op.name)
variable := fm.ResolveVar(op.qname)
if variable == nil {
return nil, fmt.Errorf("variable $%s:%s not found", op.ns, op.name)
return nil, fmt.Errorf("variable $%s not found", op.qname)
}
value := variable.Get()
if op.explode {
@ -390,8 +389,10 @@ func (cp *compiler) lambda(n *parse.Primary) valuesOpBody {
// Argument list.
argNames = make([]string, len(n.Elements))
for i, arg := range n.Elements {
qname := mustString(cp, arg, "argument name must be literal string")
explode, ns, name := ParseVariableRef(qname)
ref := mustString(cp, arg, "argument name must be literal string")
sigil, qname := SplitVariableRef(ref)
explode := sigil != ""
ns, name := SplitQNameNs(qname)
if ns != "" {
cp.errorpf(arg.Range().From, arg.Range().To, "argument name must be unqualified")
}
@ -414,7 +415,7 @@ func (cp *compiler) lambda(n *parse.Primary) valuesOpBody {
optDefaultOps = make([]valuesOp, len(n.MapPairs))
for i, opt := range n.MapPairs {
qname := mustString(cp, opt.Key, "option name must be literal string")
_, ns, name := ParseVariableRef(qname)
ns, name := SplitQNameNs(qname)
if ns != "" {
cp.errorpf(opt.Key.Range().From, opt.Key.Range().To, "option name must be unqualified")
}
@ -449,7 +450,7 @@ func (cp *compiler) lambda(n *parse.Primary) valuesOpBody {
cp.popScope()
for name := range capture {
cp.registerVariableGetQname(name)
cp.registerVariableGet(name)
}
return &lambdaOp{argNames, restArgName, optNames, optDefaultOps, capture, subop, cp.srcMeta, n.Range().From, n.Range().To}
@ -470,7 +471,7 @@ type lambdaOp struct {
func (op *lambdaOp) invoke(fm *Frame) ([]interface{}, error) {
evCapture := make(Ns)
for name := range op.capture {
evCapture[name] = fm.ResolveVar("", name)
evCapture[name] = fm.ResolveVar(":" + name)
}
optDefaults := make([]interface{}, len(op.optDefaultOps))
for i, op := range op.optDefaultOps {

View File

@ -49,6 +49,51 @@ func TestCompileValue(t *testing.T) {
// Splicing
That("x=[elvish rules]; put $@x").Puts("elvish", "rules"),
// Variable namespace
// ------------------
// Pseudo-namespace local: accesses the local scope.
That("x = outer; { local:x = inner; put $local:x }").Puts("inner"),
// Pseudo-namespace up: accesses upvalues.
That("x = outer; { local:x = inner; put $up:x }").Puts("outer"),
// Pseudo-namespace builtin: accesses builtins.
That("put $builtin:true").Puts(true),
// Unqualified name prefers local: to up:.
That("x = outer; { local:x = inner; put $x }").Puts("inner"),
// Unqualified name resolves to upvalue if no local name exists.
That("x = outer; { put $x }").Puts("outer"),
// Unqualified name resolves to builtin if no local name or upvalue
// exists.
That("put $true").Puts(true),
// A name can be explicitly unqualified by having a leading colon.
That("x = val; put $:x").Puts("val"),
That("put $:true").Puts(true),
// Pseudo-namespace E: provides read-write access to environment
// variables. Colons inside the name are supported.
That("set-env a:b VAL; put $E:a:b").Puts("VAL"),
That("E:a:b = VAL2; get-env a:b").Puts("VAL2"),
// Pseudo-namespace e: provides readonly access to external commands.
// Only names ending in ~ are resolved, and resolution always succeeds
// regardless of whether the command actually exists. Colons inside the
// name are supported.
That("put $e:a:b~").Puts(ExternalCmd{Name: "a:b"}),
// A "normal" namespace access indexes the namespace as a variable.
That("ns: = (ns [&a= val]); put $ns:a").Puts("val"),
// Multi-level namespace access is supported.
That("ns: = (ns [&a:= (ns [&b= val])]); put $ns:a:b").Puts("val"),
// Multi-level namespace access can have a leading colon to signal that
// the first component is unqualified.
That("ns: = (ns [&a:= (ns [&b= val])]); put $:ns:a:b").Puts("val"),
// Multi-level namespace access can be combined with the local:
// pseudo-namespaces.
That("ns: = (ns [&a:= (ns [&b= val])]); put $local:ns:a:b").Puts("val"),
// Multi-level namespace access can be combined with the up:
// pseudo-namespaces.
That("ns: = (ns [&a:= (ns [&b= val])]); { put $up:ns:a:b }").Puts("val"),
// Tilde
// -----
That("h=$E:HOME; E:HOME=/foo; put ~ ~/src; E:HOME=$h").Puts("/foo", "/foo/src"),

View File

@ -60,56 +60,18 @@ func (cp *compiler) popScope() {
cp.scopes = cp.scopes[:len(cp.scopes)-1]
}
func (cp *compiler) registerVariableGetQname(qname string) bool {
_, ns, name := ParseVariableRef(qname)
return cp.registerVariableGet(ns, name)
func (cp *compiler) registerVariableSet(qname string) bool {
return cp.registerVariableAccess(qname, true)
}
func (cp *compiler) registerVariableGet(ns, name string) bool {
switch ns {
case "", "local", "up":
// Handled below
case "e", "E":
return true
default:
return cp.registerModAccess(ns)
}
// Find in local scope
if ns == "" || ns == "local" {
if cp.thisScope().has(name) {
return true
}
}
// Find in upper scopes
if ns == "" || ns == "up" {
for i := len(cp.scopes) - 2; i >= 0; i-- {
if cp.scopes[i].has(name) {
// Existing name: record capture and return.
cp.capture.set(name)
return true
}
}
}
// Find in builtin scope
if ns == "" || ns == "builtin" {
if cp.builtin.has(name) {
return true
}
}
return false
func (cp *compiler) registerVariableGet(qname string) bool {
return cp.registerVariableAccess(qname, false)
}
func (cp *compiler) registerVariableSetQname(qname string) bool {
_, ns, name := ParseVariableRef(qname)
return cp.registerVariableSet(ns, name)
}
func (cp *compiler) registerVariableAccess(qname string, set bool) bool {
readLocal := func(name string) bool { return cp.thisScope().has(name) }
func (cp *compiler) registerVariableSet(ns, name string) bool {
switch ns {
case "local":
cp.thisScope().set(name)
return true
case "up":
readUpvalue := func(name string) bool {
for i := len(cp.scopes) - 2; i >= 0; i-- {
if cp.scopes[i].has(name) {
// Existing name: record capture and return.
@ -118,36 +80,43 @@ func (cp *compiler) registerVariableSet(ns, name string) bool {
}
}
return false
case "builtin":
cp.errorf("cannot set builtin variable")
return false
case "":
if cp.thisScope().has(name) {
// A name on current scope. Do nothing.
}
readBuiltin := func(name string) bool { return cp.builtin.has(name) }
readNonPseudo := func(name string) bool {
return readLocal(name) || readUpvalue(name) || readBuiltin(name)
}
createLocal := func(name string) bool {
if set && name != "" && !strings.ContainsRune(name[:len(name)-1], ':') {
cp.thisScope().set(name)
return true
}
// Walk up the upper scopes
for i := len(cp.scopes) - 2; i >= 0; i-- {
if cp.scopes[i].has(name) {
// Existing name. Do nothing
cp.capture.set(name)
return true
}
}
// New name. Register on this scope!
cp.thisScope().set(name)
return true
case "e", "E":
// Special namespaces, do nothing
return true
default:
return cp.registerModAccess(ns)
return false
}
}
func (cp *compiler) registerModAccess(name string) bool {
if strings.ContainsRune(name, ':') {
name = name[:strings.IndexByte(name, ':')]
ns, name := SplitQNameNsFirst(qname) // ns = "", name = "ns:"
name1 := name // name1 = "ns:"
if name != "" && strings.ContainsRune(name[:len(name)-1], ':') {
name1, _ = SplitQNameNsFirst(name)
}
// This switch mirrors the structure of that from (*Frame).ResoleVar.
switch ns {
case "E:":
return true
case "e:":
return !set && strings.HasSuffix(name, FnSuffix)
case "local:":
return readLocal(name1) || createLocal(name)
case "up:":
return readUpvalue(name1)
case "builtin:":
return readBuiltin(name1)
case "", ":":
return readNonPseudo(name1) || createLocal(name)
default:
return readNonPseudo(ns)
}
return cp.registerVariableGet("", name+NsSuffix)
}

View File

@ -28,10 +28,6 @@ func TestNumBgJobs(t *testing.T) {
func TestMiscEval(t *testing.T) {
Test(t,
// Pseudo-namespaces local: and up:
That("x=lorem; { local:x=ipsum; put $up:x $local:x }").Puts(
"lorem", "ipsum"),
That("x=lorem; { up:x=ipsum; put $x }; put $x").Puts("ipsum", "ipsum"),
// Pseudo-namespace E:
That("E:FOO=lorem; put $E:FOO").Puts("lorem"),
That("del E:FOO; put $E:FOO").Puts(""),

View File

@ -72,12 +72,12 @@ func (ev *Evaler) PurelyEvalPrimary(pn *parse.Primary) interface{} {
case parse.Bareword, parse.SingleQuoted, parse.DoubleQuoted:
return pn.Value
case parse.Variable:
explode, ns, name := ParseVariableRef(pn.Value)
if explode {
sigil, qname := SplitVariableRef(pn.Value)
if sigil != "" {
return nil
}
ec := NewTopFrame(ev, NewInternalSource("[purely eval]"), nil)
variable := ec.ResolveVar(ns, name)
variable := ec.ResolveVar(qname)
if variable != nil {
return variable.Get()
}

View File

@ -13,29 +13,29 @@ import (
// namespace ns that can be found from the top context.
func (ev *evalerScopes) EachVariableInTop(ns string, f func(s string)) {
switch ns {
case "builtin":
case "builtin:":
for name := range ev.Builtin {
f(name)
}
case "":
case "", ":":
for name := range ev.Global {
f(name)
}
for name := range ev.Builtin {
f(name)
}
case "e":
case "e:":
EachExternal(func(cmd string) {
f(cmd + FnSuffix)
})
case "E":
case "E:":
for _, s := range os.Environ() {
if i := strings.IndexByte(s, '='); i > 0 {
f(s[:i])
}
}
default:
segs := splitQName(ns + NsSuffix)
segs := SplitQNameNsSegs(ns)
mod := ev.Global[segs[0]]
if mod == nil {
mod = ev.Builtin[segs[0]]
@ -57,90 +57,73 @@ func (ev *evalerScopes) EachVariableInTop(ns string, f func(s string)) {
// EachNsInTop calls the passed function for each namespace that can be used
// from the top context.
func (ev *evalerScopes) EachNsInTop(f func(s string)) {
f("builtin")
f("e")
f("E")
f("builtin:")
f("e:")
f("E:")
for name := range ev.Global {
if strings.HasSuffix(name, NsSuffix) {
f(name[:len(name)-len(NsSuffix)])
f(name)
}
}
for name := range ev.Builtin {
if strings.HasSuffix(name, NsSuffix) {
f(name[:len(name)-len(NsSuffix)])
f(name)
}
}
}
// ResolveVar resolves a variable. When the variable cannot be found, nil is
// returned.
func (fm *Frame) ResolveVar(n, name string) vars.Var {
if n == "" {
return fm.resolveUnqualified(name)
}
func (fm *Frame) ResolveVar(qname string) vars.Var {
ns, name := SplitQNameNsFirst(qname)
// TODO: Let this function accept the fully qualified name.
segs := splitQName(n + ":" + name)
var ns Ns
switch segs[0] {
case "e:":
if len(segs) == 2 && strings.HasSuffix(segs[1], FnSuffix) {
return vars.NewReadOnly(ExternalCmd{Name: segs[1][:len(segs[1])-len(FnSuffix)]})
}
return nil
switch ns {
case "E:":
if len(segs) == 2 {
return vars.FromEnv(segs[1])
return vars.FromEnv(name)
case "e:":
if strings.HasSuffix(name, FnSuffix) {
return vars.NewReadOnly(ExternalCmd{name[:len(name)-len(FnSuffix)]})
}
return nil
case "local:":
ns = fm.local
return resolveNested(fm.local, name)
case "up:":
ns = fm.up
return resolveNested(fm.up, name)
case "builtin:":
ns = fm.Builtin
return resolveNested(fm.Builtin, name)
case "", ":":
return fm.resolveNonPseudo(name)
default:
v := fm.resolveUnqualified(segs[0])
if v == nil {
return nil
}
ns = v.Get().(Ns)
return fm.resolveNonPseudo(qname)
}
}
for _, seg := range segs[1 : len(segs)-1] {
v := ns[seg]
if v == nil {
func (fm *Frame) resolveNonPseudo(name string) vars.Var {
if v := resolveNested(fm.local, name); v != nil {
return v
}
if v := resolveNested(fm.up, name); v != nil {
return v
}
return resolveNested(fm.Builtin, name)
}
func resolveNested(ns Ns, name string) vars.Var {
if name == "" {
return nil
}
segs := SplitQNameNsSegs(name)
for _, seg := range segs[:len(segs)-1] {
variable := ns[seg]
if variable == nil {
return nil
}
ns = v.Get().(Ns)
nestedNs, ok := variable.Get().(Ns)
if !ok {
return nil
}
ns = nestedNs
}
return ns[segs[len(segs)-1]]
}
func splitQName(qname string) []string {
i := 0
var segs []string
for i < len(qname) {
j := strings.IndexByte(qname[i:], ':')
if j == -1 {
segs = append(segs, qname[i:])
break
}
segs = append(segs, qname[i:i+j+1])
i += j + 1
}
return segs
}
func (fm *Frame) resolveUnqualified(name string) vars.Var {
if v, ok := fm.local[name]; ok {
return v
}
if v, ok := fm.up[name]; ok {
return v
}
return fm.Builtin[name]
}

View File

@ -1,18 +0,0 @@
package eval
import (
"testing"
"github.com/elves/elvish/tt"
)
var splitQNameTests = tt.Table{
tt.Args("a").Rets([]string{"a"}),
tt.Args("a:b").Rets([]string{"a:", "b"}),
tt.Args("a:b:").Rets([]string{"a:", "b:"}),
tt.Args("a:b:c:d").Rets([]string{"a:", "b:", "c:", "d"}),
}
func TestSplitQName(t *testing.T) {
tt.Test(t, tt.Fn("splitQName", splitQName), splitQNameTests)
}

View File

@ -2,65 +2,64 @@ package eval
import "strings"
// ParseVariableRef parses a variable reference.
func ParseVariableRef(text string) (explode bool, ns string, name string) {
return parseVariableRef(text, true)
}
// ParseIncompleteVariableRef parses an incomplete variable reference.
func ParseIncompleteVariableRef(text string) (explode bool, ns string, name string) {
return parseVariableRef(text, false)
}
func parseVariableRef(text string, complete bool) (explode bool, ns string, name string) {
explodePart, nsPart, name := splitVariableRef(text, complete)
ns = nsPart
if len(ns) > 0 {
ns = ns[:len(ns)-1]
// SplitVariableRef splits a variable reference into the sigil and the
// (qualified) name.
func SplitVariableRef(ref string) (sigil string, qname string) {
if ref == "" {
return "", ""
}
return explodePart != "", ns, name
}
// SplitVariableRef splits a variable reference into three parts: an optional
// explode operator (either "" or "@"), a namespace part, and a name part.
func SplitVariableRef(text string) (explodePart, nsPart, name string) {
return splitVariableRef(text, true)
}
// SplitIncompleteVariableRef splits an incomplete variable reference into three
// parts: an optional explode operator (either "" or "@"), a namespace part, and
// a name part.
func SplitIncompleteVariableRef(text string) (explodePart, nsPart, name string) {
return splitVariableRef(text, false)
}
func splitVariableRef(text string, complete bool) (explodePart, nsPart, name string) {
if text == "" {
return "", "", ""
}
e, qname := "", text
if text[0] == '@' {
e = "@"
qname = text[1:]
switch ref[0] {
case '@':
// TODO(xiaq): Support % later.
return ref[:1], ref[1:]
default:
return "", ref
}
}
// SplitQNameNs splits a qualified variable name into the namespace part and the
// name part.
func SplitQNameNs(qname string) (ns, name string) {
if qname == "" {
return e, "", ""
return "", ""
}
i := strings.LastIndexByte(qname, ':')
if complete && i == len(qname)-1 {
i = strings.LastIndexByte(qname[:len(qname)-1], ':')
}
return e, qname[:i+1], qname[i+1:]
colon := strings.LastIndexByte(qname[:len(qname)-1], ':')
// If colon is -1, colon+1 will be 0, rendering an empty ns.
return qname[:colon+1], qname[colon+1:]
}
// MakeVariableRef builds a variable reference.
func MakeVariableRef(explode bool, ns string, name string) string {
prefix := ""
if explode {
prefix = "@"
}
if ns != "" {
prefix += ns + ":"
}
return prefix + name
// SplitQNameNs splits an incomplete qualified variable name into the namespace
// part and the name part.
func SplitQNameNsIncomplete(qname string) (ns, name string) {
colon := strings.LastIndexByte(qname, ':')
// If colon is -1, colon+1 will be 0, rendering an empty ns.
return qname[:colon+1], qname[colon+1:]
}
// SplitQNameNs splits a qualified variable name into the first part and the rest.
func SplitQNameNsFirst(qname string) (ns, rest string) {
colon := strings.IndexByte(qname, ':')
if colon == len(qname)-1 {
// Unqualified variable ending with colon ($name:).
return "", qname
}
// If colon is -1, colon+1 will be 0, rendering an empty ns.
return qname[:colon+1], qname[colon+1:]
}
// SplitIncompleteQNameNsFirst splits an incomplete qualified variable name into
// the first part and the rest.
func SplitIncompleteQNameFirstNs(qname string) (ns, rest string) {
colon := strings.IndexByte(qname, ':')
// If colon is -1, colon+1 will be 0, rendering an empty ns.
return qname[:colon+1], qname[colon+1:]
}
// SplitQNameNsSegs splits a qualified name into namespace segments.
func SplitQNameNsSegs(qname string) []string {
segs := strings.SplitAfter(qname, ":")
if len(segs) > 0 && segs[len(segs)-1] == "" {
segs = segs[:len(segs)-1]
}
return segs
}

79
eval/variable_ref_test.go Normal file
View File

@ -0,0 +1,79 @@
package eval
import (
"testing"
"github.com/elves/elvish/tt"
)
var Args = tt.Args
func TestSplitVariableRef(t *testing.T) {
tt.Test(t, tt.Fn("SplitVariableRef", SplitVariableRef), tt.Table{
Args("").Rets("", ""),
Args("x").Rets("", "x"),
Args("@x").Rets("@", "x"),
Args("a:b").Rets("", "a:b"),
Args("@a:b").Rets("@", "a:b"),
})
}
func TestSplitQNameNs(t *testing.T) {
tt.Test(t, tt.Fn("SplitQNameNs", SplitQNameNs), tt.Table{
Args("").Rets("", ""),
Args("a").Rets("", "a"),
Args("a:").Rets("", "a:"),
Args("a:b").Rets("a:", "b"),
Args("a:b:").Rets("a:", "b:"),
Args("a:b:c").Rets("a:b:", "c"),
Args("a:b:c:").Rets("a:b:", "c:"),
})
}
func TestSplitQNameNsIncomplete(t *testing.T) {
tt.Test(t, tt.Fn("SplitQNameNsIncomplete", SplitQNameNsIncomplete), tt.Table{
Args("").Rets("", ""),
Args("a").Rets("", "a"),
Args("a:").Rets("a:", ""),
Args("a:b").Rets("a:", "b"),
Args("a:b:").Rets("a:b:", ""),
Args("a:b:c").Rets("a:b:", "c"),
Args("a:b:c:").Rets("a:b:c:", ""),
})
}
func TestSplitQNameNsFirst(t *testing.T) {
tt.Test(t, tt.Fn("SplitQNameNsFirst", SplitQNameNsFirst), tt.Table{
Args("").Rets("", ""),
Args("a").Rets("", "a"),
Args("a:").Rets("", "a:"),
Args("a:b").Rets("a:", "b"),
Args("a:b:").Rets("a:", "b:"),
Args("a:b:c").Rets("a:", "b:c"),
Args("a:b:c:").Rets("a:", "b:c:"),
})
}
func TestSplitIncompleteQNameFirstNs(t *testing.T) {
tt.Test(t, tt.Fn("SplitIncompleteQNameFirstNs", SplitIncompleteQNameFirstNs), tt.Table{
Args("").Rets("", ""),
Args("a").Rets("", "a"),
Args("a:").Rets("a:", ""),
Args("a:b").Rets("a:", "b"),
Args("a:b:").Rets("a:", "b:"),
Args("a:b:c").Rets("a:", "b:c"),
Args("a:b:c:").Rets("a:", "b:c:"),
})
}
func TestSplitQNameNsSegs(t *testing.T) {
tt.Test(t, tt.Fn("SplitQNameNsSegs", SplitQNameNsSegs), tt.Table{
Args("").Rets([]string{}),
Args("a").Rets([]string{"a"}),
Args("a:").Rets([]string{"a:"}),
Args("a:b").Rets([]string{"a:", "b"}),
Args("a:b:").Rets([]string{"a:", "b:"}),
Args("a:b:c").Rets([]string{"a:", "b:", "c"}),
Args("a:b:c:").Rets([]string{"a:", "b:", "c:"}),
})
}