425 lines
11 KiB
JavaScript
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);
|