5.9 Run main module

Up to this point, we have successfully completed the initialization process for different components within Deno. Our main goal in initiating Deno was to run a basic "hello world" program. However, up until now, Deno has not yet engaged with our actual code—except for organizing the module specifier. The present moment marks the juncture where we delve into the steps of arranging, loading, and ultimately running our code.

Let's take a look at the code responsible for executing the main module:

let exit_code = worker.run().await?;

Recall that the main worker was already created for the main module:

let mut worker = worker_factory
    .create_main_worker(main_module, permissions)
    .await?;

Overview

The awaited moment has arrived with the introduction of the Run() function in Deno. This remarkable function serves as the catalyst for executing our code. Although it might seem simple with only two steps, the impact of these steps is profound and far-reaching.

Let's delve into the intricacies of these two fundamental steps:

  • Preload module -- The first step entails the preloading of the module. This involves a meticulous process of recursively fetching not only the module itself but also its interconnected dependencies. The web of connections is delicately woven as each dependent module is brought into the fold. -- Once the fetching is complete, the loaded modules are seamlessly integrated into the v8 engine, where their contents are prepared for execution.

  • Instantiate the main module -- The second step involves the instantiation of the main module. This module, representing the heart of our code, is meticulously prepared for execution. -- The module undergoes a rigorous evaluation process, where its contents are scrutinized and prepared for interpretation. Within the v8 engine, the module's instructions are comprehended and organized. -- With everything in place, the module is set to be executed within the v8 engine. The culmination of this execution triggers a crucial waiting period, during which the module's evaluation result is anticipated.

  • Run event loop -- The completion of these preparatory steps paves the way for the commencement of the event loop. This is the dynamic core of Deno's execution, where various asynchronous tasks are managed and orchestrated.

No matter how intricate the application might be, the beginning always takes place within the main module. This process occurs in a recursive manner, starting from the core, which is the main module itself. Since this operation involves asynchronous actions, the 'await' keyword is used at its conclusion.

Naturally, the execution of the 'run' function takes place within the main worker. This is a component of the central processing thread. Now, let's delve into the details of how the 'run' function is implemented:

pub async fn run(&mut self) -> Result<i32, AnyError> {
    let mut maybe_coverage_collector =
      self.maybe_setup_coverage_collector().await?;
    log::debug!("main_module {}", self.main_module);

    if self.is_main_cjs {
      deno_node::load_cjs_module(
        &mut self.worker.js_runtime,
        &self.main_module.to_file_path().unwrap().to_string_lossy(),
        true,
        self.shared.options.inspect_brk,
      )?;
    } else {
      self.execute_main_module_possibly_with_npm().await?;
    }

    self.worker.dispatch_load_event(located_script_name!())?;

    loop {
      self
        .worker
        .run_event_loop(maybe_coverage_collector.is_none())
        .await?;
      if !self
        .worker
        .dispatch_beforeunload_event(located_script_name!())?
      {
        break;
      }
    }

    self.worker.dispatch_unload_event(located_script_name!())?;

    if let Some(coverage_collector) = maybe_coverage_collector.as_mut() {
      self
        .worker
        .with_event_loop(coverage_collector.stop_collecting().boxed_local())
        .await?;
    }

    Ok(self.worker.exit_code())
  }

Module loading stands as a pivotal component within Deno, with all the associated code operating asynchronously. Our focus will delve into an intricate exploration of module loading, unraveling its inner workings concerning our code, particularly the main module. To embark on this journey, we commence with a crucial initial phase known as "preload" which is hidden inside the function execute_main_module_possibly_with_npm.

Preload module

The preload module is used to load and instantiate any ES module. This isn't limited to the main module. It is applicable to any ES module.

The code of preload_module is very simple:

pub async fn execute_main_module_possibly_with_npm(
    &mut self,
  ) -> Result<(), AnyError> {
    let id = self.worker.preload_main_module(&self.main_module).await?;
    self.evaluate_module_possibly_with_npm(id).await
}

pub async fn preload_main_module(
    &mut self,
    module_specifier: &ModuleSpecifier,
  ) -> Result<ModuleId, AnyError> {
    self
      .js_runtime
      .load_main_module(module_specifier, None)
      .await
}

The function "preload_main_module" essentially delegates the task to the JavaScript runtime's "load_main_module" function. The process of loading the main module takes place in a sequence of four steps:

  1. Loading the Main Module: Initially, the main module is loaded into the runtime environment.

  2. Recursive Dependency Loading: The process then involves delving into the main module and subsequently loading all the dependencies it requires. This step is carried out recursively, meaning that dependencies of dependencies are also loaded as needed.

  3. Main Module Instantiation: After the dependencies are loaded, the main module is instantiated. This means that the code within the main module is executed and any initializations or setups are performed.

  4. Dependency Resolution: Once the main module is instantiated, the process moves on to resolve any dependencies. This involves making sure that all the required components are available and connected properly.

The outcome of the "load_module" function is an identification number for the module. This identification number needs to be immediately utilized when evaluating the module. This sequential procedure ensures that the main module and its dependencies are loaded, prepared, and connected appropriately within the Deno runtime environment.

Evaluate module

Evaluation is essentially the same as execution. This is the term used by V8, the JavaScript engine, and it's also the term Deno adopts. When we talk about evaluation, we're essentially asking V8 to carry out the instructions contained in the code. In Deno, it's the way we prompt V8 to execute the module that has been created.

However, it's important to note that the outcome of this evaluation doesn't show up right away. Instead, it appears as something called a "future." This future represents the result that we expect from the evaluation process. It's like a placeholder that will eventually be filled with the outcome of the evaluation. Only when the module evaluation is completely done, this future gets resolved – meaning it gets filled in with the actual result we were waiting for. This process ensures that we can keep track of the progress and completion of the evaluation while allowing us to work with the results effectively once they're ready.

The job of event loop is to keep track of the evaluation result.

Receive evaluation result

The process of evaluation in Deno involves asynchronous calls that reach their conclusions through various pathways. These pathways are as follows:

  1. Completion due to Tasks Done:

    • When all tasks within the main module have been executed.

      • This includes resolving any pending operations (ops).

      • Also, this covers the completion of dynamic import processing.

  2. Unhandled Exception:

    • If an unhandled exception occurs during the execution.

When there are no pending tasks remaining and no dynamic imports to process, the program's evaluation comes to a conclusion, signifying that the program has finished its intended operations. In the context of our uncomplicated example, it's anticipated that the evaluation process will promptly come to an end since there are no asynchronous operations or never-ending event listeners in the program. We will delve deeper into the intricacies of how this evaluation process operates a little later in the text, gaining a better understanding of its functioning.

--

Now that we have an understanding of the overall procedure, let's delve into the essential stages of loading and evaluation with a more comprehensive outlook. Initially, we will explore the intricacies of the loading process. In the subsequent segment, we'll examine the loading procedure of a basic program, such as the famous "hello world". This instance is uncomplicated due to the absence of imports. It consists of a solitary main module. Devoid of dependencies, the loading process becomes simpler, enabling us to grasp the fundamental principles more effectively. As we move forward into the following chapter, our focus will shift towards a program that incorporates several imports, adding complexity to the loading mechanism. This exploration will provide us with a more comprehensive comprehension of Deno's functionalities.

Last updated