@akala/core
Version:
270 lines • 11.9 kB
JavaScript
import { ExpressionType } from '../expression-type.js';
import { BinaryExpression } from '../binary-expression.js';
import { UnaryExpression } from '../unary-expression.js';
import { TypedLambdaExpression } from '../lambda-expression.js';
import { MemberExpression } from '../member-expression.js';
import { CallExpression } from '../call-expression.js';
import { ApplySymbolExpression } from '../apply-symbol-expression.js';
import { NewExpression } from '../new-expression.js';
import { FormatExpression } from '../../parser.js';
import { TernaryExpression } from '../ternary-expression.js';
import { AssignmentExpression } from '../assignment-expression.js';
/**
* Base class for implementing the Visitor pattern for expression tree processing.
*/
export class ExpressionVisitor {
/**
* Processes an assignment expression by visiting its left and right operands.
* @template T - The expression's value type
* @param {AssignmentExpression<T>} expression - The assignment expression to process.
* @returns {AssignmentExpression<Expressions>} The processed assignment expression.
*/
visitAssign(expression) {
const left = this.visit(expression.left);
const right = this.visit(expression.right);
return left !== expression.left || right !== expression.right
? new AssignmentExpression(left, expression.operator, right)
: expression;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
visit(expression) {
return expression.accept(this);
}
/**
* Handles unknown expression types by attempting to visit them.
* @param {UnknownExpression} expression - An unknown expression type
* @returns {Expressions} The processed expression or throws an error
*/
visitUnknown(expression) {
if (expression.accept)
return expression.accept(this);
throw new Error("unsupported type");
}
/**
* Processes a format expression by visiting its components.
* @template TOutput - The formatted output type
* @param {FormatExpression<TOutput>} expression - The format expression to process
* @returns {FormatExpression<TOutput>} The processed format expression
*/
visitFormat(expression) {
const source = this.visit(expression.lhs);
if (expression.settings) {
const settings = this.visit(expression.settings);
if (source !== expression.lhs || settings !== expression.settings) {
return new FormatExpression(source, expression.formatter, settings);
}
}
if (source !== expression.lhs)
return new FormatExpression(source, expression.formatter, null);
return expression;
}
/**
* Processes a new expression by visiting its initialization members.
* @template T - The expression's value type
* @param {NewExpression<T>} expression - The new expression to process
* @returns {StrictExpressions} The processed new expression
*/
visitNew(expression) {
const members = this.visitArray(expression.init);
if (members !== expression.init)
return new NewExpression(...members);
return expression;
}
/**
* Processes an apply symbol expression by visiting its components.
* @template T - The source expression type
* @template U - The result type after applying the symbol
* @param {ApplySymbolExpression<T, U>} expression - The apply symbol expression to process
* @returns {StrictExpressions} The processed expression
*/
visitApplySymbol(expression) {
const source = this.visit(expression.source);
const arg = this.visit(expression.argument);
if (source !== expression.source || arg !== expression.argument) {
if (!this.isTypedExpression(source))
throw new Error(`Source expression of type ${source?.['type']} cannot be treated as typed`);
return new ApplySymbolExpression(source, expression.symbol, arg);
}
return expression;
}
/**
* Processes a call expression by visiting its components.
* @template T - The source expression type
* @template TMethod - The method key type
* @param {CallExpression<T, TMethod>} expression - The call expression to process
* @returns {StrictExpressions} The processed expression
*/
visitCall(expression) {
const source = this.visit(expression.source);
const args = this.visitArray(expression.arguments);
if (source !== expression.source || args !== expression.arguments) {
if (!this.isTypedExpression(source))
throw new Error(`Source expression of type ${source?.['type']} cannot be treated as typed`);
return new CallExpression(source, expression.method, args, expression.optional);
}
return expression;
}
/**
* Processes a member expression by visiting its components.
* @template T - The source expression type
* @template TMember - The member key type
* @param {MemberExpression<T, TMember, T[TMember]>} expression - The member expression to process
* @returns {StrictExpressions} The processed expression
*/
visitMember(expression) {
const source = expression.source ? this.visit(expression.source) : expression.source;
const member = this.visit(expression.member);
if (source !== expression.source || member !== expression.member) {
if (!this.isTypedExpression(source))
throw new Error(`Source expression of type ${source?.['type']} cannot be treated as typed`);
return new MemberExpression(source, member, expression.optional);
}
return expression;
}
/**
* Checks if an expression is a typed expression.
* @param {Expressions} expression - The expression to check
* @returns {expression is TypedExpression<T>} Type predicate indicating if the expression is typed
*/
isTypedExpression(expression) {
return expression != null && (expression.type === ExpressionType.ConstantExpression ||
expression.type === ExpressionType.ParameterExpression ||
expression.type === ExpressionType.MemberExpression ||
expression.type === ExpressionType.ApplySymbolExpression ||
expression.type === ExpressionType.NewExpression ||
expression.type === ExpressionType.Format ||
expression.type === ExpressionType.UnaryExpression ||
(expression.type === ExpressionType.TernaryExpression &&
this.isTypedExpression(expression.second) &&
this.isTypedExpression(expression.third)));
}
/**
* Processes a lambda expression by visiting its body and parameters.
* @template T - The lambda's function type
* @param {TypedLambdaExpression<T>} expression - The lambda expression to process
* @returns {StrictExpressions} The processed expression
*/
visitLambda(expression) {
const parameters = this.visitArray(expression.parameters);
const body = this.visit(expression.body);
if (body !== expression.body || parameters !== expression.parameters)
return new TypedLambdaExpression(body, ...parameters);
return expression;
}
/**
* Default equality comparer for items.
* @template T - The item type
* @param {T} a - First item
* @param {T} b - Second item
* @returns {boolean} True if items are strictly equal
*/
static defaultComparer(a, b) {
return a === b;
}
/**
* Visits items in an enumerable collection and applies transformation logic.
* @template T - The item type
* @template U - The transformed item type
* @param {IEnumerable<T>} source - Source collection
* @param {addToNew} addToNew - Callback to add transformed items
* @param {visitSingle} visitSingle - Transformation function per item
* @param {EqualityComparer<T>} compare - Optional comparison function
*/
visitEnumerable(source, addToNew, visitSingle, compare) {
const comparator = compare ?? ExpressionVisitor.defaultComparer;
let temp = [];
let index = 0;
for (const item of source) {
const transformed = visitSingle.call(this, item, index);
if (!comparator(transformed, item)) {
if (temp) {
temp.forEach(addToNew);
temp = null;
}
}
if (temp)
temp.push(item);
else
addToNew(transformed, index);
index++;
}
}
/**
* Visits an array of expressions and applies transformation logic.
* @template T - The input expression type
* @template U - The output expression type
* @param {T[]} expressions - Array of expressions to visit
* @param {preVisit} preVisit - Optional callback before visiting
* @returns {U[]} The transformed array of expressions
*/
visitArray(expressions, preVisit) {
let result = null;
this.visitEnumerable(expressions, (item, i) => {
if (!result) {
result = new Array(expressions.length);
}
result[i] = item;
}, (e, i) => {
preVisit?.(e, i);
return this.visit(e);
});
return result || expressions;
}
/**
* Processes a constant expression (no changes required).
* @param {ConstantExpression<unknown>} expression - The constant expression
* @returns {StrictExpressions} The same expression
*/
visitConstant(expression) {
return expression;
}
/**
* Processes a parameter expression (no changes required).
* @param {ParameterExpression<unknown>} expression - The parameter expression
* @returns {StrictExpressions} The same expression
*/
visitParameter(expression) {
return expression;
}
/**
* Processes a unary expression by visiting its operand.
* @param {UnaryExpression} expression - The unary expression to process
* @returns {Expressions} The processed unary expression
*/
visitUnary(expression) {
const operand = this.visit(expression.operand);
return operand !== expression.operand
? new UnaryExpression(operand, expression.operator)
: expression;
}
/**
* Processes a binary expression by visiting its left/right operands.
* @template T - The expression's value type
* @param {BinaryExpression<T>} expression - The binary expression to process
* @returns {BinaryExpression<Expressions>} The processed binary expression
*/
visitBinary(expression) {
const left = this.visit(expression.left);
const right = this.visit(expression.right);
return left !== expression.left || right !== expression.right
? new BinaryExpression(left, expression.operator, right)
: expression;
}
/**
* Processes a ternary expression by visiting its branches.
* @template T - The expression's value type
* @param {TernaryExpression<T>} expression - The ternary expression to process
* @returns {TernaryExpression<Expressions>} The processed ternary expression
*/
visitTernary(expression) {
const first = this.visit(expression.first);
const second = this.visit(expression.second);
const third = this.visit(expression.third);
return (first !== expression.first ||
second !== expression.second ||
third !== expression.third)
? new TernaryExpression(first, expression.operator, second, third)
: expression;
}
}
//# sourceMappingURL=expression-visitor.js.map