From 93c17dc7c2f10e965a365e4ae6bd3b22facd2cf1 Mon Sep 17 00:00:00 2001 From: Qi Xiao Date: Tue, 22 Oct 2019 22:49:32 +0100 Subject: [PATCH] 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. --- cliedit/highlight_deps.go | 29 +++--- edit/completion/complete_command.go | 11 ++- edit/completion/complete_variable.go | 23 ++--- edit/completion/complete_variable_test.go | 33 +++---- edit/edcore/highlight.go | 7 +- eval/builtin_fn_misc.go | 6 +- eval/builtin_special.go | 26 ++--- eval/builtin_special_test.go | 2 + eval/compile_effect.go | 6 +- eval/compile_lvalue.go | 38 ++++---- eval/compile_value.go | 27 +++--- eval/compile_value_test.go | 45 +++++++++ eval/compiler.go | 113 ++++++++-------------- eval/eval_test.go | 4 - eval/purely_eval.go | 6 +- eval/resolve.go | 111 +++++++++------------ eval/resolve_test.go | 18 ---- eval/variable_ref.go | 109 +++++++++++---------- eval/variable_ref_test.go | 79 +++++++++++++++ 19 files changed, 375 insertions(+), 318 deletions(-) delete mode 100644 eval/resolve_test.go create mode 100644 eval/variable_ref_test.go diff --git a/cliedit/highlight_deps.go b/cliedit/highlight_deps.go index c66a73a8..d774a05a 100644 --- a/cliedit/highlight_deps.go +++ b/cliedit/highlight_deps.go @@ -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 { diff --git a/edit/completion/complete_command.go b/edit/completion/complete_command.go index ada77c79..7eb7df5d 100644 --- a/edit/completion/complete_command.go +++ b/edit/completion/complete_command.go @@ -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{}} } }) diff --git a/edit/completion/complete_variable.go b/edit/completion/complete_variable.go index 95b88b8a..6e4cedaf 100644 --- a/edit/completion/complete_variable.go +++ b/edit/completion/complete_variable.go @@ -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 ":". - 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):]) } }) } diff --git a/edit/completion/complete_variable_test.go b/edit/completion/complete_variable_test.go index a344b81a..3fc4332b 100644 --- a/edit/completion/complete_variable_test.go +++ b/edit/completion/complete_variable_test.go @@ -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 diff --git a/edit/edcore/highlight.go b/edit/edcore/highlight.go index 220b9094..de24ade0 100644 --- a/edit/edcore/highlight.go +++ b/edit/edcore/highlight.go @@ -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 } diff --git a/eval/builtin_fn_misc.go b/eval/builtin_fn_misc.go index 29ca4a96..0f0fa809 100644 --- a/eval/builtin_fn_misc.go +++ b/eval/builtin_fn_misc.go @@ -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) + ")" } diff --git a/eval/builtin_special.go b/eval/builtin_special.go index 9b613a7a..f90133c0 100644 --- a/eval/builtin_special.go +++ b/eval/builtin_special.go @@ -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} diff --git a/eval/builtin_special_test.go b/eval/builtin_special_test.go index ee5e7a42..7fda6e46 100644 --- a/eval/builtin_special_test.go +++ b/eval/builtin_special_test.go @@ -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"), diff --git a/eval/compile_effect.go b/eval/compile_effect.go index 75dc64ee..73db48d5 100644 --- a/eval/compile_effect.go +++ b/eval/compile_effect.go @@ -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}) diff --git a/eval/compile_lvalue.go b/eval/compile_lvalue.go index 9c86e0b4..55a03c5f 100644 --- a/eval/compile_lvalue.go +++ b/eval/compile_lvalue.go @@ -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)) diff --git a/eval/compile_value.go b/eval/compile_value.go index 73aff08e..8147e392 100644 --- a/eval/compile_value.go +++ b/eval/compile_value.go @@ -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 { diff --git a/eval/compile_value_test.go b/eval/compile_value_test.go index 0266e38a..9d220861 100644 --- a/eval/compile_value_test.go +++ b/eval/compile_value_test.go @@ -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"), diff --git a/eval/compiler.go b/eval/compiler.go index 77c3f91d..f6839898 100644 --- a/eval/compiler.go +++ b/eval/compiler.go @@ -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) } diff --git a/eval/eval_test.go b/eval/eval_test.go index fb58b912..63801142 100644 --- a/eval/eval_test.go +++ b/eval/eval_test.go @@ -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(""), diff --git a/eval/purely_eval.go b/eval/purely_eval.go index 5324d2c1..c049351d 100644 --- a/eval/purely_eval.go +++ b/eval/purely_eval.go @@ -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() } diff --git a/eval/resolve.go b/eval/resolve.go index c1c5d085..83946af9 100644 --- a/eval/resolve.go +++ b/eval/resolve.go @@ -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] -} diff --git a/eval/resolve_test.go b/eval/resolve_test.go deleted file mode 100644 index c481d232..00000000 --- a/eval/resolve_test.go +++ /dev/null @@ -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) -} diff --git a/eval/variable_ref.go b/eval/variable_ref.go index bedf042e..48e32439 100644 --- a/eval/variable_ref.go +++ b/eval/variable_ref.go @@ -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 } diff --git a/eval/variable_ref_test.go b/eval/variable_ref_test.go new file mode 100644 index 00000000..1937647d --- /dev/null +++ b/eval/variable_ref_test.go @@ -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:"}), + }) +}