-
Notifications
You must be signed in to change notification settings - Fork 105
Local Module Import & Export
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.
In Source, we make the distinction between the following modules:
- 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 themodules
repository. - 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.
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.
The evaluation of a Source program follows the following high-level steps:
- First, the entrypoint file is parsed into an AST.
- The AST of the entrypoint file is then filtered for import declarations.
- For each import declaration corresponding to a local file/module, we parse the corresponding file into its own AST.
- 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.
- 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.
- 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.
- Finally, all of these transformed functions and their invocations are assembled into the final AST.