Home Reference Source Test

lib/interp/registry.js

const xRegExp = require("xregexp");

const utils = require("../utils.js");

const { SymbolTable } = require("./symboltable.js");

// specialForms and topEnv maps
const SpecialForms = Object.create(null);
const TopEnv = Object.create(null);

SpecialForms["if"] = (args, env) => {
  if (args.length !== 3) {
    throw new SyntaxError("Bad number of args passed to if");
  }

  if (args[0].evaluate(env) === true) {
    return args[1].evaluate(env);
  } else {
    return args[2].evaluate(env);
  }
};

SpecialForms["while"] = (args, env) => {
  if (args.length !== 2) {
    throw new SyntaxError("Bad number of args passed to while");
  }

  while (args[0].evaluate(env) === true) {
    args[1].evaluate(env);
  }

  // Egg has no undefined so we return false when there's no meaningful result
  return false;
};

SpecialForms["for"] = (args, env) => {
  if (args.length !== 4) {
    throw new SyntaxError("Bad number of args passed to for");
  }

  const forEnv = Object.create(env);
  forEnv["__symbol__"] = new SymbolTable();

  // Variable
  args[0].evaluate(forEnv);

  // Condition
  while (args[1].evaluate(forEnv) === true) {
    // Body
    args[3].evaluate(forEnv);

    // Increment
    args[2].evaluate(forEnv);
  }

  return false;
};

SpecialForms["foreach"] = (args, env) => {
  if (args.length !== 3) {
    throw new SyntaxError("Bad number of args passed to foreach");
  }

  if (args[0].type !== "word") {
    throw new SyntaxError("The first argument to foreach must be a valid word");
  }

  const localEnv = Object.create(env);
  localEnv["__symbol__"] = new SymbolTable();

  const iterable = args[1].evaluate(localEnv);
  for (const val of iterable) {
    localEnv[args[0].name] = val;
    args[2].evaluate(localEnv);
  }

  return false;
};

SpecialForms["do"] = (args, env) => {
  let value = false;

  args.forEach(arg => {
    value = arg.evaluate(env);
  });

  return value;
};

SpecialForms["def"] = SpecialForms["define"] = SpecialForms[":="] = (args, env) => {
  if (args.length !== 2) {
    throw new SyntaxError("Bad use of define");
  }

  // Value to assign to the variable
  let value = args[1].evaluate(env);

  // Variable name
  let valName = args[0].name;

  env[valName] = value;
  return value;
};

SpecialForms["fun"] = SpecialForms["->"] = (args, env) => {
  if (!args.length) {
    throw new SyntaxError("Functions need a body.");
  }

  function name(expr) {
    if (expr.type !== "word") {
      throw new SyntaxError("Arg names must be words");
    }

    return expr.name;
  }

  let argNames = args.slice(0, args.length - 1).map(name);
  let body = args[args.length - 1];

  return function() {
    if (arguments.length !== argNames.length) {
      throw new TypeError("Wrong number of arguments");
    }

    const localEnv = Object.create(env);
    localEnv["__symbol__"] = new SymbolTable();

    for (let i = 0; i < arguments.length; i++) {
      localEnv[argNames[i]] = arguments[i];
    }

    return body.evaluate(localEnv);
  };
};

SpecialForms["set"] = SpecialForms["="] = (args, env) => {
  if (args[0].type !== "word") {
    throw new SyntaxError("Bad use of set");
  }

  let valName = args[0].name;

  let indices = args.slice(1, -1).map(arg => arg.evaluate(env));

  let value = args[args.length - 1].evaluate(env);

  for (let scope = env; scope; scope = Object.getPrototypeOf(scope)) {
    if (scope["__symbol__"].checkAttribute("const", valName)) {
      throw new ReferenceError(`Trying to change 'const' variable ${args[0].name}`);
    }

    // TODO: Reduce code duplication
    if ("this" in scope) {
      // TODO: use hasOwnProperty ?

      if (Object.prototype.hasOwnProperty.call(scope["this"], valName)) {
        if (indices.length === 0) {
          scope["this"][valName] = value;
        } else {
          scope["this"][valName].setelem(value, ...indices);
        }

        return value;
      }
    }

    if (Object.prototype.hasOwnProperty.call(scope, valName)) {
      if (indices.length === 0) {
        scope[valName] = value;
      } else {
        scope[valName].setelem(value, ...indices);
      }

      return value;
    }
  }

  throw new ReferenceError(`Tried setting an undefined variable: ${valName}`);
};

SpecialForms["object"] = (args, env) => {
  // Create a new object and a new scope
  const object = {};
  const objectEnv = Object.create(env);
  objectEnv["__symbol__"] = new SymbolTable();

  // Add the variable 'this' as a reference to the current object
  objectEnv["this"] = object;

  // Evaluate the arguments and add the methods/properties to the object
  const evArgs = args.map(arg => arg.evaluate(objectEnv));

  for (const pair of utils.chunk(evArgs, 2)) {
    const name = pair[0];
    const value = pair[1];

    object[name] = value;
  }

  return object;
};

// TODO: Continue implementing class?
//
// SpecialForms['class'] = (args, env) => {
//   // const evArgs = args.map((arg) => evaluate(arg, env));
//
//   const newClass = function(x, y) {
//     this.x = x;
//     this.y = y;
//   }
//
//   return newClass;
// }

["+", "-", "*", "/", "==", "!=", "<", ">", ">=", "<=", "&&", "||"].forEach(op => {
  TopEnv[op] = new Function("a, b", `return a ${op} b;`);
});

TopEnv["true"] = true;
TopEnv["false"] = false;
TopEnv["null"] = null;

TopEnv["print"] = value => {
  console.log(value);
  return value;
};

TopEnv["arr"] = TopEnv["array"] = (...args) => {
  return args;
};

TopEnv["map"] = TopEnv["dict"] = (...args) => {
  return new Map(utils.chunk(args, 2));
};

TopEnv["<-"] = TopEnv["[]"] = TopEnv["element"] = (object, ...indices) => {
  return object.sub(...indices);
};

TopEnv["length"] = array => {
  return array.length;
};

TopEnv["RegExp"] = (method, ...args) => {
  return xRegExp[method](...args);
};

TopEnv["child"] = parent => {
  return Object.create(parent);
};

TopEnv["__symbol__"] = new SymbolTable();

// WIP: Continue implementing class?
// TopEnv['new'] = (...args) => {
//   const className = args[0];
//
//   // TODO: Check for more exceptions
//   if(typeof className !== "function") {
//     throw new SyntaxError(`${className} must be a class with a constructor.`)
//   }
//
//   return new className(...args.slice(1));
//

module.exports = {
  SpecialForms,
  TopEnv
};