Source: scope-class.js

const {
  buildIdentifier,
  buildVariableDeclaration,
  buildVariableDeclarator,
} = require('./ast-build');
const { difference } = require('./utils.js');

/**
 * Represents a scope in a program.
 */
class Scope {
  constructor(parent) {
    this.parent = parent;
    this.initialized = new Set();
    this.used = new Set();
    this.VarDeclarators = [];
  }

  /**
   * Adds a variable to the scope.
   * @param {string} name - The name of the variable.
   */
  add(name) {
    this.initialized.add(name);
    this.VarDeclarators.push(buildVariableDeclarator(buildIdentifier(name)));
  }

  /**
   * Sets a variable as initialized in the scope.
   * @param {string} name - The name of the variable.
   */
  setAsInitialized(name) {
    this.initialized.add(name);
  }

  /**
   * Sets a variable as used in the scope.
   * @param {string} name - The name of the variable.
   */
  setAsUsed(name) {
    this.used.add(name);
  }

  /**
   * Checks if a variable is declared in the scope.
   * @param {string} name - The name of the variable.
   * @returns {boolean} - True if the variable is declared, false otherwise.
   */
  has(name) {
    return this.initialized.has(name);
  }

  /**
   * Builds the variable declaration for the scope.
   * @returns {VariableDeclaration} - The built variable declaration.
   */
  buildDeclaration() {
    return buildVariableDeclaration(this.VarDeclarators);
  }

  /**
   * Looks up a variable in the scope and its parent scopes.
   * @param {string} name - The name of the variable.
   * @returns {Scope|null} - The scope where the variable is declared, or null if not found.
   */
  lookup(name) {
    if (this.has(name)) return this;
    if (this.parent) return this.parent.lookup(name);
    return null;
  }

  /**
   * Finds variables that are used but not declared in the scope.
   * @returns {Set<string>} - The set of not declared variables.
   */
  notDeclared() { 
    let notDeclared = difference(this.used, this.initialized);
    for(let v of this.used) {
      let s = this.lookup(v);
      if (s) notDeclared.delete(v);
    }
    return notDeclared;
  }

  /**
   * Generates a message for not declared variables in the scope.
   * @returns {string|null} - The message for not declared variables, or null if all variables are declared.
   */
  notDeclaredMessage() {
    let d = this.notDeclared();
    if (d.size > 0) { 
      return Array.from(d)
         .map(x => x.replace(/^[$]/, ''))
         .map(x => `Not declared variable '${x}'`).join(',');
    }
    return null;
  }

  /**
   * Gets the number of variable declarators in the scope.
   * @returns {number} - The number of variable declarators.
   */
  get length() {
    return this.VarDeclarators.length;
  }
}

module.exports = Scope;