compiler_nodes.go

Summary

Maintainability
F
4 days
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 (
    "github.com/ozanh/ugo/parser"
    "github.com/ozanh/ugo/token"
)

func (c *Compiler) compileIfStmt(node *parser.IfStmt) error {
    // open new symbol table for the statement
    c.symbolTable = c.symbolTable.Fork(true)
    defer func() {
        c.symbolTable = c.symbolTable.Parent(false)
    }()

    if node.Init != nil {
        if err := c.Compile(node.Init); err != nil {
            return err
        }
    }

    jumpPos1 := -1
    var skipElse bool
    if v, ok := node.Cond.(*parser.BoolLit); !ok {
        if err := c.Compile(node.Cond); err != nil {
            return err
        }

        // first jump placeholder
        jumpPos1 = c.emit(node, OpJumpFalsy, 0)
        if err := c.Compile(node.Body); err != nil {
            return err
        }
    } else if v.Value {
        if err := c.Compile(node.Body); err != nil {
            return err
        }
        skipElse = true
    } else {
        jumpPos1 = c.emit(node, OpJump, 0)
    }

    if !skipElse && node.Else != nil {
        // second jump placeholder
        jumpPos2 := c.emit(node, OpJump, 0)
        if jumpPos1 > -1 {
            // update first jump offset
            curPos := len(c.instructions)
            c.changeOperand(jumpPos1, curPos)
        }

        if err := c.Compile(node.Else); err != nil {
            return err
        }
        // update second jump offset
        curPos := len(c.instructions)
        c.changeOperand(jumpPos2, curPos)
    } else {
        if jumpPos1 > -1 {
            // update first jump offset
            curPos := len(c.instructions)
            c.changeOperand(jumpPos1, curPos)
        }
    }
    return nil
}

func (c *Compiler) compileTryStmt(node *parser.TryStmt) error {
    /*
        // create a single symbol table for try-catch-finally
        // any `return` statement in finally block ignores already thrown error.
        try {
            // emit: OpSetupTry (CatchPos, FinallyPos)

            // emit: OpJump (FinallyPos) // jump to finally block to skip catch block.
        } catch err {
            // emit: OpSetupCatch
            //
            // catch block is optional.
            // if err is elided  in `catch {}`, OpPop removes the error from stack.
            // catch pops the error from error handler, re-throw requires explicit
            // throw expression `throw err`.
        } finally {
            // emit: OpSetupFinally
            //
            // finally block is optional if catch block is defined but
            // instructions are always generated for finally block even if not explicitly defined
            // to cleanup symbols and re-throw error if not handled in catch block.
            //
            // emit: OpThrow 0 // this is implicit re-throw operation without putting stack trace
        }
    */
    // fork new symbol table for the statement
    c.symbolTable = c.symbolTable.Fork(true)
    c.tryCatchIndex++
    defer func() {
        c.symbolTable = c.symbolTable.Parent(false)
        c.emit(node, OpThrow, 0) // implicit re-throw
    }()

    optry := c.emit(node, OpSetupTry, 0, 0)
    var catchPos, finallyPos int
    if node.Body != nil && len(node.Body.Stmts) > 0 {
        // in order not to fork symbol table in Body, compile stmts here instead of in *BlockStmt
        for _, stmt := range node.Body.Stmts {
            if err := c.Compile(stmt); err != nil {
                return err
            }
        }
    }

    var opjump int
    if node.Catch != nil {
        // if there is no thrown error before catch statement, set catch ident to undefined
        // otherwise jumping to finally and accessing ident in finally access previous set same index variable.
        if node.Catch.Ident != nil {
            c.emit(node.Catch, OpNull)
            symbol, exists := c.symbolTable.DefineLocal(node.Catch.Ident.Name)
            if exists {
                c.emit(node, OpSetLocal, symbol.Index)
            } else {
                c.emit(node, OpDefineLocal, symbol.Index)
            }
        }

        opjump = c.emit(node, OpJump, 0)
        catchPos = len(c.instructions)
        if err := c.Compile(node.Catch); err != nil {
            return err
        }
    }

    c.tryCatchIndex--
    // always emit OpSetupFinally to cleanup
    if node.Finally != nil {
        finallyPos = c.emit(node.Finally, OpSetupFinally)
        if err := c.Compile(node.Finally); err != nil {
            return err
        }
    } else {
        finallyPos = c.emit(node, OpSetupFinally)
    }

    c.changeOperand(optry, catchPos, finallyPos)
    if node.Catch != nil {
        // no need jumping if catch is not defined
        c.changeOperand(opjump, finallyPos)
    }
    return nil
}

func (c *Compiler) compileCatchStmt(node *parser.CatchStmt) error {
    c.emit(node, OpSetupCatch)
    if node.Ident != nil {
        symbol, exists := c.symbolTable.DefineLocal(node.Ident.Name)
        if exists {
            c.emit(node, OpSetLocal, symbol.Index)
        } else {
            c.emit(node, OpDefineLocal, symbol.Index)
        }
    } else {
        c.emit(node, OpPop)
    }

    if node.Body == nil {
        return nil
    }

    // in order not to fork symbol table in Body, compile stmts here instead of in *BlockStmt
    for _, stmt := range node.Body.Stmts {
        if err := c.Compile(stmt); err != nil {
            return err
        }
    }
    return nil
}

func (c *Compiler) compileFinallyStmt(node *parser.FinallyStmt) error {
    if node.Body == nil {
        return nil
    }

    // in order not to fork symbol table in Body, compile stmts here instead of in *BlockStmt
    for _, stmt := range node.Body.Stmts {
        if err := c.Compile(stmt); err != nil {
            return err
        }
    }
    return nil
}

func (c *Compiler) compileThrowStmt(node *parser.ThrowStmt) error {
    if node.Expr != nil {
        if err := c.Compile(node.Expr); err != nil {
            return err
        }
    }
    c.emit(node, OpThrow, 1)
    return nil
}

func (c *Compiler) compileDeclStmt(node *parser.DeclStmt) error {
    decl := node.Decl.(*parser.GenDecl)
    if len(decl.Specs) == 0 {
        return c.errorf(node, "empty declaration not allowed")
    }

    switch decl.Tok {
    case token.Param:
        return c.compileDeclParam(decl)
    case token.Global:
        return c.compileDeclGlobal(decl)
    case token.Var, token.Const:
        return c.compileDeclValue(decl)
    }
    return nil
}

func (c *Compiler) compileDeclParam(node *parser.GenDecl) error {
    if c.symbolTable.parent != nil {
        return c.errorf(node, "param not allowed in this scope")
    }

    names := make([]string, 0, len(node.Specs))
    for _, sp := range node.Specs {
        spec := sp.(*parser.ParamSpec)
        names = append(names, spec.Ident.Name)
        if spec.Variadic {
            if c.variadic {
                return c.errorf(node,
                    "multiple variadic param declaration")
            }
            c.variadic = true
        }
    }

    if err := c.symbolTable.SetParams(names...); err != nil {
        return c.error(node, err)
    }
    return nil
}

func (c *Compiler) compileDeclGlobal(node *parser.GenDecl) error {
    if c.symbolTable.parent != nil {
        return c.errorf(node, "global not allowed in this scope")
    }

    for _, sp := range node.Specs {
        spec := sp.(*parser.ParamSpec)
        symbol, err := c.symbolTable.DefineGlobal(spec.Ident.Name)
        if err != nil {
            return c.error(node, err)
        }

        idx := c.addConstant(String(spec.Ident.Name))
        symbol.Index = idx
    }
    return nil
}

func (c *Compiler) compileDeclValue(node *parser.GenDecl) error {
    var (
        isConst  bool
        lastExpr parser.Expr
    )
    if node.Tok == token.Const {
        isConst = true
        defer func() { c.iotaVal = -1 }()
    }

    for _, sp := range node.Specs {
        spec := sp.(*parser.ValueSpec)
        if isConst {
            if v, ok := spec.Data.(int); ok {
                c.iotaVal = v
            } else {
                return c.errorf(node, "invalid iota value")
            }
        }
        for i, ident := range spec.Idents {
            leftExpr := []parser.Expr{ident}
            var v parser.Expr
            if i < len(spec.Values) {
                v = spec.Values[i]
            }

            if v == nil {
                if isConst && lastExpr != nil {
                    v = lastExpr
                } else {
                    v = &parser.UndefinedLit{TokenPos: ident.Pos()}
                }
            } else {
                lastExpr = v
            }

            rightExpr := []parser.Expr{v}
            err := c.compileAssignStmt(node, leftExpr, rightExpr, node.Tok, token.Define)
            if err != nil {
                return err
            }
        }
    }
    return nil
}

func (c *Compiler) checkAssignment(
    node parser.Node,
    lhs []parser.Expr,
    rhs []parser.Expr,
    keyword token.Token,
    op token.Token,
) (bool, error) {
    _, numRHS := len(lhs), len(rhs)
    if numRHS > 1 {
        return false, c.errorf(node,
            "multiple expressions on the right side not supported")
    }

    var selector bool
Loop:
    for _, expr := range lhs {
        switch expr.(type) {
        case *parser.SelectorExpr, *parser.IndexExpr:
            selector = true
            break Loop
        }
    }

    if selector {
        if op == token.Define {
            // using selector on new variable does not make sense
            return false, c.errorf(node, "operator ':=' not allowed with selector")
        }
    }

    return true, nil
}

func (c *Compiler) compileAssignStmt(
    node parser.Node,
    lhs []parser.Expr,
    rhs []parser.Expr,
    keyword token.Token,
    op token.Token,
) error {
    compile, err := c.checkAssignment(node, lhs, rhs, keyword, op)
    if err != nil || !compile {
        return err
    }

    var isArrDestruct bool
    var tempArrSymbol *Symbol
    // +=, -=, *=, /=
    if op != token.Assign && op != token.Define {
        if err := c.Compile(lhs[0]); err != nil {
            return err
        }
    } else if len(lhs) > 1 {
        isArrDestruct = true
        // ignore redefinition of :array symbol, it can be used multiple times
        // within a block.
        tempArrSymbol, _ = c.symbolTable.DefineLocal(":array")
        // ignore disabled builtins of symbol table for BuiltinMakeArray because
        // it is required to handle destructuring assignment.
        c.emit(node, OpGetBuiltin, int(BuiltinMakeArray))
        c.emit(node, OpConstant, c.addConstant(Int(len(lhs))))
    }

    // compile RHSs
    for _, expr := range rhs {
        if err := c.Compile(expr); err != nil {
            return err
        }
    }

    if isArrDestruct {
        return c.compileDestructuring(node, lhs, tempArrSymbol, keyword, op)
    }

    if op != token.Assign && op != token.Define {
        c.compileCompoundAssignment(node, op)
    }
    return c.compileDefineAssign(node, lhs[0], keyword, op, false)
}

func (c *Compiler) compileCompoundAssignment(
    node parser.Node,
    op token.Token,
) {
    switch op {
    case token.AddAssign:
        c.emit(node, OpBinaryOp, int(token.Add))
    case token.SubAssign:
        c.emit(node, OpBinaryOp, int(token.Sub))
    case token.MulAssign:
        c.emit(node, OpBinaryOp, int(token.Mul))
    case token.QuoAssign:
        c.emit(node, OpBinaryOp, int(token.Quo))
    case token.RemAssign:
        c.emit(node, OpBinaryOp, int(token.Rem))
    case token.AndAssign:
        c.emit(node, OpBinaryOp, int(token.And))
    case token.OrAssign:
        c.emit(node, OpBinaryOp, int(token.Or))
    case token.AndNotAssign:
        c.emit(node, OpBinaryOp, int(token.AndNot))
    case token.XorAssign:
        c.emit(node, OpBinaryOp, int(token.Xor))
    case token.ShlAssign:
        c.emit(node, OpBinaryOp, int(token.Shl))
    case token.ShrAssign:
        c.emit(node, OpBinaryOp, int(token.Shr))
    }
}

func (c *Compiler) compileDestructuring(
    node parser.Node,
    lhs []parser.Expr,
    tempArrSymbol *Symbol,
    keyword token.Token,
    op token.Token,
) error {
    c.emit(node, OpCall, 2, 0)
    c.emit(node, OpDefineLocal, tempArrSymbol.Index)
    numLHS := len(lhs)
    var found int

    for lhsIndex, expr := range lhs {
        if op == token.Define {
            if term, ok := expr.(*parser.Ident); ok {
                if _, ok = c.symbolTable.find(term.Name); ok {
                    found++
                }
            }
            if found == numLHS {
                return c.errorf(node, "no new variable on left side")
            }
        }

        c.emit(node, OpGetLocal, tempArrSymbol.Index)
        c.emit(node, OpConstant, c.addConstant(Int(lhsIndex)))
        c.emit(node, OpGetIndex, 1)
        err := c.compileDefineAssign(node, expr, keyword, op, keyword != token.Const)
        if err != nil {
            return err
        }
    }

    if !c.symbolTable.InBlock() {
        // blocks set undefined to variables defined in it after block
        c.emit(node, OpNull)
        c.emit(node, OpSetLocal, tempArrSymbol.Index)
    }
    return nil
}

func (c *Compiler) compileDefine(
    node parser.Node,
    ident string,
    allowRedefine bool,
    keyword token.Token,
) error {
    symbol, exists := c.symbolTable.DefineLocal(ident)
    if !allowRedefine && exists && ident != "_" {
        return c.errorf(node, "%q redeclared in this block", ident)
    }

    if symbol.Constant {
        return c.errorf(node, "assignment to constant variable %q", ident)
    }
    if c.iotaVal > -1 && ident == "iota" && keyword == token.Const {
        return c.errorf(node, "assignment to iota")
    }

    c.emit(node, OpDefineLocal, symbol.Index)
    symbol.Assigned = true
    symbol.Constant = keyword == token.Const && ident != "_"
    return nil
}

func (c *Compiler) compileAssign(
    node parser.Node,
    symbol *Symbol,
    ident string,
) error {
    if symbol.Constant {
        return c.errorf(node, "assignment to constant variable %q", ident)
    }

    switch symbol.Scope {
    case ScopeLocal:
        c.emit(node, OpSetLocal, symbol.Index)
        symbol.Assigned = true
    case ScopeFree:
        c.emit(node, OpSetFree, symbol.Index)
        symbol.Assigned = true
        s := symbol
        for s != nil {
            if s.Original != nil && s.Original.Scope == ScopeLocal {
                s.Original.Assigned = true
            }
            s = s.Original
        }
    case ScopeGlobal:
        c.emit(node, OpSetGlobal, symbol.Index)
        symbol.Assigned = true
    default:
        return c.errorf(node, "unresolved reference %q", ident)
    }
    return nil
}

func (c *Compiler) compileDefineAssign(
    node parser.Node,
    lhs parser.Expr,
    keyword token.Token,
    op token.Token,
    allowRedefine bool,
) error {
    ident, selectors := resolveAssignLHS(lhs)
    numSel := len(selectors)
    if numSel == 0 && op == token.Define {
        return c.compileDefine(node, ident, allowRedefine, keyword)
    }

    symbol, ok := c.symbolTable.Resolve(ident)
    if !ok {
        return c.errorf(node, "unresolved reference %q", ident)
    }

    if numSel == 0 {
        return c.compileAssign(node, symbol, ident)
    }

    // get indexes until last one and set the value to the last index
    switch symbol.Scope {
    case ScopeLocal:
        c.emit(node, OpGetLocal, symbol.Index)
    case ScopeFree:
        c.emit(node, OpGetFree, symbol.Index)
    case ScopeGlobal:
        c.emit(node, OpGetGlobal, symbol.Index)
    default:
        return c.errorf(node, "unexpected scope %q for symbol %q",
            symbol.Scope, ident)
    }

    if numSel > 1 {
        for i := 0; i < numSel-1; i++ {
            if err := c.Compile(selectors[i]); err != nil {
                return err
            }
        }
        c.emit(node, OpGetIndex, numSel-1)
    }

    if err := c.Compile(selectors[numSel-1]); err != nil {
        return err
    }

    c.emit(node, OpSetIndex)
    return nil
}

func resolveAssignLHS(expr parser.Expr) (name string, selectors []parser.Expr) {
    switch term := expr.(type) {
    case *parser.SelectorExpr:
        name, selectors = resolveAssignLHS(term.Expr)
        selectors = append(selectors, term.Sel)
    case *parser.IndexExpr:
        name, selectors = resolveAssignLHS(term.Expr)
        selectors = append(selectors, term.Index)
    case *parser.Ident:
        name = term.Name
    }
    return
}

func (c *Compiler) compileBranchStmt(node *parser.BranchStmt) error {
    switch node.Token {
    case token.Break:
        curLoop := c.currentLoop()
        if curLoop == nil {
            return c.errorf(node, "break not allowed outside loop")
        }

        var pos int
        if curLoop.lastTryCatchIndex == c.tryCatchIndex {
            pos = c.emit(node, OpJump, 0)
        } else {
            c.emit(node, OpFinalizer, curLoop.lastTryCatchIndex+1)
            pos = c.emit(node, OpJump, 0)
        }
        curLoop.breaks = append(curLoop.breaks, pos)
    case token.Continue:
        curLoop := c.currentLoop()
        if curLoop == nil {
            return c.errorf(node, "continue not allowed outside loop")
        }

        var pos int
        if curLoop.lastTryCatchIndex == c.tryCatchIndex {
            pos = c.emit(node, OpJump, 0)
        } else {
            c.emit(node, OpFinalizer, curLoop.lastTryCatchIndex+1)
            pos = c.emit(node, OpJump, 0)
        }
        curLoop.continues = append(curLoop.continues, pos)
    default:
        return c.errorf(node, "invalid branch statement: %s", node.Token.String())
    }
    return nil
}

func (c *Compiler) compileBlockStmt(node *parser.BlockStmt) error {
    if len(node.Stmts) == 0 {
        return nil
    }

    c.symbolTable = c.symbolTable.Fork(true)
    for _, stmt := range node.Stmts {
        if err := c.Compile(stmt); err != nil {
            return err
        }
    }

    c.symbolTable = c.symbolTable.Parent(false)
    return nil
}

func (c *Compiler) compileReturnStmt(node *parser.ReturnStmt) error {
    if node.Result == nil {
        if c.tryCatchIndex > -1 {
            c.emit(node, OpFinalizer, 0)
        }
        c.emit(node, OpReturn, 0)
        return nil
    }

    if err := c.Compile(node.Result); err != nil {
        return err
    }

    if c.tryCatchIndex > -1 {
        c.emit(node, OpFinalizer, 0)
    }

    c.emit(node, OpReturn, 1)
    return nil
}

func (c *Compiler) compileForStmt(stmt *parser.ForStmt) error {
    c.symbolTable = c.symbolTable.Fork(true)
    defer func() {
        c.symbolTable = c.symbolTable.Parent(false)
    }()

    // init statement
    if stmt.Init != nil {
        if err := c.Compile(stmt.Init); err != nil {
            return err
        }
    }

    // pre-condition position
    preCondPos := len(c.instructions)

    // condition expression
    postCondPos := -1
    if stmt.Cond != nil {
        if err := c.Compile(stmt.Cond); err != nil {
            return err
        }
        // condition jump position
        postCondPos = c.emit(stmt, OpJumpFalsy, 0)
    }

    // enter loop
    loop := c.enterLoop()

    // body statement
    if err := c.Compile(stmt.Body); err != nil {
        c.leaveLoop()
        return err
    }

    c.leaveLoop()

    // post-body position
    postBodyPos := len(c.instructions)

    // post statement
    if stmt.Post != nil {
        if err := c.Compile(stmt.Post); err != nil {
            return err
        }
    }

    // back to condition
    c.emit(stmt, OpJump, preCondPos)

    // post-statement position
    postStmtPos := len(c.instructions)
    if postCondPos >= 0 {
        c.changeOperand(postCondPos, postStmtPos)
    }

    // update all break/continue jump positions
    for _, pos := range loop.breaks {
        c.changeOperand(pos, postStmtPos)
    }

    for _, pos := range loop.continues {
        c.changeOperand(pos, postBodyPos)
    }
    return nil
}

func (c *Compiler) compileForInStmt(stmt *parser.ForInStmt) error {
    c.symbolTable = c.symbolTable.Fork(true)
    defer func() {
        c.symbolTable = c.symbolTable.Parent(false)
    }()

    // for-in statement is compiled like following:
    //
    //   for :it := iterator(iterable); :it.next();  {
    //     k, v := :it.get()  // set locals
    //
    //     ... body ...
    //   }
    //
    // ":it" is a local variable but it will not conflict with other user variables
    // because character ":" is not allowed in the variable names.

    // init
    //   :it = iterator(iterable)
    itSymbol, exists := c.symbolTable.DefineLocal(":it")
    if exists {
        return c.errorf(stmt, ":it redeclared in this block")
    }

    if err := c.Compile(stmt.Iterable); err != nil {
        return err
    }

    c.emit(stmt, OpIterInit)
    c.emit(stmt, OpDefineLocal, itSymbol.Index)

    // pre-condition position
    preCondPos := len(c.instructions)

    // condition
    //  :it.Next()
    c.emit(stmt, OpGetLocal, itSymbol.Index)
    c.emit(stmt, OpIterNext)

    // condition jump position
    postCondPos := c.emit(stmt, OpJumpFalsy, 0)

    // enter loop
    loop := c.enterLoop()

    // assign key variable
    if stmt.Key.Name != "_" {
        keySymbol, exists := c.symbolTable.DefineLocal(stmt.Key.Name)
        if exists {
            return c.errorf(stmt, "%q redeclared in this block", stmt.Key.Name)
        }
        c.emit(stmt, OpGetLocal, itSymbol.Index)
        c.emit(stmt, OpIterKey)
        keySymbol.Assigned = true
        c.emit(stmt, OpDefineLocal, keySymbol.Index)
    }

    // assign value variable
    if stmt.Value.Name != "_" {
        valueSymbol, exists := c.symbolTable.DefineLocal(stmt.Value.Name)
        if exists {
            return c.errorf(stmt, "%q redeclared in this block", stmt.Value.Name)
        }
        c.emit(stmt, OpGetLocal, itSymbol.Index)
        c.emit(stmt, OpIterValue)
        valueSymbol.Assigned = true
        c.emit(stmt, OpDefineLocal, valueSymbol.Index)
    }

    // body statement
    if err := c.Compile(stmt.Body); err != nil {
        c.leaveLoop()
        return err
    }

    c.leaveLoop()

    // post-body position
    postBodyPos := len(c.instructions)

    // back to condition
    c.emit(stmt, OpJump, preCondPos)

    // post-statement position
    postStmtPos := len(c.instructions)
    c.changeOperand(postCondPos, postStmtPos)

    // update all break/continue jump positions
    for _, pos := range loop.breaks {
        c.changeOperand(pos, postStmtPos)
    }

    for _, pos := range loop.continues {
        c.changeOperand(pos, postBodyPos)
    }
    return nil
}

func (c *Compiler) compileFuncLit(node *parser.FuncLit) error {
    params := make([]string, len(node.Type.Params.List))
    for i, ident := range node.Type.Params.List {
        params[i] = ident.Name
    }

    symbolTable := c.symbolTable.Fork(false)
    if err := symbolTable.SetParams(params...); err != nil {
        return c.error(node, err)
    }

    fork := c.fork(c.file, c.modulePath, c.moduleMap, symbolTable)
    fork.variadic = node.Type.Params.VarArgs
    if err := fork.Compile(node.Body); err != nil {
        return err
    }

    freeSymbols := fork.symbolTable.FreeSymbols()
    for _, s := range freeSymbols {
        switch s.Scope {
        case ScopeLocal:
            c.emit(node, OpGetLocalPtr, s.Index)
        case ScopeFree:
            c.emit(node, OpGetFreePtr, s.Index)
        }
    }

    bc := fork.Bytecode()
    if bc.Main.NumLocals > 256 {
        return c.error(node, ErrSymbolLimit)
    }

    c.constants = bc.Constants
    index := c.addConstant(bc.Main)

    if len(freeSymbols) > 0 {
        c.emit(node, OpClosure, index, len(freeSymbols))
    } else {
        c.emit(node, OpConstant, index)
    }
    return nil
}

func (c *Compiler) compileLogical(node *parser.BinaryExpr) error {
    // left side term
    if err := c.Compile(node.LHS); err != nil {
        return err
    }

    // jump position
    var jumpPos int
    if node.Token == token.LAnd {
        jumpPos = c.emit(node, OpAndJump, 0)
    } else {
        jumpPos = c.emit(node, OpOrJump, 0)
    }

    // right side term
    if err := c.Compile(node.RHS); err != nil {
        return err
    }
    c.changeOperand(jumpPos, len(c.instructions))
    return nil
}

func (c *Compiler) compileBinaryExpr(node *parser.BinaryExpr) error {
    if err := c.Compile(node.LHS); err != nil {
        return err
    }

    if err := c.Compile(node.RHS); err != nil {
        return err
    }

    switch node.Token {
    case token.Equal:
        c.emit(node, OpEqual)
    case token.NotEqual:
        c.emit(node, OpNotEqual)
    default:
        if !node.Token.IsBinaryOperator() {
            return c.errorf(node, "invalid binary operator: %s",
                node.Token.String())
        }
        c.emit(node, OpBinaryOp, int(node.Token))
    }
    return nil
}

func (c *Compiler) compileUnaryExpr(node *parser.UnaryExpr) error {
    if err := c.Compile(node.Expr); err != nil {
        return err
    }

    switch node.Token {
    case token.Not:
        c.emit(node, OpUnary, int(token.Not))
    case token.Sub:
        c.emit(node, OpUnary, int(token.Sub))
    case token.Xor:
        c.emit(node, OpUnary, int(token.Xor))
    case token.Add:
        c.emit(node, OpUnary, int(token.Add))
    default:
        return c.errorf(node,
            "invalid unary operator: %s", node.Token.String())
    }
    return nil
}

func (c *Compiler) compileSelectorExpr(node *parser.SelectorExpr) error {
    expr, selectors := resolveSelectorExprs(node)
    if err := c.Compile(expr); err != nil {
        return err
    }
    for _, selector := range selectors {
        if err := c.Compile(selector); err != nil {
            return err
        }
    }
    c.emit(node, OpGetIndex, len(selectors))
    return nil
}

func resolveSelectorExprs(node parser.Expr) (expr parser.Expr, selectors []parser.Expr) {
    expr = node
    if v, ok := node.(*parser.SelectorExpr); ok {
        expr, selectors = resolveIndexExprs(v.Expr)
        selectors = append(selectors, v.Sel)
    }
    return
}

func (c *Compiler) compileIndexExpr(node *parser.IndexExpr) error {
    expr, indexes := resolveIndexExprs(node)
    if err := c.Compile(expr); err != nil {
        return err
    }
    for _, index := range indexes {
        if err := c.Compile(index); err != nil {
            return err
        }
    }
    c.emit(node, OpGetIndex, len(indexes))
    return nil
}

func resolveIndexExprs(node parser.Expr) (expr parser.Expr, indexes []parser.Expr) {
    expr = node
    if v, ok := node.(*parser.IndexExpr); ok {
        expr, indexes = resolveIndexExprs(v.Expr)
        indexes = append(indexes, v.Index)
    }
    return
}

func (c *Compiler) compileSliceExpr(node *parser.SliceExpr) error {
    if err := c.Compile(node.Expr); err != nil {
        return err
    }

    if node.Low != nil {
        if err := c.Compile(node.Low); err != nil {
            return err
        }
    } else {
        c.emit(node, OpNull)
    }

    if node.High != nil {
        if err := c.Compile(node.High); err != nil {
            return err
        }
    } else {
        c.emit(node, OpNull)
    }

    c.emit(node, OpSliceIndex)
    return nil
}

func (c *Compiler) compileCallExpr(node *parser.CallExpr) error {
    var op = OpCall
    var selExpr *parser.SelectorExpr
    var isSelector bool
    if node.Func != nil {
        selExpr, isSelector = node.Func.(*parser.SelectorExpr)
    }

    if isSelector {
        if err := c.Compile(selExpr.Expr); err != nil {
            return err
        }
        op = OpCallName
    } else {
        if err := c.Compile(node.Func); err != nil {
            return err
        }
    }

    for _, arg := range node.Args {
        if err := c.Compile(arg); err != nil {
            return err
        }
    }

    if isSelector {
        if err := c.Compile(selExpr.Sel); err != nil {
            return err
        }
    }

    var expand int
    if node.Ellipsis.IsValid() {
        expand = 1
    }

    c.emit(node, op, len(node.Args), expand)
    return nil
}

func (c *Compiler) compileImportExpr(node *parser.ImportExpr) error {
    moduleName := node.ModuleName
    if moduleName == "" {
        return c.errorf(node, "empty module name")
    }

    importer := c.moduleMap.Get(moduleName)
    if importer == nil {
        return c.errorf(node, "module '%s' not found", moduleName)
    }

    extImp, isExt := importer.(ExtImporter)
    if isExt {
        if name := extImp.Name(); name != "" {
            moduleName = name
        }
    }

    module, exists := c.getModule(moduleName)
    if !exists {
        mod, err := importer.Import(moduleName)
        if err != nil {
            return c.error(node, err)
        }
        switch v := mod.(type) {
        case []byte:
            var moduleMap *ModuleMap
            if isExt {
                moduleMap = c.moduleMap.Fork(moduleName)
            } else {
                moduleMap = c.baseModuleMap()
            }
            cidx, err := c.compileModule(node, moduleName, moduleMap, v)
            if err != nil {
                return err
            }
            module = c.addModule(moduleName, 1, cidx)
        case Object:
            module = c.addModule(moduleName, 2, c.addConstant(v))
        default:
            return c.errorf(node, "invalid import value type: %T", v)
        }
    }

    switch module.typ {
    case 1:
        var numParams int
        mod := c.constants[module.constantIndex]
        if cf, ok := mod.(*CompiledFunction); ok {
            numParams = cf.NumParams
            if cf.Variadic {
                numParams--
            }
        }
        // load module
        // if module is already stored, load from VM.modulesCache otherwise call compiled function
        // and store copy of result to VM.modulesCache.
        c.emit(node, OpLoadModule, module.constantIndex, module.moduleIndex)
        jumpPos := c.emit(node, OpJumpFalsy, 0)
        // modules should not accept parameters, to suppress the wrong number of arguments error
        // set all params to undefined
        for i := 0; i < numParams; i++ {
            c.emit(node, OpNull)
        }
        c.emit(node, OpCall, numParams, 0)
        c.emit(node, OpStoreModule, module.moduleIndex)
        c.changeOperand(jumpPos, len(c.instructions))
    case 2:
        // load module
        // if module is already stored, load from VM.modulesCache otherwise copy object
        // and store it to VM.modulesCache.
        c.emit(node, OpLoadModule, module.constantIndex, module.moduleIndex)
        jumpPos := c.emit(node, OpJumpFalsy, 0)
        c.emit(node, OpStoreModule, module.moduleIndex)
        c.changeOperand(jumpPos, len(c.instructions))
    default:
        return c.errorf(node, "invalid module type: %v", module.typ)
    }
    return nil
}

func (c *Compiler) compileCondExpr(node *parser.CondExpr) error {
    if v, ok := node.Cond.(*parser.BoolLit); ok {
        if v.Value {
            return c.Compile(node.True)
        }
        return c.Compile(node.False)
    }

    if err := c.Compile(node.Cond); err != nil {
        return err
    }

    // first jump placeholder
    jumpPos1 := c.emit(node, OpJumpFalsy, 0)
    if err := c.Compile(node.True); err != nil {
        return err
    }

    // second jump placeholder
    jumpPos2 := c.emit(node, OpJump, 0)

    // update first jump offset
    curPos := len(c.instructions)
    c.changeOperand(jumpPos1, curPos)
    if err := c.Compile(node.False); err != nil {
        return err
    }
    // update second jump offset
    curPos = len(c.instructions)
    c.changeOperand(jumpPos2, curPos)
    return nil
}

func (c *Compiler) compileIdent(node *parser.Ident) error {
    symbol, ok := c.symbolTable.Resolve(node.Name)
    if !ok {
        if c.iotaVal < 0 || node.Name != "iota" {
            return c.errorf(node, "unresolved reference %q", node.Name)
        }
        c.emit(node, OpConstant, c.addConstant(Int(c.iotaVal)))
        return nil
    }

    switch symbol.Scope {
    case ScopeGlobal:
        c.emit(node, OpGetGlobal, symbol.Index)
    case ScopeLocal:
        c.emit(node, OpGetLocal, symbol.Index)
    case ScopeBuiltin:
        c.emit(node, OpGetBuiltin, symbol.Index)
    case ScopeFree:
        c.emit(node, OpGetFree, symbol.Index)
    }
    return nil
}

func (c *Compiler) compileArrayLit(node *parser.ArrayLit) error {
    for _, elem := range node.Elements {
        if err := c.Compile(elem); err != nil {
            return err
        }
    }

    c.emit(node, OpArray, len(node.Elements))
    return nil
}

func (c *Compiler) compileMapLit(node *parser.MapLit) error {
    for _, elt := range node.Elements {
        // key
        c.emit(node, OpConstant, c.addConstant(String(elt.Key)))
        // value
        if err := c.Compile(elt.Value); err != nil {
            return err
        }
    }

    c.emit(node, OpMap, len(node.Elements)*2)
    return nil
}