src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/eval/compiler.go (about)

     1  package eval
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"strings"
     7  
     8  	"src.elv.sh/pkg/diag"
     9  	"src.elv.sh/pkg/eval/vars"
    10  	"src.elv.sh/pkg/parse"
    11  	"src.elv.sh/pkg/prog"
    12  )
    13  
    14  // compiler maintains the set of states needed when compiling a single source
    15  // file.
    16  type compiler struct {
    17  	// Builtin namespace.
    18  	builtin *staticNs
    19  	// Lexical namespaces.
    20  	scopes []*staticNs
    21  	// Sources of captured variables.
    22  	captures []*staticUpNs
    23  	// Pragmas tied to scopes.
    24  	pragmas []*scopePragma
    25  	// Names of internal modules.
    26  	modules []string
    27  	// Destination of warning messages. This is currently only used for
    28  	// deprecation messages.
    29  	warn io.Writer
    30  	// Deprecation registry.
    31  	deprecations deprecationRegistry
    32  	// Information about the source.
    33  	srcMeta parse.Source
    34  	// Compilation errors.
    35  	errors []*CompilationError
    36  	// Suggested code to fix potential issues found during compilation.
    37  	autofixes []string
    38  }
    39  
    40  type scopePragma struct {
    41  	unknownCommandIsExternal bool
    42  }
    43  
    44  func compile(b, g *staticNs, modules []string, tree parse.Tree, w io.Writer) (nsOp, []string, error) {
    45  	g = g.clone()
    46  	cp := &compiler{
    47  		b, []*staticNs{g}, []*staticUpNs{new(staticUpNs)},
    48  		[]*scopePragma{{unknownCommandIsExternal: true}},
    49  		modules,
    50  		w, newDeprecationRegistry(), tree.Source, nil, nil}
    51  	chunkOp := cp.chunkOp(tree.Root)
    52  	return nsOp{chunkOp, g}, cp.autofixes, diag.PackErrors(cp.errors)
    53  }
    54  
    55  type nsOp struct {
    56  	inner    effectOp
    57  	template *staticNs
    58  }
    59  
    60  // Prepares the local namespace, and returns the namespace and a function for
    61  // executing the inner effectOp. Mutates fm.local.
    62  func (op nsOp) prepare(fm *Frame) (*Ns, func() Exception) {
    63  	if len(op.template.infos) > len(fm.local.infos) {
    64  		n := len(op.template.infos)
    65  		newLocal := &Ns{make([]vars.Var, n), op.template.infos}
    66  		copy(newLocal.slots, fm.local.slots)
    67  		for i := len(fm.local.infos); i < n; i++ {
    68  			// TODO: Take readOnly into account too
    69  			newLocal.slots[i] = MakeVarFromName(newLocal.infos[i].name)
    70  		}
    71  		fm.local = newLocal
    72  	} else {
    73  		// If no new variable has been created, there might still be some
    74  		// existing variables deleted.
    75  		fm.local = &Ns{fm.local.slots, op.template.infos}
    76  	}
    77  	return fm.local, func() Exception { return op.inner.exec(fm) }
    78  }
    79  
    80  type CompilationError = diag.Error[CompilationErrorTag]
    81  
    82  // CompilationErrorTag parameterizes [diag.Error] to define [CompilationError].
    83  type CompilationErrorTag struct{}
    84  
    85  func (CompilationErrorTag) ErrorTag() string { return "compilation error" }
    86  
    87  func (cp *compiler) errorpf(r diag.Ranger, format string, args ...any) {
    88  	cp.errors = append(cp.errors, &CompilationError{
    89  		Message: fmt.Sprintf(format, args...),
    90  		Context: *diag.NewContext(cp.srcMeta.Name, cp.srcMeta.Code, r)})
    91  }
    92  
    93  // UnpackCompilationErrors returns the constituent compilation errors if the
    94  // given error contains one or more compilation errors. Otherwise it returns
    95  // nil.
    96  func UnpackCompilationErrors(e error) []*CompilationError {
    97  	if errs := diag.UnpackErrors[CompilationErrorTag](e); len(errs) > 0 {
    98  		return errs
    99  	}
   100  	return nil
   101  }
   102  
   103  func (cp *compiler) thisScope() *staticNs {
   104  	return cp.scopes[len(cp.scopes)-1]
   105  }
   106  
   107  func (cp *compiler) currentPragma() *scopePragma {
   108  	return cp.pragmas[len(cp.pragmas)-1]
   109  }
   110  
   111  func (cp *compiler) pushScope() (*staticNs, *staticUpNs) {
   112  	sc := new(staticNs)
   113  	up := new(staticUpNs)
   114  	cp.scopes = append(cp.scopes, sc)
   115  	cp.captures = append(cp.captures, up)
   116  	currentPragmaCopy := *cp.currentPragma()
   117  	cp.pragmas = append(cp.pragmas, &currentPragmaCopy)
   118  	return sc, up
   119  }
   120  
   121  func (cp *compiler) popScope() {
   122  	cp.scopes[len(cp.scopes)-1] = nil
   123  	cp.scopes = cp.scopes[:len(cp.scopes)-1]
   124  	cp.captures[len(cp.captures)-1] = nil
   125  	cp.captures = cp.captures[:len(cp.captures)-1]
   126  	cp.pragmas[len(cp.pragmas)-1] = nil
   127  	cp.pragmas = cp.pragmas[:len(cp.pragmas)-1]
   128  }
   129  
   130  func (cp *compiler) checkDeprecatedBuiltin(name string, r diag.Ranger) {
   131  	msg := ""
   132  	minLevel := 20
   133  	switch name {
   134  	case "eawk~":
   135  		msg = `the "eawk" command is deprecated; use "re:awk" instead`
   136  	default:
   137  		return
   138  	}
   139  	cp.deprecate(r, msg, minLevel)
   140  }
   141  
   142  type deprecationTag struct{}
   143  
   144  func (deprecationTag) ErrorTag() string { return "deprecation" }
   145  
   146  func (cp *compiler) deprecate(r diag.Ranger, msg string, minLevel int) {
   147  	if cp.warn == nil || r == nil {
   148  		return
   149  	}
   150  	dep := deprecation{cp.srcMeta.Name, r.Range(), msg}
   151  	if prog.DeprecationLevel >= minLevel && cp.deprecations.register(dep) {
   152  		err := diag.Error[deprecationTag]{
   153  			Message: msg,
   154  			Context: *diag.NewContext(cp.srcMeta.Name, cp.srcMeta.Code, r.Range())}
   155  		fmt.Fprintln(cp.warn, err.Show(""))
   156  	}
   157  }
   158  
   159  // Given a variable that doesn't resolve, add any applicable autofixes.
   160  func (cp *compiler) autofixUnresolvedVar(qname string) {
   161  	if len(cp.modules) == 0 {
   162  		return
   163  	}
   164  	first, _ := SplitQName(qname)
   165  	mod := strings.TrimSuffix(first, ":")
   166  	if mod != first && sliceContains(cp.modules, mod) {
   167  		cp.autofixes = append(cp.autofixes, "use "+mod)
   168  	}
   169  }