@mojir/lits
Version:
Lits is a pure functional programming language implemented in TypeScript
1,471 lines (1,447 loc) • 1.28 MB
JavaScript
#!/usr/bin/env node
'use strict';
var fs = require('node:fs');
var path = require('node:path');
var readline = require('node:readline');
var os = require('node:os');
var process$1 = require('node:process');
var version = "2.6.5";
function getCodeMarker(sourceCodeInfo) {
if (!sourceCodeInfo.position || !sourceCodeInfo.code)
return '';
const leftPadding = sourceCodeInfo.position.column - 1;
const rightPadding = sourceCodeInfo.code.length - leftPadding - 1;
return `${' '.repeat(Math.max(leftPadding, 0))}^${' '.repeat(Math.max(rightPadding, 0))}`;
}
function getLitsErrorMessage(message, sourceCodeInfo) {
if (!sourceCodeInfo) {
return message;
}
const location = `${sourceCodeInfo.position.line}:${sourceCodeInfo.position.column}`;
const filePathLine = sourceCodeInfo.filePath
? `\n${sourceCodeInfo.filePath}:${location}`
: `\nLocation ${location}`;
const codeLine = `\n${sourceCodeInfo.code}`;
const codeMarker = `\n${getCodeMarker(sourceCodeInfo)}`;
return `${message}${filePathLine}${codeLine}${codeMarker}`;
}
class RecurSignal extends Error {
params;
constructor(params) {
super(`recur, params: ${params}`);
Object.setPrototypeOf(this, RecurSignal.prototype);
this.name = 'RecurSignal';
this.params = params;
}
}
class LitsError extends Error {
sourceCodeInfo;
shortMessage;
constructor(err, sourceCodeInfo) {
const message = err instanceof Error
? err.message
: `${err}`;
super(getLitsErrorMessage(message, sourceCodeInfo));
this.shortMessage = message;
this.sourceCodeInfo = sourceCodeInfo;
Object.setPrototypeOf(this, LitsError.prototype);
this.name = 'LitsError';
}
getCodeMarker() {
return this.sourceCodeInfo && getCodeMarker(this.sourceCodeInfo);
}
}
class UserDefinedError extends LitsError {
userMessage;
constructor(message, sourceCodeInfo) {
super(message, sourceCodeInfo);
this.userMessage = message;
Object.setPrototypeOf(this, UserDefinedError.prototype);
this.name = 'UserDefinedError';
}
}
class AssertionError extends LitsError {
constructor(message, sourceCodeInfo) {
super(message, sourceCodeInfo);
Object.setPrototypeOf(this, AssertionError.prototype);
this.name = 'AssertionError';
}
}
class UndefinedSymbolError extends LitsError {
symbol;
constructor(symbolName, sourceCodeInfo) {
const message = `Undefined symbol '${symbolName}'.`;
super(message, sourceCodeInfo);
this.symbol = symbolName;
Object.setPrototypeOf(this, UndefinedSymbolError.prototype);
this.name = 'UndefinedSymbolError';
}
}
const specialExpressionTypes = {
'??': 0,
'&&': 1,
'||': 2,
'array': 3,
'cond': 4,
'defined?': 5,
'block': 6,
'doseq': 7,
'0_lambda': 8,
'for': 9,
'if': 10,
'let': 11,
'loop': 12,
'object': 13,
'recur': 14,
'match': 15,
'throw': 16,
'try': 17,
'unless': 18,
'import': 19,
};
const NodeTypes = {
Number: 1,
String: 2,
NormalExpression: 3,
SpecialExpression: 4,
UserDefinedSymbol: 5,
NormalBuiltinSymbol: 6,
SpecialBuiltinSymbol: 7,
ReservedSymbol: 8,
Binding: 9,
Spread: 10,
};
const NodeTypesSet = new Set(Object.values(NodeTypes));
function getNodeTypeName(type) {
return Object.keys(NodeTypes).find(key => NodeTypes[key] === type);
}
// TODO, is this needed?
function isNodeType(type) {
return typeof type === 'number' && NodeTypesSet.has(type);
}
const functionTypes = [
'UserDefined',
'Partial',
'Comp',
'Constantly',
'Juxt',
'Complement',
'EveryPred',
'SomePred',
'Fnull',
'Builtin',
'SpecialBuiltin',
'NativeJsFunction',
'Module',
];
const functionTypeSet = new Set(functionTypes);
function isFunctionType(type) {
return typeof type === 'string' && functionTypeSet.has(type);
}
const FUNCTION_SYMBOL = '^^fn^^';
const REGEXP_SYMBOL = '^^re^^';
function isLitsFunction$1(func) {
if (func === null || typeof func !== 'object')
return false;
return FUNCTION_SYMBOL in func && 'functionType' in func && isFunctionType(func.functionType);
}
function isNode(value) {
if (!Array.isArray(value) || value.length < 2)
return false;
return isNodeType(value[0]);
}
function valueToString(value) {
if (isLitsFunction$1(value))
// eslint-disable-next-line ts/no-unsafe-member-access
return `<function ${value.name || '\u03BB'}>`;
if (isNode(value))
return `${getNodeTypeName(value[0])}-node`;
if (value === null)
return 'null';
if (typeof value === 'object' && value instanceof RegExp)
return `${value}`;
if (typeof value === 'object' && value instanceof Error)
return value.toString();
return JSON.stringify(value);
}
function getSourceCodeInfo(anyValue, sourceCodeInfo) {
// eslint-disable-next-line ts/no-unsafe-return, ts/no-unsafe-member-access
return anyValue?.sourceCodeInfo ?? sourceCodeInfo;
}
function getAssertionError(typeName, value, sourceCodeInfo) {
return new LitsError(`Expected ${typeName}, got ${valueToString(value)}.`, getSourceCodeInfo(value, sourceCodeInfo));
}
function isSymbolNode(node) {
const nodeType = node[0];
return NodeTypes.UserDefinedSymbol === nodeType
|| NodeTypes.NormalBuiltinSymbol === nodeType
|| NodeTypes.SpecialBuiltinSymbol === nodeType;
}
function assertSymbolNode(node, sourceCodeInfo) {
if (!isSymbolNode(node))
throw getAssertionError('SymbolNode', node, sourceCodeInfo);
}
function isUserDefinedSymbolNode(node) {
return NodeTypes.UserDefinedSymbol === node[0];
}
function asUserDefinedSymbolNode(node, sourceCodeInfo) {
assertUserDefinedSymbolNode(node, sourceCodeInfo);
return node;
}
function assertUserDefinedSymbolNode(node, sourceCodeInfo) {
if (!isUserDefinedSymbolNode(node))
throw getAssertionError('UserDefinedSymbolNode', node, sourceCodeInfo);
}
function isNormalBuiltinSymbolNode(node) {
return NodeTypes.NormalBuiltinSymbol === node[0];
}
function isSpecialBuiltinSymbolNode(node) {
return NodeTypes.SpecialBuiltinSymbol === node[0];
}
function isNormalExpressionNode(node) {
return node[0] === NodeTypes.NormalExpression;
}
function isNormalExpressionNodeWithName(node) {
if (!isNormalExpressionNode(node)) {
return false;
}
return isSymbolNode(node[1][0]);
}
function isSpreadNode(node) {
return node[0] === NodeTypes.Spread;
}
const getUndefinedSymbols = (ast, contextStack, builtin, evaluateNode) => {
const nodes = Array.isArray(ast)
? ast
: [[NodeTypes.SpecialExpression, [specialExpressionTypes.block, ast.body]]];
const unresolvedSymbols = new Set();
for (const subNode of nodes) {
findUnresolvedSymbolsInNode(subNode, contextStack, builtin, evaluateNode)
?.forEach(symbol => unresolvedSymbols.add(symbol));
}
return unresolvedSymbols;
};
function findUnresolvedSymbolsInNode(node, contextStack, builtin, evaluateNode) {
const nodeType = node[0];
switch (nodeType) {
case NodeTypes.UserDefinedSymbol: {
const symbolNode = node;
const lookUpResult = contextStack.lookUp(symbolNode);
if (lookUpResult === null)
return new Set([symbolNode[1]]);
return null;
}
case NodeTypes.NormalBuiltinSymbol:
case NodeTypes.SpecialBuiltinSymbol:
case NodeTypes.String:
case NodeTypes.Number:
case NodeTypes.ReservedSymbol:
case NodeTypes.Binding:
return null;
case NodeTypes.NormalExpression: {
const normalExpressionNode = node;
const unresolvedSymbols = new Set();
if (isNormalExpressionNodeWithName(normalExpressionNode)) {
const [, [symbolNode]] = normalExpressionNode;
if (isUserDefinedSymbolNode(symbolNode)) {
const lookUpResult = contextStack.lookUp(symbolNode);
if (lookUpResult === null)
unresolvedSymbols.add(symbolNode[1]);
}
}
else {
const [, [expressionNode]] = normalExpressionNode;
findUnresolvedSymbolsInNode(expressionNode, contextStack, builtin, evaluateNode)?.forEach(symbol => unresolvedSymbols.add(symbol));
}
for (const subNode of normalExpressionNode[1][1]) {
findUnresolvedSymbolsInNode(subNode, contextStack, builtin, evaluateNode)?.forEach(symbol => unresolvedSymbols.add(symbol));
}
return unresolvedSymbols;
}
case NodeTypes.SpecialExpression: {
const specialExpressionNode = node;
const specialExpressionType = specialExpressionNode[1][0];
const specialExpression = builtin.specialExpressions[specialExpressionType];
const castedGetUndefinedSymbols = specialExpression.getUndefinedSymbols;
return castedGetUndefinedSymbols(specialExpressionNode, contextStack, {
getUndefinedSymbols,
builtin,
evaluateNode,
});
}
case NodeTypes.Spread:
return findUnresolvedSymbolsInNode(node[1], contextStack, builtin, evaluateNode);
/* v8 ignore next 2 */
default:
throw new LitsError(`Unhandled node type: ${nodeType}`, node[2]);
}
}
function isNonUndefined(value) {
return value !== undefined;
}
function asNonUndefined(value, sourceCodeInfo) {
assertNonUndefined(value, sourceCodeInfo);
return value;
}
function assertNonUndefined(value, sourceCodeInfo) {
if (!isNonUndefined(value))
throw new LitsError('Unexpected undefined', getSourceCodeInfo(value, sourceCodeInfo));
}
function isUnknownRecord(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function assertUnknownRecord(value, sourceCodeInfo) {
if (!isUnknownRecord(value)) {
throw new LitsError(`Expected ${'UnknownRecord'}, got ${valueToString(value)}.`, getSourceCodeInfo(value, sourceCodeInfo));
}
}
function isLitsFunction(value) {
if (value === null || typeof value !== 'object')
return false;
return !!value[FUNCTION_SYMBOL];
}
function isAny(value) {
// TODO weak test
return value !== undefined;
}
function asAny(value, sourceCodeInfo) {
assertAny(value, sourceCodeInfo);
return value;
}
function assertAny(value, sourceCodeInfo) {
if (!isAny(value))
throw getAssertionError('not undefined', value, sourceCodeInfo);
}
function isSeq(value) {
return Array.isArray(value) || typeof value === 'string';
}
function asSeq(value, sourceCodeInfo) {
assertSeq(value, sourceCodeInfo);
return value;
}
function assertSeq(value, sourceCodeInfo) {
if (!isSeq(value))
throw getAssertionError('string or array', value, sourceCodeInfo);
}
function isObj(value) {
return !(value === null
|| typeof value !== 'object'
|| Array.isArray(value)
|| value instanceof RegExp
|| isLitsFunction(value)
|| isRegularExpression(value));
}
function assertObj(value, sourceCodeInfo) {
if (!isObj(value))
throw getAssertionError('object', value, sourceCodeInfo);
}
function isColl(value) {
return isSeq(value) || isObj(value);
}
function asColl(value, sourceCodeInfo) {
assertColl(value, sourceCodeInfo);
return value;
}
function assertColl(value, sourceCodeInfo) {
if (!isColl(value))
throw getAssertionError('string, array or object', value, sourceCodeInfo);
}
function isRegularExpression(regexp) {
if (regexp === null || typeof regexp !== 'object')
return false;
return !!regexp[REGEXP_SYMBOL];
}
function assertRegularExpression(value, sourceCodeInfo) {
if (!isRegularExpression(value))
throw getAssertionError('RegularExpression', value, sourceCodeInfo);
}
function isStringOrRegularExpression(value) {
return isRegularExpression(value) || typeof value === 'string';
}
function assertStringOrRegularExpression(value, sourceCodeInfo) {
if (!isStringOrRegularExpression(value))
throw getAssertionError('string or RegularExpression', value, sourceCodeInfo);
}
function isFunctionLike(value) {
if (typeof value === 'number')
return true;
if (isColl(value))
return true;
if (isLitsFunction(value))
return true;
return false;
}
function asFunctionLike(value, sourceCodeInfo) {
assertFunctionLike(value, sourceCodeInfo);
return value;
}
function assertFunctionLike(value, sourceCodeInfo) {
if (!isFunctionLike(value))
throw getAssertionError('FunctionLike', value, sourceCodeInfo);
}
function isString(value, options = {}) {
if (typeof value !== 'string')
return false;
if (options.nonEmpty && value.length === 0)
return false;
if (options.char && value.length !== 1)
return false;
return true;
}
function assertString(value, sourceCodeInfo, options = {}) {
if (!isString(value, options)) {
throw getAssertionError(`${options.nonEmpty ? 'non empty string' : options.char ? 'character' : 'string'}`, value, sourceCodeInfo);
}
}
function asString(value, sourceCodeInfo, options = {}) {
assertString(value, sourceCodeInfo, options);
return value;
}
function isStringOrNumber(value) {
return typeof value === 'string' || typeof value === 'number';
}
function asStringOrNumber(value, sourceCodeInfo) {
assertStringOrNumber(value, sourceCodeInfo);
return value;
}
function assertStringOrNumber(value, sourceCodeInfo) {
if (!isStringOrNumber(value))
throw getAssertionError('string or number', value, sourceCodeInfo);
}
const assertionNormalExpression = {
assert: {
evaluate: (params, sourceCodeInfo) => {
const value = params[0];
const message = params.length === 2 ? params[1] : `${value}`;
assertString(message, sourceCodeInfo);
if (!value)
throw new AssertionError(message, sourceCodeInfo);
return asAny(value, sourceCodeInfo);
},
arity: { min: 1, max: 2 },
docs: {
category: 'assertion',
description: 'If $value is falsy it throws `AssertionError` with $message. If no $message is provided, message is set to $value.',
returns: {
type: 'any',
},
args: {
value: {
type: 'any',
},
message: {
type: 'string',
},
},
variants: [
{
argumentNames: [
'value',
],
},
{
argumentNames: [
'value',
'message',
],
},
],
examples: [
'try assert(0, "Expected a positive value") catch (e) e.message end',
],
seeAlso: ['assertion.assert-truthy', 'assertion.assert-true'],
hideOperatorForm: true,
},
},
};
function getRangeString(options) {
const hasUpperAndLowerBound = (typeof options.gt === 'number' || typeof options.gte === 'number')
&& (typeof options.lt === 'number' || typeof options.lte === 'number');
if (hasUpperAndLowerBound) {
return `${typeof options.gt === 'number' ? `${options.gt} < n ` : `${options.gte} <= n `}${typeof options.lt === 'number' ? `< ${options.lt}` : `<= ${options.lte}`}`;
}
else if (typeof options.gt === 'number' || typeof options.gte === 'number') {
return `${typeof options.gt === 'number' ? `n > ${options.gt}` : `n >= ${options.gte}`}`;
}
else if (typeof options.lt === 'number' || typeof options.lte === 'number') {
return `${typeof options.lt === 'number' ? `n < ${options.lt}` : `n <= ${options.lte}`}`;
}
else {
return '';
}
}
function getSignString(options) {
return options.positive
? 'positive'
: options.negative
? 'negative'
: options.nonNegative
? 'non negative'
: options.nonPositive
? 'non positive'
: options.nonZero
? 'non zero'
: '';
}
function getNumberTypeName(options) {
if (options.zero)
return 'zero';
const sign = getSignString(options);
const numberType = options.integer ? 'integer' : 'number';
const finite = options.finite ? 'finite' : '';
const range = getRangeString(options);
return [sign, finite, numberType, range].filter(x => !!x).join(' ');
}
function isNumber(value, options = {}) {
if (typeof value !== 'number')
return false;
if (Number.isNaN(value))
return false;
if (options.integer && !Number.isInteger(value))
return false;
if (options.finite && !Number.isFinite(value))
return false;
if (options.zero && value !== 0)
return false;
if (options.nonZero && value === 0)
return false;
if (options.positive && value <= 0)
return false;
if (options.negative && value >= 0)
return false;
if (options.nonPositive && value > 0)
return false;
if (options.nonNegative && value < 0)
return false;
if (typeof options.gt === 'number' && value <= options.gt)
return false;
if (typeof options.gte === 'number' && value < options.gte)
return false;
if (typeof options.lt === 'number' && value >= options.lt)
return false;
if (typeof options.lte === 'number' && value > options.lte)
return false;
return true;
}
function assertNumber(value, sourceCodeInfo, options = {}) {
if (!isNumber(value, options)) {
throw new LitsError(`Expected ${getNumberTypeName(options)}, got ${valueToString(value)}.`, getSourceCodeInfo(value, sourceCodeInfo));
}
}
function asNumber(value, sourceCodeInfo, options = {}) {
assertNumber(value, sourceCodeInfo, options);
return value;
}
function arityAcceptsMin(arity, nbrOfParams) {
const { min } = arity;
if (typeof min === 'number' && nbrOfParams < min) {
return false;
}
return true;
}
function getCommonArityFromFunctions(params) {
return params.reduce((acc, param) => {
if (acc === null) {
return null;
}
const arity = (typeof param === 'number' || isColl(param)) ? toFixedArity(1) : param.arity;
const { min: aMin, max: aMax } = arity;
const { min: bMin, max: bMax } = acc;
const min = typeof aMin === 'number' && typeof bMin === 'number'
? Math.max(aMin, bMin)
: typeof aMin === 'number' ? aMin : typeof bMin === 'number' ? bMin : undefined;
const max = typeof aMax === 'number' && typeof bMax === 'number'
? Math.min(aMax, bMax)
: typeof aMax === 'number' ? aMax : typeof bMax === 'number' ? bMax : undefined;
if (typeof min === 'number' && typeof max === 'number' && min > max) {
return null;
}
return { min, max };
}, {});
}
function getArityFromFunction(param) {
return (typeof param === 'number' || isColl(param)) ? toFixedArity(1) : param.arity;
}
function assertNumberOfParams(arity, length, sourceCodeInfo) {
const { min, max } = arity;
if (typeof min === 'number' && length < min) {
throw new LitsError(`Wrong number of arguments, expected at least ${min}, got ${valueToString(length)}.`, sourceCodeInfo);
}
if (typeof max === 'number' && length > max) {
throw new LitsError(`Wrong number of arguments, expected at most ${max}, got ${valueToString(length)}.`, sourceCodeInfo);
}
}
function canBeOperator(count) {
if (typeof count.max === 'number' && count.max < 2) {
return false;
}
if (typeof count.min === 'number' && count.min > 2) {
return false;
}
return true;
}
function toFixedArity(arity) {
return { min: arity, max: arity };
}
function getOperatorArgs$1(a, b) {
return { a: { type: a }, b: { type: b } };
}
const bitwiseNormalExpression = {
'<<': {
evaluate: ([num, count], sourceCodeInfo) => {
assertNumber(num, sourceCodeInfo, { integer: true });
assertNumber(count, sourceCodeInfo, { integer: true, nonNegative: true });
return num << count;
},
arity: toFixedArity(2),
docs: {
category: 'bitwise',
returns: { type: 'integer' },
args: { ...getOperatorArgs$1('integer', 'integer') },
variants: [{ argumentNames: ['a', 'b'] }],
description: 'Shifts $a arithmetically left by $b bit positions.',
seeAlso: ['>>', '>>>'],
examples: [
'1 << 10',
'<<(1, 10)',
'<<(-4, 2)',
],
},
},
'>>': {
evaluate: ([num, count], sourceCodeInfo) => {
assertNumber(num, sourceCodeInfo, { integer: true });
assertNumber(count, sourceCodeInfo, { integer: true, nonNegative: true });
return num >> count;
},
arity: toFixedArity(2),
docs: {
category: 'bitwise',
returns: { type: 'integer' },
args: { ...getOperatorArgs$1('integer', 'integer') },
variants: [{ argumentNames: ['a', 'b'] }],
description: 'Shifts $a arithmetically right by $b bit positions.',
seeAlso: ['<<', '>>>'],
examples: [
'2048 >> 10',
'>>(2048, 10)',
'>>>(-16, 2)',
'>>(4, 10)',
],
},
},
'>>>': {
evaluate: ([num, count], sourceCodeInfo) => {
assertNumber(num, sourceCodeInfo, { integer: true });
assertNumber(count, sourceCodeInfo, { integer: true, nonNegative: true });
return num >>> count;
},
arity: toFixedArity(2),
docs: {
category: 'bitwise',
returns: { type: 'integer' },
args: { ...getOperatorArgs$1('integer', 'integer') },
variants: [{ argumentNames: ['a', 'b'] }],
description: 'Shifts $a arithmetically right by $b bit positions without sign extension.',
seeAlso: ['<<', '>>'],
examples: [
'-16 >>> 2',
'>>>(2048, 10)',
'>>>(-16, 2)',
'>>>(4, 10)',
'>>>(-1, 10)',
],
},
},
'&': {
evaluate: ([first, ...rest], sourceCodeInfo) => {
assertNumber(first, sourceCodeInfo, { integer: true });
return rest.reduce((result, value) => {
assertNumber(value, sourceCodeInfo, { integer: true });
return result & value;
}, first);
},
arity: { min: 2 },
docs: {
category: 'bitwise',
returns: { type: 'integer' },
args: {
...getOperatorArgs$1('integer', 'integer'),
c: { type: 'integer', rest: true },
},
variants: [
{ argumentNames: ['a', 'b'] },
{ argumentNames: ['a', 'b', 'c'] },
],
description: 'Returns bitwise `and` of all arguments.',
seeAlso: ['|', 'xor', 'bitwise.bit-not', 'bitwise.bit-and-not'],
examples: [
'0b0011 & 0b0110',
'&(0b0011, 0b0110)',
'&(0b0011, 0b0110, 0b1001)',
],
},
},
'|': {
evaluate: ([first, ...rest], sourceCodeInfo) => {
assertNumber(first, sourceCodeInfo, { integer: true });
return rest.reduce((result, value) => {
assertNumber(value, sourceCodeInfo, { integer: true });
return result | value;
}, first);
},
arity: { min: 2 },
docs: {
category: 'bitwise',
returns: { type: 'integer' },
args: {
...getOperatorArgs$1('integer', 'integer'),
c: { type: 'integer', rest: true },
},
variants: [
{ argumentNames: ['a', 'b'] },
{ argumentNames: ['a', 'b', 'c'] },
],
description: 'Returns bitwise `or` of all arguments.',
seeAlso: ['&', 'xor', 'bitwise.bit-not', 'bitwise.bit-and-not'],
examples: [
'0b0011 | 0b0110',
'|(0b0011, 0b0110)',
'|(0b1000, 0b0100, 0b0010)',
],
},
},
'xor': {
evaluate: ([first, ...rest], sourceCodeInfo) => {
assertNumber(first, sourceCodeInfo, { integer: true });
return rest.reduce((result, value) => {
assertNumber(value, sourceCodeInfo, { integer: true });
return result ^ value;
}, first);
},
arity: { min: 2 },
docs: {
category: 'bitwise',
returns: { type: 'integer' },
args: {
...getOperatorArgs$1('integer', 'integer'),
c: { type: 'integer', rest: true },
},
variants: [
{ argumentNames: ['a', 'b'] },
{ argumentNames: ['a', 'b', 'c'] },
],
description: 'Returns bitwise `xor` of all arguments.',
seeAlso: ['&', '|', 'bitwise.bit-not', 'bitwise.bit-and-not'],
examples: [
'0b0011 xor 0b0110',
'xor(0b0011, 0b0110)',
'xor(0b11110000, 0b00111100, 0b10101010)',
],
},
},
};
function collHasKey(coll, key) {
if (!isColl(coll))
return false;
if (typeof coll === 'string' || Array.isArray(coll)) {
if (!isNumber(key, { integer: true }))
return false;
return key >= 0 && key < coll.length;
}
return !!Object.getOwnPropertyDescriptor(coll, key);
}
function compare(a, b, sourceCodeInfo) {
assertStringOrNumber(a, sourceCodeInfo);
assertStringOrNumber(b, sourceCodeInfo);
if (typeof a === 'string' && typeof b === 'string') {
return a < b ? -1 : a > b ? 1 : 0;
}
if (typeof a === 'number' && typeof b === 'number') {
return Math.sign((a) - (b));
}
throw new LitsError(`Cannot compare values of different types: ${typeof a} and ${typeof b}`, sourceCodeInfo);
}
function deepEqual(a, b, sourceCodeInfo) {
if (a === b)
return true;
if (typeof a === 'number' && typeof b === 'number')
return approxEqual(a, b);
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length)
return false;
for (let i = 0; i < a.length; i += 1) {
if (!deepEqual(asAny(a[i], sourceCodeInfo), asAny(b[i], sourceCodeInfo), sourceCodeInfo))
return false;
}
return true;
}
if (isRegularExpression(a) && isRegularExpression(b))
return a.s === b.s && a.f === b.f;
if (isUnknownRecord(a) && isUnknownRecord(b)) {
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length)
return false;
for (let i = 0; i < aKeys.length; i += 1) {
const key = asString(aKeys[i], sourceCodeInfo);
if (!deepEqual(a[key], b[key], sourceCodeInfo))
return false;
}
return true;
}
return false;
}
function toNonNegativeInteger(num) {
return Math.max(0, Math.ceil(num));
}
function toAny(value) {
return (value ?? null);
}
function clone(value) {
if (isObj(value)) {
return Object.entries(value).reduce((result, entry) => {
const [key, val] = entry;
result[key] = clone(val);
return result;
}, {});
}
if (Array.isArray(value))
// eslint-disable-next-line ts/no-unsafe-return
return value.map(item => clone(item));
return value;
}
function cloneColl(value) {
return clone(value);
}
function joinSets(...results) {
const result = new Set();
for (const symbols of results)
symbols.forEach(symbol => result.add(symbol));
return result;
}
function addToSet(target, source) {
source.forEach(symbol => target.add(symbol));
}
const EPSILON = 1e-10;
function approxEqual(a, b, epsilon = EPSILON) {
if (a === b) {
return true;
}
const diff = Math.abs(a - b);
if (a === 0 || b === 0 || diff < epsilon) {
// Use absolute error for values near zero
return diff < epsilon;
}
const absA = Math.abs(a);
const absB = Math.abs(b);
// Use relative error for larger values
return diff / (absA + absB) < epsilon;
}
function approxZero(value) {
return Math.abs(value) < EPSILON;
}
function smartTrim(str, minIndent = 0) {
const lines = str.split('\n');
while (lines[0]?.match(/^\s*$/)) {
lines.shift(); // Remove leading empty lines
}
while (lines[lines.length - 1]?.match(/^\s*$/)) {
lines.pop(); // Remove trailing empty lines
}
const indent = lines.reduce((acc, line) => {
if (line.match(/^\s*$/))
return acc; // Skip empty lines
const lineIndent = line.match(/^\s*/)[0].length;
return Math.min(acc, lineIndent);
}, Infinity);
return lines.map(line => ' '.repeat(minIndent) + line.slice(indent)).join('\n').trimEnd();
}
// isArray not needed, use Array.isArary
function asArray(value, sourceCodeInfo) {
assertArray(value, sourceCodeInfo);
return value;
}
function assertArray(value, sourceCodeInfo) {
if (!Array.isArray(value))
throw getAssertionError('array', value, sourceCodeInfo);
}
function isStringArray(value) {
return Array.isArray(value) && value.every(v => typeof v === 'string');
}
function assertStringArray(value, sourceCodeInfo) {
if (!isStringArray(value))
throw getAssertionError('array of strings', value, sourceCodeInfo);
}
function isCharArray(value) {
return Array.isArray(value) && value.every(v => typeof v === 'string' && v.length === 1);
}
function assertCharArray(value, sourceCodeInfo) {
if (!isCharArray(value))
throw getAssertionError('array of strings', value, sourceCodeInfo);
}
/**
* MaybePromise utilities for transparent async support.
*
* The sync path stays zero-overhead — when no async JS functions are involved,
* everything runs synchronously with only `instanceof Promise` checks as overhead.
* When an async value is detected, the evaluation chain switches to Promise-based
* execution for the remainder.
*/
/**
* Chain a value that might be a Promise. If the value is sync, calls fn synchronously.
* If it's a Promise, chains with .then().
*/
function chain(value, fn) {
if (value instanceof Promise) {
return value.then(fn);
}
return fn(value);
}
/**
* Like Array.map but handles MaybePromise callbacks sequentially.
* In the sync case, runs as a simple loop. Switches to async only when needed.
*/
function mapSequential(arr, fn) {
const results = [];
for (let i = 0; i < arr.length; i++) {
const result = fn(arr[i], i);
if (result instanceof Promise) {
return chainRemainingMap(result, results, arr, fn, i);
}
results.push(result);
}
return results;
}
async function chainRemainingMap(currentPromise, results, arr, fn, startIndex) {
results.push(await currentPromise);
for (let i = startIndex + 1; i < arr.length; i++) {
results.push(await fn(arr[i], i));
}
return results;
}
/**
* Like Array.reduce but handles MaybePromise callbacks sequentially.
* In the sync case, runs as a simple loop. Switches to async only when needed.
*/
function reduceSequential(arr, fn, initial) {
let result = initial;
for (let i = 0; i < arr.length; i++) {
const next = fn(result, arr[i], i);
if (next instanceof Promise) {
return chainRemainingReduce(next, arr, fn, i);
}
result = next;
}
return result;
}
async function chainRemainingReduce(currentPromise, arr, fn, startIndex) {
let result = await currentPromise;
for (let i = startIndex + 1; i < arr.length; i++) {
result = await fn(result, arr[i], i);
}
return result;
}
/**
* Like Array.forEach but handles MaybePromise callbacks sequentially.
* In the sync case, runs as a simple loop. Switches to async only when needed.
*/
function forEachSequential(arr, fn) {
for (let i = 0; i < arr.length; i++) {
const result = fn(arr[i], i);
if (result instanceof Promise) {
return chainRemainingForEach(result, arr, fn, i);
}
}
}
async function chainRemainingForEach(currentPromise, arr, fn, startIndex) {
await currentPromise;
for (let i = startIndex + 1; i < arr.length; i++) {
await fn(arr[i], i);
}
}
/**
* Try/catch that handles MaybePromise values correctly.
* If tryFn returns a Promise, catches both sync throws and Promise rejections.
*/
function tryCatch(tryFn, catchFn) {
try {
const result = tryFn();
if (result instanceof Promise) {
return result.catch(catchFn);
}
return result;
}
catch (error) {
return catchFn(error);
}
}
/**
* Like Array.some but handles MaybePromise callbacks sequentially.
* Returns the first truthy MaybePromise result, or false.
*/
function someSequential(arr, fn) {
for (let i = 0; i < arr.length; i++) {
const result = fn(arr[i], i);
if (result instanceof Promise) {
return chainRemainingSome(result, arr, fn, i);
}
if (result)
return true;
}
return false;
}
async function chainRemainingSome(currentPromise, arr, fn, startIndex) {
if (await currentPromise)
return true;
for (let i = startIndex + 1; i < arr.length; i++) {
if (await fn(arr[i], i))
return true;
}
return false;
}
/**
* Like Array.every but handles MaybePromise callbacks sequentially.
* Returns false as soon as a falsy result is found.
*/
function everySequential(arr, fn) {
for (let i = 0; i < arr.length; i++) {
const result = fn(arr[i], i);
if (result instanceof Promise) {
return chainRemainingEvery(result, arr, fn, i);
}
if (!result)
return false;
}
return true;
}
async function chainRemainingEvery(currentPromise, arr, fn, startIndex) {
if (!await currentPromise)
return false;
for (let i = startIndex + 1; i < arr.length; i++) {
if (!await fn(arr[i], i))
return false;
}
return true;
}
/**
* Like Array.filter but handles MaybePromise callbacks sequentially.
* Returns the filtered array.
*/
function filterSequential(arr, fn) {
const results = [];
for (let i = 0; i < arr.length; i++) {
const result = fn(arr[i], i);
if (result instanceof Promise) {
return chainRemainingFilter(result, results, arr, fn, i);
}
if (result)
results.push(arr[i]);
}
return results;
}
async function chainRemainingFilter(currentPromise, results, arr, fn, startIndex) {
if (await currentPromise)
results.push(arr[startIndex]);
for (let i = startIndex + 1; i < arr.length; i++) {
if (await fn(arr[i], i))
results.push(arr[i]);
}
return results;
}
/**
* Like Array.findIndex but handles MaybePromise callbacks sequentially.
* Returns -1 if no element matches.
*/
function findIndexSequential(arr, fn) {
for (let i = 0; i < arr.length; i++) {
const result = fn(arr[i], i);
if (result instanceof Promise) {
return chainRemainingFindIndex(result, arr, fn, i);
}
if (result)
return i;
}
return -1;
}
async function chainRemainingFindIndex(currentPromise, arr, fn, startIndex) {
if (await currentPromise)
return startIndex;
for (let i = startIndex + 1; i < arr.length; i++) {
if (await fn(arr[i], i))
return i;
}
return -1;
}
function mapObjects({ colls, contextStack, executeFunction, fn, sourceCodeInfo, }) {
assertObj(colls[0], sourceCodeInfo);
const keys = Object.keys(colls[0]);
const params = {};
colls.forEach((obj) => {
assertObj(obj, sourceCodeInfo);
const objKeys = Object.keys(obj);
if (objKeys.length !== keys.length) {
throw new LitsError(`All objects must have the same keys. Expected: ${keys.join(', ')}. Found: ${objKeys.join(', ')}`, sourceCodeInfo);
}
if (!objKeys.every(key => keys.includes(key))) {
throw new LitsError(`All objects must have the same keys. Expected: ${keys.join(', ')}. Found: ${objKeys.join(', ')}`, sourceCodeInfo);
}
Object.entries(obj).forEach(([key, value]) => {
if (!params[key])
params[key] = [];
params[key].push(value);
});
});
const initialObj = {};
return reduceSequential(keys, (result, key) => {
return chain(executeFunction(fn, params[key], contextStack, sourceCodeInfo), (value) => {
result[key] = value;
return result;
});
}, initialObj);
}
function get$1(coll, key) {
if (isObj(coll)) {
if (typeof key === 'string' && collHasKey(coll, key))
return toAny(coll[key]);
}
else {
if (isNumber(key, { nonNegative: true, integer: true }) && key >= 0 && key < coll.length)
return toAny(coll[key]);
}
return undefined;
}
function assoc$1(coll, key, value, sourceCodeInfo) {
assertColl(coll, sourceCodeInfo);
assertStringOrNumber(key, sourceCodeInfo);
if (Array.isArray(coll) || typeof coll === 'string') {
assertNumber(key, sourceCodeInfo, { integer: true });
assertNumber(key, sourceCodeInfo, { gte: 0 });
assertNumber(key, sourceCodeInfo, { lte: coll.length });
if (typeof coll === 'string') {
assertString(value, sourceCodeInfo, { char: true });
return `${coll.slice(0, key)}${value}${coll.slice(key + 1)}`;
}
const copy = [...coll];
copy[key] = value;
return copy;
}
assertString(key, sourceCodeInfo);
const copy = { ...coll };
copy[key] = value;
return copy;
}
const collectionNormalExpression = {
'filter': {
evaluate: ([coll, fn], sourceCodeInfo, contextStack, { executeFunction }) => {
assertColl(coll, sourceCodeInfo);
assertFunctionLike(fn, sourceCodeInfo);
if (Array.isArray(coll)) {
return reduceSequential(coll, (result, elem) => {
return chain(executeFunction(fn, [elem], contextStack, sourceCodeInfo), (keep) => {
if (keep)
result.push(elem);
return result;
});
}, []);
}
if (isString(coll)) {
const chars = coll.split('');
return chain(reduceSequential(chars, (result, elem) => {
return chain(executeFunction(fn, [elem], contextStack, sourceCodeInfo), (keep) => {
if (keep)
result.push(elem);
return result;
});
}, []), filtered => filtered.join(''));
}
const entries = Object.entries(coll);
const initialObj = {};
return reduceSequential(entries, (result, [key, value]) => {
return chain(executeFunction(fn, [value], contextStack, sourceCodeInfo), (keep) => {
if (keep)
result[key] = value;
return result;
});
}, initialObj);
},
arity: toFixedArity(2),
docs: {
category: 'collection',
returns: { type: 'collection' },
args: {
a: { type: 'collection' },
b: { type: 'function' },
coll: { type: 'collection' },
fun: { type: 'function' },
},
variants: [{ argumentNames: ['coll', 'fun'] }],
description: 'Creates a new collection with all elements that pass the test implemented by $fun.',
seeAlso: ['collection.filteri', 'map', 'sequence.remove'],
examples: [
`
filter(
["Albert", "Mojir", 160, [1, 2]],
string?
)`,
`
filter(
[5, 10, 15, 20],
-> $ > 10
)`,
`
filter(
{ a: 1, b: 2 },
odd?
)`,
],
},
},
'map': {
evaluate: (params, sourceCodeInfo, contextStack, { executeFunction }) => {
const fn = asFunctionLike(params.at(-1), sourceCodeInfo);
if (isObj(params[0])) {
return mapObjects({
colls: params.slice(0, -1),
fn,
sourceCodeInfo,
contextStack,
executeFunction,
});
}
const seqs = params.slice(0, -1);
assertSeq(seqs[0], sourceCodeInfo);
const isStr = typeof seqs[0] === 'string';
let len = seqs[0].length;
seqs.slice(1).forEach((seq) => {
if (isStr) {
assertString(seq, sourceCodeInfo);
}
else {
assertArray(seq, sourceCodeInfo);
}
len = Math.min(len, seq.length);
});
const paramArray = [];
for (let i = 0; i < len; i++) {
paramArray.push(seqs.map(seq => seq[i]));
}
const mapped = mapSequential(paramArray, p => executeFunction(fn, p, contextStack, sourceCodeInfo));
if (!isStr) {
return mapped;
}
return chain(mapped, (resolvedMapped) => {
resolvedMapped.forEach(char => assertString(char, sourceCodeInfo));
return resolvedMapped.join('');
});
},
arity: { min: 2 },
docs: {
category: 'collection',
returns: { type: 'collection' },
args: {
a: { type: 'collection' },
b: { type: 'function' },
colls: { type: 'collection', rest: true, description: 'At least one.' },
fun: { type: 'function' },
},
variants: [{ argumentNames: ['colls', 'fun'] }],
description: 'Creates a new collection populated with the results of calling $fun on every element in $colls.',
seeAlso: ['collection.mapi', 'filter', 'reduce', 'mapcat', 'grid.cell-map', 'grid.cell-mapi'],
examples: [
'[1, 2, 3] map -',
'[1, 2, 3] map -> -($)',
'map(["Albert", "Mojir", 42], str)',
'map([1, 2, 3], inc)',
'map([1, 2, 3], [1, 10, 100], *)',
'map({ a: 1, b: 2 }, inc)',
'map({ a: 1, b: 2 }, { a: 10, b: 20 }, +)',
],
},
},
'reduce': {
evaluate: ([coll, fn, initial], sourceCodeInfo, contextStack, { executeFunction }) => {
assertColl(coll, sourceCodeInfo);
assertFunctionLike(fn, sourceCodeInfo);
assertAny(initial, sourceCodeInfo);
if (typeof coll === 'string') {
assertString(initial, sourceCodeInfo);
if (coll.length === 0)
return initial;
return reduceSequential(coll.split(''), (result, elem) => {
return executeFunction(fn, [result, elem], contextStack, sourceCodeInfo);
}, initial);
}
else if (Array.isArray(coll)) {
if (coll.length === 0)
return initial;
return reduceSequential(coll, (result, elem) => {
return executeFunction(fn, [result, elem], contextStack, sourceCodeInfo);
}, initial);
}
else {
if (Object.keys(coll).length === 0)
return initial;
return reduceSequential(Object.entries(coll), (result, [, elem]) => {
return executeFunction(fn, [result, elem], contextStack, sourceCodeInfo);
}, initial);
}
},
arity: toFixedArity(3),
docs: {
category: 'collection',
returns: { type: 'any' },
args: {
fun: { type: 'function' },
coll: { type: 'collection' },
initial: { type: 'any' },
},
variants: [{ argumentNames: ['coll', 'fun', 'initial'] }],
description: 'Runs $fun function on each element of the $coll, passing in the return value from the calculation on the preceding element. The final result of running the reducer across all elements of the $coll is a single value.',
seeAlso: ['collection.reduce-right', 'collection.reducei', 'collection.reductions', 'map', 'grid.cell-reduce', 'grid.cell-reducei'],
examples: [
'reduce([1, 2, 3], +, 0)',
'reduce([], +, 0)',
'reduce({ a: 1, b: 2 }, +, 0)',
`
reduce(
[1, 2, 3, 4, 5, 6, 7, 8, 9],
(result, value) -> result + (even?(value) ? value : 0),
0)`,
],
},
},
'get': {
evaluate: (params, sourceCodeInfo) => {
const [coll, key] = params;
const defaultValue = toAny(params[2]);
assertStringOrNumber(key, sourceCodeInfo);
if (coll === null)
return defaultValue;
assertColl(coll, sourceCodeInfo);
const result = get$1(coll, key);
return result === undefined ? defaultValue : result;
},
arity: { min: 2, max: 3 },
docs: {
category: 'collection',
returns: { type: 'any' },
args: {
'a': { type: 'collection' },
'b': { type: ['string', 'integer'] },
'not-found': { type: 'any', description: 'Default value to return if $b is not found.' },
},
variants: [
{ argumentNames: ['a', 'b'] },
{ argumentNames: ['a', 'b', 'not-found'] },
],
description: 'Returns value in $a mapped at $b.',
seeAlso: ['collection.get-in', 'contains?', 'find', 'nth'],
examples: [
'[1, 2, 3] get 1',
'{ a: 1 } get "a"',
'"Albert" get "3"',
`
get(
[1, 2, 3],
1, // Optional comma after last argument
)`,
`
get(
[],
1
)`,
`
get(
[],
1,
"default"
)`,
`
get(
{ a: 1 },
"a"
)`,
`
get(
{ a: 1 },
"b"
)`,
`
get(
{ a: 1 },
"b",
"default"
)`,
`
get(
null,
"a"
)`,
`
get(
null,
"b",
"default"
)`,
],
},
},
'count': {
evaluate: ([coll], sourceCodeInfo) => {
if (coll === null)
return 0;
if (typeof coll === 'string')
return coll.length;
assertColl(coll, sourceCodeInfo);
if (Array.isArray(coll))
return coll.length;
return Object.keys(coll).length;
},
arity: toFixedArity(1),
docs: {
category: 'collection',
returns: { type: 'number' },
args: {
coll: { type: ['collection', 'null'] },
},
variants: [{ argumentNames: ['coll'] }],
description: 'Returns number of elements in $coll.',
seeAlso: ['empty?'],
examples: [
'count([1, 2, 3])',
'count([])',
'count({ a: 1 })',
'count("")',
'count("Albert")',
'count(null)',
],
},
},
'contains?': {
evaluate: ([coll, key], sourceCodeInfo) => {
if (coll === null)
return false;
assertColl(coll, sourceCodeInfo);
if (isString(coll)) {
assertString(key, sourceCodeInfo);
return coll.includes(key);
}
if (isSeq(coll)) {
assertAny(key, sourceCodeInfo);
return !!coll.find(elem => deepEqual(asAny(elem), key, sourceCodeInfo));
}
assertString(key, sourceCodeInfo);
return key in coll;
},
arity: toFixedArity(2),
docs: {
category: 'collection',
returns: { type: 'boolean' },
args: {
a: { type: ['collection', 'null'] },
b: { type: ['string', 'integer'] },
},
variants: [{ argumentNames: ['a', 'b'] }],
description: 'Returns `true` if $a contains $b, otherwise returns `false`. For strings, it checks if substring is included.',
seeAlso: ['get', 'find', 'index-o