FuncAnalysis do_analyze_collect(const Index& index, Context const inputCtx, CollectedInfo& collect, ClassAnalysis* clsAnalysis, const std::vector<Type>* knownArgs) { auto const ctx = adjust_closure_context(inputCtx); FuncAnalysis ai(ctx); Trace::Bump bumper{Trace::hhbbc, kTraceFuncBump, is_trace_function(ctx.cls, ctx.func)}; FTRACE(2, "{:-^70}\n-- {}\n", "Analyze", show(ctx)); /* * Set of RPO ids that still need to be visited. * * Initially, we need each entry block in this list. As we visit * blocks, we propagate states to their successors and across their * back edges---when state merges cause a change to the block * stateIn, we will add it to this queue so it gets visited again. */ auto incompleteQ = prepare_incompleteQ(index, ai, clsAnalysis, knownArgs); /* * There are potentially infinitely growing types when we're using * union_of to merge states, so occasonially we need to apply a * widening operator. * * Currently this is done by having a straight-forward hueristic: if * you visit a block too many times, we'll start doing all the * merges with the widening operator until we've had a chance to * visit the block again. We must then continue iterating in case * the actual fixed point is higher than the result of widening. * * Terminiation is guaranteed because the widening operator has only * finite chains in the type lattice. */ auto nonWideVisits = std::vector<uint32_t>(ctx.func->nextBlockId); // For debugging, count how many times basic blocks get interpreted. auto interp_counter = uint32_t{0}; /* * Iterate until a fixed point. * * Each time a stateIn for a block changes, we re-insert the block's * rpo ID in incompleteQ. Since incompleteQ is ordered, we'll * always visit blocks with earlier RPO ids first, which hopefully * means less iterations. */ while (!incompleteQ.empty()) { auto const blk = ai.rpoBlocks[incompleteQ.pop()]; if (nonWideVisits[blk->id]++ > options.analyzeFuncWideningLimit) { nonWideVisits[blk->id] = 0; } FTRACE(2, "block #{}\nin {}{}", blk->id, state_string(*ctx.func, ai.bdata[blk->id].stateIn), property_state_string(collect.props)); ++interp_counter; auto propagate = [&] (php::Block& target, const State& st) { auto const needsWiden = nonWideVisits[target.id] >= options.analyzeFuncWideningLimit; // We haven't optimized the widening operator much, because it // doesn't happen in practice right now. We want to know when // it starts happening: if (needsWiden) { std::fprintf(stderr, "widening in %s on %s\n", ctx.unit->filename->data(), ctx.func->name->data()); } FTRACE(2, " {}-> {}\n", needsWiden ? "widening " : "", target.id); FTRACE(4, "target old {}", state_string(*ctx.func, ai.bdata[target.id].stateIn)); auto const changed = needsWiden ? widen_into(ai.bdata[target.id].stateIn, st) : merge_into(ai.bdata[target.id].stateIn, st); if (changed) { incompleteQ.push(rpoId(ai, &target)); } FTRACE(4, "target new {}", state_string(*ctx.func, ai.bdata[target.id].stateIn)); }; auto stateOut = ai.bdata[blk->id].stateIn; auto interp = Interp { index, ctx, collect, blk, stateOut }; auto flags = run(interp, propagate); if (flags.returned) { ai.inferredReturn = union_of(std::move(ai.inferredReturn), std::move(*flags.returned)); } } ai.closureUseTypes = std::move(collect.closureUseTypes); if (ctx.func->isGenerator) { if (ctx.func->isAsync) { // Async generators always return AsyncGenerator object. ai.inferredReturn = objExact(index.builtin_class(s_AsyncGenerator.get())); } else { // Non-async generators always return Generator object. ai.inferredReturn = objExact(index.builtin_class(s_Generator.get())); } } else if (ctx.func->isAsync) { // Async functions always return WaitH<T>, where T is the type returned // internally. ai.inferredReturn = wait_handle(index, ai.inferredReturn); } /* * If inferredReturn is TBottom, the callee didn't execute a return * at all. (E.g. it unconditionally throws, or is an abstract * function body.) * * In this case, we leave the return type as TBottom, to indicate * the same to callers. */ assert(ai.inferredReturn.subtypeOf(TGen)); // For debugging, print the final input states for each block. FTRACE(2, "{}", [&] { auto const bsep = std::string(60, '=') + "\n"; auto const sep = std::string(60, '-') + "\n"; auto ret = folly::format( "{}function {} ({} block interps):\n{}", bsep, show(ctx), interp_counter, bsep ).str(); for (auto& bd : ai.bdata) { ret += folly::format( "{}block {}:\nin {}", sep, ai.rpoBlocks[bd.rpoId]->id, state_string(*ctx.func, bd.stateIn) ).str(); } ret += sep + bsep; folly::format(&ret, "Inferred return type: {}\n", show(ai.inferredReturn)); ret += bsep; return ret; }()); return ai; }
FuncAnalysis do_analyze_collect(const Index& index, Context const ctx, CollectedInfo& collect, ClassAnalysis* clsAnalysis, const std::vector<Type>* knownArgs) { assertx(ctx.cls == adjust_closure_context(ctx).cls); FuncAnalysis ai{ctx}; auto const bump = trace_bump_for(ctx.cls, ctx.func); Trace::Bump bumper1{Trace::hhbbc, bump}; Trace::Bump bumper2{Trace::hhbbc_cfg, bump}; if (knownArgs) { FTRACE(2, "{:.^70}\n", "Inline Interp"); } SCOPE_EXIT { if (knownArgs) { FTRACE(2, "{:.^70}\n", "End Inline Interp"); } }; FTRACE(2, "{:-^70}\n-- {}\n", "Analyze", show(ctx)); /* * Set of RPO ids that still need to be visited. * * Initially, we need each entry block in this list. As we visit * blocks, we propagate states to their successors and across their * back edges---when state merges cause a change to the block * stateIn, we will add it to this queue so it gets visited again. */ auto incompleteQ = prepare_incompleteQ(index, ai, clsAnalysis, knownArgs); /* * There are potentially infinitely growing types when we're using union_of to * merge states, so occasionally we need to apply a widening operator. * * Currently this is done by having a straight-forward hueristic: if you visit * a block too many times, we'll start doing all the merges with the widening * operator. We must then continue iterating in case the actual fixed point is * higher than the result of widening. Likewise if we loop too much because of * local static types changing, we'll widen those. * * Termination is guaranteed because the widening operator has only finite * chains in the type lattice. */ auto totalVisits = std::vector<uint32_t>(ctx.func->blocks.size()); auto totalLoops = uint32_t{0}; // For debugging, count how many times basic blocks get interpreted. auto interp_counter = uint32_t{0}; // Used to force blocks that depended on the types of local statics // to be re-analyzed when the local statics change. std::unordered_map<borrowed_ptr<const php::Block>, std::map<LocalId, Type>> usedLocalStatics; /* * Iterate until a fixed point. * * Each time a stateIn for a block changes, we re-insert the block's * rpo ID in incompleteQ. Since incompleteQ is ordered, we'll * always visit blocks with earlier RPO ids first, which hopefully * means less iterations. */ do { while (!incompleteQ.empty()) { auto const blk = ai.rpoBlocks[incompleteQ.pop()]; totalVisits[blk->id]++; FTRACE(2, "block #{}\nin {}{}", blk->id, state_string(*ctx.func, ai.bdata[blk->id].stateIn, collect), property_state_string(collect.props)); ++interp_counter; auto propagate = [&] (BlockId target, const State* st) { if (!st) { FTRACE(2, " Force reprocess: {}\n", target); incompleteQ.push(rpoId(ai, target)); return; } auto const needsWiden = totalVisits[target] >= options.analyzeFuncWideningLimit; FTRACE(2, " {}-> {}\n", needsWiden ? "widening " : "", target); FTRACE(4, "target old {}", state_string(*ctx.func, ai.bdata[target].stateIn, collect)); auto const changed = needsWiden ? widen_into(ai.bdata[target].stateIn, *st) : merge_into(ai.bdata[target].stateIn, *st); if (changed) { incompleteQ.push(rpoId(ai, target)); } FTRACE(4, "target new {}", state_string(*ctx.func, ai.bdata[target].stateIn, collect)); }; auto stateOut = ai.bdata[blk->id].stateIn; auto interp = Interp { index, ctx, collect, blk, stateOut }; auto flags = run(interp, propagate); if (any(collect.opts & CollectionOpts::EffectFreeOnly) && !collect.effectFree) { break; } // We only care about the usedLocalStatics from the last visit if (flags.usedLocalStatics) { usedLocalStatics[blk] = std::move(*flags.usedLocalStatics); } else { usedLocalStatics.erase(blk); } if (flags.returned) { ai.inferredReturn |= std::move(*flags.returned); } } if (any(collect.opts & CollectionOpts::EffectFreeOnly) && !collect.effectFree) { break; } // maybe some local statics changed type since the last time their // blocks were visited. if (totalLoops++ >= options.analyzeFuncWideningLimit) { // If we loop too many times because of static locals, widen them to // ensure termination. for (auto& t : collect.localStaticTypes) { t = widen_type(std::move(t)); } } for (auto const& elm : usedLocalStatics) { for (auto const& ls : elm.second) { if (collect.localStaticTypes[ls.first] != ls.second) { incompleteQ.push(rpoId(ai, elm.first->id)); break; } } } } while (!incompleteQ.empty()); ai.closureUseTypes = std::move(collect.closureUseTypes); ai.cnsMap = std::move(collect.cnsMap); ai.readsUntrackedConstants = collect.readsUntrackedConstants; ai.mayUseVV = collect.mayUseVV; ai.effectFree = collect.effectFree; ai.unfoldableFuncs = collect.unfoldableFuncs; index.fixup_return_type(ctx.func, ai.inferredReturn); /* * If inferredReturn is TBottom, the callee didn't execute a return * at all. (E.g. it unconditionally throws, or is an abstract * function body.) * * In this case, we leave the return type as TBottom, to indicate * the same to callers. */ assert(ai.inferredReturn.subtypeOf(TGen)); // For debugging, print the final input states for each block. FTRACE(2, "{}", [&] { auto const bsep = std::string(60, '=') + "\n"; auto const sep = std::string(60, '-') + "\n"; auto ret = folly::format( "{}function {} ({} block interps):\n{}", bsep, show(ctx), interp_counter, bsep ).str(); for (auto& bd : ai.bdata) { folly::format( &ret, "{}block {}:\nin {}", sep, ai.rpoBlocks[bd.rpoId]->id, state_string(*ctx.func, bd.stateIn, collect) ); } ret += sep + bsep; folly::format(&ret, "Inferred return type: {}\n", show(ai.inferredReturn)); ret += bsep; return ret; }()); // Do this after the tracing above ai.localStaticTypes = std::move(collect.localStaticTypes); return ai; }