Introduction to Scoping
Task description
Using
- The ast-types module and namely the visit (opens in a new tab) methods of the module to traverse the AST and
- The recast (opens in a new tab) module for code generation (see section recast in this notes),
extend the calculator we have implemented in previous labs to support
- comma,
- identifier,
- assignment, and
print
operations so that we can process input like this:
➜ scope-intro-solution git:(2024) cat test/data/input/test-id.calc
a = 4+i,
b = 2-2i,
print(a*b)
and now we can transpile this input using calc2js
:
➜ scope-intro-solution git:(2024) bin/calc2js.mjs test/data/input/test-id.calc
#!/usr/bin/env node
const { Complex, print } = require("/Users/casianorodriguezleon/campus-virtual/2223/pl2223/practicas/scope-intro/scope-intro-solution/src/support-lib.js");
let $a, $b;
($a = Complex("4").add(Complex("i")), $b = Complex("2").sub(Complex("2i"))), print($a.mul($b));
The output can be executed like this:
➜ scope-intro-solution git:(2024) bin/calc2js.mjs test/data/input/test-id.calc | node -
10 - 6i
Analysis of dependencies
The generated JS program imports from the support library src/support-lib.js
the functions that are necessary for the proper functioning of the source code. We import Complex
and print
since these are the only one that are required to work:
const { Complex, print } = require("/Users/casianorodriguezleon/campus-virtual/2223/pl2223/practicas/scope-intro/scope-intro-solution/src/support-lib.js");
If we consider an example where the factorial is also required:
➜ scope-intro-solution git:(2024) bin/calc2js.mjs test/data/input/test-exp-fact.calc
#!/usr/bin/env node
const { print, Complex, factorial } = require("/Users/casianorodriguezleon/campus-virtual/2223/pl2223/practicas/scope-intro/scope-intro-solution/src/support-lib.js");
print(Complex("2").pow(factorial(Complex("3"))));
Notice that pow
is a method of the Complex
objects, not a support function. We can
execute the program now:
➜ scope-intro-solution git:(2024) bin/calc2js.mjs test/data/input/test-exp-fact.calc | node -
64
Also notice the long path to the required support-lib.js
:
require("/Users/casianorodriguezleon/campus-virtual/2223/pl2223/practicas/scope-intro/scope-intro-solution/src/support-lib.js");
That path will change on each installation of the compiler and thus is our job to find out where the support lib is located. We will use __dirname
for that.
Analysis of initialized variables
Variables that are initialized in source such as a
and b
must be declared as global variables following the supporting code and preceding the generated JS program:
// ... Support code
let $a, $b;
($a = Complex("4").add(Complex("i")),
$b = Complex("2").sub(Complex("2i"))
),
print($a.mul($b));
Avoiding conflicts with reserved words and supporting names
Note also how input program variables are declared with a $
prefix to avoid conflicts with supporting variables, JS reserved words, etc. that may appear in the generated JS program.
For instance, if the input program is while = 4
the potential conflict with JS's while
is resolved by translating $while = 4
.
Declaration of variables
We will assume that in our calculator, similar to what happens in Ruby, as soon as a variable is initialized it is declared (in this case in the only global scope).
A program that contains use of an uninitialized/declared variable should give an execution error. For example:
➜ scope-intro-solution git:(2024) ✗ cat test/data/input/test-scope1.calc
a = 4+d+i,
b = 2-2i,
print(c)
will produce output like:
➜ scope-intro-solution git:(2024) ✗ bin/calc2js.mjs test/data/input/test-scope1.calc
Not declared variable 'd'
Not declared variable 'c'
#!/usr/bin/env node
const { Complex, print } = require("/Users/casianorodriguezleon/campus-virtual/2223/pl2223/practicas/scope-intro/scope-intro-solution/src/support-lib.js");
let $a, $b;
($a = Complex("4").add($d).add(Complex("i")), $b = Complex("2").sub(Complex("2i"))), print($c);
Calculator Compiler Phases
The compiler can be divided into the following phases:
#!/usr/bin/env node
const p = require("./calc").parser;
const fs = require('fs/promises');
const { scopeAnalysis, dependencies } = require('./scope.js');
const codeGen = require('./code-generation.js')
const writeCode = require('./write-code.js');
module.exports = async function transpile(inputFile, outputFile) {
let input = await fs.readFile(inputFile, 'utf-8') // 1. Read the input
let ast;
try {
ast = p.parse(input); // 2. Parse the input
} catch (e) {
let m = e.message
console.error(m);
return m;
}
ast = dependencies(ast); // 3. Find which support functions are being used
ast = scopeAnalysis(ast); // 4. Create the scope object. Attributes: initialized, used
let output = codeGen(ast); // 5. Generate JS
await writeCode(output, outputFile); // 6. Write the generated JS in the output file
return output;
}
For phases 3 and 4 you will need to traverse the AST but instead of estraverse
this time we will use the visit (opens in a new tab) method from the ast-types module.
To complete the code for the dependencies
and scopeAnalysis
functions use the recast module for writing codeGen
.
Testing, Covering and Continuous Integration
Write the tests, do a coverage study using c8 (opens in a new tab) and add continuous integration using GitHub Actions.
Read the sections Testing with Mocha and Jest.
Avoiding the preamble in the tests
In your template for the code generation, use a special string like \n/* End of support code */\n\n
to mark the end of the preamble and the beginning of the generated code.
const { {{ dependencies }} } = require("{{root}}/src/support-lib.js");
/* End of support code */
{{code}}
Then in your tests, use a funtion like the following to remove the preamble by using a regexp that removes from the beginning to the from the generated code:
function removeDependencies(s) {
const REGULAR_SEPARATION = /^(.|\n)*\n\/\* End of support code \*\/\n\n/
const pruned = s.replace(REGULAR_SEPARATION, '')
return removeSpaces(pruned);
}
You can use it in your tests removing the preamble from both the expected and the actual output:
for (let i = 0; i < Test.length; i++) {
it(`transpile(${Tst[i].input}, ${Tst[i].actualjs}) (No errors: ${Boolean(Tst[i].expectedout)})`, async () => {
let actualjs = await transpile(Test[i].input, Test[i].actualjs);
let expectedjs = fs.readFileSync(Test[i].expectedjs, 'utf-8')
let trimActualJS = removeDependencies(actualjs)
let trimExpectedJS = removeDependencies(expectedjs)
assert.equal(trimActualJS, trimExpectedJS);
});
}
Documentation
Document
the module incorporating a README.md
and the exported functions using JsDoc.
Read the section Documenting the JavaScript Sources of the Chapter Creating and Publishing a Node.js module
Related topics
Videos
Class record for 28/02/2024:
Class record for 27/02/2024:
Class record for 26/02/2024:
Vídeos del 01/03/2023, 06/03/2023 y 07/03/2023:
Rubric
scope-intro Repos
References
- ast-types
- recast
- More on JSCodeshift in the article Write Code to Rewrite Your Code: jscodeshift (opens in a new tab) by Jeremy Greer
- See Tree Transformations References