diff --git a/benchmark/index.js b/benchmark/index.js index d83ecd9..a1da1b0 100644 --- a/benchmark/index.js +++ b/benchmark/index.js @@ -13,7 +13,7 @@ import * as celJsPackage from 'cel-js' // Benchmark configuration const ITERATIONS = { - parse: 50000, + parse: 10000, evaluate: 50000, warmup: 10000 } @@ -176,7 +176,7 @@ let TEST_EXPRESSIONS = [ // Macro operations (may not be supported by cel-js) { name: 'List Comprehension', - expression: 'items.filter(x, x > 10).map(x, x * 2)', + expression: 'items.filter(x, x > 10).map(x, x > 2)', context: {items: [5, 10, 15, 20, 25]} }, @@ -211,23 +211,6 @@ function formatNumber(num) { return Math.round(num).toLocaleString() } -/** - * Calculate statistics for a set of measurements - */ -function calculateStats(measurements) { - const sorted = [...measurements].sort((a, b) => a - b) - const len = sorted.length - - return { - min: sorted[0], - max: sorted[len - 1], - median: - len % 2 === 0 ? (sorted[len / 2 - 1] + sorted[len / 2]) / 2 : sorted[Math.floor(len / 2)], - mean: measurements.reduce((a, b) => a + b, 0) / len, - p95: sorted[Math.floor(len * 0.95)] - } -} - /** * Run a single benchmark with error handling */ @@ -235,24 +218,16 @@ function runBenchmark(name, fn, iterations) { try { for (let i = 0; i < ITERATIONS.warmup; i++) fn() - const measurements = [] const totalStart = performance.now() - for (let i = 0; i < iterations; i++) { - const start = performance.now() - fn() - const end = performance.now() - measurements.push(end - start) - } + for (let i = 0; i < iterations; i++) fn() const totalTime = performance.now() - totalStart - const stats = calculateStats(measurements) const opsPerSec = Math.round((iterations / totalTime) * 1000) return { totalTime, opsPerSec, - stats, supported: true } } catch (error) { @@ -306,7 +281,6 @@ function benchmarkParsing() { localResults.opsPerSec )} ops/sec)` ) - console.log(` Mean: ${localResults.stats.mean.toFixed(3)}ms`) } else { console.log(` Not Supported: šŸ”“`) console.log(` Error: ${localResults.error}`) @@ -319,7 +293,6 @@ function benchmarkParsing() { packageResults.opsPerSec )} ops/sec)` ) - console.log(` Mean: ${packageResults.stats.mean.toFixed(3)}ms`) } else { console.log(` Not Supported: šŸ”“`) } @@ -427,7 +400,6 @@ function benchmarkEvaluation() { localResults.opsPerSec )} ops/sec)` ) - console.log(` Mean: ${localResults.stats.mean.toFixed(3)}ms`) } else { console.log(` Not Supported: šŸ”“`) console.log(` Error: ${localResults.error}`) @@ -440,105 +412,6 @@ function benchmarkEvaluation() { packageResults.opsPerSec )} ops/sec)` ) - console.log(` Mean: ${packageResults.stats.mean.toFixed(3)}ms`) - } else { - console.log(` Not Supported: šŸ”“`) - } - - if (localResults.supported && packageResults.supported) { - console.log( - `\n Result: @marcbachmann/cel-js is ${speedup}x ${faster ? 'faster šŸš€' : 'slower'}` - ) - supportedByBoth++ - } else if (localResults.supported && !packageResults.supported) { - console.log(`\n Result: Only @marcbachmann/cel-js supports this expression āœ…`) - onlyLocal++ - } else if (!localResults.supported && packageResults.supported) { - console.log(`\n Result: Only cel-js package supports this expression`) - onlyPackage++ - } - - if (localResults.supported && packageResults.supported) { - results.push({ - name: test.name, - localTime: localResults.totalTime, - packageTime: packageResults.totalTime, - speedup: parseFloat(speedup), - localOpsPerSec: localResults.opsPerSec, - packageOpsPerSec: packageResults.opsPerSec - }) - } - } - - console.log(`\n\nSupport Summary:`) - console.log(` Supported by both: ${supportedByBoth}`) - console.log(` Only @marcbachmann/cel-js: ${onlyLocal}`) - console.log(` Only cel-js package: ${onlyPackage}`) - - return results -} - -/** - * Benchmark combined parse + evaluate performance - */ -function benchmarkCombined() { - console.log(`\n${'='.repeat(60)}`) - console.log('COMBINED PARSE + EVALUATE PERFORMANCE') - console.log(`${'='.repeat(60)}\n`) - - const results = [] - let supportedByBoth = 0 - let onlyLocal = 0 - let onlyPackage = 0 - - for (const test of TEST_EXPRESSIONS) { - console.log(`\nā–ø ${test.name}`) - const expr = serialize(celJsLocal.parse(test.expression).ast) - console.log(` Expression: ${expr}`) - - // Benchmark @marcbachmann/cel-js - const localResults = runBenchmark( - '@marcbachmann/cel-js', - () => celJsLocal.evaluate(test.expression, test.context), - ITERATIONS.evaluate - ) - - // Benchmark cel-js package - const packageResults = test.skipThirdParty - ? {supported: false} - : runBenchmark( - 'cel-js', - () => celJsPackage.evaluate(test.expression, test.context), - ITERATIONS.evaluate - ) - - const speedup = - localResults.supported && packageResults.supported - ? (packageResults.totalTime / localResults.totalTime).toFixed(2) - : null - const faster = speedup && speedup > 1 - - console.log(`\n @marcbachmann/cel-js:`) - if (localResults.supported) { - console.log( - ` Total: ${localResults.totalTime.toFixed(2)}ms (${formatNumber( - localResults.opsPerSec - )} ops/sec)` - ) - console.log(` Mean: ${localResults.stats.mean.toFixed(3)}ms`) - } else { - console.log(` Not Supported: šŸ”“`) - console.log(` Error: ${localResults.error}`) - } - - console.log(`\n cel-js package:`) - if (packageResults.supported) { - console.log( - ` Total: ${packageResults.totalTime.toFixed(2)}ms (${formatNumber( - packageResults.opsPerSec - )} ops/sec)` - ) - console.log(` Mean: ${packageResults.stats.mean.toFixed(3)}ms`) } else { console.log(` Not Supported: šŸ”“`) } @@ -579,7 +452,7 @@ function benchmarkCombined() { /** * Display summary statistics */ -function displaySummary(parseResults, evalResults, combinedResults) { +function displaySummary(parseResults, evalResults) { console.log(`\n${'='.repeat(60)}`) console.log('PERFORMANCE SUMMARY') console.log(`${'='.repeat(60)}\n`) @@ -612,7 +485,6 @@ function displaySummary(parseResults, evalResults, combinedResults) { const parseStats = calculateBenchmarkStats(parseResults) const evalStats = calculateBenchmarkStats(evalResults) - const combinedStats = calculateBenchmarkStats(combinedResults) if (parseStats.total > 0) { console.log('PARSING PERFORMANCE (for expressions supported by both):') @@ -632,21 +504,12 @@ function displaySummary(parseResults, evalResults, combinedResults) { console.log(` Faster in ${evalStats.fasterCount}/${evalStats.total} tests`) } - if (combinedStats.total > 0) { - console.log('\nCOMBINED PERFORMANCE (for expressions supported by both):') - console.log(` Average speedup: ${combinedStats.avgSpeedup.toFixed(2)}x`) - console.log( - ` Range: ${combinedStats.minSpeedup.toFixed(2)}x - ${combinedStats.maxSpeedup.toFixed(2)}x` - ) - console.log(` Faster in ${combinedStats.fasterCount}/${combinedStats.total} tests`) - } - console.log(`\nšŸ“Š Note: Speedup > 1.0 means @marcbachmann/cel-js is faster than cel-js package`) // Overall verdict - const totalTests = parseStats.total + evalStats.total + combinedStats.total + const totalTests = parseStats.total + evalStats.total if (totalTests > 0) { - const overallAvg = (parseStats.avgSpeedup + evalStats.avgSpeedup + combinedStats.avgSpeedup) / 3 + const overallAvg = (parseStats.avgSpeedup + evalStats.avgSpeedup) / 2 if (overallAvg > 1.5) { console.log('āœ… Overall: @marcbachmann/cel-js shows significant performance improvements!') } else if (overallAvg > 1.1) { @@ -679,9 +542,8 @@ async function runBenchmarks() { const parseResults = benchmarkParsing() const evalResults = benchmarkEvaluation() - const combinedResults = benchmarkCombined() - displaySummary(parseResults, evalResults, combinedResults) + displaySummary(parseResults, evalResults) const totalTime = ((performance.now() - startTime) / 1000).toFixed(1) console.log(`ā±ļø Total benchmark time: ${totalTime}s\n`) diff --git a/evaluator.js b/evaluator.js index 257e57a..2fd314b 100644 --- a/evaluator.js +++ b/evaluator.js @@ -769,239 +769,253 @@ function firstNode(a, b) { return Array.isArray(a) ? a : b } -const handlers = { - id(ast, s) { - const val = objectGet(s.ctx, ast[1]) - if (val === undefined) throw new EvaluationError(`Unknown variable: ${ast[1]}`, ast) - return val - }, - '||'(ast, s) { - try { - const left = s.eval(ast[1]) - if (left === true) return true - if (left !== false) throw new EvaluationError('Left operand of || is not a boolean', ast) - } catch (err) { - if (err.message.includes('Unknown variable')) throw err - if (err.message.includes('is not a boolean')) throw err +const handlers = new Map( + Object.entries({ + id(ast, s) { + const val = objectGet(s.ctx, ast[1]) + if (val === undefined) throw new EvaluationError(`Unknown variable: ${ast[1]}`, ast) + return val + }, + '||'(ast, s) { + try { + const left = s.eval(ast[1]) + if (left === true) return true + if (left !== false) throw new EvaluationError('Left operand of || is not a boolean', ast) + } catch (err) { + if (err.message.includes('Unknown variable')) throw err + if (err.message.includes('is not a boolean')) throw err + + const right = s.eval(ast[2]) + if (right === true) return true + if (right === false) throw err + throw new EvaluationError('Right operand of || is not a boolean', ast) + } const right = s.eval(ast[2]) - if (right === true) return true - if (right === false) throw err + if (typeof right === 'boolean') return right throw new EvaluationError('Right operand of || is not a boolean', ast) - } - - const right = s.eval(ast[2]) - if (typeof right === 'boolean') return right - throw new EvaluationError('Right operand of || is not a boolean', ast) - }, - '&&'(ast, s) { - try { - const left = s.eval(ast[1]) - if (left === false) return false - if (left !== true) { - throw new EvaluationError('Left operand of && is not a boolean', firstNode(ast[1], ast)) + }, + '&&'(ast, s) { + try { + const left = s.eval(ast[1]) + if (left === false) return false + if (left !== true) { + throw new EvaluationError('Left operand of && is not a boolean', firstNode(ast[1], ast)) + } + } catch (err) { + if (err.message.includes('Unknown variable')) throw err + if (err.message.includes('is not a boolean')) throw err + + const right = s.eval(ast[2]) + if (right === false) return false + if (right === true) throw err + throw new EvaluationError('Right operand of && is not a boolean', firstNode(ast[2], ast)) } - } catch (err) { - if (err.message.includes('Unknown variable')) throw err - if (err.message.includes('is not a boolean')) throw err const right = s.eval(ast[2]) - if (right === false) return false - if (right === true) throw err + if (typeof right === 'boolean') return right throw new EvaluationError('Right operand of && is not a boolean', firstNode(ast[2], ast)) - } + }, + '+'(ast, s) { + const left = s.eval(ast[1]) + const right = s.eval(ast[2]) + const leftType = debugType(left) + const rightType = debugType(right) + if (leftType !== rightType) { + throw new EvaluationError(`no such overload: ${leftType} + ${rightType}`, ast) + } - const right = s.eval(ast[2]) - if (typeof right === 'boolean') return right - throw new EvaluationError('Right operand of && is not a boolean', firstNode(ast[2], ast)) - }, - '+'(ast, s) { - const left = s.eval(ast[1]) - const right = s.eval(ast[2]) - const leftType = debugType(left) - const rightType = debugType(right) - if (leftType !== rightType) { - throw new EvaluationError(`no such overload: ${leftType} + ${rightType}`, ast) - } + switch (leftType) { + case 'Integer': + case 'Double': + case 'String': + return left + right + case 'List': + return [...left, ...right] + case 'Bytes': { + const result = new Uint8Array(left.length + right.length) + result.set(left, 0) + result.set(right, left.length) + return result + } + } - switch (leftType) { - case 'Integer': - case 'Double': - case 'String': - return left + right - case 'List': - return [...left, ...right] - case 'Bytes': { - const result = new Uint8Array(left.length + right.length) - result.set(left, 0) - result.set(right, left.length) - return result + throw new EvaluationError(`no such overload: ${leftType} + ${rightType}`, ast) + }, + '-'(ast, s) { + const left = s.eval(ast[1]) + const leftType = debugType(left) + if (ast.length === 2) { + if (leftType === 'Double' || leftType === 'Integer') return -left + throw new EvaluationError(`no such overload: -${leftType}`, ast) } - } - throw new EvaluationError(`no such overload: ${leftType} + ${rightType}`, ast) - }, - '-'(ast, s) { - const left = s.eval(ast[1]) - const leftType = debugType(left) - if (ast.length === 2) { - if (leftType === 'Double' || leftType === 'Integer') return -left - throw new EvaluationError(`no such overload: -${leftType}`, ast) - } + const right = s.eval(ast[2]) + const rightType = debugType(right) + if (leftType !== rightType || !(leftType === 'Integer' || leftType === 'Double')) { + throw new EvaluationError(`no such overload: ${leftType} - ${rightType}`, ast) + } + return left - right + }, + '=='(ast, s) { + s.__supportsEqualityOperator(ast) + return isEqual(s.left, s.right) + }, + '!='(ast, s) { + s.__supportsEqualityOperator(ast) + return !isEqual(s.left, s.right) + }, + '<'(ast, s) { + s.__supportsRelationalOperator(ast) + return s.left < s.right + }, + '<='(ast, s) { + s.__supportsRelationalOperator(ast) + return s.left <= s.right + }, + '>'(ast, s) { + s.__supportsRelationalOperator(ast) + return s.left > s.right + }, + '>='(ast, s) { + s.__supportsRelationalOperator(ast) + return s.left >= s.right + }, + '*'(ast, s) { + s.__verifyNumberOverload(ast) + return s.left * s.right + }, + '/'(ast, s) { + s.__verifyNumberOverload(ast) + if (s.right === 0 || s.right === 0n) throw new EvaluationError('division by zero') + return s.left / s.right + }, + '%'(ast, s) { + s.__verifyIntOverload(ast) + if (s.right === 0 || s.right === 0n) throw new EvaluationError('modulo by zero') + return s.left % s.right + }, + '!'(ast, s) { + const right = s.eval(ast[1]) + if (typeof right === 'boolean') return !right + throw new EvaluationError('NOT operator can only be applied to boolean values') + }, + in(ast, s) { + const left = s.eval(ast[1]) + const right = s.eval(ast[2]) - const right = s.eval(ast[2]) - const rightType = debugType(right) - if (leftType !== rightType || !(leftType === 'Integer' || leftType === 'Double')) { - throw new EvaluationError(`no such overload: ${leftType} - ${rightType}`, ast) - } - return left - right - }, - '=='(ast, s) { - const v = this.__supportsEqualityOperator(ast, s) - return isEqual(v[0], v[1]) - }, - '!='(ast, s) { - const v = this.__supportsEqualityOperator(ast, s) - return !isEqual(v[0], v[1]) - }, - '<'(ast, s) { - const v = this.__supportsRelationalOperator(ast, s) - return v[0] < v[1] - }, - '<='(ast, s) { - const v = this.__supportsRelationalOperator(ast, s) - return v[0] <= v[1] - }, - '>'(ast, s) { - const v = this.__supportsRelationalOperator(ast, s) - return v[0] > v[1] - }, - '>='(ast, s) { - const v = this.__supportsRelationalOperator(ast, s) - return v[0] >= v[1] - }, - '*'(ast, s) { - const v = this.__verifyNumberOverload(ast, s) - return v[0] * v[1] - }, - '/'(ast, s) { - const v = this.__verifyNumberOverload(ast, s) - if (v[1] === 0 || v[1] === 0n) throw new EvaluationError('division by zero') - return v[0] / v[1] - }, - '%'(ast, s) { - const v = this.__verifyNumberOverload(ast, s) - if (typeof v[1] === 'bigint' && typeof v[0] !== 'bigint') v[1] = Number(v[1]) - if (v[1] === 0 || v[1] === 0n) throw new EvaluationError('modulo by zero') - return v[0] % v[1] - }, - '!'(ast, s) { - const right = s.eval(ast[1]) - if (typeof right === 'boolean') return !right - throw new EvaluationError('NOT operator can only be applied to boolean values') - }, - in(ast, s) { - const left = s.eval(ast[1]) - const right = s.eval(ast[2]) - - if (typeof right === 'string') return typeof left === 'string' && right.includes(left) - if (right instanceof Set) return right.has(left) - if (Array.isArray(right)) { - if (typeof left === 'bigint') return right.includes(left) || right.includes(Number(left)) - return right.includes(left) - } - return objectGet(right, left) !== undefined - }, - '[]'(ast, s) { - const left = s.eval(ast[1]) - const right = s.eval(ast[2]) - const value = objectGet(left, right) - if (value !== undefined) return value - - if (Array.isArray(left)) { - if (!(typeof right === 'number' || typeof right === 'bigint')) { - throw new EvaluationError(`No such key: ${right} (${debugType(right)})`, ast) + if (typeof right === 'string') return typeof left === 'string' && right.includes(left) + if (right instanceof Set) return right.has(left) + if (Array.isArray(right)) { + if (typeof left === 'bigint') return right.includes(left) || right.includes(Number(left)) + return right.includes(left) } - if (right < 0) { - throw new EvaluationError(`No such key: index out of bounds, index ${right} < 0`, ast) + return objectGet(right, left) !== undefined + }, + '[]'(ast, s) { + const left = s.eval(ast[1]) + const right = s.eval(ast[2]) + const value = objectGet(left, right) + if (value !== undefined) return value + + if (Array.isArray(left)) { + if (!(typeof right === 'number' || typeof right === 'bigint')) { + throw new EvaluationError(`No such key: ${right} (${debugType(right)})`, ast) + } + if (right < 0) { + throw new EvaluationError(`No such key: index out of bounds, index ${right} < 0`, ast) + } + if (right >= left.length) { + throw new EvaluationError( + `No such key: index out of bounds, index ${right} >= size ${left.length}`, + ast + ) + } } - if (right >= left.length) { + throw new EvaluationError(`No such key: ${right}`, ast) + }, + rcall(ast, s) { + const functionName = ast[2] + const receiver = s.eval(ast[1]) + const type = debugType(receiver) + const fn = s.fns.get(functionName, type) + if (!fn) { throw new EvaluationError( - `No such key: index out of bounds, index ${right} >= size ${left.length}`, + `Function not found: '${functionName}' for value of type '${type}'`, ast ) } - } - throw new EvaluationError(`No such key: ${right}`, ast) - }, - rcall(ast, s) { - const functionName = ast[2] - const receiver = s.eval(ast[1]) - const type = debugType(receiver) - const fn = s.fns.get(functionName, type) - if (!fn) { - throw new EvaluationError( - `Function not found: '${functionName}' for value of type '${type}'`, - ast - ) - } - if (fn.macro) return fn.handler.call(s, receiver, ...ast[3]) - return fn.handler(receiver, ...ast[3].map((arg) => s.eval(arg))) - }, - call(ast, s) { - const functionName = ast[1] - const fn = s.fns.get(functionName) - if (!fn?.standalone) { - throw new EvaluationError(`Function not found: '${functionName}'`, ast) - } + if (fn.macro) return fn.handler.call(s, receiver, ...ast[3]) + return fn.handler(receiver, ...ast[3].map((arg) => s.eval(arg))) + }, + call(ast, s) { + const functionName = ast[1] + const fn = s.fns.get(functionName) + if (!fn?.standalone) { + throw new EvaluationError(`Function not found: '${functionName}'`, ast) + } - if (fn.macro) return fn.handler.call(s, ...ast[2]) - return fn.handler(...ast[2].map((arg) => s.eval(arg))) - }, - array(ast, s) { - const elements = ast[1] - const result = new Array(elements.length) - for (let i = 0; i < elements.length; i++) result[i] = s.eval(elements[i]) - return result - }, - object(ast, s) { - const result = {} - for (let i = 0; i < ast[1].length; i++) { - const e = ast[1][i] - result[s.eval(e[0])] = s.eval(e[1]) - } - return result - }, - '?:'(ast, s) { - const condition = s.eval(ast[1]) - if (typeof condition !== 'boolean') { - throw new EvaluationError('Ternary condition must be a boolean') + if (fn.macro) return fn.handler.call(s, ...ast[2]) + return fn.handler(...ast[2].map((arg) => s.eval(arg))) + }, + array(ast, s) { + const elements = ast[1] + const result = new Array(elements.length) + for (let i = 0; i < elements.length; i++) result[i] = s.eval(elements[i]) + return result + }, + object(ast, s) { + const result = {} + for (let i = 0; i < ast[1].length; i++) { + const e = ast[1][i] + result[s.eval(e[0])] = s.eval(e[1]) + } + return result + }, + '?:'(ast, s) { + const condition = s.eval(ast[1]) + if (typeof condition !== 'boolean') { + throw new EvaluationError('Ternary condition must be a boolean') + } + return condition ? s.eval(ast[2]) : s.eval(ast[3]) } - return condition ? s.eval(ast[2]) : s.eval(ast[3]) - }, - __supportsEqualityOperator(ast, s) { - const left = s.eval(ast[1]) - const right = s.eval(ast[2]) + }) +) + +// handler aliases +handlers.set('.', handlers.get('[]')) + +class Evaluator { + handlers = handlers + left = undefined + right = undefined + predicateEvaluator(receiver, functionName, args) { + return new PredicateEvaluator(this, receiver, functionName, args) + } + + __supportsEqualityOperator(ast) { + const left = (this.left = this.eval(ast[1])) + const right = (this.right = this.eval(ast[2])) const leftType = debugType(left) const rightType = debugType(right) - if (leftType === rightType) return [left, right] + if (leftType === rightType) return // Allow numeric type cross-compatibility for equality/inequality operators // when at least one operand is dynamic (contains variable references) if ( (leftType === 'Double' || leftType === 'Integer') && (rightType === 'Double' || rightType === 'Integer') && - (s.isDynamic(ast[1]) || s.isDynamic(ast[2])) + (this.isDynamic(ast[1]) || this.isDynamic(ast[2])) ) { - return [left, right] + return } throw new EvaluationError(`no such overload: ${leftType} ${ast[0]} ${rightType}`, ast) - }, - __supportsRelationalOperator(ast, s) { - const left = s.eval(ast[1]) - const right = s.eval(ast[2]) + } + __supportsRelationalOperator(ast) { + const left = (this.left = this.eval(ast[1])) + const right = (this.right = this.eval(ast[2])) const leftType = debugType(left) const rightType = debugType(right) @@ -1009,44 +1023,40 @@ const handlers = { case 'Integer': case 'Double': // Always allow Integer/Double cross-compatibility for relational operators - if (rightType === 'Integer' || rightType === 'Double') return [left, right] + if (rightType === 'Integer' || rightType === 'Double') return break case 'String': - if (rightType === 'String') return [left, right] + if (rightType === 'String') return break case 'Timestamp': - if (rightType === 'Timestamp') return [left, right] + if (rightType === 'Timestamp') return break } throw new EvaluationError(`no such overload: ${leftType} ${ast[0]} ${rightType}`, ast) - }, - __verifyNumberOverload(ast, s) { - const left = s.eval(ast[1]) - const right = s.eval(ast[2]) + } + __verifyNumberOverload(ast) { + const left = (this.left = this.eval(ast[1])) + const right = (this.right = this.eval(ast[2])) const leftType = debugType(left) const rightType = debugType(right) - if (leftType === rightType && (leftType === 'Integer' || leftType === 'Double')) - return [left, right] - + if (leftType === rightType && (leftType === 'Integer' || leftType === 'Double')) return throw new EvaluationError(`no such overload: ${leftType} ${ast[0]} ${rightType}`, ast) } -} - -// handler aliases -handlers['.'] = handlers['[]'] - -class Evaluator { - handlers = handlers - predicateEvaluator(receiver, functionName, args) { - return new PredicateEvaluator(this, receiver, functionName, args) + __verifyIntOverload(ast) { + const left = (this.left = this.eval(ast[1])) + const right = (this.right = this.eval(ast[2])) + const leftType = debugType(left) + const rightType = debugType(right) + if (leftType === rightType && leftType === 'Integer') return + throw new EvaluationError(`no such overload: ${leftType} ${ast[0]} ${rightType}`, ast) } eval(ast) { if (!Array.isArray(ast)) return ast - const handler = this.handlers[ast[0]] - if (handler) return handler.call(this.handlers, ast, this) + const handler = this.handlers.get(ast[0]) + if (handler) return handler(ast, this) throw new EvaluationError(`Unknown operation: ${ast[0]}`, ast) } diff --git a/test/multiplication.test.js b/test/multiplication.test.js index dbde4a0..43b6fd0 100644 --- a/test/multiplication.test.js +++ b/test/multiplication.test.js @@ -26,11 +26,30 @@ describe('multiplication and division', () => { t.assert.strictEqual(evaluate('(2 + 3) * 4'), 20n) }) - test.skip('should handle float multiplication', (t) => { - t.assert.strictEqual(evaluate('2.5 * 2'), 5n) + test('should handle float multiplication', (t) => { + t.assert.strictEqual(evaluate('2.5 * 2.0'), 5) }) - test.skip('should handle float division', (t) => { - t.assert.strictEqual(evaluate('5.5 / 2'), 2.75) + test('should handle float division', (t) => { + t.assert.strictEqual(evaluate('5.5 / 2.0'), 2.75) + }) + + test('rejects int and double combinations', (t) => { + t.assert.throws( + () => evaluate('2.5 * 2'), + /EvaluationError: no such overload: Double \* Integer/ + ) + }) + + test('rejects double modulo', (t) => { + t.assert.throws( + () => evaluate('5.5 % 2.0'), + /EvaluationError: no such overload: Double % Double/ + ) + + t.assert.throws( + () => evaluate('2 % 2.0'), + /EvaluationError: no such overload: Integer % Double/ + ) }) })