diff --git a/docs/syntax.md b/docs/syntax.md index 7190942..3ee0266 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -17,6 +17,7 @@ The parser accepts a pretty basic grammar. It's similar to normal JavaScript exp | and | Left | Logical AND | | or | Left | Logical OR | | x ? y : z | Right | Ternary conditional (if x then y else z) | +| => | Right | Arrow function (e.g., x => x * 2) | | = | Right | Variable assignment | | ; | Left | Expression separator | @@ -345,6 +346,59 @@ You can also define the functions inline: filter(isEven(x) = x % 2 == 0, [1, 2, 3, 4, 5]) ``` +### Arrow Functions + +Arrow functions provide a concise syntax for inline functions, similar to JavaScript arrow functions. They are particularly useful when passing functions to higher-order functions like `map`, `filter`, and `fold`. + +**Single parameter (no parentheses required):** + +```js +map(x => x * 2, [1, 2, 3]) // [2, 4, 6] +filter(x => x > 2, [1, 2, 3, 4]) // [3, 4] +map(x => x.name, users) // Extract property from objects +``` + +**Multiple parameters (parentheses required):** + +```js +fold((acc, x) => acc + x, 0, [1, 2, 3, 4, 5]) // 15 (sum) +fold((acc, x) => acc * x, 1, [1, 2, 3, 4, 5]) // 120 (product) +map((val, idx) => val + idx, [10, 20, 30]) // [10, 21, 32] +filter((x, i) => i >= 1, [10, 20, 30]) // [20, 30] +``` + +**Zero parameters:** + +```js +(() => 42)() // 42 +``` + +**Assignment to variable:** + +Arrow functions can be assigned to variables for reuse: + +```js +fn = x => x * 2; map(fn, [1, 2, 3]) // [2, 4, 6] +double = x => x * 2; triple = x => x * 3; map(double, map(triple, [1, 2])) // [6, 12] +``` + +**Nested arrow functions:** + +```js +map(row => map(x => x * 2, row), [[1, 2], [3, 4]]) // [[2, 4], [6, 8]] +``` + +**With member access and complex expressions:** + +```js +filter(x => x.age > 25, users) // Filter objects by property +map(x => x.value * 2 + 1, items) // Complex transformations +filter(x => x > 0 and x < 10, numbers) // Using logical operators +map(x => x > 5 ? "high" : "low", [3, 7, 2, 9]) // Using ternary operator +``` + +> **Note:** Arrow functions share the same `fndef` operator flag as traditional function definitions. If function definitions are disabled via parser options, arrow functions will also be disabled. + ## Custom JavaScript Functions If you need additional functions that aren't supported out of the box, you can easily add them in your own code. Instances of the `Parser` class have a property called `functions` that's simply an object with all the functions that are in scope. You can add, replace, or delete any of the properties to customize what's available in the expressions. For example: diff --git a/src/core/evaluate.ts b/src/core/evaluate.ts index 6cd3172..23e5179 100644 --- a/src/core/evaluate.ts +++ b/src/core/evaluate.ts @@ -5,14 +5,14 @@ * It uses a stack-based interpreter to evaluate instruction sequences produced by the parser. */ -import { ISCALAR, IOP1, IOP2, IOP3, IVAR, IVARNAME, IFUNCALL, IFUNDEF, IEXPR, IEXPREVAL, IMEMBER, IENDSTATEMENT, IARRAY, IUNDEFINED, ICASEMATCH, IWHENMATCH, ICASEELSE, ICASECOND, IWHENCOND, IOBJECT, IPROPERTY, IOBJECTEND } from '../parsing/instruction.js'; +import { ISCALAR, IOP1, IOP2, IOP3, IVAR, IVARNAME, IFUNCALL, IFUNDEF, IARROW, IEXPR, IEXPREVAL, IMEMBER, IENDSTATEMENT, IARRAY, IUNDEFINED, ICASEMATCH, IWHENMATCH, ICASEELSE, ICASECOND, IWHENCOND, IOBJECT, IPROPERTY, IOBJECTEND } from '../parsing/instruction.js'; import type { Instruction } from '../parsing/instruction.js'; import type { Expression } from './expression.js'; import type { Value, Values, VariableResolveResult } from '../types/values.js'; import { VariableError } from '../types/errors.js'; import { ExpressionValidator } from '../validation/expression-validator.js'; -// cSpell:words ISCALAR IVAR IVARNAME IFUNCALL IEXPR IEXPREVAL IMEMBER IENDSTATEMENT IARRAY +// cSpell:words ISCALAR IVAR IVARNAME IFUNCALL IEXPR IEXPREVAL IMEMBER IENDSTATEMENT IARRAY IARROW // cSpell:words IFUNDEF IUNDEFINED ICASEMATCH ICASECOND IWHENCOND IWHENMATCH ICASEELSE IPROPERTY // cSpell:words IOBJECT IOBJECTEND // cSpell:words nstack @@ -261,6 +261,34 @@ function evaluateExpressionToken(expr: Expression, values: EvaluationValues, tok values[functionName] = userDefinedFunction; return userDefinedFunction; })()); + } else if (type === IARROW) { + // Create anonymous arrow function closure + // Stack contains: param1, param2, ..., paramN, expression + // token.value is the parameter count + nstack.push((function () { + const expressionToEvaluate = nstack.pop(); + const functionParams: string[] = []; + let parameterCount = token.value as number; + while (parameterCount-- > 0) { + functionParams.unshift(nstack.pop()); + } + const arrowFunction = function (...functionArguments: any[]) { + const localScope = Object.assign({}, values); + for (let i = 0, len = functionParams.length; i < len; i++) { + localScope[functionParams[i]] = functionArguments[i]; + } + return evaluate(expressionToEvaluate, expr, localScope); + }; + // Set function name for debugging (anonymous arrow function) + Object.defineProperty(arrowFunction, 'name', { + value: '(arrow)', + writable: false + }); + // Security: Register the arrow function as allowed using a unique counter-based key + const uniqueKey = `__inline_fn_${inlineFunctionCounter++}__`; + expr.functions[uniqueKey] = arrowFunction; + return arrowFunction; + })()); } else if (type === IEXPR) { nstack.push(createExpressionEvaluator(token, expr)); } else if (type === IEXPREVAL) { diff --git a/src/core/expression-to-string.ts b/src/core/expression-to-string.ts index c694815..a706ec1 100644 --- a/src/core/expression-to-string.ts +++ b/src/core/expression-to-string.ts @@ -1,9 +1,9 @@ -// cSpell:words ISCALAR IVAR IVARNAME IFUNCALL IEXPR IEXPREVAL IMEMBER IENDSTATEMENT IARRAY +// cSpell:words ISCALAR IVAR IVARNAME IFUNCALL IEXPR IEXPREVAL IMEMBER IENDSTATEMENT IARRAY IARROW // cSpell:words IFUNDEF IUNDEFINED ICASEMATCH ICASECOND IWHENCOND IWHENMATCH ICASEELSE IPROPERTY // cSpell:words IOBJECT IOBJECTEND // cSpell:words nstack -import { ISCALAR, IOP1, IOP2, IOP3, IVAR, IVARNAME, IFUNCALL, IFUNDEF, IEXPR, IMEMBER, IENDSTATEMENT, IARRAY, IUNDEFINED, ICASEMATCH, ICASECOND, IWHENCOND, IWHENMATCH, ICASEELSE, IOBJECT, IOBJECTEND, IPROPERTY } from '../parsing/instruction.js'; +import { ISCALAR, IOP1, IOP2, IOP3, IVAR, IVARNAME, IFUNCALL, IFUNDEF, IARROW, IEXPR, IMEMBER, IENDSTATEMENT, IARRAY, IUNDEFINED, ICASEMATCH, ICASECOND, IWHENCOND, IWHENMATCH, ICASEELSE, IOBJECT, IOBJECTEND, IPROPERTY } from '../parsing/instruction.js'; import type { Instruction } from '../parsing/instruction.js'; export default function expressionToString(tokens: Instruction[], toJS?: boolean): string { @@ -101,6 +101,22 @@ export default function expressionToString(tokens: Instruction[], toJS?: boolean } else { nstack.push('(' + n1 + '(' + args.join(', ') + ') = ' + n2 + ')'); } + } else if (type === IARROW) { + n2 = nstack.pop()!; + argCount = item.value as number; + args = []; + while (argCount-- > 0) { + args.unshift(nstack.pop()!); + } + if (toJS) { + nstack.push('((' + args.join(', ') + ') => ' + n2 + ')'); + } else { + if (args.length === 1) { + nstack.push('(' + args[0] + ' => ' + n2 + ')'); + } else { + nstack.push('((' + args.join(', ') + ') => ' + n2 + ')'); + } + } } else if (type === IMEMBER) { n1 = nstack.pop()!; nstack.push(n1 + '.' + item.value); diff --git a/src/parsing/instruction.ts b/src/parsing/instruction.ts index 764ca49..e2ee520 100644 --- a/src/parsing/instruction.ts +++ b/src/parsing/instruction.ts @@ -33,6 +33,8 @@ export const IVARNAME = 'IVARNAME' as const; export const IFUNCALL = 'IFUNCALL' as const; /** Function definition instruction */ export const IFUNDEF = 'IFUNDEF' as const; +/** Arrow function instruction (anonymous inline function) */ +export const IARROW = 'IARROW' as const; /** Expression instruction (for lazy evaluation) */ export const IEXPR = 'IEXPR' as const; /** Expression evaluator instruction (compiled expression) */ @@ -74,6 +76,7 @@ export type InstructionType = | typeof IVARNAME | typeof IFUNCALL | typeof IFUNDEF + | typeof IARROW | typeof IEXPR | typeof IEXPREVAL | typeof IMEMBER @@ -132,6 +135,11 @@ export interface FunctionDefInstruction { value: number; // parameter count } +export interface ArrowFunctionInstruction { + type: typeof IARROW; + value: number; // parameter count +} + export interface ExpressionInstruction { type: typeof IEXPR; value: Instruction[]; @@ -212,6 +220,7 @@ export type TypedInstruction = | VarNameInstruction | FunctionCallInstruction | FunctionDefInstruction + | ArrowFunctionInstruction | ExpressionInstruction | ExpressionEvalInstruction | MemberInstruction @@ -268,6 +277,8 @@ export class Instruction { return 'CALL ' + this.value; case IFUNDEF: return 'DEF ' + this.value; + case IARROW: + return 'ARROW ' + this.value; case IARRAY: return 'ARRAY ' + this.value; case IMEMBER: diff --git a/src/parsing/parser-state.ts b/src/parsing/parser-state.ts index 9a175ec..5d7a1ea 100644 --- a/src/parsing/parser-state.ts +++ b/src/parsing/parser-state.ts @@ -1,5 +1,5 @@ // cSpell:words TEOF TNUMBER TSTRING TCONST TPAREN TBRACKET TCOMMA TNAME TSEMICOLON TUNDEFINED TKEYWORD TBRACE -// cSpell:words ISCALAR IVAR IFUNCALL IEXPREVAL IMEMBER IARRAY +// cSpell:words ISCALAR IVAR IFUNCALL IEXPREVAL IMEMBER IARRAY IARROW // cSpell:words IUNDEFINED ICASEMATCH ICASECOND IWHENCOND IWHENMATCH ICASEELSE IPROPERTY // cSpell:words IOBJECT IOBJECTEND @@ -7,7 +7,7 @@ import { TOP, TNUMBER, TSTRING, TPAREN, TBRACKET, TCOMMA, TNAME, TSEMICOLON, TEOF, TKEYWORD, TBRACE, Token, TokenType, TCONST } from './token.js'; -import { Instruction, ISCALAR, IVAR, IFUNCALL, IMEMBER, IARRAY, IUNDEFINED, binaryInstruction, unaryInstruction, IWHENMATCH, ICASEMATCH, ICASEELSE, ICASECOND, IWHENCOND, IPROPERTY, IOBJECT, IOBJECTEND, InstructionType } from './instruction.js'; +import { Instruction, ISCALAR, IVAR, IFUNCALL, IMEMBER, IARRAY, IUNDEFINED, binaryInstruction, unaryInstruction, IWHENMATCH, ICASEMATCH, ICASEELSE, ICASECOND, IWHENCOND, IPROPERTY, IOBJECT, IOBJECTEND, InstructionType, IVARNAME, IEXPR, IARROW } from './instruction.js'; import contains from '../core/contains.js'; import { TokenStream } from './token-stream.js'; import { ParseError, AccessError } from '../types/errors.js'; @@ -119,11 +119,21 @@ export class ParserState { // undefined is a reserved work that evaluates to JavaScript undefined. instr.push(new Instruction(IUNDEFINED)); } else { - instr.push(new Instruction(IVAR, this.current!.value)); + // Check if this is a single-parameter arrow function: x => expr + if (this.nextToken!.type === TOP && this.nextToken!.value === '=>') { + this.parseArrowFunctionFromParameter(instr, this.current!.value as string); + } else { + instr.push(new Instruction(IVAR, this.current!.value)); + } } } else if (this.accept(TNUMBER) || this.accept(TSTRING) || this.accept(TCONST)) { instr.push(new Instruction(ISCALAR, this.current!.value)); } else if (this.accept(TPAREN, '(')) { + // Check if this is a multi-parameter arrow function: (x, y) => expr + if (this.tryParseArrowFunction(instr)) { + // Arrow function was parsed successfully + return; + } this.parseExpression(instr); this.expect(TPAREN, ')'); } else if (this.accept(TBRACE, '{')) { @@ -150,6 +160,136 @@ export class ParserState { } } + /** + * Parses an arrow function when we already have a single parameter name. + * Called when we detect: `paramName =>` pattern + */ + private parseArrowFunctionFromParameter(instr: Instruction[], paramName: string): void { + // Validate that arrow functions are enabled + if (!this.parser.isOperatorEnabled('=>')) { + const coords = this.tokens.getCoordinates(); + throw new ParseError( + 'Arrow function syntax is not permitted', + { + position: { line: coords.line, column: coords.column }, + expression: this.tokens.expression + } + ); + } + + // Consume the '=>' operator + this.expect(TOP, '=>'); + + // Parse the function body expression. We use parseConditionalExpression instead of + // parseExpression because arrow function bodies should be single expressions and + // should NOT consume semicolons. The semicolon terminates the arrow function + // definition, allowing patterns like: `fn = x => x * 2; map(fn, arr)` + // If we used parseExpression, the semicolon and subsequent statements would be + // incorrectly included in the arrow function body. + const bodyInstr: Instruction[] = []; + this.parseConditionalExpression(bodyInstr); + + // Build the arrow function: push param name, body expression, and IARROW instruction + instr.push(new Instruction(IVARNAME, paramName)); + instr.push(new Instruction(IEXPR, bodyInstr)); + instr.push(new Instruction(IARROW, 1)); + } + + /** + * Attempts to parse an arrow function after seeing '('. + * Returns true if it successfully parsed an arrow function, false otherwise. + * Uses lookahead to detect arrow function syntax without consuming tokens on failure. + */ + private tryParseArrowFunction(instr: Instruction[]): boolean { + // Save current position for backtracking + this.save(); + + // Try to parse parameter list + const params: string[] = []; + + // Check for empty parameter list: () => expr + if (this.accept(TPAREN, ')')) { + // Check if followed by => + if (this.accept(TOP, '=>')) { + // Validate that arrow functions are enabled + if (!this.parser.isOperatorEnabled('=>')) { + this.restore(); + return false; + } + // Parse the function body + const bodyInstr: Instruction[] = []; + this.parseExpression(bodyInstr); + + // Build the arrow function with no parameters + instr.push(new Instruction(IEXPR, bodyInstr)); + instr.push(new Instruction(IARROW, 0)); + return true; + } + // Not an arrow function, restore and return false + this.restore(); + return false; + } + + // Try to parse comma-separated parameter names + if (!this.accept(TNAME)) { + // Not a parameter list, restore and return false + this.restore(); + return false; + } + params.push(this.current!.value as string); + + // Parse additional parameters separated by commas + while (this.accept(TCOMMA)) { + if (!this.accept(TNAME)) { + // Invalid parameter, restore and return false + this.restore(); + return false; + } + params.push(this.current!.value as string); + } + + // Expect closing parenthesis + if (!this.accept(TPAREN, ')')) { + // Not a parameter list, restore and return false + this.restore(); + return false; + } + + // Check for arrow operator + if (!this.accept(TOP, '=>')) { + // Not an arrow function, restore and return false + this.restore(); + return false; + } + + // Validate that arrow functions are enabled + if (!this.parser.isOperatorEnabled('=>')) { + const coords = this.tokens.getCoordinates(); + throw new ParseError( + 'Arrow function syntax is not permitted', + { + position: { line: coords.line, column: coords.column }, + expression: this.tokens.expression + } + ); + } + + // Parse the function body expression. We use parseConditionalExpression instead of + // parseExpression because arrow function bodies should be single expressions and + // should NOT consume semicolons. This allows patterns like: `fn = (a, b) => a + b; map(fn, arr)` + const bodyInstr: Instruction[] = []; + this.parseConditionalExpression(bodyInstr); + + // Build the arrow function: push param names, body expression, and IARROW instruction + for (const param of params) { + instr.push(new Instruction(IVARNAME, param)); + } + instr.push(new Instruction(IEXPR, bodyInstr)); + instr.push(new Instruction(IARROW, params.length)); + + return true; + } + parseExpression(instr: Instruction[]): void { const exprInstr: Instruction[] = []; diff --git a/src/parsing/parser.ts b/src/parsing/parser.ts index 6007043..05be0d8 100644 --- a/src/parsing/parser.ts +++ b/src/parsing/parser.ts @@ -327,6 +327,7 @@ export class Parser { '=': 'assignment', '[': 'array', '()=': 'fndef', + '=>': 'fndef', '??': 'coalesce', 'as': 'conversion' } as const; diff --git a/src/parsing/token-stream.ts b/src/parsing/token-stream.ts index 727c0e6..9586f53 100644 --- a/src/parsing/token-stream.ts +++ b/src/parsing/token-stream.ts @@ -531,6 +531,26 @@ export class TokenStream { return true; } + /** + * Try to match the arrow operator (=>) + * Returns true if arrow operator was matched, false otherwise + */ + private tryMatchArrowOperator(): boolean { + // Check bounds before accessing the next character + if (this.pos + 1 >= this.expression.length) { + return false; + } + if (this.expression.charAt(this.pos + 1) === '>') { + if (!this.isOperatorEnabled('=>')) { + return false; + } + this.current = this.newToken(TOP, '=>'); + this.pos++; + return true; + } + return false; + } + /** * Try to match pipe operators (| or ||) */ @@ -599,7 +619,10 @@ export class TokenStream { this.tryMatchComparisonOperator(c, '<='); } else if (c === '=') { - this.tryMatchComparisonOperator(c, '=='); + // Try arrow operator first (=>), then comparison/assignment (== or =) + if (!this.tryMatchArrowOperator()) { + this.tryMatchComparisonOperator(c, '=='); + } } else if (c === '!') { this.tryMatchComparisonOperator(c, '!='); diff --git a/test/functions/functions-arrow.ts b/test/functions/functions-arrow.ts new file mode 100644 index 0000000..45e9258 --- /dev/null +++ b/test/functions/functions-arrow.ts @@ -0,0 +1,250 @@ +/* global describe, it */ + +import assert from 'assert'; +import { Parser } from '../../index'; + +describe('Arrow Function Support', function () { + describe('Single parameter arrow functions (x => expr)', function () { + it('should work with map function', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('map(x => x * 2, [1, 2, 3])'), [2, 4, 6]); + }); + + it('should work with filter function', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('filter(x => x > 2, [1, 2, 3, 4])'), [3, 4]); + }); + + it('should work with complex expressions', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('map(x => x * x + 1, [1, 2, 3])'), [2, 5, 10]); + }); + + it('should work with member access', function () { + const parser = new Parser(); + assert.deepStrictEqual( + parser.evaluate('map(x => x.value, items)', { items: [{ value: 1 }, { value: 2 }, { value: 3 }] }), + [1, 2, 3] + ); + }); + + it('should work with nested member access', function () { + const parser = new Parser(); + assert.deepStrictEqual( + parser.evaluate('map(x => x.user.name, items)', { + items: [{ user: { name: 'Alice' } }, { user: { name: 'Bob' } }] + }), + ['Alice', 'Bob'] + ); + }); + + it('should work with comparison in body', function () { + const parser = new Parser(); + assert.deepStrictEqual( + parser.evaluate('filter(x => x.age > 25, users)', { + users: [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 20 }, { name: 'Charlie', age: 35 }] + }), + [{ name: 'Alice', age: 30 }, { name: 'Charlie', age: 35 }] + ); + }); + + it('should work with boolean expressions', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('filter(x => x and true, [true, false, true])'), [true, true]); + }); + + it('should work with ternary in body', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('map(x => x > 2 ? 1 : 0, [1, 2, 3, 4])'), [0, 0, 1, 1]); + }); + + it('should work with undefined values', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('map(x => x * 2, undefined)'), undefined); + }); + }); + + describe('Multi-parameter arrow functions ((x, y) => expr)', function () { + it('should work with fold function', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('fold((acc, x) => acc + x, 0, [1, 2, 3, 4, 5])'), 15); + }); + + it('should work with fold for multiplication', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('fold((acc, x) => acc * x, 1, [1, 2, 3, 4, 5])'), 120); + }); + + it('should work with index parameter in map', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('map((x, i) => x + i, [10, 20, 30])'), [10, 21, 32]); + }); + + it('should work with index parameter in filter', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('filter((x, i) => i > 1, [10, 20, 30, 40])'), [30, 40]); + }); + + it('should work with three parameters in fold', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('fold((acc, x, i) => acc + x * i, 0, [1, 2, 3, 4])'), 20); + }); + + it('should handle multi-parameter with member access', function () { + const parser = new Parser(); + assert.deepStrictEqual( + parser.evaluate('map((item, idx) => item.value + idx, items)', { + items: [{ value: 10 }, { value: 20 }, { value: 30 }] + }), + [10, 21, 32] + ); + }); + }); + + describe('Empty parameter arrow functions (() => expr)', function () { + it('should create a zero-argument function', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('(() => 42)()'), 42); + }); + + it('should work with complex expressions', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('(() => 2 + 3 * 4)()'), 14); + }); + }); + + describe('Nested arrow functions', function () { + it('should work with nested map calls', function () { + const parser = new Parser(); + assert.deepStrictEqual( + parser.evaluate('map(row => map(x => x * 2, row), matrix)', { + matrix: [[1, 2], [3, 4]] + }), + [[2, 4], [6, 8]] + ); + }); + + it('should work with deeply nested arrays', function () { + const parser = new Parser(); + assert.deepStrictEqual( + parser.evaluate('map(a => map(b => map(c => c + 1, b), a), data)', { + data: [[[1, 2], [3, 4]], [[5, 6], [7, 8]]] + }), + [[[2, 3], [4, 5]], [[6, 7], [8, 9]]] + ); + }); + }); + + describe('Arrow functions with operators', function () { + it('should work with arithmetic operators', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('map(x => x + 1 - 2 * 3 / 2, [10, 20, 30])'), [8, 18, 28]); + }); + + it('should work with concatenation operator', function () { + const parser = new Parser(); + assert.deepStrictEqual( + parser.evaluate('map(x => x | "!", ["hello", "world"])'), + ['hello!', 'world!'] + ); + }); + + it('should work with comparison operators', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('map(x => x == 2, [1, 2, 3])'), [false, true, false]); + assert.deepStrictEqual(parser.evaluate('map(x => x != 2, [1, 2, 3])'), [true, false, true]); + assert.deepStrictEqual(parser.evaluate('map(x => x >= 2, [1, 2, 3])'), [false, true, true]); + assert.deepStrictEqual(parser.evaluate('map(x => x <= 2, [1, 2, 3])'), [true, true, false]); + }); + }); + + describe('Arrow functions with built-in functions in body', function () { + it('should work with abs', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('map(x => abs(x), [-1, -2, 3])'), [1, 2, 3]); + }); + + it('should work with floor and ceil', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('map(x => floor(x), [1.5, 2.7, 3.1])'), [1, 2, 3]); + assert.deepStrictEqual(parser.evaluate('map(x => ceil(x), [1.5, 2.7, 3.1])'), [2, 3, 4]); + }); + + it('should work with length', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('map(x => length(x), ["a", "ab", "abc"])'), [1, 2, 3]); + }); + }); + + describe('Arrow functions vs traditional function definition', function () { + it('should produce same results as f(x) = expr syntax', function () { + const parser = new Parser(); + // Traditional syntax + const traditional = parser.evaluate('f(x) = x * 2; map(f, [1, 2, 3])'); + // Arrow syntax + const arrow = parser.evaluate('map(x => x * 2, [1, 2, 3])'); + assert.deepStrictEqual(arrow, traditional); + }); + + it('should produce same results with multi-parameter', function () { + const parser = new Parser(); + // Traditional syntax + const traditional = parser.evaluate('f(a, b) = a + b; fold(f, 0, [1, 2, 3, 4, 5])'); + // Arrow syntax + const arrow = parser.evaluate('fold((a, b) => a + b, 0, [1, 2, 3, 4, 5])'); + assert.deepStrictEqual(arrow, traditional); + }); + }); + + describe('Arrow function toString()', function () { + it('should serialize single parameter arrow function', function () { + const parser = new Parser(); + const expr = parser.parse('map(x => x * 2, arr)'); + const str = expr.toString(); + assert.ok(str.includes('=>'), 'Should contain arrow operator'); + }); + + it('should serialize multi-parameter arrow function', function () { + const parser = new Parser(); + const expr = parser.parse('fold((a, b) => a + b, 0, arr)'); + const str = expr.toString(); + assert.ok(str.includes('=>'), 'Should contain arrow operator'); + }); + }); + + describe('Edge cases', function () { + it('should work with single element array', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('map(x => x * 2, [5])'), [10]); + }); + + it('should work with empty array', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('map(x => x * 2, [])'), []); + }); + + it('should work with variables from outer scope', function () { + const parser = new Parser(); + assert.deepStrictEqual( + parser.evaluate('map(x => x * multiplier, [1, 2, 3])', { multiplier: 10 }), + [10, 20, 30] + ); + }); + + it('should handle arrow function in variable assignment', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('fn = x => x * 2; map(fn, [1, 2, 3])'), [2, 4, 6]); + }); + + it('should handle arrow function with parenthesized body', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('map(x => (x + 1) * 2, [1, 2, 3])'), [4, 6, 8]); + }); + + it('should handle whitespace variations', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('map( x => x * 2 , [1, 2, 3])'), [2, 4, 6]); + assert.deepStrictEqual(parser.evaluate('map(x=>x*2,[1,2,3])'), [2, 4, 6]); + }); + }); +}); diff --git a/test/language-service/language-service.ts b/test/language-service/language-service.ts index 27bf590..c4aa3ac 100644 --- a/test/language-service/language-service.ts +++ b/test/language-service/language-service.ts @@ -549,6 +549,30 @@ describe('Language Service', () => { expect(stringToken?.start).toBe(0); expect(stringToken?.end).toBe(text.length); }); + + it('should highlight arrow function operator', () => { + const text = 'map(x => x * 2, arr)'; + const doc = TextDocument.create('file://test', 'plaintext', 1, text); + const tokens = ls.getHighlighting(doc); + + const arrowToken = tokens.find(t => t.value === '=>'); + expect(arrowToken).toBeDefined(); + expect(arrowToken?.type).toBe('operator'); + }); + + it('should highlight multi-parameter arrow function', () => { + const text = 'fold((a, b) => a + b, 0, arr)'; + const doc = TextDocument.create('file://test', 'plaintext', 1, text); + const tokens = ls.getHighlighting(doc); + + const arrowToken = tokens.find(t => t.value === '=>'); + expect(arrowToken).toBeDefined(); + expect(arrowToken?.type).toBe('operator'); + + // Check the parameter names are recognized + const nameTokens = tokens.filter(t => t.type === 'name'); + expect(nameTokens.length).toBeGreaterThanOrEqual(3); // a, b, arr + }); }); describe('Edge cases', () => {