3223 lines
90 KiB
JavaScript
3223 lines
90 KiB
JavaScript
/*:
|
|
* @target MZ
|
|
* @author F_
|
|
* @url https://github.com/f-space/rmmz-plugins
|
|
*
|
|
* @plugindesc
|
|
*
|
|
* 🌟 Core Function Library "Fs" (v0.4.0)
|
|
*
|
|
* @help
|
|
*
|
|
* This plugin only provides functions required by other plugins.
|
|
*
|
|
* This plugin itself does not change any behavior of your game.
|
|
*
|
|
*
|
|
* ---
|
|
* Copyright (c) 2020 F_
|
|
* Released under the MIT license
|
|
* https://github.com/f-space/rmmz-plugins/blob/master/LICENSE
|
|
*/
|
|
|
|
/*:ja
|
|
* @target MZ
|
|
* @author F_
|
|
* @url https://github.com/f-space/rmmz-plugins
|
|
*
|
|
* @plugindesc
|
|
*
|
|
* 🌟 基本関数ライブラリ Fs (v0.4.0)
|
|
*
|
|
* @help
|
|
*
|
|
* このプラグインは他のプラグインの動作に必要な機能を提供します。
|
|
*
|
|
* このプラグイン自体がゲームの挙動を変更することはありません。
|
|
*
|
|
*
|
|
* ---
|
|
* Copyright (c) 2020 F_
|
|
* Released under the MIT license
|
|
* https://github.com/f-space/rmmz-plugins/blob/master/LICENSE
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
{
|
|
if (Object.hasOwn === undefined) {
|
|
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
const hasOwn = (obj, key) => hasOwnProperty.call(obj, key);
|
|
Object.defineProperty(Object, "hasOwn", {
|
|
value: hasOwn,
|
|
writable: true,
|
|
enumerable: false,
|
|
configurable: true,
|
|
});
|
|
}
|
|
|
|
const LinkedList = (() => {
|
|
const make = () => node(null);
|
|
|
|
const init = (node, value) =>
|
|
Object.assign(node, { value, prev: node, next: node });
|
|
const node = (value) => init({}, value);
|
|
const value = (node) => node.value;
|
|
const first = (list) => list.next;
|
|
const last = (list) => list.prev;
|
|
|
|
const $set = (node, value) => {
|
|
node.value = value;
|
|
};
|
|
|
|
const $insertBefore = (node, ref) => {
|
|
const prev = ref.prev;
|
|
node.prev = prev;
|
|
node.next = ref;
|
|
ref.prev = prev.next = node;
|
|
};
|
|
|
|
const $remove = (node) => {
|
|
const { prev, next } = node;
|
|
prev.next = next;
|
|
next.prev = prev;
|
|
node.prev = node.next = node;
|
|
};
|
|
|
|
return { make, node, value, first, last, $set, $insertBefore, $remove };
|
|
})();
|
|
|
|
const LruCache = (() => {
|
|
const make = (capacity) => ({
|
|
capacity: Math.max(capacity, 1),
|
|
map: new Map(),
|
|
list: LinkedList.make(),
|
|
});
|
|
|
|
const $get = (cache, key) => {
|
|
const { map, list } = cache;
|
|
const node = map.get(key);
|
|
if (node !== undefined) {
|
|
$touch(list, node);
|
|
return LinkedList.value(node).value;
|
|
} else {
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
const $set = (cache, key, value) => {
|
|
const { capacity, map, list } = cache;
|
|
const node = map.get(key);
|
|
if (node !== undefined) {
|
|
LinkedList.$set(node, value);
|
|
$touch(cache, node);
|
|
} else {
|
|
while (map.size >= capacity) {
|
|
$remove(map, LinkedList.last(list));
|
|
}
|
|
$add(map, list, LinkedList.node({ key, value }));
|
|
}
|
|
};
|
|
|
|
const $touch = (list, node) => {
|
|
LinkedList.$remove(node);
|
|
LinkedList.$insertBefore(node, LinkedList.first(list));
|
|
};
|
|
|
|
const $add = (map, list, node) => {
|
|
const { key } = LinkedList.value(node);
|
|
map.set(key, node);
|
|
LinkedList.$insertBefore(node, LinkedList.first(list));
|
|
};
|
|
|
|
const $remove = (map, node) => {
|
|
const { key } = LinkedList.value(node);
|
|
map.delete(key);
|
|
LinkedList.$remove(node);
|
|
};
|
|
|
|
return { make, $get, $set };
|
|
})();
|
|
|
|
const linearPush = (array, item) => (array.push(item), array);
|
|
|
|
const fastZip = (accept, wrap, unwrap) => (array) => {
|
|
const result = [];
|
|
for (const item of array) {
|
|
if (accept(item)) {
|
|
result.push(unwrap(item));
|
|
} else {
|
|
return item;
|
|
}
|
|
}
|
|
return wrap(result);
|
|
};
|
|
|
|
const fastLazyZip = (accept, wrap, unwrap) => (array) => {
|
|
const result = [];
|
|
for (const lazy of array) {
|
|
const item = lazy();
|
|
if (accept(item)) {
|
|
result.push(unwrap(item));
|
|
} else {
|
|
return item;
|
|
}
|
|
}
|
|
return wrap(result);
|
|
};
|
|
|
|
const F = (() => {
|
|
const identity = (x) => x;
|
|
|
|
const always = (x) => () => x;
|
|
|
|
const pipe = (...args) => args.reduce((acc, fn) => fn(acc));
|
|
|
|
const flow = (...args) => args.reduce((acc, fn) => (x) => fn(acc(x)));
|
|
|
|
const try_ = (fn, handler) => {
|
|
try {
|
|
return fn();
|
|
} catch (e) {
|
|
return handler(e);
|
|
}
|
|
};
|
|
|
|
const throw_ = (error) => {
|
|
throw error;
|
|
};
|
|
|
|
const defer = (dispose, block) => {
|
|
try {
|
|
return block();
|
|
} finally {
|
|
dispose();
|
|
}
|
|
};
|
|
|
|
const with_ = (begin, end, block) => (begin(), defer(end, block));
|
|
|
|
const panic = (message) => throw_(new Error(message));
|
|
|
|
const unreachable = () => panic("unreachable code invoked");
|
|
|
|
const unimplemented = () => panic("unimplemented code invoked");
|
|
|
|
return {
|
|
identity,
|
|
always,
|
|
pipe,
|
|
flow,
|
|
try: try_,
|
|
throw: throw_,
|
|
defer,
|
|
with: with_,
|
|
panic,
|
|
unreachable,
|
|
unimplemented,
|
|
};
|
|
})();
|
|
|
|
const O = (() => {
|
|
const some = (value) => ({ value });
|
|
|
|
const none = undefined;
|
|
|
|
const isSome = (option) => option !== undefined;
|
|
|
|
const isNone = (option) => option === undefined;
|
|
|
|
const unwrap = (option) => option.value;
|
|
|
|
const expect = (option, formatter) =>
|
|
isSome(option) ? unwrap(option) : F.panic(formatter());
|
|
|
|
const withDefault = (option, value) =>
|
|
isSome(option) ? unwrap(option) : value;
|
|
|
|
const match = (option, onSome, onNone) =>
|
|
isSome(option) ? onSome(unwrap(option)) : onNone();
|
|
|
|
const andThen = (option, fn) =>
|
|
isSome(option) ? fn(unwrap(option)) : option;
|
|
|
|
const orElse = (option, fn) => (isSome(option) ? option : fn());
|
|
|
|
const map = (option, fn) =>
|
|
isSome(option) ? some(fn(unwrap(option))) : option;
|
|
|
|
const zip = fastZip(isSome, some, unwrap);
|
|
|
|
const lazyZip = fastLazyZip(isSome, some, unwrap);
|
|
|
|
return {
|
|
some,
|
|
none,
|
|
isSome,
|
|
isNone,
|
|
unwrap,
|
|
expect,
|
|
withDefault,
|
|
match,
|
|
andThen,
|
|
orElse,
|
|
map,
|
|
zip,
|
|
lazyZip,
|
|
};
|
|
})();
|
|
|
|
const R = (() => {
|
|
const ok = (value) => ({ success: true, value });
|
|
|
|
const err = (error) => ({ success: false, error });
|
|
|
|
const isOk = (result) => result.success;
|
|
|
|
const isErr = (result) => !isOk(result);
|
|
|
|
const unwrap = (result) => result.value;
|
|
|
|
const unwrapErr = (result) => result.error;
|
|
|
|
const expect = (result, formatter) =>
|
|
isOk(result) ? unwrap(result) : F.panic(formatter(unwrapErr(result)));
|
|
|
|
const match = (result, onOk, onErr) =>
|
|
isOk(result) ? onOk(unwrap(result)) : onErr(unwrapErr(result));
|
|
|
|
const andThen = (result, fn) =>
|
|
isOk(result) ? fn(unwrap(result)) : result;
|
|
|
|
const orElse = (result, fn) =>
|
|
isOk(result) ? result : fn(unwrapErr(result));
|
|
|
|
const map = (result, fn) =>
|
|
isOk(result) ? ok(fn(unwrap(result))) : result;
|
|
|
|
const mapErr = (result, fn) =>
|
|
isOk(result) ? result : err(fn(unwrapErr(result)));
|
|
|
|
const mapBoth = (result, mapOk, mapErr) =>
|
|
match(
|
|
result,
|
|
(value) => ok(mapOk(value)),
|
|
(error) => err(mapErr(error))
|
|
);
|
|
|
|
const all = fastZip(isOk, ok, unwrap);
|
|
|
|
const any = fastZip(isErr, err, unwrapErr);
|
|
|
|
const lazyAll = fastLazyZip(isOk, ok, unwrap);
|
|
|
|
const lazyAny = fastLazyZip(isErr, err, unwrapErr);
|
|
|
|
const try_ = (fn) => F.try(() => ok(fn()), err);
|
|
|
|
return {
|
|
ok,
|
|
err,
|
|
isOk,
|
|
isErr,
|
|
unwrap,
|
|
unwrapErr,
|
|
expect,
|
|
match,
|
|
andThen,
|
|
orElse,
|
|
map,
|
|
mapErr,
|
|
mapBoth,
|
|
all,
|
|
any,
|
|
lazyAll,
|
|
lazyAny,
|
|
try: try_,
|
|
};
|
|
})();
|
|
|
|
const L = (() => {
|
|
const nil = null;
|
|
|
|
const cons = (x, xs) => ({ head: x, tail: xs });
|
|
|
|
const singleton = (x) => cons(x, nil);
|
|
|
|
const empty = (list) => list === null;
|
|
|
|
const head = (list) => list.head;
|
|
|
|
const tail = (list) => list.tail;
|
|
|
|
const match = (list, onNil, onCons) =>
|
|
empty(list) ? onNil() : onCons(head(list), tail(list));
|
|
|
|
const find = (list, fn) =>
|
|
empty(list)
|
|
? undefined
|
|
: fn(head(list))
|
|
? head(list)
|
|
: find(tail(list), fn);
|
|
|
|
const some = (list, fn) =>
|
|
empty(list) ? false : fn(head(list)) ? true : some(tail(list), fn);
|
|
|
|
const every = (list, fn) =>
|
|
empty(list) ? true : fn(head(list)) ? every(tail(list), fn) : false;
|
|
|
|
const reverse = (list) => reverseRec(list, nil);
|
|
|
|
const reverseRec = (list, acc) =>
|
|
empty(list) ? acc : reverseRec(tail(list), cons(head(list), acc));
|
|
|
|
const reduce = (list, fn, value) =>
|
|
empty(list) ? value : reduce(tail(list), fn, fn(value, head(list)));
|
|
|
|
const reduceRight = (list, fn, value) => reduce(reverse(list), fn, value);
|
|
|
|
const map = (list, fn) =>
|
|
reduceRight(list, (xs, x) => cons(fn(x), xs), nil);
|
|
|
|
const append = (as, bs) => reduceRight(as, (xs, x) => cons(x, xs), bs);
|
|
|
|
const of = (...xs) => fromArray(xs);
|
|
|
|
const fromArray = (array) =>
|
|
array.reduceRight((list, x) => cons(x, list), nil);
|
|
|
|
const toArray = (list) => reduce(list, (xs, x) => linearPush(xs, x), []);
|
|
|
|
return {
|
|
nil,
|
|
cons,
|
|
singleton,
|
|
empty,
|
|
head,
|
|
tail,
|
|
match,
|
|
find,
|
|
some,
|
|
every,
|
|
reverse,
|
|
reduce,
|
|
reduceRight,
|
|
map,
|
|
append,
|
|
of,
|
|
fromArray,
|
|
toArray,
|
|
};
|
|
})();
|
|
|
|
const D = (() => {
|
|
const ellipsis = (source, length) => {
|
|
const ELLIPSIS = "...";
|
|
if (source.length <= length) {
|
|
return source;
|
|
} else {
|
|
const cps = [...source.slice(0, length * 2 + 1)];
|
|
if (cps.length <= length) {
|
|
return cps.join("");
|
|
} else {
|
|
return cps.slice(0, length - ELLIPSIS.length).join("") + ELLIPSIS;
|
|
}
|
|
}
|
|
};
|
|
|
|
const fmt = (() => {
|
|
const fmtValue = (value, context) => {
|
|
const { replacer } = context;
|
|
const v = replacer !== undefined ? replacer(value) : value;
|
|
switch (typeof v) {
|
|
case "undefined":
|
|
return String(v);
|
|
case "number":
|
|
return String(v);
|
|
case "string":
|
|
return JSON.stringify(v);
|
|
case "boolean":
|
|
return String(v);
|
|
case "symbol":
|
|
return String(v);
|
|
case "bigint":
|
|
return `${v}n`;
|
|
case "object":
|
|
return fmtObject(v, context);
|
|
case "function":
|
|
return fmtFunction(v);
|
|
default:
|
|
return "<unknown>";
|
|
}
|
|
};
|
|
|
|
const fmtObject = (value, context) => {
|
|
const { stack } = context;
|
|
if (value === null) return "null";
|
|
if (stack.includes(value)) return "...";
|
|
return fmtObjectCore(value, { ...context, stack: [...stack, value] });
|
|
};
|
|
|
|
const fmtObjectCore = (value, context) => {
|
|
if (Array.isArray(value)) return fmtArray(value, context);
|
|
if (value instanceof RegExp) return fmtRegExp(value);
|
|
if (value instanceof Date) return fmtDate(value);
|
|
return fmtOtherObject(value, context);
|
|
};
|
|
|
|
const fmtArray = (value, context) =>
|
|
value.length !== 0
|
|
? `[ ${value.map((item) => fmtValue(item, context)).join(", ")} ]`
|
|
: "[]";
|
|
|
|
const fmtRegExp = (value) => String(value);
|
|
|
|
const fmtDate = (value) => value.toISOString();
|
|
|
|
const fmtOtherObject = (value, context) => {
|
|
const type = fmtObjectType(value);
|
|
const entries = fmtObjectEntries(value, context);
|
|
const label = type !== undefined ? `${type} ` : "";
|
|
const contents = entries !== "" ? `{ ${entries} }` : "{}";
|
|
return label + contents;
|
|
};
|
|
|
|
const fmtObjectType = (value) => {
|
|
const prototype = Object.getPrototypeOf(value);
|
|
if (prototype === null) return "(null)";
|
|
if (prototype === Object.prototype) return undefined;
|
|
return fmtFunctionName(prototype.constructor);
|
|
};
|
|
|
|
const fmtObjectEntries = (value, context) => {
|
|
if (value instanceof Map) return fmtMapEntries(value, context);
|
|
if (value instanceof Set) return fmtSetEntries(value, context);
|
|
if (value instanceof WeakMap) return "<***>";
|
|
if (value instanceof WeakSet) return "<***>";
|
|
return fmtNormalEntries(value, context);
|
|
};
|
|
|
|
const fmtMapEntries = (value, context) =>
|
|
Array.from(value)
|
|
.map(([key, value]) => fmtMapKeyValuePair(key, value, context))
|
|
.join(", ");
|
|
|
|
const fmtMapKeyValuePair = (key, value, context) =>
|
|
`${fmtValue(key, context)} => ${fmtValue(value, context)}`;
|
|
|
|
const fmtSetEntries = (value, context) =>
|
|
Array.from(value)
|
|
.map((value) => fmtValue(value, context))
|
|
.join(", ");
|
|
|
|
const fmtNormalEntries = (value, context) =>
|
|
Object.entries(value)
|
|
.map(([key, value]) => fmtNormalEntry(key, value, context))
|
|
.join(", ");
|
|
|
|
const fmtNormalEntry = (key, value, context) =>
|
|
`${fmtNormalEntryKey(key)}: ${fmtValue(value, context)}`;
|
|
|
|
const fmtNormalEntryKey = (key) =>
|
|
/^[a-z_$][a-z0-9_$]*$/i.test(key) ? key : JSON.stringify(key);
|
|
|
|
const fmtFunction = (fn) => `[Function: ${fmtFunctionName(fn)}]`;
|
|
|
|
const fmtFunctionName = ({ name }) =>
|
|
typeof name === "string" && name !== "" ? name : "(anonymous)";
|
|
|
|
return (value, replacer) => fmtValue(value, { replacer, stack: [] });
|
|
})();
|
|
|
|
return { ellipsis, fmt };
|
|
})();
|
|
|
|
const U = (() => {
|
|
const mod = (x, y) => {
|
|
const r = x % y;
|
|
return r === 0 ? 0 : r < 0 ? r + Math.abs(y) : r;
|
|
};
|
|
|
|
const clamp = (x, min, max) =>
|
|
min <= max
|
|
? Math.min(Math.max(x, min), max)
|
|
: F.panic(`assertion failed: min(${D.fmt(min)}) <= max(${D.fmt(max)})`);
|
|
|
|
const equal = (() => {
|
|
const arrayEqual = (a, b, eq) =>
|
|
a.length === b.length && a.every((v, i) => eq(v, b[i]));
|
|
|
|
const isPojo = (x) => Object.getPrototypeOf(x) === Object.prototype;
|
|
|
|
const pojoEqual = (a, b, eq) => {
|
|
const akeys = Object.getOwnPropertyNames(a);
|
|
const bkeys = Object.getOwnPropertyNames(b);
|
|
return (
|
|
akeys.length === bkeys.length &&
|
|
akeys.every((k) => Object.hasOwn(b, k) && eq(a[k], b[k]))
|
|
);
|
|
};
|
|
|
|
const equal = (a, b) => {
|
|
if (Object.is(a, b)) return true;
|
|
if (typeof a !== typeof b) return false;
|
|
if (typeof a !== "object") return false;
|
|
if (a === null || b === null) return false;
|
|
if (Array.isArray(a))
|
|
return Array.isArray(b) && arrayEqual(a, b, equal);
|
|
if (isPojo(a)) return isPojo(b) && pojoEqual(a, b, equal);
|
|
return false;
|
|
};
|
|
|
|
return equal;
|
|
})();
|
|
|
|
const memo = (() => {
|
|
const memo = (config, fn) => {
|
|
const { storage } = config;
|
|
switch (storage) {
|
|
case "map":
|
|
return memoWithMap(config, fn);
|
|
case "single":
|
|
return memoWithSingleVariable(config, fn);
|
|
case "weakmap":
|
|
return memoWithWeakMap(fn);
|
|
default:
|
|
return F.panic(`unsupported memoization storage: ${storage}`);
|
|
}
|
|
};
|
|
|
|
const memoWithMap = (config, fn) => {
|
|
const { size, serializer = defaultSerializer } = config;
|
|
|
|
const cache = LruCache.make(size);
|
|
return (...args) => {
|
|
const key = serializer(...args);
|
|
const result = LruCache.$get(cache, key);
|
|
if (result !== undefined) {
|
|
return result.value;
|
|
} else {
|
|
const value = fn(...args);
|
|
LruCache.$set(cache, key, { value });
|
|
return value;
|
|
}
|
|
};
|
|
};
|
|
|
|
const memoWithSingleVariable = (config, fn) => {
|
|
const { comparer = defaultEqualityComparer } = config;
|
|
|
|
let cache = null;
|
|
return (...args) => {
|
|
if (cache !== null && comparer(cache.args, args)) {
|
|
return cache.value;
|
|
} else {
|
|
const value = fn(...args);
|
|
cache = { args, value };
|
|
return value;
|
|
}
|
|
};
|
|
};
|
|
|
|
const memoWithWeakMap = (fn) => {
|
|
const cache = new WeakMap();
|
|
return (key) => {
|
|
if (cache.has(key)) {
|
|
return cache.get(key);
|
|
} else {
|
|
const value = fn(key);
|
|
cache.set(key, value);
|
|
return value;
|
|
}
|
|
};
|
|
};
|
|
|
|
const defaultSerializer = (...args) => JSON.stringify(args);
|
|
|
|
const defaultEqualityComparer = (a, b) => {
|
|
const maxLength = Math.max(a.length, b.length);
|
|
for (let i = 0; i < maxLength; i++) {
|
|
if (!equal(a[i], b[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
return memo;
|
|
})();
|
|
|
|
const lazy = (() => {
|
|
const UNINITIALIZED = Symbol("uninitialized");
|
|
return (fn) => {
|
|
let cache = UNINITIALIZED;
|
|
return () => (cache !== UNINITIALIZED ? cache : (cache = fn()));
|
|
};
|
|
})();
|
|
|
|
return { mod, clamp, equal, memo, lazy };
|
|
})();
|
|
|
|
const G = (() => {
|
|
const ok = (backtrackable, value, state) => ({
|
|
ok: true,
|
|
backtrackable,
|
|
value,
|
|
state,
|
|
});
|
|
const err = (backtrackable, deadEnd) => ({
|
|
ok: false,
|
|
backtrackable,
|
|
deadEnd,
|
|
});
|
|
const mkState = (source, position, stack) => ({ source, position, stack });
|
|
const mkContext = (position, context) => ({ position, context });
|
|
const mkDeadEnd = (position, cause, stack) => ({ position, cause, stack });
|
|
const match = (result, onOk, onErr) =>
|
|
result.ok ? onOk(result) : onErr(result);
|
|
|
|
const succeed = (value) => (state) => ok(true, value, state);
|
|
|
|
const fail =
|
|
(cause) =>
|
|
({ position, stack }) =>
|
|
err(false, mkDeadEnd(position, cause, stack));
|
|
|
|
const token =
|
|
(lexer) =>
|
|
({ source, position, stack }) =>
|
|
lexer(
|
|
source,
|
|
position,
|
|
(value, length) =>
|
|
ok(length === 0, value, mkState(source, position + length, stack)),
|
|
(cause) => err(true, mkDeadEnd(position, cause, stack))
|
|
);
|
|
|
|
const end = (cause) => (state) =>
|
|
state.source.length === state.position
|
|
? ok(true, undefined, state)
|
|
: err(true, mkDeadEnd(state.position, cause, state.stack));
|
|
|
|
const inContext = (context, parser) => (s0) =>
|
|
match(
|
|
parser(
|
|
mkState(
|
|
s0.source,
|
|
s0.position,
|
|
L.cons(mkContext(s0.position, context), s0.stack)
|
|
)
|
|
),
|
|
({ backtrackable, value, state: s1 }) =>
|
|
ok(backtrackable, value, mkState(s1.source, s1.position, s0.stack)),
|
|
F.identity
|
|
);
|
|
|
|
const backtrackable = (parser) => (s0) =>
|
|
match(
|
|
parser(s0),
|
|
({ value, state: s1 }) => ok(true, value, s1),
|
|
({ deadEnd }) => err(true, deadEnd)
|
|
);
|
|
|
|
const andThen = (parser, fn) => (s0) =>
|
|
match(
|
|
parser(s0),
|
|
({ backtrackable: b1, value: v1, state: s1 }) =>
|
|
match(
|
|
fn(v1)(s1),
|
|
({ backtrackable: b2, value: v2, state: s2 }) =>
|
|
ok(b1 && b2, v2, s2),
|
|
({ backtrackable: b2, deadEnd: d2 }) => err(b1 && b2, d2)
|
|
),
|
|
F.identity
|
|
);
|
|
|
|
const map = (parser, fn) => (s0) =>
|
|
match(
|
|
parser(s0),
|
|
({ backtrackable, value, state: s1 }) =>
|
|
ok(backtrackable, fn(value), s1),
|
|
F.identity
|
|
);
|
|
|
|
const map2 = (first, second, fn) => (s0) =>
|
|
match(
|
|
first(s0),
|
|
({ backtrackable: b1, value: v1, state: s1 }) =>
|
|
match(
|
|
second(s1),
|
|
({ backtrackable: b2, value: v2, state: s2 }) =>
|
|
ok(b1 && b2, fn(v1, v2), s2),
|
|
({ backtrackable: b2, deadEnd: d2 }) => err(b1 && b2, d2)
|
|
),
|
|
F.identity
|
|
);
|
|
|
|
const pipe = (...parsers) =>
|
|
parsers.reduce((acc, parser) => map2(acc, parser, (v1, v2) => v2(v1)));
|
|
|
|
const skip = (parser) => map(parser, () => F.identity);
|
|
|
|
const take = (parser) => map(parser, (value) => (fn) => fn(value));
|
|
|
|
const first = (first, second) => map2(first, second, (v1, _) => v1);
|
|
|
|
const second = (first, second) => map2(first, second, (_, v2) => v2);
|
|
|
|
const between = (parser, open, close) => first(second(open, parser), close);
|
|
|
|
const or = (first, second) => (state) =>
|
|
match(first(state), F.identity, ({ backtrackable, deadEnd }) =>
|
|
backtrackable ? second(state) : err(false, deadEnd)
|
|
);
|
|
|
|
const oneOf = (...parsers) =>
|
|
parsers.reduceRight((acc, parser) => or(parser, acc));
|
|
|
|
const optional = (parser, value) => or(parser, succeed(value));
|
|
|
|
const loop = (fn, loopState) => (state) =>
|
|
loopRec(fn, loopState, true, state);
|
|
|
|
const loopRec = (fn, loopState, b0, s0) =>
|
|
match(
|
|
fn(loopState, next, done)(s0),
|
|
({ backtrackable: b1, value: v1, state: s1 }) =>
|
|
v1.done
|
|
? ok(b0 && b1, v1.value, s1)
|
|
: loopRec(fn, v1.state, b0 && b1, s1),
|
|
({ backtrackable: b1, deadEnd: d1 }) => err(b0 && b1, d1)
|
|
);
|
|
|
|
const next = (state) => ({ done: false, state });
|
|
|
|
const done = (value) => ({ done: true, value });
|
|
|
|
const lazy = (thunk) => (state) => thunk()(state);
|
|
|
|
const buildArray = (fn) =>
|
|
map(
|
|
fn(
|
|
(acc, x) => (acc === undefined ? [x] : linearPush(acc, x)),
|
|
undefined
|
|
),
|
|
(value) => (value === undefined ? [] : value)
|
|
);
|
|
|
|
const many = (parser) =>
|
|
buildArray((reducer, init) => manyl(parser, reducer, init));
|
|
|
|
const many1 = (parser) =>
|
|
buildArray((reducer, init) => manyl1(parser, reducer, init));
|
|
|
|
const manyl = (parser, fn, value) => (state) =>
|
|
manylRec(parser, fn, true, value, state);
|
|
|
|
const manylRec = (parser, fn, b0, v0, s0) =>
|
|
match(
|
|
parser(s0),
|
|
({ backtrackable: b1, value: v1, state: s1 }) =>
|
|
manylRec(parser, fn, b0 && b1, fn(v0, v1), s1),
|
|
({ backtrackable: b1, deadEnd: d1 }) =>
|
|
b1 ? ok(b0, v0, s0) : err(b1, d1)
|
|
);
|
|
|
|
const manyl1 = (parser, fn, value) =>
|
|
andThen(parser, (first) => manyl(parser, fn, fn(value, first)));
|
|
|
|
const manyr = (parser, fn, value) =>
|
|
map(
|
|
manyl(parser, (xs, x) => L.cons(x, xs), L.nil),
|
|
(list) => L.reduce(list, fn, value)
|
|
);
|
|
|
|
const manyr1 = (parser, fn, value) =>
|
|
map2(
|
|
parser,
|
|
manyl(parser, (xs, x) => L.cons(x, xs), L.nil),
|
|
(first, rest) => fn(L.reduce(rest, fn, value), first)
|
|
);
|
|
|
|
const chain = (parser, delimiter) =>
|
|
buildArray((reducer, init) => chainl(parser, delimiter, reducer, init));
|
|
|
|
const chain1 = (parser, delimiter) =>
|
|
buildArray((reducer, init) => chainl1(parser, delimiter, reducer, init));
|
|
|
|
const chainl = (parser, delimiter, fn, value) =>
|
|
or(chainl1(parser, delimiter, fn, value), succeed(value));
|
|
|
|
const chainl1 = (parser, delimiter, fn, value) => {
|
|
const succ = second(delimiter, parser);
|
|
return andThen(parser, (first) => manyl(succ, fn, fn(value, first)));
|
|
};
|
|
|
|
const chainr = (parser, delimiter, fn, value) =>
|
|
or(chainr1(parser, delimiter, fn, value), succeed(value));
|
|
|
|
const chainr1 = (parser, delimiter, fn, value) => {
|
|
const succ = second(delimiter, parser);
|
|
return map2(parser, manyr(succ, fn, value), (first, acc) =>
|
|
fn(acc, first)
|
|
);
|
|
};
|
|
|
|
const infixl = (operand, operator) => {
|
|
const succ = map2(operator, operand, (op, rhs) => ({ op, rhs }));
|
|
return andThen(operand, (first) =>
|
|
manyl(succ, (lhs, { op, rhs }) => op(lhs, rhs), first)
|
|
);
|
|
};
|
|
|
|
const infixr = (operand, operator) => {
|
|
const succ = map2(operator, operand, (op, rhs) => ({ op, rhs }));
|
|
return map2(
|
|
operand,
|
|
manyr(
|
|
succ,
|
|
(fn, { op, rhs }) =>
|
|
(lhs) =>
|
|
op(lhs, fn(rhs)),
|
|
F.identity
|
|
),
|
|
(lhs, fn) => fn(lhs)
|
|
);
|
|
};
|
|
|
|
const proc = (generator) => (state) =>
|
|
procRec(generator(), true, undefined, state);
|
|
|
|
const procRec = (iterator, b0, v0, s0) => {
|
|
const { value, done } = iterator.next(v0);
|
|
return done
|
|
? ok(b0, value, s0)
|
|
: match(
|
|
value(s0),
|
|
({ backtrackable: b1, value: v1, state: s1 }) =>
|
|
procRec(iterator, b0 && b1, v1, s1),
|
|
({ backtrackable: b1, deadEnd: d1 }) => err(b0 && b1, d1)
|
|
);
|
|
};
|
|
|
|
const try_ = (parser) => (state) =>
|
|
match(parser(state), F.identity, () => ok(true, undefined, state));
|
|
|
|
const validate = (parser, validator) => (s0) =>
|
|
match(
|
|
parser(s0),
|
|
({ backtrackable: b1, value: v1, state: s1 }) =>
|
|
validator(
|
|
v1,
|
|
s1.position - s0.position,
|
|
(v2) => ok(b1, v2, s1),
|
|
(cause) => err(b1, mkDeadEnd(s0.position, cause, s0.stack))
|
|
),
|
|
F.identity
|
|
);
|
|
|
|
const make = (parser) => (source, position) =>
|
|
match(
|
|
parser(mkState(source, position, L.nil)),
|
|
({ value, state: { position } }) => R.ok({ value, position }),
|
|
({ deadEnd }) => R.err(processDeadEnd(deadEnd))
|
|
);
|
|
|
|
const processDeadEnd = (deadEnd) => {
|
|
const { position, cause, stack: list } = deadEnd;
|
|
const stack = L.toArray(list).reverse();
|
|
return { position, cause, stack };
|
|
};
|
|
|
|
return {
|
|
succeed,
|
|
fail,
|
|
token,
|
|
end,
|
|
inContext,
|
|
backtrackable,
|
|
andThen,
|
|
map,
|
|
map2,
|
|
pipe,
|
|
skip,
|
|
take,
|
|
first,
|
|
second,
|
|
between,
|
|
or,
|
|
oneOf,
|
|
optional,
|
|
loop,
|
|
lazy,
|
|
many,
|
|
many1,
|
|
manyl,
|
|
manyl1,
|
|
manyr,
|
|
manyr1,
|
|
chain,
|
|
chain1,
|
|
chainl,
|
|
chainl1,
|
|
chainr,
|
|
chainr1,
|
|
infixl,
|
|
infixr,
|
|
proc,
|
|
try: try_,
|
|
validate,
|
|
make,
|
|
};
|
|
})();
|
|
|
|
const J = (() => {
|
|
const RE_WHITESPACE = /^[ \t\r\n]*/;
|
|
const RE_NUMBER =
|
|
/^-?(?:0(?![0-9])|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/;
|
|
const RE_CHARS =
|
|
/(?:[^"\\\u0000-\u001f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*/;
|
|
const RE_STRING = new RegExp(`^"(?:${RE_CHARS.source})"`);
|
|
const RE_TRUE = /^true\b/;
|
|
const RE_FALSE = /^false\b/;
|
|
const RE_NULL = /^null\b/;
|
|
const RE_INVALID_CHAR = /[\u0000-\u001f]|\\(?:[^u]|u[0-9a-fA-F]{0,4})/;
|
|
const RE_INVALID_STRING = new RegExp(
|
|
`^"(?:${RE_CHARS.source})(${RE_INVALID_CHAR.source})`
|
|
);
|
|
const RE_STRUCTURAL_TOKEN = /^[[{\]}:,]$/;
|
|
const RE_NUMBER_LIKE = /(?:[+-]?[0-9]+(?:\.[0-9]*)?(?:[eE][+-]?[0-9]*)?)/u;
|
|
const RE_COMMON_PUNCTUATOR = /(?:[{}()[\]'"`;,#@\\]|[!?<=>&|+\-*/%~^:.]+)/u;
|
|
const RE_OTHER_WORD = /(?:[\p{L}\p{N}\p{S}\p{M}\p{Pc}\p{Cf}]+)/u;
|
|
const RE_NON_WHITESPACE_CHAR = /[^ \t\r\n]/u;
|
|
const RE_UNKNOWN = new RegExp(
|
|
`^(?:${[
|
|
RE_NUMBER_LIKE.source,
|
|
RE_COMMON_PUNCTUATOR.source,
|
|
RE_OTHER_WORD.source,
|
|
RE_NON_WHITESPACE_CHAR.source,
|
|
].join("|")})`,
|
|
"u"
|
|
);
|
|
|
|
const [parse, parseAsToken] = (() => {
|
|
const {
|
|
succeed,
|
|
fail,
|
|
token,
|
|
end,
|
|
backtrackable,
|
|
map,
|
|
pipe,
|
|
skip,
|
|
take,
|
|
between,
|
|
oneOf,
|
|
chain,
|
|
lazy,
|
|
make,
|
|
} = G;
|
|
|
|
const pseudotoken = (type) => backtrackable(fail(type));
|
|
|
|
const symbol = (symbol) =>
|
|
token((source, position, ok, err) => {
|
|
return source.startsWith(symbol, position)
|
|
? ok(symbol, symbol.length)
|
|
: err(symbol);
|
|
});
|
|
|
|
const match = (type, pattern) =>
|
|
token((source, position, ok, err) => {
|
|
const match = source.slice(position).match(pattern);
|
|
return match !== null ? ok(match[0], match[0].length) : err(type);
|
|
});
|
|
|
|
const whitespace = backtrackable(match(undefined, RE_WHITESPACE));
|
|
|
|
const prodValue = lazy(() => prodValueCore);
|
|
|
|
const prodNumber = map(match(undefined, RE_NUMBER), (value) =>
|
|
Number.parseFloat(value)
|
|
);
|
|
|
|
const prodString = map(match("string", RE_STRING), (value) =>
|
|
JSON.parse(value)
|
|
);
|
|
|
|
const prodTrue = map(match(undefined, RE_TRUE), () => true);
|
|
|
|
const prodFalse = map(match(undefined, RE_FALSE), () => false);
|
|
|
|
const prodNull = map(match(undefined, RE_NULL), () => null);
|
|
|
|
const prodObjectEntry = pipe(
|
|
succeed((key) => (value) => [key, value]),
|
|
take(prodString),
|
|
skip(whitespace),
|
|
skip(symbol(":")),
|
|
skip(whitespace),
|
|
take(prodValue)
|
|
);
|
|
|
|
const prodObjectEntries = chain(
|
|
prodObjectEntry,
|
|
between(symbol(","), whitespace, whitespace)
|
|
);
|
|
|
|
const prodObject = pipe(
|
|
succeed((entries) => Object.fromEntries(entries)),
|
|
skip(symbol("{")),
|
|
skip(whitespace),
|
|
take(prodObjectEntries),
|
|
skip(whitespace),
|
|
skip(symbol("}"))
|
|
);
|
|
|
|
const prodArrayEntries = chain(
|
|
prodValue,
|
|
between(symbol(","), whitespace, whitespace)
|
|
);
|
|
|
|
const prodArray = pipe(
|
|
succeed(F.identity),
|
|
skip(symbol("[")),
|
|
skip(whitespace),
|
|
take(prodArrayEntries),
|
|
skip(whitespace),
|
|
skip(symbol("]"))
|
|
);
|
|
|
|
const prodValueCore = oneOf(
|
|
prodObject,
|
|
prodArray,
|
|
prodNumber,
|
|
prodString,
|
|
prodTrue,
|
|
prodFalse,
|
|
prodNull,
|
|
pseudotoken("value")
|
|
);
|
|
|
|
const prodJustValue = pipe(
|
|
succeed(F.identity),
|
|
skip(whitespace),
|
|
take(prodValue),
|
|
skip(whitespace),
|
|
skip(end("end-of-input"))
|
|
);
|
|
|
|
const partialParser = make(prodValue);
|
|
|
|
const justParser = make(prodJustValue);
|
|
|
|
const parse = (source) =>
|
|
R.mapBoth(
|
|
justParser(source, 0),
|
|
({ value }) => value,
|
|
({ position, cause }) => ({ source, position, expected: cause })
|
|
);
|
|
|
|
const parseAsToken = (source, position) =>
|
|
R.mapErr(partialParser(source, position), ({ position, cause }) => ({
|
|
source,
|
|
position,
|
|
expected: cause,
|
|
}));
|
|
|
|
return [parse, parseAsToken];
|
|
})();
|
|
|
|
const expect = (result, errorFormatter = defaultErrorFormatter) =>
|
|
R.expect(result, errorFormatter);
|
|
|
|
const defaultErrorFormatter = (error) => {
|
|
const { source, position, expected } = error;
|
|
const slice = source.slice(position);
|
|
const invalidChar = invalidStringChar(slice, expected);
|
|
return invalidChar !== undefined
|
|
? formatEscapeError(invalidChar)
|
|
: formatTokenError(slice, expected);
|
|
};
|
|
|
|
const invalidStringChar = (slice, expected) => {
|
|
const match =
|
|
expected === "value" || expected === "string"
|
|
? slice.match(RE_INVALID_STRING)
|
|
: null;
|
|
if (match !== null) {
|
|
const char = match[1];
|
|
return char.startsWith("\\")
|
|
? { type: "escape", escapeSequence: char }
|
|
: { type: "control", codePoint: char.codePointAt(0) };
|
|
} else {
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
const formatEscapeError = (char) => {
|
|
const hex4 = (x) => x.toString(16).padStart(4, "0");
|
|
switch (char.type) {
|
|
case "escape":
|
|
return `invalid escape sequence ${char.escapeSequence}`;
|
|
case "control":
|
|
return `invalid control character \\u${hex4(char.codePoint)}`;
|
|
default:
|
|
return F.unreachable();
|
|
}
|
|
};
|
|
|
|
const formatTokenError = (slice, expected) => {
|
|
const match = slice.match(RE_UNKNOWN);
|
|
const expectedText = RE_STRUCTURAL_TOKEN.test(expected)
|
|
? `'${expected}'`
|
|
: expected;
|
|
const foundText =
|
|
match !== null ? JSON.stringify(match[0]) : "no more tokens";
|
|
return `${expectedText} expected, but ${foundText} found`;
|
|
};
|
|
|
|
return {
|
|
parse,
|
|
parseAsToken,
|
|
expect,
|
|
defaultErrorFormatter,
|
|
};
|
|
})();
|
|
|
|
const E = (() => {
|
|
const NUMBER = "number";
|
|
const BOOLEAN = "boolean";
|
|
const ANY = "any";
|
|
|
|
const RE_WHITESPACE = /^[ \t\r\n]*/;
|
|
const RE_IDENTIFIER = /^[a-z$][a-z0-9$_]*/i;
|
|
const RE_BOOLEAN = /^(?:true|false)\b/;
|
|
const RE_DECIMAL_DIGITS = `(?:[0-9]+(?:_[0-9]+)*)`;
|
|
const RE_DECIMAL_INTEGER_LITERAL = `(?:0(?![0-9])|[1-9](?:_?${RE_DECIMAL_DIGITS})?)`;
|
|
const RE_FRACTION_PART_LEADING_DIGITS = `${RE_DECIMAL_INTEGER_LITERAL}(?:\\.${RE_DECIMAL_DIGITS}?)?`;
|
|
const RE_FRACTION_PART_LEADING_DOT = `\\.${RE_DECIMAL_DIGITS}`;
|
|
const RE_FRACTION_PART = `(?:${RE_FRACTION_PART_LEADING_DIGITS}|${RE_FRACTION_PART_LEADING_DOT})`;
|
|
const RE_EXPORNENT_PART = `(?:[eE][+-]?${RE_DECIMAL_DIGITS})`;
|
|
const RE_DECIMAL_LITERAL = `(?:${RE_FRACTION_PART}${RE_EXPORNENT_PART}?)`;
|
|
const RE_BINARY_INTEGER_LITERAL = `(?:0[bB][0-1]+(?:_[0-1]+)*)`;
|
|
const RE_OCTAL_INTEGER_LITERAL = `(?:0[oO][0-7]+(?:_[0-7]+)*)`;
|
|
const RE_HEX_INTEGER_LITERAL = `(?:0[xX][0-9a-fA-F]+(?:_[0-9a-fA-F]+)*)`;
|
|
const RE_NON_DECIMAL_INTEGER_LITERAL = `(?:${[
|
|
RE_BINARY_INTEGER_LITERAL,
|
|
RE_OCTAL_INTEGER_LITERAL,
|
|
RE_HEX_INTEGER_LITERAL,
|
|
].join("|")})`;
|
|
const RE_NUMERIC_LITERAL = `(?:${RE_NON_DECIMAL_INTEGER_LITERAL}|${RE_DECIMAL_LITERAL})`;
|
|
const RE_NUMBER = new RegExp(`^${RE_NUMERIC_LITERAL}`);
|
|
const RE_PUNCTUATOR = /^[()[\].,!+\-*/%<=>&|?:]+$/;
|
|
const RE_JS_UNICODE_ESCAPE_SEQUENCE = `(?:u(?:[0-9a-fA-F]{4}|\\{[0-9a-fA-F]+(?:_[0-9a-fA-F]+)*\\}))`;
|
|
const RE_JS_IDENTIFIER_START = `(?:[\\p{ID_Start}$_]|\\\\${RE_JS_UNICODE_ESCAPE_SEQUENCE})`;
|
|
const RE_JS_IDENTIFIER_PART = `(?:[\\p{ID_Continue}$\\u200C\\u200D]|\\\\${RE_JS_UNICODE_ESCAPE_SEQUENCE})`;
|
|
const RE_JS_IDENTIFIER_NAME = `(?:${RE_JS_IDENTIFIER_START}${RE_JS_IDENTIFIER_PART}*)`;
|
|
const RE_SIGNED_NUMBER = `(?:[+-]?${RE_NUMERIC_LITERAL})`;
|
|
const RE_INVALID_NUMBER = `(?:[+-]?0[0-9]+(?:_[0-9]+)*)`;
|
|
const RE_COMMON_PUNCTUATOR = `(?:[{}()[\\]'"\`;,#@\\\\]|[!?<=>&|+\\-*/%~^:.]+)`;
|
|
const RE_OTHER_WORD = `(?:[\\p{L}\\p{N}\\p{S}\\p{M}\\p{Pc}\\p{Cf}]+)`;
|
|
const RE_NON_WHITESPACE_CHAR = `[^ \\t\\r\\n]`;
|
|
const RE_UNKNOWN = new RegExp(
|
|
`^(?:${[
|
|
RE_JS_IDENTIFIER_NAME,
|
|
RE_SIGNED_NUMBER,
|
|
RE_INVALID_NUMBER,
|
|
RE_COMMON_PUNCTUATOR,
|
|
RE_OTHER_WORD,
|
|
RE_NON_WHITESPACE_CHAR,
|
|
].join("|")})`,
|
|
"u"
|
|
);
|
|
|
|
const Node = (() => {
|
|
const identifier = (name) => ({
|
|
type: "Identifier",
|
|
name,
|
|
});
|
|
|
|
const literal = (value) => ({
|
|
type: "Literal",
|
|
value,
|
|
});
|
|
|
|
const unaryExpression = (operator, argument) => ({
|
|
type: "UnaryExpression",
|
|
operator,
|
|
argument,
|
|
});
|
|
|
|
const binaryExpression = (operator, left, right) => ({
|
|
type: "BinaryExpression",
|
|
operator,
|
|
left,
|
|
right,
|
|
});
|
|
|
|
const logicalExpression = (operator, left, right) => ({
|
|
type: "LogicalExpression",
|
|
operator,
|
|
left,
|
|
right,
|
|
});
|
|
|
|
const memberExpression = (object, property, computed) => ({
|
|
type: "MemberExpression",
|
|
object,
|
|
property,
|
|
computed,
|
|
});
|
|
|
|
const conditionalExpression = (test, consequent, alternate) => ({
|
|
type: "ConditionalExpression",
|
|
test,
|
|
consequent,
|
|
alternate,
|
|
});
|
|
|
|
const callExpression = (callee, arguments_) => ({
|
|
type: "CallExpression",
|
|
callee,
|
|
arguments: arguments_,
|
|
});
|
|
|
|
return {
|
|
identifier,
|
|
literal,
|
|
unaryExpression,
|
|
binaryExpression,
|
|
logicalExpression,
|
|
memberExpression,
|
|
conditionalExpression,
|
|
callExpression,
|
|
};
|
|
})();
|
|
|
|
const [parse, parseAsToken] = (() => {
|
|
const {
|
|
succeed,
|
|
fail,
|
|
token,
|
|
end,
|
|
backtrackable,
|
|
andThen,
|
|
map,
|
|
map2,
|
|
pipe,
|
|
skip,
|
|
take,
|
|
first,
|
|
second,
|
|
between,
|
|
oneOf,
|
|
optional,
|
|
manyl,
|
|
chain1,
|
|
chainl,
|
|
infixl,
|
|
infixr,
|
|
lazy,
|
|
make,
|
|
} = G;
|
|
|
|
const pseudotoken = (type) => backtrackable(fail(type));
|
|
|
|
const symbol = (symbol) =>
|
|
token((source, position, ok, err) => {
|
|
return source.startsWith(symbol, position)
|
|
? ok(symbol, symbol.length)
|
|
: err(symbol);
|
|
});
|
|
|
|
const match = (type, pattern) =>
|
|
token((source, position, ok, err) => {
|
|
const match = source.slice(position).match(pattern);
|
|
return match !== null ? ok(match[0], match[0].length) : err(type);
|
|
});
|
|
|
|
const prodExpression = lazy(() => prodConditionalExpression);
|
|
|
|
const whitespace = backtrackable(match(undefined, RE_WHITESPACE));
|
|
|
|
const prodIdentifier = map(match("identifier", RE_IDENTIFIER), (name) =>
|
|
Node.identifier(name)
|
|
);
|
|
|
|
const prodBooleanLiteral = map(match(undefined, RE_BOOLEAN), (value) =>
|
|
Node.literal(value === "true")
|
|
);
|
|
|
|
const prodNumericLiteral = map(match(undefined, RE_NUMBER), (value) =>
|
|
Node.literal(Number(value.replaceAll("_", "")))
|
|
);
|
|
|
|
const prodParenthesizedExpression = pipe(
|
|
succeed(F.identity),
|
|
skip(symbol("(")),
|
|
skip(whitespace),
|
|
take(prodExpression),
|
|
skip(whitespace),
|
|
skip(symbol(")"))
|
|
);
|
|
|
|
const prodPrimaryExpression = oneOf(
|
|
prodParenthesizedExpression,
|
|
prodNumericLiteral,
|
|
prodBooleanLiteral,
|
|
prodIdentifier,
|
|
pseudotoken("expression")
|
|
);
|
|
|
|
const prodDotIdentifier = pipe(
|
|
succeed(
|
|
(property) => (object) =>
|
|
Node.memberExpression(object, property, false)
|
|
),
|
|
skip(symbol(".")),
|
|
skip(whitespace),
|
|
take(prodIdentifier)
|
|
);
|
|
|
|
const prodComputedIdentifier = pipe(
|
|
succeed(
|
|
(property) => (object) =>
|
|
Node.memberExpression(object, property, true)
|
|
),
|
|
skip(symbol("[")),
|
|
skip(whitespace),
|
|
take(prodExpression),
|
|
skip(whitespace),
|
|
skip(symbol("]"))
|
|
);
|
|
|
|
const prodArgumentList = oneOf(
|
|
first(
|
|
chain1(
|
|
prodExpression,
|
|
backtrackable(between(symbol(","), whitespace, whitespace))
|
|
),
|
|
optional(second(whitespace, symbol(",")))
|
|
),
|
|
succeed([])
|
|
);
|
|
|
|
const prodArguments = pipe(
|
|
succeed(
|
|
(arguments_) => (callee) => Node.callExpression(callee, arguments_)
|
|
),
|
|
skip(symbol("(")),
|
|
skip(whitespace),
|
|
take(prodArgumentList),
|
|
skip(whitespace),
|
|
skip(symbol(")"))
|
|
);
|
|
|
|
const prodPostfixOperator = oneOf(
|
|
prodDotIdentifier,
|
|
prodComputedIdentifier,
|
|
prodArguments
|
|
);
|
|
|
|
const prodCallExpression = andThen(prodPrimaryExpression, (expr) =>
|
|
chainl(
|
|
prodPostfixOperator,
|
|
whitespace,
|
|
(expr, ctor) => ctor(expr),
|
|
expr
|
|
)
|
|
);
|
|
|
|
const prodUnaryOperator = oneOf(
|
|
match("+", /^\+(?![+=])/),
|
|
match("-", /^\-(?![-=])/),
|
|
symbol("!")
|
|
);
|
|
|
|
const prodUnaryExpression = map2(
|
|
manyl(
|
|
first(prodUnaryOperator, whitespace),
|
|
(stack, operator) => L.cons(operator, stack),
|
|
L.nil
|
|
),
|
|
prodCallExpression,
|
|
(stack, expr) =>
|
|
L.reduce(
|
|
stack,
|
|
(argument, operator) => Node.unaryExpression(operator, argument),
|
|
expr
|
|
)
|
|
);
|
|
|
|
const infixOperator = (ctor, operators) =>
|
|
map(
|
|
between(operators, whitespace, whitespace),
|
|
(operator) => (left, right) => ctor(operator, left, right)
|
|
);
|
|
|
|
const prodExponetiationExpression = infixr(
|
|
prodUnaryExpression,
|
|
infixOperator(Node.binaryExpression, match("**", /^\*\*(?!=)/))
|
|
);
|
|
|
|
const prodMultiplicativeExpresssion = infixl(
|
|
prodExponetiationExpression,
|
|
infixOperator(
|
|
Node.binaryExpression,
|
|
oneOf(
|
|
match("*", /^\*(?![*=])/),
|
|
match("/", /^\/(?!=)/),
|
|
match("%", /^\%(?!=)/)
|
|
)
|
|
)
|
|
);
|
|
|
|
const prodAdditiveExpression = infixl(
|
|
prodMultiplicativeExpresssion,
|
|
infixOperator(
|
|
Node.binaryExpression,
|
|
oneOf(match("+", /^\+(?![+=])/), match("-", /^\-(?![-=])/))
|
|
)
|
|
);
|
|
|
|
const prodRelationalExpression = infixl(
|
|
prodAdditiveExpression,
|
|
infixOperator(
|
|
Node.binaryExpression,
|
|
oneOf(symbol("<="), symbol(">="), symbol("<"), symbol(">"))
|
|
)
|
|
);
|
|
|
|
const prodEqualityExpression = infixl(
|
|
prodRelationalExpression,
|
|
infixOperator(
|
|
Node.binaryExpression,
|
|
oneOf(symbol("==="), symbol("!=="))
|
|
)
|
|
);
|
|
|
|
const prodLogicalAndExpression = infixl(
|
|
prodEqualityExpression,
|
|
infixOperator(Node.logicalExpression, match("&&", /^\&\&(?!=)/))
|
|
);
|
|
|
|
const prodLogicalOrExpression = infixl(
|
|
prodLogicalAndExpression,
|
|
infixOperator(Node.logicalExpression, match("||", /^\|\|(?!=)/))
|
|
);
|
|
|
|
const prodConditionalExpression = infixr(
|
|
prodLogicalOrExpression,
|
|
pipe(
|
|
succeed(
|
|
(consequent) => (test, alternate) =>
|
|
Node.conditionalExpression(test, consequent, alternate)
|
|
),
|
|
skip(whitespace),
|
|
skip(match("?", /^\?(?![?.])/)),
|
|
skip(whitespace),
|
|
take(prodExpression),
|
|
skip(whitespace),
|
|
skip(symbol(":")),
|
|
skip(whitespace)
|
|
)
|
|
);
|
|
|
|
const prodJustExpression = pipe(
|
|
succeed(F.identity),
|
|
skip(whitespace),
|
|
take(prodExpression),
|
|
skip(whitespace),
|
|
skip(end("end-of-input"))
|
|
);
|
|
|
|
const partialParser = make(prodExpression);
|
|
|
|
const justParser = make(prodJustExpression);
|
|
|
|
const parse = (source) =>
|
|
R.mapBoth(
|
|
justParser(source, 0),
|
|
({ value }) => value,
|
|
({ position, cause }) => ({ source, position, expected: cause })
|
|
);
|
|
|
|
const parseAsToken = (source, position) =>
|
|
R.mapErr(partialParser(source, position), ({ position, cause }) => ({
|
|
source,
|
|
position,
|
|
expected: cause,
|
|
}));
|
|
|
|
return [parse, parseAsToken];
|
|
})();
|
|
|
|
const build = (() => {
|
|
const BUILTIN_VARS = { Infinity, NaN, Math };
|
|
|
|
const MEMBER_BLOCK_LIST = ["prototype", "constructor"];
|
|
|
|
const VALUE_BLOCK_LIST = new Map([
|
|
[globalThis, "global object"],
|
|
[Object, "Object"],
|
|
[Object.prototype, "Object.prototype"],
|
|
[Function, "Function"],
|
|
[Function.prototype, "Function.prototype"],
|
|
]);
|
|
|
|
const referenceError = (name) => ({ type: "reference", name });
|
|
const propertyError = (property) => ({ type: "property", property });
|
|
const rangeError = (index) => ({ type: "range", index });
|
|
const typeError = (expected, actual) => ({
|
|
type: "type",
|
|
expected,
|
|
actual,
|
|
});
|
|
const securityError = (target) => ({ type: "security", target });
|
|
|
|
const ERROR_SYMBOL = Symbol("internal build error");
|
|
const throw_ = (error) => F.throw({ [ERROR_SYMBOL]: error });
|
|
const try_ = (fn) =>
|
|
F.try(
|
|
() => R.ok(fn()),
|
|
(e) =>
|
|
Object.hasOwn(e, ERROR_SYMBOL) ? R.err(e[ERROR_SYMBOL]) : F.throw(e)
|
|
);
|
|
|
|
const isBuiltinVariable = (name) => Object.hasOwn(BUILTIN_VARS, name);
|
|
const builtinVariableValue = (name) => BUILTIN_VARS[name];
|
|
const isBlockedMember = (name) => MEMBER_BLOCK_LIST.includes(name);
|
|
const isBlockedValue = (value) => VALUE_BLOCK_LIST.has(value);
|
|
const blockedValueName = (value) => VALUE_BLOCK_LIST.get(value);
|
|
|
|
const assertType = (name, fn) => (value) =>
|
|
fn(value) ? value : throw_(typeError(name, value));
|
|
|
|
const assertNumber = assertType("number", (x) => typeof x === "number");
|
|
|
|
const assertInteger = assertType(
|
|
"integer",
|
|
(x) => typeof x === "number" && Number.isSafeInteger(x)
|
|
);
|
|
|
|
const assertBoolean = assertType(
|
|
"boolean",
|
|
(x) => typeof x === "boolean"
|
|
);
|
|
|
|
const assertObject = assertType(
|
|
"object",
|
|
(x) => (typeof x === "object" && x !== null) || typeof x === "function"
|
|
);
|
|
|
|
const assertFunction = assertType(
|
|
"function",
|
|
(x) => typeof x === "function"
|
|
);
|
|
|
|
const assertArray = assertType("array", (x) => Array.isArray(x));
|
|
|
|
const assertSecure = (value) =>
|
|
!isBlockedValue(value)
|
|
? value
|
|
: throw_(securityError(blockedValueName(value)));
|
|
|
|
const member = (object, property) =>
|
|
property in object ? object[property] : throw_(propertyError(property));
|
|
|
|
const element = (array, index) =>
|
|
index >= 0 && index < array.length
|
|
? array[index]
|
|
: throw_(rangeError(index));
|
|
|
|
const build = (type, node) => {
|
|
const assertExprType = typeContract(type);
|
|
const evalExpr = expression(node);
|
|
return (env) => try_(() => assertExprType(evalExpr(env)));
|
|
};
|
|
|
|
const typeContract = (type) => {
|
|
switch (type) {
|
|
case NUMBER:
|
|
return assertNumber;
|
|
case BOOLEAN:
|
|
return assertBoolean;
|
|
case ANY:
|
|
return F.identity;
|
|
default:
|
|
return F.panic(`unsupported expression type: ${type}`);
|
|
}
|
|
};
|
|
|
|
const expression = (node) => {
|
|
const { type } = node;
|
|
switch (type) {
|
|
case "Identifier":
|
|
return identifier(node);
|
|
case "Literal":
|
|
return literal(node);
|
|
case "UnaryExpression":
|
|
return unaryExpression(node);
|
|
case "BinaryExpression":
|
|
return binaryExpression(node);
|
|
case "LogicalExpression":
|
|
return logicalExpression(node);
|
|
case "MemberExpression":
|
|
return memberExpression(node);
|
|
case "CallExpression":
|
|
return callExpression(node);
|
|
case "ConditionalExpression":
|
|
return conditionalExpression(node);
|
|
default:
|
|
return F.panic(`invalid AST node type: ${type}`);
|
|
}
|
|
};
|
|
|
|
const identifier = (node) => {
|
|
const { name } = node;
|
|
if (isBuiltinVariable(name)) {
|
|
const value = builtinVariableValue(name);
|
|
return () => value;
|
|
} else {
|
|
return (env) =>
|
|
Object.hasOwn(env, name) ? env[name] : throw_(referenceError(name));
|
|
}
|
|
};
|
|
|
|
const literal = (node) => {
|
|
const { value } = node;
|
|
return () => value;
|
|
};
|
|
|
|
const unaryExpression = (node) => {
|
|
const { operator, argument } = node;
|
|
const evalArgument = expression(argument);
|
|
switch (operator) {
|
|
case "+":
|
|
return (env) => +assertNumber(evalArgument(env));
|
|
case "-":
|
|
return (env) => -assertNumber(evalArgument(env));
|
|
case "!":
|
|
return (env) => !assertBoolean(evalArgument(env));
|
|
default:
|
|
return F.panic(`unsupported unary operator: ${operator}`);
|
|
}
|
|
};
|
|
|
|
const binaryExpression = (node) => {
|
|
const { operator, left, right } = node;
|
|
const evalL = expression(left);
|
|
const evalR = expression(right);
|
|
switch (operator) {
|
|
case "+":
|
|
return (env) => assertNumber(evalL(env)) + assertNumber(evalR(env));
|
|
case "-":
|
|
return (env) => assertNumber(evalL(env)) - assertNumber(evalR(env));
|
|
case "*":
|
|
return (env) => assertNumber(evalL(env)) * assertNumber(evalR(env));
|
|
case "/":
|
|
return (env) => assertNumber(evalL(env)) / assertNumber(evalR(env));
|
|
case "%":
|
|
return (env) => assertNumber(evalL(env)) % assertNumber(evalR(env));
|
|
case "**":
|
|
return (env) =>
|
|
assertNumber(evalL(env)) ** assertNumber(evalR(env));
|
|
case "===":
|
|
return (env) =>
|
|
assertNumber(evalL(env)) === assertNumber(evalR(env));
|
|
case "!==":
|
|
return (env) =>
|
|
assertNumber(evalL(env)) !== assertNumber(evalR(env));
|
|
case "<=":
|
|
return (env) =>
|
|
assertNumber(evalL(env)) <= assertNumber(evalR(env));
|
|
case ">=":
|
|
return (env) =>
|
|
assertNumber(evalL(env)) >= assertNumber(evalR(env));
|
|
case "<":
|
|
return (env) => assertNumber(evalL(env)) < assertNumber(evalR(env));
|
|
case ">":
|
|
return (env) => assertNumber(evalL(env)) > assertNumber(evalR(env));
|
|
default:
|
|
return F.panic(`unsupported binary operator: ${operator}`);
|
|
}
|
|
};
|
|
|
|
const logicalExpression = (node) => {
|
|
const { operator, left, right } = node;
|
|
const evalL = expression(left);
|
|
const evalR = expression(right);
|
|
switch (operator) {
|
|
case "&&":
|
|
return (env) =>
|
|
assertBoolean(evalL(env)) && assertBoolean(evalR(env));
|
|
case "||":
|
|
return (env) =>
|
|
assertBoolean(evalL(env)) || assertBoolean(evalR(env));
|
|
default:
|
|
return F.panic(`unsupported logical operator: ${operator}`);
|
|
}
|
|
};
|
|
|
|
const memberExpression = (node) => {
|
|
const { computed } = node;
|
|
const { evalThis, evalExpr } = computed
|
|
? computedMemberExpression(node)
|
|
: dotMemberExpression(node);
|
|
return (env) => evalExpr(evalThis(env), env);
|
|
};
|
|
|
|
const dotMemberExpression = (node) => {
|
|
const {
|
|
object,
|
|
property: { name },
|
|
} = node;
|
|
const evalThis = expression(object);
|
|
const evalExpr = isBlockedMember(name)
|
|
? () => throw_(securityError(`${name} property`))
|
|
: (this_) => assertSecure(member(assertObject(this_), name));
|
|
return { evalThis, evalExpr };
|
|
};
|
|
|
|
const computedMemberExpression = (node) => {
|
|
const { object, property } = node;
|
|
const evalThis = expression(object);
|
|
const evalProperty = expression(property);
|
|
const evalExpr = (this_, env) =>
|
|
assertSecure(
|
|
element(assertArray(this_), assertInteger(evalProperty(env)))
|
|
);
|
|
return { evalThis, evalExpr };
|
|
};
|
|
|
|
const conditionalExpression = (node) => {
|
|
const { test, alternate, consequent } = node;
|
|
const evalIf = expression(test);
|
|
const evalThen = expression(consequent);
|
|
const evalElse = expression(alternate);
|
|
return (env) =>
|
|
assertBoolean(evalIf(env)) ? evalThen(env) : evalElse(env);
|
|
};
|
|
|
|
const callExpression = (node) => {
|
|
const { callee, arguments: arguments_ } = node;
|
|
return callee.type === "MemberExpression"
|
|
? functionMemberCall(callee, arguments_)
|
|
: functionStaticCall(callee, arguments_);
|
|
};
|
|
|
|
const functionMemberCall = (node, arguments_) => {
|
|
const { computed } = node;
|
|
const { evalThis, evalExpr } = computed
|
|
? computedMemberExpression(node)
|
|
: dotMemberExpression(node);
|
|
const evalArgs = functionArgs(arguments_);
|
|
return (env) => {
|
|
const this_ = evalThis(env);
|
|
const fn = assertFunction(evalExpr(this_, env));
|
|
const args = evalArgs(env);
|
|
return assertSecure(fn.apply(this_, args));
|
|
};
|
|
};
|
|
|
|
const functionStaticCall = (node, arguments_) => {
|
|
const evalCallee = expression(node);
|
|
const evalArgs = functionArgs(arguments_);
|
|
return (env) =>
|
|
assertSecure(assertFunction(evalCallee(env))(...evalArgs(env)));
|
|
};
|
|
|
|
const functionArgs = (nodes) => {
|
|
const evalList = nodes.map((node) => expression(node));
|
|
return (env) => evalList.map((evalArg) => evalArg(env));
|
|
};
|
|
|
|
return build;
|
|
})();
|
|
|
|
const compile = (type, source) =>
|
|
R.map(parse(source), (node) => build(type, node));
|
|
|
|
const expect = (result, errorFormatter = defaultCompileErrorFormatter) =>
|
|
R.expect(result, errorFormatter);
|
|
|
|
const run = (
|
|
evaluator,
|
|
env,
|
|
errorFormatter = defaultRuntimeErrorFormatter
|
|
) => R.expect(evaluator(env), errorFormatter);
|
|
|
|
const interpret = (
|
|
type,
|
|
source,
|
|
env,
|
|
parseErrorFormatter,
|
|
runtimeErrorFormatter
|
|
) =>
|
|
run(
|
|
expect(compile(type, source), parseErrorFormatter),
|
|
env,
|
|
runtimeErrorFormatter
|
|
);
|
|
|
|
const defaultCompileErrorFormatter = (error) => {
|
|
const { source, position, expected } = error;
|
|
const match = source.slice(position).match(RE_UNKNOWN);
|
|
const expectedText = RE_PUNCTUATOR.test(expected)
|
|
? `'${expected}'`
|
|
: expected;
|
|
const foundText =
|
|
match !== null ? JSON.stringify(match[0]) : "no more tokens";
|
|
return `${expectedText} expected, but ${foundText} found`;
|
|
};
|
|
|
|
const defaultRuntimeErrorFormatter = (error) => {
|
|
switch (error?.type) {
|
|
case "reference":
|
|
return `variable "${error.name}" not defined`;
|
|
case "property":
|
|
return `property "${error.property}" not defined`;
|
|
case "range":
|
|
return `index ${error.index} out of range`;
|
|
case "type":
|
|
return `${error.expected} expected, but ${D.fmt(error.actual)} found`;
|
|
case "security":
|
|
return `<${error.target}> not allowed for security reasons`;
|
|
default:
|
|
return `unknown error: ${D.fmt(error)}`;
|
|
}
|
|
};
|
|
|
|
return {
|
|
NUMBER,
|
|
BOOLEAN,
|
|
ANY,
|
|
Node,
|
|
parse,
|
|
parseAsToken,
|
|
build,
|
|
compile,
|
|
expect,
|
|
run,
|
|
interpret,
|
|
defaultCompileErrorFormatter,
|
|
defaultRuntimeErrorFormatter,
|
|
};
|
|
})();
|
|
|
|
const T = (() => {
|
|
const RE_REST = /[ \t\r\n]*$/;
|
|
const RE_LINE = /[ \t\r]*(?:\n|$)/;
|
|
const RE_WORD = /(?:[ \t\r\n]|$)/;
|
|
const RE_SPACES = /^[ \t\r\n]*/;
|
|
const RE_SPACES1 = /^[ \t\r\n]+/;
|
|
const RE_NATURAL = /^(?:0(?![0-9])|[1-9][0-9]*)/;
|
|
const RE_INTEGER = /^(?:0(?![0-9])|[+-]?[1-9][0-9]*)/;
|
|
const RE_NUMBER =
|
|
/^[+-]?(?:0(?![0-9])|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/;
|
|
const RE_BOOLEAN = /^(?:true|false)\b/;
|
|
const RE_TEXT = /^(?:'(?:[^'`\n]|`['`])*'|"(?:[^"`\n]|`["`])*")/u;
|
|
const RE_ASCIIWORD = /^\w+/;
|
|
const RE_IDENTIFIER = /^\p{XID_Start}\p{XID_Continue}*/u;
|
|
const RE_NUMBER_LIKE = /(?:[+-]?[0-9]+(?:\.[0-9]*)?(?:[eE][+-]?[0-9]*)?)/u;
|
|
const RE_TEXT_LIKE = /(?:'[^'\n]*'|"[^"\n]*")/u;
|
|
const RE_COMMON_PUNCTUATOR = /(?:[{}()[\]'"`;,#@\\]|[!?<=>&|+\-*/%~^:.]+)/u;
|
|
const RE_OTHER_WORD = /(?:[\p{L}\p{N}\p{S}\p{M}\p{Pc}\p{Cf}]+)/u;
|
|
const RE_NON_WHITESPACE_CHAR = /[^ \t\r\n]/u;
|
|
const RE_UNKNOWN = new RegExp(
|
|
`^(?:${[
|
|
RE_NUMBER_LIKE.source,
|
|
RE_TEXT_LIKE.source,
|
|
RE_COMMON_PUNCTUATOR.source,
|
|
RE_OTHER_WORD.source,
|
|
RE_NON_WHITESPACE_CHAR.source,
|
|
].join("|")})`,
|
|
"u"
|
|
);
|
|
|
|
const {
|
|
succeed,
|
|
fail,
|
|
token,
|
|
end: endG,
|
|
inContext,
|
|
backtrackable,
|
|
andThen,
|
|
map,
|
|
pipe,
|
|
skip,
|
|
take,
|
|
first,
|
|
second,
|
|
between,
|
|
oneOf,
|
|
optional,
|
|
lazy,
|
|
many,
|
|
many1,
|
|
chain,
|
|
chain1,
|
|
proc,
|
|
try: try_,
|
|
validate: validateG,
|
|
make: makeG,
|
|
} = G;
|
|
|
|
const tokenError = (kind, expected) => ({ type: "token", kind, expected });
|
|
const jsonError = (cause) => ({ type: "json", cause });
|
|
const expressionError = (cause) => ({ type: "expression", cause });
|
|
const validationError = (cause, length) => ({
|
|
type: "validation",
|
|
cause,
|
|
length,
|
|
});
|
|
|
|
const symbolTokenError = (expected) => tokenError("symbol", expected);
|
|
const keywordTokenError = (expected) => tokenError("keyword", expected);
|
|
const patternTokenError = (expected) => tokenError("pattern", expected);
|
|
const pseudoTokenError = (expected) => tokenError("pseudo", expected);
|
|
|
|
const end = endG(pseudoTokenError("end-of-input"));
|
|
|
|
const pseudotoken = (type) => backtrackable(fail(pseudoTokenError(type)));
|
|
|
|
const matchOr = (cause, pattern) => {
|
|
if (typeof pattern === "string") return matchStringOr(cause, pattern);
|
|
if (isMatchable(pattern)) return matchMatchableOr(cause, pattern);
|
|
return F.panic(
|
|
`pattern must be string or object which has @@match method (e.g. RegExp): ${D.fmt(
|
|
pattern
|
|
)}`
|
|
);
|
|
};
|
|
|
|
const isMatchable = (value) =>
|
|
value !== undefined &&
|
|
value !== null &&
|
|
typeof value[Symbol.match] === "function";
|
|
|
|
const matchStringOr = (cause, pattern) =>
|
|
token((source, position, ok, err) => {
|
|
return source.startsWith(pattern, position)
|
|
? ok(pattern, pattern.length)
|
|
: err(cause);
|
|
});
|
|
|
|
const matchMatchableOr = (cause, pattern) =>
|
|
token((source, position, ok, err) => {
|
|
const match = source.slice(position).match(pattern);
|
|
if (match !== null && match.index === 0) {
|
|
const value = match[0];
|
|
return ok(value, value.length);
|
|
} else {
|
|
return err(cause);
|
|
}
|
|
});
|
|
|
|
const symbol = (symbol) => matchStringOr(symbolTokenError(symbol), symbol);
|
|
|
|
const keyword = (keyword) =>
|
|
matchStringOr(keywordTokenError(keyword), keyword);
|
|
|
|
const match = (type, pattern) => matchOr(patternTokenError(type), pattern);
|
|
|
|
const until = (pattern) => {
|
|
if (typeof pattern === "string") return untilString(pattern);
|
|
if (isSearchable(pattern)) return untilSearchable(pattern);
|
|
return F.panic(
|
|
`pattern must be string or object which has @@search method (e.g. RegExp): ${D.fmt(
|
|
pattern
|
|
)}`
|
|
);
|
|
};
|
|
|
|
const isSearchable = (value) =>
|
|
value !== undefined &&
|
|
value !== null &&
|
|
typeof value[Symbol.search] === "function";
|
|
|
|
const untilString = (pattern) =>
|
|
token((source, position, ok, _err) => {
|
|
const slice = source.slice(position);
|
|
const index = slice.indexOf(pattern);
|
|
return index !== -1
|
|
? ok(source.slice(position, position + index), index)
|
|
: ok(slice, slice.length);
|
|
});
|
|
|
|
const untilSearchable = (pattern) =>
|
|
token((source, position, ok, _err) => {
|
|
const slice = source.slice(position);
|
|
const index = slice.search(pattern);
|
|
return index !== -1
|
|
? ok(source.slice(position, position + index), index)
|
|
: ok(slice, slice.length);
|
|
});
|
|
|
|
const enum_ = (type, variants) =>
|
|
oneOf(
|
|
...variants.map((variant) => matchStringOr(undefined, variant)),
|
|
pseudotoken(type)
|
|
);
|
|
|
|
const rest = until(RE_REST);
|
|
|
|
const line = until(RE_LINE);
|
|
|
|
const word = until(RE_WORD);
|
|
|
|
const spaces = backtrackable(matchOr(undefined, RE_SPACES));
|
|
|
|
const spaces1 = backtrackable(match("spaces", RE_SPACES1));
|
|
|
|
const natural = map(match("natural", RE_NATURAL), (value) =>
|
|
Number.parseInt(value, 10)
|
|
);
|
|
|
|
const integer = map(match("integer", RE_INTEGER), (value) =>
|
|
Number.parseInt(value, 10)
|
|
);
|
|
|
|
const number = map(match("number", RE_NUMBER), (value) =>
|
|
Number.parseFloat(value)
|
|
);
|
|
|
|
const boolean = map(match("boolean", RE_BOOLEAN), (value) =>
|
|
value === "true" ? true : false
|
|
);
|
|
|
|
const text = map(match("text", RE_TEXT), (value) =>
|
|
value.slice(1, -1).replaceAll(/`(.)/gu, "$1")
|
|
);
|
|
|
|
const asciiword = match("asciiword", RE_ASCIIWORD);
|
|
|
|
const identifier = match("identifier", RE_IDENTIFIER);
|
|
|
|
const embed = (parser, mapErr) =>
|
|
token((source, position, ok, err) =>
|
|
R.match(
|
|
parser(source, position),
|
|
({ value, position: end }) => ok(value, end - position),
|
|
(cause) => err(mapErr(cause))
|
|
)
|
|
);
|
|
|
|
const json = embed(J.parseAsToken, jsonError);
|
|
|
|
const expression = (type) =>
|
|
map(embed(E.parseAsToken, expressionError), (node) => {
|
|
const evaluator = E.build(type, node);
|
|
const evalFn = (env, errorFormatter) =>
|
|
E.run(evaluator, env, errorFormatter);
|
|
return evalFn;
|
|
});
|
|
|
|
const numberExpr = expression(E.NUMBER);
|
|
|
|
const booleanExpr = expression(E.BOOLEAN);
|
|
|
|
const anyExpr = expression(E.ANY);
|
|
|
|
const relax = (parser) => between(parser, spaces, spaces);
|
|
|
|
const group = (parser, open, close) => between(relax(parser), open, close);
|
|
|
|
const parens = (parser) => group(parser, symbol("("), symbol(")"));
|
|
|
|
const braces = (parser) => group(parser, symbol("{"), symbol("}"));
|
|
|
|
const brackets = (parser) => group(parser, symbol("["), symbol("]"));
|
|
|
|
const just = (parser) => first(relax(parser), end);
|
|
|
|
const list = (parser) => chain(parser, spaces1);
|
|
|
|
const list1 = (parser) => chain1(parser, spaces1);
|
|
|
|
const join = (parsers, delimiter) =>
|
|
parsers.length !== 0
|
|
? map(
|
|
parsers.slice(1).reduce(
|
|
(acc, parser) =>
|
|
pipe(
|
|
succeed((xs) => (x) => L.cons(x, xs)),
|
|
take(acc),
|
|
skip(delimiter),
|
|
take(parser)
|
|
),
|
|
map(parsers[0], L.singleton)
|
|
),
|
|
(list) => L.toArray(list).reverse()
|
|
)
|
|
: F.panic("unable to join empty array of parsers");
|
|
|
|
const tuple = (...parsers) => join(parsers, spaces1);
|
|
|
|
const validate = (parser, validator) =>
|
|
validateG(parser, (value, length, ok, err) =>
|
|
validator(value, ok, (cause) => err(validationError(cause, length)))
|
|
);
|
|
|
|
const make = (parser) => {
|
|
const builtParser = makeG(parser);
|
|
return (source) =>
|
|
R.mapBoth(
|
|
builtParser(source, 0),
|
|
({ value }) => value,
|
|
({ position, cause, stack }) => ({ source, position, cause, stack })
|
|
);
|
|
};
|
|
|
|
const parse = (source, parser, errorFormatter = defaultErrorFormatter) =>
|
|
R.expect(make(parser)(source), errorFormatter);
|
|
|
|
const defaultErrorFormatter = (deadEnd) => {
|
|
const { source, position, cause: error } = deadEnd;
|
|
switch (error?.type) {
|
|
case "token":
|
|
return formatTokenError(source, position, error);
|
|
case "json":
|
|
return J.defaultErrorFormatter(error.cause);
|
|
case "expression":
|
|
return E.defaultCompileErrorFormatter(error.cause);
|
|
case "validation":
|
|
return formatValidationError(error);
|
|
default:
|
|
return `unknown error: ${D.fmt(error)}`;
|
|
}
|
|
};
|
|
|
|
const formatTokenError = (source, position, error) => {
|
|
const { kind, expected } = error;
|
|
const expectedText = formatExpectedToken(kind, expected);
|
|
const foundText = nextToken(source, position);
|
|
return `${expectedText} expected, but ${foundText} found`;
|
|
};
|
|
|
|
const formatExpectedToken = (kind, expected) => {
|
|
switch (kind) {
|
|
case "symbol":
|
|
return emphasizeExpectedToken(expected);
|
|
case "keyword":
|
|
return emphasizeExpectedToken(expected);
|
|
case "pattern":
|
|
return expected;
|
|
case "pseudo":
|
|
return expected;
|
|
default:
|
|
return F.unreachable();
|
|
}
|
|
};
|
|
|
|
const emphasizeExpectedToken = (expected) =>
|
|
expected.startsWith("'") || expected.endsWith("'")
|
|
? `<${expected}>`
|
|
: `'${expected}'`;
|
|
|
|
const nextToken = (source, position) => {
|
|
const slice = source.slice(position);
|
|
const match = slice.match(RE_UNKNOWN);
|
|
if (match !== null) {
|
|
return JSON.stringify(match[0]);
|
|
} else {
|
|
return RE_SPACES1.test(slice) ? "spaces" : "no more characters";
|
|
}
|
|
};
|
|
|
|
const formatValidationError = (error) => {
|
|
const { cause } = error;
|
|
return typeof cause === "string" ? cause : D.fmt(cause);
|
|
};
|
|
|
|
return {
|
|
succeed,
|
|
fail,
|
|
inContext,
|
|
backtrackable,
|
|
andThen,
|
|
map,
|
|
pipe,
|
|
skip,
|
|
take,
|
|
first,
|
|
second,
|
|
between,
|
|
oneOf,
|
|
optional,
|
|
lazy,
|
|
many,
|
|
many1,
|
|
chain,
|
|
chain1,
|
|
proc,
|
|
try: try_,
|
|
end,
|
|
pseudotoken,
|
|
symbol,
|
|
keyword,
|
|
match,
|
|
until,
|
|
enum: enum_,
|
|
rest,
|
|
line,
|
|
word,
|
|
spaces,
|
|
spaces1,
|
|
natural,
|
|
integer,
|
|
number,
|
|
boolean,
|
|
text,
|
|
asciiword,
|
|
identifier,
|
|
json,
|
|
numberExpr,
|
|
booleanExpr,
|
|
anyExpr,
|
|
relax,
|
|
group,
|
|
parens,
|
|
braces,
|
|
brackets,
|
|
just,
|
|
list,
|
|
list1,
|
|
join,
|
|
tuple,
|
|
validate,
|
|
make,
|
|
parse,
|
|
defaultErrorFormatter,
|
|
};
|
|
})();
|
|
|
|
const P = (() => {
|
|
const keyError = (key) => ({ type: "key", key });
|
|
const encodingError = () => ({ type: "encoding" });
|
|
const structureError = (expected) => ({ type: "structure", expected });
|
|
const notationError = (format, cause) => ({
|
|
type: "notation",
|
|
format,
|
|
cause,
|
|
});
|
|
const jsonError = (cause) => ({ type: "json", cause });
|
|
const expressionError = (cause) => ({ type: "expression", cause });
|
|
const customError = (cause) => ({ type: "custom", cause });
|
|
const validationError = (cause) => ({ type: "validation", cause });
|
|
|
|
const ok = (value) => ({ ok: true, value });
|
|
const err = (backtrackable, deadEnd) => ({
|
|
ok: false,
|
|
backtrackable,
|
|
deadEnd,
|
|
});
|
|
const mkState = (source, path) => ({ source, path });
|
|
const mkDeadEnd = (source, path, cause) => ({ source, path, cause });
|
|
const match = (result, onOk, onErr) =>
|
|
result.ok ? onOk(result) : onErr(result);
|
|
|
|
const succeed = (value) => (_state) => ok(value);
|
|
|
|
const fail =
|
|
(cause) =>
|
|
({ source, path }) =>
|
|
err(false, mkDeadEnd(source, path, cause));
|
|
|
|
const backtrackable = (parser) => (state) =>
|
|
match(parser(state), F.identity, ({ deadEnd }) => err(true, deadEnd));
|
|
|
|
const andThen = (parser, fn) => (state) =>
|
|
match(parser(state), ({ value }) => fn(value)(state), F.identity);
|
|
|
|
const map = (parser, fn) => (state) =>
|
|
match(parser(state), ({ value }) => ok(fn(value)), F.identity);
|
|
|
|
const map2 = (first, second, fn) => (state) =>
|
|
match(
|
|
first(state),
|
|
({ value: v1 }) =>
|
|
match(second(state), ({ value: v2 }) => ok(fn(v1, v2)), F.identity),
|
|
F.identity
|
|
);
|
|
|
|
const allOf = (...parsers) =>
|
|
map(
|
|
parsers.reduce(
|
|
(acc, parser) => map2(acc, parser, (xs, x) => L.cons(x, xs)),
|
|
succeed(L.nil)
|
|
),
|
|
(list) => L.toArray(list).reverse()
|
|
);
|
|
|
|
const or = (first, second) => (state) =>
|
|
match(first(state), F.identity, ({ backtrackable, deadEnd }) =>
|
|
backtrackable ? second(state) : err(false, deadEnd)
|
|
);
|
|
|
|
const oneOf = (...parsers) =>
|
|
parsers.reduceRight((acc, parser) => or(parser, acc));
|
|
|
|
const optional = (parser, value) => or(parser, succeed(value));
|
|
|
|
const validate = (parser, validator) => (state) =>
|
|
match(
|
|
parser(state),
|
|
({ value: v1 }) =>
|
|
validator(
|
|
v1,
|
|
(v2) => ok(v2),
|
|
(cause) =>
|
|
err(
|
|
false,
|
|
mkDeadEnd(state.source, state.path, validationError(cause))
|
|
)
|
|
),
|
|
F.identity
|
|
);
|
|
|
|
const keyed = (key, parser) =>
|
|
typeof key === "string" && key !== ""
|
|
? ({ source, path }) =>
|
|
Object.hasOwn(source, key)
|
|
? parser(mkState(source[key], L.cons(key, path)))
|
|
: err(true, mkDeadEnd(source, path, keyError(key)))
|
|
: F.panic(`invalid property key: ${D.fmt(key)}`);
|
|
|
|
const entry = (key, parser) =>
|
|
map(keyed(key, parser), (value) => [key, value]);
|
|
|
|
const as = (key, parser) => map(parser, ([_, value]) => [key, value]);
|
|
|
|
const object = (...parsers) =>
|
|
map(allOf(...parsers), (entries) => Object.fromEntries(entries));
|
|
|
|
const valueParser = (parser, fn) => (state) =>
|
|
parser(
|
|
state.source,
|
|
(value) => fn(value, state),
|
|
(cause) =>
|
|
err(state.source === "", mkDeadEnd(state.source, state.path, cause))
|
|
);
|
|
|
|
const simpleValue = (parser) => valueParser(parser, (value) => ok(value));
|
|
|
|
const encodedValue = (fn) =>
|
|
valueParser(
|
|
(source, ok, err) =>
|
|
F.try(
|
|
() => ok(JSON.parse(source)),
|
|
() => err(encodingError())
|
|
),
|
|
fn
|
|
);
|
|
|
|
const structuredValue = (type, validate, fn) =>
|
|
encodedValue((value, { source, path }) =>
|
|
validate(value)
|
|
? fn(mkState(value, path))
|
|
: err(false, mkDeadEnd(source, path, structureError(type)))
|
|
);
|
|
|
|
const of = (format, parser) => {
|
|
const builtParser = T.make(parser);
|
|
return simpleValue((source, ok, err) =>
|
|
R.match(
|
|
builtParser(source),
|
|
(value) => ok(value),
|
|
(cause) => err(notationError(format, cause))
|
|
)
|
|
);
|
|
};
|
|
|
|
const from = (format, parser) => of(format, T.just(parser));
|
|
|
|
const fromPrimitive = (format, parser) =>
|
|
of(format, T.first(parser, T.end));
|
|
|
|
const enum_ = (format, variants) =>
|
|
fromPrimitive(format, T.enum(format, variants));
|
|
|
|
const string = (state) => ok(state.source);
|
|
|
|
const natural = fromPrimitive("natural", T.natural);
|
|
|
|
const integer = fromPrimitive("integer", T.integer);
|
|
|
|
const number = fromPrimitive("number", T.number);
|
|
|
|
const boolean = fromPrimitive("boolean", T.boolean);
|
|
|
|
const json = simpleValue((source, ok, err) =>
|
|
R.match(
|
|
J.parse(source),
|
|
(value) => ok(value),
|
|
(cause) => err(jsonError(cause))
|
|
)
|
|
);
|
|
|
|
const expression = (type) =>
|
|
simpleValue((source, ok, err) =>
|
|
R.match(
|
|
E.compile(type, source),
|
|
(evaluator) =>
|
|
ok((env, errorFormatter) => E.run(evaluator, env, errorFormatter)),
|
|
(cause) => err(expressionError(cause))
|
|
)
|
|
);
|
|
|
|
const numberExpr = expression(E.NUMBER);
|
|
|
|
const booleanExpr = expression(E.BOOLEAN);
|
|
|
|
const anyExpr = expression(E.ANY);
|
|
|
|
const custom = (fn) =>
|
|
simpleValue((source, ok, err) =>
|
|
fn(
|
|
source,
|
|
(value) => ok(value),
|
|
(cause) => err(customError(cause))
|
|
)
|
|
);
|
|
|
|
const array = (parser) =>
|
|
structuredValue(
|
|
"array",
|
|
(value) => Array.isArray(value),
|
|
({ source, path }) => arrayRec([], 0, parser, source, path)
|
|
);
|
|
|
|
const arrayRec = (acc, index, parser, source, path) =>
|
|
index < source.length
|
|
? match(
|
|
parser(mkState(source[index], L.cons(index, path))),
|
|
({ value }) =>
|
|
arrayRec(linearPush(acc, value), index + 1, parser, source, path),
|
|
F.identity
|
|
)
|
|
: ok(acc);
|
|
|
|
const struct = (...parsers) =>
|
|
structuredValue(
|
|
"struct",
|
|
(value) =>
|
|
typeof value === "object" && value !== null && !Array.isArray(value),
|
|
object(...parsers)
|
|
);
|
|
|
|
const like = (archetype) =>
|
|
typeof archetype === "function"
|
|
? archetype
|
|
: typeof archetype === "object" && archetype !== null
|
|
? Array.isArray(archetype)
|
|
? likeArrayOf(archetype)
|
|
: likeStructOf(archetype)
|
|
: F.panic(`invalid archetype item: ${D.fmt(archetype)}`);
|
|
|
|
const likeArrayOf = (archetype) =>
|
|
archetype.length === 1
|
|
? array(like(archetype[0]))
|
|
: F.panic(`archetype array must be a singleton: ${D.fmt(archetype)}`);
|
|
|
|
const likeStructOf = (archetype) =>
|
|
struct(
|
|
...Object.entries(archetype).map(([key, value]) =>
|
|
entry(key, like(value))
|
|
)
|
|
);
|
|
|
|
const likeAll = (archetypes) =>
|
|
object(
|
|
...Object.entries(archetypes).map(([key, value]) =>
|
|
entry(key, like(value))
|
|
)
|
|
);
|
|
|
|
const make = (parser) => (source) =>
|
|
match(
|
|
parser(mkState(source, L.nil)),
|
|
({ value }) => R.ok(value),
|
|
({ deadEnd }) => R.err(processDeadEnd(deadEnd))
|
|
);
|
|
|
|
const processDeadEnd = ({ source, path, cause }) => ({
|
|
source,
|
|
path: L.toArray(path).reverse(),
|
|
cause,
|
|
});
|
|
|
|
const parse = (source, parser, errorFormatter = defaultErrorFormatter) =>
|
|
R.expect(make(parser)(source), errorFormatter);
|
|
|
|
const parseAll = (source, archetypes, errorFormatter) =>
|
|
parse(source, likeAll(archetypes), errorFormatter);
|
|
|
|
const begin = (source, errorFormatter) => (key, parser) =>
|
|
parse(source, keyed(key, parser), errorFormatter);
|
|
|
|
const defaultErrorFormatter = (deadEnd) => {
|
|
const { source, path, cause: error } = deadEnd;
|
|
const errorPath = path.length !== 0 ? formatErrorPath(path) : undefined;
|
|
const errorDetail = formatErrorDetail(source, error);
|
|
return errorPath !== undefined
|
|
? `${errorPath}: ${errorDetail}`
|
|
: errorDetail;
|
|
};
|
|
|
|
const formatErrorPath = (path) =>
|
|
path
|
|
.map((key, i) => {
|
|
switch (typeof key) {
|
|
case "string":
|
|
return i === 0 ? key : `.${key}`;
|
|
case "number":
|
|
return `[${key}]`;
|
|
default:
|
|
return F.unreachable();
|
|
}
|
|
})
|
|
.join("");
|
|
|
|
const formatErrorDetail = (source, error) => {
|
|
const dots = (s) => D.ellipsis(s, 32);
|
|
switch (error?.type) {
|
|
case "key":
|
|
return `'${error.key}' property not found`;
|
|
case "encoding":
|
|
return `not correctly encoded`;
|
|
case "structure":
|
|
return `${error.expected} expected, but "${dots(source)}" found`;
|
|
case "notation":
|
|
return `(${error.format}) ${T.defaultErrorFormatter(error.cause)}`;
|
|
case "json":
|
|
return `(json) ${J.defaultErrorFormatter(error.cause)}`;
|
|
case "expression":
|
|
return `(expression) ${E.defaultCompileErrorFormatter(error.cause)}`;
|
|
case "custom":
|
|
return formatCustomErrorDetail(error);
|
|
case "validation":
|
|
return formatValidationErrorDetail(error);
|
|
default:
|
|
return `unknown error: ${D.fmt(error)}`;
|
|
}
|
|
};
|
|
|
|
const formatCustomErrorDetail = (error) => {
|
|
const { cause } = error;
|
|
return typeof cause === "string" ? cause : D.fmt(cause);
|
|
};
|
|
|
|
const formatValidationErrorDetail = (error) => {
|
|
const { cause } = error;
|
|
return typeof cause === "string" ? cause : D.fmt(cause);
|
|
};
|
|
|
|
return {
|
|
succeed,
|
|
fail,
|
|
backtrackable,
|
|
andThen,
|
|
map,
|
|
allOf,
|
|
oneOf,
|
|
optional,
|
|
validate,
|
|
keyed,
|
|
entry,
|
|
as,
|
|
object,
|
|
of,
|
|
from,
|
|
enum: enum_,
|
|
string,
|
|
natural,
|
|
integer,
|
|
number,
|
|
boolean,
|
|
json,
|
|
numberExpr,
|
|
booleanExpr,
|
|
anyExpr,
|
|
custom,
|
|
array,
|
|
struct,
|
|
like,
|
|
likeAll,
|
|
make,
|
|
parse,
|
|
parseAll,
|
|
begin,
|
|
defaultErrorFormatter,
|
|
};
|
|
})();
|
|
|
|
const M = (() => {
|
|
const kindError = (expected) => ({ type: "kind", expected });
|
|
const notationError = (cause) => ({ type: "notation", cause });
|
|
const customError = (cause) => ({ type: "custom", cause });
|
|
const validationError = (cause) => ({ type: "validation", cause });
|
|
|
|
const ok = (value) => ({ ok: true, value });
|
|
const err = (deadEnd) => ({ ok: false, deadEnd });
|
|
const nil = () => undefined;
|
|
const mkState = (source, tag) => ({ source, tag });
|
|
const mkDeadEnd = (source, tag, cause) => ({ source, tag, cause });
|
|
const match2 = (result, onOk, onErr) =>
|
|
result === undefined ? result : result.ok ? onOk(result) : onErr(result);
|
|
const match3 = (result, onOk, onErr, onNil) =>
|
|
result === undefined
|
|
? onNil(result)
|
|
: result.ok
|
|
? onOk(result)
|
|
: onErr(result);
|
|
|
|
const succeed = (value) => (_state) => ok(value);
|
|
|
|
const fail =
|
|
(cause) =>
|
|
({ source, tag }) =>
|
|
err(mkDeadEnd(source, tag, cause));
|
|
|
|
const miss = (_state) => nil();
|
|
|
|
const andThen = (parser, fn) => (state) =>
|
|
match2(parser(state), ({ value }) => fn(value)(state), F.identity);
|
|
|
|
const map = (parser, fn) => (state) =>
|
|
match2(parser(state), ({ value }) => ok(fn(value)), F.identity);
|
|
|
|
const map2 = (first, second, fn) => (state) =>
|
|
match3(
|
|
first(state),
|
|
({ value: v1 }) =>
|
|
match2(second(state), ({ value: v2 }) => ok(fn(v1, v2)), F.identity),
|
|
F.identity,
|
|
() => match2(second(state), () => nil(), F.identity)
|
|
);
|
|
|
|
const allOf = (...parsers) =>
|
|
map(
|
|
parsers.reduce(
|
|
(acc, parser) => map2(acc, parser, (xs, x) => L.cons(x, xs)),
|
|
succeed(L.nil)
|
|
),
|
|
(list) => L.toArray(list).reverse()
|
|
);
|
|
|
|
const or = (first, second) => (state) =>
|
|
match3(first(state), F.identity, F.identity, () => second(state));
|
|
|
|
const oneOf = (...parsers) =>
|
|
parsers.reduceRight((acc, parser) => or(parser, acc));
|
|
|
|
const optional = (parser, value) => or(parser, succeed(value));
|
|
|
|
const validate = (parser, validator) => (state) =>
|
|
match2(
|
|
parser(state),
|
|
({ value: v1 }) =>
|
|
validator(
|
|
v1,
|
|
(v2) => ok(v2),
|
|
(cause) =>
|
|
err(mkDeadEnd(state.source, state.tag, validationError(cause)))
|
|
),
|
|
F.identity
|
|
);
|
|
|
|
const tagged = (name, parser) =>
|
|
typeof name === "string" && name !== ""
|
|
? ({ source }) =>
|
|
parser(
|
|
mkState(
|
|
Object.hasOwn(source, name) ? source[name] : undefined,
|
|
name
|
|
)
|
|
)
|
|
: F.panic(`invalid metadata name: ${D.fmt(name)}`);
|
|
|
|
const flag = (name) =>
|
|
tagged(name, (state) => {
|
|
const { source, tag } = state;
|
|
switch (source) {
|
|
case undefined:
|
|
return ok(false);
|
|
case true:
|
|
return ok(true);
|
|
default:
|
|
return err(mkDeadEnd(source, tag, kindError("flag")));
|
|
}
|
|
});
|
|
|
|
const attr = (name, parser) =>
|
|
tagged(name, (state) => {
|
|
const { source, tag } = state;
|
|
switch (source) {
|
|
case undefined:
|
|
return nil();
|
|
case true:
|
|
return err(mkDeadEnd(source, tag, kindError("attr")));
|
|
default:
|
|
return parser(state);
|
|
}
|
|
});
|
|
|
|
const hybrid = (name, parser, value) =>
|
|
tagged(name, (state) => {
|
|
const { source } = state;
|
|
switch (source) {
|
|
case undefined:
|
|
return nil();
|
|
case true:
|
|
return ok(value);
|
|
default:
|
|
return parser(state);
|
|
}
|
|
});
|
|
|
|
const unescape =
|
|
(parser, fn) =>
|
|
({ source, tag }) =>
|
|
parser(mkState(fn(source), tag));
|
|
|
|
const of = (parser) => {
|
|
const builtParser = T.make(parser);
|
|
return ({ source, tag }) =>
|
|
R.match(
|
|
builtParser(source),
|
|
(value) => ok(value),
|
|
(cause) => err(mkDeadEnd(source, tag, notationError(cause)))
|
|
);
|
|
};
|
|
|
|
const from = (parser) => of(T.just(parser));
|
|
|
|
const enum_ = (type, variants) => from(T.enum(type, variants));
|
|
|
|
const string = from(T.rest);
|
|
|
|
const natural = from(T.natural);
|
|
|
|
const integer = from(T.integer);
|
|
|
|
const number = from(T.number);
|
|
|
|
const boolean = from(T.boolean);
|
|
|
|
const json = from(T.json);
|
|
|
|
const numberExpr = from(T.numberExpr);
|
|
|
|
const booleanExpr = from(T.booleanExpr);
|
|
|
|
const anyExpr = from(T.anyExpr);
|
|
|
|
const custom =
|
|
(fn) =>
|
|
({ source, tag }) =>
|
|
fn(
|
|
source,
|
|
(value) => ok(value),
|
|
(cause) => err(mkDeadEnd(source, tag, customError(cause)))
|
|
);
|
|
|
|
const like = (archetype) =>
|
|
typeof archetype === "function"
|
|
? archetype
|
|
: typeof archetype === "object" && archetype !== null
|
|
? Array.isArray(archetype)
|
|
? likeArrayOf(archetype)
|
|
: likeStructOf(archetype)
|
|
: F.panic(`invalid archetype item: ${D.fmt(archetype)}`);
|
|
|
|
const likeArrayOf = (archetype) =>
|
|
allOf(...archetype.map((value) => like(value)));
|
|
|
|
const likeStructOf = (archetype) =>
|
|
map(
|
|
allOf(
|
|
...Object.entries(archetype).map(([key, value]) =>
|
|
map(like(value), (value) => [key, value])
|
|
)
|
|
),
|
|
(entries) => Object.fromEntries(entries)
|
|
);
|
|
|
|
const make = (parser) => (source) =>
|
|
match3(
|
|
parser(mkState(source, undefined)),
|
|
({ value }) => R.ok(value),
|
|
({ deadEnd }) => R.err(deadEnd),
|
|
() => R.ok(undefined)
|
|
);
|
|
|
|
const parse = (source, parser, errorFormatter = defaultErrorFormatter) =>
|
|
R.expect(make(parser)(source), errorFormatter);
|
|
|
|
const meta = (parser, errorFormatter = defaultErrorFormatter) => {
|
|
const builtParser = make(parser);
|
|
const memoizedParser = U.memo({ storage: "weakmap" }, (meta) =>
|
|
builtParser(meta)
|
|
);
|
|
return (data) =>
|
|
R.expect(memoizedParser(data.meta), (deadEnd) =>
|
|
errorFormatter({ ...deadEnd, data })
|
|
);
|
|
};
|
|
|
|
const defaultErrorFormatter = (deadEnd) => {
|
|
const { tag, cause: error, data } = deadEnd;
|
|
const prefix = formatErrorPrefix(data, tag);
|
|
const detail = formatErrorDetail(error);
|
|
return prefix !== undefined ? `${prefix}: ${detail}` : detail;
|
|
};
|
|
|
|
const formatErrorPrefix = (data, tag) => {
|
|
const segments = [
|
|
data !== undefined ? formatErrorOwnerId(data) : undefined,
|
|
data !== undefined ? formatErrorOwnerName(data) : undefined,
|
|
tag !== undefined ? formatErrorTag(tag) : undefined,
|
|
].filter((segment) => typeof segment === "string");
|
|
return segments.length !== 0 ? segments.join(" ") : undefined;
|
|
};
|
|
|
|
const formatErrorOwnerId = (data) => {
|
|
if (Object.hasOwn(data, "id")) {
|
|
const { id } = data;
|
|
return Number.isInteger(id) && id >= 0 && id < 10000
|
|
? `#${id.toString(10)}`
|
|
: undefined;
|
|
} else {
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
const formatErrorOwnerName = (data) => {
|
|
if (Object.hasOwn(data, "name")) return data.name;
|
|
if (Object.hasOwn(data, "displayName")) return data.displayName;
|
|
return undefined;
|
|
};
|
|
|
|
const formatErrorTag = (tag) => `<${tag}>`;
|
|
|
|
const formatErrorDetail = (error) => {
|
|
switch (error?.type) {
|
|
case "kind":
|
|
return formatKindErrorDetail(error);
|
|
case "notation":
|
|
return T.defaultErrorFormatter(error.cause);
|
|
case "custom":
|
|
return formatCustomErrorDetail(error);
|
|
case "validation":
|
|
return formatValidationErrorDetail(error);
|
|
default:
|
|
return `unknown error: ${D.fmt(error)}`;
|
|
}
|
|
};
|
|
|
|
const formatKindErrorDetail = (error) => {
|
|
const { expected } = error;
|
|
switch (expected) {
|
|
case "flag":
|
|
return `no value required for this metadata`;
|
|
case "attr":
|
|
return `value required for this metadata`;
|
|
default:
|
|
return `unknown metadata kind: ${expected}`;
|
|
}
|
|
};
|
|
|
|
const formatCustomErrorDetail = (error) => {
|
|
const { cause } = error;
|
|
return typeof cause === "string" ? cause : D.fmt(cause);
|
|
};
|
|
|
|
const formatValidationErrorDetail = (error) => {
|
|
const { cause } = error;
|
|
return typeof cause === "string" ? cause : D.fmt(cause);
|
|
};
|
|
|
|
return {
|
|
succeed,
|
|
fail,
|
|
miss,
|
|
andThen,
|
|
map,
|
|
allOf,
|
|
oneOf,
|
|
optional,
|
|
validate,
|
|
flag,
|
|
attr,
|
|
hybrid,
|
|
unescape,
|
|
of,
|
|
from,
|
|
enum: enum_,
|
|
string,
|
|
natural,
|
|
integer,
|
|
number,
|
|
boolean,
|
|
json,
|
|
numberExpr,
|
|
booleanExpr,
|
|
anyExpr,
|
|
custom,
|
|
like,
|
|
make,
|
|
parse,
|
|
meta,
|
|
defaultErrorFormatter,
|
|
};
|
|
})();
|
|
|
|
const Z = (() => {
|
|
const base = (target, method) => {
|
|
if (Object.hasOwn(target, method)) {
|
|
return target[method];
|
|
} else {
|
|
const proto = Object.getPrototypeOf(target);
|
|
return function () {
|
|
return proto[method].apply(this, arguments);
|
|
};
|
|
}
|
|
};
|
|
|
|
const redef = (() => {
|
|
const PROXY_HANDLER = {
|
|
get(target, key, receiver) {
|
|
const desc = Reflect.getOwnPropertyDescriptor(target, key);
|
|
if (desc === undefined) {
|
|
const parent = Reflect.getPrototypeOf(target);
|
|
if (parent === null) {
|
|
return undefined;
|
|
} else {
|
|
const value = Reflect.get(parent, key, receiver);
|
|
if (typeof value === "function") {
|
|
return function () {
|
|
return parent[key].apply(this, arguments);
|
|
};
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
} else {
|
|
return Reflect.get(target, key, receiver);
|
|
}
|
|
},
|
|
};
|
|
|
|
return (target, definition) => {
|
|
const { proxy, revoke } = Proxy.revocable(target, PROXY_HANDLER);
|
|
return F.defer(
|
|
() => revoke(),
|
|
() => definition(proxy)
|
|
);
|
|
};
|
|
})();
|
|
|
|
const extProp = (options = {}) => {
|
|
const { default: defaultValue, storage = "weakmap" } = options;
|
|
|
|
switch (storage) {
|
|
case "weakmap":
|
|
return extPropWithWeakMap(defaultValue);
|
|
case "map":
|
|
return extPropWithMap(defaultValue);
|
|
default:
|
|
return F.panic(`unsupported extension property storage: ${storage}`);
|
|
}
|
|
};
|
|
|
|
const extPropWithWeakMap = (defaultValue) => {
|
|
const store = new WeakMap();
|
|
const get = (key) => (store.has(key) ? store.get(key) : defaultValue);
|
|
const set = (key, value) => void store.set(key, value);
|
|
const reset = (key) => void store.delete(key);
|
|
return { get, set, reset };
|
|
};
|
|
|
|
const extPropWithMap = (defaultValue) => {
|
|
const store = new Map();
|
|
const get = (key) => (store.has(key) ? store.get(key) : defaultValue);
|
|
const set = (key, value) => void store.set(key, value);
|
|
const reset = (key) => void store.delete(key);
|
|
const clear = () => store.clear();
|
|
return { get, set, reset, clear };
|
|
};
|
|
|
|
const extend = (target, name, prop) => {
|
|
const { get, set } = prop;
|
|
Object.defineProperty(target, name, {
|
|
get() {
|
|
return get(this);
|
|
},
|
|
set(value) {
|
|
set(this, value);
|
|
},
|
|
configurable: true,
|
|
});
|
|
};
|
|
|
|
const swapper = (key) => (target, value, block) => {
|
|
const current = target[key];
|
|
return F.with(
|
|
() => {
|
|
target[key] = value;
|
|
},
|
|
() => {
|
|
target[key] = current;
|
|
},
|
|
block
|
|
);
|
|
};
|
|
|
|
const context = (defaultValue) => {
|
|
const { get: getContext, set: setContext } =
|
|
extPropWithWeakMap(defaultValue);
|
|
const enter = (target, value, block) => {
|
|
const current = getContext(target);
|
|
return F.with(
|
|
() => setContext(target, value),
|
|
() => setContext(target, current),
|
|
block
|
|
);
|
|
};
|
|
const value = (target) => getContext(target);
|
|
return { enter, value };
|
|
};
|
|
|
|
const view = (() => {
|
|
const PROXY_HANDLER = {
|
|
getPrototypeOf({ mask }) {
|
|
return Reflect.getPrototypeOf(mask);
|
|
},
|
|
|
|
setPrototypeOf(_target, _proto) {
|
|
return false;
|
|
},
|
|
|
|
isExtensible(_target) {
|
|
return true;
|
|
},
|
|
|
|
preventExtensions(_target) {
|
|
return false;
|
|
},
|
|
|
|
getOwnPropertyDescriptor({ source, mask, cache }, key) {
|
|
const desc = Reflect.getOwnPropertyDescriptor(mask, key);
|
|
return desc !== undefined
|
|
? {
|
|
value: viewProperty(
|
|
source[key],
|
|
desc.value,
|
|
source,
|
|
cache,
|
|
key
|
|
),
|
|
writable: true,
|
|
enumerable: true,
|
|
configurable: true,
|
|
}
|
|
: undefined;
|
|
},
|
|
|
|
defineProperty(_target, _key, _descriptor) {
|
|
return false;
|
|
},
|
|
|
|
has({ mask }, key) {
|
|
return Reflect.has(mask, key);
|
|
},
|
|
|
|
get(target, key, receiver) {
|
|
const desc = this.getOwnPropertyDescriptor(target, key);
|
|
if (desc === undefined) {
|
|
const parent = this.getPrototypeOf(target);
|
|
return parent !== null
|
|
? Reflect.get(parent, key, receiver)
|
|
: undefined;
|
|
} else {
|
|
return desc.value;
|
|
}
|
|
},
|
|
|
|
set(_target, _key, _value, _receiver) {
|
|
return false;
|
|
},
|
|
|
|
deleteProperty(_target, _key) {
|
|
return false;
|
|
},
|
|
|
|
ownKeys({ mask }) {
|
|
return Reflect.ownKeys(mask);
|
|
},
|
|
};
|
|
|
|
const view = (mask) => {
|
|
const normalized = normalizeMask(mask);
|
|
return (source) => makeProxy(source, normalized);
|
|
};
|
|
|
|
const makeProxy = (source, mask) =>
|
|
new Proxy({ source, mask, cache: {} }, PROXY_HANDLER);
|
|
|
|
const viewProperty = (source, mask, receiver, cache, key) =>
|
|
mask === true
|
|
? typeof source !== "function"
|
|
? source
|
|
: childFunctionCache(receiver, cache, key)(source)
|
|
: childProxyCache(mask, cache, key)(source);
|
|
|
|
const childFunctionCache = (receiver, cache, key) =>
|
|
Object.hasOwn(cache, key)
|
|
? cache[key]
|
|
: (cache[key] = U.memo({ storage: "weakmap" }, (source) =>
|
|
source.bind(receiver)
|
|
));
|
|
|
|
const childProxyCache = (mask, cache, key) =>
|
|
Object.hasOwn(cache, key)
|
|
? cache[key]
|
|
: (cache[key] = U.memo({ storage: "weakmap" }, (source) =>
|
|
makeProxy(source, mask)
|
|
));
|
|
|
|
const normalizeMask = (mask) => {
|
|
if (typeof mask === "string") {
|
|
return { [mask]: true };
|
|
} else if (typeof mask === "object" && mask !== null) {
|
|
return Array.isArray(mask)
|
|
? normalizeArrayMask(mask)
|
|
: normalizeObjectMask(mask);
|
|
} else {
|
|
return F.panic(`invalid view mask item: ${D.fmt(mask)}`);
|
|
}
|
|
};
|
|
|
|
const normalizeArrayMask = (mask) =>
|
|
Object.assign({}, ...mask.map((mask) => normalizeMask(mask)));
|
|
|
|
const normalizeObjectMask = (mask) =>
|
|
Object.fromEntries(
|
|
Object.entries(mask).map(([key, value]) => [
|
|
key,
|
|
value !== true ? normalizeMask(value) : true,
|
|
])
|
|
);
|
|
|
|
return view;
|
|
})();
|
|
|
|
return { base, redef, extProp, extend, swapper, context, view };
|
|
})();
|
|
|
|
globalThis.Fs = Object.assign(globalThis.Fs ?? {}, {
|
|
v0_4: { F, O, R, L, D, U, G, J, E, T, P, M, Z },
|
|
});
|
|
}
|