void XDebugProfiler::collectFrameData(FrameData& frameData, const TypedValue* retVal) { VMRegAnchor _; // Ensure consistent state for vmfp and vmpc ActRec* fp = vmfp(); bool is_func_begin = retVal == nullptr; frameData.is_func_begin = is_func_begin; // The function reference and call file/line are stored when tracing/profiling // on function enter if ((m_tracingEnabled || m_profilingEnabled) && is_func_begin) { frameData.func = fp->func(); // Need the previous frame in order to get the call line. If we cannot // get the previous frame, default to 1 Offset offset; const ActRec* prevFp = g_context->getPrevVMState(fp, &offset); if (prevFp != nullptr) { frameData.line = prevFp->unit()->getLineNumber(offset); } else { frameData.line = 1; } } else { frameData.func = nullptr; frameData.line = 1; } // Time is stored if profiling, tracing, or collect_time is enabled, but it // only needs to be collected on function exit if profiling or if computerized // tracing output is enabled if (m_profilingEnabled || (is_func_begin && (m_collectTime || m_tracingEnabled)) || (m_tracingEnabled && (m_tracingOpts & k_XDEBUG_TRACE_COMPUTERIZED))) { frameData.time = Timer::GetCurrentTimeMicros(); } else { frameData.time = 0; } // Memory usage is stored on function begin if tracing, or if collect_memory // is enabled, or on function end if computerized tracing output is enabled if ((is_func_begin && (m_tracingEnabled || m_collectMemory)) || (m_tracingEnabled && (m_tracingOpts & k_XDEBUG_TRACE_COMPUTERIZED))) { frameData.memory_usage = MM().getStats().usage; } else { frameData.memory_usage = 0; } // If tracing is enabled, we may need to collect a serialized version of // the arguments or the return value. if (m_tracingEnabled && is_func_begin && XDEBUG_GLOBAL(CollectParams) > 0) { // TODO(#3704) This relies on xdebug_var_dump throw_not_implemented("Tracing with collect_params enabled"); } else if (m_tracingEnabled && !is_func_begin && XDEBUG_GLOBAL(CollectReturn)) { // TODO(#3704) This relies on xdebug_var_dump throw_not_implemented("Tracing with collect_return enabled"); } else { frameData.context_str = nullptr; } }
void phpDebuggerStepIn() { // If this is called in the middle of a flow command we short-circuit the // other commands auto& req_data = RID(); req_data.setDebuggerStepIn(true); req_data.setDebuggerStepOut(StepOutState::NONE); req_data.setDebuggerNext(false); // Ensure the flow filter is fresh auto flow_filter = getFlowFilter(); flow_filter->clear(); // Check if the site is valid. VMRegAnchor _; ActRec* fp = vmfp(); PC pc = vmpc(); if (fp == nullptr || pc == nullptr) { TRACE(5, "Could not grab stack or program counter\n"); return; } // Try to get needed context info. Bail if we can't const Func* func = fp->func(); const Unit* unit = func != nullptr ? func->unit() : nullptr; if (func == nullptr || func == nullptr) { TRACE(5, "Could not grab the current unit or function\n"); return; } // We use line1 here because it works better than line0 in our // bytecode-source mapping. int line; SourceLoc source_loc; if (unit->getSourceLoc(unit->offsetOf(pc), source_loc)) { line = source_loc.line1; } else { TRACE(5, "Could not grab the current line number\n"); return; } TRACE(3, "Prepare location filter for %s:%d, unit %p:\n", unit->filepath()->data(), line, unit); // Get offset ranges for the whole line. OffsetRangeVec ranges; if (!unit->getOffsetRanges(line, ranges)) { ranges.clear(); } flow_filter->addRanges(unit, ranges); }
c_Generator::~c_Generator() { if (LIKELY(getState() == State::Done)) { return; } assert(getState() != State::Running); tvRefcountedDecRef(m_key); tvRefcountedDecRef(m_value); // Free locals, but don't trigger the EventHook for FunctionReturn since // the generator has already been exited. We don't want redundant calls. ActRec* ar = actRec(); frame_free_locals_inl_no_hook<false>(ar, ar->func()->numLocals()); }
int64_t HHVM_FUNCTION(func_num_args) { EagerCallerFrame cf; ActRec* ar = cf.actRecForArgs(); if (ar == nullptr) { return -1; } if (ar->func()->isPseudoMain()) { raise_warning( "func_num_args(): Called from the global scope - no function context" ); return -1; } return ar->numArgs(); }
ALWAYS_INLINE static int64_t func_num_args_impl() { EagerCallerFrame cf; ActRec* ar = cf.actRecForArgs(); if (ar == nullptr) { return -1; } if (ar->func()->isPseudoMain()) { raise_warning( "func_num_args(): Called from the global scope - no function context" ); return -1; } return ar->numArgs(); }
ALWAYS_INLINE static Variant func_get_arg_impl(int arg_num) { CallerFrame cf; ActRec* ar = cf.actRecForArgs(); if (ar == nullptr) { return false; } if (ar->func()->isPseudoMain()) { raise_warning( "func_get_arg(): Called from the global scope - no function context" ); return false; } if (arg_num < 0) { raise_warning( "func_get_arg(): The argument number should be >= 0" ); return false; } if (arg_num >= ar->numArgs()) { raise_warning( "func_get_arg(): Argument %d not passed to function", arg_num ); return false; } const int numParams = ar->m_func->numNonVariadicParams(); if (arg_num < numParams) { // Formal parameter. Value is on the stack. TypedValue* loc = (TypedValue*)(uintptr_t(ar) - (arg_num + 1) * sizeof(TypedValue)); return tvAsVariant(loc); } const int numArgs = ar->numArgs(); const int extraArgs = numArgs - numParams; // Not a formal parameter. Value is potentially in the // ExtraArgs/VarEnv. const int extraArgNum = arg_num - numParams; if (extraArgNum < extraArgs) { return tvAsVariant(ar->getExtraArg(extraArgNum)); } return false; }
File* GlobStreamWrapper::open(const String& filename, const String& mode, int options, CVarRef context) { // Can't open a glob as a file, it's meant to be opened as a directory // if the function was called via FCallBuiltin, we'll get a bogus name as // the stack frame will be wrong ActRec* ar = g_vmContext->getStackFrame(); const char* fn = (ar != nullptr) ? ar->func()->name()->data() : "OPTIMIZED_BUILTIN"; raise_warning("%s(%s): failed to open stream: " "wrapper does not support stream open", fn, filename.data()); return nullptr; }
void XDebugProfiler::enableTracing(const String& filename, int64_t opts) { assert(!m_tracingEnabled); // Attempt to open the passed filename. php5 xdebug doesn't enable tracing // if we cannot open the file, so we need to open it now as opposed to when we // actually do the writing in order to ensure we handle this case. We keep the // file handle open in order to ensure we can still write on tracing stop FILE* file; if (opts & k_XDEBUG_TRACE_APPEND) { file = fopen(filename.data(), "a"); } else { file = fopen(filename.data(), "w"); } // If file is null, opening the passed filename failed. php5 xdebug doesn't // do anything in this case, but we should probably notify the user if (file == nullptr) { raise_warning("xdebug profiler failed to open tracing file %s for writing.", filename.data()); return; } m_tracingEnabled = true; m_tracingStartIdx = m_nextFrameIdx; m_tracingFilename = filename; m_tracingFile = file; m_tracingOpts = opts; // If we're not at the top level, need to grab the call sites for each frame // on the stack. VMRegAnchor _; Offset offset; ActRec* fp = vmfp(); while ((fp = g_context->getPrevVMState(fp, &offset)) != nullptr) { FrameData frame; frame.func = fp->func(); frame.line = fp->unit()->getLineNumber(offset); m_tracingStartFrameData.push_back(frame); } }
bool EventHook::RunInterceptHandler(ActRec* ar) { const Func* func = ar->func(); if (LIKELY(func->maybeIntercepted() == 0)) return true; // Intercept only original generator / async function calls, not resumption. if (ar->resumed()) return true; Variant* h = get_intercept_handler(func->fullNameStr(), &func->maybeIntercepted()); if (!h) return true; /* * In production mode, only functions that we have assumed can be * intercepted during static analysis should actually be * intercepted. */ if (RuntimeOption::RepoAuthoritative && !RuntimeOption::EvalJitEnableRenameFunction) { if (!(func->attrs() & AttrInterceptable)) { raise_error("fb_intercept was used on a non-interceptable function (%s) " "in RepoAuthoritative mode", func->fullName()->data()); } } VMRegAnchor _; PC savePc = vmpc(); Variant doneFlag = true; Variant called_on; if (ar->hasThis()) { called_on = Variant(ar->getThis()); } else if (ar->hasClass()) { // For static methods, give handler the name of called class called_on = Variant(const_cast<StringData*>(ar->getClass()->name())); } Variant intArgs = PackedArrayInit(5) .append(VarNR(ar->func()->fullName())) .append(called_on) .append(get_frame_args_with_ref(ar)) .append(h->asCArrRef()[1]) .appendRef(doneFlag) .toArray(); Variant ret = vm_call_user_func(h->asCArrRef()[0], intArgs); if (doneFlag.toBoolean()) { Offset pcOff; ActRec* outer = g_context->getPrevVMState(ar, &pcOff); frame_free_locals_inl_no_hook<true>(ar, ar->func()->numLocals()); Stack& stack = vmStack(); stack.top() = (Cell*)(ar + 1); cellDup(*ret.asCell(), *stack.allocTV()); vmfp() = outer; vmpc() = outer ? outer->func()->unit()->at(pcOff) : nullptr; return false; } vmfp() = ar; vmpc() = savePc; return true; }
Array createBacktrace(const BacktraceArgs& btArgs) { auto bt = Array::Create(); // If there is a parser frame, put it at the beginning of the backtrace. if (btArgs.m_parserFrame) { bt.append( make_map_array( s_file, btArgs.m_parserFrame->filename, s_line, btArgs.m_parserFrame->lineNumber ) ); } VMRegAnchor _; // If there are no VM frames, we're done. if (!rds::header() || !vmfp()) return bt; int depth = 0; ActRec* fp = nullptr; Offset pc = 0; // Get the fp and pc of the top frame (possibly skipping one frame). if (btArgs.m_skipTop) { fp = getPrevActRec(vmfp(), &pc); // We skipped over the only VM frame, we're done. if (!fp) return bt; } else { fp = vmfp(); auto const unit = fp->func()->unit(); assert(unit); pc = unit->offsetOf(vmpc()); } // Handle the top frame. if (btArgs.m_withSelf) { // Builtins don't have a file and line number. if (!fp->func()->isBuiltin()) { auto const unit = fp->func()->unit(); assert(unit); auto const filename = fp->func()->filename(); ArrayInit frame(btArgs.m_parserFrame ? 4 : 2, ArrayInit::Map{}); frame.set(s_file, Variant{const_cast<StringData*>(filename)}); frame.set(s_line, unit->getLineNumber(pc)); if (btArgs.m_parserFrame) { frame.set(s_function, s_include); frame.set(s_args, Array::Create(btArgs.m_parserFrame->filename)); } bt.append(frame.toVariant()); depth++; } } // Handle the subsequent VM frames. Offset prevPc = 0; for (auto prevFp = getPrevActRec(fp, &prevPc); fp != nullptr && (btArgs.m_limit == 0 || depth < btArgs.m_limit); fp = prevFp, pc = prevPc, prevFp = getPrevActRec(fp, &prevPc)) { // Do not capture frame for HPHP only functions. if (fp->func()->isNoInjection()) continue; ArrayInit frame(7, ArrayInit::Map{}); auto const curUnit = fp->func()->unit(); auto const curOp = *reinterpret_cast<const Op*>(curUnit->at(pc)); auto const isReturning = curOp == Op::RetC || curOp == Op::RetV || curOp == Op::CreateCont || curOp == Op::Await || fp->localsDecRefd(); // Builtins and generators don't have a file and line number if (prevFp && !prevFp->func()->isBuiltin()) { auto const prevUnit = prevFp->func()->unit(); auto prevFile = prevUnit->filepath(); if (prevFp->func()->originalFilename()) { prevFile = prevFp->func()->originalFilename(); } assert(prevFile); frame.set(s_file, Variant{const_cast<StringData*>(prevFile)}); // In the normal method case, the "saved pc" for line number printing is // pointing at the cell conversion (Unbox/Pop) instruction, not the call // itself. For multi-line calls, this instruction is associated with the // subsequent line which results in an off-by-n. We're subtracting one // in order to look up the line associated with the FCall/FCallArray // instruction. Exception handling and the other opcodes (ex. BoxR) // already do the right thing. The emitter associates object access with // the subsequent expression and this would be difficult to modify. auto const opAtPrevPc = *reinterpret_cast<const Op*>(prevUnit->at(prevPc)); Offset pcAdjust = 0; if (opAtPrevPc == Op::PopR || opAtPrevPc == Op::UnboxR || opAtPrevPc == Op::UnboxRNop) { pcAdjust = 1; } frame.set(s_line, prevFp->func()->unit()->getLineNumber(prevPc - pcAdjust)); } // Check for include. String funcname{const_cast<StringData*>(fp->func()->name())}; if (fp->func()->isClosureBody()) { // Strip the file hash from the closure name. String fullName{const_cast<StringData*>(fp->func()->baseCls()->name())}; funcname = fullName.substr(0, fullName.find(';')); } // Check for pseudomain. if (funcname.empty()) { if (!prevFp && !btArgs.m_withPseudoMain) continue; else if (!prevFp) funcname = s_main; else funcname = s_include; } frame.set(s_function, funcname); if (!funcname.same(s_include)) { // Closures have an m_this but they aren't in object context. auto ctx = arGetContextClass(fp); if (ctx != nullptr && !fp->func()->isClosureBody()) { frame.set(s_class, Variant{const_cast<StringData*>(ctx->name())}); if (fp->hasThis() && !isReturning) { if (btArgs.m_withThis) { frame.set(s_object, Object(fp->getThis())); } frame.set(s_type, s_arrow); } else { frame.set(s_type, s_double_colon); } } } bool const mayUseVV = fp->func()->attrs() & AttrMayUseVV; auto const withNames = btArgs.m_withArgNames; auto const withValues = btArgs.m_withArgValues; if (!btArgs.m_withArgNames && !btArgs.m_withArgValues) { // do nothing } else if (funcname.same(s_include)) { if (depth != 0) { auto filepath = const_cast<StringData*>(curUnit->filepath()); frame.set(s_args, make_packed_array(filepath)); } } else if (!RuntimeOption::EnableArgsInBacktraces || isReturning) { // Provide an empty 'args' array to be consistent with hphpc. frame.set(s_args, empty_array()); } else { auto args = Array::Create(); auto const nparams = fp->func()->numNonVariadicParams(); auto const nargs = fp->numArgs(); auto const nformals = std::min<int>(nparams, nargs); if (UNLIKELY(mayUseVV) && UNLIKELY(fp->hasVarEnv() && fp->getVarEnv()->getFP() != fp)) { // VarEnv is attached to eval or debugger frame, other than the current // frame. Access locals thru VarEnv. auto varEnv = fp->getVarEnv(); auto func = fp->func(); for (int i = 0; i < nformals; i++) { auto const argname = func->localVarName(i); auto const tv = varEnv->lookup(argname); Variant val; if (tv != nullptr) { // the variable hasn't been unset val = withValues ? tvAsVariant(tv) : ""; } if (withNames) { args.set(String(const_cast<StringData*>(argname)), val); } else { args.append(val); } } } else { for (int i = 0; i < nformals; i++) { Variant val = withValues ? tvAsVariant(frame_local(fp, i)) : ""; if (withNames) { auto const argname = fp->func()->localVarName(i); args.set(String(const_cast<StringData*>(argname)), val); } else { args.append(val); } } } // Builtin extra args are not stored in varenv. if (UNLIKELY(mayUseVV) && nargs > nparams && fp->hasExtraArgs()) { for (int i = nparams; i < nargs; i++) { auto arg = fp->getExtraArg(i - nparams); args.append(tvAsVariant(arg)); } } frame.set(s_args, args); } if (btArgs.m_withMetadata && !isReturning) { if (UNLIKELY(mayUseVV) && UNLIKELY(fp->hasVarEnv())) { auto tv = fp->getVarEnv()->lookup(s_86metadata.get()); if (tv != nullptr && tv->m_type != KindOfUninit) { frame.set(s_metadata, tvAsVariant(tv)); } } else { auto local = fp->func()->lookupVarId(s_86metadata.get()); if (local != kInvalidId) { auto tv = frame_local(fp, local); if (tv->m_type != KindOfUninit) { frame.set(s_metadata, tvAsVariant(tv)); } } } } bt.append(frame.toVariant()); depth++; } return bt; }
bool UrlFile::open(const String& input_url, const String& mode) { String url = input_url; const char* modestr = mode.c_str(); if (strchr(modestr, '+') || strchr(modestr, 'a') || strchr(modestr, 'w')) { std::string msg = "cannot open a url stream for write/append operation: "; msg += url.c_str(); m_error = msg; return false; } HttpClient http(m_timeout, m_maxRedirect); auto ctx = this->getStreamContext(); if (ctx) { http.setStreamContextOptions(ctx->getOptions()); } m_response.clear(); if (!m_proxyHost.empty()) { http.proxy(m_proxyHost, m_proxyPort, m_proxyUsername, m_proxyPassword); } HeaderMap *pHeaders = nullptr; HeaderMap requestHeaders; if (!m_headers.empty()) { pHeaders = &requestHeaders; for (ArrayIter iter(m_headers); iter; ++iter) { requestHeaders[std::string(iter.first().toString().data())]. push_back(iter.second().toString().data()); } } Variant user = f_parse_url(url, k_PHP_URL_USER); if (user.isString()) { Variant pass = f_parse_url(url, k_PHP_URL_PASS); http.auth(user.toString().c_str(), pass.toString().c_str()); url = HHVM_FN(preg_replace)( s_remove_user_pass_pattern, s_remove_user_pass_replace, url, 1 ).toString(); } int code; std::vector<String> responseHeaders; if (m_get) { code = http.get(url.c_str(), m_response, pHeaders, &responseHeaders); } else { code = http.request(m_method, url.c_str(), m_postData.data(), m_postData.size(), m_response, pHeaders, &responseHeaders); } m_responseHeaders.reset(); for (unsigned int i = 0; i < responseHeaders.size(); i++) { m_responseHeaders.append(responseHeaders[i]); } VMRegAnchor vra; ActRec* fp = vmfp(); while (fp->skipFrame()) { fp = g_context->getPrevVMState(fp); } auto id = fp->func()->lookupVarId(s_http_response_header.get()); if (id != kInvalidId) { auto tvTo = frame_local(fp, id); Variant varFrom(m_responseHeaders); const auto tvFrom(varFrom.asTypedValue()); if (tvTo->m_type == KindOfRef) { tvTo = tvTo->m_data.pref->tv(); } tvDup(*tvFrom, *tvTo); } else if ((fp->func()->attrs() & AttrMayUseVV) && fp->hasVarEnv()) { fp->getVarEnv()->set(s_http_response_header.get(), Variant(m_responseHeaders).asTypedValue()); } /* * If code == 0, Curl failed to connect; per PHP5, ignore_errors just means * to not worry if we get an http resonse code that isn't between 200 and 400, * but we shouldn't ignore other errors. * all status codes in the 2xx range are defined by the specification as * successful; * all status codes in the 3xx range are for redirection, and so also should * never fail. */ if ((code >= 200 && code < 400) || (m_ignoreErrors && code != 0)) { setName(url.toCppString()); m_data = const_cast<char*>(m_response.data()); m_len = m_response.size(); return true; } else { m_error = http.getLastError().c_str(); return false; } }
Array createBacktrace(const BacktraceArgs& btArgs) { Array bt = Array::Create(); // If there is a parser frame, put it at the beginning of // the backtrace if (btArgs.m_parserFrame) { bt.append( make_map_array( s_file, btArgs.m_parserFrame->filename, s_line, btArgs.m_parserFrame->lineNumber ) ); } VMRegAnchor _; if (!vmfp()) { // If there are no VM frames, we're done return bt; } int depth = 0; ActRec* fp = nullptr; Offset pc = 0; // Get the fp and pc of the top frame (possibly skipping one frame) { if (btArgs.m_skipTop) { fp = g_context->getPrevVMState(vmfp(), &pc); if (!fp) { // We skipped over the only VM frame, we're done return bt; } } else { fp = vmfp(); Unit *unit = vmfp()->m_func->unit(); assert(unit); pc = unit->offsetOf(vmpc()); } // Handle the top frame if (btArgs.m_withSelf) { // Builtins don't have a file and line number if (!fp->m_func->isBuiltin()) { Unit* unit = fp->m_func->unit(); assert(unit); const char* filename = fp->m_func->filename()->data(); Offset off = pc; ArrayInit frame(btArgs.m_parserFrame ? 4 : 2, ArrayInit::Map{}); frame.set(s_file, filename); frame.set(s_line, unit->getLineNumber(off)); if (btArgs.m_parserFrame) { frame.set(s_function, s_include); frame.set(s_args, Array::Create(btArgs.m_parserFrame->filename)); } bt.append(frame.toVariant()); depth++; } } } // Handle the subsequent VM frames Offset prevPc = 0; for (ActRec* prevFp = g_context->getPrevVMState(fp, &prevPc); fp != nullptr && (btArgs.m_limit == 0 || depth < btArgs.m_limit); fp = prevFp, pc = prevPc, prevFp = g_context->getPrevVMState(fp, &prevPc)) { // do not capture frame for HPHP only functions if (fp->m_func->isNoInjection()) { continue; } ArrayInit frame(7, ArrayInit::Map{}); auto const curUnit = fp->m_func->unit(); auto const curOp = *reinterpret_cast<const Op*>(curUnit->at(pc)); auto const isReturning = curOp == Op::RetC || curOp == Op::RetV || curOp == Op::CreateCont || curOp == Op::Await || fp->localsDecRefd(); // Builtins and generators don't have a file and line number if (prevFp && !prevFp->m_func->isBuiltin() && !fp->resumed()) { auto const prevUnit = prevFp->m_func->unit(); auto prevFile = prevUnit->filepath(); if (prevFp->m_func->originalFilename()) { prevFile = prevFp->m_func->originalFilename(); } assert(prevFile); frame.set(s_file, const_cast<StringData*>(prevFile)); // In the normal method case, the "saved pc" for line number printing is // pointing at the cell conversion (Unbox/Pop) instruction, not the call // itself. For multi-line calls, this instruction is associated with the // subsequent line which results in an off-by-n. We're subtracting one // in order to look up the line associated with the FCall/FCallArray // instruction. Exception handling and the other opcodes (ex. BoxR) // already do the right thing. The emitter associates object access with // the subsequent expression and this would be difficult to modify. auto const opAtPrevPc = *reinterpret_cast<const Op*>(prevUnit->at(prevPc)); Offset pcAdjust = 0; if (opAtPrevPc == OpPopR || opAtPrevPc == OpUnboxR) { pcAdjust = 1; } frame.set(s_line, prevFp->m_func->unit()->getLineNumber(prevPc - pcAdjust)); } // check for include String funcname = const_cast<StringData*>(fp->m_func->name()); if (fp->m_func->isClosureBody()) { static StringData* s_closure_label = makeStaticString("{closure}"); funcname = s_closure_label; } // check for pseudomain if (funcname.empty()) { if (!prevFp) continue; funcname = s_include; } frame.set(s_function, funcname); if (!funcname.same(s_include)) { // Closures have an m_this but they aren't in object context Class* ctx = arGetContextClass(fp); if (ctx != nullptr && !fp->m_func->isClosureBody()) { frame.set(s_class, ctx->name()->data()); if (fp->hasThis() && !isReturning) { if (btArgs.m_withThis) { frame.set(s_object, Object(fp->getThis())); } frame.set(s_type, "->"); } else { frame.set(s_type, "::"); } } } Array args = Array::Create(); if (btArgs.m_ignoreArgs) { // do nothing } else if (funcname.same(s_include)) { if (depth) { args.append(const_cast<StringData*>(curUnit->filepath())); frame.set(s_args, args); } } else if (!RuntimeOption::EnableArgsInBacktraces || isReturning) { // Provide an empty 'args' array to be consistent with hphpc frame.set(s_args, args); } else { const int nparams = fp->m_func->numNonVariadicParams(); int nargs = fp->numArgs(); int nformals = std::min(nparams, nargs); if (UNLIKELY(fp->hasVarEnv() && fp->getVarEnv()->getFP() != fp)) { // VarEnv is attached to eval or debugger frame, other than the current // frame. Access locals thru VarEnv. auto varEnv = fp->getVarEnv(); auto func = fp->func(); for (int i = 0; i < nformals; i++) { TypedValue *arg = varEnv->lookup(func->localVarName(i)); args.append(tvAsVariant(arg)); } } else { for (int i = 0; i < nformals; i++) { TypedValue *arg = frame_local(fp, i); args.append(tvAsVariant(arg)); } } /* builtin extra args are not stored in varenv */ if (nargs > nparams && fp->hasExtraArgs()) { for (int i = nparams; i < nargs; i++) { TypedValue *arg = fp->getExtraArg(i - nparams); args.append(tvAsVariant(arg)); } } frame.set(s_args, args); } bt.append(frame.toVariant()); depth++; } return bt; }