UNPKG

@mojir/lits

Version:

Lits is a pure functional programming language implemented in TypeScript

1,471 lines (1,447 loc) 1.28 MB
#!/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