From 2dd6313dfc5ca90da8a235d6bc170f6a62d6ba3c Mon Sep 17 00:00:00 2001 From: elioat Date: Fri, 3 Mar 2023 10:21:04 -0500 Subject: very tiny lisp --- lisp/js/lisp.js | 424 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ lisp/js/repl.js | 11 ++ 2 files changed, 435 insertions(+) create mode 100644 lisp/js/lisp.js create mode 100644 lisp/js/repl.js (limited to 'lisp/js') diff --git a/lisp/js/lisp.js b/lisp/js/lisp.js new file mode 100644 index 0000000..8f7d3c1 --- /dev/null +++ b/lisp/js/lisp.js @@ -0,0 +1,424 @@ +// lifted from https://github.com/maryrosecook/littlelisp +// extended by eli + +(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 + add: (...args) => { + return args.reduce((sum, val) => sum + val); + }, + + sub: (...args) => { // Subtracts values. + return args.reduce((sum, val) => sum - val); + }, + + mul: (...args) => { // Multiplies values. + return args.reduce((sum, val) => sum * val); + }, + + div: (...args) => { // Divides values. + return args.reduce((sum, val) => sum / val); + }, + + mod: (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, + TWO_PI: 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 + gt: (a, b) => { // Returns true if a is greater than b, else false. + return a > b; + }, + + lt: (a, b) => { // Returns true if a is less than b, else false. + return a < b; + }, + + eq: (a, b) => { // Returns true if a is equal to b, else false. + return a === b; + }, + + 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; + }, + + 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; + }, + defn: 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)); + }; + + exports.lisp = { + parse: parse, + interpret: interpret, + }; +})(typeof exports === "undefined" ? this : exports); diff --git a/lisp/js/repl.js b/lisp/js/repl.js new file mode 100644 index 0000000..6ea6929 --- /dev/null +++ b/lisp/js/repl.js @@ -0,0 +1,11 @@ +// lifted from https://github.com/maryrosecook/littlelisp +var repl = require("repl"); +var lisp = require("./lisp").lisp; + +repl.start({ + prompt: "* ", + eval: function (cmd, context, filename, callback) { + var ret = lisp.interpret(lisp.parse(cmd)); + callback(null, ret); + }, +}); -- cgit 1.4.1-2-gfad0