eval.go

Summary

Maintainability
A
0 mins
Test Coverage
// Copyright (c) 2020-2023 Ozan Hacıbekiroğlu.
// Use of this source code is governed by a MIT License
// that can be found in the LICENSE file.

package ugo

import (
    "context"
)

// Eval compiles and runs scripts within same scope.
// If executed script's return statement has no value to return or return is
// omitted, it returns last value on stack.
// Warning: Eval is not safe to use concurrently.
type Eval struct {
    Locals       []Object
    Globals      Object
    Opts         CompilerOptions
    VM           *VM
    ModulesCache []Object
}

// NewEval returns new Eval object.
func NewEval(opts CompilerOptions, globals Object, args ...Object) *Eval {
    if globals == nil {
        globals = Map{}
    }
    if opts.SymbolTable == nil {
        opts.SymbolTable = NewSymbolTable()
    }
    if opts.moduleStore == nil {
        opts.moduleStore = newModuleStore()
    }

    return &Eval{
        Locals:  args,
        Globals: globals,
        Opts:    opts,
        VM:      NewVM(nil).SetRecover(true),
    }
}

// Run compiles, runs given script and returns last value on stack.
func (r *Eval) Run(ctx context.Context, script []byte) (Object, *Bytecode, error) {
    bytecode, err := Compile(script, r.Opts)
    if err != nil {
        return nil, nil, err
    }

    bytecode.Main.NumParams = bytecode.Main.NumLocals
    r.Opts.Constants = bytecode.Constants
    r.fixOpPop(bytecode)
    r.VM.SetBytecode(bytecode)

    if ctx == nil {
        ctx = context.Background()
    }

    r.VM.modulesCache = r.ModulesCache
    ret, err := r.run(ctx)
    r.ModulesCache = r.VM.modulesCache
    r.Locals = r.VM.GetLocals(r.Locals)
    r.VM.Clear()

    if err != nil {
        return nil, bytecode, err
    }
    return ret, bytecode, nil
}

func (r *Eval) run(ctx context.Context) (ret Object, err error) {
    ret = Undefined
    doneCh := make(chan struct{})
    // Always check whether context is done before running VM because
    // parser and compiler may take longer than expected or context may be
    // canceled for any reason before run, so use two selects.
    select {
    case <-ctx.Done():
        r.VM.Abort()
        err = ctx.Err()
    default:
        go func() {
            defer close(doneCh)
            ret, err = r.VM.Run(r.Globals, r.Locals...)
        }()

        select {
        case <-ctx.Done():
            r.VM.Abort()
            <-doneCh
            if err == nil {
                err = ctx.Err()
            }
        case <-doneCh:
        }
    }
    return
}

// fixOpPop changes OpPop and OpReturn Opcodes to force VM to return last value on top of stack.
func (*Eval) fixOpPop(bytecode *Bytecode) {
    var prevOp byte
    var lastOp byte
    var fixPos int

    IterateInstructions(bytecode.Main.Instructions,
        func(pos int, opcode Opcode, operands []int, offset int) bool {
            if prevOp == 0 {
                prevOp = opcode
            } else {
                prevOp = lastOp
            }
            fixPos = -1
            lastOp = opcode
            if prevOp == OpPop && lastOp == OpReturn && operands[0] == 0 {
                fixPos = pos - 1
            }
            return true
        },
    )

    if fixPos > 0 {
        bytecode.Main.Instructions[fixPos] = OpNoOp // overwrite OpPop
        bytecode.Main.Instructions[fixPos+2] = 1    // set number of return to 1
    }
}