elves/elvish

View on GitHub
pkg/eval/closure.go

Summary

Maintainability
A
1 hr
Test Coverage
package eval

import (
    "fmt"
    "sort"
    "strconv"
    "strings"
    "unsafe"

    "src.elv.sh/pkg/diag"
    "src.elv.sh/pkg/eval/errs"
    "src.elv.sh/pkg/eval/vals"
    "src.elv.sh/pkg/eval/vars"
    "src.elv.sh/pkg/parse"
    "src.elv.sh/pkg/persistent/hash"
)

// Closure is a function defined with Elvish code. Each Closure has its unique
// identity.
type Closure struct {
    ArgNames []string
    // The index of the rest argument. -1 if there is no rest argument.
    RestArg     int
    OptNames    []string
    OptDefaults []any
    SrcMeta     parse.Source
    DefRange    diag.Ranging
    op          effectOp
    newLocal    []staticVarInfo
    captured    *Ns
}

var (
    _ Callable       = &Closure{}
    _ vals.PseudoMap = &Closure{}
)

// Kind returns "fn".
func (*Closure) Kind() string {
    return "fn"
}

// Equal compares by address.
func (c *Closure) Equal(rhs any) bool {
    return c == rhs
}

// Hash returns the hash of the address of the closure.
func (c *Closure) Hash() uint32 {
    return hash.Pointer(unsafe.Pointer(c))
}

// Call calls a closure.
func (c *Closure) Call(fm *Frame, args []any, opts map[string]any) error {
    // Check number of arguments.
    if c.RestArg != -1 {
        if len(args) < len(c.ArgNames)-1 {
            return errs.ArityMismatch{What: "arguments",
                ValidLow: len(c.ArgNames) - 1, ValidHigh: -1, Actual: len(args)}
        }
    } else {
        if len(args) != len(c.ArgNames) {
            return errs.ArityMismatch{What: "arguments",
                ValidLow: len(c.ArgNames), ValidHigh: len(c.ArgNames), Actual: len(args)}
        }
    }
    // Check whether all supplied options are supported. This map contains the
    // subset of keys from opts that can be found in c.OptNames.
    optSupported := make(map[string]struct{})
    for _, name := range c.OptNames {
        _, ok := opts[name]
        if ok {
            optSupported[name] = struct{}{}
        }
    }
    if len(optSupported) < len(opts) {
        // Report all the options that are not supported.
        unsupported := make([]string, 0, len(opts)-len(optSupported))
        for name := range opts {
            _, supported := optSupported[name]
            if !supported {
                unsupported = append(unsupported, parse.Quote(name))
            }
        }
        sort.Strings(unsupported)
        return UnsupportedOptionsError{unsupported}
    }

    // This Frame is dedicated to the current form, so we can modify it in place.

    // BUG(xiaq): When evaluating closures, async access to global variables
    // and ports can be problematic.

    // Make upvalue namespace and capture variables.
    fm.up = c.captured

    // Populate local scope with arguments, options, and newly created locals.
    localSize := len(c.ArgNames) + len(c.OptNames) + len(c.newLocal)
    local := &Ns{make([]vars.Var, localSize), make([]staticVarInfo, localSize)}

    for i, name := range c.ArgNames {
        local.infos[i] = staticVarInfo{name, false, false}
    }
    if c.RestArg == -1 {
        for i := range c.ArgNames {
            local.slots[i] = vars.FromInit(args[i])
        }
    } else {
        for i := 0; i < c.RestArg; i++ {
            local.slots[i] = vars.FromInit(args[i])
        }
        restOff := len(args) - len(c.ArgNames)
        local.slots[c.RestArg] = vars.FromInit(
            vals.MakeList(args[c.RestArg : c.RestArg+restOff+1]...))
        for i := c.RestArg + 1; i < len(c.ArgNames); i++ {
            local.slots[i] = vars.FromInit(args[i+restOff])
        }
    }

    offset := len(c.ArgNames)
    for i, name := range c.OptNames {
        v, ok := opts[name]
        if !ok {
            v = c.OptDefaults[i]
        }
        local.infos[offset+i] = staticVarInfo{name, false, false}
        local.slots[offset+i] = vars.FromInit(v)
    }

    offset += len(c.OptNames)
    for i, info := range c.newLocal {
        local.infos[offset+i] = info
        // TODO: Take info.readOnly into account too when creating variable
        local.slots[offset+i] = MakeVarFromName(info.name)
    }

    fm.local = local
    fm.srcMeta = c.SrcMeta
    fm.defers = new([]func(*Frame) Exception)
    exc := c.op.exec(fm)
    excDefer := fm.runDefers()
    // TODO: Combine exc and excDefer if both are not nil
    if excDefer != nil && exc == nil {
        exc = excDefer
    }
    return exc
}

// MakeVarFromName creates a Var with a suitable type constraint inferred from
// the name.
func MakeVarFromName(name string) vars.Var {
    switch {
    case strings.HasSuffix(name, FnSuffix):
        val := nopGoFn
        return vars.FromPtr(&val)
    case strings.HasSuffix(name, NsSuffix):
        val := &Ns{}
        return vars.FromPtr(&val)
    default:
        return vars.FromInit(nil)
    }
}

// UnsupportedOptionsError is an error returned by a closure call when there are
// unsupported options.
type UnsupportedOptionsError struct {
    Options []string
}

func (er UnsupportedOptionsError) Error() string {
    if len(er.Options) == 1 {
        return fmt.Sprintf("unsupported option: %s", er.Options[0])
    }
    return fmt.Sprintf("unsupported options: %s", strings.Join(er.Options, ", "))
}

func (c *Closure) Fields() vals.StructMap { return closureFields{c} }

type closureFields struct{ c *Closure }

func (closureFields) IsStructMap() {}

func (cf closureFields) ArgNames() vals.List { return vals.MakeListSlice(cf.c.ArgNames) }
func (cf closureFields) RestArg() string     { return strconv.Itoa(cf.c.RestArg) }
func (cf closureFields) OptNames() vals.List { return vals.MakeListSlice(cf.c.OptNames) }
func (cf closureFields) Src() parse.Source   { return cf.c.SrcMeta }

func (cf closureFields) OptDefaults() vals.List {
    return vals.MakeList(cf.c.OptDefaults...)
}

func (cf closureFields) Body() string {
    r := cf.c.op.(diag.Ranger).Range()
    return cf.c.SrcMeta.Code[r.From:r.To]
}

func (cf closureFields) Def() string {
    return cf.c.SrcMeta.Code[cf.c.DefRange.From:cf.c.DefRange.To]
}