ucl/ucl/eval.go
Leon Mika 03e6878524
Some checks failed
Build / build (push) Failing after 1m35s
Fixed nil panic
2025-05-27 21:21:10 +10:00

454 lines
11 KiB
Go

package ucl
import (
"context"
"errors"
"fmt"
"strings"
)
type evaluator struct {
inst *Inst
}
func (e evaluator) evalBlock(ctx context.Context, ec *evalCtx, n *astBlock) (lastRes Object, err error) {
// TODO: push scope?
for _, s := range n.Statements {
lastRes, err = e.evalStatement(ctx, ec, s)
if err != nil {
return nil, err
}
}
return lastRes, nil
}
func (e evaluator) evalScript(ctx context.Context, ec *evalCtx, n *astScript) (lastRes Object, err error) {
return e.evalStatement(ctx, ec, n.Statements)
}
func (e evaluator) evalStatement(ctx context.Context, ec *evalCtx, n *astStatements) (Object, error) {
if n == nil {
return nil, nil
}
res, err := e.evalAssignOrPipeline(ctx, ec, n.First)
if err != nil {
return nil, err
}
if len(n.Rest) == 0 {
return res, nil
}
for _, rest := range n.Rest {
out, err := e.evalAssignOrPipeline(ctx, ec, rest)
if err != nil {
return nil, err
}
res = out
}
return res, nil
}
func (e evaluator) evalAssignOrPipeline(ctx context.Context, ec *evalCtx, n *astAssignOrPipeline) (Object, error) {
switch {
case n.Assign != nil:
// Assignment
assignVal, err := e.evalPipeline(ctx, ec, n.Assign)
if err != nil {
return nil, err
}
return e.assignCmd(ctx, ec, n.First, assignVal)
case len(n.Pipeline) > 0:
res, err := e.evalCmd(ctx, ec, nil, n.First)
if err != nil {
return nil, err
}
for _, rest := range n.Pipeline {
out, err := e.evalCmd(ctx, ec, res, rest)
if err != nil {
return nil, err
}
res = out
}
return res, nil
}
return e.evalCmd(ctx, ec, nil, n.First)
}
func (e evaluator) evalPipeline(ctx context.Context, ec *evalCtx, n *astPipeline) (Object, error) {
res, err := e.evalCmd(ctx, ec, nil, n.First)
if err != nil {
return nil, err
}
if len(n.Rest) == 0 {
return res, nil
}
// Command is a pipeline, so build it out
for _, rest := range n.Rest {
out, err := e.evalCmd(ctx, ec, res, rest)
if err != nil {
return nil, err
}
res = out
}
return res, nil
}
func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentPipe Object, ast *astCmd) (Object, error) {
switch {
case (ast.Name.Arg.Ident != nil) && len(ast.Name.DotSuffix) == 0:
name := ast.Name.Arg.Ident.String()
// Regular command
if cmd := ec.lookupInvokable(name); cmd != nil {
return e.evalInvokable(ctx, ec, currentPipe, ast, cmd)
} else if macro := ec.lookupMacro(name); macro != nil {
return e.evalMacro(ctx, ec, currentPipe != nil, currentPipe, ast, macro)
} else if missingHandler := e.inst.missingBuiltinHandler; missingHandler != nil {
return e.evalInvokable(ctx, ec, currentPipe, ast, e.inst.missingHandlerInvokable(name))
} else {
return nil, errors.New("unknown command: " + name)
}
case len(ast.InvokeArgs) > 0:
nameElem, err := e.evalDot(ctx, ec, ast.Name)
if err != nil {
return nil, err
}
inv, ok := nameElem.(invokable)
if !ok {
return nil, errors.New("command is not invokable")
}
return e.evalInvokable(ctx, ec, currentPipe, ast, inv)
}
nameElem, err := e.evalDot(ctx, ec, ast.Name)
if err != nil {
return nil, err
}
return nameElem, nil
}
func (e evaluator) assignCmd(ctx context.Context, ec *evalCtx, ast *astCmd, toVal Object) (Object, error) {
if len(ast.InvokeArgs) != 0 {
return nil, errors.New("cannot assign to multiple values")
}
return e.assignDot(ctx, ec, ast.Name, toVal)
}
func (e evaluator) evalInvokable(ctx context.Context, ec *evalCtx, currentPipe Object, ast *astCmd, cmd invokable) (Object, error) {
var (
pargs ListObject
kwargs map[string]*ListObject
argsPtr *ListObject
)
argsPtr = &pargs
if currentPipe != nil {
argsPtr.Append(currentPipe)
}
for _, arg := range ast.InvokeArgs {
if ident := arg.Arg.Ident; len(arg.DotSuffix) == 0 && ident != nil && ident.String()[0] == '-' {
// Arg switch
if kwargs == nil {
kwargs = make(map[string]*ListObject)
}
argsPtr = &ListObject{}
kwargs[ident.String()[1:]] = argsPtr
} else {
ae, err := e.evalDot(ctx, ec, arg)
if err != nil {
return nil, err
}
argsPtr.Append(ae)
}
}
invArgs := invocationArgs{eval: e, ec: ec, inst: e.inst, args: pargs, kwargs: kwargs}
return cmd.invoke(ctx, invArgs)
}
func (e evaluator) evalMacro(ctx context.Context, ec *evalCtx, hasPipe bool, pipeArg Object, ast *astCmd, cmd macroable) (Object, error) {
return cmd.invokeMacro(ctx, macroArgs{
eval: e,
ec: ec,
hasPipe: hasPipe,
pipeArg: pipeArg,
ast: ast,
})
}
func (e evaluator) evalDot(ctx context.Context, ec *evalCtx, n astDot) (Object, error) {
res, err := e.evalArg(ctx, ec, n.Arg)
if err != nil {
return nil, err
} else if len(n.DotSuffix) == 0 {
return res, nil
}
for _, dot := range n.DotSuffix {
var idx Object
if dot.KeyIdent != nil {
idx = StringObject(dot.KeyIdent.String())
} else {
idx, err = e.evalPipeline(ctx, ec, dot.Pipeline)
if err != nil {
return nil, err
}
}
res, err = indexLookup(ctx, res, idx)
if err != nil {
return nil, err
}
}
return res, nil
}
func (e evaluator) assignDot(ctx context.Context, ec *evalCtx, n astDot, toVal Object) (Object, error) {
if len(n.DotSuffix) == 0 {
return e.assignArg(ctx, ec, n.Arg, toVal)
}
return nil, errors.New("TODO")
}
func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Object, error) {
switch {
case n.Literal != nil:
return e.evalLiteral(ctx, ec, n.Literal)
case n.Ident != nil:
return StringObject(n.Ident.String()), nil
case n.Var != nil:
if v, ok := ec.getVar(*n.Var); ok {
return v, nil
}
return nil, nil
case n.PseudoVar != nil:
if v, ok := ec.getPseudoVar(*n.PseudoVar); ok {
return v.get(ctx, *n.PseudoVar)
}
if mph := e.inst.missingPseudoVarHandler; mph != nil {
return mph.get(ctx, *n.PseudoVar)
}
return nil, errors.New("unknown pseudo-variable: " + *n.PseudoVar)
case n.MaybeSub != nil:
sub := n.MaybeSub.Sub
if sub == nil {
return nil, nil
}
return e.evalSub(ctx, ec, sub)
case n.ListOrHash != nil:
return e.evalListOrHash(ctx, ec, n.ListOrHash)
case n.Block != nil:
return blockObject{block: n.Block, closedEC: ec}, nil
}
return nil, errors.New("unhandled arg type")
}
func (e evaluator) assignArg(ctx context.Context, ec *evalCtx, n astCmdArg, toVal Object) (Object, error) {
switch {
case n.Literal != nil:
// We may use this for variable setting?
return nil, errors.New("cannot assign to a literal")
case n.Var != nil:
ec.setOrDefineVar(*n.Var, toVal)
return toVal, nil
case n.PseudoVar != nil:
pvar, ok := ec.getPseudoVar(*n.PseudoVar)
if ok {
if err := pvar.set(ctx, *n.PseudoVar, toVal); err != nil {
return nil, err
}
return toVal, nil
}
if pvar := e.inst.missingPseudoVarHandler; pvar != nil {
if err := pvar.set(ctx, *n.PseudoVar, toVal); err != nil {
return nil, err
}
return toVal, nil
}
return nil, errors.New("unknown pseudo-variable: " + *n.Var)
case n.MaybeSub != nil:
return nil, errors.New("cannot assign to a subexpression")
case n.ListOrHash != nil:
return nil, errors.New("cannot assign to a list or hash")
case n.Block != nil:
return nil, errors.New("cannot assign to a block")
}
return nil, errors.New("unhandled arg type")
}
func (e evaluator) evalListOrHash(ctx context.Context, ec *evalCtx, loh *astListOrHash) (Object, error) {
if loh.EmptyList {
return &ListObject{}, nil
} else if loh.EmptyHash {
return hashObject{}, nil
}
if firstIsHash := loh.Elements[0].Right != nil; firstIsHash {
h := hashObject{}
for _, el := range loh.Elements {
if el.Right == nil {
return nil, errors.New("miss-match of lists and hash")
}
n, err := e.evalDot(ctx, ec, el.Left)
if err != nil {
return nil, err
}
v, err := e.evalDot(ctx, ec, *el.Right)
if err != nil {
return nil, err
}
h[n.String()] = v
}
return h, nil
}
l := ListObject{}
for _, el := range loh.Elements {
if el.Right != nil {
return nil, errors.New("miss-match of lists and hash")
}
v, err := e.evalDot(ctx, ec, el.Left)
if err != nil {
return nil, err
}
l = append(l, v)
}
return &l, nil
}
func (e evaluator) evalLiteral(ctx context.Context, ec *evalCtx, n *astLiteral) (Object, error) {
switch {
case n.StrInter != nil:
sval, err := e.interpolateDoubleQuotedString(ctx, ec, n.StrInter)
if err != nil {
return nil, err
}
return sval, nil
case n.SingleStrInter != nil:
sval, err := e.interpolateSingleQuotedString(ctx, ec, n.SingleStrInter)
if err != nil {
return nil, err
}
return sval, nil
case n.Int != nil:
return IntObject(*n.Int), nil
}
return nil, errors.New("unhandled literal type")
}
func (e evaluator) interpolateSingleQuotedString(ctx context.Context, ec *evalCtx, s *astSingleString) (Object, error) {
var sb strings.Builder
for _, n := range s.Spans {
switch {
case n.Chars != nil:
sb.WriteString(*n.Chars)
}
}
return StringObject(sb.String()), nil
}
func (e evaluator) interpolateDoubleQuotedString(ctx context.Context, ec *evalCtx, s *astDoubleString) (Object, error) {
var sb strings.Builder
for _, n := range s.Spans {
switch {
case n.Chars != nil:
sb.WriteString(*n.Chars)
case n.Escaped != nil:
switch (*n.Escaped)[1:] {
case "\\":
sb.WriteByte('\\')
case "n":
sb.WriteByte('\n')
case "t":
sb.WriteByte('\t')
case "$":
sb.WriteByte('$')
default:
return nil, errors.New("unrecognised escaped pattern: \\" + *n.Escaped)
}
case n.IdentRef != nil:
identVal := (*n.IdentRef)[1:]
if v, ok := ec.getVar(identVal); ok && v != nil {
sb.WriteString(v.String())
}
case n.LongIdentRef != nil:
v, err := e.interpolateLongIdent(ctx, ec, n.LongIdentRef)
if err != nil {
return nil, err
}
sb.WriteString(v)
case n.SubExpr != nil:
res, err := e.evalPipeline(ctx, ec, n.SubExpr)
if err != nil {
return nil, err
}
if res != nil {
sb.WriteString(res.String())
}
}
}
return StringObject(sb.String()), nil
}
func (e evaluator) interpolateLongIdent(ctx context.Context, ec *evalCtx, n *astLongIdent) (_ string, err error) {
res, ok := ec.getVar(n.VarName)
if !ok {
return "", nil
}
for _, dot := range n.DotSuffix {
if res == nil {
return "", errorWithPos{fmt.Errorf("attempt to get field from nil value '%v'", n.VarName), n.Pos}
}
var idx Object
if dot.KeyName != nil {
idx = StringObject(*dot.KeyName)
} else {
idx, err = e.evalPipeline(ctx, ec, dot.Pipeline)
if err != nil {
return "", err
}
}
res, err = indexLookup(ctx, res, idx)
if err != nil {
return "", err
}
}
if res == nil {
return "", nil
}
return res.String(), nil
}
func (e evaluator) evalSub(ctx context.Context, ec *evalCtx, n *astPipeline) (Object, error) {
pipelineRes, err := e.evalPipeline(ctx, ec, n)
if err != nil {
return nil, err
}
return pipelineRes, nil
}
type pseudoVar interface {
get(ctx context.Context, name string) (Object, error)
set(ctx context.Context, name string, v Object) error
}