@thi.ng/lispy
Version:
Lightweight, extensible, interpreted Lisp-style DSL for embedding in other projects
293 lines (292 loc) • 8.53 kB
JavaScript
import { always, identity, never } from "@thi.ng/api/fn";
import { isFunction } from "@thi.ng/checks/is-function";
import { OPERATORS } from "@thi.ng/compare/ops";
import { DEFAULT, defmulti } from "@thi.ng/defmulti";
import { assert } from "@thi.ng/errors/assert";
import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
import { illegalArity } from "@thi.ng/errors/illegal-arity";
import { deg, rad } from "@thi.ng/math/angle";
import { HALF_PI, TAU } from "@thi.ng/math/api";
import { fit } from "@thi.ng/math/fit";
import { clamp } from "@thi.ng/math/interval";
import { mix } from "@thi.ng/math/mix";
import { smoothStep, step } from "@thi.ng/math/step";
import { selectKeysObj } from "@thi.ng/object-utils/select-keys";
import { parse } from "@thi.ng/sexpr/parse";
import { runtime } from "@thi.ng/sexpr/runtime";
import { capitalize, lower, upper } from "@thi.ng/strings/case";
import { KERNEL } from "./kernel.js";
const __mathOp = (fn, fn1) => (first, ...args) => {
return args.length > 0 ? (
// use a reduction for 2+ args
args.reduce((acc, x) => fn(acc, x), first)
) : (
// apply special case unary function
fn1(first)
);
};
const __fnImpl = (args, body, env) => (...xs) => {
const $args = args.children;
if ($args.length !== xs.length) illegalArity($args.length);
const $env = $args.reduce(
(acc, a, i) => (acc[a.value] = xs[i], acc),
{ ...env }
);
return body.reduce((_, x) => interpret(x, $env), void 0);
};
const __threadOp = (fn) => ([_, ...body], env) => {
const n = body.length;
if (!n) return;
let res = body[0];
for (let i = 1; i < n; i++) {
const curr = { ...body[i] };
assert(
curr.type === "expr",
`expected expression, got ${curr.type}`
);
const $curr = curr;
$curr.children = fn(res, $curr.children);
res = $curr;
}
return interpret(res, env);
};
const __injectBindings = (args, env) => {
const pairs = args.children;
assert(
args.type === "expr" && !(pairs.length % 2),
`require pairs of key-val bindings`
);
for (let i = 0, n = pairs.length; i < n; i += 2) {
assert(
pairs[i].type === "sym",
`expected symbol, got: ${pairs[i].type}`
);
env[pairs[i].value] = interpret(pairs[i + 1], env);
}
};
const BUILTINS = defmulti(
(x) => x[0].value,
{},
{
// defines as new symbol with given value, stores it in the environment and
// then returns the value, e.g. `(def magic 42)`
def: ([_, name, value], env) => env[name.value] = interpret(value, env),
// defines a new function with given name, args and body, stores it in the
// environment and returns it, e.g. `(defn madd (a b c) (+ (* a b) c))`
defn: ([_, name, args, ...body], env) => {
return env[name.value] = __fnImpl(args, body, env);
},
// anonymous function definition
fn: ([_, args, ...body], env) => __fnImpl(args, body, env),
// if/then conditional with optional else
if: ([_, test, truthy, falsy], env) => interpret(test, env) ? interpret(truthy, env) : falsy ? interpret(falsy, env) : void 0,
// create local symbol/variable bindings, e.g.
// `(let (a 1 b 2 c (+ a b)) (print a b c))`
let: ([_, args, ...body], env) => {
const $env = { ...env };
__injectBindings(args, $env);
return body.reduce((_2, x) => interpret(x, $env), void 0);
},
while: ([, test, ...body], env) => {
const n = body.length;
while (interpret(test, env)) {
for (let i = 0; i < n; i++) interpret(body[i], env);
}
},
"env!": ([_, args], env) => __injectBindings(args, env),
list: ([_, ...body], env) => evalArgs(body, env),
obj: ([_, ...body], env) => {
assert(!(body.length % 2), `require pairs of key-val bindings`);
const obj = {};
for (let i = 0, n = body.length; i < n; i += 2) {
const key = body[i].type === "expr" ? interpret(body[i], env) : body[i].value;
obj[String(key)] = interpret(body[i + 1], env);
}
return obj;
},
keys: ([_, x], env) => Object.keys(interpret(x, env)),
vals: ([_, x], env) => Object.values(interpret(x, env)),
pairs: ([_, x], env) => Object.entries(interpret(x, env)),
// rewriting operators:
// iteratively threads first child as FIRST arg of next child
// (-> a (+ b) (* c)) = (* (+ a b) c)
"->": __threadOp((res, [f, ...children]) => [f, res, ...children]),
// iteratively threads first child as LAST arg of next child
// (->> a (+ b) (* c)) = (* c (+ b a))
"->>": __threadOp((res, children) => [...children, res]),
env: (_, env) => JSON.stringify(
env,
(_2, val) => isFunction(val) ? "<function>" : val,
2
),
syms: (_, env) => Object.keys(env),
// add default/fallback implementation to allow calling functions defined in
// the environment (either externally or via `defn`)
[DEFAULT]: ([x, ...args], env) => {
const name = x.value;
const f = env[name];
if (f == null) illegalArgs(`${name} is not defined`);
if (!isFunction(f)) illegalArgs(`${name} is not a function`);
return f.apply(null, evalArgs(args, env));
}
}
);
const ENV = {
// prettier-ignore
"+": __mathOp((acc, x) => acc + x, (x) => x),
// prettier-ignore
"*": __mathOp((acc, x) => acc * x, (x) => x),
// prettier-ignore
"-": __mathOp((acc, x) => acc - x, (x) => -x),
// prettier-ignore
"/": __mathOp((acc, x) => acc / x, (x) => 1 / x),
inc: (x) => x + 1,
dec: (x) => x - 1,
// predicates
"null?": (x) => x == null,
"zero?": (x) => x === 0,
"neg?": (x) => x < 0,
"pos?": (x) => x > 0,
"nan?": (x) => isNaN(x),
// comparisons
...OPERATORS,
// boolean logic
T: true,
F: false,
null: null,
INF: Infinity,
"-INF": -Infinity,
and: (...args) => {
let x;
for (x of args) {
if (!x) return false;
}
return x;
},
or: (...args) => {
for (let x of args) {
if (x) return x;
}
return false;
},
not: (x) => !x,
// binary
"<<": (x, y) => x << y,
">>": (x, y) => x >> y,
">>>": (x, y) => x >>> y,
"bit-and": (x, y) => x & y,
"bit-or": (x, y) => x | y,
"bit-xor": (x, y) => x ^ y,
"bit-not": (x) => ~x,
// JS-native Math
...selectKeysObj(Math, [
"E",
"LN10",
"LN2",
"LOG10E",
"LOG2E",
"PI",
"SQRT1_2",
"SQRT2",
"abs",
"acos",
"acosh",
"asin",
"asinh",
"atan",
"atan2",
"atanh",
"cbrt",
"ceil",
"clz32",
"cos",
"cosh",
"exp",
"expm1",
"floor",
"fround",
"hypot",
"imul",
"log",
"log10",
"log1p",
"log2",
"max",
"min",
"pow",
"random",
"round",
"sign",
"sin",
"sinh",
"sqrt",
"tan",
"tanh",
"trunc"
]),
HALF_PI,
TAU,
clamp,
deg,
fit,
mix,
rad,
step,
smoothstep: smoothStep,
get: (arr, i) => arr[i],
"set!": (arr, i, x) => (arr[i] = x, arr),
push: (list, ...x) => (list.push(...x), list),
concat: (list, ...x) => list.concat(...x),
// returns length of first argument (presumably a list or string)
// (e.g. `(count "abc")` => 3)
count: (arg) => arg.length,
// returns first item of list/string
first: (arg) => arg[0],
// returns remaining list/string (from 2nd item onward) or undefined if
// there're no further items
next: (arg) => arg.length > 1 ? arg.slice(1) : void 0,
// strings
str: (...args) => args.join(""),
join: (sep, args) => args.join(sep),
lower,
upper,
capitalize,
"pad-left": (x, width, fill) => x.padStart(width, fill),
"pad-right": (x, width, fill) => x.padEnd(width, fill),
substr: (x, from, to) => x.substring(from, to),
trim: (x) => x.trimEnd(),
regexp: (src, flags) => new RegExp(src, flags),
"re-test": (regexp, x) => regexp.test(x),
"re-match": (regexp, x) => regexp.exec(x),
replace: (x, regexp, replacement) => x.replace(regexp, replacement),
// functions
identity,
always,
never,
int: parseInt,
float: parseFloat,
//json
"json-parse": JSON.parse,
"json-str": JSON.stringify,
// misc
print: console.log
};
const evalSource = (src, env = ENV, opts) => parse(src, opts).children.reduce(
(_, x) => interpret(x, env),
void 0
);
const evalArgs = (nodes, env = ENV) => nodes.map((a) => interpret(a, env));
const interpret = runtime({
expr: (x, env) => BUILTINS(x.children, env),
sym: (x, env) => env[x.value],
str: (x) => x.value,
num: (x) => x.value
});
evalSource(KERNEL);
export {
BUILTINS,
ENV,
evalArgs,
evalSource,
interpret
};