elvish/pkg/eval/frame.go
Qi Xiao 5775c8b3ed pkg/diag: Rework API and presentation of Context and Error.
- Change Context to export all its fields.

- Include end position in Context, and include it in Show.

- Remove the Type field from Error, and express it using an ErrorTag type
  parameter instead.

- Make {Pack Unpack}CognateErrors type-safe with the new ErrorTag mechanism, and
  rename them to just {Pack Unpack}Errors.
2024-01-16 11:04:52 +00:00

298 lines
7.8 KiB
Go

package eval
import (
"bufio"
"context"
"fmt"
"io"
"os"
"sync"
"src.elv.sh/pkg/diag"
"src.elv.sh/pkg/eval/errs"
"src.elv.sh/pkg/parse"
"src.elv.sh/pkg/prog"
"src.elv.sh/pkg/strutil"
)
// Frame contains information of the current running function, akin to a call
// frame in native CPU execution. A Frame is only modified during and very
// shortly after creation; new Frame's are "forked" when needed.
type Frame struct {
Evaler *Evaler
srcMeta parse.Source
local, up *Ns
defers *[]func(*Frame) Exception
// The godoc of the context package states:
//
// > Do not store Contexts inside a struct type; instead, pass a Context
// > explicitly to each function that needs it.
//
// However, that advice is considered by many to be overly aggressive
// (https://github.com/golang/go/issues/22602). The Frame struct doesn't fit
// the "parameter struct" definition in that discussion, but it is itself is
// a "context struct". Storing a Context inside it seems fine.
ctx context.Context
ports []*Port
traceback *StackTrace
background bool
}
// PrepareEval prepares a piece of code for evaluation in a copy of the current
// Frame. If r is not nil, it is added to the traceback of the evaluation
// context. If ns is not nil, it is used in place of the current local namespace
// as the namespace to evaluate the code in.
//
// If there is any parse error or compilation error, it returns a nil *Ns, nil
// function and the error. If there is no parse error or compilation error, it
// returns the altered local namespace, function that can be called to actuate
// the evaluation, and a nil error.
func (fm *Frame) PrepareEval(src parse.Source, r diag.Ranger, ns *Ns) (*Ns, func() Exception, error) {
tree, err := parse.Parse(src, parse.Config{WarningWriter: fm.ErrorFile()})
if err != nil {
return nil, nil, err
}
local := fm.local
if ns != nil {
local = ns
}
traceback := fm.traceback
if r != nil {
traceback = fm.addTraceback(r)
}
newFm := &Frame{
fm.Evaler, src, local, new(Ns), nil, fm.ctx, fm.ports, traceback, fm.background}
op, _, err := compile(fm.Evaler.Builtin().static(), local.static(), nil, tree, fm.ErrorFile())
if err != nil {
return nil, nil, err
}
newLocal, exec := op.prepare(newFm)
return newLocal, exec, nil
}
// Eval evaluates a piece of code in a copy of the current Frame. It returns the
// altered local namespace, and any parse error, compilation error or exception.
//
// See PrepareEval for a description of the arguments.
func (fm *Frame) Eval(src parse.Source, r diag.Ranger, ns *Ns) (*Ns, error) {
newLocal, exec, err := fm.PrepareEval(src, r, ns)
if err != nil {
return nil, err
}
return newLocal, exec()
}
// Close releases resources allocated for this frame. It always returns a nil
// error. It may be called only once.
func (fm *Frame) Close() error {
for _, port := range fm.ports {
port.close()
}
return nil
}
// InputChan returns a channel from which input can be read.
func (fm *Frame) InputChan() chan any {
return fm.ports[0].Chan
}
// InputFile returns a file from which input can be read.
func (fm *Frame) InputFile() *os.File {
return fm.ports[0].File
}
// ValueOutput returns a handle for writing value outputs.
func (fm *Frame) ValueOutput() ValueOutput {
p := fm.ports[1]
return valueOutput{p.Chan, p.sendStop, p.sendError}
}
// ByteOutput returns a handle for writing byte outputs.
func (fm *Frame) ByteOutput() ByteOutput {
return byteOutput{fm.ports[1].File}
}
// ErrorFile returns a file onto which error messages can be written.
func (fm *Frame) ErrorFile() *os.File {
return fm.ports[2].File
}
// Port returns port i. If the port doesn't exist, it returns nil
//
// This is a low-level construct that shouldn't be used for writing output; for
// that purpose, use [(*Frame).ValueOutput] and [(*Frame).ByteOutput] instead.
func (fm *Frame) Port(i int) *Port {
if i >= len(fm.ports) {
return nil
}
return fm.ports[i]
}
// IterateInputs calls the passed function for each input element.
func (fm *Frame) IterateInputs(f func(any)) {
var wg sync.WaitGroup
inputs := make(chan any)
wg.Add(2)
go func() {
linesToChan(fm.InputFile(), inputs)
wg.Done()
}()
go func() {
for v := range fm.ports[0].Chan {
inputs <- v
}
wg.Done()
}()
go func() {
wg.Wait()
close(inputs)
}()
for v := range inputs {
f(v)
}
}
func linesToChan(r io.Reader, ch chan<- any) {
filein := bufio.NewReader(r)
for {
line, err := filein.ReadString('\n')
if line != "" {
ch <- strutil.ChopLineEnding(line)
}
if err != nil {
if err != io.EOF {
logger.Println("error on reading:", err)
}
break
}
}
}
// Context returns a Context associated with the Frame.
func (fm *Frame) Context() context.Context {
return fm.ctx
}
// Canceled reports whether the Context of the Frame has been canceled.
func (fm *Frame) Canceled() bool {
select {
case <-fm.ctx.Done():
return true
default:
return false
}
}
// Fork returns a modified copy of fm. The ports are forked, and the name is
// changed to the given value. Other fields are copied shallowly.
func (fm *Frame) Fork(name string) *Frame {
newPorts := make([]*Port, len(fm.ports))
for i, p := range fm.ports {
if p != nil {
newPorts[i] = p.fork()
}
}
return &Frame{
fm.Evaler, fm.srcMeta,
fm.local, fm.up, fm.defers,
fm.ctx, newPorts,
fm.traceback, fm.background,
}
}
// A shorthand for forking a frame and setting the output port.
func (fm *Frame) forkWithOutput(name string, p *Port) *Frame {
newFm := fm.Fork(name)
newFm.ports[1] = p
return newFm
}
// CaptureOutput captures the output of a given callback that operates on a Frame.
func (fm *Frame) CaptureOutput(f func(*Frame) error) ([]any, error) {
outPort, collect, err := ValueCapturePort()
if err != nil {
return nil, err
}
err = f(fm.forkWithOutput("[output capture]", outPort))
return collect(), err
}
// PipeOutput calls a callback with output piped to the given output handlers.
func (fm *Frame) PipeOutput(f func(*Frame) error, vCb func(<-chan any), bCb func(*os.File)) error {
outPort, done, err := PipePort(vCb, bCb)
if err != nil {
return err
}
err = f(fm.forkWithOutput("[output pipe]", outPort))
done()
return err
}
func (fm *Frame) addTraceback(r diag.Ranger) *StackTrace {
return &StackTrace{
Head: diag.NewContext(fm.srcMeta.Name, fm.srcMeta.Code, r.Range()),
Next: fm.traceback,
}
}
// Returns an Exception with specified range and cause.
func (fm *Frame) errorp(r diag.Ranger, e error) Exception {
switch e := e.(type) {
case nil:
return nil
case Exception:
return e
default:
if _, ok := e.(errs.SetReadOnlyVar); ok {
r := r.Range()
e = errs.SetReadOnlyVar{VarName: fm.srcMeta.Code[r.From:r.To]}
}
ctx := diag.NewContext(fm.srcMeta.Name, fm.srcMeta.Code, r)
return &exception{e, &StackTrace{Head: ctx, Next: fm.traceback}}
}
}
// Returns an Exception with specified range and error text.
func (fm *Frame) errorpf(r diag.Ranger, format string, args ...any) Exception {
return fm.errorp(r, fmt.Errorf(format, args...))
}
// Deprecate shows a deprecation message. The message is not shown if the same
// deprecation message has been shown for the same location before.
func (fm *Frame) Deprecate(msg string, ctx *diag.Context, minLevel int) {
if prog.DeprecationLevel < minLevel {
return
}
if ctx == nil {
ctx = fm.traceback.Head
}
if fm.Evaler.registerDeprecation(deprecation{ctx.Name, ctx.Ranging, msg}) {
err := diag.Error[deprecationTag]{Message: msg, Context: *ctx}
fm.ErrorFile().WriteString(err.Show("") + "\n")
}
}
func (fm *Frame) addDefer(f func(*Frame) Exception) {
*fm.defers = append(*fm.defers, f)
}
func (fm *Frame) runDefers() Exception {
var exc Exception
defers := *fm.defers
for i := len(defers) - 1; i >= 0; i-- {
exc2 := defers[i](fm)
// TODO: Combine exc and exc2 if both are not nil
if exc2 != nil && exc == nil {
exc = exc2
}
}
return exc
}