UNPKG

@thi.ng/lispy

Version:

Lightweight, extensible, interpreted Lisp-style DSL for embedding in other projects

293 lines (292 loc) 8.53 kB
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 };