UNPKG

simplex-lang

Version:

SimplEx - simple expression language

503 lines (501 loc) 17.6 kB
/* eslint-disable @typescript-eslint/ban-ts-comment */ // eslint-disable-next-line n/no-missing-import import { parse } from '../parser/index.js'; import { CompileError, ExpressionError, UnexpectedTypeError } from './errors.js'; import assert from 'node:assert'; import { castToBoolean } from './tools/cast.js'; import { ensureFunction, ensureRelationalComparable, ensureNumber } from './tools/ensure.js'; import { isSimpleValue } from './tools/guards.js'; import { castToString, typeOf } from './tools/index.js'; var hasOwn = Object.hasOwn; var ERROR_STACK_REGEX = /<anonymous>:(?<row>\d+):(?<col>\d+)/g; var TOPIC_TOKEN = '%'; const defaultContextHelpers = { castToBoolean, ensureFunction, getIdentifierValue: (identifierName, globals, data) => { // TODO Should test on parse time? if (identifierName === TOPIC_TOKEN) { throw new Error(`Topic reference "${TOPIC_TOKEN}" is unbound; it must be inside a pipe body.`); } if (identifierName === 'undefined') return undefined; if (globals != null && Object.hasOwn(globals, identifierName)) { return globals[identifierName]; } if (data != null && Object.hasOwn(data, identifierName)) { return data[identifierName]; } throw new Error(`Unknown identifier - ${identifierName}`); }, getProperty(obj, key) { if (obj == null) return obj; if (typeof obj !== 'object') { throw new UnexpectedTypeError(['object'], obj); } if (isSimpleValue(key) === false) { throw new UnexpectedTypeError(['simple type object key'], key); } if (hasOwn(obj, key)) { // @ts-expect-error Type cannot be used as an index type return obj[key]; } return undefined; }, callFunction(fn, args) { return (args === null ? ensureFunction(fn)() : ensureFunction(fn).apply(null, args)); }, pipe(head, tail) { var result = head; for (const it of tail) { if (it.opt && result == null) return result; result = it.next(result); } return result; } }; export const defaultUnaryOperators = { '+': val => ensureNumber(val), '-': val => -ensureNumber(val), 'not': val => !castToBoolean(val), 'typeof': val => typeof val }; export const defaultBinaryOperators = { '!=': (a, b) => a !== b, '==': (a, b) => a === b, // TIPS give the opportunity to get a base js error '*': (a, b) => { // @ts-expect-error // eslint-disable-next-line @typescript-eslint/no-unsafe-return return ensureNumber(a) * ensureNumber(b); }, '+': (a, b) => { // @ts-expect-error // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/restrict-plus-operands return ensureNumber(a) + ensureNumber(b); }, '-': (a, b) => { // @ts-expect-error // eslint-disable-next-line @typescript-eslint/no-unsafe-return return ensureNumber(a) - ensureNumber(b); }, '/': (a, b) => { // @ts-expect-error // eslint-disable-next-line @typescript-eslint/no-unsafe-return return ensureNumber(a) / ensureNumber(b); }, 'mod': (a, b) => { // @ts-expect-error // eslint-disable-next-line @typescript-eslint/no-unsafe-return return ensureNumber(a) % ensureNumber(b); }, '^': (a, b) => { // @ts-expect-error // eslint-disable-next-line @typescript-eslint/no-unsafe-return return ensureNumber(a) ** ensureNumber(b); }, '&': (a, b) => castToString(a) + castToString(b), '<': (a, b) => ensureRelationalComparable(a) < ensureRelationalComparable(b), '<=': (a, b) => ensureRelationalComparable(a) <= ensureRelationalComparable(b), '>': (a, b) => ensureRelationalComparable(a) > ensureRelationalComparable(b), '>=': (a, b) => ensureRelationalComparable(a) >= ensureRelationalComparable(b), 'in': (a, b) => { if (isSimpleValue(a) && b != null && typeof b === 'object') { return Object.hasOwn(b, a); } else { throw new TypeError(`Cannot use "in" operator to search for ${typeOf(a)} key in ${typeOf(b)}`); } } }; const logicalAndOperatorFn = (a, b) => castToBoolean(a()) && castToBoolean(b()); const logicalOrOperatorFn = (a, b) => castToBoolean(a()) || castToBoolean(b()); export const defaultLogicalOperators = { // TODO Use castToBoolean from compile options? 'and': logicalAndOperatorFn, '&&': logicalAndOperatorFn, 'or': logicalOrOperatorFn, '||': logicalOrOperatorFn }; const codePart = (codePart, ownerNode) => ({ code: codePart, offsets: [{ len: codePart.length, location: ownerNode.location }] }); const combineVisitResults = (parts) => { return parts.reduce((res, it) => { return { code: res.code + it.code, offsets: res.offsets.concat(it.offsets) }; }); }; const visitors = { Literal: node => { const parts = [codePart(JSON.stringify(node.value), node)]; return parts; }, Identifier: node => { const parts = [ codePart(`get(scope,${JSON.stringify(node.name)})`, node) ]; return parts; }, UnaryExpression: (node, visit) => { const parts = [ codePart(`uop["${node.operator}"](`, node), ...visit(node.argument), codePart(')', node) ]; return parts; }, BinaryExpression: (node, visit) => { const parts = [ codePart(`bop["${node.operator}"](`, node), ...visit(node.left), codePart(',', node), ...visit(node.right), codePart(')', node) ]; return parts; }, LogicalExpression: (node, visit) => { const parts = [ codePart(`lop["${node.operator}"](()=>(`, node), ...visit(node.left), codePart('),()=>(', node), ...visit(node.right), codePart('))', node) ]; return parts; }, ConditionalExpression: (node, visit) => { const parts = [ codePart('(bool(', node), ...visit(node.test), codePart(')?', node), ...visit(node.consequent), codePart(':', node), ...(node.alternate !== null ? visit(node.alternate) : [codePart('undefined', node)]), codePart(')', node) ]; return parts; }, ObjectExpression: (node, visit) => { const innerObj = node.properties .map((p) => { if (p.key.type === 'Identifier') { return [codePart(p.key.name, p), visit(p.value)]; } // else if (p.key.type === 'Literal') { // TODO look for ECMA spec return [codePart(JSON.stringify(p.key.value), p), visit(p.value)]; } // else { // TODO Restrict on parse step // TODO Error with locations throw new TypeError(`Incorrect object key type ${p.key.type}`); } }) .flatMap(([k, v]) => { return [k, codePart(':', node), ...v, codePart(',', node)]; }); // remove last comma if (innerObj.length > 1) { innerObj.pop(); } const parts = [ codePart('{', node), ...innerObj, codePart('}', node) ]; return parts; }, ArrayExpression: (node, visit) => { const innerArrParts = node.elements.flatMap(el => { return el === null ? [codePart(',', node)] : [...visit(el), codePart(',', node)]; }); // remove last comma if (innerArrParts.length > 1) { innerArrParts.pop(); } const parts = [ codePart('[', node), ...innerArrParts, codePart(']', node) ]; return parts; }, MemberExpression: (node, visit) => { const { computed, object, property } = node; // TODO Pass computed to prop? const parts = [ codePart('prop(', node), ...visit(object), codePart(',', node), ...(computed ? visit(property) : [codePart(JSON.stringify(property.name), property)]), codePart(')', node) ]; return parts; }, CallExpression: (node, visit) => { if (node.arguments.length > 0) { const innerArgs = node.arguments.flatMap((arg, index) => [ ...(arg.type === 'CurryPlaceholder' ? [codePart(`a${index}`, arg)] : visit(arg)), codePart(',', node) ]); const curriedArgs = node.arguments.flatMap((arg, index) => arg.type === 'CurryPlaceholder' ? [`a${index}`] : []); // remove last comma innerArgs?.pop(); // call({{callee}},[{{arguments}}]) let parts = [ codePart('call(', node), ...visit(node.callee), codePart(',[', node), ...innerArgs, codePart('])', node) ]; if (curriedArgs.length > 0) { parts = [ codePart(`(scope=>(${curriedArgs.join()})=>`, node), ...parts, codePart(')(scope)', node) ]; } return parts; } // else { const parts = [ codePart('call(', node), ...visit(node.callee), codePart(',null)', node) ]; return parts; } }, NullishCoalescingExpression: (node, visit) => { const parts = [ codePart('(', node), ...visit(node.left), codePart('??', node), ...visit(node.right), codePart(')', node) ]; return parts; }, PipeSequence: (node, visit) => { const headCode = visit(node.head); const tailsCodeArrInner = node.tail.flatMap(t => { const opt = t.operator === '|?'; const tailParts = [ codePart(`{opt:${opt},next:(scope=>topic=>{scope=[["%"],[topic],scope];return `, t.expression), ...visit(t.expression), codePart(`})(scope)}`, t.expression), codePart(`,`, t.expression) ]; return tailParts; }); // remove last comma tailsCodeArrInner.pop(); const parts = [ codePart('pipe(', node), ...headCode, codePart(',[', node), ...tailsCodeArrInner, codePart('])', node) ]; return parts; }, TopicReference: node => { const parts = [codePart(`get(scope,"${TOPIC_TOKEN}")`, node)]; return parts; }, LambdaExpression: (node, visit) => { // Lambda with parameters if (node.params.length > 0) { const paramsNames = node.params.map(p => p.name); const fnParams = Array.from({ length: paramsNames.length }, (_, index) => `p${index}`); const fnParamsList = fnParams.join(); const fnParamsNamesList = paramsNames.map(p => JSON.stringify(p)).join(); // TODO Is "...args" more performant? // (params => function (p0, p1) { // var scope = [params, [p0, p1], scope] // return {{code}} // })(["a", "b"]) const parts = [ codePart(`((scope,params)=>function(${fnParamsList}){scope=[params,[${fnParamsList}],scope];return `, node), ...visit(node.expression), codePart(`})(scope,[${fnParamsNamesList}])`, node) ]; return parts; } // Lambda without parameters else { // (() => {{code}}) const parts = [ codePart(`(()=>`, node), ...visit(node.expression), codePart(`)`, node) ]; return parts; } }, LetExpression: (node, visit) => { const declarationsNamesSet = new Set(); for (const d of node.declarations) { if (declarationsNamesSet.has(d.id.name)) { throw new CompileError(`"${d.id.name}" name defined inside let expression was repeated`, '', d.id.location); } declarationsNamesSet.add(d.id.name); } // (scope=> { // var _varNames = []; // var _varValues = []; // scope = [_varNames, _varValues, scope]; // // a = {{init}} // _varNames.push("a"); // _varValues.push({{init}}); // // {{expression}} // return {{expression}} // })(scope) const parts = [ codePart(`(scope=>{var _varNames=[];var _varValues=[];scope=[_varNames,_varValues,scope];`, node), ...node.declarations.flatMap(d => [ codePart(`_varValues.push(`, d), ...visit(d.init), codePart(`);`, d), codePart(`_varNames.push(`, d), codePart(JSON.stringify(d.id.name), d.id), codePart(`);`, d) ]), codePart(`return `, node), ...visit(node.expression), codePart(`})(scope)`, node) ]; return parts; } }; const visit = node => { const nodeTypeVisitor = visitors[node.type]; if (nodeTypeVisitor === undefined) { throw new Error(`No handler for node type - ${node.type}`); } const innerVisit = (childNode) => { return visit(childNode, node); }; // @ts-expect-error skip node is never return nodeTypeVisitor(node, innerVisit); }; export function traverse(tree) { return combineVisitResults(visit(tree.expression, null)); } function getExpressionErrorLocation(colOffset, locations) { var curCol = 0; for (const loc of locations) { curCol += loc.len; if (curCol >= colOffset) return loc.location; } return null; } export function compile(expression, options) { const tree = parse(expression); let traverseResult; try { traverseResult = traverse(tree); } catch (err) { // TODO Use class to access expression from visitors? if (err instanceof CompileError) { err.expression = expression; } throw err; } const { code: expressionCode, offsets } = traverseResult; const bootstrapCodeHead = ` var bool=ctx.castToBoolean; var bop=ctx.binaryOperators; var lop=ctx.logicalOperators; var uop=ctx.unaryOperators; var call=ctx.callFunction; var getIdentifierValue=ctx.getIdentifierValue; var prop=ctx.getProperty; var pipe=ctx.pipe; var globals=ctx.globals??null; function _get(_scope,name){ if(_scope===null)return getIdentifierValue(name,globals,this); var paramIndex=_scope[0].findIndex(it=>it===name); if(paramIndex===-1)return _get.call(this,_scope[2],name); return _scope[1][paramIndex] }; return data=>{ var scope=null; var get=_get.bind(data); return ` .split('\n') .map(it => it.trim()) .filter(it => it !== '') .join('') + ' '; const bootstrapCodeHeadLen = bootstrapCodeHead.length; const functionCode = bootstrapCodeHead + expressionCode + '}'; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const defaultOptions = { ...defaultContextHelpers, ...{ unaryOperators: defaultUnaryOperators, binaryOperators: defaultBinaryOperators, logicalOperators: defaultLogicalOperators }, ...options }; const func = new Function('ctx', functionCode)(defaultOptions); return function (data) { try { return func(data); } catch (err) { if (err instanceof Error === false) throw err; const stackRows = err.stack?.split('\n').map(row => row.trim()); const evalRow = stackRows?.find(row => row.startsWith('at eval ')); if (evalRow === undefined) { throw err; } ERROR_STACK_REGEX.lastIndex = 0; const match = ERROR_STACK_REGEX.exec(evalRow); if (match == null) { throw err; } const rowOffsetStr = match.groups?.['row']; const colOffsetStr = match.groups?.['col']; if (rowOffsetStr === undefined || colOffsetStr === undefined) { throw err; } const rowOffset = Number.parseInt(rowOffsetStr); assert.equal(rowOffset, 3); const colOffset = Number.parseInt(colOffsetStr); const adjustedColOffset = colOffset - bootstrapCodeHeadLen; assert.ok(adjustedColOffset >= 0); const errorLocation = getExpressionErrorLocation(adjustedColOffset, offsets); throw new ExpressionError(err.message, expression, errorLocation, { cause: err }); } }; } //# sourceMappingURL=compiler.js.map