sans-bot/lisp.js

425 lines
11 KiB
JavaScript

// 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);