Value TraceInterpreter::execFunction()

in API/hermes/TraceInterpreter.cpp [983:1577]


Value TraceInterpreter::execFunction(
    const TraceInterpreter::Call &call,
    const Value &thisVal,
    const Value *args,
    uint64_t count,
    const PropNameID *nativePropNameToConsumeAsDef) {
  llvh::SaveAndRestore<uint64_t> depthGuard(depth_, depth_ + 1);
  // A mapping from an ObjectID to the Object for local variables.
  // Invariant: value is Object or String;
  std::unordered_map<ObjectID, Value> locals;
  std::unordered_map<ObjectID, PropNameID> pniLocals;
  // Save a value so that Call can set it, and Return can access it.
  Value retval;
  // Carry the return value from BeginJSExec to EndJSExec.
  Value overallRetval;
#ifndef NDEBUG
  if (depth_ != 1) {
    RecordType firstRecType = call.pieces.front().records.front()->getType();
    assert(
        (firstRecType == RecordType::CallToNative ||
         firstRecType == RecordType::GetNativePropertyNames ||
         firstRecType == RecordType::GetPropertyNative ||
         firstRecType == RecordType::SetPropertyNative) &&
        "Illegal starting record");
  }
#endif
#ifndef NDEBUG
  // We'll want the first record, to verify that it's the one that consumes
  // nativePropNameToConsumeAsDef if that is non-null.
  const SynthTrace::Record *firstRec = call.pieces.at(0).records.at(0);
#endif
  for (const TraceInterpreter::Call::Piece &piece : call.pieces) {
    uint64_t globalRecordNum = piece.start;
    const auto getJSIValueForUseOpt =
        [this, &call, &locals, &globalRecordNum](
            ObjectID obj) -> llvh::Optional<Value> {
      // Check locals, then globals.
      auto it = locals.find(obj);
      if (it != locals.end()) {
        // Satisfiable locally
        Value val{rt_, it->second};
        assert(val.isObject() || val.isString() || val.isSymbol());
        // If it was the last local use, delete that object id from locals.
        auto defAndUse = call.locals.find(obj);
        if (defAndUse != call.locals.end() &&
            defAndUse->second.lastUse == globalRecordNum) {
          assert(
              defAndUse->second.lastDefBeforeFirstUse != DefAndUse::kUnused &&
              "All uses must be preceded by a def");
          locals.erase(it);
        }
        return val;
      }
      auto defAndUse = globalDefsAndUses_.find(obj);
      // Since the use might not be a jsi::Value, it might be found in the
      // locals table for non-Values, and therefore might not be in the global
      // use/def table.
      if (defAndUse == globalDefsAndUses_.end()) {
        return llvh::None;
      }
      it = gom_.find(obj);
      if (it != gom_.end()) {
        Value val{rt_, it->second};
        assert(val.isObject() || val.isString() || val.isSymbol());
        // If it was the last global use, delete that object id from globals.
        if (defAndUse->second.lastUse == globalRecordNum) {
          gom_.erase(it);
        }
        return val;
      }
      return llvh::None;
    };
    const auto getJSIValueForUse =
        [&getJSIValueForUseOpt](ObjectID obj) -> Value {
      auto valOpt = getJSIValueForUseOpt(obj);
      assert(valOpt && "There must be a definition for all uses.");
      return std::move(*valOpt);
    };
    const auto getObjForUse = [this,
                               getJSIValueForUse](ObjectID obj) -> Object {
      return getJSIValueForUse(obj).getObject(rt_);
    };
    const auto getPropNameIDForUse =
        [this, &call, &pniLocals, &globalRecordNum](
            ObjectID obj) -> PropNameID {
      // Check locals, then globals.
      auto it = pniLocals.find(obj);
      if (it != pniLocals.end()) {
        // Satisfiable locally
        PropNameID propNameID{rt_, it->second};
        // If it was the last local use, delete that object id from locals.
        auto defAndUse = call.locals.find(obj);
        if (defAndUse != call.locals.end() &&
            defAndUse->second.lastUse == globalRecordNum) {
          assert(
              defAndUse->second.lastDefBeforeFirstUse != DefAndUse::kUnused &&
              "All uses must be preceded by a def");
          pniLocals.erase(it);
        }
        return propNameID;
      }
      auto defAndUse = globalDefsAndUses_.find(obj);
      assert(
          defAndUse != globalDefsAndUses_.end() &&
          "All global uses must have a global definition");
      it = gpnm_.find(obj);
      assert(
          it != gpnm_.end() &&
          "If there is a global definition, it must exist in one of the maps");
      PropNameID propNameID{rt_, it->second};
      // If it was the last global use, delete that object id from globals.
      if (defAndUse->second.lastUse == globalRecordNum) {
        gpnm_.erase(it);
      }
      return propNameID;
    };

    for (const SynthTrace::Record *rec : piece.records) {
      try {
        switch (rec->getType()) {
          case RecordType::BeginExecJS: {
            const auto &bejsr =
                dynamic_cast<const SynthTrace::BeginExecJSRecord &>(*rec);
            auto it = bundles_.find(bejsr.sourceHash());
            if (it == bundles_.end()) {
              if ((options_.disableSourceHashCheck ||
                   isAllZeroSourceHash(bejsr.sourceHash())) &&
                  bundles_.size() == 1) {
                // Normally, if a bundle's source hash doesn't match, it would
                // be an error. However, for convenience and backwards
                // compatibility, allow an all-zero hash to automatically assume
                // a bundle if that was the only bundle supplied.
                it = bundles_.begin();
              } else {
                throw std::invalid_argument(
                    "Trace expected the source hash " +
                    ::hermes::hashAsString(bejsr.sourceHash()) +
                    ", that wasn't provided as a file");
              }
            }

            // Copy the shared pointer to the buffer in case this file is
            // executed multiple times.
            auto bundle = it->second;
            if (!HermesRuntime::isHermesBytecode(
                    bundle->data(), bundle->size())) {
              llvh::errs()
                  << "Note: You are running from source code, not HBC bytecode.\n"
                  << "      This run will reflect dev performance, not production.\n";
            }
            // overallRetval is to be consumed when we get an EndExecJS record.
            overallRetval =
                rt_.evaluateJavaScript(std::move(bundle), bejsr.sourceURL());
            break;
          }
          case RecordType::EndExecJS: {
            const auto &eejsr =
                dynamic_cast<const SynthTrace::EndExecJSRecord &>(*rec);
            ifObjectAddToDefs(
                eejsr.retVal_,
                std::move(overallRetval),
                call,
                globalRecordNum,
                locals);
            LLVM_FALLTHROUGH;
          }
          case RecordType::Marker: {
            const auto &mr =
                dynamic_cast<const SynthTrace::MarkerRecord &>(*rec);
            // If the tag is the requested tag, and the stats have not already
            // been collected, collect them.
            checkMarker(mr.tag_);
            if (auto *tracingRT = dynamic_cast<TracingRuntime *>(&rt_)) {
              if (rec->getType() != RecordType::EndExecJS) {
                // If tracing is on, re-emit the marker into the result stream.
                // This way, the trace emitted from replay will be the same as
                // the trace input. Don't do this for EndExecJSRecord, since
                // that falls through to here, and it already emits a marker
                // automatically.
                tracingRT->addMarker(mr.tag_);
              }
            }
            break;
          }
          case RecordType::CreateObject: {
            const auto &cor =
                static_cast<const SynthTrace::CreateObjectRecord &>(*rec);
            // Make an empty object to be used.
            addJSIValueToDefs(
                call, cor.objID_, globalRecordNum, Object(rt_), locals);
            break;
          }
          case RecordType::CreateString: {
            const auto &csr =
                static_cast<const SynthTrace::CreateStringRecord &>(*rec);
            Value str;
            if (csr.ascii_) {
              str = String::createFromAscii(
                  rt_, csr.chars_.data(), csr.chars_.size());
            } else {
              str = String::createFromUtf8(
                  rt_,
                  reinterpret_cast<const uint8_t *>(csr.chars_.data()),
                  csr.chars_.size());
            }
            assert(str.asString(rt_).utf8(rt_) == csr.chars_);
            addJSIValueToDefs(
                call, csr.objID_, globalRecordNum, std::move(str), locals);
            break;
          }
          case RecordType::CreatePropNameID: {
            const auto &cpnr =
                static_cast<const SynthTrace::CreatePropNameIDRecord &>(*rec);
            // We perform the calls below for their side effects (for example,
            auto propNameID = [&] {
              switch (cpnr.valueType_) {
                case SynthTrace::CreatePropNameIDRecord::ASCII:
                  return PropNameID::forAscii(
                      rt_, cpnr.chars_.data(), cpnr.chars_.size());
                case SynthTrace::CreatePropNameIDRecord::UTF8:
                  return PropNameID::forUtf8(
                      rt_,
                      reinterpret_cast<const uint8_t *>(cpnr.chars_.data()),
                      cpnr.chars_.size());
                case SynthTrace::CreatePropNameIDRecord::TRACEVALUE: {
                  auto val = traceValueToJSIValue(
                      rt_, trace_, getJSIValueForUse, cpnr.traceValue_);
                  if (val.isSymbol())
                    return PropNameID::forSymbol(rt_, val.getSymbol(rt_));
                  return PropNameID::forString(rt_, val.getString(rt_));
                }
              }
              llvm_unreachable("No other way to construct PropNameID");
            }();
            addPropNameIDToDefs(
                call,
                cpnr.propNameID_,
                globalRecordNum,
                std::move(propNameID),
                pniLocals);
            break;
          }
          case RecordType::CreateHostObject: {
            const auto &chor =
                static_cast<const SynthTrace::CreateHostObjectRecord &>(*rec);
            const ObjectID objID = chor.objID_;
            auto iterAndDidCreate =
                hostObjects_.emplace(objID, createHostObject(objID));
            assert(
                iterAndDidCreate.second &&
                "This should always be creating a new host object");
            addJSIValueToDefs(
                call,
                objID,
                globalRecordNum,
                Value(rt_, iterAndDidCreate.first->second),
                locals);
            break;
          }
          case RecordType::CreateHostFunction: {
            const auto &chfr =
                static_cast<const SynthTrace::CreateHostFunctionRecord &>(*rec);
            auto iterAndDidCreate = hostFunctions_.emplace(
                chfr.objID_,
                createHostFunction(
                    chfr, getPropNameIDForUse(chfr.propNameID_)));
            assert(
                iterAndDidCreate.second &&
                "This should always be creating a new host function");
            addJSIValueToDefs(
                call,
                chfr.objID_,
                globalRecordNum,
                Value(rt_, iterAndDidCreate.first->second),
                locals);
            break;
          }
          case RecordType::DrainMicrotasks: {
            const auto &drainRecord =
                static_cast<const SynthTrace::DrainMicrotasksRecord &>(*rec);
            rt_.drainMicrotasks(drainRecord.maxMicrotasksHint_);
            break;
          }
          case RecordType::GetProperty: {
            const auto &gpr =
                static_cast<const SynthTrace::GetPropertyRecord &>(*rec);
            // Call get property on the object specified, and possibly define
            // the result.
            jsi::Value value;
            const auto &obj = getObjForUse(gpr.objID_);
            auto propIDValOpt = getJSIValueForUseOpt(gpr.propID_);
            // TODO(T111638575): We have to check whether the value is a string,
            // because we cannot disambiguate Symbols from PropNameIDs.
            if (propIDValOpt && propIDValOpt->isString()) {
              const jsi::String propString = (*propIDValOpt).asString(rt_);
#ifdef HERMESVM_API_TRACE_DEBUG
              assert(propString.utf8(rt_) == gpr.propNameDbg_);
#endif
              value = obj.getProperty(rt_, propString);
            } else {
              auto propNameID = getPropNameIDForUse(gpr.propID_);
#ifdef HERMESVM_API_TRACE_DEBUG
              assert(propNameID.utf8(rt_) == gpr.propNameDbg_);
#endif
              value = obj.getProperty(rt_, propNameID);
            }
            ifObjectAddToDefs(
                gpr.value_, std::move(value), call, globalRecordNum, locals);
            break;
          }
          case RecordType::SetProperty: {
            const auto &spr =
                static_cast<const SynthTrace::SetPropertyRecord &>(*rec);
            auto obj = getObjForUse(spr.objID_);
            // Call set property on the object specified and give it the value.
            auto propIDValOpt = getJSIValueForUseOpt(spr.propID_);
            // TODO(T111638575)
            if (propIDValOpt && propIDValOpt->isString()) {
              const jsi::String propString = (*propIDValOpt).asString(rt_);
#ifdef HERMESVM_API_TRACE_DEBUG
              assert(propString.utf8(rt_) == spr.propNameDbg_);
#endif
              obj.setProperty(
                  rt_,
                  propString,
                  traceValueToJSIValue(
                      rt_, trace_, getJSIValueForUse, spr.value_));
            } else {
              auto propNameID = getPropNameIDForUse(spr.propID_);
#ifdef HERMESVM_API_TRACE_DEBUG
              assert(propNameID.utf8(rt_) == spr.propNameDbg_);
#endif
              obj.setProperty(
                  rt_,
                  propNameID,
                  traceValueToJSIValue(
                      rt_, trace_, getJSIValueForUse, spr.value_));
            }
            break;
          }
          case RecordType::HasProperty: {
            const auto &hpr =
                static_cast<const SynthTrace::HasPropertyRecord &>(*rec);
            auto obj = getObjForUse(hpr.objID_);
            auto propIDValOpt = getJSIValueForUseOpt(hpr.propID_);
            // TODO(T111638575)
            if (propIDValOpt && propIDValOpt->isString()) {
              const jsi::String propString = (*propIDValOpt).asString(rt_);
#ifdef HERMESVM_API_TRACE_DEBUG
              assert(propString.utf8(rt_) == hpr.propNameDbg_);
#endif
              obj.hasProperty(rt_, propString);
            } else {
              auto propNameID = getPropNameIDForUse(hpr.propID_);
#ifdef HERMESVM_API_TRACE_DEBUG
              assert(propNameID.utf8(rt_) == hpr.propNameDbg_);
#endif
              obj.hasProperty(rt_, propNameID);
            }
            break;
          }
          case RecordType::GetPropertyNames: {
            const auto &gpnr =
                static_cast<const SynthTrace::GetPropertyNamesRecord &>(*rec);
            jsi::Array arr = getObjForUse(gpnr.objID_).getPropertyNames(rt_);
            addJSIValueToDefs(
                call,
                gpnr.propNamesID_,
                globalRecordNum,
                std::move(arr),
                locals);
            break;
          }
          case RecordType::CreateArray: {
            const auto &car =
                static_cast<const SynthTrace::CreateArrayRecord &>(*rec);
            // Make an array of the appropriate length to be used.
            addJSIValueToDefs(
                call,
                car.objID_,
                globalRecordNum,
                Array(rt_, car.length_),
                locals);
            break;
          }
          case RecordType::ArrayRead: {
            const auto &arr =
                static_cast<const SynthTrace::ArrayReadRecord &>(*rec);
            // Read from the specified array, and possibly define the result.
            auto value = getObjForUse(arr.objID_)
                             .asArray(rt_)
                             .getValueAtIndex(rt_, arr.index_);
            ifObjectAddToDefs(
                arr.value_, std::move(value), call, globalRecordNum, locals);
            break;
          }
          case RecordType::ArrayWrite: {
            const auto &awr =
                static_cast<const SynthTrace::ArrayWriteRecord &>(*rec);
            // Write to the array and give it the value.
            getObjForUse(awr.objID_)
                .asArray(rt_)
                .setValueAtIndex(
                    rt_,
                    awr.index_,
                    traceValueToJSIValue(
                        rt_, trace_, getJSIValueForUse, awr.value_));
            break;
          }
          case RecordType::CallFromNative: {
            const auto &cfnr =
                static_cast<const SynthTrace::CallFromNativeRecord &>(*rec);
            auto func = getObjForUse(cfnr.functionID_).asFunction(rt_);
            std::vector<Value> args;
            for (const auto &arg : cfnr.args_) {
              args.emplace_back(
                  traceValueToJSIValue(rt_, trace_, getJSIValueForUse, arg));
            }
            // Save the return result into retval so that ReturnToNative can
            // access it and put it at the correct object id.
            const Value *argStart = args.data();
            if (cfnr.thisArg_.isUndefined()) {
              retval = func.call(rt_, argStart, args.size());
            } else {
              assert(
                  cfnr.thisArg_.isObject() &&
                  "Encountered a thisArg which was not undefined or an object");
              retval = func.callWithThis(
                  rt_,
                  getObjForUse(cfnr.thisArg_.getUID()),
                  argStart,
                  args.size());
            }
            break;
          }
          case RecordType::ConstructFromNative: {
            const auto &cfnr =
                static_cast<const SynthTrace::ConstructFromNativeRecord &>(
                    *rec);
            // Essentially the same implementation as CallFromNative, except
            // calls the construct path.
            auto func = getObjForUse(cfnr.functionID_).asFunction(rt_);
            std::vector<Value> args;
            for (const auto &arg : cfnr.args_) {
              args.emplace_back(
                  traceValueToJSIValue(rt_, trace_, getJSIValueForUse, arg));
            }
            assert(
                cfnr.thisArg_.isUndefined() &&
                "The this arg should always be undefined for a construct call");
            // Save the return result into retval so that ReturnToNative can
            // access it and put it at the correct object id.
            const Value *argStart = args.data();
            retval = func.callAsConstructor(rt_, argStart, args.size());
            break;
          }
          case RecordType::ReturnFromNative: {
            const auto &rfnr =
                dynamic_cast<const SynthTrace::ReturnFromNativeRecord &>(*rec);
            return traceValueToJSIValue(
                rt_, trace_, getJSIValueForUse, rfnr.retVal_);
          }
          case RecordType::ReturnToNative: {
            const auto &rtnr =
                dynamic_cast<const SynthTrace::ReturnToNativeRecord &>(*rec);
            ifObjectAddToDefs(
                rtnr.retVal_, std::move(retval), call, globalRecordNum, locals);
            // If the return value wasn't an object, it can be ignored.
            break;
          }
          case RecordType::CallToNative: {
            const auto &ctnr =
                static_cast<const SynthTrace::CallToNativeRecord &>(*rec);
            // Associate the this arg with its object id.
            ifObjectAddToDefs(
                ctnr.thisArg_,
                std::move(thisVal),
                call,
                globalRecordNum,
                locals,
                /* isThis = */ true);
            // Associate each argument with its object id.
            assert(
                ctnr.args_.size() == count &&
                "Called at runtime with a different number of args than "
                "the trace expected");
            for (uint64_t i = 0; i < ctnr.args_.size(); ++i) {
              ifObjectAddToDefs(
                  ctnr.args_[i],
                  std::move(args[i]),
                  call,
                  globalRecordNum,
                  locals);
            }
            break;
          }
          case RecordType::GetPropertyNative: {
            assert(count == 0 && "Should have no arguments");
            const auto &gpnr =
                static_cast<const SynthTrace::GetPropertyNativeRecord &>(*rec);
            // The propName is a definition.
            assert(nativePropNameToConsumeAsDef);
            // This must be the first record in the call.
            assert(rec == firstRec);
            addPropNameIDToDefs(
                call,
                gpnr.propNameID_,
                globalRecordNum,
                *nativePropNameToConsumeAsDef,
                pniLocals);
            // Don't add this to the locals, it is not technically provided to
            // the function.
            break;
          }
          case RecordType::GetPropertyNativeReturn: {
            const auto &gpnrr =
                dynamic_cast<const SynthTrace::GetPropertyNativeReturnRecord &>(
                    *rec);
            return traceValueToJSIValue(
                rt_, trace_, getObjForUse, gpnrr.retVal_);
          }
          case RecordType::SetPropertyNative: {
            const auto &spnr =
                static_cast<const SynthTrace::SetPropertyNativeRecord &>(*rec);
            assert(
                count == 1 &&
                "There should be exactly one argument to SetPropertyNative");
            // The propName is a definition.
            assert(nativePropNameToConsumeAsDef);
            // This must be the first record in the call.
            assert(rec == firstRec);
            addPropNameIDToDefs(
                call,
                spnr.propNameID_,
                globalRecordNum,
                *nativePropNameToConsumeAsDef,
                pniLocals);
            // Associate the single argument with its object id (if it's an
            // object).
            ifObjectAddToDefs(
                spnr.value_, std::move(args[0]), call, globalRecordNum, locals);
            break;
          }
          case RecordType::SetPropertyNativeReturn: {
            const auto &spnr =
                static_cast<const SynthTrace::SetPropertyNativeRecord &>(*rec);
            // The propName is a definition.
            assert(nativePropNameToConsumeAsDef);
            // This must be the first record in the call.
            assert(rec == firstRec);
            addPropNameIDToDefs(
                call,
                spnr.propNameID_,
                globalRecordNum,
                *nativePropNameToConsumeAsDef,
                pniLocals);
            // Since a SetPropertyNative does not have a return value, return
            // undefined.
            return Value::undefined();
            break;
          }
          case RecordType::GetNativePropertyNames: {
            // Nothing actually needs to happen here, as no defs are provided to
            // the local function. The HostObject already handles accessing the
            // property names.
            break;
          }
          case RecordType::GetNativePropertyNamesReturn: {
            // Accessing the list of property names on a host object doesn't
            // return a JS value.
            return Value::undefined();
            break;
          }
        }
      } catch (const std::exception &e) {
        crashOnException(e, globalRecordNum);
      }
      // If the top of the stack is reached after a marker flushed the stats,
      // exit early.
      if (depth_ == 1 && markerFound_) {
        return Value::undefined();
      }
      globalRecordNum++;
    }
  }
  if (depth_ != 1) {
    // For the non-entry point, there should always be an explicit
    // ReturnFromNative or Get/SetPropertyNativeReturn which will return early
    // from this function. If there was no explicit return, the trace is
    // malformed.
    llvm_unreachable("There was no return in the call");
  }
  // Return undefined for the entry point, it is ignored anyway.
  return Value::undefined();
}