github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/eval/var_ref.go (about)

     1  package eval
     2  
     3  import (
     4  	"strings"
     5  
     6  	"github.com/markusbkk/elvish/pkg/diag"
     7  	"github.com/markusbkk/elvish/pkg/eval/vars"
     8  )
     9  
    10  // This file implements variable resolution. Elvish has fully static lexical
    11  // scopes, so variable resolution involves some work in the compilation phase as
    12  // well.
    13  //
    14  // During compilation, a qualified variable name (whether in lvalue, like "x
    15  // = foo", or in variable use, like "$x") is searched in compiler's staticNs
    16  // tables to determine which scope they belong to, as well as their indices in
    17  // that scope. This step is just called "resolve" in the code, and it stores
    18  // information in a varRef struct.
    19  //
    20  // During evaluation, the varRef is then used to look up the Var for the
    21  // variable. This step is called "deref" in the code.
    22  //
    23  // The resolve phase can take place during evaluation as well for introspection.
    24  
    25  // Keeps all the information statically about a variable referenced by a
    26  // qualified name.
    27  type varRef struct {
    28  	scope    varScope
    29  	info     staticVarInfo
    30  	index    int
    31  	subNames []string
    32  }
    33  
    34  type varScope int
    35  
    36  const (
    37  	localScope varScope = 1 + iota
    38  	captureScope
    39  	builtinScope
    40  	envScope
    41  	externalScope
    42  )
    43  
    44  // An interface satisfied by both *compiler and *Frame. Used to implement
    45  // resolveVarRef as a function that works for both types.
    46  type scopeSearcher interface {
    47  	searchLocal(k string) (staticVarInfo, int)
    48  	searchCapture(k string) (staticVarInfo, int)
    49  	searchBuiltin(k string, r diag.Ranger) (staticVarInfo, int)
    50  }
    51  
    52  // Resolves a qname into a varRef.
    53  func resolveVarRef(s scopeSearcher, qname string, r diag.Ranger) *varRef {
    54  	if strings.HasPrefix(qname, ":") {
    55  		// $:foo is reserved for fully-qualified names in future
    56  		return nil
    57  	}
    58  	if ref := resolveVarRefLocal(s, qname); ref != nil {
    59  		return ref
    60  	}
    61  	if ref := resolveVarRefCapture(s, qname); ref != nil {
    62  		return ref
    63  	}
    64  	if ref := resolveVarRefBuiltin(s, qname, r); ref != nil {
    65  		return ref
    66  	}
    67  	return nil
    68  }
    69  
    70  func resolveVarRefLocal(s scopeSearcher, qname string) *varRef {
    71  	first, rest := SplitQName(qname)
    72  	if info, index := s.searchLocal(first); index != -1 {
    73  		return &varRef{localScope, info, index, SplitQNameSegs(rest)}
    74  	}
    75  	return nil
    76  }
    77  
    78  func resolveVarRefCapture(s scopeSearcher, qname string) *varRef {
    79  	first, rest := SplitQName(qname)
    80  	if info, index := s.searchCapture(first); index != -1 {
    81  		return &varRef{captureScope, info, index, SplitQNameSegs(rest)}
    82  	}
    83  	return nil
    84  }
    85  
    86  func resolveVarRefBuiltin(s scopeSearcher, qname string, r diag.Ranger) *varRef {
    87  	first, rest := SplitQName(qname)
    88  	if rest != "" {
    89  		// Try special namespace first.
    90  		switch first {
    91  		case "e:":
    92  			if strings.HasSuffix(rest, FnSuffix) {
    93  				return &varRef{scope: externalScope, subNames: []string{rest[:len(rest)-1]}}
    94  			}
    95  		case "E:":
    96  			return &varRef{scope: envScope, subNames: []string{rest}}
    97  		}
    98  	}
    99  	if info, index := s.searchBuiltin(first, r); index != -1 {
   100  		return &varRef{builtinScope, info, index, SplitQNameSegs(rest)}
   101  	}
   102  	return nil
   103  }
   104  
   105  // Tries to resolve the command head as an internal command, i.e. a builtin
   106  // special command or a function.
   107  func resolveCmdHeadInternally(s scopeSearcher, head string, r diag.Ranger) (compileBuiltin, *varRef) {
   108  	special, ok := builtinSpecials[head]
   109  	if ok {
   110  		return special, nil
   111  	}
   112  	sigil, qname := SplitSigil(head)
   113  	if sigil == "" {
   114  		varName := qname + FnSuffix
   115  		ref := resolveVarRef(s, varName, r)
   116  		if ref != nil {
   117  			return nil, ref
   118  		}
   119  	}
   120  	return nil, nil
   121  }
   122  
   123  // Dereferences a varRef into a Var.
   124  func deref(fm *Frame, ref *varRef) vars.Var {
   125  	variable, subNames := derefBase(fm, ref)
   126  	for _, subName := range subNames {
   127  		ns, ok := variable.Get().(*Ns)
   128  		if !ok {
   129  			return nil
   130  		}
   131  		variable = ns.IndexString(subName)
   132  		if variable == nil {
   133  			return nil
   134  		}
   135  	}
   136  	return variable
   137  }
   138  
   139  func derefBase(fm *Frame, ref *varRef) (vars.Var, []string) {
   140  	switch ref.scope {
   141  	case localScope:
   142  		return fm.local.slots[ref.index], ref.subNames
   143  	case captureScope:
   144  		return fm.up.slots[ref.index], ref.subNames
   145  	case builtinScope:
   146  		return fm.Evaler.Builtin().slots[ref.index], ref.subNames
   147  	case envScope:
   148  		return vars.FromEnv(ref.subNames[0]), nil
   149  	case externalScope:
   150  		return vars.NewReadOnly(NewExternalCmd(ref.subNames[0])), nil
   151  	default:
   152  		return nil, nil
   153  	}
   154  }
   155  
   156  func (cp *compiler) searchLocal(k string) (staticVarInfo, int) {
   157  	return cp.thisScope().lookup(k)
   158  }
   159  
   160  func (cp *compiler) searchCapture(k string) (staticVarInfo, int) {
   161  	for i := len(cp.scopes) - 2; i >= 0; i-- {
   162  		info, index := cp.scopes[i].lookup(k)
   163  		if index != -1 {
   164  			// Record the capture from i+1 to len(cp.scopes)-1, and reuse the
   165  			// index to keep the index into the previous scope.
   166  			index = cp.captures[i+1].add(k, true, index)
   167  			for j := i + 2; j < len(cp.scopes); j++ {
   168  				index = cp.captures[j].add(k, false, index)
   169  			}
   170  			return info, index
   171  		}
   172  	}
   173  	return staticVarInfo{}, -1
   174  }
   175  
   176  func (cp *compiler) searchBuiltin(k string, r diag.Ranger) (staticVarInfo, int) {
   177  	info, index := cp.builtin.lookup(k)
   178  	if index != -1 {
   179  		cp.checkDeprecatedBuiltin(k, r)
   180  	}
   181  	return info, index
   182  }
   183  
   184  func (fm *Frame) searchLocal(k string) (staticVarInfo, int) {
   185  	return fm.local.lookup(k)
   186  }
   187  
   188  func (fm *Frame) searchCapture(k string) (staticVarInfo, int) {
   189  	return fm.up.lookup(k)
   190  }
   191  
   192  func (fm *Frame) searchBuiltin(k string, r diag.Ranger) (staticVarInfo, int) {
   193  	return fm.Evaler.Builtin().lookup(k)
   194  }