Source: scope.js

/**
 * Universidad de La Laguna
 * Escuela Superior de Ingeniería y Tecnología
 * Grado en Ingeniería Informática
 * Procesadores de Lenguajes
 *
 * @author Juan Rodríguez Suárez
 * @since Mar 04 2024
 * @desc Contains functions to analyze the scope of the program and the dependencies of the functions.
 */

const astTypes = require("ast-types");

const support = require("./support-lib.js");
// Build regexp matching the names of the support functions /factorial|power|min|max|.../
const FUNCTION_NAMES = Object.keys(support).map(n => n.replace(/[$*.^]/, '[$&]')); // Escape regexp special characters
const patternIsSupport = new RegExp(FUNCTION_NAMES.join('|'));

/**
 * @brief Builds the set of function dependencies
 * @param {object} dAst - The decorated AST
 * @returns {object} The decorated AST
 */
function dependencies(dAst) {
  let usedFunctions = new Set();
  astTypes.visit(dAst.ast, {
    visitCallExpression(path) {
      const NODE = path.node;
      if (patternIsSupport.test(NODE.callee.name)) {
        usedFunctions.add(NODE.callee.name);
      }
      this.traverse(path);
    }
  });
  dAst.dependencies = usedFunctions;
  return dAst;
}

/**
 * @brief Analyzes the scope of the program
 * @param {object} dAst - The decorated AST
 * @returns {object} The decorated AST
 */
const scopeAnalysis = (dAst) => {
  const Scope = require("./scope-class.js");
  let scope = new Scope(null); // global scope
  let ast = dAst.ast;

  astTypes.visit(ast, {
    visitFunctionExpression(path) {
      let node = path.node;
      scope = new Scope(scope);

      // mark parameters as initialized
      const PARAMETERS = node.params;
      for (const PARAM of PARAMETERS) {
        scope.setAsInitialized(PARAM.name);
      }
      this.traverse(path);
      if (scope.length > 0) { // insert declarations at the beginning of the function
        node.body.body.unshift(scope.buildDeclaration());
      }

      node.scope = scope;

      const NOT_DECLARED_MESSAGE = scope.notDeclaredMessage();
      if (NOT_DECLARED_MESSAGE) {
        console.error('*** WARNING: ' + NOT_DECLARED_MESSAGE + ' in function scope ***\n');
      }
      scope = scope.parent;
    },
    visitAssignmentExpression(path) {
      const NODE = path.node;
      if (NODE.left.type === 'Identifier') {
        let name = NODE.left.name;
        if (name && !scope.has(name)) {
          if (!dAst.dependencies.has(name)) {
            scope.add(name);
          }
        }
      }
      this.traverse(path);
    },
    visitIdentifier(path) {
      const NAME = path.node.name;
      if (/^[$]/.test(NAME) && !dAst.dependencies.has(NAME)) {
        scope.setAsUsed(NAME);
      }
      this.traverse(path);
    }
  });

  if (scope.length > 0) { // insert declarations at the beginning of the program
    ast.body.unshift(scope.buildDeclaration());
  }

  ast.scope = scope;

  const NOT_DECLARED_MESSAGE = scope.notDeclaredMessage();
  if (NOT_DECLARED_MESSAGE) {
    console.error('*** WARNING: ' + NOT_DECLARED_MESSAGE + ' in global scope ***\n');
  }

  return dAst;
};

module.exports = {
  dependencies,
  scopeAnalysis
}