Skip to content

Local Module Import & Export

Ian Yong edited this page Jan 15, 2023 · 14 revisions

Overview

The importing & exporting of local modules (not to be confused with Source modules) is available in Source 2+. It is done as a preprocessing step where the Abstract Syntax Trees (ASTs) that are parsed from the various files/modules involved are transformed in such a way that outputs a single AST that handles all imports & exports.

Local Modules vs Source Modules

In Source, we make the distinction between the following modules:

  1. Source modules, which can be thought of as libraries which can be imported from. There is a separate system for handling these in js-slang. The modules that are available can be found in the modules repository.
  2. Local modules, which are essentially user-written files to allow for multi-file Source programs.

More details on how this distinction is made can be found below.

Example Program

How the AST transformation works is best described by the equivalent code.

Say we have a Source 2+ program consisting of 4 files where /a.js is the entrypoint file (i.e., the file containing the code that is the entrypoint of the program - because evaluation of any program needs to have a defined starting point):

  • /a.js
    import { a as x, b as y } from "./b.js";
    
    x + y;
  • /b.js
    import y, { square } from "./c.js";
    
    const a = square(y);
    const b = 3;
    export { a, b };
  • /c.js
    import { mysteryFunction } from "./d.js";
    
    const x = mysteryFunction(5);
    export function square(x) {
      return x * x;
    }
    export default x;
  • /d.js
    const addTwo = x => x + 2;
    export { addTwo as mysteryFunction };

The resulting AST after transformation will be equivalent to the following Source code:

function __$b$dot$js__(___$c$dot$js___) {
  const y = __access_export__(___$c$dot$js___, "default");
  const square = __access_export__(___$c$dot$js___, "square");

  const a = square(y);
  const b = 3;

  return pair(null, list(pair("a", a), pair("b", b)));
}

function __$c$dot$js__(___$d$dot$js___) {
  const mysteryFunction = __access_export__(___$d$dot$js___, "mysteryFunction");

  const x = mysteryFunction(5);
  function square(x) {
    return x * x;
  }

  return pair(x, list(pair("square", square)));
}

function __$d$dot$js__() {
  const addTwo = x => x + 2;

  return pair(null, list(pair("mysteryFunction", addTwo)));
}

const ___$d$dot$js___ = __$d$dot$js__();
const ___$c$dot$js___ = __$c$dot$js__(___$d$dot$js___);
const ___$b$dot$js___ = __$b$dot$js__(___$c$dot$js___);

const x = __access_export__(___$b$dot$js___, "a");
const y = __access_export__(___$b$dot$js___, "b");

x + y;

Note however that the transformation is not done as the syntax (code) level, but at the AST level. The above example merely serves as a mental model for how imports/exports work.

Handling Imports/Exports as a Preprocessing Step

The evaluation of a Source program follows the following high-level steps:

  1. First, the entrypoint file is parsed into an AST.
  2. The AST of the entrypoint file is then filtered for import declarations.
  3. For each import declaration corresponding to a local file/module, we parse the corresponding file into its own AST.
  4. Then, we recursively filter its AST for import declarations to determine which other files to parse. While this is done, we build up a directed graph of local module imports.
  5. If our module graph is not acyclic (meaning that there are circular imports), the program evaluation fails. This is because our mental model is sequential and cannot support cyclic imports.
  6. Otherwise, every AST except the one corresponding to the entrypoint file is transformed into a function whose parameters are the results of the transformed functions it imports from, and its return value is a structure containing all of its exported values.
  7. Finally, all of these transformed functions and their invocations are assembled into the final AST.