ClassAnalysis analyze_class()

in hphp/hhbbc/analyze.cpp [612:1040]


ClassAnalysis analyze_class(const Index& index, const Context& ctx) {

  assertx(ctx.cls && !ctx.func && !is_used_trait(*ctx.cls));

  {
    Trace::Bump bumper{Trace::hhbbc, kSystemLibBump,
      is_systemlib_part(*ctx.unit)};
    FTRACE(2, "{:#^70}\n", "Class");
  }

  ClassAnalysis clsAnalysis(ctx);
  auto const associatedClosures = index.lookup_closures(ctx.cls);
  auto const associatedMethods  = index.lookup_extra_methods(ctx.cls);
  auto const isHNIBuiltin       = ctx.cls->attrs & AttrBuiltin;

  /*
   * Initialize inferred private property types to their in-class
   * initializers.
   *
   * We need to loosen_all on instance properties, because the class could be
   * unserialized, which we don't guarantee preserves those aspects of the
   * type.
   *
   * Also, set Uninit properties to TBottom, so that analysis
   * of 86pinit methods sets them to the correct type.
   */
  for (auto& prop : const_cast<php::Class*>(ctx.cls)->properties) {
    auto const cellTy = from_cell(prop.val);

    if (prop_might_have_bad_initial_value(index, *ctx.cls, prop)) {
      prop.attrs = (Attr)(prop.attrs & ~AttrInitialSatisfiesTC);
      // If Uninit, it will be determined in the 86[s,p]init function.
      if (!cellTy.subtypeOf(BUninit)) clsAnalysis.badPropInitialValues = true;
    } else {
      prop.attrs |= AttrInitialSatisfiesTC;
    }

    if (!(prop.attrs & AttrPrivate)) continue;

    if (isHNIBuiltin) {
      auto const hniTy = from_hni_constraint(prop.userType);
      if (!cellTy.subtypeOf(hniTy)) {
        always_assert_flog(
          false,
          "hni {}::{} has impossible type. "
          "The annotation says it is type ({}) "
          "but the default value is type ({}).\n",
          ctx.cls->name,
          prop.name,
          show(hniTy),
          show(cellTy)
        );
      }
    }

    if (!(prop.attrs & AttrStatic)) {
      auto t = loosen_this_prop_for_serialization(*ctx.cls, prop.name, cellTy);

      if (!is_closure(*ctx.cls) && t.subtypeOf(BUninit)) {
        /*
         * For non-closure classes, a property of type KindOfUninit
         * means that it has non-scalar initializer which will be set
         * by a 86pinit method.  For these classes, we want the
         * initial type of the property to be the type set by the
         * 86pinit method, so we set the type to TBottom.
         *
         * Closures will not have an 86pinit body, but still may have
         * properties of kind KindOfUninit (they will later contain
         * used variables).  We don't want to touch those.
         */
        t = TBottom;
      } else if (!(prop.attrs & AttrSystemInitialValue)) {
        t = adjust_type_for_prop(index, *ctx.cls, &prop.typeConstraint, t);
      }
      auto& elem = clsAnalysis.privateProperties[prop.name];
      elem.ty = std::move(t);
      elem.tc = &prop.typeConstraint;
      elem.attrs = prop.attrs;
      elem.everModified = false;
    } else {
      // Same thing as the above regarding TUninit and TBottom.
      // Static properties don't need to exclude closures for this,
      // though---we use instance properties for the closure use vars.
      auto t = cellTy.subtypeOf(BUninit)
        ? TBottom
        : (prop.attrs & AttrSystemInitialValue)
          ? cellTy
          : adjust_type_for_prop(index, *ctx.cls, &prop.typeConstraint, cellTy);
      auto& elem = clsAnalysis.privateStatics[prop.name];
      elem.ty = std::move(t);
      elem.tc = &prop.typeConstraint;
      elem.attrs = prop.attrs;
      elem.everModified = false;
    }
  }

  /*
   * For builtins, we assume the runtime can write to the properties
   * in un-analyzable ways (but won't violate their type-hint). So,
   * expand the analyzed types to at least include the type-hint.
   */
  if (isHNIBuiltin) expand_hni_prop_types(clsAnalysis);

  /*
   * For classes with non-scalar initializers, the 86pinit, 86sinit,
   * 86linit, 86cinit, and 86reifiedinit methods are guaranteed to run
   * before any other method, and are never called afterwards. Thus,
   * we can analyze these methods first to determine the initial types
   * of properties with non-scalar initializers, and these need not be
   * be run again as part of the fixedpoint computation.
   */
  CompactVector<FuncAnalysis> initResults;
  auto analyze_86init = [&](const StaticString &name) {
    if (auto func = find_method(ctx.cls, name.get())) {
      auto const wf = php::WideFunc::cns(func);
      auto const context = AnalysisContext { ctx.unit, wf, ctx.cls };
      initResults.push_back(do_analyze(index, context, &clsAnalysis));
    }
  };
  analyze_86init(s_86pinit);
  analyze_86init(s_86sinit);
  analyze_86init(s_86linit);
  analyze_86init(s_86cinit);
  analyze_86init(s_86reifiedinit);

  // NB: Properties can still be TBottom at this point if their initial values
  // cannot possibly satisfy their type-constraints. The classes of such
  // properties cannot be instantiated.

  /*
   * Similar to the function case in do_analyze, we have to handle the
   * fact that there are infinitely growing chains in our type lattice
   * under union_of.
   *
   * So if we've visited a func some number of times and still aren't
   * at a fixed point, we'll set the property state to the result of
   * widening the old state with the new state, and then reset the
   * counter.  This guarantees eventual termination.
   */

  ClassAnalysisWork work;
  clsAnalysis.work = &work;

  clsAnalysis.methods.reserve(initResults.size() + ctx.cls->methods.size());
  for (auto& m : initResults) {
    clsAnalysis.methods.emplace_back(std::move(m));
  }
  if (associatedClosures) {
    clsAnalysis.closures.reserve(associatedClosures->size());
  }

  auto const startPrivateProperties = clsAnalysis.privateProperties;
  auto const startPrivateStatics = clsAnalysis.privateStatics;

  struct FuncMeta {
    const php::Unit* unit;
    const php::Class* cls;
    CompactVector<FuncAnalysisResult>* output;
    size_t startReturnRefinements;
    size_t localReturnRefinements = 0;
    int outputIdx = -1;
    size_t visits = 0;
  };
  hphp_fast_map<const php::Func*, FuncMeta> funcMeta;

  auto const getMeta = [&] (const php::Func& f) -> FuncMeta& {
    auto metaIt = funcMeta.find(&f);
    assertx(metaIt != funcMeta.end());
    return metaIt->second;
  };

  // Build up the initial worklist:
  for (auto const& f : ctx.cls->methods) {
    if (f->name->isame(s_86pinit.get()) ||
        f->name->isame(s_86sinit.get()) ||
        f->name->isame(s_86linit.get()) ||
        f->name->isame(s_86cinit.get()) ||
        f->name->isame(s_86reifiedinit.get())) {
      continue;
    }
    auto const DEBUG_ONLY inserted = work.worklist.schedule(*f);
    assertx(inserted);
    auto [type, refinements] = index.lookup_return_type_raw(f.get());
    work.returnTypes.emplace(f.get(), std::move(type));
    funcMeta.emplace(
      f.get(),
      FuncMeta{ctx.unit, ctx.cls, &clsAnalysis.methods, refinements}
    );
  }

  if (associatedClosures) {
    for (auto const c : *associatedClosures) {
      auto const f = c->methods[0].get();
      auto const DEBUG_ONLY inserted = work.worklist.schedule(*f);
      assertx(inserted);
      auto [type, refinements] = index.lookup_return_type_raw(f);
      work.returnTypes.emplace(f, std::move(type));
      funcMeta.emplace(
        f, FuncMeta{ctx.unit, c, &clsAnalysis.closures, refinements}
      );
    }
  }
  if (associatedMethods) {
    for (auto const m : *associatedMethods) {
      auto const DEBUG_ONLY inserted = work.worklist.schedule(*m);
      assertx(inserted);
      funcMeta.emplace(m, FuncMeta{m->unit, ctx.cls, nullptr, 0, 0});
    }
  }

  // Keep analyzing until we have more functions scheduled (the fixed
  // point).
  while (!work.worklist.empty()) {
    // First analyze funcs until we hit a fixed point for the
    // properties. Until we reach that, the return types are *not*
    // guaranteed to be correct.
    while (auto const f = work.worklist.next()) {
      auto& meta = getMeta(*f);

      auto const wf = php::WideFunc::cns(f);
      auto const context = AnalysisContext { meta.unit, wf, meta.cls };
      auto results = do_analyze(index, context, &clsAnalysis);

      if (meta.output) {
        if (meta.outputIdx < 0) {
          meta.outputIdx = meta.output->size();
          meta.output->emplace_back(std::move(results));
        } else {
          (*meta.output)[meta.outputIdx] = std::move(results);
        }
      }

      if (meta.visits++ >= options.analyzeClassWideningLimit) {
        for (auto& prop : clsAnalysis.privateProperties) {
          auto wide = widen_type(prop.second.ty);
          if (prop.second.ty.strictlyMoreRefined(wide)) {
            prop.second.ty = std::move(wide);
            work.worklist.scheduleForProp(prop.first);
          }
        }
        for (auto& prop : clsAnalysis.privateStatics) {
          auto wide = widen_type(prop.second.ty);
          if (prop.second.ty.strictlyMoreRefined(wide)) {
            prop.second.ty = std::move(wide);
            work.worklist.scheduleForProp(prop.first);
          }
        }
      }
    }

    // We've hit a fixed point for the properties. Other local
    // information (such as return type information) is now correct
    // (but might not be optimal).

    auto bail = false;

    // Reflect any improved return types into the results. This will
    // make them available for local analysis and they'll eventually
    // be written back into the Index.
    for (auto& kv : funcMeta) {
      auto const f = kv.first;
      auto& meta = kv.second;
      if (!meta.output) continue;
      assertx(meta.outputIdx >= 0);
      auto& results = (*meta.output)[meta.outputIdx];

      auto const oldTypeIt = work.returnTypes.find(f);
      assertx(oldTypeIt != work.returnTypes.end());
      auto& oldType = oldTypeIt->second;
      results.inferredReturn =
        loosen_interfaces(std::move(results.inferredReturn));

      // Heed the return type refinement limit
      if (results.inferredReturn.strictlyMoreRefined(oldType)) {
        if (meta.startReturnRefinements + meta.localReturnRefinements
            < options.returnTypeRefineLimit) {
          oldType = results.inferredReturn;
          work.worklist.scheduleForReturnType(*f);
        } else if (meta.localReturnRefinements > 0) {
          results.inferredReturn = oldType;
        }
        ++meta.localReturnRefinements;
      } else if (!more_refined_for_index(results.inferredReturn, oldType)) {
        // If we have a monotonicity violation, bail out immediately
        // and let the Index complain.
        bail = true;
      }

      results.localReturnRefinements = meta.localReturnRefinements;
      if (results.localReturnRefinements > 0) --results.localReturnRefinements;
    }
    if (bail) break;

    hphp_fast_set<const php::Func*> changed;

    // We've made the return types available for local analysis. Now
    // iterate again and see if we can improve them.
    while (auto const f = work.worklist.next()) {
      auto& meta = getMeta(*f);

      auto const wf = php::WideFunc::cns(f);
      auto const context = AnalysisContext { meta.unit, wf, meta.cls };

      work.propsRefined = false;
      auto results = do_analyze(index, context, &clsAnalysis);
      assertx(!work.propsRefined);

      if (!meta.output) continue;

      auto returnTypeIt = work.returnTypes.find(f);
      assertx(returnTypeIt != work.returnTypes.end());

      auto& oldReturn = returnTypeIt->second;
      results.inferredReturn =
        loosen_interfaces(std::move(results.inferredReturn));

      // Heed the return type refinement limit
      if (results.inferredReturn.strictlyMoreRefined(oldReturn)) {
        if (meta.startReturnRefinements + meta.localReturnRefinements
            < options.returnTypeRefineLimit) {
          oldReturn = results.inferredReturn;
          work.worklist.scheduleForReturnType(*f);
          changed.emplace(f);
        } else if (meta.localReturnRefinements > 0) {
          results.inferredReturn = oldReturn;
        }
        ++meta.localReturnRefinements;
      } else if (!more_refined_for_index(results.inferredReturn, oldReturn)) {
        // If we have a monotonicity violation, bail out immediately
        // and let the Index complain.
        bail = true;
      }

      results.localReturnRefinements = meta.localReturnRefinements;
      if (results.localReturnRefinements > 0) --results.localReturnRefinements;

      assertx(meta.outputIdx >= 0);
      (*meta.output)[meta.outputIdx] = std::move(results);
    }
    if (bail) break;

    // Return types have reached a fixed point. However, this means
    // that we might be able to further improve property types. So, if
    // a method has an improved return return, examine the methods
    // which depend on that return type. Drop any property info for
    // properties those methods write to. Reschedule any methods which
    // or write to those properties. The idea is we want to re-analyze
    // all mutations of those properties again, since the refined
    // returned types may result in better property types. This
    // process may repeat multiple times, but will eventually reach a
    // fixed point.

    if (!work.propMutators.empty()) {
      auto const resetProp = [&] (SString name,
                                  const PropState& src,
                                  PropState& dst) {
        auto dstIt = dst.find(name);
        auto const srcIt = src.find(name);
        if (dstIt == dst.end()) {
          assertx(srcIt == src.end());
          return;
        }
        assertx(srcIt != src.end());
        dstIt->second.ty = srcIt->second.ty;
        dstIt->second.everModified = srcIt->second.everModified;
      };

      hphp_fast_set<SString> retryProps;
      for (auto const f : changed) {
        auto const deps = work.worklist.depsForReturnType(*f);
        if (!deps) continue;
        for (auto const dep : *deps) {
          auto const propsIt = work.propMutators.find(dep);
          if (propsIt == work.propMutators.end()) continue;
          for (auto const prop : propsIt->second) retryProps.emplace(prop);
        }
      }

      // Schedule the funcs which mutate the props before the ones
      // that read them.
      for (auto const prop : retryProps) {
        resetProp(prop, startPrivateProperties,
                  clsAnalysis.privateProperties);
        resetProp(prop, startPrivateStatics,
                  clsAnalysis.privateStatics);
        work.worklist.scheduleForPropMutate(prop);
      }
      for (auto const prop : retryProps) {
        work.worklist.scheduleForProp(prop);
      }
    }

    // This entire loop will eventually terminate when we cannot
    // improve properties nor return types.
  }

  Trace::Bump bumper{Trace::hhbbc, kSystemLibBump,
    is_systemlib_part(*ctx.unit)};

  // For debugging, print the final state of the class analysis.
  FTRACE(2, "{}", [&] {
    auto const bsep = std::string(60, '+') + "\n";
    auto ret = folly::format(
      "{}class {}:\n{}",
      bsep,
      ctx.cls->name,
      bsep
    ).str();
    for (auto& kv : clsAnalysis.privateProperties) {
      ret += folly::format(
        "private ${: <14} :: {}\n",
        kv.first,
        show(kv.second.ty)
      ).str();
    }
    for (auto& kv : clsAnalysis.privateStatics) {
      ret += folly::format(
        "private static ${: <14} :: {}\n",
        kv.first,
        show(kv.second.ty)
      ).str();
    }
    ret += bsep;
    return ret;
  }());

  clsAnalysis.work = nullptr;
  return clsAnalysis;
}