// Copyright 2017 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "src/objects/module.h"

#include <unordered_map>
#include <unordered_set>

#include "include/v8-template.h"
#include "src/api/api-inl.h"
#include "src/ast/modules.h"
#include "src/builtins/accessors.h"
#include "src/common/assert-scope.h"
#include "src/heap/heap-inl.h"
#include "src/objects/cell-inl.h"
#include "src/objects/hash-table-inl.h"
#include "src/objects/js-generator-inl.h"
#include "src/objects/module-inl.h"
#include "src/objects/objects-inl.h"
#include "src/objects/property-descriptor.h"
#include "src/objects/source-text-module.h"
#include "src/objects/synthetic-module-inl.h"
#include "src/utils/ostreams.h"

namespace v8 {
namespace internal {

namespace {
#ifdef DEBUG
void PrintModuleName(Tagged<Module> module, std::ostream& os) {
  if (IsSourceTextModule(module)) {
    Print(Cast<SourceTextModule>(module)->GetScript()->GetNameOrSourceURL(),
          os);
  } else {
    Print(Cast<SyntheticModule>(module)->name(), os);
  }
#ifndef OBJECT_PRINT
  os << "\n";
#endif  // OBJECT_PRINT
}

void PrintStatusTransition(Tagged<Module> module, Module::Status old_status) {
  if (!v8_flags.trace_module_status) return;
  StdoutStream os;
  os << "Changing module status from " << Module::StatusString(old_status)
     << " to "
     << Module::StatusString(static_cast<Module::Status>(module->status()))
     << " for ";
  PrintModuleName(module, os);
}

void PrintStatusMessage(Tagged<Module> module, const char* message) {
  if (!v8_flags.trace_module_status) return;
  StdoutStream os;
  os << "Instantiating module ";
  PrintModuleName(module, os);
}
#endif  // DEBUG

void SetStatusInternal(Tagged<Module> module, Module::Status new_status) {
  DisallowGarbageCollection no_gc;
#ifdef DEBUG
  Module::Status old_status = static_cast<Module::Status>(module->status());
  module->set_status(new_status);
  PrintStatusTransition(module, old_status);
#else
  module->set_status(new_status);
#endif  // DEBUG
}

}  // end namespace

#ifdef DEBUG
// static
const char* Module::StatusString(Module::Status status) {
  switch (status) {
    case Module::kUnlinked:
      return "Unlinked";
    case Module::kPreLinking:
      return "PreLinking";
    case Module::kLinking:
      return "Linking";
    case Module::kLinked:
      return "Linked";
    case Module::kEvaluating:
      return "Evaluating";
    case Module::kEvaluatingAsync:
      return "EvaluatingAsync";
    case Module::kEvaluated:
      return "Evaluated";
    case Module::kErrored:
      return "Errored";
  }
}
#endif  // DEBUG

void Module::SetStatus(Status new_status) {
  DisallowGarbageCollection no_gc;
  DCHECK_LE(status(), new_status);
  DCHECK_NE(new_status, Module::kErrored);
  SetStatusInternal(*this, new_status);
}

void Module::RecordError(Isolate* isolate, Tagged<Object> error) {
  DisallowGarbageCollection no_gc;
  // Allow overriding exceptions with termination exceptions.
  DCHECK_IMPLIES(isolate->is_catchable_by_javascript(error),
                 IsTheHole(exception(), isolate));
  DCHECK(!IsTheHole(error, isolate));
  if (IsSourceTextModule(*this)) {
    // Revert to minimal SFI in case we have already been instantiating or
    // evaluating.
    auto self = Cast<SourceTextModule>(*this);
    self->set_code(self->GetSharedFunctionInfo());
  }
  SetStatusInternal(*this, Module::kErrored);
  if (isolate->is_catchable_by_javascript(error)) {
    set_exception(error);
  } else {
    // v8::TryCatch uses `null` for termination exceptions.
    set_exception(ReadOnlyRoots(isolate).null_value());
  }
}

void Module::ResetGraph(Isolate* isolate, DirectHandle<Module> module) {
  DCHECK_NE(module->status(), kEvaluating);
  if (module->status() != kPreLinking && module->status() != kLinking) {
    return;
  }

  DirectHandle<FixedArray> requested_modules =
      IsSourceTextModule(*module)
          ? DirectHandle<FixedArray>(
                Cast<SourceTextModule>(*module)->requested_modules(), isolate)
          : DirectHandle<FixedArray>();
  Reset(isolate, module);

  if (!IsSourceTextModule(*module)) {
    DCHECK(IsSyntheticModule(*module));
    return;
  }
  for (int i = 0; i < requested_modules->length(); ++i) {
    DirectHandle<Object> descendant(requested_modules->get(i), isolate);
    if (IsModule(*descendant)) {
      ResetGraph(isolate, Cast<Module>(descendant));
    } else {
      // The requested module is either an undefined or a WasmModule object.
#if V8_ENABLE_WEBASSEMBLY
      DCHECK(IsUndefined(*descendant, isolate) ||
             IsWasmModuleObject(*descendant));
#else
      DCHECK(IsUndefined(*descendant, isolate));
#endif
    }
  }
}

void Module::Reset(Isolate* isolate, DirectHandle<Module> module) {
  DCHECK(module->status() == kPreLinking || module->status() == kLinking);
  DCHECK(IsTheHole(module->exception(), isolate));
  // The namespace object cannot exist, because it would have been created
  // by RunInitializationCode, which is called only after this module's SCC
  // succeeds instantiation.
  DCHECK(!IsJSModuleNamespace(module->module_namespace()) &&
         !IsJSModuleNamespace(module->deferred_module_namespace()));
  const int export_count =
      IsSourceTextModule(*module)
          ? Cast<SourceTextModule>(*module)->regular_exports()->length()
          : Cast<SyntheticModule>(*module)->export_names()->length();
  DirectHandle<ObjectHashTable> exports =
      ObjectHashTable::New(isolate, export_count);

  if (IsSourceTextModule(*module)) {
    SourceTextModule::Reset(isolate, Cast<SourceTextModule>(module));
  }

  module->set_exports(*exports);
  SetStatusInternal(*module, kUnlinked);
}

Tagged<Object> Module::GetException() {
  DisallowGarbageCollection no_gc;
  DCHECK_EQ(status(), Module::kErrored);
  DCHECK(!IsTheHole(exception()));
  return exception();
}

MaybeHandle<Cell> Module::ResolveExport(Isolate* isolate, Handle<Module> module,
                                        DirectHandle<String> module_specifier,
                                        Handle<String> export_name,
                                        MessageLocation loc, bool must_resolve,
                                        Module::ResolveSet* resolve_set) {
  DCHECK_GE(module->status(), kPreLinking);
  DCHECK_NE(module->status(), kEvaluating);

  if (IsSourceTextModule(*module)) {
    return SourceTextModule::ResolveExport(
        isolate, Cast<SourceTextModule>(module), module_specifier, export_name,
        loc, must_resolve, resolve_set);
  } else {
    return SyntheticModule::ResolveExport(
        isolate, Cast<SyntheticModule>(module), module_specifier, export_name,
        loc, must_resolve);
  }
}

bool Module::Instantiate(Isolate* isolate, Handle<Module> module,
                         v8::Local<v8::Context> context,
                         const Module::UserResolveCallbacks& callbacks) {
#ifdef DEBUG
  PrintStatusMessage(*module, "Instantiating module ");
#endif  // DEBUG

  if (!PrepareInstantiate(isolate, module, context, callbacks)) {
    ResetGraph(isolate, module);
    DCHECK_EQ(module->status(), kUnlinked);
    return false;
  }
  Zone zone(isolate->allocator(), ZONE_NAME);
  ZoneForwardList<Handle<SourceTextModule>> stack(&zone);
  unsigned dfs_index = 0;
  if (!FinishInstantiate(isolate, module, &stack, &dfs_index, &zone)) {
    ResetGraph(isolate, module);
    DCHECK_EQ(module->status(), kUnlinked);
    return false;
  }
  DCHECK_GE(module->status(), kLinked);
  DCHECK(stack.empty());
  return true;
}

bool Module::PrepareInstantiate(Isolate* isolate, DirectHandle<Module> module,
                                v8::Local<v8::Context> context,
                                const UserResolveCallbacks& callbacks) {
  DCHECK_NE(module->status(), kEvaluating);
  DCHECK_NE(module->status(), kLinking);
  if (module->status() >= kPreLinking) return true;
  module->SetStatus(kPreLinking);
  STACK_CHECK(isolate, false);

  if (IsSourceTextModule(*module)) {
    return SourceTextModule::PrepareInstantiate(
        isolate, Cast<SourceTextModule>(module), context, callbacks);
  } else {
    return SyntheticModule::PrepareInstantiate(
        isolate, Cast<SyntheticModule>(module), context);
  }
}

bool Module::FinishInstantiate(Isolate* isolate, Handle<Module> module,
                               ZoneForwardList<Handle<SourceTextModule>>* stack,
                               unsigned* dfs_index, Zone* zone) {
  DCHECK_NE(module->status(), kEvaluating);
  if (module->status() >= kLinking) return true;
  DCHECK_EQ(module->status(), kPreLinking);
  STACK_CHECK(isolate, false);

  if (IsSourceTextModule(*module)) {
    return SourceTextModule::FinishInstantiate(
        isolate, Cast<SourceTextModule>(module), stack, dfs_index, zone);
  } else {
    return SyntheticModule::FinishInstantiate(isolate,
                                              Cast<SyntheticModule>(module));
  }
}

MaybeDirectHandle<Object> Module::Evaluate(Isolate* isolate,
                                           Handle<Module> module) {
#ifdef DEBUG
  PrintStatusMessage(*module, "Evaluating module ");
#endif  // DEBUG
  int module_status = module->status();

  // In the event of errored evaluation, return a rejected promise.
  if (module_status == kErrored) {
    // If we have a top level capability we assume it has already been
    // rejected, and return it here. Otherwise create a new promise and
    // reject it with the module's exception.
    if (IsJSPromise(module->top_level_capability())) {
      DirectHandle<JSPromise> top_level_capability(
          Cast<JSPromise>(module->top_level_capability()), isolate);
      DCHECK(top_level_capability->status() == Promise::kRejected &&
             top_level_capability->result() == module->exception());
      return top_level_capability;
    }
    DirectHandle<JSPromise> capability = isolate->factory()->NewJSPromise();
    JSPromise::Reject(capability, direct_handle(module->exception(), isolate));
    return capability;
  }

  // Start of Evaluate () Concrete Method
  // 2. Assert: module.[[Status]] is one of LINKED, EVALUATING-ASYNC, or
  //    EVALUATED.
  CHECK(module_status == kLinked || module_status == kEvaluatingAsync ||
        module_status == kEvaluated);

  // 3. If module.[[Status]] is either EVALUATING-ASYNC or EVALUATED, set module
  //    to module.[[CycleRoot]].
  // A Synthetic Module has no children so it is its own cycle root.
  if (module_status >= kEvaluatingAsync && IsSourceTextModule(*module)) {
    module = Cast<SourceTextModule>(module)->GetCycleRoot(isolate);
  }

  // 4. If module.[[TopLevelCapability]] is not EMPTY, then
  //    a. Return module.[[TopLevelCapability]].[[Promise]].
  if (IsJSPromise(module->top_level_capability())) {
    return direct_handle(Cast<JSPromise>(module->top_level_capability()),
                         isolate);
  }
  DCHECK(IsUndefined(module->top_level_capability()));

  if (IsSourceTextModule(*module)) {
    return SourceTextModule::Evaluate(isolate, Cast<SourceTextModule>(module));
  } else {
    return SyntheticModule::Evaluate(isolate, Cast<SyntheticModule>(module));
  }
}

Handle<Cell> Module::GetModuleNamespaceCell(Isolate* isolate,
                                            Handle<Module> module,
                                            ModuleImportPhase phase) {
  ReadOnlyRoots roots(isolate);

  DCHECK(phase == ModuleImportPhase::kEvaluation ||
         phase == ModuleImportPhase::kDefer);

  Tagged<Object> maybe_cell = phase == ModuleImportPhase::kEvaluation
                                  ? module->module_namespace()
                                  : module->deferred_module_namespace();
  if (!IsUndefined(maybe_cell, roots)) {
    return handle(Cast<Cell>(maybe_cell), isolate);
  }
  Handle<Cell> cell = isolate->factory()->NewCell();
  if (phase == ModuleImportPhase::kEvaluation) {
    module->set_module_namespace(*cell);
  } else {
    module->set_deferred_module_namespace(*cell);
  }
  return cell;
}

DirectHandle<JSModuleNamespace> Module::GetModuleNamespace(
    Isolate* isolate, Handle<Module> module, ModuleImportPhase phase) {
  ReadOnlyRoots roots(isolate);

  DCHECK(phase == ModuleImportPhase::kEvaluation ||
         phase == ModuleImportPhase::kDefer);
  DirectHandle<Cell> ns_cell = GetModuleNamespaceCell(isolate, module, phase);

  if (auto cur = ns_cell->value(); !IsUndefined(cur, roots)) {
    return direct_handle(Cast<JSModuleNamespace>(cur), isolate);
  }

  // Collect the export names.
  Zone zone(isolate->allocator(), ZONE_NAME);
  UnorderedModuleSet visited(&zone);

  if (IsSourceTextModule(*module)) {
    SourceTextModule::FetchStarExports(isolate, Cast<SourceTextModule>(module),
                                       &zone, &visited);
  }

  DirectHandle<ObjectHashTable> exports(module->exports(), isolate);
  ZoneVector<IndirectHandle<String>> names(&zone);
  names.reserve(exports->NumberOfElements());
  for (InternalIndex i : exports->IterateEntries()) {
    Tagged<Object> key;
    if (!exports->ToKey(roots, i, &key)) continue;
    names.push_back(handle(Cast<String>(key), isolate));
  }
  DCHECK_EQ(static_cast<int>(names.size()), exports->NumberOfElements());

  // Sort them alphabetically.
  std::sort(names.begin(), names.end(),
            [&isolate](IndirectHandle<String> a, IndirectHandle<String> b) {
              return String::Compare(isolate, a, b) ==
                     ComparisonResult::kLessThan;
            });

  // Create the namespace object (initially empty).
  DirectHandle<JSModuleNamespace> ns =
      phase == ModuleImportPhase::kEvaluation
          ? isolate->factory()->NewJSModuleNamespace()
          : isolate->factory()->NewJSDeferredModuleNamespace();
  ns->set_module(*module);
  ns_cell->set_value(*ns);

  // Create the properties in the namespace object. Transition the object
  // to dictionary mode so that property addition is faster.
  PropertyAttributes attr = DONT_DELETE;
  JSObject::NormalizeProperties(isolate, ns, CLEAR_INOBJECT_PROPERTIES,
                                static_cast<int>(names.size()),
                                "JSModuleNamespace");
  JSObject::NormalizeElements(isolate, ns);
  DirectHandle<AccessorInfo> module_namespace_property_accessor =
      isolate->factory()->module_namespace_property_accessor();
  for (const auto& name : names) {
    uint32_t index = 0;
    if (name->AsArrayIndex(&index)) {
      JSObject::SetNormalizedElement(
          ns, index, module_namespace_property_accessor,
          PropertyDetails(PropertyKind::kAccessor, attr,
                          PropertyCellType::kMutable));
    } else {
      JSObject::SetNormalizedProperty(
          ns, name, module_namespace_property_accessor,
          PropertyDetails(PropertyKind::kAccessor, attr,
                          PropertyCellType::kMutable));
    }
  }
  JSObject::PreventExtensions(isolate, ns, kThrowOnError).ToChecked();

  // Optimize the namespace object as a prototype, for two reasons:
  // - The object's map is guaranteed not to be shared. ICs rely on this.
  // - We can store a pointer from the map back to the namespace object.
  //   Turbofan can use this for inlining the access.
  JSObject::OptimizeAsPrototype(ns);

  DirectHandle<PrototypeInfo> proto_info =
      Map::GetOrCreatePrototypeInfo(ns, isolate);
  proto_info->set_module_namespace(*ns);
  return ns;
}

bool JSModuleNamespace::HasExport(Isolate* isolate, DirectHandle<String> name) {
  DirectHandle<Object> object(module()->exports()->Lookup(name), isolate);
  return !IsTheHole(*object, isolate);
}

MaybeDirectHandle<Object> JSModuleNamespace::GetExport(
    Isolate* isolate, DirectHandle<String> name) {
  DirectHandle<Object> object(module()->exports()->Lookup(name), isolate);
  if (IsTheHole(*object, isolate)) {
    return isolate->factory()->undefined_value();
  }

  DirectHandle<Object> value(Cast<Cell>(*object)->value(), isolate);
  if (IsTheHole(*value, isolate)) {
    // According to https://tc39.es/ecma262/#sec-InnerModuleLinking
    // step 10 and
    // https://tc39.es/ecma262/#sec-source-text-module-record-initialize-environment
    // step 8-25, variables must be declared in Link. And according to
    // https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-get-p-receiver,
    // here accessing uninitialized variable error should be thrown.
    THROW_NEW_ERROR(isolate,
                    NewReferenceError(
                        MessageTemplate::kAccessedUninitializedVariable, name));
  }

  return value;
}

Maybe<PropertyAttributes> JSModuleNamespace::GetPropertyAttributes(
    LookupIterator* it) {
  DirectHandle<JSModuleNamespace> object = it->GetHolder<JSModuleNamespace>();
  DirectHandle<String> name = Cast<String>(it->GetName());
  DCHECK(it->state() == LookupIterator::ACCESSOR ||
         it->state() == LookupIterator::MODULE_NAMESPACE);

  Isolate* isolate = it->isolate();

  DirectHandle<Object> lookup(object->module()->exports()->Lookup(name),
                              isolate);
  if (IsTheHole(*lookup, isolate)) return Just(ABSENT);

  DirectHandle<Object> value(Cast<Cell>(lookup)->value(), isolate);
  if (IsTheHole(*value, isolate)) {
    isolate->Throw(*isolate->factory()->NewReferenceError(
        MessageTemplate::kNotDefined, name));
    return Nothing<PropertyAttributes>();
  }

  if (IsJSDeferredModuleNamespace(*object)) [[unlikely]] {
    return Just(DONT_DELETE);
  } else {
    return Just(it->property_attributes());
  }
}

// https://tc39.es/proposal-defer-import-eval/#sec-EvaluateModuleSync
void JSDeferredModuleNamespace::EvaluateModuleSync(
    Isolate* isolate, DirectHandle<JSDeferredModuleNamespace> holder) {
  Handle<Module> module = handle(holder->module(), isolate);

  // https://tc39.es/proposal-defer-import-eval/#sec-GetModuleExportsList
  Zone zone(isolate->allocator(), ZONE_NAME);
  UnorderedModuleSet seenModules(&zone);
  if (!SourceTextModule::ReadyForSyncExecution(isolate, module, &seenModules)) {
    isolate->Throw(*isolate->factory()->NewTypeError(
        MessageTemplate::kNotReadyForSyncExec));
    return;
  }

  MaybeDirectHandle<Object> maybe_result = Module::Evaluate(isolate, module);
  DirectHandle<Object> result;
  if (!maybe_result.ToHandle(&result)) {
    return;
  }

  // If there's a result, it needs to be a promise with either Reject or
  // Fulfilled status.
  DCHECK(IsJSPromise(*result));
  DirectHandle<JSPromise> promise = Cast<JSPromise>(result);
  // 5. If promise.[[PromiseState]] is rejected, then
  if (promise->status() == Promise::kRejected) {
    // a. If promise.[[PromiseIsHandled]] is false, perform
    // HostPromiseRejectionTracker(promise, "handle").
    if (!promise->has_handler()) {
      isolate->ReportPromiseReject(promise, DirectHandle<Object>(),
                                   v8::kPromiseHandlerAddedAfterReject);
    }
    promise->set_has_handler(true);
    isolate->Throw(promise->result());
    return;
  }
  DCHECK_EQ(promise->status(), Promise::kFulfilled);
}

bool JSDeferredModuleNamespace::TriggersEvaluation(LookupIterator* it) {
  DirectHandle<JSModuleNamespace> ns = it->GetHolder<JSModuleNamespace>();
  if (IsJSDeferredModuleNamespace(*ns)) {
    Isolate* isolate = it->isolate();
    DirectHandle<Name> name = it->GetName();
    // https://tc39.es/proposal-defer-import-eval/#sec-IsSymbolLikeNamespaceKey
    if (*name == ReadOnlyRoots(isolate).then_string() || IsSymbol(*name)) {
      return false;
    }
    return ns->module()->status() != Module::kEvaluated;
  }

  return false;
}

// ES
// https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-defineownproperty-p-desc
// static
Maybe<bool> JSModuleNamespace::DefineOwnProperty(
    Isolate* isolate, DirectHandle<JSModuleNamespace> object,
    DirectHandle<Object> key, PropertyDescriptor* desc,
    Maybe<ShouldThrow> should_throw) {
  // 1. If Type(P) is Symbol, return OrdinaryDefineOwnProperty(O, P, Desc).
  if (IsSymbol(*key)) {
    return OrdinaryDefineOwnProperty(isolate, object, key, desc, should_throw);
  }

  // 2. Let current be ? O.[[GetOwnProperty]](P).
  PropertyKey lookup_key(isolate, key);
  LookupIterator it(isolate, object, lookup_key, LookupIterator::OWN);
  PropertyDescriptor current;
  Maybe<bool> has_own = GetOwnPropertyDescriptor(&it, &current);
  MAYBE_RETURN(has_own, Nothing<bool>());

  // 3. If current is undefined, return false.
  // 4. If Desc.[[Configurable]] is present and has value true, return false.
  // 5. If Desc.[[Enumerable]] is present and has value false, return false.
  // 6. If ! IsAccessorDescriptor(Desc) is true, return false.
  // 7. If Desc.[[Writable]] is present and has value false, return false.
  // 8. If Desc.[[Value]] is present, return
  //    SameValue(Desc.[[Value]], current.[[Value]]).
  if (!has_own.FromJust() ||
      (desc->has_configurable() && desc->configurable()) ||
      (desc->has_enumerable() && !desc->enumerable()) ||
      PropertyDescriptor::IsAccessorDescriptor(desc) ||
      (desc->has_writable() && !desc->writable()) ||
      (desc->has_value() &&
       !Object::SameValue(*desc->value(), *current.value()))) {
    RETURN_FAILURE(isolate, GetShouldThrow(isolate, should_throw),
                   NewTypeError(MessageTemplate::kRedefineDisallowed, key));
  }

  return Just(true);
}

bool Module::IsGraphAsync(Isolate* isolate) const {
  DisallowGarbageCollection no_gc;

  // Only SourceTextModules may be async.
  if (!IsSourceTextModule(*this)) return false;
  Tagged<SourceTextModule> root = Cast<SourceTextModule>(*this);
  DCHECK_GE(root->status(), kLinked);

  Zone zone(isolate->allocator(), ZONE_NAME);
  const size_t bucket_count = 2;
  ZoneUnorderedSet<Tagged<SourceTextModule>, Module::Hash> visited(
      &zone, bucket_count);
  ZoneVector<Tagged<SourceTextModule>> worklist(&zone);
  visited.insert(root);
  worklist.push_back(root);

  do {
    Tagged<SourceTextModule> current = worklist.back();
    worklist.pop_back();
    DCHECK_GE(current->status(), kLinked);

    if (current->has_toplevel_await()) return true;
    Tagged<FixedArray> requested_modules = current->requested_modules();
    for (int i = 0, length = requested_modules->length(); i < length; ++i) {
      Tagged<Object> raw_descendant = requested_modules->get(i);
      // The current module must have been linked as the root has been linked.
      // If the request is a source phase import, the descendant can be a
      // JavaScript object, and it can not be async. Skip it.
      // If the request is an evaluation phase import, the descendant can be
      // either a SourceTextModule or a SyntheticModule. Visit it if it is a
      // SourceTextModule.
      if (IsSourceTextModule(raw_descendant)) {
        Tagged<SourceTextModule> descendant =
            Cast<SourceTextModule>(raw_descendant);
        const bool cycle = !visited.insert(descendant).second;
        if (!cycle) worklist.push_back(descendant);
      }
    }
  } while (!worklist.empty());

  return false;
}

}  // namespace internal
}  // namespace v8
