about summary refs log blame commit diff stats
path: root/lisp/js/lisp.js
blob: 5f7c550b4e7b91aeb7df6c57ce7bcdb059558a82 (plain) (tree)
div class='alt'>

                                                    
(function (exports) {
  const library = {
    print: (x) => {
      console.log(x);
      return x;
    },

    display: (x) => {
      console.log(x);
      return x;
    },

    concat: (...items) => {
      return items.reduce((acc, item) => {
        return `${acc}${item}`;
      }, "");
    },

    // math
    '+': (...args) => {
      return args.reduce((sum, val) => sum + val);
    },

    '-': (...args) => { // Subtracts values.
      return args.reduce((sum, val) => sum - val);
    },

    '*': (...args) => { // Multiplies values.
      return args.reduce((sum, val) => sum * val);
    },

    '/': (...args) => { // Divides values.
      return args.reduce((sum, val) => sum / val);
    },

    '%': (a, b) => { // Returns the modulo of a and b.
      return a % b;
    },

    clamp: (val, min, max) => { // Clamps a value between min and max.
      return Math.min(max, Math.max(min, val));
    },

    step: (val, step) => {
      return Math.round(val / step) * step;
    },

    min: Math.min,
    max: Math.max,
    ceil: Math.ceil,
    floor: Math.floor, // round down to the nearest integer.
    sin: Math.sin,
    cos: Math.cos,
    log: Math.log, // calculates on the base of e.

    pow: (a, b) => { // calculates a^b.
      return Math.pow(a, b);
    },

    sqrt: Math.sqrt, // calculate the square root.

    sq: (a) => { // calculate the square.
      return a * a;
    },

    PI: Math.PI,
    TAU: Math.PI * 2,

    random: (...args) => {
      if (args.length >= 2) {
        // (random start end)
        return args[0] + Math.random() * (args[1] - args[0]);
      } else if (args.length === 1) {
        // (random max)
        return Math.random() * args[0];
      }
      return Math.random();
    },

    // logic
    '>': (a, b) => { // Returns true if a is greater than b, else false.
      return a > b;
    },

    '<': (a, b) => { // Returns true if a is less than b, else false.
      return a < b;
    },

    '=': (a, b) => { // Returns true if a is equal to b, else false.
      return a === b;
    }, // consider eq? instead 
    // what about an not equal?

    and: (a, b, ...rest) => { // Returns true if all conditions are true.
      const args = [a, b].concat(rest);
      for (let i = 0; i < args.length; i++) {
        if (!args[i]) {
          return args[i];
        }
      }
      return args[args.length - 1];
    },

    or: (a, b, ...rest) => { // Returns true if at least one condition is true.
      const args = [a, b].concat(rest);
      for (let i = 0; i < args.length; i++) {
        if (args[i]) {
          return args[i];
        }
      }
      return args[args.length - 1];
    },

    // arrays
    map: async (fn, arr) => {
      let res = [];
      for (let i = 0; i < arr.length; i++) {
        const arg = arr[i];
        res.push(await fn(arr[i], i));
      }
      return res;
    },

    filter: (fn, arr) => {
      const list = Array.from(arr);
      return Promise.all(list.map((element, index) => fn(element, index, list)))
        .then((result) => {
          return list.filter((_, index) => {
            return result[index];
          });
        });
    },

    reduce: async (fn, arr, acc) => {
      const length = arr.length;
      let result = acc === undefined ? subject[0] : acc;
      for (let i = acc === undefined ? 1 : 0; i < length; i++) {
        result = await fn(result, arr[i], i, arr);
      }
      return result;
    },

    len: (item) => { // returns the length of a list.
      return item.length;
    },

    first: (arr) => { // returns the first item of a list.
      return arr[0];
    },

    car: (arr) => { // returns the first item of a list.
      return arr[0];
    },

    last: (arr) => { // returns the last
      return arr[arr.length - 1];
    },

    rest: ([_, ...arr]) => {
      return arr;
    },

    cdr: ([_, ...arr]) => {
      return arr;
    },

    range: (start, end, step = 1) => {
      const arr = [];
      if (step > 0) {
        for (let i = start; i <= end; i += step) {
          arr.push(i);
        }
      } else {
        for (let i = start; i >= end; i += step) {
          arr.push(i);
        }
      }
      return arr;
    },

    // objects
    get: (item, key) => { // gets an object's parameter with name.
      return item[key];
    },

    set: (item, ...args) => { // sets an object's parameter with name as value.
      for (let i = 0; i < args.length; i += 2) {
        const key = args[i];
        const val = args[i + 1];
        item[key] = val;
      }
      return item;
    },

    of: (h, ...keys) => { // gets object parameters with names.
      return keys.reduce((acc, key) => {
        return acc[key];
      }, h);
    },

    keys: (item) => { // returns a list of the object's keys
      return Object.keys(item);
    },

    values: (item) => { // returns a list of the object's values
      return Object.values(item);
    },

    time: (rate = 1) => { // returns timestamp in milliseconds.
      return (Date.now() * rate);
    },

    js: () => { // Javascript interop.
      return window; // note, this only works in the browser
    },

    test: (name, a, b) => {
      if (`${a}` !== `${b}`) {
        console.warn("failed " + name, a, b);
      } else {
        console.log("passed " + name, a);
      }
      return a === b;
    },

    assert: (condition, message) => {
      if (!condition) {
          throw new Error(message || 'Assertion failed');
      }
    },

    benchmark: async (fn) => { // logs time taken to execute a function.
      const start = Date.now();
      const result = await fn();
      console.log(`time taken: ${Date.now() - start}ms`);
      return result;
    },
  };

  const TYPES = {
    identifier: 0,
    number: 1,
    string: 2,
    bool: 3,
  };

  const Context = function (scope, parent) {
    this.scope = scope;
    this.parent = parent;
    this.get = function (identifier) {
      if (identifier in this.scope) {
        return this.scope[identifier];
      } else if (this.parent !== undefined) {
        return this.parent.get(identifier);
      }
    };
  };

  const special = {
    let: function (input, context) {
      const letContext = input[1].reduce(function (acc, x) {
        acc.scope[x[0].value] = interpret(x[1], context);
        return acc;
      }, new Context({}, context));
      return interpret(input[2], letContext);
    },
    def: function (input, context) {
      const identifier = input[1].value;
      const value = input[2].type === TYPES.string && input[3]
        ? input[3]
        : input[2];
      context.scope[identifier] = interpret(value, context);
      return value;
    },
    fn: function (input, context) {
      const fnName = input[1].value;
      const fnParams = input[2].type === TYPES.string && input[3]
        ? input[3]
        : input[2];
      const fnBody = input[2].type === TYPES.string && input[4]
        ? input[4]
        : input[3];
      context.scope[fnName] = async function () {
        const lambdaArguments = arguments;
        const lambdaScope = fnParams.reduce(function (acc, x, i) {
          acc[x.value] = lambdaArguments[i];
          return acc;
        }, {});
        return interpret(fnBody, new Context(lambdaScope, context));
      };
    },
    lambda: function (input, context) {
      return async function () {
        const lambdaArguments = arguments;
        const lambdaScope = input[1].reduce(function (acc, x, i) {
          acc[x.value] = lambdaArguments[i];
          return acc;
        }, {});
        return interpret(input[2], new Context(lambdaScope, context));
      };
    },
    if: async function (input, context) {
      if (await interpret(input[1], context)) {
        return interpret(input[2], context);
      }
      return input[3] ? interpret(input[3], context) : [];
    },
    __fn: function (input, context) {
      return async function () {
        const lambdaArguments = arguments;
        const keys = [
          ...new Set(
            input.slice(2).flat(100).filter((i) =>
              i.type === TYPES.identifier &&
              i.value[0] === "%"
            ).map((x) => x.value).sort(),
          ),
        ];
        const lambdaScope = keys.reduce(function (acc, x, i) {
          acc[x] = lambdaArguments[i];
          return acc;
        }, {});
        return interpret(input.slice(1), new Context(lambdaScope, context));
      };
    },
    __obj: async function (input, context) {
      const obj = {};
      for (let i = 1; i < input.length; i += 2) {
        obj[await interpret(input[i], context)] = await interpret(
          input[i + 1],
          context,
        );
      }
      return obj;
    },
  };

  const interpretList = function (input, context) {
    if (input.length > 0 && input[0].value in special) {
      return special[input[0].value](input, context);
    } else {
      var list = input.map(function (x) {
        return interpret(x, context);
      });
      if (list[0] instanceof Function) {
        return list[0].apply(undefined, list.slice(1));
      } else {
        return list;
      }
    }
  };

  const interpret = function (input, context) {
    if (context === undefined) {
      return interpret(input, new Context(library));
    } else if (input instanceof Array) {
      return interpretList(input, context);
    } else if (input.type === "identifier") {
      return context.get(input.value);
    } else if (input.type === "number" || input.type === "string") {
      return input.value;
    }
  };

  let categorize = function (input) {
    if (!isNaN(parseFloat(input))) {
      return {
        type: "number",
        value: parseFloat(input),
      };
    } else if (input[0] === '"' && input.slice(-1) === '"') {
      return {
        type: "string",
        value: input.slice(1, -1),
      };
    } else {
      return {
        type: "identifier",
        value: input,
      };
    }
  };

  let parenthesize = function (input, list) {
    if (list === undefined) {
      return parenthesize(input, []);
    } else {
      let token = input.shift();
      if (token === undefined) {
        return list.pop();
      } else if (token === "(") {
        list.push(parenthesize(input, []));
        return parenthesize(input, list);
      } else if (token === ")") {
        return list;
      } else {
        return parenthesize(input, list.concat(categorize(token)));
      }
    }
  };

  let tokenize = function (input) {
    return input.split('"')
      .map(function (x, i) {
        if (i % 2 === 0) { // not in string
          return x.replace(/\(/g, " ( ")
            .replace(/\)/g, " ) ");
        } else { // in string
          return x.replace(/ /g, "!whitespace!");
        }
      })
      .join('"')
      .trim()
      .split(/\s+/)
      .map(function (x) {
        return x.replace(/!whitespace!/g, " ");
      });
  };

  let parse = function (input) {
    return parenthesize(tokenize(input));
  };

  let run = function (input) { 
    return interpret(parse(input)) 
  };

  exports.lisp = {
    parse: parse,
    interpret: interpret,
    run: run,
  };
})(typeof exports === "undefined" ? this : exports);