package org.jruby.ir.interpreter;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.jruby.Ruby;
import org.jruby.RubyModule;
import org.jruby.ast.Node;
import org.jruby.ast.RootNode;
import org.jruby.exceptions.RaiseException;
import org.jruby.exceptions.Unrescuable;
import org.jruby.ir.Counter;
import org.jruby.ir.IRBuilder;
import org.jruby.ir.IRClosure;
import org.jruby.ir.IREvalScript;
import org.jruby.ir.IRMethod;
import org.jruby.ir.IRScope;
import org.jruby.ir.IRScriptBody;
import org.jruby.ir.Operation;
import org.jruby.ir.instructions.BreakInstr;
import org.jruby.ir.instructions.CallBase;
import org.jruby.ir.instructions.CheckArityInstr;
import org.jruby.ir.instructions.CopyInstr;
import org.jruby.ir.instructions.Instr;
import org.jruby.ir.instructions.JumpInstr;
import org.jruby.ir.instructions.LineNumberInstr;
import org.jruby.ir.instructions.NonlocalReturnInstr;
import org.jruby.ir.instructions.ReceiveExceptionInstr;
import org.jruby.ir.instructions.ReceivePreReqdArgInstr;
import org.jruby.ir.instructions.ReceiveOptArgBase;
import org.jruby.ir.instructions.ReceiveRestArgBase;
import org.jruby.ir.instructions.ResultInstr;
import org.jruby.ir.instructions.ReturnBase;
import org.jruby.ir.instructions.RuntimeHelperCall;
import org.jruby.ir.instructions.ruby19.ReceivePostReqdArgInstr;
import org.jruby.ir.operands.IRException;
import org.jruby.ir.operands.LocalVariable;
import org.jruby.ir.operands.Operand;
import org.jruby.ir.operands.TemporaryVariable;
import org.jruby.ir.operands.Variable;
import org.jruby.ir.operands.WrappedIRClosure;
import org.jruby.ir.representations.BasicBlock;
import org.jruby.ir.runtime.IRRuntimeHelpers;
import org.jruby.ir.runtime.IRBreakJump;
import org.jruby.ir.runtime.IRReturnJump;
import org.jruby.parser.IRStaticScope;
import org.jruby.parser.IRStaticScopeFactory;
import org.jruby.parser.StaticScope;
import org.jruby.runtime.Block;
import org.jruby.runtime.DynamicScope;
import org.jruby.runtime.RubyEvent;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.log.Logger;
import org.jruby.util.log.LoggerFactory;
import org.jruby.runtime.CallSite;
import org.jruby.runtime.callsite.CachingCallSite;
import org.jruby.runtime.callsite.CacheEntry;
import org.jruby.internal.runtime.methods.InterpretedIRMethod;
import org.jruby.internal.runtime.methods.DynamicMethod;
import org.jruby.util.unsafe.UnsafeFactory;

public class Interpreter {
    private static final Logger LOG = LoggerFactory.getLogger("Interpreter");

    private static int inlineCount = 0;
    private static int interpInstrsCount = 0;
    private static int codeModificationsCount = 0;
    private static int numCyclesWithNoModifications = 0;
    private static int globalThreadPollCount = 0;
    private static HashMap<IRScope, Counter> scopeThreadPollCounts = new HashMap<IRScope, Counter>();

    private static IRScope getEvalContainerScope(Ruby runtime, StaticScope evalScope) {
        // SSS FIXME: Weirdness here.  We cannot get the containing IR scope from evalScope because of static-scope wrapping
        // that is going on
        // 1. In all cases, DynamicScope.getEvalScope wraps the executing static scope in a new local scope.
        // 2. For instance-eval (module-eval, class-eval) scenarios, there is an extra scope that is added to
        //    the stack in ThreadContext.java:preExecuteUnder
        // I dont know what rule to apply when.  However, in both these cases, since there is no IR-scope associated,
        // I have used the hack below where I first unwrap once and see if I get a non-null IR scope.  If that doesn't
        // work, I unwarp once more and I am guaranteed to get the IR scope I want.
        IRScope containingIRScope = ((IRStaticScope)evalScope.getEnclosingScope()).getIRScope();
        if (containingIRScope == null) containingIRScope = ((IRStaticScope)evalScope.getEnclosingScope().getEnclosingScope()).getIRScope();
        return containingIRScope;
    }

    public static IRubyObject interpretCommonEval(Ruby runtime, String file, int lineNumber, String backtraceName, RootNode rootNode, IRubyObject self, Block block) {
        // SSS FIXME: Is this required here since the IR version cannot change from eval-to-eval? This is much more of a global setting.
        boolean is_1_9 = runtime.is1_9();
        if (is_1_9) IRBuilder.setRubyVersion("1.9");

        StaticScope ss = rootNode.getStaticScope();
        IRScope containingIRScope = getEvalContainerScope(runtime, ss);
        IREvalScript evalScript = IRBuilder.createIRBuilder(runtime.getIRManager(), is_1_9).buildEvalRoot(ss, containingIRScope, file, lineNumber, rootNode);
        evalScript.prepareForInterpretation(false);
//        evalScript.runCompilerPass(new CallSplitter());
        ThreadContext context = runtime.getCurrentContext();
        runBeginEndBlocks(evalScript.getBeginBlocks(), context, self, null); // FIXME: No temp vars yet right?
        IRubyObject rv = evalScript.call(context, self, evalScript.getStaticScope().getModule(), rootNode.getScope(), block, backtraceName);
        runBeginEndBlocks(evalScript.getEndBlocks(), context, self, null); // FIXME: No temp vars right?
        return rv;
    }

    public static IRubyObject interpretSimpleEval(Ruby runtime, String file, int lineNumber, String backtraceName, Node node, IRubyObject self) {
        return interpretCommonEval(runtime, file, lineNumber, backtraceName, (RootNode)node, self, Block.NULL_BLOCK);
    }

    public static IRubyObject interpretBindingEval(Ruby runtime, String file, int lineNumber, String backtraceName, Node node, IRubyObject self, Block block) {
        return interpretCommonEval(runtime, file, lineNumber, backtraceName, (RootNode)node, self, block);
    }

    public static void runBeginEndBlocks(List<IRClosure> beBlocks, ThreadContext context, IRubyObject self, Object[] temp) {
        if (beBlocks == null) return;

        for (IRClosure b: beBlocks) {
            // SSS FIXME: Should I piggyback on WrappedIRClosure.retrieve or just copy that code here?
            b.prepareForInterpretation(false);
            Block blk = (Block)(new WrappedIRClosure(b)).retrieve(context, self, context.getCurrentScope(), temp);
            blk.yield(context, null);
        }
    }

    public static IRubyObject interpret(Ruby runtime, Node rootNode, IRubyObject self) {
        if (runtime.is1_9()) IRBuilder.setRubyVersion("1.9");

        IRScriptBody root = (IRScriptBody) IRBuilder.createIRBuilder(runtime.getIRManager(), runtime.is1_9()).buildRoot((RootNode) rootNode);

        // We get the live object ball rolling here.  This give a valid value for the top
        // of this lexical tree.  All new scope can then retrieve and set based on lexical parent.
        if (root.getStaticScope().getModule() == null) { // If an eval this may already be setup.
            root.getStaticScope().setModule(runtime.getObject());
        }

        RubyModule currModule = root.getStaticScope().getModule();

        // Scope state for root?
        IRStaticScopeFactory.newIRLocalScope(null).setModule(currModule);
        ThreadContext context = runtime.getCurrentContext();

        try {
            runBeginEndBlocks(root.getBeginBlocks(), context, self, null); // FIXME: No temp vars yet...not needed?
            InterpretedIRMethod method = new InterpretedIRMethod(root, currModule);
            IRubyObject rv =  method.call(context, self, currModule, "(root)", IRubyObject.NULL_ARRAY);
            runBeginEndBlocks(root.getEndBlocks(), context, self, null); // FIXME: No temp vars yet...not needed?
            if (IRRuntimeHelpers.isDebug() || IRRuntimeHelpers.inProfileMode()) LOG.info("-- Interpreted instructions: {}", interpInstrsCount);
            return rv;
        } catch (IRBreakJump bj) {
            throw IRException.BREAK_LocalJumpError.getException(context.runtime);
        }
    }

    private static void analyzeProfile() {
        //if (inlineCount == 2) return;

        if (codeModificationsCount == 0) numCyclesWithNoModifications++;
        else numCyclesWithNoModifications = 0;

        codeModificationsCount = 0;

        if (numCyclesWithNoModifications < 3) return;

        // We are now good to go -- start analyzing the profile
        ArrayList<IRScope> scopes = new ArrayList<IRScope>(scopeThreadPollCounts.keySet());
        Collections.sort(scopes, new java.util.Comparator<IRScope> () {
            @Override
            public int compare(IRScope a, IRScope b) {
                float aCount = scopeThreadPollCounts.get(a).count;
                float bCount = scopeThreadPollCounts.get(b).count;
                if (aCount == bCount) return 0;
                return (aCount < bCount) ? 1 : -1;
            }
        });

        // Find top N scopes
        Set<IRScope> hotScopes = new HashSet<IRScope>();
        int i = 0;
        float f = 0.0f;
        for (IRScope s: scopes) {
            long sCount = scopeThreadPollCounts.get(s).count;

            // If the scope is accounting for very little additional execution, exit!
            float sPerc = ((sCount*1000)/globalThreadPollCount)/10.0f;
            if (sPerc < 1) {
                Instr[] instrs = s.getInstrsForInterpretation();
                if (instrs == null) continue; // can happen if a previously inlined method hasn't been rebuilt

                // Allow smaller methods to inline more liberally
                if (instrs.length > (5 + sPerc * 10)) continue;
            }

            //System.out.println("Hot scope: " + s + "; %contribution: " + sPerc + "; cumulative: " + (f + sPerc));
            hotScopes.add(s);

            f += sPerc;
            i++;
            if (i == 50 || f >= 99.0) break;
        }

        // Identify inlining sites
        // Heuristic: In hot methods, identify monomorphic call sites to hot methods
        boolean revisitScope = false;
        Iterator<IRScope> hsIter = hotScopes.iterator();
        IRScope hs = null;
        while (hsIter.hasNext()) {
            if (!revisitScope) hs = hsIter.next();
            revisitScope = false;

            boolean skip = false;
            boolean isHotClosure = hs instanceof IRClosure;
            IRScope hc = isHotClosure ? hs : null;
            hs = isHotClosure ? hs.getLexicalParent() : hs;
            for (BasicBlock b : hs.getCFG().getBasicBlocks()) {
                for (Instr instr : b.getInstrs()) {
                    if ((instr instanceof CallBase) && !((CallBase)instr).inliningBlocked()) {
                        // System.out.println("checking: " + instr);
                        CallBase call = (CallBase)instr;
                        CallSite cs = call.getCallSite();
                        // System.out.println("callsite: " + cs);
                        if (cs != null && (cs instanceof CachingCallSite)) {
                            CachingCallSite ccs = (CachingCallSite)cs;
                            // SSS FIXME: To use this, CachingCallSite.java needs modification
                            // isPolymorphic or something equivalent needs to be enabled there.
                            if (ccs.isOptimizable()) {
                                CacheEntry ce = ccs.getCache();
                                DynamicMethod tgt = ce.method;
                                if (tgt instanceof InterpretedIRMethod) {
                                    InterpretedIRMethod dynMeth = (InterpretedIRMethod)tgt;
                                    IRScope tgtMethod = dynMeth.getIRMethod();
                                    Instr[] instrs = tgtMethod.getInstrsForInterpretation();
                                    // Dont inline large methods -- 200 is arbitrary
                                    // Can be null if a previously inlined method hasn't been rebuilt
                                    if ((instrs == null) || instrs.length > 150) continue;

                                    RubyModule implClass = dynMeth.getImplementationClass();
                                    int classToken = implClass.getGeneration();
                                    String n = tgtMethod.getName();
                                    boolean inlineCall = false;
                                    if (isHotClosure) {
                                        Operand clArg = call.getClosureArg(null);
                                        inlineCall = (clArg instanceof WrappedIRClosure) && (((WrappedIRClosure)clArg).getClosure() == hc);
                                    } else if (hotScopes.contains(tgtMethod)) {
                                        inlineCall = true;
                                    }

                                    if (inlineCall) {
                                        System.out.println("Inlining " + tgtMethod + " in " + hs + " @ instr " + instr);

                                        hs.inlineMethod(tgtMethod, implClass, classToken, b, call);
                                        // reset tp counters
                                        scopeThreadPollCounts.remove(isHotClosure ? hc : hs);
                                        scopeThreadPollCounts.remove(tgtMethod);
                                        inlineCount++;
                                        skip = true;
                                        revisitScope = true;

                                        break;
                                        //return;
                                    }
                                }
                            }
                        }
                    }
                }

                // We skip the rest of the method because we will run into concurrent modification exceptions in the iterators
                // SSS FIXME: We may miss some inlining sites because of this
                if (skip) break;
            }
        }
    }

    private static void outputProfileStats() {
        ArrayList<IRScope> scopes = new ArrayList<IRScope>(scopeThreadPollCounts.keySet());
        Collections.sort(scopes, new java.util.Comparator<IRScope> () {
            @Override
            public int compare(IRScope a, IRScope b) {
                // In non-methods and non-closures, we may not have any thread poll instrs.
                int aden = a.getThreadPollInstrsCount();
                if (aden == 0) aden = 1;
                int bden = b.getThreadPollInstrsCount();
                if (bden == 0) bden = 1;

                // Use estimated instr count to order scopes -- rather than raw thread-poll count
                float aCount = scopeThreadPollCounts.get(a).count * (1.0f * a.getInstrsForInterpretation().length/aden);
                float bCount = scopeThreadPollCounts.get(b).count * (1.0f * b.getInstrsForInterpretation().length/bden);
                if (aCount == bCount) return 0;
                return (aCount < bCount) ? 1 : -1;
            }
        });


        LOG.info("------------------------");
        LOG.info("Stats after " + globalThreadPollCount + " thread polls:");
        LOG.info("------------------------");
        LOG.info("# instructions: " + interpInstrsCount);
        LOG.info("# code modifications in this period : " + codeModificationsCount);
        LOG.info("------------------------");
        int i = 0;
        float f1 = 0.0f;
        for (IRScope s: scopes) {
            long n = scopeThreadPollCounts.get(s).count;
            float p1 =  ((n*1000)/globalThreadPollCount)/10.0f;
            String msg = i + ". " + s + " [file:" + s.getFileName() + ":" + s.getLineNumber() + "] = " + n + "; (" + p1 + "%)";
            if (s instanceof IRClosure) {
                IRMethod m = s.getNearestMethod();
                if (m != null) LOG.info(msg + " -- nearest enclosing method: " + m);
                else LOG.info(msg + " -- no enclosing method --");
            } else {
                LOG.info(msg);
            }
            i++;
            f1 += p1;

            // Top 20 or those that account for 95% of thread poll events.
            if (i == 20 || f1 >= 95.0) break;
        }

        // reset code modification counter
        codeModificationsCount = 0;

        // Every 1M thread polls, discard stats by reallocating the thread-poll count map
        if (globalThreadPollCount % 1000000 == 0)  {
            System.out.println("---- resetting thread-poll counters ----");
            scopeThreadPollCounts = new HashMap<IRScope, Counter>();
            globalThreadPollCount = 0;
        }
    }

    private static IRubyObject interpret(ThreadContext context, IRubyObject self,
            IRScope scope, Visibility visibility, RubyModule implClass, IRubyObject[] args, Block block, Block.Type blockType) {
        boolean debug = IRRuntimeHelpers.isDebug();
        boolean profile = IRRuntimeHelpers.inProfileMode();
        Instr[] instrs = scope.getInstrsForInterpretation();

        // The base IR may not have been processed yet
        if (instrs == null) instrs = scope.prepareForInterpretation(blockType == Block.Type.LAMBDA);

        int temporaryVariablesSize = scope.getTemporaryVariableSize();
        Object[] temp = temporaryVariablesSize > 0 ? new Object[temporaryVariablesSize] : null;
        int n   = instrs.length;
        int ipc = 0;
        Instr instr = null;
        Object exception = null;
        Ruby runtime = context.runtime;
        DynamicScope currDynScope = context.getCurrentScope();

        // Set up thread-poll counter for this scope
        Counter tpCount = null;
        if (profile) {
            tpCount = scopeThreadPollCounts.get(scope);
            if (tpCount == null) {
                tpCount = new Counter();
                scopeThreadPollCounts.put(scope, tpCount);
            }
        }

        // Enter the looooop!
        while (ipc < n) {
            instr = instrs[ipc];
            ipc++;
            Operation operation = instr.getOperation();

            if (debug) {
                LOG.info("I: {}", instr);
               interpInstrsCount++;
            } else if (profile) {
                if (operation.modifiesCode()) codeModificationsCount++;
               interpInstrsCount++;
            }

            try {
                Variable resultVar = null;
                Object result = null;
                switch(operation) {

                // ----------- Control-transfer instructions -------------
                case JUMP: {
                    ipc = ((JumpInstr)instr).getJumpTarget().getTargetPC();
                    break;
                }
                case MODULE_GUARD:
                case JUMP_INDIRECT:
                case B_TRUE:
                case B_FALSE:
                case B_NIL:
                case B_UNDEF:
                case BEQ:
                case BNE: {
                    ipc = instr.interpretAndGetNewIPC(context, currDynScope, self, temp, ipc);
                    break;
                }

                // ------------- Arg-receive instructions ------------
                case RECV_PRE_REQD_ARG: {
                    ReceivePreReqdArgInstr ra = (ReceivePreReqdArgInstr)instr;
                    int argIndex = ra.getArgIndex();
                    result = (argIndex < args.length) ? args[argIndex] : context.nil; // SSS FIXME: This check is only required for closures, not methods
                    resultVar = ra.getResult();
                    break;
                }
                case RECV_POST_REQD_ARG: {
                    ReceivePostReqdArgInstr ra = (ReceivePostReqdArgInstr)instr;
                    result = ra.receivePostReqdArg(args);
                    if (result == null) result = context.nil; // For blocks
                    resultVar = ra.getResult();
                    break;
                }
                case RECV_OPT_ARG: {
                    ReceiveOptArgBase ra = (ReceiveOptArgBase)instr;
                    result = ra.receiveOptArg(args);
                    resultVar = ra.getResult();
                    break;
                }
                case RECV_REST_ARG: {
                    ReceiveRestArgBase ra = (ReceiveRestArgBase)instr;
                    result = ra.receiveRestArg(runtime, args);
                    resultVar = ra.getResult();
                    break;
                }
                case RECV_CLOSURE: {
                    result = (block == Block.NULL_BLOCK) ? context.nil : runtime.newProc(Block.Type.PROC, block);
                    resultVar = ((ResultInstr)instr).getResult();
                    break;
                }
                case RECV_EXCEPTION: {
                    ReceiveExceptionInstr rei = (ReceiveExceptionInstr)instr;
                    result = (exception instanceof RaiseException && rei.checkType) ? ((RaiseException)exception).getException() : exception;
                    resultVar = rei.getResult();
                    break;
                }

                // --------- Return flavored instructions --------
                case BREAK: {
                    BreakInstr bi = (BreakInstr)instr;
                    IRubyObject rv = (IRubyObject)bi.getReturnValue().retrieve(context, self, currDynScope, temp);
                    // This also handles breaks in lambdas -- by converting them to a return
                    return IRRuntimeHelpers.initiateBreak(context, scope, bi.getScopeToReturnTo(), rv, blockType);
                }
                case RETURN: {
                    return (IRubyObject)((ReturnBase)instr).getReturnValue().retrieve(context, self, currDynScope, temp);
                }
                case NONLOCAL_RETURN: {
                    NonlocalReturnInstr ri = (NonlocalReturnInstr)instr;
                    IRubyObject rv = (IRubyObject)ri.getReturnValue().retrieve(context, self, currDynScope, temp);
                    ipc = n;
                    // If not in a lambda, check if this was a non-local return
                    if (!IRRuntimeHelpers.inLambda(blockType)) {
                        IRRuntimeHelpers.initiateNonLocalReturn(context, scope, ri.methodToReturnFrom, rv);
                    }
                    return rv;
                }

                // --------- Bookkeeping instructions --------
                case CHECK_ARITY: {
                    ((CheckArityInstr)instr).checkArity(runtime, args.length);
                    break;
                }
                case PUSH_FRAME: {
                    context.preMethodFrameAndClass(implClass, scope.getName(), self, block, scope.getStaticScope());
                    context.setCurrentVisibility(visibility);
                    break;
                }
                case PUSH_BINDING: {
                    // SSS NOTE: Method scopes only!
                    //
                    // Blocks are a headache -- so, these instrs. are only added to IRMethods.
                    // Blocks have more complicated logic for pushing a dynamic scope (see InterpretedIRBlockBody)
                    currDynScope = DynamicScope.newDynamicScope(scope.getStaticScope());
                    context.pushScope(currDynScope);
                    break;
                }
                case POP_FRAME: {
                    context.popFrame();
                    context.popRubyClass();
                    break;
                }
                case POP_BINDING: {
                    context.popScope();
                    break;
                }
                case THREAD_POLL: {
                    if (profile) {
                        tpCount.count++;
                        globalThreadPollCount++;
                        // SSS: Uncomment this to analyze profile
                        // Every 10K profile counts, spit out profile stats
                        // if (globalThreadPollCount % 10000 == 0) analyzeProfile(); //outputProfileStats();
                    }
                    context.callThreadPoll();
                    break;
                }
                case LINE_NUM: {
                    context.setLine(((LineNumberInstr)instr).lineNumber);
                    break;
                }
                case RUNTIME_HELPER: {
                    resultVar = ((ResultInstr)instr).getResult();
                    result = ((RuntimeHelperCall)instr).callHelper(context, currDynScope, self, temp, scope, blockType);
                    break;
                }

                // ---------- Common instruction ---------
                case COPY: {
                    CopyInstr c = (CopyInstr)instr;
                    result = c.getSource().retrieve(context, self, currDynScope, temp);
                    resultVar = ((ResultInstr)instr).getResult();
                    break;
                }

                // ---------- All the rest ---------
                default:
                    if (instr instanceof ResultInstr) resultVar = ((ResultInstr)instr).getResult();
                    result = instr.interpret(context, currDynScope, self, temp, block);
                    break;
                }

                if (resultVar != null) {
                    if (resultVar instanceof TemporaryVariable) {
                        temp[((TemporaryVariable)resultVar).offset] = result;
                    }
                    else {
                        LocalVariable lv = (LocalVariable)resultVar;
                        currDynScope.setValue((IRubyObject) result, lv.getLocation(), lv.getScopeDepth());
                    }
                }
            } catch (Throwable t) {
                // Unrescuable:
                //    IRReturnJump, ThreadKill, RubyContinuation, MainExitException, etc.
                //    These cannot be rescued -- only run ensure blocks
                //
                // Others:
                //    IRBreakJump, Ruby exceptions, errors, and other java exceptions.
                //    These can be rescued -- run rescue blocks

                if (debug) LOG.info("in scope: " + scope + ", caught Java throwable: " + t + "; excepting instr: " + instr);
                ipc = (t instanceof Unrescuable) ? scope.getEnsurerPC(instr) : scope.getRescuerPC(instr);
                if (debug) LOG.info("ipc for rescuer/ensurer: " + ipc);

                if (ipc == -1) {
                    UnsafeFactory.getUnsafe().throwException((Throwable)t);
                } else {
                    exception = t;
                }
            }
        }

        // Control should never get here!
        // SSS FIXME: But looks like BEGIN/END blocks get here -- needs fixing
        return null;
    }

    public static IRubyObject INTERPRET_EVAL(ThreadContext context, IRubyObject self,
            IRScope scope, RubyModule clazz, IRubyObject[] args, String name, Block block, Block.Type blockType) {
        try {
            ThreadContext.pushBacktrace(context, name, scope.getFileName(), context.getLine());
            return interpret(context, self, scope, null, clazz, args, block, blockType);
        } finally {
            ThreadContext.popBacktrace(context);
        }
    }

    public static IRubyObject INTERPRET_BLOCK(ThreadContext context, IRubyObject self,
            IRScope scope, IRubyObject[] args, String name, Block block, Block.Type blockType) {
        try {
            ThreadContext.pushBacktrace(context, name, scope.getFileName(), context.getLine());
            return interpret(context, self, scope, null, null, args, block, blockType);
        } finally {
            ThreadContext.popBacktrace(context);
        }
    }

    public static IRubyObject INTERPRET_METHOD(ThreadContext context, InterpretedIRMethod irMethod,
        IRubyObject self, String name, IRubyObject[] args, Block block, Block.Type blockType, boolean isTraceable) {
        Ruby       runtime   = context.runtime;
        IRScope    scope     = irMethod.getIRMethod();
        RubyModule implClass = irMethod.getImplementationClass();
        Visibility viz       = irMethod.getVisibility();
        boolean syntheticMethod = name == null || name.equals("");

        try {
            if (!syntheticMethod) ThreadContext.pushBacktrace(context, name, scope.getFileName(), context.getLine());
            if (isTraceable) methodPreTrace(runtime, context, name, implClass);
            return interpret(context, self, scope, viz, implClass, args, block, blockType);
        } finally {
            if (isTraceable) {
                try {methodPostTrace(runtime, context, name, implClass);}
                finally { if (!syntheticMethod) ThreadContext.popBacktrace(context);}
            } else {
                if (!syntheticMethod) ThreadContext.popBacktrace(context);
            }
        }
    }

    private static void methodPreTrace(Ruby runtime, ThreadContext context, String name, RubyModule implClass) {
        if (runtime.hasEventHooks()) context.trace(RubyEvent.CALL, name, implClass);
    }

    private static void methodPostTrace(Ruby runtime, ThreadContext context, String name, RubyModule implClass) {
        if (runtime.hasEventHooks()) context.trace(RubyEvent.RETURN, name, implClass);
    }
}
