@pawel-up/jexl
Version:
Javascript Expression Language: Powerful context-based expression parser and evaluator
856 lines (823 loc) • 29.5 kB
text/typescript
/* eslint-disable @typescript-eslint/no-explicit-any */
import type {
Grammar,
ASTNode,
ArrayLiteralNode,
BinaryExpressionNode,
ConditionalExpressionNode,
FilterExpressionNode,
IdentifierNode,
LiteralNode,
ObjectLiteralNode,
FunctionCallNode,
UnaryExpressionNode,
} from '../grammar.js'
const poolNames = {
functions: 'Jexl Function',
transforms: 'Transform',
} as const
/**
* The Evaluator executes Jexl expression trees (AST) within a given context and returns the computed results.
*
* This class is the core execution engine that takes a parsed expression tree from the {@link Parser}
* and evaluates it against provided data contexts. It handles all Jexl operations including:
* - Variable resolution and property access
* - Mathematical and logical operations
* - Function and transform execution
* - Array filtering and object manipulation
* - Conditional expressions (ternary operators)
*
* @example Basic usage
* ```typescript
* const grammar = getGrammar()
* const context = { user: { name: 'John', age: 30 }, items: [1, 2, 3] }
* const evaluator = new Evaluator(grammar, context)
*
* // Evaluate a simple expression tree
* const result = await evaluator.eval(expressionTree)
* ```
*
* @example With transforms and functions
* ```typescript
* const grammar = {
* ...getGrammar(),
* transforms: {
* upper: (val: string) => val.toUpperCase(),
* multiply: (val: number, factor: number) => val * factor
* }
* }
* const evaluator = new Evaluator(grammar, { name: 'john', score: 85 })
* ```
*
* @example With relative context for filtering
* ```typescript
* const users = [{ name: 'Alice', active: true }, { name: 'Bob', active: false }]
* // Each user object becomes relative context when filtering
* const evaluator = new Evaluator(grammar, { users })
* // Expression: users[.active == true] would use each user as relative context
* ```
*/
export default class Evaluator {
/**
* The grammar object containing operators, functions, and transforms used for evaluation.
* This defines the language rules and available operations for expressions.
*
* @example
* ```typescript
* const grammar = {
* elements: { '+': { type: 'binaryOp', eval: (a, b) => a + b } },
* functions: { max: Math.max },
* transforms: { upper: (s: string) => s.toUpperCase() }
* }
* ```
*/
_grammar: Grammar
/**
* The main context object containing variables and values accessible in expressions.
* Non-relative identifiers (like `user.name`) are resolved against this context.
*
* @example
* ```typescript
* const context = {
* user: { name: 'Alice', age: 25 },
* settings: { theme: 'dark' },
* data: [1, 2, 3, 4, 5]
* }
* // Expression "user.name" resolves to "Alice"
* // Expression "settings.theme" resolves to "dark"
* ```
*/
_context: Record<string, unknown>
/**
* The relative context used for resolving relative identifiers (those starting with `.`).
* This is typically used in filter expressions where each array element becomes the relative context.
*
* @example
* ```typescript
* // When filtering users[.age > 18]:
* // For each user object like { name: 'Bob', age: 25 }
* // The relative context becomes that user object
* // So ".age" resolves to 25 for that iteration
* const users = [
* { name: 'Alice', age: 25 },
* { name: 'Bob', age: 17 }
* ]
* // During filter evaluation, _relContext = { name: 'Alice', age: 25 } for first user
* ```
*/
_relContext: Record<string, unknown>
/**
* Creates a new Evaluator instance for executing Jexl expressions.
*
* @param grammar - Grammar object containing operators, functions, and transforms
* @param context - Main variable context for resolving non-relative identifiers
* @param relativeContext - Context for resolving relative identifiers (defaults to main context)
*
* @example Creating an evaluator
* ```typescript
* const grammar = getGrammar()
* const context = {
* user: { name: 'John', posts: 5 },
* threshold: 10
* }
* const evaluator = new Evaluator(grammar, context)
* ```
*
* @example With custom relative context
* ```typescript
* const currentUser = { name: 'Admin', role: 'admin' }
* const evaluator = new Evaluator(grammar, globalContext, currentUser)
* // Now relative identifiers like ".name" resolve to "Admin"
* ```
*/
constructor(grammar: Grammar, context: Record<string, unknown>, relativeContext?: Record<string, unknown>) {
this._grammar = grammar
this._context = context || {}
this._relContext = relativeContext || this._context
}
/**
* Evaluates a Jexl expression tree (AST) and returns the computed result.
*
* This is the main entry point for expression evaluation. It processes the AST node
* based on its type and delegates to the appropriate handler method.
*
* @param ast - The expression tree node to evaluate
* @returns Promise resolving to the evaluation result
*
* @example Basic evaluation
* ```typescript
* // For expression "user.age + 10"
* const ast = {
* type: 'BinaryExpression',
* operator: '+',
* left: { type: 'Identifier', value: 'user', from: { type: 'Identifier', value: 'age' } },
* right: { type: 'Literal', value: 10 }
* }
* const result = await evaluator.eval(ast) // Returns 35 if user.age is 25
* ```
*
* @example Complex evaluation
* ```typescript
* // For expression "users[.active].length > 0"
* const result = await evaluator.eval(complexAst)
* // Returns true if there are active users, false otherwise
* ```
*
* @throws {Error} When encountering unknown AST node types or evaluation errors
*/
async eval(ast: ASTNode): Promise<unknown> {
switch (ast.type) {
case 'ArrayLiteral':
return this._handleArrayLiteral(ast)
case 'BinaryExpression':
return this._handleBinaryExpression(ast)
case 'ConditionalExpression':
return this._handleConditionalExpression(ast)
case 'FilterExpression':
return this._handleFilterExpression(ast)
case 'Identifier':
return this._handleIdentifier(ast)
case 'Literal':
return this._handleLiteral(ast)
case 'ObjectLiteral':
return this._handleObjectLiteral(ast)
case 'FunctionCall':
return this._handleFunctionCall(ast)
case 'UnaryExpression':
return this._handleUnaryExpression(ast)
default:
throw new Error(`Unknown AST node type: ${(ast as any).type}`)
}
}
/**
* Evaluates an array of expression trees in parallel and returns results in the same order.
*
* This method is useful for evaluating multiple expressions simultaneously, such as
* function arguments, array elements, or object property values.
*
* @param arr - Array of expression tree nodes to evaluate
* @returns Promise resolving to array of evaluation results in the same order
*
* @example Evaluating function arguments
* ```typescript
* // For function call "max(user.score, 100, bonus)"
* const args = [
* { type: 'Identifier', value: 'user', from: { type: 'Identifier', value: 'score' } },
* { type: 'Literal', value: 100 },
* { type: 'Identifier', value: 'bonus' }
* ]
* const results = await evaluator.evalArray(args)
* // Returns [85, 100, 15] if user.score=85 and bonus=15
* ```
*
* @example Evaluating array literal elements
* ```typescript
* // For array literal "[name, age * 2, active]"
* const elements = [
* { type: 'Identifier', value: 'name' },
* { type: 'BinaryExpression', operator: '*', left: {...}, right: {...} },
* { type: 'Identifier', value: 'active' }
* ]
* const results = await evaluator.evalArray(elements)
* // Returns ['John', 50, true] if name='John', age=25, active=true
* ```
*/
evalArray(arr: ASTNode[]): Promise<unknown[]> {
return Promise.all(arr.map((elem) => this.eval(elem)))
}
/**
* Evaluates a map of expression trees in parallel and returns a map with the same keys.
*
* This method is primarily used for evaluating object literal properties where each
* property value is an expression that needs to be evaluated.
*
* @param map - Map from property names to expression tree nodes
* @returns Promise resolving to map with same keys but evaluated values
*
* @example Evaluating object literal
* ```typescript
* // For object literal "{ fullName: name + ' ' + surname, isAdult: age >= 18 }"
* const map = {
* fullName: {
* type: 'BinaryExpression',
* operator: '+',
* left: { type: 'Identifier', value: 'name' },
* right: { type: 'BinaryExpression', operator: '+', ... }
* },
* isAdult: {
* type: 'BinaryExpression',
* operator: '>=',
* left: { type: 'Identifier', value: 'age' },
* right: { type: 'Literal', value: 18 }
* }
* }
* const result = await evaluator.evalMap(map)
* // Returns { fullName: 'John Doe', isAdult: true } if name='John', surname='Doe', age=25
* ```
*
* @throws {Error} When a key in the map has no corresponding AST
*/
async evalMap(map: Record<string, ASTNode>): Promise<Record<string, unknown>> {
const keys = Object.keys(map)
const result: Record<string, unknown> = {}
const asts = keys.map((key) => {
const ast = map[key]
if (!ast) {
throw new Error(`No AST found for key: ${key}`)
}
return this.eval(ast)
})
const vals = await Promise.all(asts)
vals.forEach((val, idx) => {
const key = keys[idx]
if (key !== undefined) {
result[key] = val
}
})
return result
}
/**
* Applies a filter expression with relative identifiers to an array of subjects.
*
* This method implements array filtering where each element becomes the relative context
* for evaluating the filter expression. Elements that produce a truthy result are included
* in the returned array. This is used for expressions like `users[.active == true]`.
*
* @param subject - The value to filter (converted to array if not already)
* @param expr - Filter expression tree with relative identifiers (starting with '.')
* @returns Promise resolving to filtered array of elements that passed the test
*
* @example Basic array filtering
* ```typescript
* const users = [
* { name: 'Alice', age: 25, active: true },
* { name: 'Bob', age: 17, active: false },
* { name: 'Carol', age: 30, active: true }
* ]
* // Filter expression: .active == true
* const filterExpr = {
* type: 'BinaryExpression',
* operator: '==',
* left: { type: 'Identifier', value: 'active', relative: true },
* right: { type: 'Literal', value: true }
* }
* const result = await evaluator._filterRelative(users, filterExpr)
* // Returns [{ name: 'Alice', age: 25, active: true }, { name: 'Carol', age: 30, active: true }]
* ```
*
* @example Complex filtering with multiple conditions
* ```typescript
* // Filter expression: .age >= 18 && .score > 80
* const result = await evaluator._filterRelative(students, complexFilterExpr)
* // Returns only adult students with high scores
* ```
*
* @example Non-array subject
* ```typescript
* const singleUser = { name: 'John', active: true }
* // Gets converted to [singleUser] before filtering
* const result = await evaluator._filterRelative(singleUser, filterExpr)
* // Returns [singleUser] if filter passes, [] if it doesn't
* ```
*
* @private
*/
async _filterRelative(subject: unknown, expr: ASTNode): Promise<unknown[]> {
const promises: Promise<unknown>[] = []
let subjectArray: unknown[]
if (!Array.isArray(subject)) {
subjectArray = subject === undefined ? [] : [subject]
} else {
subjectArray = subject
}
subjectArray.forEach((elem) => {
const evalInst = new Evaluator(this._grammar, this._context, elem as Record<string, unknown>)
promises.push(evalInst.eval(expr))
})
const values = await Promise.all(promises)
const results: unknown[] = []
values.forEach((value, idx) => {
if (value) {
results.push(subjectArray[idx])
}
})
return results
}
/**
* Applies a static filter expression to access object properties or array elements.
*
* This method handles bracket notation access like `obj[key]` or `arr[index]`. If the
* filter expression evaluates to a boolean, it returns the subject for true or undefined
* for false. For other values, it uses the result as a property key or array index.
*
* @param subject - The object or array to access
* @param expr - Expression tree that evaluates to a property key, array index, or boolean
* @returns Promise resolving to the accessed value or undefined
*
* @example Object property access
* ```typescript
* const user = { name: 'John', email: 'john@example.com' }
* // Filter expression: "em" + "ail"
* const keyExpr = {
* type: 'BinaryExpression',
* operator: '+',
* left: { type: 'Literal', value: 'em' },
* right: { type: 'Literal', value: 'ail' }
* }
* const result = await evaluator._filterStatic(user, keyExpr)
* // Returns 'john@example.com'
* ```
*
* @example Array index access
* ```typescript
* const items = ['apple', 'banana', 'cherry']
* // Filter expression: 1 + 1
* const indexExpr = {
* type: 'BinaryExpression',
* operator: '+',
* left: { type: 'Literal', value: 1 },
* right: { type: 'Literal', value: 1 }
* }
* const result = await evaluator._filterStatic(items, indexExpr)
* // Returns 'cherry' (items[2])
* ```
*
* @example Boolean filtering
* ```typescript
* const data = { value: 42 }
* // Filter expression: true
* const boolExpr = { type: 'Literal', value: true }
* const result = await evaluator._filterStatic(data, boolExpr)
* // Returns { value: 42 } (the original data)
* ```
*
* @private
*/
async _filterStatic(subject: unknown, expr: ASTNode): Promise<unknown> {
const res = await this.eval(expr)
if (typeof res === 'boolean') {
return res ? subject : undefined
}
// Type guard for indexable types
if (subject === undefined) {
return undefined
}
if (subject === null) {
return null
}
if (typeof subject === 'object' || Array.isArray(subject)) {
return (subject as any)[res as string | number]
}
return undefined
}
// ===== Private Handler Methods =====
// These methods handle specific AST node types during evaluation
/**
* Evaluates an ArrayLiteral node by evaluating each element expression.
*
* @param ast - ArrayLiteral node containing array of element expressions
* @returns Promise resolving to array with evaluated element values
*
* @example
* ```typescript
* // For array literal [name, age + 1, "static"]
* const ast = {
* type: 'ArrayLiteral',
* value: [
* { type: 'Identifier', value: 'name' },
* { type: 'BinaryExpression', operator: '+', left: {...}, right: {...} },
* { type: 'Literal', value: 'static' }
* ]
* }
* const result = await evaluator._handleArrayLiteral(ast)
* // Returns ['John', 26, 'static'] if name='John' and age=25
* ```
*/
private async _handleArrayLiteral(ast: ArrayLiteralNode): Promise<unknown[]> {
return this.evalArray(ast.value)
}
/**
* Evaluates a BinaryExpression node by applying the operator to left and right operands.
*
* Supports two evaluation modes:
* - `eval`: Pre-evaluates both operands and passes values to operator function
* - `evalOnDemand`: Passes wrapped operands that can be evaluated conditionally (for && and ||)
*
* @param ast - BinaryExpression node with operator and left/right operands
* @returns Promise resolving to the operation result
*
* @example Arithmetic operation
* ```typescript
* // For expression "price * quantity"
* const ast = {
* type: 'BinaryExpression',
* operator: '*',
* left: { type: 'Identifier', value: 'price' },
* right: { type: 'Identifier', value: 'quantity' }
* }
* const result = await evaluator._handleBinaryExpression(ast)
* // Returns 150 if price=15 and quantity=10
* ```
*
* @example Logical operation (evalOnDemand)
* ```typescript
* // For expression "user && user.active"
* // Right side only evaluated if left side is truthy
* const result = await evaluator._handleBinaryExpression(logicalAst)
* // Returns user.active value if user exists, otherwise false
* ```
*
* @throws {Error} When operator is unknown or has no eval function
*/
private async _handleBinaryExpression(ast: BinaryExpressionNode): Promise<unknown> {
const grammarOp = this._grammar.elements[ast.operator]
if (!grammarOp) {
throw new Error(`Unknown binary operator: ${ast.operator}`)
}
if ('evalOnDemand' in grammarOp && grammarOp.evalOnDemand) {
const wrap = (subAst: ASTNode) => ({ eval: () => this.eval(subAst) })
return grammarOp.evalOnDemand(wrap(ast.left), wrap(ast.right))
}
if ('eval' in grammarOp && grammarOp.eval) {
const [leftVal, rightVal] = await Promise.all([this.eval(ast.left), this.eval(ast.right)])
return grammarOp.eval(leftVal, rightVal)
}
throw new Error(`Binary operator ${ast.operator} has no eval function`)
}
/**
* Evaluates a ConditionalExpression node (ternary operator: test ? consequent : alternate).
*
* First evaluates the test expression. If truthy, evaluates and returns the consequent.
* If falsy, evaluates and returns the alternate. If consequent is missing (Elvis operator),
* returns the test result itself.
*
* @param ast - ConditionalExpression node with test, consequent, and alternate
* @returns Promise resolving to either consequent or alternate result
*
* @example Standard ternary
* ```typescript
* // For expression "age >= 18 ? 'adult' : 'minor'"
* const ast = {
* type: 'ConditionalExpression',
* test: { type: 'BinaryExpression', operator: '>=', ... },
* consequent: { type: 'Literal', value: 'adult' },
* alternate: { type: 'Literal', value: 'minor' }
* }
* const result = await evaluator._handleConditionalExpression(ast)
* // Returns 'adult' if age >= 18, 'minor' otherwise
* ```
*
* @example Elvis operator (missing consequent)
* ```typescript
* // For expression "user.nickname ?: user.name"
* // If nickname is truthy, return it; otherwise return name
* const result = await evaluator._handleConditionalExpression(elvisAst)
* // Returns nickname if it exists and is truthy, otherwise returns name
* ```
*/
private async _handleConditionalExpression(ast: ConditionalExpressionNode): Promise<unknown> {
const res = await this.eval(ast.test)
if (res) {
if (ast.consequent) {
return this.eval(ast.consequent)
}
return res
}
return this.eval(ast.alternate)
}
/**
* Evaluates a FilterExpression node for array filtering or property access.
*
* Delegates to either relative filtering (for expressions like `users[.active]`) or
* static filtering (for expressions like `user[key]` or `items[0]`).
*
* @param ast - FilterExpression node with subject, filter expression, and relative flag
* @returns Promise resolving to filtered array or accessed property value
*
* @example Relative filtering
* ```typescript
* // For expression "users[.age > 21]"
* const ast = {
* type: 'FilterExpression',
* subject: { type: 'Identifier', value: 'users' },
* expr: { type: 'BinaryExpression', operator: '>', ... },
* relative: true
* }
* const result = await evaluator._handleFilterExpression(ast)
* // Returns array of users with age > 21
* ```
*
* @example Static property access
* ```typescript
* // For expression "config['api' + 'Key']"
* const ast = {
* type: 'FilterExpression',
* subject: { type: 'Identifier', value: 'config' },
* expr: { type: 'BinaryExpression', operator: '+', ... },
* relative: false
* }
* const result = await evaluator._handleFilterExpression(ast)
* // Returns config.apiKey value
* ```
*/
private async _handleFilterExpression(ast: FilterExpressionNode): Promise<unknown> {
const subject = await this.eval(ast.subject)
if (ast.relative) {
return this._filterRelative(subject, ast.expr)
}
return this._filterStatic(subject, ast.expr)
}
/**
* Evaluates an Identifier node to resolve variable or property values.
*
* Handles multiple scenarios:
* - Simple identifiers: resolved from main or relative context
* - Property access: resolves the 'from' object first, then accesses the property
* - Array indexing: automatically uses first element if accessing property on array
*
* @param ast - Identifier node with value, optional 'from' expression, and relative flag
* @returns Promise resolving to the identifier's value or undefined if not found
*
* @example Simple identifier
* ```typescript
* // For identifier "username"
* const ast = {
* type: 'Identifier',
* value: 'username',
* relative: false
* }
* const result = await evaluator._handleIdentifier(ast)
* // Returns context.username value
* ```
*
* @example Property access
* ```typescript
* // For expression "user.profile.avatar"
* const ast = {
* type: 'Identifier',
* value: 'avatar',
* from: {
* type: 'Identifier',
* value: 'profile',
* from: { type: 'Identifier', value: 'user' }
* }
* }
* const result = await evaluator._handleIdentifier(ast)
* // Returns user.profile.avatar value
* ```
*
* @example Relative identifier
* ```typescript
* // For relative identifier ".status" in filter context
* const ast = {
* type: 'Identifier',
* value: 'status',
* relative: true
* }
* const result = await evaluator._handleIdentifier(ast)
* // Returns current relative context's status property
* ```
*
* @example Array property access
* ```typescript
* // For "users.name" where users is an array
* // Automatically accesses users[0].name
* const result = await evaluator._handleIdentifier(arrayPropertyAst)
* // Returns first user's name
* ```
*/
private async _handleIdentifier(ast: IdentifierNode): Promise<unknown> {
if (!ast.from) {
return ast.relative ? this._relContext[ast.value] : this._context[ast.value]
}
const context = await this.eval(ast.from)
if (context === undefined) {
return undefined
}
if (context === null) {
return null
}
let targetContext = context
if (Array.isArray(context)) {
targetContext = context[0]
}
return (targetContext as any)?.[ast.value]
}
/**
* Evaluates a Literal node by returning its stored value.
*
* Literals represent constant values like strings, numbers, booleans, null, etc.
* This is the simplest evaluation case - just return the pre-parsed value.
*
* @param ast - Literal node containing the constant value
* @returns The literal value (string, number, boolean, null, etc.)
*
* @example String literal
* ```typescript
* const ast = { type: 'Literal', value: 'Hello World' }
* const result = evaluator._handleLiteral(ast)
* // Returns 'Hello World'
* ```
*
* @example Number literal
* ```typescript
* const ast = { type: 'Literal', value: 42.5 }
* const result = evaluator._handleLiteral(ast)
* // Returns 42.5
* ```
*
* @example Boolean literal
* ```typescript
* const ast = { type: 'Literal', value: true }
* const result = evaluator._handleLiteral(ast)
* // Returns true
* ```
*/
private _handleLiteral(ast: LiteralNode): unknown {
return ast.value
}
/**
* Evaluates an ObjectLiteral node by evaluating each property value expression.
*
* Creates a new object with the same property keys but with evaluated values.
* Each property value is an expression that gets evaluated independently.
*
* @param ast - ObjectLiteral node containing map of property names to value expressions
* @returns Promise resolving to object with same keys but evaluated values
*
* @example Simple object literal
* ```typescript
* // For object literal "{ name: firstName, age: currentYear - birthYear }"
* const ast = {
* type: 'ObjectLiteral',
* value: {
* name: { type: 'Identifier', value: 'firstName' },
* age: {
* type: 'BinaryExpression',
* operator: '-',
* left: { type: 'Identifier', value: 'currentYear' },
* right: { type: 'Identifier', value: 'birthYear' }
* }
* }
* }
* const result = await evaluator._handleObjectLiteral(ast)
* // Returns { name: 'John', age: 25 } if firstName='John', currentYear=2023, birthYear=1998
* ```
*
* @example Nested object literal
* ```typescript
* // For "{ user: { id: userId, active: true }, meta: { created: now() } }"
* const result = await evaluator._handleObjectLiteral(nestedAst)
* // Returns fully evaluated nested object structure
* ```
*/
private async _handleObjectLiteral(ast: ObjectLiteralNode): Promise<Record<string, unknown>> {
return this.evalMap(ast.value)
}
/**
* Evaluates a FunctionCall node by calling the function with evaluated arguments.
*
* Looks up the function in the appropriate pool (functions or transforms), evaluates
* all arguments, and calls the function with the results. Transforms receive the
* subject as the first argument, followed by any additional arguments.
*
* @param ast - FunctionCall node with name, arguments, and pool specification
* @returns Promise resolving to the function's return value
*
* @example Regular function call
* ```typescript
* // For function call "max(score1, score2, 100)"
* const ast = {
* type: 'FunctionCall',
* name: 'max',
* pool: 'functions',
* args: [
* { type: 'Identifier', value: 'score1' },
* { type: 'Identifier', value: 'score2' },
* { type: 'Literal', value: 100 }
* ]
* }
* const result = await evaluator._handleFunctionCall(ast)
* // Returns 100 if score1=85 and score2=92
* ```
*
* @example Transform call
* ```typescript
* // For transform "name|upper" (transforms are also FunctionCall nodes)
* const ast = {
* type: 'FunctionCall',
* name: 'upper',
* pool: 'transforms',
* args: [{ type: 'Identifier', value: 'name' }]
* }
* const result = await evaluator._handleFunctionCall(ast)
* // Returns 'JOHN' if name='john' and upper transform converts to uppercase
* ```
*
* @throws {Error} When function pool is invalid or function is not defined
*/
private async _handleFunctionCall(ast: FunctionCallNode): Promise<unknown> {
const poolName = poolNames[ast.pool]
if (!poolName) {
throw new Error(`Corrupt AST: Pool '${ast.pool}' not found`)
}
const pool = this._grammar[ast.pool]
const func = pool[ast.name]
if (!func) {
throw new Error(`${poolName} ${ast.name} is not defined.`)
}
const args = await this.evalArray(ast.args || [])
return (func as any)(...args)
}
/**
* Evaluates a UnaryExpression node by applying the unary operator to its operand.
*
* Evaluates the right-side operand first, then applies the unary operator's
* evaluation function to the result. Common unary operators include negation (!).
*
* @param ast - UnaryExpression node with operator and right operand
* @returns Promise resolving to the operation result
*
* @example Logical negation
* ```typescript
* // For expression "!user.active"
* const ast = {
* type: 'UnaryExpression',
* operator: '!',
* right: {
* type: 'Identifier',
* value: 'active',
* from: { type: 'Identifier', value: 'user' }
* }
* }
* const result = await evaluator._handleUnaryExpression(ast)
* // Returns false if user.active is true, true if user.active is false
* ```
*
* @example Numeric negation (if supported)
* ```typescript
* // For expression "-price"
* const ast = {
* type: 'UnaryExpression',
* operator: '-',
* right: { type: 'Identifier', value: 'price' }
* }
* const result = await evaluator._handleUnaryExpression(ast)
* // Returns -15.99 if price=15.99
* ```
*
* @throws {Error} When operator is unknown or has no eval function
*/
private async _handleUnaryExpression(ast: UnaryExpressionNode): Promise<unknown> {
const right = await this.eval(ast.right)
const grammarOp = this._grammar.elements[ast.operator]
if (!grammarOp) {
throw new Error(`Unknown unary operator: ${ast.operator}`)
}
if ('eval' in grammarOp && grammarOp.eval) {
// Unary operators only take one argument
return (grammarOp.eval as any)(right)
}
throw new Error(`Unary operator ${ast.operator} has no eval function`)
}
}