Scope Intro

Introduction to Scoping

Task description

Using

  1. The ast-types module and namely the visit (opens in a new tab) methods of the module to traverse the AST and
  2. 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

  1. comma,
  2. identifier,
  3. assignment, and
  4. print

operations so that we can process input like this:

test/data/input/test-id.calc
➜  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